@henriquecosta/chaos-api 1.0.0

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 (61) hide show
  1. package/dist/adapters/express.d.ts +21 -0
  2. package/dist/adapters/express.js +40 -0
  3. package/dist/adapters/fastify.d.ts +12 -0
  4. package/dist/adapters/fastify.js +44 -0
  5. package/dist/adapters/koa.d.ts +12 -0
  6. package/dist/adapters/koa.js +47 -0
  7. package/dist/adapters/nestjs.d.ts +34 -0
  8. package/dist/adapters/nestjs.js +52 -0
  9. package/dist/bin/chaos-api.d.ts +2 -0
  10. package/dist/bin/chaos-api.js +33 -0
  11. package/dist/core/activity-log.d.ts +24 -0
  12. package/dist/core/activity-log.js +26 -0
  13. package/dist/core/scenario-engine.d.ts +12 -0
  14. package/dist/core/scenario-engine.js +38 -0
  15. package/dist/core/state-store.d.ts +26 -0
  16. package/dist/core/state-store.js +69 -0
  17. package/dist/core/types.d.ts +41 -0
  18. package/dist/core/types.js +1 -0
  19. package/dist/dashboard/server/control-api.d.ts +9 -0
  20. package/dist/dashboard/server/control-api.js +140 -0
  21. package/dist/dashboard/server/index.d.ts +10 -0
  22. package/dist/dashboard/server/index.js +49 -0
  23. package/dist/dashboard/ui/app.js +336 -0
  24. package/dist/dashboard/ui/index.html +215 -0
  25. package/dist/guardrail.d.ts +10 -0
  26. package/dist/guardrail.js +20 -0
  27. package/dist/index.d.ts +22 -0
  28. package/dist/index.js +13 -0
  29. package/dist/outbound/chaos-fetch.d.ts +13 -0
  30. package/dist/outbound/chaos-fetch.js +50 -0
  31. package/dist/outbound/index.d.ts +1 -0
  32. package/dist/outbound/index.js +1 -0
  33. package/dist/presets/catalog.d.ts +8 -0
  34. package/dist/presets/catalog.js +157 -0
  35. package/dist/presets/index.d.ts +15 -0
  36. package/dist/presets/index.js +22 -0
  37. package/dist/presets/types.d.ts +17 -0
  38. package/dist/presets/types.js +1 -0
  39. package/dist/scenarios/connection-reset.d.ts +7 -0
  40. package/dist/scenarios/connection-reset.js +8 -0
  41. package/dist/scenarios/delay.d.ts +6 -0
  42. package/dist/scenarios/delay.js +9 -0
  43. package/dist/scenarios/error-response.d.ts +9 -0
  44. package/dist/scenarios/error-response.js +13 -0
  45. package/dist/scenarios/index.d.ts +7 -0
  46. package/dist/scenarios/index.js +7 -0
  47. package/dist/scenarios/malformed-response.d.ts +11 -0
  48. package/dist/scenarios/malformed-response.js +13 -0
  49. package/dist/scenarios/random-error.d.ts +6 -0
  50. package/dist/scenarios/random-error.js +7 -0
  51. package/dist/scenarios/random-timeout.d.ts +7 -0
  52. package/dist/scenarios/random-timeout.js +8 -0
  53. package/dist/scenarios/registry.d.ts +12 -0
  54. package/dist/scenarios/registry.js +20 -0
  55. package/dist/scenarios/stale-response.d.ts +9 -0
  56. package/dist/scenarios/stale-response.js +10 -0
  57. package/dist/scenarios/unavailable-503.d.ts +5 -0
  58. package/dist/scenarios/unavailable-503.js +9 -0
  59. package/dist/scenarios/unavailable.d.ts +7 -0
  60. package/dist/scenarios/unavailable.js +9 -0
  61. package/package.json +59 -0
@@ -0,0 +1,215 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Chaos API — Dashboard</title>
6
+ <style>
7
+ :root {
8
+ --bg: #0F1115;
9
+ --surface: #1A1D24;
10
+ --primary: #5B8CFF;
11
+ --active: #FF6B4A;
12
+ --text: #E6E8EC;
13
+ --text-muted: #8A8F98;
14
+ --error: #E5484D;
15
+ }
16
+ * { box-sizing: border-box; }
17
+ body {
18
+ margin: 0;
19
+ font-family: Inter, system-ui, sans-serif;
20
+ background: var(--bg);
21
+ color: var(--text);
22
+ font-size: 14px;
23
+ }
24
+ header {
25
+ padding: 16px 24px;
26
+ border-bottom: 1px solid #262A33;
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: space-between;
30
+ gap: 16px;
31
+ }
32
+ h1 { font-size: 24px; font-weight: 600; margin: 0; }
33
+ #status-banner {
34
+ display: none;
35
+ background: var(--active);
36
+ color: #1A0A05;
37
+ padding: 8px 24px;
38
+ font-weight: 600;
39
+ }
40
+ main { padding: 24px; max-width: 720px; margin: 0 auto; }
41
+ .row { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; }
42
+ input, select, button {
43
+ font-family: inherit;
44
+ font-size: 13px;
45
+ background: var(--surface);
46
+ color: var(--text);
47
+ border: 1px solid #262A33;
48
+ border-radius: 4px;
49
+ padding: 8px;
50
+ }
51
+ input[type="text"], input[type="number"] { font-family: "JetBrains Mono", monospace; }
52
+ button {
53
+ cursor: pointer;
54
+ background: var(--primary);
55
+ color: #0B1220;
56
+ border: none;
57
+ font-weight: 600;
58
+ }
59
+ button.danger { background: var(--error); color: #fff; }
60
+ #control-url { flex: 1; }
61
+ .card {
62
+ background: var(--surface);
63
+ border-radius: 8px;
64
+ padding: 16px;
65
+ margin-bottom: 12px;
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 12px;
69
+ }
70
+ .card.on { border-left: 3px solid var(--active); }
71
+ .card .meta { flex: 1; }
72
+ .card .type { font-weight: 600; text-transform: capitalize; }
73
+ .card .scope { color: var(--text-muted); font-family: "JetBrains Mono", monospace; font-size: 12px; }
74
+ form.add-form {
75
+ background: var(--surface);
76
+ border-radius: 8px;
77
+ padding: 16px;
78
+ display: grid;
79
+ gap: 8px;
80
+ grid-template-columns: 1fr 1fr;
81
+ }
82
+ form.add-form .full { grid-column: 1 / -1; }
83
+ #error { color: var(--error); margin-bottom: 12px; }
84
+ h2 { font-size: 16px; margin: 24px 0 8px; }
85
+ #activity-list {
86
+ background: var(--surface);
87
+ border-radius: 8px;
88
+ max-height: 240px;
89
+ overflow-y: auto;
90
+ font-family: "JetBrains Mono", monospace;
91
+ font-size: 12px;
92
+ }
93
+ .activity-row {
94
+ display: flex;
95
+ gap: 12px;
96
+ padding: 8px 12px;
97
+ border-bottom: 1px solid #262A33;
98
+ }
99
+ .activity-row:last-child { border-bottom: none; }
100
+ .activity-time { color: var(--text-muted); flex-shrink: 0; }
101
+ .activity-type { color: var(--active); flex-shrink: 0; }
102
+ .activity-dir { color: var(--text-muted); flex-shrink: 0; }
103
+ .activity-path { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
104
+ #preset-categories { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
105
+ #preset-categories button {
106
+ background: transparent;
107
+ color: var(--text-muted);
108
+ font-weight: 400;
109
+ }
110
+ #preset-categories button.active { background: var(--primary); color: #0B1220; font-weight: 600; }
111
+ .preset-card {
112
+ background: var(--surface);
113
+ border-radius: 8px;
114
+ padding: 12px 16px;
115
+ margin-bottom: 8px;
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 12px;
119
+ }
120
+ .preset-card .meta { flex: 1; }
121
+ .preset-card .name { font-weight: 600; font-family: "JetBrains Mono", monospace; font-size: 13px; }
122
+ .preset-card .description { color: var(--text-muted); font-size: 12px; }
123
+ form.runner-form {
124
+ background: var(--surface);
125
+ border-radius: 8px;
126
+ padding: 16px;
127
+ display: grid;
128
+ gap: 8px;
129
+ grid-template-columns: 120px 1fr;
130
+ }
131
+ form.runner-form .full { grid-column: 1 / -1; }
132
+ form.runner-form textarea {
133
+ font-family: "JetBrains Mono", monospace;
134
+ font-size: 12px;
135
+ resize: vertical;
136
+ min-height: 60px;
137
+ }
138
+ #runner-response {
139
+ background: var(--surface);
140
+ border-radius: 8px;
141
+ padding: 12px 16px;
142
+ margin-top: 12px;
143
+ font-family: "JetBrains Mono", monospace;
144
+ font-size: 12px;
145
+ white-space: pre-wrap;
146
+ word-break: break-word;
147
+ }
148
+ #runner-response .status { font-weight: 600; margin-bottom: 8px; }
149
+ #runner-response .status.ok { color: #3DD68C; }
150
+ #runner-response .status.error { color: var(--error); }
151
+ </style>
152
+ </head>
153
+ <body>
154
+ <div id="status-banner"></div>
155
+ <header>
156
+ <h1>Chaos API</h1>
157
+ <div class="row">
158
+ <span style="color: var(--text-muted)">control API:</span>
159
+ <input id="control-url" type="text" value="http://localhost:51820" />
160
+ <button id="connect">Conectar</button>
161
+ </div>
162
+ </header>
163
+ <main>
164
+ <div id="error"></div>
165
+ <div id="scenario-list"></div>
166
+
167
+ <h2>Atividade recente</h2>
168
+ <div id="activity-list"></div>
169
+
170
+ <h2>Biblioteca de presets</h2>
171
+ <div id="preset-categories"></div>
172
+ <div id="preset-list"></div>
173
+
174
+ <h2>Import/export de config</h2>
175
+ <div class="row">
176
+ <button id="export-config" type="button">Exportar JSON</button>
177
+ <button id="import-config" type="button">Importar JSON</button>
178
+ <input id="import-file" type="file" accept="application/json" style="display: none" />
179
+ </div>
180
+
181
+ <h2>Runner de requisição de teste</h2>
182
+ <form class="runner-form" id="runner-form">
183
+ <select name="method" id="runner-method">
184
+ <option>GET</option>
185
+ <option>POST</option>
186
+ <option>PUT</option>
187
+ <option>PATCH</option>
188
+ <option>DELETE</option>
189
+ </select>
190
+ <input name="url" type="text" placeholder="http://localhost:3000/orders" required />
191
+ <textarea class="full" name="headers" placeholder='headers JSON (ex: {"Authorization":"Bearer ..."})'></textarea>
192
+ <textarea class="full" name="body" placeholder="body (JSON ou texto — ignorado em GET/DELETE)"></textarea>
193
+ <button class="full" type="submit">Enviar</button>
194
+ </form>
195
+ <div id="runner-response"></div>
196
+
197
+ <h2>Adicionar cenário</h2>
198
+ <form class="add-form" id="add-form">
199
+ <select name="type" id="type">
200
+ <option value="delay">delay</option>
201
+ <option value="error-response">error-response</option>
202
+ <option value="connection-reset">connection-reset</option>
203
+ <option value="unavailable">unavailable</option>
204
+ <option value="malformed-response">malformed-response</option>
205
+ <option value="stale-response">stale-response</option>
206
+ </select>
207
+ <input name="rate" type="number" min="0" max="1" step="0.1" value="1" placeholder="rate (0-1)" />
208
+ <input class="full" name="pattern" type="text" placeholder="rota (glob, ex: /orders/*) — vazio = global" />
209
+ <input class="full" name="options" type="text" placeholder='options JSON (ex: {"minMs":500,"maxMs":2000})' />
210
+ <button class="full" type="submit">Ativar cenário</button>
211
+ </form>
212
+ </main>
213
+ <script src="app.js"></script>
214
+ </body>
215
+ </html>
@@ -0,0 +1,10 @@
1
+ export interface GuardrailOptions {
2
+ allowInProduction?: boolean;
3
+ }
4
+ /**
5
+ * Blocks chaos scenarios when NODE_ENV=production unless explicitly overridden.
6
+ * See docs/PRD.md "Riscos" — an activated scenario leaking to prod breaks real clients.
7
+ */
8
+ export declare function isBlockedByGuardrail(options?: GuardrailOptions): boolean;
9
+ /** Test-only: resets the one-time warning so guardrail tests can assert on it repeatedly. */
10
+ export declare function resetGuardrailWarning(): void;
@@ -0,0 +1,20 @@
1
+ let warned = false;
2
+ /**
3
+ * Blocks chaos scenarios when NODE_ENV=production unless explicitly overridden.
4
+ * See docs/PRD.md "Riscos" — an activated scenario leaking to prod breaks real clients.
5
+ */
6
+ export function isBlockedByGuardrail(options = {}) {
7
+ const isProduction = process.env.NODE_ENV === "production";
8
+ if (!isProduction || options.allowInProduction)
9
+ return false;
10
+ if (!warned) {
11
+ warned = true;
12
+ console.warn("[chaos-api] NODE_ENV=production detected — chaos scenarios disabled. " +
13
+ "Pass { allowInProduction: true } to chaos() to override (not recommended).");
14
+ }
15
+ return true;
16
+ }
17
+ /** Test-only: resets the one-time warning so guardrail tests can assert on it repeatedly. */
18
+ export function resetGuardrailWarning() {
19
+ warned = false;
20
+ }
@@ -0,0 +1,22 @@
1
+ export { chaos } from "./adapters/express.js";
2
+ export type { ChaosOptions, ChaosInstance } from "./adapters/express.js";
3
+ export { chaosFastifyPlugin } from "./adapters/fastify.js";
4
+ export type { ChaosFastifyPlugin } from "./adapters/fastify.js";
5
+ export { createChaosNestMiddleware } from "./adapters/nestjs.js";
6
+ export type { ChaosNestMiddleware, NestLikeRequest, NestLikeResponse, NestMiddlewareFn } from "./adapters/nestjs.js";
7
+ export { chaosKoaMiddleware } from "./adapters/koa.js";
8
+ export type { ChaosKoaMiddleware } from "./adapters/koa.js";
9
+ export { StateStore, globToRegex } from "./core/state-store.js";
10
+ export type { RegisterScenarioInput, UpdateScenarioInput } from "./core/state-store.js";
11
+ export { ActivityLog } from "./core/activity-log.js";
12
+ export type { ActivityEvent, RecordActivityInput } from "./core/activity-log.js";
13
+ export { ScenarioEngine } from "./core/scenario-engine.js";
14
+ export type { ChaosRequestInfo, ChaosResponseController, LegacyScenarioType, ScenarioConfig, ScenarioDirection, ScenarioHandler, ScenarioResult, ScenarioScope, ScenarioType, } from "./core/types.js";
15
+ export * from "./scenarios/index.js";
16
+ export { PRESET_CATALOG, applyPreset, findPreset, listPresets } from "./presets/index.js";
17
+ export type { ApplyPresetOverrides, PresetCategory, PresetDefinition } from "./presets/index.js";
18
+ export { createChaosFetch } from "./outbound/index.js";
19
+ export type { ChaosFetchOptions } from "./outbound/index.js";
20
+ export { createControlApi } from "./dashboard/server/control-api.js";
21
+ export { startDashboard } from "./dashboard/server/index.js";
22
+ export { isBlockedByGuardrail, resetGuardrailWarning } from "./guardrail.js";
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export { chaos } from "./adapters/express.js";
2
+ export { chaosFastifyPlugin } from "./adapters/fastify.js";
3
+ export { createChaosNestMiddleware } from "./adapters/nestjs.js";
4
+ export { chaosKoaMiddleware } from "./adapters/koa.js";
5
+ export { StateStore, globToRegex } from "./core/state-store.js";
6
+ export { ActivityLog } from "./core/activity-log.js";
7
+ export { ScenarioEngine } from "./core/scenario-engine.js";
8
+ export * from "./scenarios/index.js";
9
+ export { PRESET_CATALOG, applyPreset, findPreset, listPresets } from "./presets/index.js";
10
+ export { createChaosFetch } from "./outbound/index.js";
11
+ export { createControlApi } from "./dashboard/server/control-api.js";
12
+ export { startDashboard } from "./dashboard/server/index.js";
13
+ export { isBlockedByGuardrail, resetGuardrailWarning } from "./guardrail.js";
@@ -0,0 +1,13 @@
1
+ import type { StateStore } from "../core/state-store.js";
2
+ import { type GuardrailOptions } from "../guardrail.js";
3
+ export interface ChaosFetchOptions extends GuardrailOptions {
4
+ /** Real fetch to call through to when no outbound scenario applies. Defaults to global `fetch`. */
5
+ baseFetch?: typeof fetch;
6
+ }
7
+ /**
8
+ * Wraps `fetch` so outbound calls can be intercepted by chaos scenarios scoped to a
9
+ * destination host (docs/PRD.md 6.4) — same `StateStore` the inbound middleware uses, scenarios
10
+ * registered with `{ direction: "outbound" }`. Fast-path: calls straight through to the real
11
+ * fetch when no outbound scenario matches the destination host, same no-op principle as inbound.
12
+ */
13
+ export declare function createChaosFetch(store: StateStore, options?: ChaosFetchOptions): typeof fetch;
@@ -0,0 +1,50 @@
1
+ import { ScenarioEngine } from "../core/scenario-engine.js";
2
+ import { isBlockedByGuardrail } from "../guardrail.js";
3
+ function extractHost(input) {
4
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
5
+ return new URL(url).host;
6
+ }
7
+ function bodyToString(body) {
8
+ return typeof body === "string" ? body : JSON.stringify(body ?? null);
9
+ }
10
+ /**
11
+ * Wraps `fetch` so outbound calls can be intercepted by chaos scenarios scoped to a
12
+ * destination host (docs/PRD.md 6.4) — same `StateStore` the inbound middleware uses, scenarios
13
+ * registered with `{ direction: "outbound" }`. Fast-path: calls straight through to the real
14
+ * fetch when no outbound scenario matches the destination host, same no-op principle as inbound.
15
+ */
16
+ export function createChaosFetch(store, options = {}) {
17
+ const baseFetch = options.baseFetch ?? fetch;
18
+ const engine = new ScenarioEngine(store);
19
+ return (async (input, init) => {
20
+ const host = extractHost(input);
21
+ if (isBlockedByGuardrail(options) || store.getActiveOutbound(host).length === 0) {
22
+ return baseFetch(input, init);
23
+ }
24
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
25
+ const recorded = { headers: {} };
26
+ const controller = {
27
+ status: (code) => {
28
+ recorded.status = code;
29
+ },
30
+ header: (name, value) => {
31
+ recorded.headers[name] = value;
32
+ },
33
+ send: (body) => {
34
+ recorded.body = body;
35
+ },
36
+ };
37
+ const result = await engine.resolveOutbound({ method, path: host }, controller);
38
+ if (result === "continue") {
39
+ return baseFetch(input, init);
40
+ }
41
+ if (recorded.status === undefined) {
42
+ // connection-reset: no response was ever written — simulate a dropped/failed connection.
43
+ throw new TypeError("chaos-api: simulated network failure (connection-reset)");
44
+ }
45
+ const headers = new Headers(recorded.headers);
46
+ if (!headers.has("Content-Type"))
47
+ headers.set("Content-Type", "application/json");
48
+ return new Response(bodyToString(recorded.body), { status: recorded.status, headers });
49
+ });
50
+ }
@@ -0,0 +1 @@
1
+ export { createChaosFetch, type ChaosFetchOptions } from "./chaos-fetch.js";
@@ -0,0 +1 @@
1
+ export { createChaosFetch } from "./chaos-fetch.js";
@@ -0,0 +1,8 @@
1
+ import type { PresetDefinition } from "./types.js";
2
+ /**
3
+ * Subset of the ~85-item catalog (docs/PRD.md 6.3) shipped in this increment — the categories
4
+ * flagged HTTP-simulável in the "Next" roadmap that don't need chaos outbound (6.4) or a
5
+ * composed-preset design (erro humano/black swan) to be useful today. Each entry is metadata
6
+ * layered on top of the 6 primitives (6.2) — it does not add a new `ScenarioType`.
7
+ */
8
+ export declare const PRESET_CATALOG: PresetDefinition[];
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Subset of the ~85-item catalog (docs/PRD.md 6.3) shipped in this increment — the categories
3
+ * flagged HTTP-simulável in the "Next" roadmap that don't need chaos outbound (6.4) or a
4
+ * composed-preset design (erro humano/black swan) to be useful today. Each entry is metadata
5
+ * layered on top of the 6 primitives (6.2) — it does not add a new `ScenarioType`.
6
+ */
7
+ export const PRESET_CATALOG = [
8
+ // Segurança
9
+ {
10
+ name: "auth-service-down",
11
+ category: "seguranca",
12
+ description: "Serviço de autenticação/autorização indisponível",
13
+ type: "unavailable",
14
+ options: { statusCode: 503 },
15
+ },
16
+ {
17
+ name: "expired-credentials",
18
+ category: "seguranca",
19
+ description: "Credenciais/token expirados",
20
+ type: "error-response",
21
+ options: { statusCodes: [401] },
22
+ },
23
+ {
24
+ name: "authz-denied",
25
+ category: "seguranca",
26
+ description: "Autorização negada (permissão insuficiente)",
27
+ type: "error-response",
28
+ options: { statusCodes: [403] },
29
+ },
30
+ {
31
+ name: "secret-rotation-failure",
32
+ category: "seguranca",
33
+ description: "Falha na rotação de secret — serviço não consegue autenticar",
34
+ type: "error-response",
35
+ options: { statusCodes: [500], body: { error: "secret rotation failure" } },
36
+ },
37
+ // Dependências externas
38
+ {
39
+ name: "third-party-timeout",
40
+ category: "dependencias-externas",
41
+ description: "Dependência de terceiro (API externa) não responde",
42
+ type: "connection-reset",
43
+ },
44
+ {
45
+ name: "third-party-500",
46
+ category: "dependencias-externas",
47
+ description: "Dependência de terceiro retorna erro 5xx",
48
+ type: "error-response",
49
+ options: { statusCodes: [502] },
50
+ },
51
+ {
52
+ name: "third-party-rate-limit",
53
+ category: "dependencias-externas",
54
+ description: "Dependência de terceiro aplica rate limit (429)",
55
+ type: "error-response",
56
+ options: { statusCodes: [429], headers: { "Retry-After": "30" } },
57
+ },
58
+ {
59
+ name: "object-storage-down",
60
+ category: "dependencias-externas",
61
+ description: "Object storage externo (S3 e similares) indisponível",
62
+ type: "unavailable",
63
+ options: { statusCode: 503 },
64
+ },
65
+ {
66
+ name: "idp-down",
67
+ category: "dependencias-externas",
68
+ description: "Identity provider externo indisponível",
69
+ type: "unavailable",
70
+ options: { statusCode: 503 },
71
+ },
72
+ // Configuração
73
+ {
74
+ name: "missing-env-var",
75
+ category: "configuracao",
76
+ description: "Variável de ambiente obrigatória faltando",
77
+ type: "error-response",
78
+ options: { statusCodes: [500], body: { error: "missing required environment variable" } },
79
+ },
80
+ {
81
+ name: "invalid-config",
82
+ category: "configuracao",
83
+ description: "Configuração inválida detectada na inicialização",
84
+ type: "error-response",
85
+ options: { statusCodes: [500], body: { error: "invalid configuration" } },
86
+ },
87
+ {
88
+ name: "wrong-endpoint",
89
+ category: "configuracao",
90
+ description: "Endpoint de dependência configurado errado",
91
+ type: "unavailable",
92
+ options: { statusCode: 503 },
93
+ },
94
+ {
95
+ name: "feature-flag-misconfigured",
96
+ category: "configuracao",
97
+ description: "Feature flag com valor incorreto quebra o fluxo",
98
+ type: "error-response",
99
+ options: { statusCodes: [500], body: { error: "feature flag misconfigured" } },
100
+ },
101
+ // Resource Exhaustion
102
+ {
103
+ name: "thread-pool-exhausted",
104
+ category: "resource-exhaustion",
105
+ description: "Thread pool esgotado",
106
+ type: "unavailable",
107
+ options: { statusCode: 503, retryAfterSeconds: 5 },
108
+ },
109
+ {
110
+ name: "connection-pool-exhausted",
111
+ category: "resource-exhaustion",
112
+ description: "Connection pool (DB/HTTP) esgotado",
113
+ type: "unavailable",
114
+ options: { statusCode: 503, retryAfterSeconds: 5 },
115
+ },
116
+ {
117
+ name: "ephemeral-ports-exhausted",
118
+ category: "resource-exhaustion",
119
+ description: "Portas efêmeras esgotadas — novas conexões falham",
120
+ type: "connection-reset",
121
+ },
122
+ {
123
+ name: "disk-iops-exhausted",
124
+ category: "resource-exhaustion",
125
+ description: "IOPS de disco no limite — respostas fortemente lentas",
126
+ type: "delay",
127
+ options: { minMs: 3000, maxMs: 8000 },
128
+ },
129
+ // Filesystem
130
+ {
131
+ name: "permission-denied",
132
+ category: "filesystem",
133
+ description: "Permissão negada ao acessar arquivo/recurso",
134
+ type: "error-response",
135
+ options: { statusCodes: [403] },
136
+ },
137
+ {
138
+ name: "missing-tls-cert",
139
+ category: "filesystem",
140
+ description: "Certificado TLS ausente no filesystem",
141
+ type: "error-response",
142
+ options: { statusCodes: [495], body: { error: "TLS certificate not found" } },
143
+ },
144
+ {
145
+ name: "nfs-unavailable",
146
+ category: "filesystem",
147
+ description: "Volume NFS indisponível",
148
+ type: "unavailable",
149
+ options: { statusCode: 503 },
150
+ },
151
+ {
152
+ name: "filesystem-corruption",
153
+ category: "filesystem",
154
+ description: "Corrupção de dados no filesystem — resposta corrompida",
155
+ type: "malformed-response",
156
+ },
157
+ ];
@@ -0,0 +1,15 @@
1
+ import type { StateStore } from "../core/state-store.js";
2
+ import type { ScenarioConfig, ScenarioScope } from "../core/types.js";
3
+ import type { PresetCategory, PresetDefinition } from "./types.js";
4
+ export { PRESET_CATALOG } from "./catalog.js";
5
+ export type { PresetCategory, PresetDefinition } from "./types.js";
6
+ export declare function findPreset(name: string): PresetDefinition | undefined;
7
+ export declare function listPresets(category?: PresetCategory): PresetDefinition[];
8
+ export interface ApplyPresetOverrides {
9
+ scope?: ScenarioScope;
10
+ rate?: number;
11
+ enabled?: boolean;
12
+ options?: Record<string, unknown>;
13
+ }
14
+ /** Registers a preset by name on `store`, resolving it to its `{primitivo, options, scope}` (docs/PRD.md 6.3). */
15
+ export declare function applyPreset(store: StateStore, name: string, overrides?: ApplyPresetOverrides): ScenarioConfig;
@@ -0,0 +1,22 @@
1
+ import { PRESET_CATALOG } from "./catalog.js";
2
+ export { PRESET_CATALOG } from "./catalog.js";
3
+ export function findPreset(name) {
4
+ return PRESET_CATALOG.find((preset) => preset.name === name);
5
+ }
6
+ export function listPresets(category) {
7
+ return category ? PRESET_CATALOG.filter((preset) => preset.category === category) : [...PRESET_CATALOG];
8
+ }
9
+ /** Registers a preset by name on `store`, resolving it to its `{primitivo, options, scope}` (docs/PRD.md 6.3). */
10
+ export function applyPreset(store, name, overrides = {}) {
11
+ const preset = findPreset(name);
12
+ if (!preset) {
13
+ throw new Error(`unknown preset "${name}"`);
14
+ }
15
+ return store.register({
16
+ type: preset.type,
17
+ scope: overrides.scope ?? preset.scope ?? "global",
18
+ rate: overrides.rate ?? preset.rate ?? 1,
19
+ enabled: overrides.enabled,
20
+ options: { ...preset.options, ...overrides.options },
21
+ });
22
+ }
@@ -0,0 +1,17 @@
1
+ import type { ScenarioScope, ScenarioType } from "../core/types.js";
2
+ /**
3
+ * Category subset shipped in this increment (docs/PRD.md 6.3 "Next" roadmap): only the
4
+ * categories flagged HTTP-simulável that don't need chaos outbound (6.4) or a composed-preset
5
+ * design (erro humano/black swan) to be useful today.
6
+ */
7
+ export type PresetCategory = "seguranca" | "dependencias-externas" | "configuracao" | "resource-exhaustion" | "filesystem";
8
+ export interface PresetDefinition {
9
+ /** Stable kebab-case id, e.g. "auth-service-down". Unique within PRESET_CATALOG. */
10
+ name: string;
11
+ category: PresetCategory;
12
+ description: string;
13
+ type: ScenarioType;
14
+ options?: Record<string, unknown>;
15
+ scope?: ScenarioScope;
16
+ rate?: number;
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { ScenarioHandler } from "../core/types.js";
2
+ /**
3
+ * Simulates a dropped connection / hung upstream: intentionally never writes a response.
4
+ * Adapters must not call `next()`/end the request when this scenario terminates the
5
+ * chain — the connection stays open until the client or server times out.
6
+ */
7
+ export declare const connectionResetScenario: ScenarioHandler;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Simulates a dropped connection / hung upstream: intentionally never writes a response.
3
+ * Adapters must not call `next()`/end the request when this scenario terminates the
4
+ * chain — the connection stays open until the client or server times out.
5
+ */
6
+ export const connectionResetScenario = () => {
7
+ return "terminated";
8
+ };
@@ -0,0 +1,6 @@
1
+ import type { ScenarioHandler } from "../core/types.js";
2
+ export interface DelayOptions {
3
+ minMs?: number;
4
+ maxMs?: number;
5
+ }
6
+ export declare const delayScenario: ScenarioHandler;
@@ -0,0 +1,9 @@
1
+ function sleep(ms) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
4
+ export const delayScenario = async (_ctx, options) => {
5
+ const { minMs = 500, maxMs } = options;
6
+ const ms = maxMs !== undefined && maxMs > minMs ? minMs + Math.random() * (maxMs - minMs) : minMs;
7
+ await sleep(ms);
8
+ return "continue";
9
+ };
@@ -0,0 +1,9 @@
1
+ import type { ScenarioHandler } from "../core/types.js";
2
+ export interface ErrorResponseOptions {
3
+ statusCodes?: number[];
4
+ body?: unknown;
5
+ headers?: Record<string, string>;
6
+ /** Restrict to these HTTP methods (e.g. write verbs only). Omitted = all methods. */
7
+ methods?: string[];
8
+ }
9
+ export declare const errorResponseScenario: ScenarioHandler;
@@ -0,0 +1,13 @@
1
+ export const errorResponseScenario = (ctx, options) => {
2
+ const { statusCodes = [500], body, headers, methods } = options;
3
+ if (methods && !methods.includes(ctx.req.method.toUpperCase()))
4
+ return "continue";
5
+ const code = statusCodes[Math.floor(Math.random() * statusCodes.length)];
6
+ if (headers) {
7
+ for (const [name, value] of Object.entries(headers))
8
+ ctx.res.header(name, value);
9
+ }
10
+ ctx.res.status(code);
11
+ ctx.res.send(body ?? { error: "chaos-api: injected error", status: code });
12
+ return "terminated";
13
+ };
@@ -0,0 +1,7 @@
1
+ export { delayScenario, type DelayOptions } from "./delay.js";
2
+ export { errorResponseScenario, type ErrorResponseOptions } from "./error-response.js";
3
+ export { connectionResetScenario } from "./connection-reset.js";
4
+ export { unavailableScenario, type UnavailableOptions } from "./unavailable.js";
5
+ export { malformedResponseScenario, type MalformedResponseOptions } from "./malformed-response.js";
6
+ export { staleResponseScenario, type StaleResponseOptions } from "./stale-response.js";
7
+ export { SCENARIO_REGISTRY, type ScenarioDefinition } from "./registry.js";