@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/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 url = `${this.buildUrl(bucket, key)}?uploads`;
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
- return uploadIdMatch[1];
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 uploadQueue = [];
8508
+ const inFlight = new Map;
8500
8509
  const uploadPart = async (data, num) => {
8501
- const url = `${this.buildUrl(bucket, key)}?partNumber=${num}&uploadId=${encodeURIComponent(uploadId)}`;
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 (uploadQueue.length >= concurrency) {
8539
- const completed = await Promise.race(uploadQueue);
8540
- parts.push(completed);
8541
- uploadQueue.splice(uploadQueue.indexOf(Promise.resolve(completed)), 1);
8556
+ if (inFlight.size >= concurrency) {
8557
+ await drainOne();
8542
8558
  }
8543
- const uploadPromise = uploadPart(partData, currentPartNumber);
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
- const remaining = await Promise.all(uploadQueue);
8559
- parts.push(...remaining);
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 = `${this.buildUrl(bucket, key)}?uploadId=${encodeURIComponent(uploadId)}`;
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
- resources.WebAclAssociation = {
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" || type === "wordpress" ? "public" : "";
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
- function buildMonitoringScript(enabled2 = true) {
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 -euo pipefail",
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
- "CPUS=$(nproc)",
84346
- "cat > " + METRICS_PATH + " <<JSON",
84347
- '{"load":$LOAD,"cpus":$CPUS,"memTotalMb":$MEM_TOTAL,"memUsedMb":$MEM_USED,"diskUsedPct":$DISK_PCT}',
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 "this match has way too little entropy to be a real secret."
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.16",
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.16",
93
- "@ts-cloud/core": "0.5.16",
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": {