@stacksjs/ts-cloud 0.7.108 → 0.7.110

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.
Files changed (135) hide show
  1. package/dist/alerts/delivery.d.ts +10 -0
  2. package/dist/alerts/model.d.ts +1 -1
  3. package/dist/api/client.d.ts +53 -0
  4. package/dist/api/handler.d.ts +10 -0
  5. package/dist/api/service.d.ts +1 -0
  6. package/dist/aws/index.js +10 -10
  7. package/dist/aws/lambda.d.ts +19 -0
  8. package/dist/bin/cli.js +1520 -1204
  9. package/dist/bin/dashboard-server.js +935 -621
  10. package/dist/chunk-19bs955w.js +12203 -0
  11. package/dist/{chunk-fmt40tpw.js → chunk-53jatzaw.js} +2 -2
  12. package/dist/{chunk-ef2dwfrg.js → chunk-8kh577qh.js} +5 -5
  13. package/dist/{chunk-s9c2x5jv.js → chunk-9v2pwxv9.js} +2 -2
  14. package/dist/chunk-aa2dkn7b.js +205 -0
  15. package/dist/{chunk-33sy72r8.js → chunk-d3vx2ekv.js} +71 -25
  16. package/dist/chunk-d48vtdj4.js +433 -0
  17. package/dist/{chunk-c6db5m63.js → chunk-t2s3gdq6.js} +253 -8
  18. package/dist/chunk-va2yd85b.js +200 -0
  19. package/dist/{chunk-3g4r09dd.js → chunk-wmbep3b5.js} +1 -1
  20. package/dist/{chunk-eg8ck25v.js → chunk-wmz8x3cy.js} +6935 -15274
  21. package/dist/{chunk-ybcz6sxc.js → chunk-x6yzgv7y.js} +33 -0
  22. package/dist/control-plane/types.d.ts +1 -1
  23. package/dist/deploy/dashboard-protection.test.d.ts +1 -0
  24. package/dist/deploy/dashboard-spend.test.d.ts +1 -0
  25. package/dist/deploy/index.js +11 -8
  26. package/dist/deploy/local-dashboard-server.d.ts +2 -0
  27. package/dist/drivers/index.js +4 -2
  28. package/dist/drivers/shared/protection.d.ts +43 -0
  29. package/dist/drivers/shared/protection.test.d.ts +1 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +364 -102
  32. package/dist/protection/controls.d.ts +127 -0
  33. package/dist/protection/controls.test.d.ts +1 -0
  34. package/dist/protection/ddos.d.ts +130 -0
  35. package/dist/protection/ddos.test.d.ts +1 -0
  36. package/dist/protection/index.d.ts +11 -0
  37. package/dist/protection/index.js +89 -0
  38. package/dist/protection/ratelimit.d.ts +154 -0
  39. package/dist/protection/ratelimit.test.d.ts +1 -0
  40. package/dist/protection/waf.d.ts +143 -0
  41. package/dist/protection/waf.test.d.ts +1 -0
  42. package/dist/spend/anomaly-config.d.ts +130 -0
  43. package/dist/spend/anomaly-config.test.d.ts +1 -0
  44. package/dist/spend/anomaly.d.ts +114 -0
  45. package/dist/spend/anomaly.test.d.ts +1 -0
  46. package/dist/spend/api.d.ts +42 -0
  47. package/dist/spend/api.test.d.ts +1 -0
  48. package/dist/spend/appliers.d.ts +126 -0
  49. package/dist/spend/docs.test.d.ts +1 -0
  50. package/dist/spend/enforcement.d.ts +111 -0
  51. package/dist/spend/enforcement.integration.test.d.ts +1 -0
  52. package/dist/spend/evaluator.d.ts +70 -0
  53. package/dist/spend/evaluator.test.d.ts +1 -0
  54. package/dist/spend/focus.d.ts +93 -0
  55. package/dist/spend/focus.test.d.ts +1 -0
  56. package/dist/spend/gate.d.ts +96 -0
  57. package/dist/spend/index.d.ts +20 -0
  58. package/dist/spend/index.js +173 -0
  59. package/dist/spend/lease.d.ts +54 -0
  60. package/dist/spend/lease.test.d.ts +1 -0
  61. package/dist/spend/meter.d.ts +70 -0
  62. package/dist/spend/meter.test.d.ts +1 -0
  63. package/dist/spend/model.d.ts +228 -0
  64. package/dist/spend/notifications.d.ts +58 -0
  65. package/dist/spend/pricing.d.ts +109 -0
  66. package/dist/spend/pricing.test.d.ts +1 -0
  67. package/dist/spend/projection.d.ts +36 -0
  68. package/dist/spend/runner.d.ts +130 -0
  69. package/dist/spend/service.d.ts +129 -0
  70. package/dist/spend/signals.d.ts +131 -0
  71. package/dist/spend/signals.test.d.ts +1 -0
  72. package/dist/spend/sms-sdk.test.d.ts +1 -0
  73. package/dist/spend/spend.integration.test.d.ts +1 -0
  74. package/dist/spend/store.d.ts +166 -0
  75. package/dist/spend/transports.d.ts +169 -0
  76. package/dist/spend/transports.test.d.ts +1 -0
  77. package/dist/spend/window.d.ts +52 -0
  78. package/dist/spend/window.test.d.ts +1 -0
  79. package/dist/ui/404.html +1 -0
  80. package/dist/ui/access-denied.html +1 -1
  81. package/dist/ui/account/automation.html +3 -3
  82. package/dist/ui/account/security.html +2 -2
  83. package/dist/ui/applications/compose.html +4 -4
  84. package/dist/ui/applications/new.html +2 -2
  85. package/dist/ui/data/backups.html +4 -4
  86. package/dist/ui/data/services.html +4 -4
  87. package/dist/ui/data/volumes.html +4 -4
  88. package/dist/ui/index.html +4 -4
  89. package/dist/ui/infrastructure/topology.html +4 -4
  90. package/dist/ui/integrations.html +2 -2
  91. package/dist/ui/operations/alerts.html +4 -4
  92. package/dist/ui/operations/configuration.html +4 -4
  93. package/dist/ui/operations/jobs.html +3 -3
  94. package/dist/ui/operations/maintenance.html +4 -4
  95. package/dist/ui/operations/observability.html +4 -4
  96. package/dist/ui/operations/previews.html +4 -4
  97. package/dist/ui/operations/queue.html +3 -3
  98. package/dist/ui/operations/regions.html +4 -4
  99. package/dist/ui/operations/releases.html +4 -4
  100. package/dist/ui/operations/spend.html +1634 -0
  101. package/dist/ui/operations/workloads.html +4 -4
  102. package/dist/ui/security.html +2 -2
  103. package/dist/ui/server/actions.html +4 -4
  104. package/dist/ui/server/activity.html +2 -2
  105. package/dist/ui/server/capacity.html +4 -4
  106. package/dist/ui/server/database.html +4 -4
  107. package/dist/ui/server/deployments.html +4 -4
  108. package/dist/ui/server/diagnostics.html +2 -2
  109. package/dist/ui/server/firewall.html +100 -161
  110. package/dist/ui/server/fleet.html +4 -4
  111. package/dist/ui/server/logs.html +4 -4
  112. package/dist/ui/server/metrics.html +4 -4
  113. package/dist/ui/server/security.html +2 -2
  114. package/dist/ui/server/services.html +2 -2
  115. package/dist/ui/server/sites.html +4 -4
  116. package/dist/ui/server/ssh-keys.html +4 -4
  117. package/dist/ui/server/team.html +4 -4
  118. package/dist/ui/server/terminal.html +2 -2
  119. package/dist/ui/serverless/alarms.html +4 -4
  120. package/dist/ui/serverless/assets.html +2 -2
  121. package/dist/ui/serverless/cost.html +2 -2
  122. package/dist/ui/serverless/data.html +4 -4
  123. package/dist/ui/serverless/deployments.html +1 -1
  124. package/dist/ui/serverless/firewall.html +2 -2
  125. package/dist/ui/serverless/functions.html +4 -4
  126. package/dist/ui/serverless/logs.html +4 -4
  127. package/dist/ui/serverless/metrics.html +2 -2
  128. package/dist/ui/serverless/queues.html +4 -4
  129. package/dist/ui/serverless/secrets.html +4 -4
  130. package/dist/ui/serverless/traces.html +4 -4
  131. package/dist/ui/serverless.html +3 -3
  132. package/dist/ui-src/pages/operations/spend.stx +225 -0
  133. package/dist/ui-src/pages/partials/nav.stx +2 -0
  134. package/dist/ui-src/pages/server/firewall.stx +89 -0
  135. package/package.json +11 -3
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  buildAndPushServerlessImage
3
- } from "./chunk-3g4r09dd.js";
3
+ } from "./chunk-wmbep3b5.js";
4
4
  import"./chunk-50jpda9q.js";
5
- import"./chunk-c6db5m63.js";
6
5
  import"./chunk-zqtpg06c.js";
6
+ import"./chunk-t2s3gdq6.js";
7
7
  import"./chunk-v0bahtg2.js";
8
8
  export {
9
9
  buildAndPushServerlessImage
@@ -8,12 +8,12 @@ import {
8
8
  UnifiedDnsValidator,
9
9
  createDnsProvider
10
10
  } from "./chunk-gttvakpv.js";
11
- import {
12
- CloudFormationClient
13
- } from "./chunk-4cjrg98a.js";
14
11
  import {
15
12
  Route53Client
16
13
  } from "./chunk-32e7ya18.js";
14
+ import {
15
+ CloudFormationClient
16
+ } from "./chunk-4cjrg98a.js";
17
17
  import {
18
18
  S3Client
19
19
  } from "./chunk-wj3s95p9.js";
@@ -815,7 +815,7 @@ async function deployStaticSiteWithExternalDnsFull(config) {
815
815
  }
816
816
  }
817
817
  onProgress?.("upload", "Uploading files to S3...");
818
- const { uploadStaticFiles } = await import("./chunk-s9c2x5jv.js");
818
+ const { uploadStaticFiles } = await import("./chunk-9v2pwxv9.js");
819
819
  const uploadResult = await uploadStaticFiles({
820
820
  sourceDir,
821
821
  bucket: infraResult.bucket,
@@ -835,7 +835,7 @@ async function deployStaticSiteWithExternalDnsFull(config) {
835
835
  }
836
836
  if (infraResult.distributionId && uploadResult.uploaded > 0) {
837
837
  onProgress?.("invalidate", "Invalidating CloudFront cache...");
838
- const { invalidateCache } = await import("./chunk-s9c2x5jv.js");
838
+ const { invalidateCache } = await import("./chunk-9v2pwxv9.js");
839
839
  await invalidateCache(infraResult.distributionId);
840
840
  }
841
841
  onProgress?.("complete", "Deployment complete!");
@@ -5,11 +5,11 @@ import {
5
5
  generateStaticSiteTemplate,
6
6
  invalidateCache,
7
7
  uploadStaticFiles
8
- } from "./chunk-ef2dwfrg.js";
8
+ } from "./chunk-8kh577qh.js";
9
9
  import"./chunk-01d86gt1.js";
10
10
  import"./chunk-gttvakpv.js";
11
- import"./chunk-4cjrg98a.js";
12
11
  import"./chunk-32e7ya18.js";
12
+ import"./chunk-4cjrg98a.js";
13
13
  import"./chunk-wj3s95p9.js";
14
14
  import"./chunk-zqtpg06c.js";
15
15
  import"./chunk-v0bahtg2.js";
@@ -0,0 +1,205 @@
1
+ // src/protection/ddos.ts
2
+ var DEFAULT_DDOS_THRESHOLDS = {
3
+ newConnectionsPerSecond: 50,
4
+ concurrentPerSource: 100,
5
+ synPerSecond: 2000,
6
+ burst: 100,
7
+ icmpPerSecond: 5,
8
+ banSeconds: 600
9
+ };
10
+ function resolveThresholds(config) {
11
+ return { ...DEFAULT_DDOS_THRESHOLDS, ...config.thresholds };
12
+ }
13
+ var CIDR_PATTERN = /^(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?$|^[0-9a-f:]+(?:\/\d{1,3})?$/i;
14
+ function isValidCidr(value) {
15
+ if (!CIDR_PATTERN.test(value))
16
+ return false;
17
+ const [address, prefix] = value.split("/");
18
+ if (address.includes(":"))
19
+ return prefix == null || Number(prefix) >= 0 && Number(prefix) <= 128;
20
+ const octets = address.split(".");
21
+ if (octets.length !== 4 || octets.some((octet) => Number(octet) > 255 || octet === ""))
22
+ return false;
23
+ return prefix == null || Number(prefix) >= 0 && Number(prefix) <= 32;
24
+ }
25
+ function validCidrs(values) {
26
+ return [...new Set((values ?? []).map((value) => value.trim()).filter(isValidCidr))];
27
+ }
28
+ function validPorts(values) {
29
+ const ports = (values ?? [80, 443]).filter((port) => Number.isInteger(port) && port >= 1 && port <= 65535);
30
+ return [...new Set(ports)].sort((a, b) => a - b);
31
+ }
32
+ function sysctlHardening(config = {}) {
33
+ const thresholds = resolveThresholds(config);
34
+ return {
35
+ "net.ipv4.tcp_syncookies": "1",
36
+ "net.ipv4.tcp_max_syn_backlog": String(Math.max(4096, thresholds.synPerSecond * 4)),
37
+ "net.ipv4.tcp_synack_retries": "2",
38
+ "net.ipv4.tcp_abort_on_overflow": "1",
39
+ "net.ipv4.tcp_fin_timeout": "15",
40
+ "net.ipv4.tcp_keepalive_time": "300",
41
+ "net.ipv4.tcp_keepalive_intvl": "30",
42
+ "net.ipv4.tcp_keepalive_probes": "3",
43
+ "net.core.somaxconn": "4096",
44
+ "net.core.netdev_max_backlog": "16384",
45
+ "net.ipv4.conf.all.rp_filter": "1",
46
+ "net.ipv4.conf.default.rp_filter": "1",
47
+ "net.ipv4.conf.all.accept_source_route": "0",
48
+ "net.ipv4.conf.all.accept_redirects": "0",
49
+ "net.ipv4.conf.all.send_redirects": "0",
50
+ "net.ipv6.conf.all.accept_redirects": "0",
51
+ "net.ipv6.conf.all.accept_source_route": "0",
52
+ "net.ipv4.icmp_echo_ignore_broadcasts": "1",
53
+ "net.ipv4.icmp_ignore_bogus_error_responses": "1",
54
+ "net.netfilter.nf_conntrack_max": "262144",
55
+ "net.netfilter.nf_conntrack_tcp_timeout_established": "3600"
56
+ };
57
+ }
58
+ function renderSysctlFile(config = {}) {
59
+ const lines = [
60
+ "# Managed by ts-cloud. Edits are overwritten on the next deploy.",
61
+ "# Network hardening against L3/L4 floods.",
62
+ ""
63
+ ];
64
+ for (const [key, value] of Object.entries(sysctlHardening(config)))
65
+ lines.push(`${key} = ${value}`);
66
+ return `${lines.join(`
67
+ `)}
68
+ `;
69
+ }
70
+ function renderNftablesRuleset(config = {}) {
71
+ const thresholds = resolveThresholds(config);
72
+ const ports = validPorts(config.ports);
73
+ const allowlist = validCidrs(config.allowlist);
74
+ const blocklist = validCidrs(config.blocklist);
75
+ const verdict = config.monitorOnly ? "counter" : "counter drop";
76
+ const cdnRanges = config.originProtection?.enabled ? validCidrs(config.originProtection.cdnRanges) : [];
77
+ const ipv4Allow = allowlist.filter((cidr) => !cidr.includes(":"));
78
+ const ipv6Allow = allowlist.filter((cidr) => cidr.includes(":"));
79
+ const ipv4Block = blocklist.filter((cidr) => !cidr.includes(":"));
80
+ const lines = [
81
+ "#!/usr/sbin/nft -f",
82
+ "# Managed by ts-cloud. Edits are overwritten on the next deploy.",
83
+ "",
84
+ "table inet ts_cloud_ddos",
85
+ "delete table inet ts_cloud_ddos",
86
+ "",
87
+ "table inet ts_cloud_ddos {",
88
+ " # Sources that tripped a limit, with a timeout so a ban self-expires.",
89
+ ` set banned4 { type ipv4_addr; flags timeout; timeout ${thresholds.banSeconds}s; }`,
90
+ ` set banned6 { type ipv6_addr; flags timeout; timeout ${thresholds.banSeconds}s; }`
91
+ ];
92
+ if (ipv4Allow.length > 0)
93
+ lines.push(` set allow4 { type ipv4_addr; flags interval; elements = { ${ipv4Allow.join(", ")} } }`);
94
+ if (ipv6Allow.length > 0)
95
+ lines.push(` set allow6 { type ipv6_addr; flags interval; elements = { ${ipv6Allow.join(", ")} } }`);
96
+ if (ipv4Block.length > 0)
97
+ lines.push(` set block4 { type ipv4_addr; flags interval; elements = { ${ipv4Block.join(", ")} } }`);
98
+ if (cdnRanges.length > 0)
99
+ lines.push(` set cdn4 { type ipv4_addr; flags interval; elements = { ${cdnRanges.filter((cidr) => !cidr.includes(":")).join(", ")} } }`);
100
+ lines.push("", " chain input {", " type filter hook input priority -150; policy accept;", "", " # Established traffic is already paid for; never re-inspect it.", " ct state established,related accept", " iif lo accept", "", " # Invalid packets are never legitimate and are cheap to drop early.", " ct state invalid counter drop");
101
+ if (ipv4Allow.length > 0)
102
+ lines.push(" ip saddr @allow4 accept");
103
+ if (ipv6Allow.length > 0)
104
+ lines.push(" ip6 saddr @allow6 accept");
105
+ if (ipv4Block.length > 0)
106
+ lines.push(" ip saddr @block4 counter drop");
107
+ lines.push(" ip saddr @banned4 counter drop", " ip6 saddr @banned6 counter drop", "");
108
+ lines.push(" # Nonsense TCP flag combinations: scanners and stack fingerprinting.", " tcp flags & (fin|syn) == (fin|syn) counter drop", " tcp flags & (syn|rst) == (syn|rst) counter drop", " tcp flags & (fin|rst) == (fin|rst) counter drop", " tcp flags & (fin|ack) == fin counter drop", " tcp flags & (syn|fin|ack|rst|psh|urg) == 0x0 counter drop", "", ` # Ping stays usable, ping floods do not.`, ` ip protocol icmp icmp type echo-request limit rate ${thresholds.icmpPerSecond}/second burst ${thresholds.burst} packets accept`, " ip protocol icmp icmp type echo-request counter drop", "");
109
+ if (ports.length > 0) {
110
+ const portSet = ports.join(", ");
111
+ lines.push(` # Platform-wide SYN ceiling. Above it the kernel falls back to cookies.`, ` tcp dport { ${portSet} } ct state new limit rate ${thresholds.synPerSecond}/second burst ${thresholds.burst} packets accept`, "", ` # Per-source new-connection rate. This is what stops one host hammering.`, ` tcp dport { ${portSet} } ct state new meter conn_rate4 { ip saddr limit rate over ${thresholds.newConnectionsPerSecond}/second burst ${thresholds.burst} packets } add @banned4 { ip saddr } ${verdict}`, ` tcp dport { ${portSet} } ct state new meter conn_rate6 { ip6 saddr limit rate over ${thresholds.newConnectionsPerSecond}/second burst ${thresholds.burst} packets } add @banned6 { ip6 saddr } ${verdict}`, "", ` # Concurrency ceiling per source: the slow-loris defence.`, ` tcp dport { ${portSet} } ct state new meter conn_count4 { ip saddr ct count over ${thresholds.concurrentPerSource} } ${verdict}`, ` tcp dport { ${portSet} } ct state new meter conn_count6 { ip6 saddr ct count over ${thresholds.concurrentPerSource} } ${verdict}`, "");
112
+ if (cdnRanges.length > 0)
113
+ lines.push(" # Origin protection: reject traffic that skipped the CDN and came", " # straight to the box, which is how an attacker bypasses the edge.", ` tcp dport { ${portSet} } ip saddr != @cdn4 counter drop`, "");
114
+ }
115
+ lines.push(" }", "}", "");
116
+ return lines.join(`
117
+ `);
118
+ }
119
+ function renderDdosInstallScript(config = {}) {
120
+ return `#!/usr/bin/env bash
121
+ # Managed by ts-cloud. Installs L3/L4 flood mitigation.
122
+ set -euo pipefail
123
+
124
+ if ! command -v nft >/dev/null 2>&1; then
125
+ export DEBIAN_FRONTEND=noninteractive
126
+ apt-get update -qq
127
+ apt-get install -y -qq nftables
128
+ fi
129
+
130
+ install -d -m 0755 /etc/ts-cloud /etc/nftables.d
131
+ cat > /etc/sysctl.d/90-ts-cloud-ddos.conf <<'SYSCTL_EOF'
132
+ ${renderSysctlFile(config)}SYSCTL_EOF
133
+ sysctl --system >/dev/null
134
+
135
+ cat > /etc/nftables.d/ts-cloud-ddos.nft.new <<'NFT_EOF'
136
+ ${renderNftablesRuleset(config)}NFT_EOF
137
+
138
+ # Syntax-check before applying: a bad ruleset would leave the box unprotected.
139
+ if ! nft -c -f /etc/nftables.d/ts-cloud-ddos.nft.new; then
140
+ echo "ts-cloud: generated nftables ruleset failed validation, keeping the previous one" >&2
141
+ rm -f /etc/nftables.d/ts-cloud-ddos.nft.new
142
+ exit 1
143
+ fi
144
+
145
+ mv /etc/nftables.d/ts-cloud-ddos.nft.new /etc/nftables.d/ts-cloud-ddos.nft
146
+ nft -f /etc/nftables.d/ts-cloud-ddos.nft
147
+
148
+ # Applying the ruleset does not survive a reboot on its own: nftables.service
149
+ # loads /etc/nftables.conf, so the file has to be included from there or the box
150
+ # comes back up unprotected and nothing says so.
151
+ touch /etc/nftables.conf
152
+ if ! grep -q 'ts-cloud-ddos.nft' /etc/nftables.conf; then
153
+ printf '
154
+ include "/etc/nftables.d/ts-cloud-ddos.nft"
155
+ ' >> /etc/nftables.conf
156
+ fi
157
+ systemctl enable nftables >/dev/null 2>&1 || true
158
+ echo "ts-cloud: L3/L4 mitigation active"
159
+ `;
160
+ }
161
+ var LEVEL_ORDER = ["off", "monitor", "rate_limit", "challenge", "lockdown"];
162
+ function planMitigation(signals, topSources = []) {
163
+ const reasons = [];
164
+ const baseline = Math.max(1, signals.baselineRequestsPerSecond);
165
+ const ratio = signals.requestsPerSecond / baseline;
166
+ let levelIndex = 0;
167
+ const escalate = (next, reason) => {
168
+ reasons.push(reason);
169
+ levelIndex = Math.max(levelIndex, LEVEL_ORDER.indexOf(next));
170
+ };
171
+ const surging = ratio >= 3;
172
+ if (surging)
173
+ escalate("monitor", `Traffic is ${ratio.toFixed(1)}x the baseline.`);
174
+ const concentrated = signals.topSourceShare >= 0.25;
175
+ const failing = signals.errorRate >= 0.25;
176
+ const scripted = (signals.suspiciousAgentShare ?? 0) >= 0.5;
177
+ const thinlySpread = signals.uniqueSources > 0 && signals.requestsPerSecond / signals.uniqueSources >= 50;
178
+ if (surging && concentrated)
179
+ escalate("rate_limit", `One source accounts for ${(signals.topSourceShare * 100).toFixed(0)}% of traffic.`);
180
+ if (surging && thinlySpread)
181
+ escalate("rate_limit", "Request rate per unique source is far above what a browser produces.");
182
+ if (surging && failing)
183
+ escalate("challenge", `Error rate is ${(signals.errorRate * 100).toFixed(0)}% under load.`);
184
+ if (surging && scripted)
185
+ escalate("challenge", "Most requests carry no recognisable browser agent.");
186
+ if (ratio >= 20 && (concentrated || failing || scripted || thinlySpread))
187
+ escalate("lockdown", `Traffic is ${ratio.toFixed(0)}x the baseline with attack-shaped characteristics.`);
188
+ if (!surging && signals.topSourceShare >= 0.6 && signals.requestsPerSecond > baseline)
189
+ escalate("rate_limit", "A single source dominates traffic without an overall surge.");
190
+ const level = LEVEL_ORDER[levelIndex];
191
+ const factor = { off: 1, monitor: 1, rate_limit: 0.5, challenge: 0.25, lockdown: 0.1 }[level];
192
+ return {
193
+ level,
194
+ reasons,
195
+ rateLimitFactor: factor,
196
+ challengeEnabled: level === "challenge" || level === "lockdown",
197
+ staticOnly: level === "lockdown",
198
+ banCandidates: level === "off" || level === "monitor" ? [] : validCidrs(topSources)
199
+ };
200
+ }
201
+ function applyMitigationFactor(limit, plan) {
202
+ return Math.max(1, Math.floor(limit * plan.rateLimitFactor));
203
+ }
204
+
205
+ export { DEFAULT_DDOS_THRESHOLDS, isValidCidr, sysctlHardening, renderSysctlFile, renderNftablesRuleset, renderDdosInstallScript, planMitigation, applyMitigationFactor };
@@ -1,14 +1,3 @@
1
- import {
2
- deploymentCoexistenceError,
3
- hasManagementDashboardSite,
4
- isManagementDashboardSiteName,
5
- resolveAppDatabase,
6
- resolveCloudProvider,
7
- resolveManagementDashboardSites,
8
- resolveProjectStackName,
9
- resolveStatePath,
10
- statePath
11
- } from "./chunk-c6db5m63.js";
12
1
  import {
13
2
  EC2Client,
14
3
  SSMClient
@@ -19,6 +8,23 @@ import {
19
8
  import {
20
9
  S3Client
21
10
  } from "./chunk-wj3s95p9.js";
11
+ import {
12
+ renderWafInstallScript
13
+ } from "./chunk-va2yd85b.js";
14
+ import {
15
+ renderDdosInstallScript
16
+ } from "./chunk-aa2dkn7b.js";
17
+ import {
18
+ deploymentCoexistenceError,
19
+ hasManagementDashboardSite,
20
+ isManagementDashboardSiteName,
21
+ resolveAppDatabase,
22
+ resolveCloudProvider,
23
+ resolveManagementDashboardSites,
24
+ resolveProjectStackName,
25
+ resolveStatePath,
26
+ statePath
27
+ } from "./chunk-t2s3gdq6.js";
22
28
  import {
23
29
  __require
24
30
  } from "./chunk-v0bahtg2.js";
@@ -1926,6 +1932,45 @@ function escapeSed(value) {
1926
1932
  return value.replace(/[.*[\]\\/^$]/g, "\\$&");
1927
1933
  }
1928
1934
 
1935
+ // src/drivers/shared/protection.ts
1936
+ function runAsScript(name, script) {
1937
+ const path = `/tmp/ts-cloud-${name}.sh`;
1938
+ const marker = `TS_CLOUD_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_EOF`;
1939
+ return [
1940
+ `cat > ${path} <<'${marker}'`,
1941
+ script.replace(/\n$/, ""),
1942
+ marker,
1943
+ `chmod +x ${path}`,
1944
+ `bash ${path} || echo "ts-cloud: ${name} setup did not complete; continuing" >&2`,
1945
+ `rm -f ${path}`
1946
+ ];
1947
+ }
1948
+ function resolve(setting, fallback) {
1949
+ if (setting === false)
1950
+ return;
1951
+ if (setting === true || setting === undefined)
1952
+ return fallback;
1953
+ return setting;
1954
+ }
1955
+ var DEFAULT_COMPUTE_DDOS = { enabled: true, ports: [80, 443] };
1956
+ var DEFAULT_COMPUTE_WAF = { mode: "detection", paranoiaLevel: 1 };
1957
+ function buildDdosScript(setting = true, extraPorts = []) {
1958
+ const config = resolve(setting, DEFAULT_COMPUTE_DDOS);
1959
+ if (!config || config.enabled === false)
1960
+ return [];
1961
+ const ports = [...new Set([...config.ports ?? DEFAULT_COMPUTE_DDOS.ports ?? [], ...extraPorts])].sort((a, b) => a - b);
1962
+ return runAsScript("ddos", renderDdosInstallScript({ ...config, ports }));
1963
+ }
1964
+ function buildWafScript(setting = true) {
1965
+ const config = resolve(setting, DEFAULT_COMPUTE_WAF);
1966
+ if (!config || config.mode === "off")
1967
+ return [];
1968
+ return runAsScript("waf", renderWafInstallScript(config));
1969
+ }
1970
+ function buildProtectionScript(protection = {}, extraPorts = []) {
1971
+ return [...buildDdosScript(protection.ddos, extraPorts), ...buildWafScript(protection.waf)];
1972
+ }
1973
+
1929
1974
  // src/drivers/shared/ufw.ts
1930
1975
  var UFW_BASE_PORTS = [80, 443];
1931
1976
  function buildUfwScript(firewall = {}) {
@@ -1974,6 +2019,7 @@ function buildComputeProvisionScripts(config) {
1974
2019
  extras.push(...buildServicesProvisionScript(compute.managedServices), ...buildDatabaseSetupScript(appDatabase, compute.managedServices));
1975
2020
  }
1976
2021
  extras.push(...buildUfwScript(compute.firewall ?? (phpBox ? { enabled: true } : { enabled: false })));
2022
+ extras.push(...buildProtectionScript({ ddos: compute.ddos, waf: compute.waf }, compute.firewall?.allowedPorts ?? []));
1977
2023
  extras.push(...buildAutoUpdatesScript(compute.autoUpdates ?? phpBox));
1978
2024
  extras.push(...buildMonitoringScript(compute.monitoring ?? phpBox));
1979
2025
  extras.push(...buildAuthorizedKeysScript(compute.sshKeys));
@@ -2507,7 +2553,7 @@ class AwsDriver {
2507
2553
  const terminalStatuses = new Set(["Success", "Failed", "Cancelled", "TimedOut"]);
2508
2554
  let lastInvocations = [];
2509
2555
  while (Date.now() - startTime < maxWait) {
2510
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
2556
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
2511
2557
  try {
2512
2558
  const invocations = await ssm.listCommandInvocations({ CommandId: commandId, Details: true });
2513
2559
  lastInvocations = invocations;
@@ -2886,7 +2932,7 @@ class HetznerClient {
2886
2932
  while (Date.now() - start < grace) {
2887
2933
  if ((await this.getServer(serverId)).status === "off")
2888
2934
  return;
2889
- await new Promise((resolve) => setTimeout(resolve, poll));
2935
+ await new Promise((resolve2) => setTimeout(resolve2, poll));
2890
2936
  }
2891
2937
  const action = await this.powerOffServer(serverId);
2892
2938
  await this.waitForAction(action.id);
@@ -2941,7 +2987,7 @@ class HetznerClient {
2941
2987
  if (action.status === "error") {
2942
2988
  throw new HetznerActionError(action.error?.message || "Hetzner action failed", action);
2943
2989
  }
2944
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
2990
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
2945
2991
  }
2946
2992
  throw new Error(`Timed out waiting for Hetzner action ${actionId}`);
2947
2993
  }
@@ -2953,7 +2999,7 @@ class HetznerClient {
2953
2999
  const server = await this.getServer(serverId);
2954
3000
  if (server.status === "running")
2955
3001
  return server;
2956
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
3002
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
2957
3003
  }
2958
3004
  throw new Error(`Timed out waiting for server ${serverId} to reach running state`);
2959
3005
  }
@@ -2965,7 +3011,7 @@ class HetznerClient {
2965
3011
  const server = await this.getServer(serverId);
2966
3012
  if (server.status === status)
2967
3013
  return server;
2968
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
3014
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
2969
3015
  }
2970
3016
  throw new Error(`Timed out waiting for server ${serverId} to reach ${status} state`);
2971
3017
  }
@@ -3913,7 +3959,7 @@ class HetznerDriver {
3913
3959
  return [...ports].filter((port) => ![80, 443].includes(port));
3914
3960
  }
3915
3961
  async sleep(ms) {
3916
- await new Promise((resolve) => setTimeout(resolve, ms));
3962
+ await new Promise((resolve2) => setTimeout(resolve2, ms));
3917
3963
  }
3918
3964
  async waitForSshReady(host) {
3919
3965
  const { sshIntervalMs, sshTimeoutMs } = this.bootWait;
@@ -4324,7 +4370,7 @@ async function waitForSsh(host, options = {}) {
4324
4370
  const r = await sshExec(host, "echo ready", options);
4325
4371
  if (r.code === 0 && r.stdout.includes("ready"))
4326
4372
  return;
4327
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
4373
+ await new Promise((resolve2) => setTimeout(resolve2, pollIntervalMs));
4328
4374
  }
4329
4375
  throw new Error(`SSH not ready on ${host} after ${timeoutMs}ms`);
4330
4376
  }
@@ -4338,7 +4384,7 @@ async function waitForCloudInit(host, options = {}) {
4338
4384
  return;
4339
4385
  if (r.stdout.includes("status: error"))
4340
4386
  throw new Error(`cloud-init reported an error on ${host}; check /var/log/cloud-init-output.log`);
4341
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
4387
+ await new Promise((resolve2) => setTimeout(resolve2, pollIntervalMs));
4342
4388
  }
4343
4389
  throw new Error(`cloud-init did not finish on ${host} after ${timeoutMs}ms`);
4344
4390
  }
@@ -4934,7 +4980,7 @@ class HetznerBoxProvisioner {
4934
4980
  } catch {
4935
4981
  if (i === 9)
4936
4982
  break;
4937
- await new Promise((resolve) => setTimeout(resolve, 3000));
4983
+ await new Promise((resolve2) => setTimeout(resolve2, 3000));
4938
4984
  }
4939
4985
  }
4940
4986
  }
@@ -4974,7 +5020,7 @@ class AwsBoxProvisioner {
4974
5020
  const instance = await this.ec2.getInstance(instanceId);
4975
5021
  if (instance?.State?.Name === "running" && instance.PublicIpAddress)
4976
5022
  return instance;
4977
- await new Promise((resolve) => setTimeout(resolve, 5000));
5023
+ await new Promise((resolve2) => setTimeout(resolve2, 5000));
4978
5024
  }
4979
5025
  throw new Error(`EC2 instance ${instanceId} did not reach running with a public IP within ${timeoutMs}ms`);
4980
5026
  }
@@ -5050,7 +5096,7 @@ class AwsBoxProvisioner {
5050
5096
  const current = await this.ec2.getInstance(instance.InstanceId);
5051
5097
  if (!current || current.State?.Name === "terminated")
5052
5098
  break;
5053
- await new Promise((resolve) => setTimeout(resolve, 5000));
5099
+ await new Promise((resolve2) => setTimeout(resolve2, 5000));
5054
5100
  }
5055
5101
  }
5056
5102
  const groups = await this.ec2.describeSecurityGroups({
@@ -5066,7 +5112,7 @@ class AwsBoxProvisioner {
5066
5112
  } catch {
5067
5113
  if (i === 9)
5068
5114
  break;
5069
- await new Promise((resolve) => setTimeout(resolve, 3000));
5115
+ await new Promise((resolve2) => setTimeout(resolve2, 3000));
5070
5116
  }
5071
5117
  }
5072
5118
  }
@@ -5501,7 +5547,7 @@ import { execSync as execSync2 } from "node:child_process";
5501
5547
  import { randomBytes } from "node:crypto";
5502
5548
  import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, statSync, writeFileSync } from "node:fs";
5503
5549
  import { tmpdir } from "node:os";
5504
- import { basename, dirname, isAbsolute, join as join5, resolve } from "node:path";
5550
+ import { basename, dirname, isAbsolute, join as join5, resolve as resolve2 } from "node:path";
5505
5551
  import { fileURLToPath } from "node:url";
5506
5552
 
5507
5553
  // src/deploy/dashboard-config-module.ts
@@ -5621,7 +5667,7 @@ function stageLiveDashboardRoot(config, cwd, logger) {
5621
5667
  let dependency = resolveDashboardVersion();
5622
5668
  if (dependency.startsWith("file:")) {
5623
5669
  const requested = dependency.slice("file:".length);
5624
- const source = isAbsolute(requested) ? requested : resolve(cwd, requested);
5670
+ const source = isAbsolute(requested) ? requested : resolve2(cwd, requested);
5625
5671
  if (!existsSync2(source) || !statSync(source).isFile()) {
5626
5672
  dependency = resolvePackagedDashboardVersion();
5627
5673
  logger.warn(`Management dashboard: TS_CLOUD_UI_VERSION points to a missing package file (${source}); using the deploying ts-cloud version ${dependency} instead.`);