@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.d.ts +4 -0
- package/dist/index.js +1284 -0
- package/dist/presets/index.d.ts +2 -0
- package/dist/presets/laravel.d.ts +4 -4
- package/dist/presets/serverless-laravel.d.ts +38 -0
- package/dist/presets/serverless-node.d.ts +36 -0
- package/dist/serverless/adapter.test.d.ts +1 -0
- package/dist/serverless/app-image.d.ts +22 -0
- package/dist/serverless/app-image.test.d.ts +1 -0
- package/dist/serverless/bootstrap.d.ts +13 -0
- package/dist/serverless/composer.d.ts +43 -0
- package/dist/serverless/composer.test.d.ts +1 -0
- package/dist/serverless/index.d.ts +10 -0
- package/dist/serverless/naming.test.d.ts +1 -0
- package/dist/serverless/package.d.ts +46 -0
- package/dist/serverless/package.test.d.ts +1 -0
- package/dist/serverless/runtime/adapter.d.ts +122 -0
- package/dist/serverless/zip.d.ts +25 -0
- package/dist/serverless/zip.test.d.ts +1 -0
- package/dist/serverless-php/dockerfile.d.ts +36 -0
- package/dist/serverless-php/index.d.ts +10 -0
- package/dist/serverless-php/layer-build.d.ts +33 -0
- package/dist/serverless-php/package-php.d.ts +43 -0
- package/dist/serverless-php/php-fpm-conf.d.ts +16 -0
- package/dist/serverless-php/runtime-assets.d.ts +34 -0
- package/dist/serverless-php/serverless-php.test.d.ts +1 -0
- package/dist/stack-naming.d.ts +17 -0
- package/dist/types.d.ts +185 -3
- package/package.json +2 -2
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,
|