@ts-cloud/core 0.2.27 → 0.3.0

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/index.js CHANGED
@@ -1670,6 +1670,15 @@ function resolveStorageBucketName(slug, environment, bucketKey, explicitBucket)
1670
1670
  function resolveDeployBucketName(slug, environment) {
1671
1671
  return `${slug}-${environment}-deploy`;
1672
1672
  }
1673
+ function resolveServerlessAppStackName(config, environment) {
1674
+ return `${config.project.slug}-${environment}-app`;
1675
+ }
1676
+ function resolveServerlessArtifactBucketName(slug, environment) {
1677
+ return `${slug}-${environment}-deployments`;
1678
+ }
1679
+ function resolveServerlessAssetBucketName(slug, environment) {
1680
+ return `${slug}-${environment}-assets`;
1681
+ }
1673
1682
  // src/dependency-graph.ts
1674
1683
  class DependencyGraph {
1675
1684
  nodes = new Map;
@@ -20632,6 +20641,45 @@ function createNodeJsServerlessPreset(options) {
20632
20641
  }
20633
20642
  };
20634
20643
  }
20644
+ // src/presets/serverless-node.ts
20645
+ function createServerlessNodePreset(options) {
20646
+ const {
20647
+ name,
20648
+ slug,
20649
+ entry,
20650
+ domain,
20651
+ runtime = "nodejs20.x",
20652
+ memory = 1024,
20653
+ build,
20654
+ deploy,
20655
+ assets,
20656
+ queues = true,
20657
+ scheduler = "on",
20658
+ region = "us-east-1"
20659
+ } = options;
20660
+ return {
20661
+ project: { name, slug, region },
20662
+ mode: "serverless",
20663
+ environments: {
20664
+ production: {
20665
+ type: "production",
20666
+ domain,
20667
+ app: {
20668
+ kind: "node",
20669
+ runtime,
20670
+ entry,
20671
+ memory,
20672
+ build,
20673
+ deploy,
20674
+ assets,
20675
+ queues,
20676
+ scheduler,
20677
+ domain
20678
+ }
20679
+ }
20680
+ }
20681
+ };
20682
+ }
20635
20683
  // src/presets/fullstack-app.ts
20636
20684
  function createFullStackAppPreset(options) {
20637
20685
  const {
@@ -22202,6 +22250,48 @@ function createLaravelPreset(options) {
22202
22250
  }
22203
22251
  };
22204
22252
  }
22253
+ // src/presets/serverless-laravel.ts
22254
+ function createServerlessLaravelPreset(options) {
22255
+ const {
22256
+ name,
22257
+ slug,
22258
+ domain,
22259
+ layers,
22260
+ phpVersion = "8.3",
22261
+ architecture = "x86_64",
22262
+ memory = 1024,
22263
+ build,
22264
+ deploy = ["migrate --force"],
22265
+ cache: cache2 = "dynamodb",
22266
+ scheduler = "on",
22267
+ region = "us-east-1"
22268
+ } = options;
22269
+ return {
22270
+ project: { name, slug, region },
22271
+ mode: "serverless",
22272
+ environments: {
22273
+ production: {
22274
+ type: "production",
22275
+ domain,
22276
+ app: {
22277
+ kind: "php",
22278
+ runtime: "provided.al2023",
22279
+ phpVersion,
22280
+ architecture,
22281
+ layers,
22282
+ memory,
22283
+ build,
22284
+ deploy,
22285
+ assets: "public",
22286
+ queues: true,
22287
+ scheduler,
22288
+ cache: { driver: cache2 },
22289
+ domain
22290
+ }
22291
+ }
22292
+ }
22293
+ };
22294
+ }
22205
22295
  // src/presets/dashboard.ts
22206
22296
  function createDashboardSite(options) {
22207
22297
  return {
@@ -25075,6 +25165,27 @@ var cloud_config_schema_default = {
25075
25165
  region: {
25076
25166
  type: "string",
25077
25167
  description: "AWS region override for this environment"
25168
+ },
25169
+ app: {
25170
+ type: "object",
25171
+ description: "Serverless application manifest (Laravel-Vapor-equivalent). Defining this opts the environment into the serverless app deploy pipeline (http/queue/cli Lambda functions, assets, build/deploy hooks).",
25172
+ properties: {
25173
+ runtime: { type: "string", description: "Lambda runtime (e.g. nodejs20.x, provided.al2023)" },
25174
+ kind: { type: "string", enum: ["node", "bun", "php"], description: "Application kind (drives packaging + runtime)" },
25175
+ entry: { type: "string", description: "Entry file exporting the request handler" },
25176
+ memory: { type: "number", description: "HTTP function memory in MB" },
25177
+ timeout: { type: "number", description: "HTTP request timeout in seconds" },
25178
+ gatewayVersion: { type: "number", enum: [1, 2], description: "API Gateway version (2 = HTTP API, 1 = REST)" },
25179
+ warm: { type: "number", description: "Keep-warm / provisioned concurrency count" },
25180
+ queues: { description: "Queue names (true = single default queue, false = disabled)" },
25181
+ scheduler: { type: "string", enum: ["off", "on", "sub-minute"], description: "Task scheduler mode" },
25182
+ build: { type: "array", items: { type: "string" }, description: "Commands run locally before packaging" },
25183
+ deploy: { type: "array", items: { type: "string" }, description: "Commands run remotely after activation (e.g. migrations)" },
25184
+ octane: { type: "boolean", description: "Persistent application mode (Laravel Octane)" },
25185
+ packaging: { type: "string", enum: ["zip", "image"], description: "Deployment package format (zip or container image)" },
25186
+ phpVersion: { type: "string", description: "PHP version for the runtime layer (kind: php)" },
25187
+ architecture: { type: "string", enum: ["x86_64", "arm64"], description: "CPU architecture" }
25188
+ }
25078
25189
  }
25079
25190
  }
25080
25191
  },
@@ -45025,6 +45136,1145 @@ class QueueManagementManager {
45025
45136
  }
45026
45137
  }
45027
45138
  var queueManagementManager = new QueueManagementManager;
45139
+ // src/serverless/zip.ts
45140
+ import { deflateRawSync } from "node:zlib";
45141
+ var CRC_TABLE = (() => {
45142
+ const table2 = [];
45143
+ for (let i = 0;i < 256; i++) {
45144
+ let c = i;
45145
+ for (let j = 0;j < 8; j++)
45146
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
45147
+ table2[i] = c >>> 0;
45148
+ }
45149
+ return table2;
45150
+ })();
45151
+ function crc32(data) {
45152
+ let crc = 4294967295;
45153
+ for (let i = 0;i < data.length; i++)
45154
+ crc = CRC_TABLE[(crc ^ data[i]) & 255] ^ crc >>> 8;
45155
+ return (crc ^ 4294967295) >>> 0;
45156
+ }
45157
+ function toBuffer(data) {
45158
+ if (typeof data === "string")
45159
+ return Buffer.from(data, "utf-8");
45160
+ if (Buffer.isBuffer(data))
45161
+ return data;
45162
+ return Buffer.from(data);
45163
+ }
45164
+ function dosTimeDate(date) {
45165
+ const year = Math.max(1980, date.getFullYear());
45166
+ const time = (date.getHours() << 11 | date.getMinutes() << 5 | date.getSeconds() >> 1) & 65535;
45167
+ const d = (year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate()) & 65535;
45168
+ return { time, date: d };
45169
+ }
45170
+ function createZip(entries) {
45171
+ const localParts = [];
45172
+ const centralParts = [];
45173
+ let offset = 0;
45174
+ for (const entry of entries) {
45175
+ const raw = toBuffer(entry.data);
45176
+ const compressed = deflateRawSync(raw);
45177
+ const crc = crc32(raw);
45178
+ const nameBuf = Buffer.from(entry.name.replace(/\\/g, "/"), "utf-8");
45179
+ const { time, date } = dosTimeDate(entry.date ?? new Date(0));
45180
+ const mode = entry.mode ?? 420;
45181
+ const local = Buffer.alloc(30 + nameBuf.length);
45182
+ local.writeUInt32LE(67324752, 0);
45183
+ local.writeUInt16LE(20, 4);
45184
+ local.writeUInt16LE(0, 6);
45185
+ local.writeUInt16LE(8, 8);
45186
+ local.writeUInt16LE(time, 10);
45187
+ local.writeUInt16LE(date, 12);
45188
+ local.writeUInt32LE(crc, 14);
45189
+ local.writeUInt32LE(compressed.length, 18);
45190
+ local.writeUInt32LE(raw.length, 22);
45191
+ local.writeUInt16LE(nameBuf.length, 26);
45192
+ local.writeUInt16LE(0, 28);
45193
+ nameBuf.copy(local, 30);
45194
+ const central = Buffer.alloc(46 + nameBuf.length);
45195
+ central.writeUInt32LE(33639248, 0);
45196
+ central.writeUInt16LE(798, 4);
45197
+ central.writeUInt16LE(20, 6);
45198
+ central.writeUInt16LE(0, 8);
45199
+ central.writeUInt16LE(8, 10);
45200
+ central.writeUInt16LE(time, 12);
45201
+ central.writeUInt16LE(date, 14);
45202
+ central.writeUInt32LE(crc, 16);
45203
+ central.writeUInt32LE(compressed.length, 20);
45204
+ central.writeUInt32LE(raw.length, 24);
45205
+ central.writeUInt16LE(nameBuf.length, 28);
45206
+ central.writeUInt16LE(0, 30);
45207
+ central.writeUInt16LE(0, 32);
45208
+ central.writeUInt16LE(0, 34);
45209
+ central.writeUInt16LE(0, 36);
45210
+ central.writeUInt32LE((mode & 65535) << 16, 38);
45211
+ central.writeUInt32LE(offset, 42);
45212
+ nameBuf.copy(central, 46);
45213
+ localParts.push(local, compressed);
45214
+ centralParts.push(central);
45215
+ offset += local.length + compressed.length;
45216
+ }
45217
+ const centralDir = Buffer.concat(centralParts);
45218
+ const end = Buffer.alloc(22);
45219
+ end.writeUInt32LE(101010256, 0);
45220
+ end.writeUInt16LE(0, 4);
45221
+ end.writeUInt16LE(0, 6);
45222
+ end.writeUInt16LE(entries.length, 8);
45223
+ end.writeUInt16LE(entries.length, 10);
45224
+ end.writeUInt32LE(centralDir.length, 12);
45225
+ end.writeUInt32LE(offset, 16);
45226
+ end.writeUInt16LE(0, 20);
45227
+ return Buffer.concat([...localParts, centralDir, end]);
45228
+ }
45229
+ // src/serverless/bootstrap.ts
45230
+ function generateBootstrap(opts) {
45231
+ const adapter = opts.adapterImport ?? "./adapter";
45232
+ const entry = opts.entryImport.replace(/\\/g, "/");
45233
+ return `// Generated by ts-cloud — serverless app bootstrap. Do not edit.
45234
+ import * as __userModule from ${JSON.stringify(entry)}
45235
+ import { resolveApp, createHandlers } from ${JSON.stringify(adapter)}
45236
+
45237
+ const __app = resolveApp(__userModule)
45238
+ const __handlers = createHandlers(__app)
45239
+
45240
+ export const http = __handlers.http
45241
+ export const queue = __handlers.queue
45242
+ export const cli = __handlers.cli
45243
+ `;
45244
+ }
45245
+ // src/serverless/package.ts
45246
+ import { execSync } from "node:child_process";
45247
+ import { createHash as createHash4 } from "node:crypto";
45248
+ import { cpSync, mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
45249
+ import { tmpdir } from "node:os";
45250
+ import { dirname as dirname2, isAbsolute, join as join6, resolve } from "node:path";
45251
+ import { fileURLToPath } from "node:url";
45252
+ function adapterSourcePath() {
45253
+ return join6(dirname2(fileURLToPath(import.meta.url)), "runtime", "adapter.ts");
45254
+ }
45255
+ function runBuildHooks(hooks, cwd, onStep) {
45256
+ for (const hook of hooks ?? []) {
45257
+ onStep?.(`build: ${hook}`);
45258
+ execSync(hook, { stdio: "inherit", cwd });
45259
+ }
45260
+ }
45261
+ function sha256(data) {
45262
+ return createHash4("sha256").update(data).digest("hex");
45263
+ }
45264
+ function artifactKey(slug, environment, hash2) {
45265
+ return `deployments/${slug}/${environment}/${hash2}.zip`;
45266
+ }
45267
+ async function packageServerlessApp(opts) {
45268
+ const projectRoot = resolve(opts.projectRoot ?? process.cwd());
45269
+ const { app } = opts;
45270
+ if (!opts.skipBuild)
45271
+ runBuildHooks(app.build, projectRoot, opts.onStep);
45272
+ const entry = app.entry;
45273
+ if (!entry)
45274
+ throw new Error("serverless app: `entry` is required to package a Node/Bun application");
45275
+ const entryPath = isAbsolute(entry) ? entry : join6(projectRoot, entry);
45276
+ const stage = mkdtempSync(join6(tmpdir(), "tscloud-pkg-"));
45277
+ try {
45278
+ cpSync(adapterSourcePath(), join6(stage, "adapter.ts"));
45279
+ const bootstrapPath = join6(stage, "bootstrap.ts");
45280
+ writeFileSync3(bootstrapPath, generateBootstrap({ entryImport: entryPath, adapterImport: "./adapter" }));
45281
+ opts.onStep?.("bundling application");
45282
+ const result = await Bun.build({
45283
+ entrypoints: [bootstrapPath],
45284
+ target: "node",
45285
+ format: "esm",
45286
+ minify: false,
45287
+ sourcemap: "none"
45288
+ });
45289
+ if (!result.success) {
45290
+ const logs = result.logs.map((l) => String(l)).join(`
45291
+ `);
45292
+ throw new Error(`serverless app bundle failed:
45293
+ ${logs}`);
45294
+ }
45295
+ const output = result.outputs[0];
45296
+ const bundle = Buffer.from(await output.arrayBuffer());
45297
+ const handlerFile = "index";
45298
+ const zip = createZip([{ name: `${handlerFile}.mjs`, data: bundle }]);
45299
+ return {
45300
+ zip,
45301
+ bundle,
45302
+ sha256: sha256(zip),
45303
+ handlerFile,
45304
+ handlers: {
45305
+ http: app.handlers?.http ?? `${handlerFile}.http`,
45306
+ queue: app.handlers?.queue ?? `${handlerFile}.queue`,
45307
+ cli: app.handlers?.cli ?? `${handlerFile}.cli`
45308
+ },
45309
+ bundleBytes: bundle.length
45310
+ };
45311
+ } finally {
45312
+ rmSync(stage, { recursive: true, force: true });
45313
+ }
45314
+ }
45315
+ // src/serverless/composer.ts
45316
+ function resolveQueueNames(app, slug, env) {
45317
+ if (app.queues === false)
45318
+ return [];
45319
+ if (app.queues === undefined || app.queues === true)
45320
+ return [`${slug}-${env}-default`];
45321
+ return app.queues.map((q) => {
45322
+ const name = typeof q === "string" ? q : Object.keys(q)[0];
45323
+ return `${slug}-${env}-${name}`;
45324
+ });
45325
+ }
45326
+ function composeServerlessAppTemplate(opts) {
45327
+ const { app, environment, handlers } = opts;
45328
+ const slug = opts.config.project.slug;
45329
+ const runtime = app.runtime ?? "nodejs20.x";
45330
+ const architecture = app.architecture ?? "x86_64";
45331
+ const region = opts.config.project.region;
45332
+ const functionNames = {
45333
+ http: `${slug}-${environment}-http`,
45334
+ queue: `${slug}-${environment}-queue`,
45335
+ cli: `${slug}-${environment}-cli`
45336
+ };
45337
+ const queueNames = resolveQueueNames(app, slug, environment);
45338
+ const hasQueue = queueNames.length > 0;
45339
+ const imageMode = app.packaging === "image";
45340
+ const schedulerEnabled = (app.scheduler ?? "on") !== "off";
45341
+ const cacheEnabled = (app.cache?.driver ?? "dynamodb") === "dynamodb";
45342
+ const assetsEnabled = Boolean(app.assets);
45343
+ const assetsBucket = resolveServerlessAssetBucketName(slug, environment);
45344
+ const tmpStorage = app.tmpStorage ?? 512;
45345
+ const resources = {};
45346
+ const outputs = {};
45347
+ const inlinePolicies = [
45348
+ {
45349
+ PolicyName: "tscloud-serverless-app",
45350
+ PolicyDocument: {
45351
+ Version: "2012-10-17",
45352
+ Statement: [
45353
+ {
45354
+ Effect: "Allow",
45355
+ Action: ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
45356
+ Resource: Fn2.sub("arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*")
45357
+ },
45358
+ {
45359
+ Effect: "Allow",
45360
+ Action: ["lambda:InvokeFunction"],
45361
+ Resource: Fn2.sub("arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:" + `${slug}-${environment}-*`)
45362
+ },
45363
+ {
45364
+ Effect: "Allow",
45365
+ Action: ["secretsmanager:GetSecretValue", "ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"],
45366
+ Resource: [
45367
+ Fn2.sub("arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:" + `${slug}/${environment}/*`),
45368
+ Fn2.sub("arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/" + `${slug}/${environment}/*`)
45369
+ ]
45370
+ },
45371
+ {
45372
+ Effect: "Allow",
45373
+ Action: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
45374
+ Resource: [
45375
+ Fn2.sub(`arn:aws:s3:::${assetsBucket}`),
45376
+ Fn2.sub(`arn:aws:s3:::${assetsBucket}/*`),
45377
+ ...app.storage?.bucket ? [Fn2.sub(`arn:aws:s3:::${app.storage.bucket}`), Fn2.sub(`arn:aws:s3:::${app.storage.bucket}/*`)] : []
45378
+ ]
45379
+ }
45380
+ ]
45381
+ }
45382
+ }
45383
+ ];
45384
+ if (hasQueue) {
45385
+ inlinePolicies[0].PolicyDocument.Statement.push({
45386
+ Effect: "Allow",
45387
+ Action: ["sqs:SendMessage", "sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes", "sqs:GetQueueUrl"],
45388
+ Resource: Fn2.sub("arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:" + `${slug}-${environment}-*`)
45389
+ });
45390
+ }
45391
+ if (cacheEnabled) {
45392
+ inlinePolicies[0].PolicyDocument.Statement.push({
45393
+ Effect: "Allow",
45394
+ Action: ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem"],
45395
+ Resource: Fn2.sub("arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/" + `${slug}-${environment}-cache*`)
45396
+ });
45397
+ }
45398
+ const managedPolicies = ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"];
45399
+ if (app.vpc?.subnets?.length)
45400
+ managedPolicies.push("arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole");
45401
+ resources.AppRole = {
45402
+ Type: "AWS::IAM::Role",
45403
+ Properties: {
45404
+ RoleName: `${slug}-${environment}-app-role`,
45405
+ AssumeRolePolicyDocument: {
45406
+ Version: "2012-10-17",
45407
+ Statement: [{ Effect: "Allow", Principal: { Service: "lambda.amazonaws.com" }, Action: "sts:AssumeRole" }]
45408
+ },
45409
+ ManagedPolicyArns: managedPolicies,
45410
+ Policies: inlinePolicies
45411
+ }
45412
+ };
45413
+ const baseEnv = (mode) => ({
45414
+ TSCLOUD_LAMBDA_MODE: mode,
45415
+ TSCLOUD_ENV: environment,
45416
+ ...app.octane ? { TSCLOUD_OCTANE: "1" } : {},
45417
+ ...cacheEnabled ? { TSCLOUD_CACHE_TABLE: `${slug}-${environment}-cache` } : {},
45418
+ ...hasQueue ? { TSCLOUD_QUEUE: queueNames[0] } : {},
45419
+ ...app.env ?? {}
45420
+ });
45421
+ const subnets = app.vpc?.subnets ?? [];
45422
+ const hasVpc = subnets.length > 0;
45423
+ const needsDataVpc = app.cache?.driver === "elasticache" || app.database?.connection === "aurora-serverless" || Boolean(app.rdsProxy);
45424
+ if (needsDataVpc && !hasVpc) {
45425
+ throw new Error("serverless app: elasticache / aurora-serverless / rdsProxy require app.vpc.subnets (private subnets) to be set.");
45426
+ }
45427
+ const vpcConfig = hasVpc ? {
45428
+ VpcConfig: {
45429
+ SubnetIds: subnets,
45430
+ SecurityGroupIds: [
45431
+ ...app.vpc?.securityGroups ?? [],
45432
+ ...needsDataVpc ? [Fn2.getAtt("DataSecurityGroup", "GroupId")] : []
45433
+ ]
45434
+ }
45435
+ } : {};
45436
+ function addFunction(logicalId, name, handler8, mode, memory, timeout, reservedConcurrency) {
45437
+ resources[`${logicalId}LogGroup`] = {
45438
+ Type: "AWS::Logs::LogGroup",
45439
+ Properties: { LogGroupName: `/aws/lambda/${name}`, RetentionInDays: 14 }
45440
+ };
45441
+ const codeProps = imageMode ? {
45442
+ PackageType: "Image",
45443
+ Code: { ImageUri: Fn2.ref("ImageUri") },
45444
+ ...app.kind === "php" ? {} : { ImageConfig: { Command: [handler8] } }
45445
+ } : {
45446
+ Runtime: runtime,
45447
+ Handler: handler8,
45448
+ Code: { S3Bucket: Fn2.ref("ArtifactBucket"), S3Key: Fn2.ref("ArtifactKey") },
45449
+ ...opts.runtimeLayers?.length ? { Layers: opts.runtimeLayers } : {}
45450
+ };
45451
+ resources[logicalId] = {
45452
+ Type: "AWS::Lambda::Function",
45453
+ DependsOn: [`${logicalId}LogGroup`],
45454
+ Properties: {
45455
+ FunctionName: name,
45456
+ Architectures: [architecture],
45457
+ MemorySize: memory,
45458
+ Timeout: timeout,
45459
+ Role: Fn2.getAtt("AppRole", "Arn"),
45460
+ Environment: { Variables: baseEnv(mode) },
45461
+ EphemeralStorage: { Size: tmpStorage },
45462
+ ...codeProps,
45463
+ ...reservedConcurrency !== undefined ? { ReservedConcurrentExecutions: reservedConcurrency } : {},
45464
+ ...vpcConfig
45465
+ }
45466
+ };
45467
+ }
45468
+ addFunction("HttpFunction", functionNames.http, handlers.http, "http", app.memory ?? 1024, app.timeout ?? 28, app.concurrency);
45469
+ addFunction("CliFunction", functionNames.cli, handlers.cli, "cli", app.cliMemory ?? 1024, app.cliTimeout ?? 900);
45470
+ if (hasQueue)
45471
+ addFunction("QueueFunction", functionNames.queue, handlers.queue, "queue", app.queueMemory ?? 1024, app.queueTimeout ?? 120);
45472
+ resources.HttpApi = {
45473
+ Type: "AWS::ApiGatewayV2::Api",
45474
+ Properties: {
45475
+ Name: `${slug}-${environment}`,
45476
+ ProtocolType: "HTTP"
45477
+ }
45478
+ };
45479
+ resources.HttpIntegration = {
45480
+ Type: "AWS::ApiGatewayV2::Integration",
45481
+ Properties: {
45482
+ ApiId: Fn2.ref("HttpApi"),
45483
+ IntegrationType: "AWS_PROXY",
45484
+ IntegrationUri: Fn2.getAtt("HttpFunction", "Arn"),
45485
+ PayloadFormatVersion: "2.0"
45486
+ }
45487
+ };
45488
+ resources.HttpRoute = {
45489
+ Type: "AWS::ApiGatewayV2::Route",
45490
+ Properties: {
45491
+ ApiId: Fn2.ref("HttpApi"),
45492
+ RouteKey: "$default",
45493
+ Target: Fn2.join("/", ["integrations", Fn2.ref("HttpIntegration")])
45494
+ }
45495
+ };
45496
+ resources.HttpStage = {
45497
+ Type: "AWS::ApiGatewayV2::Stage",
45498
+ Properties: {
45499
+ ApiId: Fn2.ref("HttpApi"),
45500
+ StageName: "$default",
45501
+ AutoDeploy: true
45502
+ }
45503
+ };
45504
+ resources.HttpPermission = {
45505
+ Type: "AWS::Lambda::Permission",
45506
+ Properties: {
45507
+ FunctionName: Fn2.ref("HttpFunction"),
45508
+ Action: "lambda:InvokeFunction",
45509
+ Principal: "apigateway.amazonaws.com",
45510
+ SourceArn: Fn2.sub("arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${HttpApi}/*/*")
45511
+ }
45512
+ };
45513
+ outputs.HttpApiEndpoint = {
45514
+ Description: "HTTP API endpoint",
45515
+ Value: Fn2.getAtt("HttpApi", "ApiEndpoint")
45516
+ };
45517
+ outputs.HttpApiId = { Description: "HTTP API id", Value: Fn2.ref("HttpApi") };
45518
+ if (hasQueue) {
45519
+ resources.AppQueueDlq = {
45520
+ Type: "AWS::SQS::Queue",
45521
+ Properties: {
45522
+ QueueName: `${slug}-${environment}-dlq`,
45523
+ MessageRetentionPeriod: 1209600
45524
+ }
45525
+ };
45526
+ queueNames.forEach((qName, i) => {
45527
+ const qId = `AppQueue${i}`;
45528
+ resources[qId] = {
45529
+ Type: "AWS::SQS::Queue",
45530
+ Properties: {
45531
+ QueueName: qName,
45532
+ VisibilityTimeout: Math.max(app.queueTimeout ?? 120, app.queueTimeout ?? 120),
45533
+ RedrivePolicy: {
45534
+ deadLetterTargetArn: Fn2.getAtt("AppQueueDlq", "Arn"),
45535
+ maxReceiveCount: app.queueTries ?? 3
45536
+ }
45537
+ }
45538
+ };
45539
+ resources[`${qId}Mapping`] = {
45540
+ Type: "AWS::Lambda::EventSourceMapping",
45541
+ Properties: {
45542
+ EventSourceArn: Fn2.getAtt(qId, "Arn"),
45543
+ FunctionName: Fn2.ref("QueueFunction"),
45544
+ BatchSize: 1,
45545
+ FunctionResponseTypes: ["ReportBatchItemFailures"],
45546
+ ...app.queueConcurrency ? { ScalingConfig: { MaximumConcurrency: Math.max(2, app.queueConcurrency) } } : {}
45547
+ }
45548
+ };
45549
+ outputs[`QueueUrl${i}`] = { Description: `Queue URL: ${qName}`, Value: Fn2.ref(qId) };
45550
+ });
45551
+ }
45552
+ if (schedulerEnabled) {
45553
+ resources.SchedulerRule = {
45554
+ Type: "AWS::Events::Rule",
45555
+ Properties: {
45556
+ Name: `${slug}-${environment}-scheduler`,
45557
+ ScheduleExpression: "rate(1 minute)",
45558
+ State: "ENABLED",
45559
+ Targets: [{
45560
+ Id: "cli",
45561
+ Arn: Fn2.getAtt("CliFunction", "Arn"),
45562
+ Input: JSON.stringify({ command: "schedule:run" })
45563
+ }]
45564
+ }
45565
+ };
45566
+ resources.SchedulerPermission = {
45567
+ Type: "AWS::Lambda::Permission",
45568
+ Properties: {
45569
+ FunctionName: Fn2.ref("CliFunction"),
45570
+ Action: "lambda:InvokeFunction",
45571
+ Principal: "events.amazonaws.com",
45572
+ SourceArn: Fn2.getAtt("SchedulerRule", "Arn")
45573
+ }
45574
+ };
45575
+ }
45576
+ if (app.warm && app.warm > 0) {
45577
+ const TARGETS_PER_RULE = 5;
45578
+ const ruleCount = Math.ceil(app.warm / TARGETS_PER_RULE);
45579
+ let warmed = 0;
45580
+ for (let r = 0;r < ruleCount; r++) {
45581
+ const targets = Math.min(TARGETS_PER_RULE, app.warm - warmed);
45582
+ resources[`WarmerRule${r}`] = {
45583
+ Type: "AWS::Events::Rule",
45584
+ Properties: {
45585
+ Name: `${slug}-${environment}-warmer-${r}`,
45586
+ ScheduleExpression: "rate(5 minutes)",
45587
+ State: "ENABLED",
45588
+ Targets: Array.from({ length: targets }, (_, i) => ({
45589
+ Id: `warm-${r}-${i}`,
45590
+ Arn: Fn2.getAtt("HttpFunction", "Arn"),
45591
+ Input: JSON.stringify({ warmer: true })
45592
+ }))
45593
+ }
45594
+ };
45595
+ warmed += targets;
45596
+ }
45597
+ resources.WarmerPermission = {
45598
+ Type: "AWS::Lambda::Permission",
45599
+ Properties: {
45600
+ FunctionName: Fn2.ref("HttpFunction"),
45601
+ Action: "lambda:InvokeFunction",
45602
+ Principal: "events.amazonaws.com",
45603
+ SourceArn: Fn2.sub("arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/" + `${slug}-${environment}-warmer-*`)
45604
+ }
45605
+ };
45606
+ }
45607
+ if (cacheEnabled) {
45608
+ resources.CacheTable = {
45609
+ Type: "AWS::DynamoDB::Table",
45610
+ Properties: {
45611
+ TableName: `${slug}-${environment}-cache`,
45612
+ BillingMode: "PAY_PER_REQUEST",
45613
+ AttributeDefinitions: [{ AttributeName: "key", AttributeType: "S" }],
45614
+ KeySchema: [{ AttributeName: "key", KeyType: "HASH" }],
45615
+ TimeToLiveSpecification: { AttributeName: "expires_at", Enabled: true }
45616
+ }
45617
+ };
45618
+ outputs.CacheTableName = { Description: "DynamoDB cache table", Value: Fn2.ref("CacheTable") };
45619
+ }
45620
+ if (assetsEnabled) {
45621
+ resources.AssetsBucket = {
45622
+ Type: "AWS::S3::Bucket",
45623
+ Properties: {
45624
+ BucketName: assetsBucket,
45625
+ PublicAccessBlockConfiguration: {
45626
+ BlockPublicAcls: true,
45627
+ BlockPublicPolicy: true,
45628
+ IgnorePublicAcls: true,
45629
+ RestrictPublicBuckets: true
45630
+ }
45631
+ }
45632
+ };
45633
+ resources.AssetsOAC = {
45634
+ Type: "AWS::CloudFront::OriginAccessControl",
45635
+ Properties: {
45636
+ OriginAccessControlConfig: {
45637
+ Name: `${slug}-${environment}-assets-oac`,
45638
+ OriginAccessControlOriginType: "s3",
45639
+ SigningBehavior: "always",
45640
+ SigningProtocol: "sigv4"
45641
+ }
45642
+ }
45643
+ };
45644
+ resources.AssetsDistribution = {
45645
+ Type: "AWS::CloudFront::Distribution",
45646
+ Properties: {
45647
+ DistributionConfig: {
45648
+ Enabled: true,
45649
+ DefaultCacheBehavior: {
45650
+ TargetOriginId: "assets",
45651
+ ViewerProtocolPolicy: "redirect-to-https",
45652
+ Compress: true,
45653
+ CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6"
45654
+ },
45655
+ Origins: [{
45656
+ Id: "assets",
45657
+ DomainName: Fn2.getAtt("AssetsBucket", "RegionalDomainName"),
45658
+ OriginAccessControlId: Fn2.ref("AssetsOAC"),
45659
+ S3OriginConfig: { OriginAccessIdentity: "" }
45660
+ }]
45661
+ }
45662
+ }
45663
+ };
45664
+ resources.AssetsBucketPolicy = {
45665
+ Type: "AWS::S3::BucketPolicy",
45666
+ Properties: {
45667
+ Bucket: Fn2.ref("AssetsBucket"),
45668
+ PolicyDocument: {
45669
+ Version: "2012-10-17",
45670
+ Statement: [{
45671
+ Effect: "Allow",
45672
+ Principal: { Service: "cloudfront.amazonaws.com" },
45673
+ Action: "s3:GetObject",
45674
+ Resource: Fn2.sub(`arn:aws:s3:::${assetsBucket}/*`),
45675
+ Condition: { StringEquals: { "AWS:SourceArn": Fn2.sub("arn:aws:cloudfront::${AWS::AccountId}:distribution/${AssetsDistribution}") } }
45676
+ }]
45677
+ }
45678
+ }
45679
+ };
45680
+ outputs.AssetsBucketName = { Description: "Assets bucket", Value: Fn2.ref("AssetsBucket") };
45681
+ outputs.AssetsCdnDomain = { Description: "Assets CloudFront domain", Value: Fn2.getAtt("AssetsDistribution", "DomainName") };
45682
+ }
45683
+ if (app.firewall?.enabled) {
45684
+ const wafRules = [];
45685
+ let priority = 0;
45686
+ if (app.firewall.rateLimit) {
45687
+ wafRules.push({
45688
+ Name: "rate-limit",
45689
+ Priority: priority++,
45690
+ Action: { Block: {} },
45691
+ Statement: { RateBasedStatement: { Limit: app.firewall.rateLimit, AggregateKeyType: "IP" } },
45692
+ VisibilityConfig: { SampledRequestsEnabled: true, CloudWatchMetricsEnabled: true, MetricName: `${slug}-${environment}-rate` }
45693
+ });
45694
+ }
45695
+ const managed = {
45696
+ sqlInjection: "AWSManagedRulesSQLiRuleSet",
45697
+ xss: "AWSManagedRulesCommonRuleSet",
45698
+ common: "AWSManagedRulesCommonRuleSet",
45699
+ botControl: "AWSManagedRulesBotControlRuleSet",
45700
+ ipReputation: "AWSManagedRulesAmazonIpReputationList"
45701
+ };
45702
+ for (const rule of app.firewall.rules ?? []) {
45703
+ const name = managed[rule];
45704
+ if (!name)
45705
+ continue;
45706
+ wafRules.push({
45707
+ Name: `managed-${rule}`,
45708
+ Priority: priority++,
45709
+ OverrideAction: { None: {} },
45710
+ Statement: { ManagedRuleGroupStatement: { VendorName: "AWS", Name: name } },
45711
+ VisibilityConfig: { SampledRequestsEnabled: true, CloudWatchMetricsEnabled: true, MetricName: `${slug}-${environment}-${rule}` }
45712
+ });
45713
+ }
45714
+ resources.WebAcl = {
45715
+ Type: "AWS::WAFv2::WebACL",
45716
+ Properties: {
45717
+ Name: `${slug}-${environment}-waf`,
45718
+ Scope: "REGIONAL",
45719
+ DefaultAction: { Allow: {} },
45720
+ Rules: wafRules,
45721
+ VisibilityConfig: { SampledRequestsEnabled: true, CloudWatchMetricsEnabled: true, MetricName: `${slug}-${environment}-waf` }
45722
+ }
45723
+ };
45724
+ resources.WebAclAssociation = {
45725
+ Type: "AWS::WAFv2::WebACLAssociation",
45726
+ DependsOn: ["HttpStage"],
45727
+ Properties: {
45728
+ ResourceArn: Fn2.sub("arn:aws:apigateway:${AWS::Region}::/apis/${HttpApi}/stages/$default"),
45729
+ WebACLArn: Fn2.getAtt("WebAcl", "Arn")
45730
+ }
45731
+ };
45732
+ }
45733
+ if (hasVpc && needsDataVpc) {
45734
+ resources.DataSecurityGroup = {
45735
+ Type: "AWS::EC2::SecurityGroup",
45736
+ Properties: {
45737
+ GroupDescription: `${slug}-${environment} serverless data access`,
45738
+ SecurityGroupIngress: [{ IpProtocol: "-1", CidrIp: "10.0.0.0/8" }]
45739
+ }
45740
+ };
45741
+ }
45742
+ if (app.cache?.driver === "elasticache") {
45743
+ resources.CacheSubnetGroup = {
45744
+ Type: "AWS::ElastiCache::SubnetGroup",
45745
+ Properties: { Description: `${slug}-${environment} cache subnets`, SubnetIds: subnets }
45746
+ };
45747
+ resources.CacheCluster = {
45748
+ Type: "AWS::ElastiCache::ReplicationGroup",
45749
+ Properties: {
45750
+ ReplicationGroupId: `${slug}-${environment}-cache`,
45751
+ ReplicationGroupDescription: `${slug}-${environment} redis`,
45752
+ Engine: "redis",
45753
+ CacheNodeType: "cache.t4g.micro",
45754
+ NumCacheClusters: 1,
45755
+ AutomaticFailoverEnabled: false,
45756
+ CacheSubnetGroupName: Fn2.ref("CacheSubnetGroup"),
45757
+ SecurityGroupIds: [Fn2.getAtt("DataSecurityGroup", "GroupId")],
45758
+ TransitEncryptionEnabled: false
45759
+ }
45760
+ };
45761
+ outputs.CacheEndpoint = { Description: "Redis primary endpoint", Value: Fn2.getAtt("CacheCluster", "PrimaryEndPoint.Address") };
45762
+ }
45763
+ if (app.database?.connection === "aurora-serverless") {
45764
+ resources.DbSubnetGroup = {
45765
+ Type: "AWS::RDS::DBSubnetGroup",
45766
+ Properties: { DBSubnetGroupDescription: `${slug}-${environment} db subnets`, SubnetIds: subnets }
45767
+ };
45768
+ resources.DbSecret = {
45769
+ Type: "AWS::SecretsManager::Secret",
45770
+ Properties: {
45771
+ Name: `${slug}/${environment}/db`,
45772
+ GenerateSecretString: {
45773
+ SecretStringTemplate: JSON.stringify({ username: "app" }),
45774
+ GenerateStringKey: "password",
45775
+ PasswordLength: 32,
45776
+ ExcludePunctuation: true
45777
+ }
45778
+ }
45779
+ };
45780
+ resources.DbCluster = {
45781
+ Type: "AWS::RDS::DBCluster",
45782
+ Properties: {
45783
+ Engine: "aurora-mysql",
45784
+ EngineMode: "provisioned",
45785
+ DBClusterIdentifier: `${slug}-${environment}-db`,
45786
+ MasterUsername: Fn2.sub("{{resolve:secretsmanager:${DbSecret}:SecretString:username}}"),
45787
+ MasterUserPassword: Fn2.sub("{{resolve:secretsmanager:${DbSecret}:SecretString:password}}"),
45788
+ ServerlessV2ScalingConfiguration: { MinCapacity: 0.5, MaxCapacity: 4 },
45789
+ DBSubnetGroupName: Fn2.ref("DbSubnetGroup"),
45790
+ VpcSecurityGroupIds: [Fn2.getAtt("DataSecurityGroup", "GroupId")]
45791
+ }
45792
+ };
45793
+ resources.DbInstance = {
45794
+ Type: "AWS::RDS::DBInstance",
45795
+ Properties: {
45796
+ Engine: "aurora-mysql",
45797
+ DBInstanceClass: "db.serverless",
45798
+ DBClusterIdentifier: Fn2.ref("DbCluster")
45799
+ }
45800
+ };
45801
+ outputs.DbEndpoint = { Description: "Aurora cluster endpoint", Value: Fn2.getAtt("DbCluster", "Endpoint.Address") };
45802
+ }
45803
+ if (app.rdsProxy && app.database?.connection === "aurora-serverless") {
45804
+ resources.DbProxyRole = {
45805
+ Type: "AWS::IAM::Role",
45806
+ Properties: {
45807
+ AssumeRolePolicyDocument: {
45808
+ Version: "2012-10-17",
45809
+ Statement: [{ Effect: "Allow", Principal: { Service: "rds.amazonaws.com" }, Action: "sts:AssumeRole" }]
45810
+ },
45811
+ Policies: [{
45812
+ PolicyName: "read-db-secret",
45813
+ PolicyDocument: {
45814
+ Version: "2012-10-17",
45815
+ Statement: [{ Effect: "Allow", Action: ["secretsmanager:GetSecretValue"], Resource: Fn2.ref("DbSecret") }]
45816
+ }
45817
+ }]
45818
+ }
45819
+ };
45820
+ resources.DbProxy = {
45821
+ Type: "AWS::RDS::DBProxy",
45822
+ Properties: {
45823
+ DBProxyName: typeof app.rdsProxy === "object" && app.rdsProxy.name ? app.rdsProxy.name : `${slug}-${environment}-proxy`,
45824
+ EngineFamily: "MYSQL",
45825
+ RoleArn: Fn2.getAtt("DbProxyRole", "Arn"),
45826
+ Auth: [{ AuthScheme: "SECRETS", SecretArn: Fn2.ref("DbSecret"), IAMAuth: "DISABLED" }],
45827
+ VpcSubnetIds: subnets,
45828
+ VpcSecurityGroupIds: [Fn2.getAtt("DataSecurityGroup", "GroupId")],
45829
+ RequireTLS: false
45830
+ }
45831
+ };
45832
+ outputs.DbProxyEndpoint = { Description: "RDS Proxy endpoint", Value: Fn2.getAtt("DbProxy", "Endpoint") };
45833
+ }
45834
+ outputs.HttpFunctionName = { Description: "HTTP function name", Value: Fn2.ref("HttpFunction") };
45835
+ outputs.CliFunctionName = { Description: "CLI function name", Value: Fn2.ref("CliFunction") };
45836
+ if (hasQueue)
45837
+ outputs.QueueFunctionName = { Description: "Queue function name", Value: Fn2.ref("QueueFunction") };
45838
+ const template = {
45839
+ AWSTemplateFormatVersion: "2010-09-09",
45840
+ Description: `Serverless application for ${opts.config.project.name} (${slug}-${environment})`,
45841
+ Parameters: imageMode ? {
45842
+ ImageUri: { Type: "String", Description: "ECR image URI of the deployment artifact" }
45843
+ } : {
45844
+ ArtifactBucket: { Type: "String", Description: "S3 bucket holding the deployment artifact" },
45845
+ ArtifactKey: { Type: "String", Description: "S3 key of the deployment artifact (zip)" }
45846
+ },
45847
+ Resources: resources,
45848
+ Outputs: outputs
45849
+ };
45850
+ const resourceSummary = {};
45851
+ for (const r of Object.values(resources)) {
45852
+ resourceSummary[r.Type] = (resourceSummary[r.Type] ?? 0) + 1;
45853
+ }
45854
+ return { template, functionNames, queueNames, resourceSummary };
45855
+ }
45856
+ // src/serverless-php/dockerfile.ts
45857
+ var PHP_LAYER_EXTENSIONS = [
45858
+ "cli",
45859
+ "fpm",
45860
+ "mbstring",
45861
+ "xml",
45862
+ "pdo",
45863
+ "mysqlnd",
45864
+ "gd",
45865
+ "bcmath",
45866
+ "intl",
45867
+ "opcache",
45868
+ "sodium",
45869
+ "process",
45870
+ "pecl-redis6",
45871
+ "pecl-apcu",
45872
+ "pgsql"
45873
+ ];
45874
+ function phpLayerPackages(phpVersion) {
45875
+ const scl = `php${phpVersion.replace(".", "")}`;
45876
+ return PHP_LAYER_EXTENSIONS.map((ext) => `${scl}-php-${ext}`);
45877
+ }
45878
+ function phpLayerBuildStage(phpVersion, asName) {
45879
+ const scl = `php${phpVersion.replace(".", "")}`;
45880
+ const packages = phpLayerPackages(phpVersion).join(" \\\n ");
45881
+ const sclRoot = `/opt/remi/${scl}/root`;
45882
+ const from = asName ? `FROM amazonlinux:2023 AS ${asName}` : "FROM amazonlinux:2023";
45883
+ return `${from}
45884
+
45885
+ # Remi provides version-isolated PHP SCL packages for EL9 (AL2023 compatible).
45886
+ RUN dnf -y install dnf-plugins-core 'dnf-command(config-manager)' && \\
45887
+ dnf -y install https://rpms.remirepo.net/enterprise/remi-release-9.rpm && \\
45888
+ dnf -y update && \\
45889
+ dnf -y install \\
45890
+ ${packages} \\
45891
+ findutils tar gzip && \\
45892
+ dnf clean all
45893
+
45894
+ # Relocate PHP + php-fpm + extensions and their shared libs under /opt.
45895
+ RUN set -eux; \\
45896
+ mkdir -p /opt/php/bin /opt/php/sbin /opt/php/lib /opt/php/lib/php/modules /opt/php/etc/php.d /opt/tscloud; \\
45897
+ cp ${sclRoot}/usr/bin/php /opt/php/bin/php; \\
45898
+ cp ${sclRoot}/usr/sbin/php-fpm /opt/php/sbin/php-fpm; \\
45899
+ EXT_DIR="$(${sclRoot}/usr/bin/php -r 'echo ini_get("extension_dir");')"; \\
45900
+ cp -a "$EXT_DIR"/*.so /opt/php/lib/php/modules/ || true; \\
45901
+ cp -a ${sclRoot}/etc/php.d/*.ini /opt/php/etc/php.d/ 2>/dev/null || true; \\
45902
+ # Copy shared-library dependencies of php, php-fpm, and the extension modules.
45903
+ for bin in /opt/php/bin/php /opt/php/sbin/php-fpm /opt/php/lib/php/modules/*.so; do \\
45904
+ ldd "$bin" 2>/dev/null | awk '/=>/{print $3}/ld-linux/{print $1}' | sort -u | while read -r lib; do \\
45905
+ [ -f "$lib" ] && cp -Ln "$lib" /opt/php/lib/ || true; \\
45906
+ done; \\
45907
+ done
45908
+
45909
+ # Point PHP at the relocated config + extension dir.
45910
+ RUN printf 'extension_dir=/opt/php/lib/php/modules\\n' > /opt/php/etc/php.ini && \\
45911
+ cat /opt/php/etc/php.d/*.ini >> /opt/php/etc/php.ini 2>/dev/null || true
45912
+ ENV PHP_INI_SCAN_DIR=/opt/php/etc/php.d
45913
+
45914
+ # Runtime assets (bootstrap, runtime loops, fpm config) are added by the build
45915
+ # orchestrator after the image is produced. Export /opt as the layer payload.
45916
+ CMD ["true"]
45917
+ `;
45918
+ }
45919
+ function generatePhpLayerDockerfile(options = {}) {
45920
+ const phpVersion = options.phpVersion ?? "8.3";
45921
+ return `# ts-cloud PHP ${phpVersion} runtime layer (generated) — AWS Lambda provided.al2023.
45922
+ ${phpLayerBuildStage(phpVersion)}`;
45923
+ }
45924
+
45925
+ // src/serverless/app-image.ts
45926
+ function generateAppImageDockerfile(options) {
45927
+ if (options.kind === "php") {
45928
+ const phpVersion = options.phpVersion ?? "8.3";
45929
+ return `# ts-cloud serverless PHP app image (generated, multi-stage).
45930
+ ${phpLayerBuildStage(phpVersion, "phpbuild")}
45931
+
45932
+ FROM public.ecr.aws/lambda/provided:al2023
45933
+ # Bake the relocated PHP runtime from the build stage.
45934
+ COPY --from=phpbuild /opt/ /opt/
45935
+ # ts-cloud runtime assets (bootstrap, runtime loops, fpm config).
45936
+ COPY runtime/ /opt/
45937
+ # The application source tree.
45938
+ COPY app/ /var/task/
45939
+ # /opt/bootstrap is the runtime entrypoint; mode comes from TSCLOUD_LAMBDA_MODE.
45940
+ ENTRYPOINT [ "/opt/bootstrap" ]
45941
+ `;
45942
+ }
45943
+ const nodeMajor = options.nodeMajor ?? "20";
45944
+ return `# ts-cloud serverless Node app image (generated).
45945
+ FROM public.ecr.aws/lambda/nodejs:${nodeMajor}
45946
+
45947
+ # The bundled handler artifact (index.mjs) at the Lambda task root.
45948
+ COPY app/ \${LAMBDA_TASK_ROOT}/
45949
+
45950
+ # Per-function CMD (e.g. index.http) is supplied via ImageConfig.Command.
45951
+ CMD [ "index.http" ]
45952
+ `;
45953
+ }
45954
+ // src/serverless/runtime/adapter.ts
45955
+ function resolveApp(mod) {
45956
+ const m = mod;
45957
+ const def = m?.default ?? m;
45958
+ if (typeof def === "function")
45959
+ return { fetch: def };
45960
+ return {
45961
+ fetch: def?.fetch ?? m?.fetch,
45962
+ queue: def?.queue ?? m?.queue,
45963
+ cli: def?.cli ?? m?.cli
45964
+ };
45965
+ }
45966
+ var TEXT_CONTENT = /^(?:text\/|application\/(?:json|xml|javascript|graphql|x-www-form-urlencoded|.*\+json|.*\+xml)|image\/svg)/i;
45967
+ function isTextContentType(contentType) {
45968
+ if (!contentType)
45969
+ return true;
45970
+ return TEXT_CONTENT.test(contentType);
45971
+ }
45972
+ function readMaintenance(opts) {
45973
+ if (opts?.maintenance)
45974
+ return opts.maintenance;
45975
+ const env = globalThis.process?.env ?? {};
45976
+ return {
45977
+ enabled: env.MAINTENANCE_MODE === "1" || env.MAINTENANCE_MODE === "true",
45978
+ bypassSecret: env.MAINTENANCE_BYPASS_SECRET
45979
+ };
45980
+ }
45981
+ function eventToRequest(event) {
45982
+ const host = event.requestContext?.domainName ?? "localhost";
45983
+ const query = event.rawQueryString ? `?${event.rawQueryString}` : "";
45984
+ const url = `https://${host}${event.rawPath || "/"}${query}`;
45985
+ const headers = new Headers;
45986
+ for (const [key, value] of Object.entries(event.headers ?? {})) {
45987
+ if (value !== undefined)
45988
+ headers.set(key, value);
45989
+ }
45990
+ if (event.cookies?.length)
45991
+ headers.set("cookie", event.cookies.join("; "));
45992
+ const method = event.requestContext.http.method;
45993
+ let body;
45994
+ if (event.body !== undefined && method !== "GET" && method !== "HEAD") {
45995
+ body = event.isBase64Encoded ? new Uint8Array(Buffer.from(event.body, "base64")) : new TextEncoder().encode(event.body);
45996
+ }
45997
+ return new Request(url, { method, headers, body });
45998
+ }
45999
+ async function responseToResult(response) {
46000
+ const headers = {};
46001
+ const cookies = [];
46002
+ const setCookies = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : [];
46003
+ for (const c of setCookies)
46004
+ cookies.push(c);
46005
+ response.headers.forEach((value, key) => {
46006
+ if (key.toLowerCase() === "set-cookie")
46007
+ return;
46008
+ headers[key] = value;
46009
+ });
46010
+ const buffer = Buffer.from(await response.arrayBuffer());
46011
+ const textual = isTextContentType(response.headers.get("content-type"));
46012
+ return {
46013
+ statusCode: response.status,
46014
+ headers,
46015
+ ...cookies.length ? { cookies } : {},
46016
+ body: textual ? buffer.toString("utf-8") : buffer.toString("base64"),
46017
+ isBase64Encoded: !textual
46018
+ };
46019
+ }
46020
+ function createHttpHandler(handler8, opts) {
46021
+ return async (event) => {
46022
+ if (event.warmer) {
46023
+ return { statusCode: 200, headers: { "content-type": "text/plain" }, body: "warm", isBase64Encoded: false };
46024
+ }
46025
+ if (!handler8) {
46026
+ return { statusCode: 501, headers: { "content-type": "text/plain" }, body: "No HTTP handler configured", isBase64Encoded: false };
46027
+ }
46028
+ const maintenance = readMaintenance(opts);
46029
+ if (maintenance.enabled) {
46030
+ const bypass = event.headers?.["x-maintenance-bypass"] ?? event.cookies?.find((c) => c.startsWith("tscloud_bypass="))?.split("=")[1];
46031
+ if (!maintenance.bypassSecret || bypass !== maintenance.bypassSecret) {
46032
+ return {
46033
+ statusCode: 503,
46034
+ headers: { "content-type": "text/plain", "retry-after": "120" },
46035
+ body: "Service temporarily unavailable (maintenance mode)",
46036
+ isBase64Encoded: false
46037
+ };
46038
+ }
46039
+ }
46040
+ const request = eventToRequest(event);
46041
+ const response = await handler8(request);
46042
+ return responseToResult(response);
46043
+ };
46044
+ }
46045
+ function parseRecordBody(body) {
46046
+ try {
46047
+ return JSON.parse(body);
46048
+ } catch {
46049
+ return body;
46050
+ }
46051
+ }
46052
+ function createQueueHandler(handler8) {
46053
+ return async (event) => {
46054
+ const batchItemFailures = [];
46055
+ if (!handler8)
46056
+ return { batchItemFailures };
46057
+ for (const record of event.Records ?? []) {
46058
+ try {
46059
+ await handler8(parseRecordBody(record.body), record);
46060
+ } catch {
46061
+ batchItemFailures.push({ itemIdentifier: record.messageId });
46062
+ }
46063
+ }
46064
+ return { batchItemFailures };
46065
+ };
46066
+ }
46067
+ function createCliHandler(handler8) {
46068
+ return async (event) => {
46069
+ if (!handler8)
46070
+ return { statusCode: 501, output: "No CLI handler configured" };
46071
+ return handler8(event);
46072
+ };
46073
+ }
46074
+ function createHandlers(app, opts) {
46075
+ return {
46076
+ http: createHttpHandler(app.fetch, opts),
46077
+ queue: createQueueHandler(app.queue),
46078
+ cli: createCliHandler(app.cli)
46079
+ };
46080
+ }
46081
+ // src/serverless-php/php-fpm-conf.ts
46082
+ function generatePhpFpmConfig(options = {}) {
46083
+ const socket = options.socketPath ?? "/tmp/.tscloud-fpm.sock";
46084
+ const maxChildren = options.maxChildren ?? 1;
46085
+ const errorLog = options.errorLog ?? "/tmp/storage/logs/php-fpm.log";
46086
+ return `; ts-cloud php-fpm configuration (generated) — AWS Lambda custom runtime.
46087
+ [global]
46088
+ daemonize = no
46089
+ error_log = ${errorLog}
46090
+ log_level = warning
46091
+
46092
+ [www]
46093
+ listen = ${socket}
46094
+ listen.mode = 0666
46095
+ pm = static
46096
+ pm.max_children = ${maxChildren}
46097
+ catch_workers_output = yes
46098
+ decorate_workers_output = no
46099
+ ; Surface fatal errors / fpm logs to the Lambda log stream.
46100
+ php_admin_value[error_log] = /dev/stderr
46101
+ php_admin_flag[log_errors] = on
46102
+ clear_env = no
46103
+ `;
46104
+ }
46105
+ // src/serverless-php/runtime-assets.ts
46106
+ import { readFileSync as readFileSync4 } from "node:fs";
46107
+ import { dirname as dirname3, join as join7 } from "node:path";
46108
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
46109
+ function assetsDir() {
46110
+ return join7(dirname3(fileURLToPath2(import.meta.url)), "runtime-assets");
46111
+ }
46112
+ function phpRuntimeLayerAssets() {
46113
+ const dir = assetsDir();
46114
+ const read = (f) => readFileSync4(join7(dir, f), "utf-8");
46115
+ return [
46116
+ { path: "bootstrap", contents: read("bootstrap"), mode: 493 },
46117
+ { path: "tscloud/runtime.php", contents: read("runtime.php"), mode: 420 },
46118
+ { path: "tscloud/octane-runtime.php", contents: read("octane-runtime.php"), mode: 420 },
46119
+ { path: "tscloud/cli-runtime.php", contents: read("cli-runtime.php"), mode: 420 },
46120
+ { path: "tscloud/fastcgi-client.php", contents: read("fastcgi-client.php"), mode: 420 },
46121
+ { path: "tscloud/php-fpm.conf", contents: generatePhpFpmConfig(), mode: 420 }
46122
+ ];
46123
+ }
46124
+ function laravelServerlessEnvDefaults(opts = {}) {
46125
+ const cache2 = opts.cacheDriver ?? "dynamodb";
46126
+ return {
46127
+ APP_ENV: "production",
46128
+ LOG_CHANNEL: "stderr",
46129
+ CACHE_STORE: cache2,
46130
+ CACHE_DRIVER: cache2,
46131
+ SESSION_DRIVER: cache2,
46132
+ QUEUE_CONNECTION: "sqs",
46133
+ FILESYSTEM_DISK: "s3",
46134
+ VIEW_COMPILED_PATH: "/tmp/storage/framework/views",
46135
+ APP_SERVICES_CACHE: "/tmp/bootstrap/cache/services.php",
46136
+ APP_PACKAGES_CACHE: "/tmp/bootstrap/cache/packages.php",
46137
+ APP_CONFIG_CACHE: "/tmp/bootstrap/cache/config.php",
46138
+ APP_ROUTES_CACHE: "/tmp/bootstrap/cache/routes.php",
46139
+ APP_EVENTS_CACHE: "/tmp/bootstrap/cache/events.php"
46140
+ };
46141
+ }
46142
+ var LARAVEL_SERVERLESS_BUILD_STEPS = [
46143
+ "composer install --no-dev --optimize-autoloader --no-interaction",
46144
+ "php artisan config:cache",
46145
+ "php artisan route:cache",
46146
+ "php artisan event:cache",
46147
+ "php artisan view:cache"
46148
+ ];
46149
+ // src/serverless-php/layer-build.ts
46150
+ import { execFileSync } from "node:child_process";
46151
+ import { existsSync as existsSync5, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
46152
+ import { tmpdir as tmpdir2 } from "node:os";
46153
+ import { join as join8, relative as relative2 } from "node:path";
46154
+ function* walk(dir) {
46155
+ for (const entry of readdirSync5(dir)) {
46156
+ const full = join8(dir, entry);
46157
+ if (statSync3(full).isDirectory())
46158
+ yield* walk(full);
46159
+ else
46160
+ yield full;
46161
+ }
46162
+ }
46163
+ function buildPhpRuntimeLayerZip(options = {}) {
46164
+ const architecture = options.architecture ?? "x86_64";
46165
+ const platform = options.platform ?? (architecture === "arm64" ? "linux/arm64" : "linux/amd64");
46166
+ const step = options.onStep ?? (() => {});
46167
+ const stage = mkdtempSync2(join8(tmpdir2(), "tscloud-php-layer-"));
46168
+ const imageTag = "tscloud-php-layer:build";
46169
+ try {
46170
+ writeFileSync4(join8(stage, "Dockerfile"), generatePhpLayerDockerfile(options));
46171
+ step("Building PHP runtime image (docker)");
46172
+ execFileSync("docker", ["build", "--platform", platform, "-t", imageTag, stage], { stdio: "inherit" });
46173
+ step("Extracting /opt from image");
46174
+ const cid = execFileSync("docker", ["create", "--platform", platform, imageTag], { encoding: "utf-8" }).trim();
46175
+ const optDir = join8(stage, "opt");
46176
+ try {
46177
+ execFileSync("docker", ["cp", `${cid}:/opt/.`, optDir], { stdio: "inherit" });
46178
+ } finally {
46179
+ execFileSync("docker", ["rm", cid], { stdio: "ignore" });
46180
+ }
46181
+ if (!existsSync5(optDir))
46182
+ throw new Error("layer build produced no /opt directory");
46183
+ const entries = [];
46184
+ for (const file of walk(optDir)) {
46185
+ const rel = relative2(optDir, file).replace(/\\/g, "/");
46186
+ const mode = statSync3(file).mode & 73 ? 493 : 420;
46187
+ entries.push({ name: rel, data: readFileSync5(file), mode });
46188
+ }
46189
+ step("Injecting runtime assets");
46190
+ const assetPaths = new Set(phpRuntimeLayerAssets().map((a) => a.path));
46191
+ const filtered = entries.filter((e) => !assetPaths.has(e.name));
46192
+ for (const asset of phpRuntimeLayerAssets()) {
46193
+ filtered.push({ name: asset.path, data: asset.contents, mode: asset.mode });
46194
+ }
46195
+ step("Packaging layer ZIP");
46196
+ const zip2 = createZip(filtered);
46197
+ return { zip: zip2, architecture, fileCount: filtered.length };
46198
+ } finally {
46199
+ rmSync2(stage, { recursive: true, force: true });
46200
+ }
46201
+ }
46202
+ // src/serverless-php/package-php.ts
46203
+ import { execSync as execSync2 } from "node:child_process";
46204
+ import { createHash as createHash5 } from "node:crypto";
46205
+ import { readdirSync as readdirSync6, readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
46206
+ import { join as join9, relative as relative3, resolve as resolve2 } from "node:path";
46207
+ var PHP_DEFAULT_EXCLUDES = [
46208
+ ".git",
46209
+ ".github",
46210
+ "node_modules",
46211
+ "tests",
46212
+ "storage/logs",
46213
+ "storage/framework/cache",
46214
+ "storage/framework/sessions",
46215
+ "storage/framework/views",
46216
+ ".env",
46217
+ ".env.local",
46218
+ ".vapor",
46219
+ ".ts-cloud",
46220
+ "dist-lambda"
46221
+ ];
46222
+ function isExcluded(rel, excludes) {
46223
+ return excludes.some((ex) => rel === ex || rel.startsWith(`${ex}/`));
46224
+ }
46225
+ function* walk2(dir, root, excludes) {
46226
+ for (const entry of readdirSync6(dir)) {
46227
+ const full = join9(dir, entry);
46228
+ const rel = relative3(root, full).replace(/\\/g, "/");
46229
+ if (isExcluded(rel, excludes))
46230
+ continue;
46231
+ if (statSync4(full).isDirectory())
46232
+ yield* walk2(full, root, excludes);
46233
+ else
46234
+ yield full;
46235
+ }
46236
+ }
46237
+ function runPhpBuildHooks(opts) {
46238
+ if (opts.skipBuild)
46239
+ return;
46240
+ const projectRoot = resolve2(opts.projectRoot ?? process.cwd());
46241
+ const steps = opts.app.build ?? LARAVEL_SERVERLESS_BUILD_STEPS;
46242
+ for (const step of steps) {
46243
+ opts.onStep?.(`build: ${step}`);
46244
+ execSync2(step, { stdio: "inherit", cwd: projectRoot });
46245
+ }
46246
+ }
46247
+ function collectPhpAppEntries(projectRoot, exclude = []) {
46248
+ const root = resolve2(projectRoot);
46249
+ const excludes = [...PHP_DEFAULT_EXCLUDES, ...exclude];
46250
+ const entries = [];
46251
+ for (const file of walk2(root, root, excludes)) {
46252
+ const rel = relative3(root, file).replace(/\\/g, "/");
46253
+ const executable = (statSync4(file).mode & 73) !== 0;
46254
+ entries.push({ name: rel, data: readFileSync6(file), mode: executable ? 493 : 420 });
46255
+ }
46256
+ return entries;
46257
+ }
46258
+ function packagePhpApp(opts) {
46259
+ const projectRoot = resolve2(opts.projectRoot ?? process.cwd());
46260
+ runPhpBuildHooks(opts);
46261
+ opts.onStep?.("packaging application tree");
46262
+ const entries = collectPhpAppEntries(projectRoot, opts.exclude);
46263
+ if (!entries.length)
46264
+ throw new Error(`No files to package under ${projectRoot}`);
46265
+ const zip2 = createZip(entries);
46266
+ const handler8 = opts.app.handlers?.http ?? "public/index.php";
46267
+ return {
46268
+ zip: zip2,
46269
+ sha256: createHash5("sha256").update(zip2).digest("hex"),
46270
+ handlers: {
46271
+ http: handler8,
46272
+ queue: opts.app.handlers?.queue ?? handler8,
46273
+ cli: opts.app.handlers?.cli ?? handler8
46274
+ },
46275
+ fileCount: entries.length
46276
+ };
46277
+ }
45028
46278
  // src/static-site/index.ts
45029
46279
  class StaticSiteManager {
45030
46280
  optimizations = new Map;
@@ -45493,6 +46743,7 @@ export {
45493
46743
  stackDependencyManager,
45494
46744
  signRequestAsync,
45495
46745
  signRequest,
46746
+ sha256,
45496
46747
  serviceMeshManager,
45497
46748
  sequence,
45498
46749
  senderReputationManager,
@@ -45502,18 +46753,26 @@ export {
45502
46753
  secretsManager,
45503
46754
  searchCommands,
45504
46755
  sanitizeName,
46756
+ runPhpBuildHooks,
46757
+ runBuildHooks,
45505
46758
  route53RoutingManager,
45506
46759
  route53ResolverManager,
46760
+ responseToResult,
45507
46761
  resourceManagementManager,
45508
46762
  resolveStorageBucketName,
45509
46763
  resolveSiteStackName,
45510
46764
  resolveSiteResourceName,
45511
46765
  resolveSiteBucketName,
46766
+ resolveServerlessAssetBucketName,
46767
+ resolveServerlessArtifactBucketName,
46768
+ resolveServerlessAppStackName,
45512
46769
  resolveRegion,
46770
+ resolveQueueNames,
45513
46771
  resolveProjectStackName,
45514
46772
  resolveDeployBucketName,
45515
46773
  resolveCredentials,
45516
46774
  resolveCloudProvider,
46775
+ resolveApp,
45517
46776
  requiresReplacement,
45518
46777
  replicaManager,
45519
46778
  regionPairManager,
@@ -45523,12 +46782,17 @@ export {
45523
46782
  processInChunks,
45524
46783
  previewNotifications,
45525
46784
  previewManager,
46785
+ phpRuntimeLayerAssets,
46786
+ phpLayerPackages,
46787
+ phpLayerBuildStage,
45526
46788
  performanceManager,
45527
46789
  parseXMLResponse,
45528
46790
  parseJSONResponse,
45529
46791
  parallelWithRetry,
45530
46792
  parallelMap,
45531
46793
  parallel,
46794
+ packageServerlessApp,
46795
+ packagePhpApp,
45532
46796
  organizationManager,
45533
46797
  networkSecurityManager,
45534
46798
  multiRegionManager,
@@ -45540,6 +46804,7 @@ export {
45540
46804
  makeAWSRequestAsync,
45541
46805
  makeAWSRequest,
45542
46806
  logsManager,
46807
+ laravelServerlessEnvDefaults,
45543
46808
  lambdaVersionsManager,
45544
46809
  lambdaVPCManager,
45545
46810
  lambdaLayersManager,
@@ -45590,6 +46855,8 @@ export {
45590
46855
  generateResourceName,
45591
46856
  generatePreviewWorkflow,
45592
46857
  generatePreviewPipeline,
46858
+ generatePhpLayerDockerfile,
46859
+ generatePhpFpmConfig,
45593
46860
  generateParallelConfig,
45594
46861
  generatePRPreviewWorkflow,
45595
46862
  generateMultiEnvWorkflow,
@@ -45604,7 +46871,9 @@ export {
45604
46871
  generateCrossAccountRoleCF,
45605
46872
  generateCostReportWorkflow,
45606
46873
  generateCleanupWorkflow,
46874
+ generateBootstrap,
45607
46875
  generateApprovalConfig,
46876
+ generateAppImageDockerfile,
45608
46877
  fromWebIdentity,
45609
46878
  fromSharedCredentials,
45610
46879
  fromEnvironment,
@@ -45628,6 +46897,7 @@ export {
45628
46897
  findChangedFiles,
45629
46898
  fifoQueueManager,
45630
46899
  extendPreset,
46900
+ eventToRequest,
45631
46901
  emailTemplateManager,
45632
46902
  emailAnalyticsManager,
45633
46903
  drManager,
@@ -45639,11 +46909,15 @@ export {
45639
46909
  defaultLocalConfig,
45640
46910
  databaseUserManager,
45641
46911
  crossRegionReferenceManager,
46912
+ createZip,
45642
46913
  createWordPressPreset,
45643
46914
  createTraditionalWebAppPreset,
45644
46915
  createStaticSitePreset,
46916
+ createServerlessNodePreset,
46917
+ createServerlessLaravelPreset,
45645
46918
  createS3Client,
45646
46919
  createRealtimeAppPreset,
46920
+ createQueueHandler,
45647
46921
  createPresignedUrlAsync,
45648
46922
  createPresignedUrl,
45649
46923
  createPreset,
@@ -45654,14 +46928,19 @@ export {
45654
46928
  createMLApiPreset,
45655
46929
  createLaravelPreset,
45656
46930
  createJamstackPreset,
46931
+ createHttpHandler,
46932
+ createHandlers,
45657
46933
  createFullStackAppPreset,
45658
46934
  createError,
45659
46935
  createDataPipelinePreset,
45660
46936
  createDashboardSite,
45661
46937
  createCredentialProvider,
46938
+ createCliHandler,
45662
46939
  createApiBackendPreset,
45663
46940
  containerRegistryManager,
46941
+ composeServerlessAppTemplate,
45664
46942
  composePresets,
46943
+ collectPhpAppEntries,
45665
46944
  cloudTrailManager,
45666
46945
  cloud_config_schema_default as cloudConfigSchema,
45667
46946
  clearSigningKeyCache,
@@ -45671,6 +46950,7 @@ export {
45671
46950
  certificateManager,
45672
46951
  categorizeChanges,
45673
46952
  canaryManager,
46953
+ buildPhpRuntimeLayerZip,
45674
46954
  buildOptimizationManager,
45675
46955
  buildCloudFormationTemplate,
45676
46956
  bounceComplaintHandler,
@@ -45680,6 +46960,7 @@ export {
45680
46960
  backupManager,
45681
46961
  awsConfigManager,
45682
46962
  autocomplete,
46963
+ artifactKey,
45683
46964
  analyzeStackDiff,
45684
46965
  abTestManager,
45685
46966
  XRayManager,
@@ -45737,6 +47018,8 @@ export {
45737
47018
  Permissions,
45738
47019
  PerformanceManager,
45739
47020
  ParameterStore,
47021
+ PHP_LAYER_EXTENSIONS,
47022
+ PHP_DEFAULT_EXCLUDES,
45740
47023
  OrganizationManager,
45741
47024
  NetworkSecurityManager,
45742
47025
  Network,
@@ -45757,6 +47040,7 @@ export {
45757
47040
  LambdaDestinationsManager,
45758
47041
  LambdaDLQManager,
45759
47042
  LambdaConcurrencyManager,
47043
+ LARAVEL_SERVERLESS_BUILD_STEPS,
45760
47044
  JobLoader,
45761
47045
  ImageScanningManager,
45762
47046
  HealthCheckManager,