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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,
@@ -366,6 +366,7 @@ import {
366
366
  DeleteFunctionUrlConfigCommand,
367
367
  GetAliasCommand,
368
368
  LambdaClient,
369
+ ListAliasesCommand,
369
370
  UpdateAliasCommand,
370
371
  UpdateFunctionCodeCommand
371
372
  } from "@aws-sdk/client-lambda";
@@ -386,6 +387,22 @@ var getLambdaAlias = async (lambda, functionName, name) => {
386
387
  return;
387
388
  }
388
389
  };
390
+ var listLambdaAliases = async (lambda, functionName, functionVersion) => {
391
+ const aliases = [];
392
+ let marker;
393
+ do {
394
+ const result = await lambda.send(
395
+ new ListAliasesCommand({
396
+ FunctionName: functionName,
397
+ FunctionVersion: functionVersion,
398
+ Marker: marker
399
+ })
400
+ );
401
+ aliases.push(...result.Aliases ?? []);
402
+ marker = result.NextMarker;
403
+ } while (marker);
404
+ return aliases;
405
+ };
389
406
  var upsertLambdaAlias = async (lambda, props) => {
390
407
  const input = {
391
408
  FunctionName: props.functionName,
@@ -528,6 +545,8 @@ import {
528
545
  CreateAliasCommand as CreateAliasCommand2,
529
546
  CreateFunctionUrlConfigCommand,
530
547
  GetFunctionUrlConfigCommand,
548
+ RemovePermissionCommand,
549
+ UpdateFunctionUrlConfigCommand,
531
550
  LambdaClient as LambdaClient2,
532
551
  PutFunctionEventInvokeConfigCommand
533
552
  } from "@aws-sdk/client-lambda";
@@ -559,7 +578,8 @@ var createLambdaProvider = ({ credentials, region }) => {
559
578
  deploymentId: z2.string(),
560
579
  functionName: z2.string(),
561
580
  functionVersion: z2.string(),
562
- onFailureArn: z2.string()
581
+ onFailureArn: z2.string(),
582
+ sourceAccount: z2.string().optional()
563
583
  });
564
584
  const bundleDeploymentStateSchema = bundleDeploymentInputSchema.extend({
565
585
  deploymentAlias: z2.string(),
@@ -596,14 +616,15 @@ var createLambdaProvider = ({ credentials, region }) => {
596
616
  })
597
617
  );
598
618
  };
599
- const createPublicUrl = async (functionName, alias) => {
619
+ const createDeploymentUrl = async (functionName, alias, sourceAccount) => {
620
+ const authType = sourceAccount ? "AWS_IAM" : "NONE";
600
621
  let url;
601
622
  try {
602
623
  const result = await lambda.send(
603
624
  new CreateFunctionUrlConfigCommand({
604
625
  FunctionName: functionName,
605
626
  Qualifier: alias,
606
- AuthType: "NONE"
627
+ AuthType: authType
607
628
  })
608
629
  );
609
630
  url = result.FunctionUrl;
@@ -612,21 +633,39 @@ var createLambdaProvider = ({ credentials, region }) => {
612
633
  throw error;
613
634
  }
614
635
  const result = await lambda.send(
615
- new GetFunctionUrlConfigCommand({
636
+ new UpdateFunctionUrlConfigCommand({
616
637
  FunctionName: functionName,
617
- Qualifier: alias
638
+ Qualifier: alias,
639
+ AuthType: authType
618
640
  })
619
641
  );
620
642
  url = result.FunctionUrl;
621
643
  }
622
- const permissions = [
644
+ const permissions = sourceAccount ? [
645
+ {
646
+ StatementId: "cloudfront-url",
647
+ Principal: "cloudfront.amazonaws.com",
648
+ SourceAccount: sourceAccount,
649
+ Action: "lambda:InvokeFunctionUrl",
650
+ FunctionUrlAuthType: "AWS_IAM"
651
+ },
652
+ {
653
+ StatementId: "cloudfront-invoke",
654
+ Principal: "cloudfront.amazonaws.com",
655
+ SourceAccount: sourceAccount,
656
+ Action: "lambda:InvokeFunction",
657
+ InvokedViaFunctionUrl: true
658
+ }
659
+ ] : [
623
660
  {
624
661
  StatementId: "public-url",
662
+ Principal: "*",
625
663
  Action: "lambda:InvokeFunctionUrl",
626
664
  FunctionUrlAuthType: "NONE"
627
665
  },
628
666
  {
629
667
  StatementId: "public-invoke",
668
+ Principal: "*",
630
669
  Action: "lambda:InvokeFunction",
631
670
  InvokedViaFunctionUrl: true
632
671
  }
@@ -637,7 +676,6 @@ var createLambdaProvider = ({ credentials, region }) => {
637
676
  new AddPermissionCommand({
638
677
  FunctionName: functionName,
639
678
  Qualifier: alias,
640
- Principal: "*",
641
679
  ...permission
642
680
  })
643
681
  );
@@ -647,6 +685,23 @@ var createLambdaProvider = ({ credentials, region }) => {
647
685
  }
648
686
  }
649
687
  }
688
+ if (sourceAccount) {
689
+ for (const statementId of ["public-url", "public-invoke"]) {
690
+ try {
691
+ await lambda.send(
692
+ new RemovePermissionCommand({
693
+ FunctionName: functionName,
694
+ Qualifier: alias,
695
+ StatementId: statementId
696
+ })
697
+ );
698
+ } catch (error) {
699
+ if (!isError(error, "ResourceNotFoundException")) {
700
+ throw error;
701
+ }
702
+ }
703
+ }
704
+ }
650
705
  return url;
651
706
  };
652
707
  const createBundleDeployment = async (state2) => {
@@ -658,7 +713,7 @@ var createLambdaProvider = ({ credentials, region }) => {
658
713
  name: deploymentAlias
659
714
  });
660
715
  await configureVersion(state2);
661
- const url = await createPublicUrl(state2.functionName, deploymentAlias);
716
+ const url = await createDeploymentUrl(state2.functionName, deploymentAlias, state2.sourceAccount);
662
717
  return {
663
718
  ...state2,
664
719
  deploymentAlias,
@@ -769,7 +824,21 @@ var createLambdaProvider = ({ credentials, region }) => {
769
824
  deploymentAliases.push(deploymentAlias);
770
825
  }
771
826
  await configureVersion(proposed);
772
- const url = await createPublicUrl(proposed.functionName, deploymentAlias);
827
+ const url = await createDeploymentUrl(proposed.functionName, deploymentAlias, proposed.sourceAccount);
828
+ if (proposed.sourceAccount && prior.sourceAccount !== proposed.sourceAccount) {
829
+ for (const name of deploymentAliases) {
830
+ if (name === deploymentAlias) {
831
+ continue;
832
+ }
833
+ try {
834
+ await createDeploymentUrl(proposed.functionName, name, proposed.sourceAccount);
835
+ } catch (error) {
836
+ if (!isError(error, "ResourceNotFoundException")) {
837
+ throw error;
838
+ }
839
+ }
840
+ }
841
+ }
773
842
  return {
774
843
  ...proposed,
775
844
  deploymentAlias,
@@ -1259,6 +1328,60 @@ var markPromoted = async (client, appId, id) => {
1259
1328
  var removeDeployment = async (client, appId, id) => {
1260
1329
  await deleteItem(table, { appId, id }, { client });
1261
1330
  };
1331
+ var selectPrunableDeployments = (items, liveId, options) => {
1332
+ const rollbackTarget = items.filter((item) => item.promotedAt && item.id !== liveId).sort((a, b) => b.promotedAt.localeCompare(a.promotedAt))[0];
1333
+ const keep = Math.max(1, Number(options.keep) || 10);
1334
+ const mainSlug = slugifyBranch(options.main);
1335
+ const keptMain = new Set(
1336
+ items.filter((item) => item.branch === mainSlug && item.functionVersion).map((item) => item.seq).sort((a, b) => b - a).slice(0, keep)
1337
+ );
1338
+ const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1e3).toISOString();
1339
+ return items.filter((item) => {
1340
+ if (item.id === liveId || item.id === rollbackTarget?.id) {
1341
+ return false;
1342
+ }
1343
+ if (options.branch) {
1344
+ return item.branch === slugifyBranch(options.branch);
1345
+ }
1346
+ if (!item.functionVersion) {
1347
+ return item.createdAt < dayAgo;
1348
+ }
1349
+ if (item.branch === mainSlug) {
1350
+ return !keptMain.has(item.seq);
1351
+ }
1352
+ return item.commit ? isCommitMerged(item.commit, options.main) : false;
1353
+ });
1354
+ };
1355
+ var pruneFunctionVersion = async (lambda, functionName, version) => {
1356
+ for (const alias of await listLambdaAliases(lambda, functionName, version)) {
1357
+ if (alias.Name && alias.Name !== LIVE_LAMBDA_ALIAS) {
1358
+ await deleteLambdaAlias(lambda, functionName, alias.Name);
1359
+ }
1360
+ }
1361
+ try {
1362
+ await lambda.send(
1363
+ new DeleteFunctionCommand({
1364
+ FunctionName: functionName,
1365
+ Qualifier: version
1366
+ })
1367
+ );
1368
+ } catch (error) {
1369
+ if (!isError(error, "ResourceNotFoundException")) {
1370
+ throw error;
1371
+ }
1372
+ }
1373
+ };
1374
+ var selectPrunableVersions = async (props) => {
1375
+ const surviving = props.items.filter((item) => !props.prunable.includes(item));
1376
+ const kept = new Set(surviving.map((item) => item.functionVersion));
1377
+ const live = await getLambdaAlias(props.lambda, props.functionName, LIVE_LAMBDA_ALIAS);
1378
+ if (live?.FunctionVersion) {
1379
+ kept.add(live.FunctionVersion);
1380
+ }
1381
+ return new Set(
1382
+ props.prunable.map((item) => item.functionVersion).filter((version) => Boolean(version && !kept.has(version)))
1383
+ );
1384
+ };
1262
1385
  var readLiveDeploymentId = async (lambda, functionName) => {
1263
1386
  return (await getLambdaAlias(lambda, functionName, LIVE_LAMBDA_ALIAS))?.Description || void 0;
1264
1387
  };
@@ -1275,21 +1398,21 @@ var readDeployedFunctionVersion = (state2) => {
1275
1398
  return;
1276
1399
  };
1277
1400
  var formatDeploymentSummary = (props) => {
1278
- let previewUrl;
1279
- let lambdaUrl;
1401
+ const previewUrls = /* @__PURE__ */ new Map();
1280
1402
  for (const [urn, node] of readStateNodes(props.state)) {
1281
1403
  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;
1404
+ const router = urn.match(/router:\{([^}]+)\}/)?.[1];
1405
+ if (router) {
1406
+ previewUrls.set(router, `https://${node.output.domainName}`);
1407
+ }
1286
1408
  }
1287
1409
  }
1288
- return Object.keys(props.appConfig.defaults.router ?? {}).map((routerId, index) => {
1410
+ return Object.keys(props.appConfig.defaults.router ?? {}).map((routerId) => {
1411
+ const previewUrl = previewUrls.get(routerId);
1289
1412
  return [
1290
1413
  `${routerId}: deployment #${props.id}`,
1291
- index === 0 ? previewUrl : void 0,
1292
- index === 0 ? lambdaUrl : void 0
1414
+ previewUrl,
1415
+ previewUrl ? `${previewUrl}/?awsless-deployment=${props.id}` : void 0
1293
1416
  ].filter(Boolean).join("\n");
1294
1417
  });
1295
1418
  };
@@ -3843,7 +3966,6 @@ var bundleTypeScriptWithRolldown = async ({
3843
3966
  format: format3 = "esm",
3844
3967
  minify = true,
3845
3968
  file,
3846
- nativeDir,
3847
3969
  external,
3848
3970
  importAsString: importAsStringList
3849
3971
  }) => {
@@ -3854,7 +3976,10 @@ var bundleTypeScriptWithRolldown = async ({
3854
3976
  return importee.startsWith("@aws-sdk") || importee.startsWith("aws-sdk") || external?.includes(importee);
3855
3977
  },
3856
3978
  treeshake: {
3857
- moduleSideEffects: (id, isExternal) => isExternal ? external?.includes(id) === true : id.startsWith(`${directories.root}/`)
3979
+ // Dependencies are treated as side-effect free, so unused imports
3980
+ // like the local test servers never reach the production bundle.
3981
+ // The bundle guard below fails the build if one slips through.
3982
+ moduleSideEffects: (id, isExternal) => isExternal ? external?.includes(id) === true : id.startsWith(`${directories.root}/`) && !id.includes("/node_modules/")
3858
3983
  },
3859
3984
  onwarn: (error) => {
3860
3985
  debugError(error.message);
@@ -3901,6 +4026,7 @@ var bundleTypeScriptWithRolldown = async ({
3901
4026
  chunkFileNames: `[name].${ext}`,
3902
4027
  minify
3903
4028
  });
4029
+ assertNoTestOnlyModules(result.output);
3904
4030
  const hash = createHash4("sha1");
3905
4031
  const files = [];
3906
4032
  for (const item of result.output) {
@@ -3922,6 +4048,22 @@ var bundleTypeScriptWithRolldown = async ({
3922
4048
  files
3923
4049
  };
3924
4050
  };
4051
+ var TEST_ONLY_MODULES = ["dynamo-db-local", "@awsless/dynamodb-server", "redis-memory-server"];
4052
+ var assertNoTestOnlyModules = (output) => {
4053
+ for (const item of output) {
4054
+ if (item.type !== "chunk") {
4055
+ continue;
4056
+ }
4057
+ for (const id of item.moduleIds ?? []) {
4058
+ const found = TEST_ONLY_MODULES.find((name) => id.includes(`/node_modules/${name}/`));
4059
+ if (found) {
4060
+ throw new Error(
4061
+ `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.`
4062
+ );
4063
+ }
4064
+ }
4065
+ }
4066
+ };
3925
4067
 
3926
4068
  // src/feature/bundle/util.ts
3927
4069
  var ROUTE_HEADER = "x-awsless-route";
@@ -4038,6 +4180,11 @@ var bundleFeature = defineFeature({
4038
4180
  const addLayer = (layer) => {
4039
4181
  layers.push(layer);
4040
4182
  };
4183
+ const layerIds = Object.keys(ctx.appConfig.defaults.layers ?? {});
4184
+ const layerPackages = layerIds.flatMap((id) => ctx.shared.entry("layer", "packages", id));
4185
+ for (const id of layerIds) {
4186
+ addLayer(ctx.shared.entry("layer", "arn", id));
4187
+ }
4041
4188
  const name = getBundleFunctionName(ctx.app.name);
4042
4189
  const shortName = formatGlobalResourceName({
4043
4190
  appName: ctx.app.name,
@@ -4051,7 +4198,7 @@ var bundleFeature = defineFeature({
4051
4198
  name,
4052
4199
  handlers,
4053
4200
  minify: defaults.minify,
4054
- external: defaults.external
4201
+ external: [...defaults.external ?? [], ...layerPackages]
4055
4202
  })
4056
4203
  );
4057
4204
  const sourceHash = new Output3(envDeps, async (resolve2) => {
@@ -4207,7 +4354,8 @@ var bundleFeature = defineFeature({
4207
4354
  deploymentId: ctx.deploymentId ?? "local-0",
4208
4355
  functionName: lambda.functionName,
4209
4356
  functionVersion: lambda.version,
4210
- onFailureArn: onFailure
4357
+ onFailureArn: onFailure,
4358
+ sourceAccount: ctx.accountId
4211
4359
  },
4212
4360
  {
4213
4361
  // Make sure the permissions are in place before any event source is wired up.
@@ -6922,14 +7070,15 @@ var siteFeature = defineFeature({
6922
7070
  env,
6923
7071
  stdout: "pipe",
6924
7072
  stderr: "pipe"
6925
- // stdout: 'ignore',
6926
- // stderr: ''
6927
- // stdout: 'inherit',
6928
- // stderr: 'inherit',
6929
7073
  });
6930
- await instance.exited;
7074
+ const [output, errors] = await Promise.all([
7075
+ new Response(instance.stdout).text(),
7076
+ new Response(instance.stderr).text(),
7077
+ instance.exited
7078
+ ]);
6931
7079
  if (instance.exitCode !== null && instance.exitCode > 0) {
6932
- throw new Error("Site build failed");
7080
+ throw new ExpectedError(`Site build failed:
7081
+ ${(errors.trim() || output.trim()).slice(-2e3)}`);
6933
7082
  }
6934
7083
  await write("HASH", fingerprint);
6935
7084
  return {
@@ -9117,7 +9266,7 @@ var getViewerRequestFunctionCode = (props) => {
9117
9266
  ].join("\n")
9118
9267
  ) : ""
9119
9268
  ],
9120
- ACTIVE_PREFIX(props.router)
9269
+ props.preview ? PREVIEW_PREFIX(props.router) : ACTIVE_PREFIX(props.router)
9121
9270
  );
9122
9271
  };
9123
9272
  var BLOCK_DIRECT_ACCESS_TO_CLOUDFRONT = `
@@ -9172,11 +9321,64 @@ var PASSWORD_AUTH_CHECK = (password) => `
9172
9321
  authMethods.push('Password realm="Protected"');
9173
9322
 
9174
9323
  if(!isAuthorized) {
9175
- if(authHeader && authHeader.startsWith('Password ') && authHeader.slice(9) === '${password}') {
9324
+ if(authHeader && authHeader.startsWith('Password ') && authHeader.slice(9) === ${JSON.stringify(password)}) {
9176
9325
  isAuthorized = true;
9177
9326
  }
9178
9327
  }
9179
9328
  `;
9329
+ var PREVIEW_PREFIX = (router) => `
9330
+ const router = ${JSON.stringify(router)};
9331
+ let deployment;
9332
+
9333
+ if (request.querystring['awsless-deployment'] && request.querystring['awsless-deployment'].value) {
9334
+ deployment = request.querystring['awsless-deployment'].value;
9335
+ } else if (request.cookies && request.cookies['awsless-deployment'] && request.cookies['awsless-deployment'].value) {
9336
+ deployment = request.cookies['awsless-deployment'].value;
9337
+ }
9338
+
9339
+ let prefix;
9340
+
9341
+ try {
9342
+ const pointer = deployment ? '$deploy:' + deployment : '$active';
9343
+ prefix = (await cf.kvs().get(pointer)).split(':')[0] + ':' + router + ':';
9344
+ } catch (e) {
9345
+ return deployment
9346
+ ? { statusCode: 404, statusDescription: 'Unknown Deployment' }
9347
+ : { statusCode: 503, statusDescription: 'Service Unavailable' };
9348
+ }
9349
+
9350
+ if (deployment && request.querystring['awsless-deployment']) {
9351
+ delete request.querystring['awsless-deployment'];
9352
+
9353
+ const query = [];
9354
+
9355
+ for (const key in request.querystring) {
9356
+ const entry = request.querystring[key];
9357
+
9358
+ if (entry.multiValue) {
9359
+ // The CloudFront js runtime doesn't support for...of.
9360
+ for (const i in entry.multiValue) {
9361
+ query.push(key + '=' + entry.multiValue[i].value);
9362
+ }
9363
+ } else {
9364
+ query.push(key + '=' + entry.value);
9365
+ }
9366
+ }
9367
+
9368
+ return {
9369
+ statusCode: 302,
9370
+ statusDescription: 'Found',
9371
+ headers: {
9372
+ location: { value: request.uri + (query.length ? '?' + query.join('&') : '') }
9373
+ },
9374
+ cookies: {
9375
+ 'awsless-deployment': {
9376
+ value: deployment,
9377
+ attributes: 'Path=/; Secure; SameSite=Lax'
9378
+ }
9379
+ }
9380
+ };
9381
+ }`;
9180
9382
  var ACTIVE_PREFIX = (router) => `
9181
9383
  const router = ${JSON.stringify(router)};
9182
9384
  let prefix;
@@ -9450,7 +9652,6 @@ var routerFeature = defineFeature({
9450
9652
  const distributionIds = [];
9451
9653
  let hasLambdaRoutes = false;
9452
9654
  let routeStore;
9453
- let previewDistribution;
9454
9655
  for (const [id, props] of routers) {
9455
9656
  const group = new Group29(ctx.base, "router", id);
9456
9657
  const name = formatGlobalResourceName({
@@ -9787,19 +9988,20 @@ var routerFeature = defineFeature({
9787
9988
  ],
9788
9989
  webAclId: waf?.arn
9789
9990
  });
9790
- if (id === defaultRouter) {
9991
+ {
9791
9992
  const previewFunction = new aws30.cloudfront.Function(group, "preview-function", {
9792
9993
  name: `${name.slice(0, 55)}--preview`,
9793
9994
  runtime: "cloudfront-js-2.0",
9794
9995
  code: getViewerRequestFunctionCode({
9795
9996
  router: id,
9997
+ preview: true,
9796
9998
  basicAuth: props.basicAuth,
9797
9999
  passwordAuth: props.passwordAuth
9798
10000
  }),
9799
10001
  publish: true,
9800
10002
  keyValueStoreAssociations: [routeStore.arn]
9801
10003
  });
9802
- previewDistribution = new aws30.cloudfront.Distribution(group, "preview", {
10004
+ const previewDistribution = new aws30.cloudfront.Distribution(group, "preview", {
9803
10005
  tags: {
9804
10006
  name: `${name}-preview`
9805
10007
  },
@@ -9863,6 +10065,9 @@ var routerFeature = defineFeature({
9863
10065
  webAclId: waf?.arn
9864
10066
  });
9865
10067
  distributionIds.push(previewDistribution.id);
10068
+ ctx.shared.add("router", "preview-id", id, previewDistribution.id);
10069
+ }
10070
+ if (id === defaultRouter) {
9866
10071
  ctx.onReadyLast(() => {
9867
10072
  const bundle = ctx.shared.get("bundle", "main");
9868
10073
  let lambdaUrlHost;
@@ -9901,26 +10106,9 @@ var routerFeature = defineFeature({
9901
10106
  dependsOn: Array.from(routeDependencies)
9902
10107
  }
9903
10108
  );
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
10109
  });
9921
10110
  }
9922
10111
  ctx.shared.add("router", "id", id, distribution.id);
9923
- ctx.shared.add("router", "preview-id", id, previewDistribution.id);
9924
10112
  distributionIds.push(distribution.id);
9925
10113
  if (props.domain) {
9926
10114
  const domainName = formatFullDomainName(ctx.appConfig, props.domain, props.subDomain);
@@ -11483,9 +11671,10 @@ var deploy = (program2) => {
11483
11671
  }
11484
11672
  });
11485
11673
  playSuccessSound();
11486
- const details = deployments2.length > 0 ? `
11487
- ${deployments2.join("\n")}` : "";
11488
- return `Deployment #${deployment.id} is live.${details}`;
11674
+ for (const summary of deployments2) {
11675
+ log20.message(summary);
11676
+ }
11677
+ return `Deployment #${deployment.id} is live.`;
11489
11678
  });
11490
11679
  });
11491
11680
  };
@@ -11976,7 +12165,7 @@ var bind = (program2) => {
11976
12165
  stderr: "inherit"
11977
12166
  });
11978
12167
  await instance.exited;
11979
- process.exit(0);
12168
+ process.exit(instance.exitCode ?? 1);
11980
12169
  });
11981
12170
  });
11982
12171
  };
@@ -12096,7 +12285,7 @@ var resources = (program2) => {
12096
12285
  return `${color.dim("{")}${color.warning(v)}${color.dim("}")}`;
12097
12286
  }).replaceAll(":", color.dim(":"));
12098
12287
  };
12099
- const formatStatus = (status) => {
12288
+ const formatStatus2 = (status) => {
12100
12289
  if (status === "created") {
12101
12290
  return color.success(status);
12102
12291
  }
@@ -12121,7 +12310,7 @@ var resources = (program2) => {
12121
12310
  stack.resources.map((r) => {
12122
12311
  return [
12123
12312
  //
12124
- formatStatus(r.status),
12313
+ formatStatus2(r.status),
12125
12314
  color.dim(icon.arrow.right),
12126
12315
  formatResource(stack.urn, r.urn)
12127
12316
  ].join(" ");
@@ -13047,7 +13236,7 @@ var activity = (program2) => {
13047
13236
  // src/cli/command/deployment.ts
13048
13237
  import { CloudFrontClient as CloudFrontClient6 } from "@aws-sdk/client-cloudfront";
13049
13238
  import { CloudFrontKeyValueStoreClient as CloudFrontKeyValueStoreClient3 } from "@aws-sdk/client-cloudfront-keyvaluestore";
13050
- import { DeleteFunctionCommand, LambdaClient as LambdaClient7 } from "@aws-sdk/client-lambda";
13239
+ import { LambdaClient as LambdaClient7 } from "@aws-sdk/client-lambda";
13051
13240
  import { log as log35, prompt as prompt19 } from "@awsless/clui";
13052
13241
  import { DynamoDBClient as DynamoDBClient6 } from "@awsless/dynamodb";
13053
13242
  var createClients = async (appConfig) => {
@@ -13070,6 +13259,12 @@ var formatAge = (iso) => {
13070
13259
  if (minutes8 < 60 * 24) return `${Math.floor(minutes8 / 60)}h ago`;
13071
13260
  return `${Math.floor(minutes8 / (60 * 24))}d ago`;
13072
13261
  };
13262
+ var formatStatus = (item, liveId) => {
13263
+ if (item.id === liveId) return color.success("live ");
13264
+ if (item.promotedAt) return "promoted";
13265
+ if (item.functionVersion) return color.info("staged ");
13266
+ return color.dim("pending ");
13267
+ };
13073
13268
  var deployments = (program2) => {
13074
13269
  program2.command("deployments").description("List the deployment history of your app").action(async () => {
13075
13270
  await layout("deployments", async ({ appConfig }) => {
@@ -13083,17 +13278,16 @@ var deployments = (program2) => {
13083
13278
  }
13084
13279
  const idWidth = Math.max(...items.map((item) => item.id.length));
13085
13280
  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 [
13281
+ items.map(
13282
+ (item) => [
13089
13283
  color.label(item.id.padEnd(idWidth)),
13090
- status,
13284
+ formatStatus(item, liveId),
13091
13285
  formatAge(item.createdAt).padEnd(8),
13092
13286
  color.dim(item.commit?.slice(0, 7) ?? "-------"),
13093
13287
  (item.message ?? "").slice(0, 50).padEnd(50),
13094
13288
  color.dim(item.user ?? "")
13095
- ].join(" ");
13096
- }).join("\n")
13289
+ ].join(" ")
13290
+ ).join("\n")
13097
13291
  );
13098
13292
  return `Found ${items.length} deployments.`;
13099
13293
  });
@@ -13107,28 +13301,7 @@ var prune = (program2) => {
13107
13301
  listDeployments(dynamo, appId),
13108
13302
  readLiveDeploymentId(lambda, functionName)
13109
13303
  ]);
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
- });
13304
+ const prunable = selectPrunableDeployments(items, liveId, options);
13132
13305
  if (prunable.length === 0) {
13133
13306
  return `Nothing to prune.`;
13134
13307
  }
@@ -13148,28 +13321,9 @@ var prune = (program2) => {
13148
13321
  for (const item of prunable) {
13149
13322
  await deleteLambdaAlias(lambda, functionName, getDeploymentLambdaAliasName(item.id));
13150
13323
  }
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
- );
13324
+ const versions = await selectPrunableVersions({ lambda, functionName, items, prunable });
13160
13325
  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
- }
13326
+ await pruneFunctionVersion(lambda, functionName, version);
13173
13327
  }
13174
13328
  const storeArn = await getRouteStoreArn(
13175
13329
  cloudfront,
@@ -13255,8 +13409,8 @@ program.option("--stage <string>", "The stage to use");
13255
13409
  program.option("-c --no-cache", "Always build & test without the cache");
13256
13410
  program.option("-s --skip-prompt", "Skip prompts");
13257
13411
  program.option("-v --verbose", "Print verbose logs");
13258
- program.exitOverride(() => {
13259
- process.exit(0);
13412
+ program.exitOverride((error) => {
13413
+ process.exit(error.exitCode);
13260
13414
  });
13261
13415
  program.on("option:verbose", () => {
13262
13416
  process.env.VERBOSE = program.opts().verbose ? "1" : void 0;
@@ -3,128 +3,6 @@ import { patch, unpatch } from "@awsless/json";
3
3
  import { ExpectedError, invoke, isErrorResponse } from "@awsless/lambda";
4
4
  import { formatRoutePayload, getCurrentRoute, withRoute } from "awsless";
5
5
 
6
- // src/feature/bundle/server/preview.ts
7
- import { GetObjectCommand, NoSuchKey } from "@aws-sdk/client-s3";
8
- import { s3Client } from "@awsless/s3";
9
- var getPossibleRouteKeys = (path) => {
10
- if (path === "" || path === "/") {
11
- return ["/", "/*"];
12
- }
13
- const parts = path.split("/");
14
- const root = path.startsWith("/") ? parts[1] : parts[0];
15
- const file = parts[parts.length - 1].includes(".");
16
- if (root.includes(".")) {
17
- return [path, "/*.", "/*"];
18
- }
19
- if (file) {
20
- return [path, "/" + root + "/*.", "/" + root + "/*", "/*.", "/*"];
21
- }
22
- return [path, "/" + root + "/*", "/*"];
23
- };
24
- var findRoute = (props, path, method) => {
25
- for (const key of getPossibleRouteKeys(path)) {
26
- const route = props.routes[`${props.router}:${key}`];
27
- if (!route) {
28
- continue;
29
- }
30
- if (route.type === "s3" && method !== "GET" && method !== "HEAD") {
31
- continue;
32
- }
33
- return route;
34
- }
35
- return;
36
- };
37
- var rewritePath = (route, path) => {
38
- if (!route.rewrite) {
39
- return path;
40
- }
41
- if (route.rewrite.regex) {
42
- return path.replace(new RegExp(route.rewrite.regex), route.rewrite.to);
43
- }
44
- return route.rewrite.to;
45
- };
46
- var serveObject = async (route, path, method) => {
47
- const bucket = route.domainName.split(".s3")[0];
48
- const key = rewritePath(route, path).replace(/^\//, "");
49
- let result;
50
- try {
51
- result = await s3Client().send(new GetObjectCommand({
52
- Bucket: bucket,
53
- Key: key
54
- }));
55
- } catch (error) {
56
- if (error instanceof NoSuchKey) {
57
- return {
58
- statusCode: 404
59
- };
60
- }
61
- throw error;
62
- }
63
- const headers = {};
64
- if (result.ContentType) {
65
- headers["content-type"] = result.ContentType;
66
- }
67
- if (result.CacheControl) {
68
- headers["cache-control"] = result.CacheControl;
69
- }
70
- if (result.ETag) {
71
- headers["etag"] = result.ETag;
72
- }
73
- if (method === "HEAD") {
74
- return { statusCode: 200, headers };
75
- }
76
- return {
77
- statusCode: 200,
78
- headers,
79
- body: await result.Body.transformToString("base64"),
80
- isBase64Encoded: true
81
- };
82
- };
83
- var createPreviewHandler = (props) => {
84
- return async (event) => {
85
- const method = event.requestContext.http.method;
86
- const headers = event.headers ?? {};
87
- let path = event.rawPath;
88
- try {
89
- path = decodeURIComponent(path);
90
- } catch {}
91
- if (method === "OPTIONS") {
92
- return {
93
- statusCode: 204,
94
- headers: {
95
- "access-control-allow-origin": "*",
96
- "access-control-allow-methods": "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS",
97
- "access-control-allow-headers": "*",
98
- "access-control-max-age": "86400"
99
- }
100
- };
101
- }
102
- const route = findRoute(props, path, method);
103
- if (!route) {
104
- return {
105
- statusCode: 404
106
- };
107
- }
108
- if (route.type === "s3") {
109
- return serveObject(route, path, method);
110
- }
111
- for (const [name, value] of Object.entries(route.requestHeaders ?? {})) {
112
- headers[name] = value;
113
- }
114
- if (headers.authorization) {
115
- headers["x-awsless-authorization"] = headers.authorization;
116
- } else {
117
- delete headers["x-awsless-authorization"];
118
- }
119
- if (route.forwardHost && headers.host) {
120
- headers["x-forwarded-host"] = headers.host;
121
- }
122
- event.headers = headers;
123
- event.rawPath = rewritePath(route, path);
124
- return props.dispatch(event);
125
- };
126
- };
127
-
128
6
  // src/feature/bundle/server/resource/util.ts
129
7
  var asyncRoute = (key, payload) => {
130
8
  process.env.THROW_EXPECTED_ERRORS = "1";
@@ -434,7 +312,6 @@ var topicHandler = (event, routes) => {
434
312
  // src/feature/bundle/server/handle.ts
435
313
  var createBundle = (handlers) => {
436
314
  const routes = Object.keys(handlers);
437
- let previewConfig;
438
315
  const matchers = [
439
316
  functionHandler,
440
317
  cronHandler,
@@ -473,7 +350,8 @@ var createBundle = (handlers) => {
473
350
  process.env.AWSLESS_ROUTE = match2.key;
474
351
  return withRoute(match2.key, invokeRoute, async () => {
475
352
  const handle = await load();
476
- return handle(match2.payload ?? {}, context);
353
+ const routedContext = { ...context, route: match2.key };
354
+ return handle(match2.payload ?? {}, routedContext);
477
355
  });
478
356
  };
479
357
  const invokeRoute = async (key, payload) => {
@@ -502,20 +380,6 @@ var createBundle = (handlers) => {
502
380
  }
503
381
  return response;
504
382
  };
505
- const raw = event;
506
- if (process.env.AWSLESS_PREVIEW && raw?.requestContext?.http && !event?.headers?.["x-awsless-route"]) {
507
- previewConfig ??= JSON.parse(process.env.AWSLESS_PREVIEW);
508
- return createPreviewHandler({
509
- ...previewConfig,
510
- dispatch: async (event2) => {
511
- const match2 = matchRoute(event2);
512
- if (Array.isArray(match2)) {
513
- throw new Error("Unknown bundle route");
514
- }
515
- return handleRoute(match2);
516
- }
517
- })(event);
518
- }
519
383
  const match = matchRoute(event);
520
384
  if (Array.isArray(match)) {
521
385
  const name = `${process.env.AWS_LAMBDA_FUNCTION_NAME}:${process.env.AWS_LAMBDA_FUNCTION_VERSION}`;
@@ -1,8 +1,81 @@
1
1
  // src/feature/on-failure/server/handle.ts
2
- import { parse, patch } from "@awsless/json";
2
+ import { parse as parse2, patch } from "@awsless/json";
3
3
  import { invoke } from "@awsless/lambda";
4
4
  import { deleteObject, getObject } from "@awsless/s3";
5
5
  import { formatRoutePayload, getRouteEnv } from "awsless";
6
+
7
+ // src/feature/on-failure/server/util.ts
8
+ import { parse } from "@awsless/json";
9
+ var isDynamoDBFailureEvent = (event) => {
10
+ return "DDBStreamBatchInfo" in event;
11
+ };
12
+ var logicalResourceName = (physical) => {
13
+ const segments = physical.replace(/\.fifo$/, "").split("--");
14
+ if (segments.length === 4) {
15
+ return `${segments[1]}:${segments[2]}:${segments[3]}`;
16
+ }
17
+ if (segments.length === 3) {
18
+ return `${segments[1]}:${segments[2]}`;
19
+ }
20
+ return physical;
21
+ };
22
+ var getFailureSource = (payload) => {
23
+ const record = getDeliveryRecord(payload);
24
+ if (!record) {
25
+ return;
26
+ }
27
+ if (isTopicRecord(record)) {
28
+ return {
29
+ resource: record.Sns.TopicArn ? logicalResourceName(lastArnSegment(record.Sns.TopicArn)) : "topic",
30
+ event: parseEvent(record.Sns.Message)
31
+ };
32
+ }
33
+ if (isStreamRecord(record)) {
34
+ const table = record.eventSourceARN.split("/")[1];
35
+ return {
36
+ resource: table ? logicalResourceName(table) : "table-stream"
37
+ };
38
+ }
39
+ if (isQueueRecord(record)) {
40
+ return {
41
+ resource: logicalResourceName(lastArnSegment(record.eventSourceARN)),
42
+ event: parseEvent(record.body)
43
+ };
44
+ }
45
+ return;
46
+ };
47
+ var getDeliveryRecord = (payload) => {
48
+ if (!payload || typeof payload !== "object") {
49
+ return;
50
+ }
51
+ const records = payload.Records;
52
+ const record = Array.isArray(records) ? records[0] : undefined;
53
+ return record && typeof record === "object" ? record : undefined;
54
+ };
55
+ var isTopicRecord = (record) => {
56
+ return "Sns" in record && typeof record.Sns === "object" && record.Sns !== null;
57
+ };
58
+ var isStreamRecord = (record) => {
59
+ return "eventSource" in record && record.eventSource === "aws:dynamodb" && "eventSourceARN" in record;
60
+ };
61
+ var isQueueRecord = (record) => {
62
+ return "eventSource" in record && record.eventSource === "aws:sqs" && "eventSourceARN" in record;
63
+ };
64
+ var lastArnSegment = (arn) => {
65
+ return arn.split(":").at(-1);
66
+ };
67
+ var parseEvent = (value) => {
68
+ if (typeof value !== "string") {
69
+ return value;
70
+ }
71
+ try {
72
+ return parse(value);
73
+ } catch {
74
+ return value;
75
+ }
76
+ };
77
+
78
+ // src/feature/on-failure/server/handle.ts
6
79
  var handle_default = async (event, context) => {
7
80
  if (!Array.isArray(event.Records)) {
8
81
  throw new TypeError(`Unknown Event Type: ${JSON.stringify(event)}`);
@@ -28,13 +101,16 @@ var sqsRecord = async (record, context) => {
28
101
  await Promise.all(s3Records.map((record2) => s3Record(record2, context)));
29
102
  return;
30
103
  }
104
+ const queueName = record.messageAttributes.queueName?.stringValue;
105
+ const body = parsePayload(record.body);
31
106
  const payload = {
32
107
  type: "queue",
33
108
  id: record.messageId,
34
109
  date: new Date(Number(record.attributes.SentTimestamp)),
35
- payload: parsePayload(record.body),
110
+ payload: body,
111
+ source: queueName ? { resource: logicalResourceName(queueName), event: body } : undefined,
36
112
  queue: {
37
- name: record.messageAttributes.queueName?.stringValue,
113
+ name: queueName,
38
114
  url: record.messageAttributes.queueUrl?.stringValue
39
115
  }
40
116
  };
@@ -70,9 +146,6 @@ var s3Record = async (record, context) => {
70
146
  key
71
147
  });
72
148
  };
73
- var isDynamoDBFailureEvent = (event) => {
74
- return "DDBStreamBatchInfo" in event;
75
- };
76
149
  var formatUnknownFailureEvent = (event) => {
77
150
  if (isDynamoDBFailureEvent(event)) {
78
151
  return formatDynamoDBStreamFailureEvent(event);
@@ -90,6 +163,7 @@ var formatAsyncLambdaFailureEvent = (event) => {
90
163
  name: typeof route === "string" ? route : event.requestContext.functionArn.split(":")[6]
91
164
  },
92
165
  payload: typeof route === "string" ? payload.event ?? {} : payload,
166
+ source: typeof route === "string" ? { resource: route, event: payload.event ?? {} } : getFailureSource(payload),
93
167
  error: {
94
168
  type: event.responsePayload.errorType,
95
169
  message: event.responsePayload.errorMessage,
@@ -98,6 +172,9 @@ var formatAsyncLambdaFailureEvent = (event) => {
98
172
  };
99
173
  };
100
174
  var formatDynamoDBStreamFailureEvent = (event) => {
175
+ const payload = parsePayload(event.payload);
176
+ const streamArn = event.DDBStreamBatchInfo?.streamArn;
177
+ const table = streamArn?.split("/")[1];
101
178
  return {
102
179
  type: "dynamodb-stream",
103
180
  date: new Date(event.timestamp),
@@ -105,12 +182,13 @@ var formatDynamoDBStreamFailureEvent = (event) => {
105
182
  function: {
106
183
  name: event.requestContext.functionArn.split(":")[6]
107
184
  },
108
- payload: parsePayload(event.payload)
185
+ payload,
186
+ source: table ? { resource: logicalResourceName(table) } : getFailureSource(payload)
109
187
  };
110
188
  };
111
189
  var parsePayload = (payload) => {
112
190
  try {
113
- return parse(payload);
191
+ return parse2(payload);
114
192
  } catch {
115
193
  return payload;
116
194
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awsless/cli",
3
- "version": "0.0.46-next.1",
3
+ "version": "0.0.46-next.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -83,23 +83,23 @@
83
83
  "zod": "^3.24.2",
84
84
  "zod-to-json-schema": "^3.24.3",
85
85
  "@awsless/big-float": "^0.1.7",
86
- "@awsless/clui": "^0.0.8",
87
86
  "@awsless/cloudwatch": "^0.0.1",
88
87
  "@awsless/duration": "^0.0.4",
88
+ "@awsless/clui": "^0.0.9",
89
89
  "@awsless/dynamodb": "^0.3.21",
90
90
  "@awsless/iot": "^0.0.5",
91
91
  "@awsless/json": "^0.0.11",
92
- "@awsless/lambda": "^0.0.44",
93
92
  "@awsless/s3": "^0.0.21",
94
93
  "@awsless/redis": "^0.1.13",
95
94
  "@awsless/size": "^0.0.2",
96
- "@awsless/sns": "^0.0.10",
95
+ "@awsless/scheduler": "^0.0.4",
96
+ "@awsless/lambda": "^0.0.45",
97
97
  "@awsless/sqs": "^0.0.24",
98
- "@awsless/ts-file-cache": "^0.0.16",
99
- "@awsless/weak-cache": "^0.0.1",
98
+ "@awsless/sns": "^0.0.10",
100
99
  "@awsless/validate": "^0.1.7",
101
100
  "awsless": "^0.0.15-next.0",
102
- "@awsless/scheduler": "^0.0.4"
101
+ "@awsless/weak-cache": "^0.0.1",
102
+ "@awsless/ts-file-cache": "^0.0.16"
103
103
  },
104
104
  "devDependencies": {
105
105
  "@hono/node-server": "1.19.9",