@stacksjs/ts-cloud 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -45,6 +45,12 @@ export type CodeSource = {
45
45
  kind: 'image';
46
46
  imageUri: string;
47
47
  };
48
+ /**
49
+ * AWS caps the aggregate size of a function's environment variables at 4 KB.
50
+ * Check before the API call so we fail with an actionable message (which vars,
51
+ * how far over) instead of a cryptic `InvalidParameterValueException` mid-deploy.
52
+ */
53
+ export declare function assertEnvWithinLimit(name: string, env: Record<string, string>): void;
48
54
  export declare function deployServerlessApp(config: CloudConfig, environment: EnvironmentType, opts?: DeployServerlessOptions): Promise<void>;
49
55
  export declare function redeployServerlessApp(config: CloudConfig, environment: EnvironmentType): Promise<void>;
50
56
  export declare function rollbackServerlessApp(config: CloudConfig, environment: EnvironmentType): Promise<void>;
package/dist/index.js CHANGED
@@ -25845,16 +25845,21 @@ function resolveServerlessRuntime(app) {
25845
25845
  layerEnvVar: "TSCLOUD_NODE_LAYER_ARN"
25846
25846
  };
25847
25847
  }
25848
- function resolveQueueNames(app, slug, env) {
25848
+ function resolveQueues(app, slug, env) {
25849
25849
  if (app.queues === false)
25850
25850
  return [];
25851
25851
  if (app.queues === undefined || app.queues === true)
25852
- return [`${slug}-${env}-default`];
25852
+ return [{ name: `${slug}-${env}-default` }];
25853
25853
  return app.queues.map((q) => {
25854
- const name = typeof q === "string" ? q : Object.keys(q)[0];
25855
- return `${slug}-${env}-${name}`;
25854
+ if (typeof q === "string")
25855
+ return { name: `${slug}-${env}-${q}` };
25856
+ const [name, concurrency] = Object.entries(q)[0];
25857
+ return { name: `${slug}-${env}-${name}`, concurrency };
25856
25858
  });
25857
25859
  }
25860
+ function resolveQueueNames(app, slug, env) {
25861
+ return resolveQueues(app, slug, env).map((q) => q.name);
25862
+ }
25858
25863
  function composeServerlessAppTemplate(opts) {
25859
25864
  const { app, environment, handlers } = opts;
25860
25865
  const slug = opts.config.project.slug;
@@ -25866,8 +25871,12 @@ function composeServerlessAppTemplate(opts) {
25866
25871
  queue: `${slug}-${environment}-queue`,
25867
25872
  cli: `${slug}-${environment}-cli`
25868
25873
  };
25869
- const queueNames = resolveQueueNames(app, slug, environment);
25874
+ if (app.gatewayVersion === 1)
25875
+ throw new Error("serverless app: `gatewayVersion: 1` (REST API) is not supported — ts-cloud uses API Gateway HTTP API (v2). Remove `gatewayVersion` or set it to 2.");
25876
+ const queues = resolveQueues(app, slug, environment);
25877
+ const queueNames = queues.map((q) => q.name);
25870
25878
  const hasQueue = queueNames.length > 0;
25879
+ const logRetention = app.logRetention ?? 14;
25871
25880
  const imageMode = app.packaging === "image";
25872
25881
  const schedulerEnabled = (app.scheduler ?? "on") !== "off";
25873
25882
  const cacheEnabled = (app.cache?.driver ?? "dynamodb") === "dynamodb";
@@ -25983,7 +25992,7 @@ function composeServerlessAppTemplate(opts) {
25983
25992
  function addFunction(logicalId, name, handler8, mode, memory, timeout, reservedConcurrency, tmp = tmpStorage) {
25984
25993
  resources[`${logicalId}LogGroup`] = {
25985
25994
  Type: "AWS::Logs::LogGroup",
25986
- Properties: { LogGroupName: `/aws/lambda/${name}`, RetentionInDays: 14 }
25995
+ Properties: { LogGroupName: `/aws/lambda/${name}`, RetentionInDays: logRetention }
25987
25996
  };
25988
25997
  const codeProps = imageMode ? {
25989
25998
  PackageType: "Image",
@@ -26124,19 +26133,21 @@ function composeServerlessAppTemplate(opts) {
26124
26133
  MessageRetentionPeriod: 1209600
26125
26134
  }
26126
26135
  };
26127
- queueNames.forEach((qName, i) => {
26136
+ queues.forEach((q, i) => {
26128
26137
  const qId = `AppQueue${i}`;
26138
+ const fnTimeout = app.queueTimeout ?? 120;
26129
26139
  resources[qId] = {
26130
26140
  Type: "AWS::SQS::Queue",
26131
26141
  Properties: {
26132
- QueueName: qName,
26133
- VisibilityTimeout: Math.max(app.queueTimeout ?? 120, app.queueTimeout ?? 120),
26142
+ QueueName: q.name,
26143
+ VisibilityTimeout: Math.min(43200, fnTimeout * 6),
26134
26144
  RedrivePolicy: {
26135
26145
  deadLetterTargetArn: Fn2.getAtt("AppQueueDlq", "Arn"),
26136
26146
  maxReceiveCount: app.queueTries ?? 3
26137
26147
  }
26138
26148
  }
26139
26149
  };
26150
+ const concurrency = q.concurrency ?? app.queueConcurrency;
26140
26151
  resources[`${qId}Mapping`] = {
26141
26152
  Type: "AWS::Lambda::EventSourceMapping",
26142
26153
  Properties: {
@@ -26144,10 +26155,10 @@ function composeServerlessAppTemplate(opts) {
26144
26155
  FunctionName: Fn2.ref("QueueFunction"),
26145
26156
  BatchSize: 1,
26146
26157
  FunctionResponseTypes: ["ReportBatchItemFailures"],
26147
- ...app.queueConcurrency ? { ScalingConfig: { MaximumConcurrency: Math.max(2, app.queueConcurrency) } } : {}
26158
+ ...concurrency ? { ScalingConfig: { MaximumConcurrency: Math.max(2, concurrency) } } : {}
26148
26159
  }
26149
26160
  };
26150
- outputs[`QueueUrl${i}`] = { Description: `Queue URL: ${qName}`, Value: Fn2.ref(qId) };
26161
+ outputs[`QueueUrl${i}`] = { Description: `Queue URL: ${q.name}`, Value: Fn2.ref(qId) };
26151
26162
  });
26152
26163
  }
26153
26164
  if (schedulerEnabled) {
@@ -26262,7 +26273,7 @@ function composeServerlessAppTemplate(opts) {
26262
26273
  ...app.assetDomain ? {
26263
26274
  Aliases: [app.assetDomain],
26264
26275
  ViewerCertificate: {
26265
- AcmCertificateArn: app.assetCertificateArn,
26276
+ AcmCertificateArn: app.assetCertificateArn ?? Fn2.ref("AssetsCertificate"),
26266
26277
  SslSupportMethod: "sni-only",
26267
26278
  MinimumProtocolVersion: "TLSv1.2_2021"
26268
26279
  }
@@ -26271,8 +26282,20 @@ function composeServerlessAppTemplate(opts) {
26271
26282
  }
26272
26283
  };
26273
26284
  if (app.assetDomain) {
26274
- if (!app.assetCertificateArn)
26275
- throw new Error("serverless app: `assetDomain` requires `assetCertificateArn` (a us-east-1 ACM cert — CloudFront only accepts certs from us-east-1).");
26285
+ if (!app.assetCertificateArn) {
26286
+ if (!app.hostedZoneId)
26287
+ throw new Error("serverless app: `assetDomain` requires either `assetCertificateArn` (a us-east-1 ACM cert) or `hostedZoneId` (to auto-issue + validate one). CloudFront only accepts certs from us-east-1.");
26288
+ if (region !== "us-east-1")
26289
+ throw new Error(`serverless app: auto-issuing an asset-domain cert needs a us-east-1 app (CloudFront certs must be us-east-1); this app is in ${region}. Supply a pre-issued us-east-1 \`assetCertificateArn\` instead.`);
26290
+ resources.AssetsCertificate = {
26291
+ Type: "AWS::CertificateManager::Certificate",
26292
+ Properties: {
26293
+ DomainName: app.assetDomain,
26294
+ ValidationMethod: "DNS",
26295
+ DomainValidationOptions: [{ DomainName: app.assetDomain, HostedZoneId: app.hostedZoneId }]
26296
+ }
26297
+ };
26298
+ }
26276
26299
  if (app.hostedZoneId) {
26277
26300
  resources.AssetsDomainRecord = {
26278
26301
  Type: "AWS::Route53::RecordSet",
@@ -83282,7 +83305,15 @@ async function uploadAssets(s32, bucket, dir, prefix, includeDotfiles = false) {
83282
83305
  }
83283
83306
  return count;
83284
83307
  }
83308
+ function assertEnvWithinLimit(name, env2) {
83309
+ const bytes = Object.entries(env2).reduce((n, [k, v]) => n + Buffer.byteLength(`${k}=${v}`, "utf8"), 0);
83310
+ if (bytes > 4096) {
83311
+ const biggest = Object.entries(env2).map(([k, v]) => [k, Buffer.byteLength(`${k}=${v}`, "utf8")]).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k, n]) => `${k} (${n}B)`).join(", ");
83312
+ throw new Error(`${name}: environment variables total ${bytes}B, over AWS's 4096B limit (${bytes - 4096}B too large). ` + `Largest: ${biggest}. Move large/secret values out of env — store them in Secrets Manager and fetch at cold start.`);
83313
+ }
83314
+ }
83285
83315
  async function applyFunction(lambda2, name, env2, code) {
83316
+ assertEnvWithinLimit(name, env2);
83286
83317
  await withConflictRetry(() => lambda2.updateFunctionConfiguration({ FunctionName: name, Environment: { Variables: env2 } }));
83287
83318
  await lambda2.waitForFunctionActive(name, 120);
83288
83319
  const codeParams = code.kind === "image" ? { FunctionName: name, ImageUri: code.imageUri } : { FunctionName: name, S3Bucket: code.bucket, S3Key: code.key };
@@ -83437,6 +83468,7 @@ async function deployServerlessApp(config6, environment, opts = {}) {
83437
83468
  code: codeSource,
83438
83469
  previousSha: prior?.sha,
83439
83470
  previousCode: prior?.code,
83471
+ previousFunctionEnv: prior?.functionEnv,
83440
83472
  functionEnv,
83441
83473
  functionNames: {
83442
83474
  http: composed.functionNames.http,
@@ -83508,14 +83540,17 @@ async function rollbackServerlessApp(config6, environment) {
83508
83540
  if (!name)
83509
83541
  continue;
83510
83542
  step(`Restoring ${mode} (${name})`);
83511
- await applyFunction(lambda2, name, release.functionEnv[mode] ?? {}, release.previousCode);
83543
+ const env2 = release.previousFunctionEnv?.[mode] ?? release.functionEnv[mode] ?? {};
83544
+ await applyFunction(lambda2, name, env2, release.previousCode);
83512
83545
  }
83513
83546
  await writeRelease(s32, ctx.artifactBucket, ctx.slug, environment, {
83514
83547
  ...release,
83515
83548
  sha: release.previousSha ?? release.sha,
83516
83549
  code: release.previousCode,
83550
+ functionEnv: release.previousFunctionEnv ?? release.functionEnv,
83517
83551
  previousSha: undefined,
83518
83552
  previousCode: undefined,
83553
+ previousFunctionEnv: undefined,
83519
83554
  timestamp: new Date().toISOString()
83520
83555
  });
83521
83556
  success("Rollback complete");
@@ -86720,6 +86755,7 @@ export {
86720
86755
  resolveServerlessArtifactBucketName,
86721
86756
  resolveServerlessAppStackName,
86722
86757
  resolveRegion,
86758
+ resolveQueues,
86723
86759
  resolveQueueNames,
86724
86760
  resolveProjectStackName,
86725
86761
  resolveObjectStorage,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/ts-cloud",
3
3
  "type": "module",
4
- "version": "0.5.4",
4
+ "version": "0.5.6",
5
5
  "description": "A lightweight, performant infrastructure-as-code library and CLI for deploying both server-based (EC2) and serverless applications.",
6
6
  "author": "Chris Breuer <chris@stacksjs.com>",
7
7
  "license": "MIT",
@@ -89,8 +89,8 @@
89
89
  "test": "bun test"
90
90
  },
91
91
  "dependencies": {
92
- "@ts-cloud/aws-types": "0.5.4",
93
- "@ts-cloud/core": "0.5.4",
92
+ "@ts-cloud/aws-types": "0.5.6",
93
+ "@ts-cloud/core": "0.5.6",
94
94
  "@stacksjs/ts-xml": "^0.1.0"
95
95
  },
96
96
  "devDependencies": {