@stacksjs/ts-cloud 0.5.16 → 0.5.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +639 -601
- package/dist/deploy/hooks.d.ts +25 -0
- package/dist/deploy/quick-deploy.d.ts +25 -0
- package/dist/drivers/hetzner/driver.d.ts +1 -1
- package/dist/drivers/shared/backups.d.ts +12 -0
- package/dist/drivers/shared/compute-ops.d.ts +10 -1
- package/dist/drivers/shared/laravel-deploy.d.ts +8 -0
- package/dist/drivers/shared/monitoring.d.ts +15 -7
- package/dist/drivers/shared/nginx-vhost.d.ts +19 -0
- package/dist/drivers/shared/php-fpm-pool.d.ts +34 -0
- package/dist/index.js +224 -47
- package/dist/security/pre-deploy-scanner.d.ts +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -8440,18 +8440,19 @@ class S3Client {
|
|
|
8440
8440
|
stream = blob.stream();
|
|
8441
8441
|
totalSize = blob.size;
|
|
8442
8442
|
}
|
|
8443
|
-
const uploadId = await this.initiateMultipartUpload(bucket, key, options);
|
|
8443
|
+
const { uploadId, baseUrl } = await this.initiateMultipartUpload(bucket, key, options);
|
|
8444
8444
|
try {
|
|
8445
|
-
const parts = await this.uploadParts(bucket, key, uploadId, stream, partSize, credentials, totalSize, options.concurrency || 4, options.onProgress);
|
|
8446
|
-
return await this.completeMultipartUpload(bucket, key, uploadId, parts);
|
|
8445
|
+
const parts = await this.uploadParts(bucket, key, baseUrl, uploadId, stream, partSize, credentials, totalSize, options.concurrency || 4, options.onProgress);
|
|
8446
|
+
return await this.completeMultipartUpload(bucket, key, baseUrl, uploadId, parts);
|
|
8447
8447
|
} catch (error) {
|
|
8448
|
-
await this.abortMultipartUpload(bucket, key, uploadId).catch(() => {});
|
|
8448
|
+
await this.abortMultipartUpload(bucket, key, uploadId, baseUrl).catch(() => {});
|
|
8449
8449
|
throw error;
|
|
8450
8450
|
}
|
|
8451
8451
|
}
|
|
8452
8452
|
async initiateMultipartUpload(bucket, key, options) {
|
|
8453
8453
|
const credentials = await this.getCredentials();
|
|
8454
|
-
const
|
|
8454
|
+
const baseUrl = this.buildUrl(bucket, key);
|
|
8455
|
+
const url = `${baseUrl}?uploads`;
|
|
8455
8456
|
const headers = {
|
|
8456
8457
|
"Content-Type": options.contentType || detectContentType(key)
|
|
8457
8458
|
};
|
|
@@ -8487,18 +8488,26 @@ class S3Client {
|
|
|
8487
8488
|
if (!uploadIdMatch) {
|
|
8488
8489
|
throw new S3Error("Failed to parse upload ID from response", 0, bucket, key);
|
|
8489
8490
|
}
|
|
8490
|
-
|
|
8491
|
+
let resolvedBaseUrl = baseUrl;
|
|
8492
|
+
if (response.url) {
|
|
8493
|
+
try {
|
|
8494
|
+
const resolved = new URL(response.url);
|
|
8495
|
+
resolved.search = "";
|
|
8496
|
+
resolvedBaseUrl = resolved.toString();
|
|
8497
|
+
} catch {}
|
|
8498
|
+
}
|
|
8499
|
+
return { uploadId: uploadIdMatch[1], baseUrl: resolvedBaseUrl };
|
|
8491
8500
|
}
|
|
8492
|
-
async uploadParts(bucket, key, uploadId, stream, partSize, credentials, totalSize, concurrency, onProgress) {
|
|
8501
|
+
async uploadParts(bucket, key, baseUrl, uploadId, stream, partSize, credentials, totalSize, concurrency, onProgress) {
|
|
8493
8502
|
const parts = [];
|
|
8494
8503
|
const reader = stream.getReader();
|
|
8495
8504
|
let partNumber = 1;
|
|
8496
8505
|
let buffer = new Uint8Array(0);
|
|
8497
8506
|
let loaded = 0;
|
|
8498
8507
|
const totalParts = totalSize ? Math.ceil(totalSize / partSize) : undefined;
|
|
8499
|
-
const
|
|
8508
|
+
const inFlight = new Map;
|
|
8500
8509
|
const uploadPart = async (data, num) => {
|
|
8501
|
-
const url = `${
|
|
8510
|
+
const url = `${baseUrl}?partNumber=${num}&uploadId=${encodeURIComponent(uploadId)}`;
|
|
8502
8511
|
const headers = {
|
|
8503
8512
|
"Content-Length": String(data.byteLength),
|
|
8504
8513
|
"x-amz-content-sha256": "UNSIGNED-PAYLOAD"
|
|
@@ -8514,15 +8523,24 @@ class S3Client {
|
|
|
8514
8523
|
const response = await fetch(signed.url, {
|
|
8515
8524
|
method: signed.method,
|
|
8516
8525
|
headers: signed.headers,
|
|
8517
|
-
body: data
|
|
8526
|
+
body: data,
|
|
8527
|
+
redirect: "error"
|
|
8518
8528
|
});
|
|
8519
8529
|
if (!response.ok) {
|
|
8520
8530
|
const error = await response.text();
|
|
8521
8531
|
throw new S3Error(`Failed to upload part ${num}: ${error}`, response.status, bucket, key);
|
|
8522
8532
|
}
|
|
8523
8533
|
const etag = (response.headers.get("ETag") || "").replace(/"/g, "");
|
|
8534
|
+
if (!etag) {
|
|
8535
|
+
throw new S3Error(`Missing ETag for part ${num}`, response.status, bucket, key);
|
|
8536
|
+
}
|
|
8524
8537
|
return { partNumber: num, etag };
|
|
8525
8538
|
};
|
|
8539
|
+
const drainOne = async () => {
|
|
8540
|
+
const completed = await Promise.race(inFlight.values());
|
|
8541
|
+
inFlight.delete(completed.partNumber);
|
|
8542
|
+
parts.push(completed);
|
|
8543
|
+
};
|
|
8526
8544
|
while (true) {
|
|
8527
8545
|
const { done, value } = await reader.read();
|
|
8528
8546
|
if (value) {
|
|
@@ -8535,13 +8553,10 @@ class S3Client {
|
|
|
8535
8553
|
const partData = buffer.slice(0, partSize);
|
|
8536
8554
|
buffer = buffer.slice(partSize);
|
|
8537
8555
|
const currentPartNumber = partNumber++;
|
|
8538
|
-
if (
|
|
8539
|
-
|
|
8540
|
-
parts.push(completed);
|
|
8541
|
-
uploadQueue.splice(uploadQueue.indexOf(Promise.resolve(completed)), 1);
|
|
8556
|
+
if (inFlight.size >= concurrency) {
|
|
8557
|
+
await drainOne();
|
|
8542
8558
|
}
|
|
8543
|
-
|
|
8544
|
-
uploadQueue.push(uploadPromise);
|
|
8559
|
+
inFlight.set(currentPartNumber, uploadPart(partData, currentPartNumber));
|
|
8545
8560
|
loaded += partData.byteLength;
|
|
8546
8561
|
if (onProgress) {
|
|
8547
8562
|
onProgress({
|
|
@@ -8555,14 +8570,15 @@ class S3Client {
|
|
|
8555
8570
|
if (done)
|
|
8556
8571
|
break;
|
|
8557
8572
|
}
|
|
8558
|
-
|
|
8559
|
-
|
|
8573
|
+
while (inFlight.size > 0) {
|
|
8574
|
+
await drainOne();
|
|
8575
|
+
}
|
|
8560
8576
|
parts.sort((a, b) => a.partNumber - b.partNumber);
|
|
8561
8577
|
return parts;
|
|
8562
8578
|
}
|
|
8563
|
-
async completeMultipartUpload(bucket, key, uploadId, parts) {
|
|
8579
|
+
async completeMultipartUpload(bucket, key, baseUrl, uploadId, parts) {
|
|
8564
8580
|
const credentials = await this.getCredentials();
|
|
8565
|
-
const url = `${
|
|
8581
|
+
const url = `${baseUrl}?uploadId=${encodeURIComponent(uploadId)}`;
|
|
8566
8582
|
const partsXml = parts.map((p) => `<Part><PartNumber>${p.partNumber}</PartNumber><ETag>"${p.etag}"</ETag></Part>`).join("");
|
|
8567
8583
|
const body = `<?xml version="1.0" encoding="UTF-8"?><CompleteMultipartUpload>${partsXml}</CompleteMultipartUpload>`;
|
|
8568
8584
|
const signed = signRequest({
|
|
@@ -8588,9 +8604,9 @@ class S3Client {
|
|
|
8588
8604
|
const etag = etagMatch ? etagMatch[1] : "";
|
|
8589
8605
|
return { etag };
|
|
8590
8606
|
}
|
|
8591
|
-
async abortMultipartUpload(bucket, key, uploadId) {
|
|
8607
|
+
async abortMultipartUpload(bucket, key, uploadId, baseUrl) {
|
|
8592
8608
|
const credentials = await this.getCredentials();
|
|
8593
|
-
const url = `${this.buildUrl(bucket, key)}?uploadId=${encodeURIComponent(uploadId)}`;
|
|
8609
|
+
const url = `${baseUrl ?? this.buildUrl(bucket, key)}?uploadId=${encodeURIComponent(uploadId)}`;
|
|
8594
8610
|
const signed = signRequest({
|
|
8595
8611
|
method: "DELETE",
|
|
8596
8612
|
url,
|
|
@@ -26399,14 +26415,7 @@ function composeServerlessAppTemplate(opts) {
|
|
|
26399
26415
|
VisibilityConfig: { SampledRequestsEnabled: true, CloudWatchMetricsEnabled: true, MetricName: `${slug}-${environment}-waf` }
|
|
26400
26416
|
}
|
|
26401
26417
|
};
|
|
26402
|
-
|
|
26403
|
-
Type: "AWS::WAFv2::WebACLAssociation",
|
|
26404
|
-
DependsOn: ["HttpStage"],
|
|
26405
|
-
Properties: {
|
|
26406
|
-
ResourceArn: Fn2.sub("arn:aws:apigateway:${AWS::Region}::/apis/${HttpApi}/stages/$default"),
|
|
26407
|
-
WebACLArn: Fn2.getAtt("WebAcl", "Arn")
|
|
26408
|
-
}
|
|
26409
|
-
};
|
|
26418
|
+
outputs.WafAclArn = { Description: "WAF web ACL ARN. Attach to a CloudFront distribution fronting the API to enforce (HTTP API v2 stages do not support direct WAF association).", Value: Fn2.getAtt("WebAcl", "Arn") };
|
|
26410
26419
|
}
|
|
26411
26420
|
if (hasVpc && needsDataVpc) {
|
|
26412
26421
|
if (!app.vpc?.id)
|
|
@@ -84081,6 +84090,31 @@ function resolveNginxSnippet(nginx, templates) {
|
|
|
84081
84090
|
out.push(...nginx.serverSnippet);
|
|
84082
84091
|
return out;
|
|
84083
84092
|
}
|
|
84093
|
+
function hstsHeader(hsts) {
|
|
84094
|
+
if (!hsts)
|
|
84095
|
+
return "";
|
|
84096
|
+
const o = typeof hsts === "object" ? hsts : {};
|
|
84097
|
+
const maxAge = o.maxAge ?? 31536000;
|
|
84098
|
+
const parts = [`max-age=${maxAge}`];
|
|
84099
|
+
if (o.includeSubDomains ?? true)
|
|
84100
|
+
parts.push("includeSubDomains");
|
|
84101
|
+
if (o.preload)
|
|
84102
|
+
parts.push("preload");
|
|
84103
|
+
return ` add_header Strict-Transport-Security "${parts.join("; ")}" always;`;
|
|
84104
|
+
}
|
|
84105
|
+
function securityRules(security) {
|
|
84106
|
+
if (!security || !security.allow?.length && !security.deny?.length)
|
|
84107
|
+
return [];
|
|
84108
|
+
const lines = [];
|
|
84109
|
+
for (const ip of security.deny || [])
|
|
84110
|
+
lines.push(` deny ${ip};`);
|
|
84111
|
+
if (security.allow?.length) {
|
|
84112
|
+
for (const ip of security.allow)
|
|
84113
|
+
lines.push(` allow ${ip};`);
|
|
84114
|
+
lines.push(" deny all;");
|
|
84115
|
+
}
|
|
84116
|
+
return lines;
|
|
84117
|
+
}
|
|
84084
84118
|
function htpasswdPath(siteName) {
|
|
84085
84119
|
return `/etc/nginx/.htpasswd-${siteName}`;
|
|
84086
84120
|
}
|
|
@@ -84089,7 +84123,7 @@ function isPhpSiteType(type) {
|
|
|
84089
84123
|
return PHP_TYPES.has(type);
|
|
84090
84124
|
}
|
|
84091
84125
|
function defaultWebDirectory(type) {
|
|
84092
|
-
return type === "laravel" || type === "statamic"
|
|
84126
|
+
return type === "laravel" || type === "statamic" ? "public" : "";
|
|
84093
84127
|
}
|
|
84094
84128
|
function resolveRoot(appDir, webDirectory) {
|
|
84095
84129
|
const base = appDir.replace(/\/+$/, "");
|
|
@@ -84106,13 +84140,14 @@ function vhostBody(options) {
|
|
|
84106
84140
|
` root ${root};`,
|
|
84107
84141
|
"",
|
|
84108
84142
|
' add_header X-Frame-Options "SAMEORIGIN";',
|
|
84109
|
-
' add_header X-Content-Type-Options "nosniff";'
|
|
84110
|
-
"",
|
|
84111
|
-
` index ${isPhp ? "index.php index.html" : "index.html index.htm"};`,
|
|
84112
|
-
"",
|
|
84113
|
-
" charset utf-8;",
|
|
84114
|
-
""
|
|
84143
|
+
' add_header X-Content-Type-Options "nosniff";'
|
|
84115
84144
|
];
|
|
84145
|
+
const hsts = hstsHeader(options.hsts);
|
|
84146
|
+
if (hsts)
|
|
84147
|
+
lines.push(hsts);
|
|
84148
|
+
for (const rule of securityRules(options.security))
|
|
84149
|
+
lines.push(rule);
|
|
84150
|
+
lines.push("", ` index ${isPhp ? "index.php index.html" : "index.html index.htm"};`, "", " charset utf-8;", "");
|
|
84116
84151
|
if (options.clientMaxBodySize) {
|
|
84117
84152
|
lines.push(` client_max_body_size ${options.clientMaxBodySize};`, "");
|
|
84118
84153
|
}
|
|
@@ -84125,7 +84160,10 @@ function vhostBody(options) {
|
|
|
84125
84160
|
if (Object.keys(options.redirects || {}).length > 0)
|
|
84126
84161
|
lines.push("");
|
|
84127
84162
|
if (isPhp) {
|
|
84128
|
-
lines.push(" location / {", " try_files $uri $uri/ /index.php?$query_string;", " }", "", " location = /favicon.ico { access_log off; log_not_found off; }", " location = /robots.txt { access_log off; log_not_found off; }", "", " error_page 404 /index.php;", "", " location ~ \\.php$ {", ` fastcgi_pass ${phpFpmSocketPath(phpVersion)};`, " fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;", " include fastcgi_params;", " }", "", " location ~ /\\.(?!well-known).* {", " deny all;", " }");
|
|
84163
|
+
lines.push(" location / {", " try_files $uri $uri/ /index.php?$query_string;", " }", "", " location = /favicon.ico { access_log off; log_not_found off; }", " location = /robots.txt { access_log off; log_not_found off; }", "", " error_page 404 /index.php;", "", " location ~ \\.php$ {", ` fastcgi_pass ${options.fastcgiPass ?? phpFpmSocketPath(phpVersion)};`, " fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;", " include fastcgi_params;", " }", "", " location ~ /\\.(?!well-known).* {", " deny all;", " }");
|
|
84164
|
+
if (type === "wordpress") {
|
|
84165
|
+
lines.push("", " location = /xmlrpc.php { deny all; }", " location ~* /(?:wp-config\\.php|readme\\.html|license\\.txt)$ { deny all; }", " location ~* /wp-content/uploads/.*\\.php$ { deny all; }");
|
|
84166
|
+
}
|
|
84129
84167
|
} else if (type === "spa") {
|
|
84130
84168
|
lines.push(" location / {", " try_files $uri $uri/ /index.html;", " }");
|
|
84131
84169
|
} else {
|
|
@@ -84157,6 +84195,7 @@ function buildNginxVhost(options) {
|
|
|
84157
84195
|
` server_name ${serverNames};`,
|
|
84158
84196
|
` ssl_certificate ${options.ssl.certPath};`,
|
|
84159
84197
|
` ssl_certificate_key ${options.ssl.keyPath};`,
|
|
84198
|
+
...options.tlsProtocols?.length ? [` ssl_protocols ${options.tlsProtocols.join(" ")};`] : [],
|
|
84160
84199
|
"",
|
|
84161
84200
|
...body,
|
|
84162
84201
|
"}"
|
|
@@ -84330,22 +84369,71 @@ function buildAutoUpdatesScript(enabled2 = true) {
|
|
|
84330
84369
|
|
|
84331
84370
|
// src/drivers/shared/monitoring.ts
|
|
84332
84371
|
var METRICS_PATH = "/var/lib/ts-cloud/metrics.json";
|
|
84333
|
-
|
|
84372
|
+
var ALERT_STATE_PATH = "/var/lib/ts-cloud/alert-state";
|
|
84373
|
+
var DEFAULT_CPU_LOAD_PER_CORE = 2;
|
|
84374
|
+
var DEFAULT_MEM_PERCENT = 90;
|
|
84375
|
+
var DEFAULT_DISK_PERCENT = 90;
|
|
84376
|
+
var SERVICE_PROBES = [
|
|
84377
|
+
["nginx", 80],
|
|
84378
|
+
["phpFpm", 9074],
|
|
84379
|
+
["mysql", 3306],
|
|
84380
|
+
["postgres", 5432],
|
|
84381
|
+
["redis", 6379],
|
|
84382
|
+
["meilisearch", 7700]
|
|
84383
|
+
];
|
|
84384
|
+
function resolveMonitoring(monitoring = true) {
|
|
84385
|
+
const obj = typeof monitoring === "object" ? monitoring : {};
|
|
84386
|
+
const enabled2 = typeof monitoring === "boolean" ? monitoring : monitoring.enabled !== false;
|
|
84387
|
+
return {
|
|
84388
|
+
enabled: enabled2,
|
|
84389
|
+
cpuLoadPerCore: obj.alerts?.cpuLoadPerCore ?? DEFAULT_CPU_LOAD_PER_CORE,
|
|
84390
|
+
memPercent: obj.alerts?.memPercent ?? DEFAULT_MEM_PERCENT,
|
|
84391
|
+
diskPercent: obj.alerts?.diskPercent ?? DEFAULT_DISK_PERCENT
|
|
84392
|
+
};
|
|
84393
|
+
}
|
|
84394
|
+
function buildMonitoringScript(monitoring = true) {
|
|
84395
|
+
const { enabled: enabled2, cpuLoadPerCore, memPercent, diskPercent } = resolveMonitoring(monitoring);
|
|
84334
84396
|
if (!enabled2)
|
|
84335
84397
|
return [];
|
|
84398
|
+
const probeLines = SERVICE_PROBES.map(([name, port]) => `SVC_${name.toUpperCase()}=$(probe ${port})`);
|
|
84399
|
+
const servicesJson = SERVICE_PROBES.map(([name]) => `"${name}":"$SVC_${name.toUpperCase()}"`).join(",");
|
|
84336
84400
|
return [
|
|
84337
84401
|
"mkdir -p /var/lib/ts-cloud",
|
|
84338
84402
|
"cat > /usr/local/bin/ts-cloud-metrics.sh <<'TS_CLOUD_METRICS_EOF'",
|
|
84339
84403
|
"#!/bin/bash",
|
|
84340
|
-
"set -
|
|
84404
|
+
"set -uo pipefail",
|
|
84341
84405
|
"LOAD=$(cut -d' ' -f1 /proc/loadavg)",
|
|
84406
|
+
"CPUS=$(nproc)",
|
|
84342
84407
|
"MEM_TOTAL=$(free -m | awk '/^Mem:/{print $2}')",
|
|
84343
84408
|
"MEM_USED=$(free -m | awk '/^Mem:/{print $3}')",
|
|
84409
|
+
"SWAP_TOTAL=$(free -m | awk '/^Swap:/{print $2}')",
|
|
84410
|
+
"SWAP_USED=$(free -m | awk '/^Swap:/{print $3}')",
|
|
84344
84411
|
`DISK_PCT=$(df -P / | awk 'NR==2{gsub("%","",$5); print $5}')`,
|
|
84345
|
-
"
|
|
84346
|
-
|
|
84347
|
-
'
|
|
84412
|
+
"UPTIME_SEC=$(cut -d' ' -f1 /proc/uptime | cut -d. -f1)",
|
|
84413
|
+
`RX_BYTES=$(awk -F'[: ]+' 'NR>2 && $2!="lo"{rx+=$3} END{print rx+0}' /proc/net/dev)`,
|
|
84414
|
+
`TX_BYTES=$(awk -F'[: ]+' 'NR>2 && $2!="lo"{tx+=$11} END{print tx+0}' /proc/net/dev)`,
|
|
84415
|
+
"probe(){ (exec 3<>/dev/tcp/127.0.0.1/$1) 2>/dev/null && echo up || echo down; }",
|
|
84416
|
+
...probeLines,
|
|
84417
|
+
"LOAD=${LOAD:-0}; CPUS=${CPUS:-1}; MEM_TOTAL=${MEM_TOTAL:-0}; MEM_USED=${MEM_USED:-0}",
|
|
84418
|
+
"SWAP_TOTAL=${SWAP_TOTAL:-0}; SWAP_USED=${SWAP_USED:-0}; DISK_PCT=${DISK_PCT:-0}",
|
|
84419
|
+
"UPTIME_SEC=${UPTIME_SEC:-0}; RX_BYTES=${RX_BYTES:-0}; TX_BYTES=${TX_BYTES:-0}",
|
|
84420
|
+
"MEM_PCT=$(( MEM_TOTAL > 0 ? MEM_USED * 100 / MEM_TOTAL : 0 ))",
|
|
84421
|
+
`cat > ${METRICS_PATH}.tmp <<JSON`,
|
|
84422
|
+
'{"load":$LOAD,"cpus":$CPUS,"memTotalMb":$MEM_TOTAL,"memUsedMb":$MEM_USED,"memUsedPct":$MEM_PCT,"swapTotalMb":$SWAP_TOTAL,"swapUsedMb":$SWAP_USED,"diskUsedPct":$DISK_PCT,"uptimeSec":$UPTIME_SEC,"network":{"rxBytes":$RX_BYTES,"txBytes":$TX_BYTES},"services":{' + servicesJson + "}}",
|
|
84348
84423
|
"JSON",
|
|
84424
|
+
`mv -f ${METRICS_PATH}.tmp ${METRICS_PATH}`,
|
|
84425
|
+
'ALERTS=""',
|
|
84426
|
+
`if awk -v l="$LOAD" -v c="$CPUS" -v t=${cpuLoadPerCore} 'BEGIN{exit !(c>0 && l/c > t)}'; then ALERTS="$ALERTS load=$LOAD/${cpuLoadPerCore}xCPU"; fi`,
|
|
84427
|
+
`if [ "\${MEM_PCT:-0}" -ge ${memPercent} ]; then ALERTS="$ALERTS mem=\${MEM_PCT}%"; fi`,
|
|
84428
|
+
`if [ "\${DISK_PCT:-0}" -ge ${diskPercent} ]; then ALERTS="$ALERTS disk=\${DISK_PCT}%"; fi`,
|
|
84429
|
+
`PREV=$(cat ${ALERT_STATE_PATH} 2>/dev/null || echo ok)`,
|
|
84430
|
+
'if [ -n "$ALERTS" ]; then',
|
|
84431
|
+
' if [ "$PREV" != alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "⚠️ $(hostname): resource alert —$ALERTS" || true; fi',
|
|
84432
|
+
` echo alert > ${ALERT_STATE_PATH}`,
|
|
84433
|
+
"else",
|
|
84434
|
+
' if [ "$PREV" = alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "✅ $(hostname): resource usage back to normal" || true; fi',
|
|
84435
|
+
` echo ok > ${ALERT_STATE_PATH}`,
|
|
84436
|
+
"fi",
|
|
84349
84437
|
"TS_CLOUD_METRICS_EOF",
|
|
84350
84438
|
"chmod +x /usr/local/bin/ts-cloud-metrics.sh",
|
|
84351
84439
|
"cat > /etc/systemd/system/ts-cloud-metrics.service <<'TS_CLOUD_METRICS_SVC_EOF'",
|
|
@@ -85386,6 +85474,10 @@ function buildRpxProvisionScript(options) {
|
|
|
85386
85474
|
const version2 = proxy.version ?? "latest";
|
|
85387
85475
|
const certsDir = config6.productionCerts.certsDir;
|
|
85388
85476
|
const launcher = renderRpxLauncher(config6);
|
|
85477
|
+
const upstreamTimeout = proxy.upstreamTimeout ?? 60;
|
|
85478
|
+
const poolEnv = [`Environment=RPX_UPSTREAM_TIMEOUT=${upstreamTimeout}`];
|
|
85479
|
+
if (typeof proxy.maxUpstreamConns === "number")
|
|
85480
|
+
poolEnv.push(`Environment=RPX_MAX_UPSTREAM_CONNS=${proxy.maxUpstreamConns}`);
|
|
85389
85481
|
return [
|
|
85390
85482
|
"set -euo pipefail",
|
|
85391
85483
|
`mkdir -p ${RPX_DIR} ${certsDir}`,
|
|
@@ -85401,6 +85493,7 @@ function buildRpxProvisionScript(options) {
|
|
|
85401
85493
|
"Type=simple",
|
|
85402
85494
|
`ExecStart=${bunBin} ${RPX_LAUNCHER_PATH}`,
|
|
85403
85495
|
`Environment=BUN_INSTALL=/root/.bun`,
|
|
85496
|
+
...poolEnv,
|
|
85404
85497
|
"Restart=always",
|
|
85405
85498
|
"RestartSec=5",
|
|
85406
85499
|
"LimitNOFILE=1048576",
|
|
@@ -86440,12 +86533,12 @@ function defaultDeployScriptFor(type) {
|
|
|
86440
86533
|
MACRO_RESTART_QUEUES
|
|
86441
86534
|
];
|
|
86442
86535
|
case "php":
|
|
86536
|
+
case "wordpress":
|
|
86443
86537
|
return [
|
|
86444
86538
|
MACRO_CREATE_RELEASE,
|
|
86445
86539
|
`${COMPOSER_INSTALL} || true`,
|
|
86446
86540
|
MACRO_ACTIVATE_RELEASE
|
|
86447
86541
|
];
|
|
86448
|
-
case "wordpress":
|
|
86449
86542
|
case "static":
|
|
86450
86543
|
case "spa":
|
|
86451
86544
|
return [
|
|
@@ -86481,6 +86574,19 @@ function buildCredentialFiles(releaseDir, creds) {
|
|
|
86481
86574
|
out.push(...writeFileHeredoc2(`${releaseDir}/.npmrc`, creds.npmrc, "TS_CLOUD_NPMRC_EOF"));
|
|
86482
86575
|
return out;
|
|
86483
86576
|
}
|
|
86577
|
+
function buildHealthCheckScript(site) {
|
|
86578
|
+
const path = site.healthCheck?.path;
|
|
86579
|
+
if (!path || !site.domain)
|
|
86580
|
+
return [];
|
|
86581
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
86582
|
+
const url = `http://127.0.0.1${p}`;
|
|
86583
|
+
return [
|
|
86584
|
+
`echo "[ts-cloud] health check ${url} (Host: ${site.domain})"`,
|
|
86585
|
+
"TS_CLOUD_HC_OK=0",
|
|
86586
|
+
`for i in $(seq 1 10); do TS_CLOUD_HC=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: ${site.domain}" -m 10 "${url}" 2>/dev/null || echo 000); case "$TS_CLOUD_HC" in 2*|3*) TS_CLOUD_HC_OK=1; break ;; esac; sleep 3; done`,
|
|
86587
|
+
'[ "$TS_CLOUD_HC_OK" = 1 ] && echo "health check passed ($TS_CLOUD_HC)" || { echo "health check FAILED (last=$TS_CLOUD_HC)" >&2; exit 1; }'
|
|
86588
|
+
];
|
|
86589
|
+
}
|
|
86484
86590
|
function buildLaravelDeployScript(options) {
|
|
86485
86591
|
const { siteName, site, releaseId, commit } = options;
|
|
86486
86592
|
if (!site.repository?.url)
|
|
@@ -86662,6 +86768,68 @@ function siteHasServices(site) {
|
|
|
86662
86768
|
return !!(site.queues?.length || site.daemons?.length || site.scheduler);
|
|
86663
86769
|
}
|
|
86664
86770
|
|
|
86771
|
+
// src/drivers/shared/php-fpm-pool.ts
|
|
86772
|
+
var PHP_FPM_POOL_DIR = "/var/lib/pantry/php-fpm/pool.d";
|
|
86773
|
+
var POOL_PORT_BASE = 9100;
|
|
86774
|
+
var POOL_PORT_SPAN = 400;
|
|
86775
|
+
function siteToken(siteName) {
|
|
86776
|
+
const base = siteName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
86777
|
+
return (base || "site").slice(0, 28);
|
|
86778
|
+
}
|
|
86779
|
+
function siteUser(siteName) {
|
|
86780
|
+
return `web_${siteToken(siteName)}`;
|
|
86781
|
+
}
|
|
86782
|
+
function phpFpmPoolPort(siteName) {
|
|
86783
|
+
const token = siteToken(siteName);
|
|
86784
|
+
let h = 0;
|
|
86785
|
+
for (let i = 0;i < token.length; i++)
|
|
86786
|
+
h = h * 31 + token.charCodeAt(i) >>> 0;
|
|
86787
|
+
return POOL_PORT_BASE + h % POOL_PORT_SPAN;
|
|
86788
|
+
}
|
|
86789
|
+
function phpFpmPoolListen(siteName) {
|
|
86790
|
+
return `127.0.0.1:${phpFpmPoolPort(siteName)}`;
|
|
86791
|
+
}
|
|
86792
|
+
function buildPhpFpmPoolConf(options) {
|
|
86793
|
+
const pool = siteToken(options.siteName);
|
|
86794
|
+
const user = siteUser(options.siteName);
|
|
86795
|
+
const maxChildren = options.maxChildren ?? 10;
|
|
86796
|
+
return [
|
|
86797
|
+
`; ts-cloud-managed pool for ${options.siteName} (site isolation)`,
|
|
86798
|
+
`[${pool}]`,
|
|
86799
|
+
`user = ${user}`,
|
|
86800
|
+
`group = ${user}`,
|
|
86801
|
+
`listen = ${phpFpmPoolListen(options.siteName)}`,
|
|
86802
|
+
"pm = dynamic",
|
|
86803
|
+
`pm.max_children = ${maxChildren}`,
|
|
86804
|
+
"pm.start_servers = 2",
|
|
86805
|
+
"pm.min_spare_servers = 1",
|
|
86806
|
+
"pm.max_spare_servers = 3",
|
|
86807
|
+
"pm.max_requests = 500",
|
|
86808
|
+
"catch_workers_output = yes",
|
|
86809
|
+
`php_admin_value[open_basedir] = ${options.appBase}:/tmp`,
|
|
86810
|
+
""
|
|
86811
|
+
].join(`
|
|
86812
|
+
`);
|
|
86813
|
+
}
|
|
86814
|
+
function buildPhpFpmPoolScript(options) {
|
|
86815
|
+
const user = siteUser(options.siteName);
|
|
86816
|
+
const pool = siteToken(options.siteName);
|
|
86817
|
+
const conf = `${PHP_FPM_POOL_DIR}/${pool}.conf`;
|
|
86818
|
+
return [
|
|
86819
|
+
`getent group ${user} >/dev/null || groupadd --system ${user}`,
|
|
86820
|
+
`id -u ${user} >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin -g ${user} ${user}`,
|
|
86821
|
+
`usermod -aG ${user} www-data 2>/dev/null || true`,
|
|
86822
|
+
`chown -R ${user}:${user} ${options.appBase} || true`,
|
|
86823
|
+
`chmod -R g+rX ${options.appBase} || true`,
|
|
86824
|
+
`chmod 600 ${options.appBase}/shared/.env 2>/dev/null || true`,
|
|
86825
|
+
`mkdir -p ${PHP_FPM_POOL_DIR}`,
|
|
86826
|
+
`cat > ${conf} <<'TS_CLOUD_FPMPOOL_EOF'`,
|
|
86827
|
+
buildPhpFpmPoolConf(options).replace(/\n$/, ""),
|
|
86828
|
+
"TS_CLOUD_FPMPOOL_EOF",
|
|
86829
|
+
`(cd ${PANTRY_PROJECT_DIR} && pantry restart php-fpm) 2>/dev/null || true`
|
|
86830
|
+
];
|
|
86831
|
+
}
|
|
86832
|
+
|
|
86665
86833
|
// src/drivers/shared/compute-deploy.ts
|
|
86666
86834
|
var noopLogger = {
|
|
86667
86835
|
info: () => {},
|
|
@@ -86709,6 +86877,7 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86709
86877
|
const useNginx = compute2?.webServer !== "rpx";
|
|
86710
86878
|
const sslProvider = resolveSslProvider(site);
|
|
86711
86879
|
const customCert = sslProvider === "custom" && site.ssl?.certPath && site.ssl?.keyPath ? { certPath: site.ssl.certPath, keyPath: site.ssl.keyPath } : undefined;
|
|
86880
|
+
const poolScript = site.isolation ? buildPhpFpmPoolScript({ siteName, appBase }) : [];
|
|
86712
86881
|
const vhostScript = useNginx ? buildNginxVhostScript({
|
|
86713
86882
|
siteName,
|
|
86714
86883
|
domain: site.domain || siteName,
|
|
@@ -86717,18 +86886,23 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86717
86886
|
appDir: `${appBase}/current`,
|
|
86718
86887
|
webDirectory: site.webDirectory,
|
|
86719
86888
|
phpVersion,
|
|
86889
|
+
fastcgiPass: site.isolation ? phpFpmPoolListen(siteName) : undefined,
|
|
86720
86890
|
redirects: site.redirects,
|
|
86721
86891
|
ssl: customCert,
|
|
86722
86892
|
auth: site.auth && site.auth.enabled !== false && site.auth.password ? { username: site.auth.username || "admin", password: site.auth.password, realm: site.auth.realm } : undefined,
|
|
86723
86893
|
serverSnippet: resolveNginxSnippet(site.nginx, compute2?.nginxTemplates),
|
|
86724
|
-
clientMaxBodySize: site.nginx?.clientMaxBodySize
|
|
86894
|
+
clientMaxBodySize: site.nginx?.clientMaxBodySize,
|
|
86895
|
+
hsts: site.ssl?.hsts,
|
|
86896
|
+
tlsProtocols: site.ssl?.tlsProtocols,
|
|
86897
|
+
security: site.security
|
|
86725
86898
|
}) : [];
|
|
86726
86899
|
const sslScript = useNginx ? buildSslScript(site) : [];
|
|
86727
86900
|
const servicesScript = siteHasServices(site) ? buildSiteServicesScript({ slug, siteName, site, phpVersion, appBase }) : [];
|
|
86901
|
+
const healthCheckScript = useNginx ? buildHealthCheckScript(site) : [];
|
|
86728
86902
|
logger4.step(`Deploying PHP site '${siteName}' to ${targets.length} target(s)...`);
|
|
86729
86903
|
const phpResult = await driver.runRemoteDeploy({
|
|
86730
86904
|
targets,
|
|
86731
|
-
commands: [...deployScript, ...vhostScript, ...sslScript, ...servicesScript],
|
|
86905
|
+
commands: [...deployScript, ...poolScript, ...vhostScript, ...sslScript, ...servicesScript, ...healthCheckScript],
|
|
86732
86906
|
comment: `ts-cloud deploy ${slug}/${siteName}@${sha}`,
|
|
86733
86907
|
tags: { Project: slug, Environment: environment, Role: "app" }
|
|
86734
86908
|
});
|
|
@@ -86786,7 +86960,10 @@ async function deploySiteRelease(driver, options, logger4 = noopLogger) {
|
|
|
86786
86960
|
redirects: site.redirects,
|
|
86787
86961
|
auth: site.auth && site.auth.enabled !== false && site.auth.password ? { username: site.auth.username || "admin", password: site.auth.password, realm: site.auth.realm } : undefined,
|
|
86788
86962
|
serverSnippet: resolveNginxSnippet(site.nginx, compute?.nginxTemplates),
|
|
86789
|
-
clientMaxBodySize: site.nginx?.clientMaxBodySize
|
|
86963
|
+
clientMaxBodySize: site.nginx?.clientMaxBodySize,
|
|
86964
|
+
hsts: site.ssl?.hsts,
|
|
86965
|
+
tlsProtocols: site.ssl?.tlsProtocols,
|
|
86966
|
+
security: site.security
|
|
86790
86967
|
}) : [];
|
|
86791
86968
|
const staticSsl = wantsNginxStatic ? buildSslScript(site) : [];
|
|
86792
86969
|
const remoteScript = [...baseScript, ...staticVhost, ...staticSsl];
|
|
@@ -67,7 +67,7 @@ export declare class PreDeployScanner {
|
|
|
67
67
|
*/
|
|
68
68
|
private isLikelyPlaceholder;
|
|
69
69
|
/**
|
|
70
|
-
* Heuristic for
|
|
70
|
+
* Heuristic for `this match has way too little entropy to be a real secret`.
|
|
71
71
|
* Real cryptographic keys spread their characters across the alphabet; ASCII
|
|
72
72
|
* art dividers, padding placeholders, and runs of repeated chars do not.
|
|
73
73
|
*
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/ts-cloud",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.18",
|
|
5
5
|
"description": "A lightweight, performant infrastructure-as-code library and CLI for deploying both server-based (EC2) and serverless applications.",
|
|
6
6
|
"author": "Chris Breuer <chris@stacksjs.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
"test": "bun test"
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"@ts-cloud/aws-types": "0.5.
|
|
93
|
-
"@ts-cloud/core": "0.5.
|
|
92
|
+
"@ts-cloud/aws-types": "0.5.18",
|
|
93
|
+
"@ts-cloud/core": "0.5.18",
|
|
94
94
|
"@stacksjs/ts-xml": "^0.1.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|