@stacksjs/ts-cloud 0.7.108 → 0.7.109
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/alerts/delivery.d.ts +10 -0
- package/dist/alerts/model.d.ts +1 -1
- package/dist/api/client.d.ts +53 -0
- package/dist/api/handler.d.ts +10 -0
- package/dist/api/service.d.ts +1 -0
- package/dist/aws/index.js +10 -10
- package/dist/aws/lambda.d.ts +19 -0
- package/dist/bin/cli.js +1677 -1369
- package/dist/bin/dashboard-server.js +927 -621
- package/dist/{chunk-fmt40tpw.js → chunk-53jatzaw.js} +2 -2
- package/dist/{chunk-ef2dwfrg.js → chunk-8kh577qh.js} +5 -5
- package/dist/{chunk-s9c2x5jv.js → chunk-9v2pwxv9.js} +2 -2
- package/dist/chunk-aa2dkn7b.js +205 -0
- package/dist/{chunk-eg8ck25v.js → chunk-cktwk9hy.js} +6935 -15274
- package/dist/{chunk-33sy72r8.js → chunk-d3vx2ekv.js} +71 -25
- package/dist/chunk-grx1rp09.js +12163 -0
- package/dist/{chunk-c6db5m63.js → chunk-t2s3gdq6.js} +253 -8
- package/dist/chunk-va2yd85b.js +200 -0
- package/dist/{chunk-3g4r09dd.js → chunk-wmbep3b5.js} +1 -1
- package/dist/{chunk-ybcz6sxc.js → chunk-x6yzgv7y.js} +33 -0
- package/dist/chunk-xrtwv3v2.js +420 -0
- package/dist/control-plane/types.d.ts +1 -1
- package/dist/deploy/dashboard-protection.test.d.ts +1 -0
- package/dist/deploy/dashboard-spend.test.d.ts +1 -0
- package/dist/deploy/index.js +11 -8
- package/dist/deploy/local-dashboard-server.d.ts +2 -0
- package/dist/drivers/index.js +4 -2
- package/dist/drivers/shared/protection.d.ts +43 -0
- package/dist/drivers/shared/protection.test.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +364 -102
- package/dist/protection/controls.d.ts +113 -0
- package/dist/protection/controls.test.d.ts +1 -0
- package/dist/protection/ddos.d.ts +130 -0
- package/dist/protection/ddos.test.d.ts +1 -0
- package/dist/protection/index.d.ts +11 -0
- package/dist/protection/index.js +87 -0
- package/dist/protection/ratelimit.d.ts +154 -0
- package/dist/protection/ratelimit.test.d.ts +1 -0
- package/dist/protection/waf.d.ts +143 -0
- package/dist/protection/waf.test.d.ts +1 -0
- package/dist/spend/anomaly-config.d.ts +130 -0
- package/dist/spend/anomaly-config.test.d.ts +1 -0
- package/dist/spend/anomaly.d.ts +114 -0
- package/dist/spend/anomaly.test.d.ts +1 -0
- package/dist/spend/api.d.ts +42 -0
- package/dist/spend/api.test.d.ts +1 -0
- package/dist/spend/appliers.d.ts +126 -0
- package/dist/spend/docs.test.d.ts +1 -0
- package/dist/spend/enforcement.d.ts +111 -0
- package/dist/spend/enforcement.integration.test.d.ts +1 -0
- package/dist/spend/evaluator.d.ts +70 -0
- package/dist/spend/evaluator.test.d.ts +1 -0
- package/dist/spend/focus.d.ts +93 -0
- package/dist/spend/focus.test.d.ts +1 -0
- package/dist/spend/gate.d.ts +96 -0
- package/dist/spend/index.d.ts +20 -0
- package/dist/spend/index.js +173 -0
- package/dist/spend/lease.d.ts +54 -0
- package/dist/spend/lease.test.d.ts +1 -0
- package/dist/spend/meter.d.ts +70 -0
- package/dist/spend/meter.test.d.ts +1 -0
- package/dist/spend/model.d.ts +228 -0
- package/dist/spend/notifications.d.ts +58 -0
- package/dist/spend/pricing.d.ts +109 -0
- package/dist/spend/pricing.test.d.ts +1 -0
- package/dist/spend/projection.d.ts +36 -0
- package/dist/spend/runner.d.ts +130 -0
- package/dist/spend/service.d.ts +129 -0
- package/dist/spend/signals.d.ts +110 -0
- package/dist/spend/signals.test.d.ts +1 -0
- package/dist/spend/sms-sdk.test.d.ts +1 -0
- package/dist/spend/spend.integration.test.d.ts +1 -0
- package/dist/spend/store.d.ts +166 -0
- package/dist/spend/transports.d.ts +169 -0
- package/dist/spend/transports.test.d.ts +1 -0
- package/dist/spend/window.d.ts +52 -0
- package/dist/spend/window.test.d.ts +1 -0
- package/dist/ui/404.html +1 -0
- package/dist/ui/access-denied.html +2 -2
- package/dist/ui/account/automation.html +3 -3
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +3 -3
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +4 -4
- package/dist/ui/data/services.html +4 -4
- package/dist/ui/data/volumes.html +4 -4
- package/dist/ui/index.html +4 -4
- package/dist/ui/infrastructure/topology.html +4 -4
- package/dist/ui/integrations.html +1 -1
- package/dist/ui/operations/alerts.html +4 -4
- package/dist/ui/operations/configuration.html +4 -4
- package/dist/ui/operations/jobs.html +4 -4
- package/dist/ui/operations/maintenance.html +4 -4
- package/dist/ui/operations/observability.html +4 -4
- package/dist/ui/operations/previews.html +4 -4
- package/dist/ui/operations/queue.html +4 -4
- package/dist/ui/operations/regions.html +4 -4
- package/dist/ui/operations/releases.html +4 -4
- package/dist/ui/operations/spend.html +1634 -0
- package/dist/ui/operations/workloads.html +4 -4
- package/dist/ui/security.html +2 -2
- package/dist/ui/server/actions.html +4 -4
- package/dist/ui/server/activity.html +2 -2
- package/dist/ui/server/capacity.html +4 -4
- package/dist/ui/server/database.html +4 -4
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +100 -161
- package/dist/ui/server/fleet.html +4 -4
- package/dist/ui/server/logs.html +4 -4
- package/dist/ui/server/metrics.html +4 -4
- package/dist/ui/server/security.html +2 -2
- package/dist/ui/server/services.html +2 -2
- package/dist/ui/server/sites.html +3 -3
- package/dist/ui/server/ssh-keys.html +4 -4
- package/dist/ui/server/team.html +4 -4
- package/dist/ui/server/terminal.html +2 -2
- package/dist/ui/serverless/alarms.html +4 -4
- package/dist/ui/serverless/assets.html +2 -2
- package/dist/ui/serverless/cost.html +2 -2
- package/dist/ui/serverless/data.html +4 -4
- package/dist/ui/serverless/deployments.html +1 -1
- package/dist/ui/serverless/firewall.html +2 -2
- package/dist/ui/serverless/functions.html +4 -4
- package/dist/ui/serverless/logs.html +4 -4
- package/dist/ui/serverless/metrics.html +2 -2
- package/dist/ui/serverless/queues.html +4 -4
- package/dist/ui/serverless/secrets.html +4 -4
- package/dist/ui/serverless/traces.html +4 -4
- package/dist/ui/serverless.html +4 -4
- package/dist/ui-src/pages/operations/spend.stx +225 -0
- package/dist/ui-src/pages/partials/nav.stx +2 -0
- package/dist/ui-src/pages/server/firewall.stx +89 -0
- package/package.json +11 -3
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isValidCidr
|
|
3
|
+
} from "./chunk-aa2dkn7b.js";
|
|
4
|
+
// src/protection/ratelimit.ts
|
|
5
|
+
var RATE_LIMIT_KEY_SEPARATOR = "\x00";
|
|
6
|
+
var GLOB_PLACEHOLDER = "\x01";
|
|
7
|
+
function globMatches(pattern, value) {
|
|
8
|
+
if (pattern === "*" || pattern === "**")
|
|
9
|
+
return true;
|
|
10
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, GLOB_PLACEHOLDER).replace(/\*/g, "[^/]*").replace(/\u0001/g, ".*");
|
|
11
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
12
|
+
}
|
|
13
|
+
function hostMatches(pattern, host) {
|
|
14
|
+
if (pattern.startsWith("*.")) {
|
|
15
|
+
const suffix = pattern.slice(1);
|
|
16
|
+
return host.endsWith(suffix) && host.length > suffix.length;
|
|
17
|
+
}
|
|
18
|
+
return pattern === host;
|
|
19
|
+
}
|
|
20
|
+
function ruleMatches(rule, request) {
|
|
21
|
+
if (rule.enabled === false)
|
|
22
|
+
return false;
|
|
23
|
+
if (rule.methods?.length && !rule.methods.some((method) => method.toUpperCase() === request.method.toUpperCase()))
|
|
24
|
+
return false;
|
|
25
|
+
if (rule.path && !globMatches(rule.path, request.path))
|
|
26
|
+
return false;
|
|
27
|
+
if (rule.host && !hostMatches(rule.host, request.host ?? ""))
|
|
28
|
+
return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function rateLimitKey(rule, request) {
|
|
32
|
+
const spec = rule.key ?? { source: "ip" };
|
|
33
|
+
const fallback = spec.fallbackToIp !== false;
|
|
34
|
+
let identity;
|
|
35
|
+
if (spec.source === "ip")
|
|
36
|
+
identity = request.ip;
|
|
37
|
+
else if (spec.source === "global")
|
|
38
|
+
identity = "global";
|
|
39
|
+
else if (spec.source === "host")
|
|
40
|
+
identity = request.host ?? "";
|
|
41
|
+
else if (spec.source === "path")
|
|
42
|
+
identity = request.path;
|
|
43
|
+
else if (spec.source === "header")
|
|
44
|
+
identity = spec.name ? request.headers?.[spec.name.toLowerCase()] : undefined;
|
|
45
|
+
else if (spec.source === "cookie")
|
|
46
|
+
identity = spec.name ? request.cookies?.[spec.name] : undefined;
|
|
47
|
+
if (!identity && spec.source !== "global")
|
|
48
|
+
identity = fallback ? request.ip : "__missing__";
|
|
49
|
+
return `${rule.id}${RATE_LIMIT_KEY_SEPARATOR}${identity}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class RateLimiter {
|
|
53
|
+
clock;
|
|
54
|
+
maxTrackedKeys;
|
|
55
|
+
buckets = new Map;
|
|
56
|
+
windows = new Map;
|
|
57
|
+
rules;
|
|
58
|
+
constructor(rules, clock = () => Date.now(), maxTrackedKeys = 1e5) {
|
|
59
|
+
this.clock = clock;
|
|
60
|
+
this.maxTrackedKeys = maxTrackedKeys;
|
|
61
|
+
this.rules = [...rules].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
62
|
+
}
|
|
63
|
+
check(request) {
|
|
64
|
+
const now = this.clock();
|
|
65
|
+
const matching = this.rules.filter((rule) => ruleMatches(rule, request));
|
|
66
|
+
let blocked;
|
|
67
|
+
for (const rule of matching) {
|
|
68
|
+
const test = this.test(rule, request, now);
|
|
69
|
+
if (test.allowed)
|
|
70
|
+
continue;
|
|
71
|
+
if (!blocked || severity(test.action) > severity(blocked.action))
|
|
72
|
+
blocked = test;
|
|
73
|
+
}
|
|
74
|
+
if (blocked)
|
|
75
|
+
return blocked;
|
|
76
|
+
let tightest = { action: "allow", allowed: true };
|
|
77
|
+
for (const rule of matching) {
|
|
78
|
+
const decision = this.consume(rule, request, now);
|
|
79
|
+
if (decision.remaining != null && (tightest.remaining ?? Infinity) > decision.remaining)
|
|
80
|
+
tightest = decision;
|
|
81
|
+
}
|
|
82
|
+
return tightest;
|
|
83
|
+
}
|
|
84
|
+
test(rule, request, now) {
|
|
85
|
+
const key = rateLimitKey(rule, request);
|
|
86
|
+
if ((rule.algorithm ?? "token_bucket") === "token_bucket") {
|
|
87
|
+
const capacity = rule.burst ?? rule.limit;
|
|
88
|
+
const state = this.buckets.get(key);
|
|
89
|
+
const tokens = state ? Math.min(capacity, state.tokens + this.refill(rule, now - state.updatedAt)) : capacity;
|
|
90
|
+
if (tokens >= 1)
|
|
91
|
+
return { action: "allow", allowed: true };
|
|
92
|
+
return this.blocked(rule, key, 0, this.msPerToken(rule) * (1 - tokens), now);
|
|
93
|
+
}
|
|
94
|
+
const hits = (this.windows.get(key)?.hits ?? []).filter((at) => at > now - rule.windowMs);
|
|
95
|
+
if (hits.length < rule.limit)
|
|
96
|
+
return { action: "allow", allowed: true };
|
|
97
|
+
return this.blocked(rule, key, 0, hits[0] + rule.windowMs - now, now);
|
|
98
|
+
}
|
|
99
|
+
peek(request) {
|
|
100
|
+
const now = this.clock();
|
|
101
|
+
for (const rule of this.rules) {
|
|
102
|
+
if (!ruleMatches(rule, request))
|
|
103
|
+
continue;
|
|
104
|
+
const decision = this.test(rule, request, now);
|
|
105
|
+
if (!decision.allowed)
|
|
106
|
+
return decision;
|
|
107
|
+
}
|
|
108
|
+
return { action: "allow", allowed: true };
|
|
109
|
+
}
|
|
110
|
+
msPerToken(rule) {
|
|
111
|
+
return rule.windowMs / Math.max(1, rule.limit);
|
|
112
|
+
}
|
|
113
|
+
refill(rule, elapsedMs) {
|
|
114
|
+
return Math.max(0, elapsedMs) / this.msPerToken(rule);
|
|
115
|
+
}
|
|
116
|
+
blocked(rule, key, remaining, retryAfterMs, now) {
|
|
117
|
+
const action = rule.action ?? "deny";
|
|
118
|
+
return {
|
|
119
|
+
action,
|
|
120
|
+
allowed: action === "allow" || action === "log",
|
|
121
|
+
ruleId: rule.id,
|
|
122
|
+
limit: rule.limit,
|
|
123
|
+
remaining,
|
|
124
|
+
retryAfterMs: Math.max(0, Math.ceil(retryAfterMs)),
|
|
125
|
+
resetAt: now + Math.max(0, retryAfterMs),
|
|
126
|
+
key
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
consume(rule, request, now) {
|
|
130
|
+
const key = rateLimitKey(rule, request);
|
|
131
|
+
const algorithm = rule.algorithm ?? "token_bucket";
|
|
132
|
+
if (algorithm === "token_bucket") {
|
|
133
|
+
const capacity = rule.burst ?? rule.limit;
|
|
134
|
+
const state2 = this.buckets.get(key) ?? { tokens: capacity, updatedAt: now };
|
|
135
|
+
const tokens = Math.min(capacity, state2.tokens + this.refill(rule, now - state2.updatedAt));
|
|
136
|
+
if (tokens < 1) {
|
|
137
|
+
this.buckets.set(key, { tokens, updatedAt: now });
|
|
138
|
+
return this.blocked(rule, key, 0, this.msPerToken(rule) * (1 - tokens), now);
|
|
139
|
+
}
|
|
140
|
+
this.evictIfNeeded(this.buckets);
|
|
141
|
+
this.buckets.set(key, { tokens: tokens - 1, updatedAt: now });
|
|
142
|
+
return {
|
|
143
|
+
action: "allow",
|
|
144
|
+
allowed: true,
|
|
145
|
+
ruleId: rule.id,
|
|
146
|
+
limit: capacity,
|
|
147
|
+
remaining: Math.floor(tokens - 1),
|
|
148
|
+
resetAt: now + this.msPerToken(rule) * (capacity - (tokens - 1)),
|
|
149
|
+
key
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const state = this.windows.get(key) ?? { hits: [] };
|
|
153
|
+
const cutoff = now - rule.windowMs;
|
|
154
|
+
const hits = state.hits.filter((at) => at > cutoff);
|
|
155
|
+
if (hits.length >= rule.limit) {
|
|
156
|
+
this.windows.set(key, { hits });
|
|
157
|
+
return this.blocked(rule, key, 0, hits[0] + rule.windowMs - now, now);
|
|
158
|
+
}
|
|
159
|
+
hits.push(now);
|
|
160
|
+
this.evictIfNeeded(this.windows);
|
|
161
|
+
this.windows.set(key, { hits });
|
|
162
|
+
return {
|
|
163
|
+
action: "allow",
|
|
164
|
+
allowed: true,
|
|
165
|
+
ruleId: rule.id,
|
|
166
|
+
limit: rule.limit,
|
|
167
|
+
remaining: rule.limit - hits.length,
|
|
168
|
+
resetAt: hits[0] + rule.windowMs,
|
|
169
|
+
key
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
evictIfNeeded(map) {
|
|
173
|
+
if (map.size < this.maxTrackedKeys)
|
|
174
|
+
return;
|
|
175
|
+
const excess = map.size - this.maxTrackedKeys + 1;
|
|
176
|
+
let removed = 0;
|
|
177
|
+
for (const key of map.keys()) {
|
|
178
|
+
map.delete(key);
|
|
179
|
+
if (++removed >= excess)
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
sweep() {
|
|
184
|
+
const now = this.clock();
|
|
185
|
+
let removed = 0;
|
|
186
|
+
for (const [key, state] of this.windows) {
|
|
187
|
+
const ruleId = key.split(RATE_LIMIT_KEY_SEPARATOR)[0];
|
|
188
|
+
const rule = this.rules.find((candidate) => candidate.id === ruleId);
|
|
189
|
+
if (!rule) {
|
|
190
|
+
this.windows.delete(key);
|
|
191
|
+
removed++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (state.hits.every((at) => at <= now - rule.windowMs)) {
|
|
195
|
+
this.windows.delete(key);
|
|
196
|
+
removed++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const [key, state] of this.buckets) {
|
|
200
|
+
const ruleId = key.split(RATE_LIMIT_KEY_SEPARATOR)[0];
|
|
201
|
+
const rule = this.rules.find((candidate) => candidate.id === ruleId);
|
|
202
|
+
if (!rule || now - state.updatedAt > rule.windowMs * 2) {
|
|
203
|
+
this.buckets.delete(key);
|
|
204
|
+
removed++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return removed;
|
|
208
|
+
}
|
|
209
|
+
get trackedKeys() {
|
|
210
|
+
return this.buckets.size + this.windows.size;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function severity(action) {
|
|
214
|
+
return { allow: 0, log: 1, throttle: 2, challenge: 3, deny: 4 }[action];
|
|
215
|
+
}
|
|
216
|
+
function rateLimitHeaders(decision) {
|
|
217
|
+
const headers = {};
|
|
218
|
+
if (decision.limit != null)
|
|
219
|
+
headers["ratelimit-limit"] = String(decision.limit);
|
|
220
|
+
if (decision.remaining != null)
|
|
221
|
+
headers["ratelimit-remaining"] = String(decision.remaining);
|
|
222
|
+
if (decision.resetAt != null)
|
|
223
|
+
headers["ratelimit-reset"] = String(Math.max(0, Math.ceil((decision.resetAt - Date.now()) / 1000)));
|
|
224
|
+
if (!decision.allowed && decision.retryAfterMs != null)
|
|
225
|
+
headers["retry-after"] = String(Math.max(1, Math.ceil(decision.retryAfterMs / 1000)));
|
|
226
|
+
return headers;
|
|
227
|
+
}
|
|
228
|
+
function defaultRateLimitRules() {
|
|
229
|
+
return [
|
|
230
|
+
{ id: "global-ip", limit: 600, windowMs: 60000, burst: 100, key: { source: "ip" }, action: "deny", priority: 100 },
|
|
231
|
+
{
|
|
232
|
+
id: "api-ip",
|
|
233
|
+
limit: 120,
|
|
234
|
+
windowMs: 60000,
|
|
235
|
+
burst: 30,
|
|
236
|
+
path: "/api/**",
|
|
237
|
+
key: { source: "ip" },
|
|
238
|
+
action: "deny",
|
|
239
|
+
priority: 50
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "auth-ip",
|
|
243
|
+
limit: 10,
|
|
244
|
+
windowMs: 60000,
|
|
245
|
+
algorithm: "sliding_window",
|
|
246
|
+
path: "/auth/**",
|
|
247
|
+
methods: ["POST"],
|
|
248
|
+
key: { source: "ip" },
|
|
249
|
+
action: "challenge",
|
|
250
|
+
priority: 10
|
|
251
|
+
}
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
// src/protection/controls.ts
|
|
255
|
+
var PROTECTION_SETTING = "protection.controls";
|
|
256
|
+
var MAX_CONTROL_HOURS = 24;
|
|
257
|
+
var DEFAULT_ATTACK_MODE_HOURS = 4;
|
|
258
|
+
var EMPTY = { ipRules: { allow: [], block: [] } };
|
|
259
|
+
function activeControl(control, now) {
|
|
260
|
+
if (!control?.enabled)
|
|
261
|
+
return;
|
|
262
|
+
return new Date(control.expiresAt).getTime() > now.getTime() ? control : undefined;
|
|
263
|
+
}
|
|
264
|
+
function clampHours(hours, fallback) {
|
|
265
|
+
const value = Number.isFinite(hours) ? Number(hours) : fallback;
|
|
266
|
+
return Math.min(MAX_CONTROL_HOURS, Math.max(0.25, value));
|
|
267
|
+
}
|
|
268
|
+
var MAX_IP_RULES = 500;
|
|
269
|
+
|
|
270
|
+
class ProtectionControlStore {
|
|
271
|
+
controlPlane;
|
|
272
|
+
options;
|
|
273
|
+
constructor(controlPlane, options = {}) {
|
|
274
|
+
this.controlPlane = controlPlane;
|
|
275
|
+
this.options = options;
|
|
276
|
+
}
|
|
277
|
+
now() {
|
|
278
|
+
return this.options.now?.() ?? new Date;
|
|
279
|
+
}
|
|
280
|
+
read() {
|
|
281
|
+
const raw = this.controlPlane.getSetting(PROTECTION_SETTING);
|
|
282
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
283
|
+
return { ...EMPTY, ipRules: { allow: [], block: [] } };
|
|
284
|
+
const record = raw;
|
|
285
|
+
return {
|
|
286
|
+
attackMode: record.attackMode,
|
|
287
|
+
mitigationPause: record.mitigationPause,
|
|
288
|
+
ipRules: {
|
|
289
|
+
allow: Array.isArray(record.ipRules?.allow) ? record.ipRules.allow : [],
|
|
290
|
+
block: Array.isArray(record.ipRules?.block) ? record.ipRules.block : []
|
|
291
|
+
},
|
|
292
|
+
updatedAt: record.updatedAt
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
write(next) {
|
|
296
|
+
const stored = { ...next, updatedAt: this.now().toISOString() };
|
|
297
|
+
this.controlPlane.setSetting(PROTECTION_SETTING, stored);
|
|
298
|
+
return stored;
|
|
299
|
+
}
|
|
300
|
+
raw() {
|
|
301
|
+
return this.read();
|
|
302
|
+
}
|
|
303
|
+
current() {
|
|
304
|
+
const now = this.now();
|
|
305
|
+
const stored = this.read();
|
|
306
|
+
return {
|
|
307
|
+
attackMode: activeControl(stored.attackMode, now),
|
|
308
|
+
mitigationPause: activeControl(stored.mitigationPause, now),
|
|
309
|
+
ipRules: stored.ipRules
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
enableAttackMode(input) {
|
|
313
|
+
const now = this.now();
|
|
314
|
+
const control = {
|
|
315
|
+
enabled: true,
|
|
316
|
+
startedAt: now.toISOString(),
|
|
317
|
+
expiresAt: new Date(now.getTime() + clampHours(input.hours, DEFAULT_ATTACK_MODE_HOURS) * 3600000).toISOString(),
|
|
318
|
+
reason: input.reason.trim() || "Attack mode enabled by an operator.",
|
|
319
|
+
actorId: input.actorId
|
|
320
|
+
};
|
|
321
|
+
this.write({ ...this.read(), attackMode: control });
|
|
322
|
+
return control;
|
|
323
|
+
}
|
|
324
|
+
disableAttackMode() {
|
|
325
|
+
const stored = this.read();
|
|
326
|
+
if (!stored.attackMode?.enabled)
|
|
327
|
+
return false;
|
|
328
|
+
this.write({ ...stored, attackMode: { ...stored.attackMode, enabled: false } });
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
pauseMitigations(input) {
|
|
332
|
+
const reason = input.reason.trim();
|
|
333
|
+
if (!reason)
|
|
334
|
+
throw new Error("Pausing mitigations requires a reason: you are liable for the traffic it admits.");
|
|
335
|
+
const now = this.now();
|
|
336
|
+
const control = {
|
|
337
|
+
enabled: true,
|
|
338
|
+
startedAt: now.toISOString(),
|
|
339
|
+
expiresAt: new Date(now.getTime() + clampHours(input.hours, MAX_CONTROL_HOURS) * 3600000).toISOString(),
|
|
340
|
+
reason,
|
|
341
|
+
actorId: input.actorId
|
|
342
|
+
};
|
|
343
|
+
this.write({ ...this.read(), mitigationPause: control });
|
|
344
|
+
return control;
|
|
345
|
+
}
|
|
346
|
+
resumeMitigations() {
|
|
347
|
+
const stored = this.read();
|
|
348
|
+
if (!stored.mitigationPause?.enabled)
|
|
349
|
+
return false;
|
|
350
|
+
this.write({ ...stored, mitigationPause: { ...stored.mitigationPause, enabled: false } });
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
addIpRule(list, cidr) {
|
|
354
|
+
const value = cidr.trim();
|
|
355
|
+
if (!isValidCidr(value))
|
|
356
|
+
throw new Error(`Not a valid IP or CIDR: ${cidr}`);
|
|
357
|
+
const stored = this.read();
|
|
358
|
+
const next = { ...stored.ipRules, [list]: [...new Set([...stored.ipRules[list], value])] };
|
|
359
|
+
if (next[list].length > MAX_IP_RULES)
|
|
360
|
+
throw new Error(`At most ${MAX_IP_RULES} ${list} rules are supported.`);
|
|
361
|
+
const other = list === "allow" ? "block" : "allow";
|
|
362
|
+
next[other] = next[other].filter((entry) => entry !== value);
|
|
363
|
+
this.write({ ...stored, ipRules: next });
|
|
364
|
+
return next;
|
|
365
|
+
}
|
|
366
|
+
removeIpRule(list, cidr) {
|
|
367
|
+
const stored = this.read();
|
|
368
|
+
const next = { ...stored.ipRules, [list]: stored.ipRules[list].filter((entry) => entry !== cidr.trim()) };
|
|
369
|
+
this.write({ ...stored, ipRules: next });
|
|
370
|
+
return next;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function describePosture(controls) {
|
|
374
|
+
if (controls.mitigationPause)
|
|
375
|
+
return {
|
|
376
|
+
attackMode: !!controls.attackMode,
|
|
377
|
+
mitigationPaused: true,
|
|
378
|
+
summary: `Automatic mitigation is paused until ${controls.mitigationPause.expiresAt}. All traffic is being served and billed.`,
|
|
379
|
+
expiresAt: controls.mitigationPause.expiresAt
|
|
380
|
+
};
|
|
381
|
+
if (controls.attackMode)
|
|
382
|
+
return {
|
|
383
|
+
attackMode: true,
|
|
384
|
+
mitigationPaused: false,
|
|
385
|
+
summary: `Attack mode is on until ${controls.attackMode.expiresAt}. Every visitor is being challenged.`,
|
|
386
|
+
expiresAt: controls.attackMode.expiresAt
|
|
387
|
+
};
|
|
388
|
+
return { attackMode: false, mitigationPaused: false, summary: "Protection is in its normal posture." };
|
|
389
|
+
}
|
|
390
|
+
function applyControlsToRateLimits(rules, controls) {
|
|
391
|
+
if (controls.mitigationPause)
|
|
392
|
+
return rules.map((rule) => ({ ...rule, enabled: false }));
|
|
393
|
+
if (!controls.attackMode)
|
|
394
|
+
return [...rules];
|
|
395
|
+
return rules.map((rule) => ({
|
|
396
|
+
...rule,
|
|
397
|
+
action: rule.action === "deny" ? "deny" : "challenge",
|
|
398
|
+
burst: Math.max(1, Math.min(rule.burst ?? rule.limit, 5))
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
function applyControlsToDdos(config, controls) {
|
|
402
|
+
const allowlist = [...new Set([...config.allowlist ?? [], ...controls.ipRules.allow])];
|
|
403
|
+
const blocklist = [...new Set([...config.blocklist ?? [], ...controls.ipRules.block])];
|
|
404
|
+
if (controls.mitigationPause)
|
|
405
|
+
return { ...config, allowlist, blocklist, monitorOnly: true };
|
|
406
|
+
if (!controls.attackMode)
|
|
407
|
+
return { ...config, allowlist, blocklist };
|
|
408
|
+
const thresholds = config.thresholds ?? {};
|
|
409
|
+
return {
|
|
410
|
+
...config,
|
|
411
|
+
allowlist,
|
|
412
|
+
blocklist,
|
|
413
|
+
thresholds: {
|
|
414
|
+
...thresholds,
|
|
415
|
+
newConnectionsPerSecond: Math.max(1, Math.floor((thresholds.newConnectionsPerSecond ?? 50) / 5)),
|
|
416
|
+
concurrentPerSource: Math.max(1, Math.floor((thresholds.concurrentPerSource ?? 100) / 5))
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
export { RATE_LIMIT_KEY_SEPARATOR, globMatches, ruleMatches, rateLimitKey, RateLimiter, rateLimitHeaders, defaultRateLimitRules, PROTECTION_SETTING, MAX_CONTROL_HOURS, DEFAULT_ATTACK_MODE_HOURS, ProtectionControlStore, describePosture, applyControlsToRateLimits, applyControlsToDdos };
|
|
@@ -9,7 +9,7 @@ export type OrganizationRoleTemplate = 'owner' | 'admin' | 'deployer' | 'operato
|
|
|
9
9
|
export type AuthorizationScopeType = 'organization' | 'project' | 'environment' | 'resource';
|
|
10
10
|
export type AuthorizationEffect = 'allow' | 'deny';
|
|
11
11
|
export type AuthorizationRecordSource = 'manual' | 'legacy' | 'invitation';
|
|
12
|
-
export type AuthorizationCapability = 'project:read' | 'config:read' | 'config:write' | 'deployments:read' | 'deployments:create' | 'deployments:cancel' | 'deployments:rollback' | 'runtime:read' | 'runtime:restart' | 'runtime:logs' | 'runtime:terminal' | 'data:read' | 'data:write' | 'data:admin' | 'backups:read' | 'backups:create' | 'backups:restore' | 'secrets:read' | 'secrets:write' | 'fleet:read' | 'fleet:manage' | 'users:read' | 'users:manage' | 'users:transfer-ownership' | 'audit:read' | 'security:read' | 'security:manage' | 'security:waive' | 'sources:read' | 'sources:manage' | 'applications:read' | 'applications:manage' | 'automation:read' | 'automation:manage' | 'tags:manage';
|
|
12
|
+
export type AuthorizationCapability = 'project:read' | 'config:read' | 'config:write' | 'deployments:read' | 'deployments:create' | 'deployments:cancel' | 'deployments:rollback' | 'runtime:read' | 'runtime:restart' | 'runtime:logs' | 'runtime:terminal' | 'data:read' | 'data:write' | 'data:admin' | 'backups:read' | 'backups:create' | 'backups:restore' | 'secrets:read' | 'secrets:write' | 'fleet:read' | 'fleet:manage' | 'users:read' | 'users:manage' | 'users:transfer-ownership' | 'audit:read' | 'billing:read' | 'billing:manage' | 'security:read' | 'security:manage' | 'security:waive' | 'sources:read' | 'sources:manage' | 'applications:read' | 'applications:manage' | 'automation:read' | 'automation:manage' | 'tags:manage';
|
|
13
13
|
export interface AuthorizationScope {
|
|
14
14
|
type: AuthorizationScopeType;
|
|
15
15
|
id?: ControlPlaneId;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/deploy/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
synchronizeDashboardUsers,
|
|
37
37
|
trackDashboardOperation,
|
|
38
38
|
verifyStaticApiOrigin
|
|
39
|
-
} from "../chunk-
|
|
39
|
+
} from "../chunk-cktwk9hy.js";
|
|
40
40
|
import {
|
|
41
41
|
deleteStaticSite,
|
|
42
42
|
deployStaticSite,
|
|
@@ -47,15 +47,17 @@ import {
|
|
|
47
47
|
generateStaticSiteTemplate,
|
|
48
48
|
invalidateCache,
|
|
49
49
|
uploadStaticFiles
|
|
50
|
-
} from "../chunk-
|
|
50
|
+
} from "../chunk-8kh577qh.js";
|
|
51
51
|
import {
|
|
52
52
|
buildAndPushServerlessImage
|
|
53
|
-
} from "../chunk-
|
|
54
|
-
import"../chunk-
|
|
53
|
+
} from "../chunk-wmbep3b5.js";
|
|
54
|
+
import"../chunk-x6yzgv7y.js";
|
|
55
|
+
import"../chunk-hpv68b4a.js";
|
|
55
56
|
import"../chunk-01d86gt1.js";
|
|
56
57
|
import"../chunk-50jpda9q.js";
|
|
57
58
|
import"../chunk-he9a874b.js";
|
|
58
59
|
import"../chunk-gttvakpv.js";
|
|
60
|
+
import"../chunk-32e7ya18.js";
|
|
59
61
|
import {
|
|
60
62
|
IPV6_EXCLUDED_HOST_LABELS,
|
|
61
63
|
MANAGEMENT_DASHBOARD_SITE,
|
|
@@ -76,14 +78,15 @@ import {
|
|
|
76
78
|
siteInstallBase,
|
|
77
79
|
validateDeploymentConfig,
|
|
78
80
|
verifyAddressRecord
|
|
79
|
-
} from "../chunk-
|
|
80
|
-
import"../chunk-c6db5m63.js";
|
|
81
|
+
} from "../chunk-d3vx2ekv.js";
|
|
81
82
|
import"../chunk-7m60qnc8.js";
|
|
82
83
|
import"../chunk-4cjrg98a.js";
|
|
83
|
-
import"../chunk-hpv68b4a.js";
|
|
84
|
-
import"../chunk-32e7ya18.js";
|
|
85
84
|
import"../chunk-wj3s95p9.js";
|
|
86
85
|
import"../chunk-zqtpg06c.js";
|
|
86
|
+
import"../chunk-grx1rp09.js";
|
|
87
|
+
import"../chunk-va2yd85b.js";
|
|
88
|
+
import"../chunk-aa2dkn7b.js";
|
|
89
|
+
import"../chunk-t2s3gdq6.js";
|
|
87
90
|
import"../chunk-v0bahtg2.js";
|
|
88
91
|
export {
|
|
89
92
|
verifyStaticApiOrigin,
|
|
@@ -22,6 +22,8 @@ export interface LocalDashboardServerOptions {
|
|
|
22
22
|
oidcFetch?: OidcFetch;
|
|
23
23
|
/** Run the persistent deployment worker in this process. Defaults off in tests. */
|
|
24
24
|
queueWorker?: boolean;
|
|
25
|
+
/** Evaluate spend budgets on a timer. Off in tests, like the queue worker. */
|
|
26
|
+
spendLoop?: boolean;
|
|
25
27
|
/** Maximum number of operations this process may execute in parallel. */
|
|
26
28
|
queueParallelism?: number;
|
|
27
29
|
/** Resolve the provider-specific immutable activation primitive for release jobs. */
|
package/dist/drivers/index.js
CHANGED
|
@@ -68,12 +68,14 @@ import {
|
|
|
68
68
|
waitForSsh,
|
|
69
69
|
wrapCloudInitUserData,
|
|
70
70
|
writeResizeCheckpoint
|
|
71
|
-
} from "../chunk-
|
|
72
|
-
import"../chunk-c6db5m63.js";
|
|
71
|
+
} from "../chunk-d3vx2ekv.js";
|
|
73
72
|
import"../chunk-7m60qnc8.js";
|
|
74
73
|
import"../chunk-4cjrg98a.js";
|
|
75
74
|
import"../chunk-wj3s95p9.js";
|
|
76
75
|
import"../chunk-zqtpg06c.js";
|
|
76
|
+
import"../chunk-va2yd85b.js";
|
|
77
|
+
import"../chunk-aa2dkn7b.js";
|
|
78
|
+
import"../chunk-t2s3gdq6.js";
|
|
77
79
|
import"../chunk-v0bahtg2.js";
|
|
78
80
|
export {
|
|
79
81
|
writeResizeCheckpoint,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge protection at provision time.
|
|
3
|
+
*
|
|
4
|
+
* UFW (see `ufw.ts`) decides which ports are open. This decides what happens to
|
|
5
|
+
* the traffic that arrives on them: kernel-level flood mitigation, and the WAF.
|
|
6
|
+
*
|
|
7
|
+
* Both are **on by default**. A protection that has to be switched on protects
|
|
8
|
+
* only the people who already knew they needed it, which is never the box that
|
|
9
|
+
* gets hit. The escape hatches are explicit (`ddos: false`, `waf: false`) and
|
|
10
|
+
* the WAF starts in detection-only, so the default posture is "see everything,
|
|
11
|
+
* block only floods".
|
|
12
|
+
*/
|
|
13
|
+
import type { DdosConfig } from '../../protection/ddos';
|
|
14
|
+
import type { WafConfig } from '../../protection/waf';
|
|
15
|
+
/** `true` for defaults, `false` to skip, or an object to tune. */
|
|
16
|
+
export type ProtectionSetting<T> = boolean | T | undefined;
|
|
17
|
+
/** L3/L4 defaults: protect the web ports, count-and-drop, ten-minute bans. */
|
|
18
|
+
export declare const DEFAULT_COMPUTE_DDOS: DdosConfig;
|
|
19
|
+
/**
|
|
20
|
+
* WAF defaults: paranoia 1, detection only.
|
|
21
|
+
*
|
|
22
|
+
* Detection rather than blocking because zig-waf is pre-alpha and because a
|
|
23
|
+
* ruleset nobody has read their own logs against will block real traffic. The
|
|
24
|
+
* operator promotes it once they have looked.
|
|
25
|
+
*/
|
|
26
|
+
export declare const DEFAULT_COMPUTE_WAF: WafConfig;
|
|
27
|
+
/** Kernel-level flood mitigation. Returns no lines when disabled. */
|
|
28
|
+
export declare function buildDdosScript(setting?: ProtectionSetting<DdosConfig>, extraPorts?: number[]): string[];
|
|
29
|
+
/** WAF configuration. Returns no lines when disabled or when the binary is absent. */
|
|
30
|
+
export declare function buildWafScript(setting?: ProtectionSetting<WafConfig>): string[];
|
|
31
|
+
export interface ComputeProtectionConfig {
|
|
32
|
+
ddos?: ProtectionSetting<DdosConfig>;
|
|
33
|
+
waf?: ProtectionSetting<WafConfig>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Both layers, in order: kernel first, then application.
|
|
37
|
+
*
|
|
38
|
+
* Order matters on a box that is already being hit. The packet filter costs
|
|
39
|
+
* nothing per request and starts dropping immediately; the WAF has to parse
|
|
40
|
+
* every request it inspects, so bringing it up first would spend CPU on
|
|
41
|
+
* traffic the kernel was about to discard anyway.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildProtectionScript(protection?: ComputeProtectionConfig, extraPorts?: number[]): string[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export * from './compose';
|
|
|
9
9
|
export * from './release';
|
|
10
10
|
export * from './runtime';
|
|
11
11
|
export * from './telemetry';
|
|
12
|
+
export * from './spend';
|
|
13
|
+
export { applyControlsToDdos, applyControlsToRateLimits, applyMitigationFactor, DEFAULT_ATTACK_MODE_HOURS, defaultRateLimitRules, describePosture, MAX_CONTROL_HOURS, ProtectionControlStore, DEFAULT_DDOS_THRESHOLDS, DEFAULT_RECURSION_LIMITS, escapeSecLang, functionFingerprint, globMatches, globToRegex, inspectInvocation, isSafePattern, isValidCidr, parseChain, planMitigation, propagationHeaders, rateLimitHeaders, rateLimitKey, RateLimiter as EdgeRateLimiter, RATE_LIMIT_KEY_SEPARATOR, RecursionGuard, recursionBlockedResponse, renderDdosInstallScript, renderNftablesRuleset, renderSysctlFile, renderWafConfig, renderWafInstallScript, ruleMatches, sysctlHardening, wafEventTelemetry, CHAIN_HEADER, DEPTH_HEADER, TRACE_HEADER, } from './protection';
|
|
14
|
+
export type { DdosConfig, DdosThresholds, IpRules, ProtectionControls, ProtectionPosture, TimeBoxedControl, InvocationContext, MitigationLevel, MitigationPlan, ParanoiaLevel, RateLimitAction, RateLimitAlgorithm, RateLimitDecision, RateLimitKeySource, RateLimitKeySpec, RateLimitRule as EdgeRateLimitRule, RecursionLimits, RecursionReason, RecursionVerdict, RequestDescriptor, TrafficSignals, WafConfig as EdgeWafConfig, WafEvent, WafGenerationResult, WafMode, WafRuleExclusion, } from './protection';
|
|
12
15
|
export { AlertEvaluator, AlertStore, HealthCheckRunner, NotificationRouter, evaluateTelemetryAlertRules, isQuietHours, } from './alerts';
|
|
13
16
|
export type { Alert, AlertEvaluation, AlertRule, AlertSample, AlertSeverity, AlertState, HealthCheck as AlertHealthCheck, HealthResult as AlertHealthResult, HealthStatus as AlertHealthStatus, NotificationChannel as AlertNotificationChannel, NotificationDelivery as AlertNotificationDelivery, NotificationRoute as AlertNotificationRoute, } from './alerts';
|
|
14
17
|
export * from './jobs';
|