@stacksjs/ts-cloud 0.7.105 → 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.
Files changed (149) 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 +1677 -1370
  9. package/dist/bin/dashboard-server.js +926 -620
  10. package/dist/{chunk-fmt40tpw.js → chunk-53jatzaw.js} +2 -2
  11. package/dist/{chunk-ef2dwfrg.js → chunk-8kh577qh.js} +5 -5
  12. package/dist/{chunk-s9c2x5jv.js → chunk-9v2pwxv9.js} +2 -2
  13. package/dist/chunk-aa2dkn7b.js +205 -0
  14. package/dist/{chunk-vkd2y0fs.js → chunk-cktwk9hy.js} +7169 -14800
  15. package/dist/{chunk-dmzez5f8.js → chunk-d3vx2ekv.js} +123 -28
  16. package/dist/chunk-grx1rp09.js +12163 -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-ybcz6sxc.js → chunk-x6yzgv7y.js} +33 -0
  21. package/dist/chunk-xrtwv3v2.js +420 -0
  22. package/dist/control-plane/types.d.ts +1 -1
  23. package/dist/deploy/dashboard-bundle-encoding.test.d.ts +1 -0
  24. package/dist/deploy/dashboard-payload.d.ts +20 -0
  25. package/dist/deploy/dashboard-payload.test.d.ts +1 -0
  26. package/dist/deploy/dashboard-protection.test.d.ts +1 -0
  27. package/dist/deploy/dashboard-spend.test.d.ts +1 -0
  28. package/dist/deploy/dashboard-topology.d.ts +85 -0
  29. package/dist/deploy/dashboard-topology.test.d.ts +1 -0
  30. package/dist/deploy/egress-collection.d.ts +60 -0
  31. package/dist/deploy/index.d.ts +2 -0
  32. package/dist/deploy/index.js +23 -8
  33. package/dist/deploy/local-dashboard-server.d.ts +2 -0
  34. package/dist/drivers/index.js +4 -2
  35. package/dist/drivers/shared/protection.d.ts +43 -0
  36. package/dist/drivers/shared/protection.test.d.ts +1 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.js +364 -102
  39. package/dist/protection/controls.d.ts +113 -0
  40. package/dist/protection/controls.test.d.ts +1 -0
  41. package/dist/protection/ddos.d.ts +130 -0
  42. package/dist/protection/ddos.test.d.ts +1 -0
  43. package/dist/protection/index.d.ts +11 -0
  44. package/dist/protection/index.js +87 -0
  45. package/dist/protection/ratelimit.d.ts +154 -0
  46. package/dist/protection/ratelimit.test.d.ts +1 -0
  47. package/dist/protection/waf.d.ts +143 -0
  48. package/dist/protection/waf.test.d.ts +1 -0
  49. package/dist/spend/anomaly-config.d.ts +130 -0
  50. package/dist/spend/anomaly-config.test.d.ts +1 -0
  51. package/dist/spend/anomaly.d.ts +114 -0
  52. package/dist/spend/anomaly.test.d.ts +1 -0
  53. package/dist/spend/api.d.ts +42 -0
  54. package/dist/spend/api.test.d.ts +1 -0
  55. package/dist/spend/appliers.d.ts +126 -0
  56. package/dist/spend/docs.test.d.ts +1 -0
  57. package/dist/spend/enforcement.d.ts +111 -0
  58. package/dist/spend/enforcement.integration.test.d.ts +1 -0
  59. package/dist/spend/evaluator.d.ts +70 -0
  60. package/dist/spend/evaluator.test.d.ts +1 -0
  61. package/dist/spend/focus.d.ts +93 -0
  62. package/dist/spend/focus.test.d.ts +1 -0
  63. package/dist/spend/gate.d.ts +96 -0
  64. package/dist/spend/index.d.ts +20 -0
  65. package/dist/spend/index.js +173 -0
  66. package/dist/spend/lease.d.ts +54 -0
  67. package/dist/spend/lease.test.d.ts +1 -0
  68. package/dist/spend/meter.d.ts +70 -0
  69. package/dist/spend/meter.test.d.ts +1 -0
  70. package/dist/spend/model.d.ts +228 -0
  71. package/dist/spend/notifications.d.ts +58 -0
  72. package/dist/spend/pricing.d.ts +109 -0
  73. package/dist/spend/pricing.test.d.ts +1 -0
  74. package/dist/spend/projection.d.ts +36 -0
  75. package/dist/spend/runner.d.ts +130 -0
  76. package/dist/spend/service.d.ts +129 -0
  77. package/dist/spend/signals.d.ts +110 -0
  78. package/dist/spend/signals.test.d.ts +1 -0
  79. package/dist/spend/sms-sdk.test.d.ts +1 -0
  80. package/dist/spend/spend.integration.test.d.ts +1 -0
  81. package/dist/spend/store.d.ts +166 -0
  82. package/dist/spend/transports.d.ts +169 -0
  83. package/dist/spend/transports.test.d.ts +1 -0
  84. package/dist/spend/window.d.ts +52 -0
  85. package/dist/spend/window.test.d.ts +1 -0
  86. package/dist/ui/404.html +2 -0
  87. package/dist/ui/access-denied.html +2 -2
  88. package/dist/ui/account/automation.html +4 -4
  89. package/dist/ui/account/security.html +2 -2
  90. package/dist/ui/applications/compose.html +4 -4
  91. package/dist/ui/applications/new.html +2 -2
  92. package/dist/ui/data/backups.html +22 -9
  93. package/dist/ui/data/services.html +22 -9
  94. package/dist/ui/data/volumes.html +22 -9
  95. package/dist/ui/index.html +22 -9
  96. package/dist/ui/infrastructure/topology.html +2223 -0
  97. package/dist/ui/integrations.html +2 -2
  98. package/dist/ui/operations/alerts.html +22 -9
  99. package/dist/ui/operations/configuration.html +4 -4
  100. package/dist/ui/operations/jobs.html +22 -9
  101. package/dist/ui/operations/maintenance.html +4 -4
  102. package/dist/ui/operations/observability.html +22 -9
  103. package/dist/ui/operations/previews.html +4 -4
  104. package/dist/ui/operations/queue.html +4 -4
  105. package/dist/ui/operations/regions.html +3 -3
  106. package/dist/ui/operations/releases.html +3 -3
  107. package/dist/ui/operations/spend.html +1634 -0
  108. package/dist/ui/operations/workloads.html +22 -9
  109. package/dist/ui/security.html +2 -2
  110. package/dist/ui/server/actions.html +4 -4
  111. package/dist/ui/server/activity.html +2 -2
  112. package/dist/ui/server/capacity.html +4 -4
  113. package/dist/ui/server/database.html +4 -4
  114. package/dist/ui/server/deployments.html +4 -4
  115. package/dist/ui/server/diagnostics.html +2 -2
  116. package/dist/ui/server/firewall.html +100 -161
  117. package/dist/ui/server/fleet.html +22 -9
  118. package/dist/ui/server/logs.html +22 -9
  119. package/dist/ui/server/metrics.html +130 -14
  120. package/dist/ui/server/security.html +2 -2
  121. package/dist/ui/server/services.html +1 -1
  122. package/dist/ui/server/sites.html +22 -9
  123. package/dist/ui/server/ssh-keys.html +3 -3
  124. package/dist/ui/server/team.html +4 -4
  125. package/dist/ui/server/terminal.html +2 -2
  126. package/dist/ui/serverless/alarms.html +4 -4
  127. package/dist/ui/serverless/assets.html +2 -2
  128. package/dist/ui/serverless/cost.html +2 -2
  129. package/dist/ui/serverless/data.html +4 -4
  130. package/dist/ui/serverless/deployments.html +2 -2
  131. package/dist/ui/serverless/firewall.html +2 -2
  132. package/dist/ui/serverless/functions.html +4 -4
  133. package/dist/ui/serverless/logs.html +22 -9
  134. package/dist/ui/serverless/metrics.html +2 -2
  135. package/dist/ui/serverless/queues.html +4 -4
  136. package/dist/ui/serverless/secrets.html +4 -4
  137. package/dist/ui/serverless/traces.html +4 -4
  138. package/dist/ui/serverless.html +4 -4
  139. package/dist/ui-src/functions/requestJson.ts +41 -0
  140. package/dist/ui-src/functions/useOpConfirm.ts +132 -0
  141. package/dist/ui-src/functions/usePolling.ts +98 -0
  142. package/dist/ui-src/functions/useTerminal.ts +79 -0
  143. package/dist/ui-src/pages/infrastructure/topology.stx +517 -0
  144. package/dist/ui-src/pages/operations/spend.stx +225 -0
  145. package/dist/ui-src/pages/partials/nav.stx +4 -0
  146. package/dist/ui-src/pages/server/firewall.stx +89 -0
  147. package/dist/ui-src/pages/server/metrics.stx +140 -6
  148. package/dist/ui-src/src/topology.ts +485 -0
  149. 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,20 @@
1
+ /**
2
+ * Serialize the dashboard payload for the trip through an environment variable.
3
+ *
4
+ * The UI is rebuilt by spawning stx with the resolved data in
5
+ * `TSCLOUD_DASHBOARD_DATA`. Environment variables are bytes, and the child
6
+ * decodes them using its locale — so on a box whose locale is `POSIX`/`C`
7
+ * (the default on most minimal server images) every non-ASCII character in the
8
+ * payload comes back mis-decoded and is then re-encoded as UTF-8, arriving in
9
+ * the built page as mojibake: `—` renders as `â€"`, `·` as `·`.
10
+ *
11
+ * That is not hypothetical or cosmetic. It corrupts any project name, domain,
12
+ * or status text carrying an accent or a dash, on exactly the hosts operators
13
+ * are most likely to provision.
14
+ *
15
+ * Escaping every non-ASCII code point makes the transport pure ASCII, which no
16
+ * locale can misread. `JSON.parse` on the other side turns the escapes back
17
+ * into the original characters, so the payload is unchanged — only its wire
18
+ * form is narrowed.
19
+ */
20
+ export declare function encodeDashboardPayload(data: unknown): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Derive the infrastructure topology the dashboard draws as a diagram.
3
+ *
4
+ * Deliberately built from the *resolved dashboard data* rather than straight
5
+ * from the cloud config: the diagram then shows what the rest of the cockpit
6
+ * shows — the same sites, services, functions and queues, with the same
7
+ * statuses — instead of a second, subtly different reading of the world.
8
+ *
9
+ * Nothing here invents a resource. A slice that could not be read simply
10
+ * contributes no node, and the reason lands in `notes` so the page can say so
11
+ * out loud. See `packages/ui/pages/infrastructure/topology.stx`.
12
+ */
13
+ export type TopologyStatus = 'ok' | 'warn' | 'bad' | 'idle' | 'unknown';
14
+ export type TopologyKind = 'internet' | 'dns' | 'waf' | 'firewall' | 'cdn' | 'gateway' | 'proxy' | 'site' | 'function' | 'worker' | 'scheduler' | 'queue' | 'cache' | 'database' | 'storage' | 'filesystem' | 'backup';
15
+ /** How traffic or state moves along an edge — drives the edge styling. */
16
+ export type TopologyFlow = 'request' | 'async' | 'data' | 'backup';
17
+ export interface TopologyFact {
18
+ label: string;
19
+ value: string;
20
+ }
21
+ export interface TopologyNode {
22
+ id: string;
23
+ label: string;
24
+ kind: TopologyKind;
25
+ /** Row in the diagram: 0 is the internet, higher is deeper in the stack. */
26
+ layer: number;
27
+ status: TopologyStatus;
28
+ /** One-line subtitle: runtime, engine, instance class… */
29
+ sub?: string;
30
+ facts: TopologyFact[];
31
+ /** Dashboard page this resource is managed from. */
32
+ href?: string;
33
+ /** Boundary this node sits inside (a box, a VPC, a managed account). */
34
+ group?: string;
35
+ /** Outside the deployment — drawn as an open shape. */
36
+ external?: boolean;
37
+ }
38
+ export interface TopologyLink {
39
+ source: string;
40
+ target: string;
41
+ flow: TopologyFlow;
42
+ label?: string;
43
+ status?: TopologyStatus;
44
+ }
45
+ export interface TopologyGroup {
46
+ id: string;
47
+ label: string;
48
+ sub?: string;
49
+ }
50
+ export interface TopologyLayer {
51
+ index: number;
52
+ id: string;
53
+ label: string;
54
+ }
55
+ /** Identity the data bag doesn't carry on its own. */
56
+ export interface TopologyContext {
57
+ project?: string;
58
+ environment?: string;
59
+ }
60
+ export interface TopologyModel {
61
+ mode: 'server' | 'serverless';
62
+ project: string;
63
+ environment: string;
64
+ provider: string;
65
+ region: string;
66
+ generatedAt: string;
67
+ layers: TopologyLayer[];
68
+ groups: TopologyGroup[];
69
+ nodes: TopologyNode[];
70
+ links: TopologyLink[];
71
+ /** What the diagram could not show, and why. Never left implicit. */
72
+ notes: string[];
73
+ }
74
+ /**
75
+ * Server-mode topology: clients → DNS → host firewall → reverse proxy → sites,
76
+ * with the box's workers, scheduler, data services and backups hanging off it.
77
+ */
78
+ export declare function buildServerTopology(data: Record<string, any>, context?: TopologyContext): TopologyModel;
79
+ /**
80
+ * Serverless-mode topology: clients → DNS → WAF/CDN → the HTTP entry point →
81
+ * functions, with queues, database, cache and object storage behind them.
82
+ */
83
+ export declare function buildServerlessTopology(data: Record<string, any>, context?: TopologyContext): TopologyModel;
84
+ /** Build the topology matching the dashboard mode the data was resolved for. */
85
+ export declare function buildTopology(mode: 'server' | 'serverless', data: Record<string, any>, context?: TopologyContext): TopologyModel;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Object-storage egress collection.
3
+ *
4
+ * Host network counters answer a different question than the one an
5
+ * object-storage invoice asks. When an application redirects downloads to a
6
+ * bucket (or to a CDN in front of one), those bytes never traverse the host's
7
+ * NIC — the box can sit idle while the bucket serves terabytes. The only
8
+ * component that knows a transfer happened, and how large it was, is the
9
+ * application that authorized it, so this polls it instead of guessing.
10
+ *
11
+ * The reports carry a per-day history rather than a single counter, and that is
12
+ * deliberately exploited here: each day becomes one metric point stamped with
13
+ * that day's own timestamp. Because record ids are a deterministic hash of
14
+ * (source, name, timestamp) and the store inserts with OR IGNORE, replaying a
15
+ * history is idempotent. A collector that was down for six hours — or six days
16
+ * — backfills what it missed on its next run instead of leaving a hole, and
17
+ * re-reading the same day never double-counts it.
18
+ */
19
+ import type { EgressEndpointConfig, EgressReport } from '@ts-cloud/core';
20
+ export interface EgressMetric {
21
+ name: string;
22
+ value: number;
23
+ unit: string;
24
+ /** ISO timestamp this point belongs to. */
25
+ timestamp: string;
26
+ attributes: Record<string, string>;
27
+ }
28
+ export interface EgressCollectionResult {
29
+ metrics: EgressMetric[];
30
+ errors: Array<{
31
+ source: string;
32
+ message: string;
33
+ }>;
34
+ }
35
+ /**
36
+ * Turn one report into metric points.
37
+ *
38
+ * Pure: no clock, no network. `now` is the timestamp for the point-in-time
39
+ * series (month-to-date, projection, allowance), which describe the moment of
40
+ * collection rather than a calendar day.
41
+ */
42
+ export declare function egressReportMetrics(report: EgressReport | null | undefined, options: {
43
+ name: string;
44
+ now: string;
45
+ maxReplayDays?: number;
46
+ }): EgressMetric[];
47
+ /**
48
+ * Fetch one endpoint's report.
49
+ *
50
+ * Returns null on any failure. An egress endpoint is supplementary — it must
51
+ * never be able to fail a telemetry collection that is also carrying host
52
+ * metrics, so problems surface as an error entry rather than a throw.
53
+ */
54
+ export declare function fetchEgressReport(endpoint: EgressEndpointConfig, env?: NodeJS.ProcessEnv, fetchImpl?: typeof fetch): Promise<EgressReport | null>;
55
+ /** Poll every configured endpoint and flatten the results. */
56
+ export declare function collectEgressMetrics(endpoints: EgressEndpointConfig[] | undefined, options: {
57
+ now: string;
58
+ env?: NodeJS.ProcessEnv;
59
+ fetchImpl?: typeof fetch;
60
+ }): Promise<EgressCollectionResult>;
@@ -15,5 +15,7 @@ export * from './container-image';
15
15
  export { buildFunctionEnv, type CodeSource, deployServerlessApp, type DeployServerlessOptions, infraEnvFromOutputs, redeployServerlessApp, type ResolvedContext, rollbackServerlessApp, runRemoteCommand, setMaintenance, } from './serverless-app';
16
16
  export { buildAndPushServerlessImage, type BuildImageOptions, type BuiltImage } from './serverless-image';
17
17
  export { type DashboardData, resolveDashboardData } from './dashboard-data';
18
+ export { buildServerlessTopology, buildServerTopology, buildTopology, type TopologyContext, type TopologyFact, type TopologyFlow, type TopologyGroup, type TopologyKind, type TopologyLayer, type TopologyLink, type TopologyModel, type TopologyNode, type TopologyStatus, } from './dashboard-topology';
19
+ export { collectEgressMetrics, type EgressCollectionResult, egressReportMetrics, type EgressMetric, fetchEgressReport, } from './egress-collection';
18
20
  export { dashboardActions, resolveDashboardAction, sanitizeCloudConfig, startLocalDashboardServer, type DashboardAction, type LocalDashboardServer, type LocalDashboardServerOptions, } from './local-dashboard-server';
19
21
  export { buildManagementDashboardArtifact, dashboardCredentialsFile, ensureManagementDashboard, type EnsureDashboardLogger, MANAGEMENT_DASHBOARD_SITE, resolveDashboardAuth, type ResolvedDashboardAuth, resolveUiSource, } from './management-dashboard';