@henriquecosta/chaos-api 1.0.1 → 1.0.3
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/adapters/express.d.ts +9 -0
- package/dist/adapters/express.js +4 -3
- package/dist/adapters/fastify.js +5 -4
- package/dist/adapters/koa.js +4 -3
- package/dist/adapters/nestjs.js +5 -4
- package/dist/bin/chaos-api.js +9 -4
- package/dist/core/ignore-paths.d.ts +7 -0
- package/dist/core/ignore-paths.js +11 -0
- package/dist/dashboard/server/control-api.d.ts +35 -2
- package/dist/dashboard/server/control-api.js +57 -7
- package/dist/dashboard/server/index.d.ts +2 -0
- package/dist/dashboard/server/index.js +3 -2
- package/dist/dashboard/ui/app.js +19 -1
- package/dist/dashboard/ui/index.html +2 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { NextFunction, Request, Response } from "express";
|
|
2
2
|
import type { Server } from "node:http";
|
|
3
3
|
import { ActivityLog } from "../core/activity-log.js";
|
|
4
|
+
import { type IgnorePathPattern } from "../core/ignore-paths.js";
|
|
4
5
|
import { StateStore } from "../core/state-store.js";
|
|
5
6
|
export interface ChaosOptions {
|
|
6
7
|
/** Bypass the NODE_ENV=production guardrail. Not recommended. */
|
|
@@ -9,8 +10,16 @@ export interface ChaosOptions {
|
|
|
9
10
|
store?: StateStore;
|
|
10
11
|
/** Reuse an existing ActivityLog (e.g. to share state across adapters in tests). */
|
|
11
12
|
activityLog?: ActivityLog;
|
|
13
|
+
/** Max events kept by the activity feed (docs/PRD.md 6.5). Default 200. Ignored if `activityLog` is passed. */
|
|
14
|
+
activityLogCapacity?: number;
|
|
12
15
|
/** If set, starts the local control API on this port for the dashboard UI to talk to. */
|
|
13
16
|
controlPort?: number;
|
|
17
|
+
/** Bind address for the control API. Default `"127.0.0.1"` — keep it off the network unless you mean to expose it. */
|
|
18
|
+
controlHost?: string;
|
|
19
|
+
/** `Access-Control-Allow-Origin` for the control API. Default `"*"`. */
|
|
20
|
+
corsOrigin?: string;
|
|
21
|
+
/** Paths that always bypass chaos scenarios (glob strings like `"/health*"` or RegExp), e.g. health checks. */
|
|
22
|
+
ignorePaths?: IgnorePathPattern[];
|
|
14
23
|
}
|
|
15
24
|
export interface ChaosInstance {
|
|
16
25
|
(req: Request, res: Response, next: NextFunction): void;
|
package/dist/adapters/express.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { ActivityLog } from "../core/activity-log.js";
|
|
2
|
+
import { isIgnoredPath } from "../core/ignore-paths.js";
|
|
2
3
|
import { ScenarioEngine } from "../core/scenario-engine.js";
|
|
3
4
|
import { StateStore } from "../core/state-store.js";
|
|
4
5
|
import { createControlApi } from "../dashboard/server/control-api.js";
|
|
5
6
|
import { isBlockedByGuardrail } from "../guardrail.js";
|
|
6
7
|
export function chaos(options = {}) {
|
|
7
8
|
const store = options.store ?? new StateStore();
|
|
8
|
-
const activityLog = options.activityLog ?? new ActivityLog();
|
|
9
|
+
const activityLog = options.activityLog ?? new ActivityLog(options.activityLogCapacity);
|
|
9
10
|
const engine = new ScenarioEngine(store, activityLog);
|
|
10
11
|
const middleware = ((req, res, next) => {
|
|
11
|
-
if (isBlockedByGuardrail(options)) {
|
|
12
|
+
if (isBlockedByGuardrail(options) || isIgnoredPath(req.path, options.ignorePaths)) {
|
|
12
13
|
next();
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
@@ -34,7 +35,7 @@ export function chaos(options = {}) {
|
|
|
34
35
|
middleware.store = store;
|
|
35
36
|
middleware.activityLog = activityLog;
|
|
36
37
|
if (options.controlPort) {
|
|
37
|
-
middleware.controlApi = createControlApi(store, activityLog).listen(options.controlPort);
|
|
38
|
+
middleware.controlApi = createControlApi(store, activityLog, { corsOrigin: options.corsOrigin }).listen(options.controlPort, options.controlHost ?? "127.0.0.1");
|
|
38
39
|
}
|
|
39
40
|
return middleware;
|
|
40
41
|
}
|
package/dist/adapters/fastify.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { ActivityLog } from "../core/activity-log.js";
|
|
2
|
+
import { isIgnoredPath } from "../core/ignore-paths.js";
|
|
2
3
|
import { ScenarioEngine } from "../core/scenario-engine.js";
|
|
3
4
|
import { StateStore } from "../core/state-store.js";
|
|
4
5
|
import { createControlApi } from "../dashboard/server/control-api.js";
|
|
5
6
|
import { isBlockedByGuardrail } from "../guardrail.js";
|
|
6
7
|
export function chaosFastifyPlugin(options = {}) {
|
|
7
8
|
const store = options.store ?? new StateStore();
|
|
8
|
-
const activityLog = options.activityLog ?? new ActivityLog();
|
|
9
|
+
const activityLog = options.activityLog ?? new ActivityLog(options.activityLogCapacity);
|
|
9
10
|
const engine = new ScenarioEngine(store, activityLog);
|
|
10
11
|
const plugin = (async (fastify) => {
|
|
11
12
|
fastify.addHook("onRequest", async (request, reply) => {
|
|
12
|
-
|
|
13
|
+
const path = request.url.split("?")[0];
|
|
14
|
+
if (isBlockedByGuardrail(options) || isIgnoredPath(path, options.ignorePaths))
|
|
13
15
|
return;
|
|
14
16
|
const controller = {
|
|
15
17
|
status: (code) => {
|
|
@@ -22,7 +24,6 @@ export function chaosFastifyPlugin(options = {}) {
|
|
|
22
24
|
reply.send(body);
|
|
23
25
|
},
|
|
24
26
|
};
|
|
25
|
-
const path = request.url.split("?")[0];
|
|
26
27
|
const result = await engine.resolve({ method: request.method, path }, controller);
|
|
27
28
|
if (result === "terminated" && !reply.sent) {
|
|
28
29
|
// random-timeout: no response was written on purpose — take Fastify
|
|
@@ -38,7 +39,7 @@ export function chaosFastifyPlugin(options = {}) {
|
|
|
38
39
|
plugin.store = store;
|
|
39
40
|
plugin.activityLog = activityLog;
|
|
40
41
|
if (options.controlPort) {
|
|
41
|
-
plugin.controlApi = createControlApi(store, activityLog).listen(options.controlPort);
|
|
42
|
+
plugin.controlApi = createControlApi(store, activityLog, { corsOrigin: options.corsOrigin }).listen(options.controlPort, options.controlHost ?? "127.0.0.1");
|
|
42
43
|
}
|
|
43
44
|
return plugin;
|
|
44
45
|
}
|
package/dist/adapters/koa.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ActivityLog } from "../core/activity-log.js";
|
|
2
|
+
import { isIgnoredPath } from "../core/ignore-paths.js";
|
|
2
3
|
import { ScenarioEngine } from "../core/scenario-engine.js";
|
|
3
4
|
import { StateStore } from "../core/state-store.js";
|
|
4
5
|
import { createControlApi } from "../dashboard/server/control-api.js";
|
|
@@ -6,10 +7,10 @@ import { isBlockedByGuardrail } from "../guardrail.js";
|
|
|
6
7
|
/** docs/PRD.md 6.6 — thin adapter over the shared core, same pattern as adapters/express.ts and adapters/fastify.ts. */
|
|
7
8
|
export function chaosKoaMiddleware(options = {}) {
|
|
8
9
|
const store = options.store ?? new StateStore();
|
|
9
|
-
const activityLog = options.activityLog ?? new ActivityLog();
|
|
10
|
+
const activityLog = options.activityLog ?? new ActivityLog(options.activityLogCapacity);
|
|
10
11
|
const engine = new ScenarioEngine(store, activityLog);
|
|
11
12
|
const middleware = (async (ctx, next) => {
|
|
12
|
-
if (isBlockedByGuardrail(options)) {
|
|
13
|
+
if (isBlockedByGuardrail(options) || isIgnoredPath(ctx.path, options.ignorePaths)) {
|
|
13
14
|
await next();
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
@@ -41,7 +42,7 @@ export function chaosKoaMiddleware(options = {}) {
|
|
|
41
42
|
middleware.store = store;
|
|
42
43
|
middleware.activityLog = activityLog;
|
|
43
44
|
if (options.controlPort) {
|
|
44
|
-
middleware.controlApi = createControlApi(store, activityLog).listen(options.controlPort);
|
|
45
|
+
middleware.controlApi = createControlApi(store, activityLog, { corsOrigin: options.corsOrigin }).listen(options.controlPort, options.controlHost ?? "127.0.0.1");
|
|
45
46
|
}
|
|
46
47
|
return middleware;
|
|
47
48
|
}
|
package/dist/adapters/nestjs.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ActivityLog } from "../core/activity-log.js";
|
|
2
|
+
import { isIgnoredPath } from "../core/ignore-paths.js";
|
|
2
3
|
import { ScenarioEngine } from "../core/scenario-engine.js";
|
|
3
4
|
import { StateStore } from "../core/state-store.js";
|
|
4
5
|
import { createControlApi } from "../dashboard/server/control-api.js";
|
|
@@ -10,10 +11,11 @@ import { isBlockedByGuardrail } from "../guardrail.js";
|
|
|
10
11
|
*/
|
|
11
12
|
export function createChaosNestMiddleware(options = {}) {
|
|
12
13
|
const store = options.store ?? new StateStore();
|
|
13
|
-
const activityLog = options.activityLog ?? new ActivityLog();
|
|
14
|
+
const activityLog = options.activityLog ?? new ActivityLog(options.activityLogCapacity);
|
|
14
15
|
const engine = new ScenarioEngine(store, activityLog);
|
|
15
16
|
const middleware = ((req, res, next) => {
|
|
16
|
-
|
|
17
|
+
const path = (req.originalUrl ?? req.url).split("?")[0];
|
|
18
|
+
if (isBlockedByGuardrail(options) || isIgnoredPath(path, options.ignorePaths)) {
|
|
17
19
|
next();
|
|
18
20
|
return;
|
|
19
21
|
}
|
|
@@ -34,7 +36,6 @@ export function createChaosNestMiddleware(options = {}) {
|
|
|
34
36
|
res.end(typeof body === "string" ? body : JSON.stringify(body));
|
|
35
37
|
},
|
|
36
38
|
};
|
|
37
|
-
const path = (req.originalUrl ?? req.url).split("?")[0];
|
|
38
39
|
engine
|
|
39
40
|
.resolve({ method: req.method, path }, controller)
|
|
40
41
|
.then((result) => {
|
|
@@ -46,7 +47,7 @@ export function createChaosNestMiddleware(options = {}) {
|
|
|
46
47
|
middleware.store = store;
|
|
47
48
|
middleware.activityLog = activityLog;
|
|
48
49
|
if (options.controlPort) {
|
|
49
|
-
middleware.controlApi = createControlApi(store, activityLog).listen(options.controlPort);
|
|
50
|
+
middleware.controlApi = createControlApi(store, activityLog, { corsOrigin: options.corsOrigin }).listen(options.controlPort, options.controlHost ?? "127.0.0.1");
|
|
50
51
|
}
|
|
51
52
|
return middleware;
|
|
52
53
|
}
|
package/dist/bin/chaos-api.js
CHANGED
|
@@ -14,20 +14,25 @@ function hasFlag(flag) {
|
|
|
14
14
|
}
|
|
15
15
|
if (command === "dashboard") {
|
|
16
16
|
const portFlag = flagValue("--port");
|
|
17
|
-
|
|
17
|
+
const hostFlag = flagValue("--host");
|
|
18
|
+
startDashboard({ port: portFlag ? Number(portFlag) : undefined, host: hostFlag });
|
|
18
19
|
if (!hasFlag("--no-control-api")) {
|
|
19
20
|
const controlPortFlag = flagValue("--control-port");
|
|
21
|
+
const controlHostFlag = flagValue("--control-host");
|
|
22
|
+
const corsOriginFlag = flagValue("--cors-origin");
|
|
20
23
|
const controlPort = controlPortFlag ? Number(controlPortFlag) : DEFAULT_CONTROL_PORT;
|
|
24
|
+
const controlHost = controlHostFlag ?? "127.0.0.1";
|
|
21
25
|
const store = new StateStore();
|
|
22
26
|
const activityLog = new ActivityLog();
|
|
23
|
-
createControlApi(store, activityLog).listen(controlPort, () => {
|
|
24
|
-
console.log(`[chaos-api] standalone control API running at http
|
|
27
|
+
createControlApi(store, activityLog, { corsOrigin: corsOriginFlag }).listen(controlPort, controlHost, () => {
|
|
28
|
+
console.log(`[chaos-api] standalone control API running at http://${controlHost}:${controlPort} ` +
|
|
25
29
|
"(demo StateStore, not wired to a real app — pass --no-control-api if your app " +
|
|
26
30
|
"already runs chaos({ controlPort }) itself)");
|
|
27
31
|
});
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
else {
|
|
31
|
-
console.error(`Unknown command: ${command ?? "(none)"}. Usage: chaos-api dashboard
|
|
35
|
+
console.error(`Unknown command: ${command ?? "(none)"}. Usage: chaos-api dashboard ` +
|
|
36
|
+
"[--port <n>] [--host <addr>] [--control-port <n>] [--control-host <addr>] [--cors-origin <origin>] [--no-control-api]");
|
|
32
37
|
process.exit(1);
|
|
33
38
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type IgnorePathPattern = string | RegExp;
|
|
2
|
+
/**
|
|
3
|
+
* True if `path` matches one of the configured ignore patterns — those requests bypass chaos
|
|
4
|
+
* scenarios entirely (e.g. health checks, the control API's own routes when mounted on the same
|
|
5
|
+
* server as the host app). String patterns use the same glob syntax as scenario scopes (`*`).
|
|
6
|
+
*/
|
|
7
|
+
export declare function isIgnoredPath(path: string, patterns?: IgnorePathPattern[]): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { globToRegex } from "./state-store.js";
|
|
2
|
+
/**
|
|
3
|
+
* True if `path` matches one of the configured ignore patterns — those requests bypass chaos
|
|
4
|
+
* scenarios entirely (e.g. health checks, the control API's own routes when mounted on the same
|
|
5
|
+
* server as the host app). String patterns use the same glob syntax as scenario scopes (`*`).
|
|
6
|
+
*/
|
|
7
|
+
export function isIgnoredPath(path, patterns) {
|
|
8
|
+
if (!patterns?.length)
|
|
9
|
+
return false;
|
|
10
|
+
return patterns.some((pattern) => (typeof pattern === "string" ? globToRegex(pattern) : pattern).test(path));
|
|
11
|
+
}
|
|
@@ -1,9 +1,42 @@
|
|
|
1
|
-
import { type Server } from "node:http";
|
|
1
|
+
import { type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
2
2
|
import type { ActivityLog } from "../../core/activity-log.js";
|
|
3
3
|
import type { StateStore } from "../../core/state-store.js";
|
|
4
|
+
export interface ControlApiOptions {
|
|
5
|
+
/** `Access-Control-Allow-Origin` value. Default `"*"` (dashboard-ui may run on any origin/port). */
|
|
6
|
+
corsOrigin?: string;
|
|
7
|
+
}
|
|
8
|
+
export type ControlApiNext = (err?: unknown) => void;
|
|
4
9
|
/**
|
|
5
10
|
* Local control API for the scenario StateStore living inside the user's app process.
|
|
6
11
|
* dashboard-ui talks to this directly (CORS-open, localhost-only by convention) — the
|
|
7
12
|
* dashboard-server process only serves static UI files, it does not proxy this API.
|
|
13
|
+
*
|
|
14
|
+
* Runs as its own `http.Server` on `controlPort`. If you'd rather not open a second port,
|
|
15
|
+
* see `createControlApiMiddleware` — it mounts these same routes onto the host app's own
|
|
16
|
+
* server/port instead.
|
|
8
17
|
*/
|
|
9
|
-
export declare function createControlApi(store: StateStore, activityLog?: ActivityLog): Server;
|
|
18
|
+
export declare function createControlApi(store: StateStore, activityLog?: ActivityLog, options?: ControlApiOptions): Server;
|
|
19
|
+
export type ControlApiMiddleware = (req: IncomingMessage, res: ServerResponse, next: ControlApiNext) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Express/Nest-compatible middleware (`(req, res, next)`) that serves the same routes as
|
|
22
|
+
* `createControlApi`, but mounted directly onto the host app's own server — no extra port.
|
|
23
|
+
* Calls `next()` untouched for any request that isn't one of ours, so it can sit alongside the
|
|
24
|
+
* host's real routes: `app.use(createControlApiMiddleware(store, activityLog))`.
|
|
25
|
+
*
|
|
26
|
+
* For Fastify/Koa, use `createControlApiFastifyPlugin` / `createControlApiKoaMiddleware` instead —
|
|
27
|
+
* their request/response objects aren't raw `http.IncomingMessage`/`ServerResponse`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createControlApiMiddleware(store: StateStore, activityLog?: ActivityLog, options?: ControlApiOptions): ControlApiMiddleware;
|
|
30
|
+
/**
|
|
31
|
+
* True if `pathname` targets one of this control API's own resources, wherever it's mounted.
|
|
32
|
+
* Exported so the Fastify/Koa wrappers (which can't rely on an Express-style `next`) can decide
|
|
33
|
+
* up front whether a request is ours before touching their request/response objects.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isControlApiRoute(pathname: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Handles one control API request against raw Node req/res. Exported for framework wrappers
|
|
38
|
+
* (Fastify's `request.raw`/`reply.raw`, Koa's `ctx.req`/`ctx.res`) that already know — via
|
|
39
|
+
* `isControlApiRoute` — that the request is ours, so they call this directly instead of going
|
|
40
|
+
* through the `next`-based fallback in `createControlApiMiddleware`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function handleControlApiRequest(req: IncomingMessage, res: ServerResponse, store: StateStore, activityLog: ActivityLog | undefined, corsOrigin: string, next?: ControlApiNext): Promise<void>;
|
|
@@ -1,34 +1,84 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { applyPreset, findPreset, listPresets } from "../../presets/index.js";
|
|
3
3
|
const CORS_HEADERS = {
|
|
4
|
-
"Access-Control-Allow-Origin": "*",
|
|
5
4
|
"Access-Control-Allow-Methods": "GET,POST,PATCH,DELETE,OPTIONS",
|
|
6
5
|
"Access-Control-Allow-Headers": "Content-Type",
|
|
7
6
|
};
|
|
7
|
+
/** Resource names this control API owns — anything else isn't ours (see `isControlApiRoute` below). */
|
|
8
|
+
const KNOWN_RESOURCES = ["scenarios", "activity", "presets", "config"];
|
|
8
9
|
/**
|
|
9
10
|
* Local control API for the scenario StateStore living inside the user's app process.
|
|
10
11
|
* dashboard-ui talks to this directly (CORS-open, localhost-only by convention) — the
|
|
11
12
|
* dashboard-server process only serves static UI files, it does not proxy this API.
|
|
13
|
+
*
|
|
14
|
+
* Runs as its own `http.Server` on `controlPort`. If you'd rather not open a second port,
|
|
15
|
+
* see `createControlApiMiddleware` — it mounts these same routes onto the host app's own
|
|
16
|
+
* server/port instead.
|
|
12
17
|
*/
|
|
13
|
-
export function createControlApi(store, activityLog) {
|
|
18
|
+
export function createControlApi(store, activityLog, options = {}) {
|
|
19
|
+
const corsOrigin = options.corsOrigin ?? "*";
|
|
14
20
|
return createServer((req, res) => {
|
|
15
|
-
void
|
|
21
|
+
void handleControlApiRequest(req, res, store, activityLog, corsOrigin);
|
|
16
22
|
});
|
|
17
23
|
}
|
|
18
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Express/Nest-compatible middleware (`(req, res, next)`) that serves the same routes as
|
|
26
|
+
* `createControlApi`, but mounted directly onto the host app's own server — no extra port.
|
|
27
|
+
* Calls `next()` untouched for any request that isn't one of ours, so it can sit alongside the
|
|
28
|
+
* host's real routes: `app.use(createControlApiMiddleware(store, activityLog))`.
|
|
29
|
+
*
|
|
30
|
+
* For Fastify/Koa, use `createControlApiFastifyPlugin` / `createControlApiKoaMiddleware` instead —
|
|
31
|
+
* their request/response objects aren't raw `http.IncomingMessage`/`ServerResponse`.
|
|
32
|
+
*/
|
|
33
|
+
export function createControlApiMiddleware(store, activityLog, options = {}) {
|
|
34
|
+
const corsOrigin = options.corsOrigin ?? "*";
|
|
35
|
+
return (req, res, next) => {
|
|
36
|
+
void handleControlApiRequest(req, res, store, activityLog, corsOrigin, next);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* True if `pathname` targets one of this control API's own resources, wherever it's mounted.
|
|
41
|
+
* Exported so the Fastify/Koa wrappers (which can't rely on an Express-style `next`) can decide
|
|
42
|
+
* up front whether a request is ours before touching their request/response objects.
|
|
43
|
+
*/
|
|
44
|
+
export function isControlApiRoute(pathname) {
|
|
45
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
46
|
+
const apiIndex = segments.lastIndexOf("api");
|
|
47
|
+
return apiIndex !== -1 && KNOWN_RESOURCES.includes(segments[apiIndex + 1] ?? "");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Handles one control API request against raw Node req/res. Exported for framework wrappers
|
|
51
|
+
* (Fastify's `request.raw`/`reply.raw`, Koa's `ctx.req`/`ctx.res`) that already know — via
|
|
52
|
+
* `isControlApiRoute` — that the request is ours, so they call this directly instead of going
|
|
53
|
+
* through the `next`-based fallback in `createControlApiMiddleware`.
|
|
54
|
+
*/
|
|
55
|
+
export async function handleControlApiRequest(req, res, store, activityLog, corsOrigin, next) {
|
|
56
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
57
|
+
// When mounted as middleware (`next` present), a non-matching request belongs to the host
|
|
58
|
+
// app — pass it through untouched instead of claiming it with our own CORS headers/404.
|
|
59
|
+
if (next && !isControlApiRoute(url.pathname)) {
|
|
60
|
+
next();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.setHeader("Access-Control-Allow-Origin", corsOrigin);
|
|
19
64
|
for (const [key, value] of Object.entries(CORS_HEADERS))
|
|
20
65
|
res.setHeader(key, value);
|
|
21
66
|
if (req.method === "OPTIONS") {
|
|
22
67
|
res.writeHead(204).end();
|
|
23
68
|
return;
|
|
24
69
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
70
|
+
const allSegments = url.pathname.split("/").filter(Boolean);
|
|
71
|
+
// Anchor on the *last* "api" segment, not position 0 — a host may mount this control API
|
|
72
|
+
// (as middleware, or behind a proxy) underneath its own base path (e.g.
|
|
73
|
+
// `/api/v1/notebooks/api/activity`). Anchoring on position 0 breaks in that case; anchoring
|
|
74
|
+
// on the last "api" keeps the package route-independent of wherever the host puts it.
|
|
75
|
+
const apiIndex = allSegments.lastIndexOf("api");
|
|
27
76
|
try {
|
|
28
|
-
if (
|
|
77
|
+
if (apiIndex === -1) {
|
|
29
78
|
notFound(res);
|
|
30
79
|
return;
|
|
31
80
|
}
|
|
81
|
+
const segments = allSegments.slice(apiIndex);
|
|
32
82
|
if (segments[1] === "activity" && segments.length === 2 && req.method === "GET") {
|
|
33
83
|
const limitParam = url.searchParams.get("limit");
|
|
34
84
|
sendJson(res, 200, activityLog?.list(limitParam ? Number(limitParam) : undefined) ?? []);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Server } from "node:http";
|
|
2
2
|
export interface StartDashboardOptions {
|
|
3
3
|
port?: number;
|
|
4
|
+
/** Bind address. Default `"127.0.0.1"` — set to `"0.0.0.0"` to expose it beyond localhost. */
|
|
5
|
+
host?: string;
|
|
4
6
|
}
|
|
5
7
|
/**
|
|
6
8
|
* Serves the static dashboard-ui. This process does not hold any scenario state —
|
|
@@ -16,11 +16,12 @@ const MIME = {
|
|
|
16
16
|
*/
|
|
17
17
|
export function startDashboard(options = {}) {
|
|
18
18
|
const port = options.port ?? 4000;
|
|
19
|
+
const host = options.host ?? "127.0.0.1";
|
|
19
20
|
const server = createServer((req, res) => {
|
|
20
21
|
void serveStatic(req.url ?? "/", res);
|
|
21
22
|
});
|
|
22
|
-
server.listen(port, () => {
|
|
23
|
-
console.log(`[chaos-api] dashboard running at http
|
|
23
|
+
server.listen(port, host, () => {
|
|
24
|
+
console.log(`[chaos-api] dashboard running at http://${host}:${port}/dashboard`);
|
|
24
25
|
});
|
|
25
26
|
return server;
|
|
26
27
|
}
|
package/dist/dashboard/ui/app.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const STORAGE_KEY = "chaos-api:control-url";
|
|
2
|
+
const POLL_STORAGE_KEY = "chaos-api:poll-interval";
|
|
2
3
|
|
|
3
4
|
const controlUrlInput = document.getElementById("control-url");
|
|
5
|
+
const pollIntervalInput = document.getElementById("poll-interval");
|
|
4
6
|
const connectButton = document.getElementById("connect");
|
|
5
7
|
const listEl = document.getElementById("scenario-list");
|
|
6
8
|
const bannerEl = document.getElementById("status-banner");
|
|
@@ -26,6 +28,7 @@ const PRESET_CATEGORIES = [
|
|
|
26
28
|
let activePresetCategory = "";
|
|
27
29
|
|
|
28
30
|
controlUrlInput.value = localStorage.getItem(STORAGE_KEY) || controlUrlInput.value;
|
|
31
|
+
pollIntervalInput.value = localStorage.getItem(POLL_STORAGE_KEY) || pollIntervalInput.value;
|
|
29
32
|
|
|
30
33
|
function controlUrl() {
|
|
31
34
|
return controlUrlInput.value.replace(/\/$/, "");
|
|
@@ -192,13 +195,28 @@ async function refreshPresets() {
|
|
|
192
195
|
if (presets) renderPresets(presets);
|
|
193
196
|
}
|
|
194
197
|
|
|
198
|
+
let pollTimer;
|
|
199
|
+
|
|
200
|
+
function restartPolling() {
|
|
201
|
+
clearInterval(pollTimer);
|
|
202
|
+
const ms = Number(pollIntervalInput.value) || 3000;
|
|
203
|
+
pollTimer = setInterval(refreshActivity, ms);
|
|
204
|
+
}
|
|
205
|
+
|
|
195
206
|
connectButton.addEventListener("click", () => {
|
|
196
207
|
localStorage.setItem(STORAGE_KEY, controlUrlInput.value);
|
|
208
|
+
localStorage.setItem(POLL_STORAGE_KEY, pollIntervalInput.value);
|
|
209
|
+
restartPolling();
|
|
197
210
|
refresh();
|
|
198
211
|
refreshActivity();
|
|
199
212
|
refreshPresets();
|
|
200
213
|
});
|
|
201
214
|
|
|
215
|
+
pollIntervalInput.addEventListener("change", () => {
|
|
216
|
+
localStorage.setItem(POLL_STORAGE_KEY, pollIntervalInput.value);
|
|
217
|
+
restartPolling();
|
|
218
|
+
});
|
|
219
|
+
|
|
202
220
|
exportButton.addEventListener("click", async () => {
|
|
203
221
|
const config = await withErrorHandling(() => api("/api/config"));
|
|
204
222
|
if (!config) return;
|
|
@@ -299,7 +317,7 @@ function buildRunnerResult(ok, statusLine, headersText, bodyText) {
|
|
|
299
317
|
return wrapper;
|
|
300
318
|
}
|
|
301
319
|
|
|
302
|
-
|
|
320
|
+
restartPolling();
|
|
303
321
|
|
|
304
322
|
addForm.addEventListener("submit", async (e) => {
|
|
305
323
|
e.preventDefault();
|
|
@@ -157,6 +157,8 @@
|
|
|
157
157
|
<div class="row">
|
|
158
158
|
<span style="color: var(--text-muted)">control API:</span>
|
|
159
159
|
<input id="control-url" type="text" value="http://localhost:51820" />
|
|
160
|
+
<span style="color: var(--text-muted)">polling (ms):</span>
|
|
161
|
+
<input id="poll-interval" type="number" min="500" step="500" value="3000" style="width: 80px" />
|
|
160
162
|
<button id="connect">Conectar</button>
|
|
161
163
|
</div>
|
|
162
164
|
</header>
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export { StateStore, globToRegex } from "./core/state-store.js";
|
|
|
10
10
|
export type { RegisterScenarioInput, UpdateScenarioInput } from "./core/state-store.js";
|
|
11
11
|
export { ActivityLog } from "./core/activity-log.js";
|
|
12
12
|
export type { ActivityEvent, RecordActivityInput } from "./core/activity-log.js";
|
|
13
|
+
export { isIgnoredPath } from "./core/ignore-paths.js";
|
|
14
|
+
export type { IgnorePathPattern } from "./core/ignore-paths.js";
|
|
13
15
|
export { ScenarioEngine } from "./core/scenario-engine.js";
|
|
14
16
|
export type { ChaosRequestInfo, ChaosResponseController, LegacyScenarioType, ScenarioConfig, ScenarioDirection, ScenarioHandler, ScenarioResult, ScenarioScope, ScenarioType, } from "./core/types.js";
|
|
15
17
|
export * from "./scenarios/index.js";
|
|
@@ -18,5 +20,7 @@ export type { ApplyPresetOverrides, PresetCategory, PresetDefinition } from "./p
|
|
|
18
20
|
export { createChaosFetch } from "./outbound/index.js";
|
|
19
21
|
export type { ChaosFetchOptions } from "./outbound/index.js";
|
|
20
22
|
export { createControlApi } from "./dashboard/server/control-api.js";
|
|
23
|
+
export type { ControlApiOptions } from "./dashboard/server/control-api.js";
|
|
21
24
|
export { startDashboard } from "./dashboard/server/index.js";
|
|
25
|
+
export type { StartDashboardOptions } from "./dashboard/server/index.js";
|
|
22
26
|
export { isBlockedByGuardrail, resetGuardrailWarning } from "./guardrail.js";
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { createChaosNestMiddleware } from "./adapters/nestjs.js";
|
|
|
4
4
|
export { chaosKoaMiddleware } from "./adapters/koa.js";
|
|
5
5
|
export { StateStore, globToRegex } from "./core/state-store.js";
|
|
6
6
|
export { ActivityLog } from "./core/activity-log.js";
|
|
7
|
+
export { isIgnoredPath } from "./core/ignore-paths.js";
|
|
7
8
|
export { ScenarioEngine } from "./core/scenario-engine.js";
|
|
8
9
|
export * from "./scenarios/index.js";
|
|
9
10
|
export { PRESET_CATALOG, applyPreset, findPreset, listPresets } from "./presets/index.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henriquecosta/chaos-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Middleware pra simular falhas de producao (delay, erros, timeout, indisponibilidade, respostas malformadas/obsoletas) em APIs Express/Fastify/NestJS/Koa durante desenvolvimento.",
|
|
6
6
|
"type": "module",
|