@henriquecosta/chaos-api 1.0.2 → 1.0.4

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.
@@ -12,11 +12,11 @@ export interface ChaosOptions {
12
12
  activityLog?: ActivityLog;
13
13
  /** Max events kept by the activity feed (docs/PRD.md 6.5). Default 200. Ignored if `activityLog` is passed. */
14
14
  activityLogCapacity?: number;
15
- /** If set, starts the local control API on this port for the dashboard UI to talk to. */
15
+ /** If set (or `CHAOS_CONTROL_PORT` is set), starts the local control API on this port for the dashboard UI to talk to. */
16
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. */
17
+ /** Bind address for the control API. Default `CHAOS_CONTROL_HOST` env var, else `"127.0.0.1"`. */
18
18
  controlHost?: string;
19
- /** `Access-Control-Allow-Origin` for the control API. Default `"*"`. */
19
+ /** `Access-Control-Allow-Origin` for the control API. Default `CHAOS_CORS_ORIGIN` env var, else `"*"`. */
20
20
  corsOrigin?: string;
21
21
  /** Paths that always bypass chaos scenarios (glob strings like `"/health*"` or RegExp), e.g. health checks. */
22
22
  ignorePaths?: IgnorePathPattern[];
@@ -1,5 +1,7 @@
1
1
  import { ActivityLog } from "../core/activity-log.js";
2
+ import { resolveControlApiConfig } from "../core/control-api-env.js";
2
3
  import { isIgnoredPath } from "../core/ignore-paths.js";
4
+ import { warnOnPortCollision } from "../core/safe-listen.js";
3
5
  import { ScenarioEngine } from "../core/scenario-engine.js";
4
6
  import { StateStore } from "../core/state-store.js";
5
7
  import { createControlApi } from "../dashboard/server/control-api.js";
@@ -34,8 +36,10 @@ export function chaos(options = {}) {
34
36
  });
35
37
  middleware.store = store;
36
38
  middleware.activityLog = activityLog;
37
- if (options.controlPort) {
38
- middleware.controlApi = createControlApi(store, activityLog, { corsOrigin: options.corsOrigin }).listen(options.controlPort, options.controlHost ?? "127.0.0.1");
39
+ const controlApiConfig = resolveControlApiConfig(options);
40
+ if (controlApiConfig.port) {
41
+ const server = createControlApi(store, activityLog, { corsOrigin: controlApiConfig.corsOrigin }).listen(controlApiConfig.port, controlApiConfig.host);
42
+ middleware.controlApi = warnOnPortCollision(server, "control API", controlApiConfig.port, controlApiConfig.host);
39
43
  }
40
44
  return middleware;
41
45
  }
@@ -2,6 +2,7 @@ import type { FastifyInstance } from "fastify";
2
2
  import type { Server } from "node:http";
3
3
  import { ActivityLog } from "../core/activity-log.js";
4
4
  import { StateStore } from "../core/state-store.js";
5
+ import { type ControlApiOptions } from "../dashboard/server/control-api.js";
5
6
  import type { ChaosOptions } from "./express.js";
6
7
  export interface ChaosFastifyPlugin {
7
8
  (fastify: FastifyInstance): Promise<void>;
@@ -10,3 +11,12 @@ export interface ChaosFastifyPlugin {
10
11
  controlApi?: Server;
11
12
  }
12
13
  export declare function chaosFastifyPlugin(options?: ChaosOptions): ChaosFastifyPlugin;
14
+ export interface ChaosControlApiFastifyPlugin {
15
+ (fastify: FastifyInstance): Promise<void>;
16
+ }
17
+ /**
18
+ * Fastify-compatible plugin serving the same routes as `createControlApi`, mounted directly onto
19
+ * the host app's own Fastify instance — no extra port, no port-collision risk. Requests that
20
+ * aren't ours fall through to Fastify's normal routing untouched.
21
+ */
22
+ export declare function createControlApiFastifyPlugin(store: StateStore, activityLog?: ActivityLog, options?: ControlApiOptions): ChaosControlApiFastifyPlugin;
@@ -1,8 +1,10 @@
1
1
  import { ActivityLog } from "../core/activity-log.js";
2
+ import { resolveControlApiConfig } from "../core/control-api-env.js";
2
3
  import { isIgnoredPath } from "../core/ignore-paths.js";
4
+ import { warnOnPortCollision } from "../core/safe-listen.js";
3
5
  import { ScenarioEngine } from "../core/scenario-engine.js";
4
6
  import { StateStore } from "../core/state-store.js";
5
- import { createControlApi } from "../dashboard/server/control-api.js";
7
+ import { createControlApi, handleControlApiRequest, isControlApiRoute, } from "../dashboard/server/control-api.js";
6
8
  import { isBlockedByGuardrail } from "../guardrail.js";
7
9
  export function chaosFastifyPlugin(options = {}) {
8
10
  const store = options.store ?? new StateStore();
@@ -38,8 +40,28 @@ export function chaosFastifyPlugin(options = {}) {
38
40
  plugin[Symbol.for("skip-override")] = true;
39
41
  plugin.store = store;
40
42
  plugin.activityLog = activityLog;
41
- if (options.controlPort) {
42
- plugin.controlApi = createControlApi(store, activityLog, { corsOrigin: options.corsOrigin }).listen(options.controlPort, options.controlHost ?? "127.0.0.1");
43
+ const controlApiConfig = resolveControlApiConfig(options);
44
+ if (controlApiConfig.port) {
45
+ const server = createControlApi(store, activityLog, { corsOrigin: controlApiConfig.corsOrigin }).listen(controlApiConfig.port, controlApiConfig.host);
46
+ plugin.controlApi = warnOnPortCollision(server, "control API", controlApiConfig.port, controlApiConfig.host);
43
47
  }
44
48
  return plugin;
45
49
  }
50
+ /**
51
+ * Fastify-compatible plugin serving the same routes as `createControlApi`, mounted directly onto
52
+ * the host app's own Fastify instance — no extra port, no port-collision risk. Requests that
53
+ * aren't ours fall through to Fastify's normal routing untouched.
54
+ */
55
+ export function createControlApiFastifyPlugin(store, activityLog, options = {}) {
56
+ const corsOrigin = options.corsOrigin ?? "*";
57
+ const plugin = (async (fastify) => {
58
+ fastify.addHook("onRequest", async (request, reply) => {
59
+ if (!isControlApiRoute(request.url.split("?")[0]))
60
+ return;
61
+ reply.hijack();
62
+ await handleControlApiRequest(request.raw, reply.raw, store, activityLog, corsOrigin);
63
+ });
64
+ });
65
+ plugin[Symbol.for("skip-override")] = true;
66
+ return plugin;
67
+ }
@@ -2,6 +2,7 @@ import type { Middleware } from "koa";
2
2
  import type { Server } from "node:http";
3
3
  import { ActivityLog } from "../core/activity-log.js";
4
4
  import { StateStore } from "../core/state-store.js";
5
+ import { type ControlApiOptions } from "../dashboard/server/control-api.js";
5
6
  import type { ChaosOptions } from "./express.js";
6
7
  export interface ChaosKoaMiddleware extends Middleware {
7
8
  store: StateStore;
@@ -10,3 +11,9 @@ export interface ChaosKoaMiddleware extends Middleware {
10
11
  }
11
12
  /** docs/PRD.md 6.6 — thin adapter over the shared core, same pattern as adapters/express.ts and adapters/fastify.ts. */
12
13
  export declare function chaosKoaMiddleware(options?: ChaosOptions): ChaosKoaMiddleware;
14
+ /**
15
+ * Koa-compatible middleware serving the same routes as `createControlApi`, mounted directly onto
16
+ * the host app's own Koa instance — no extra port, no port-collision risk. Requests that aren't
17
+ * ours fall through to Koa's normal middleware chain untouched.
18
+ */
19
+ export declare function createControlApiKoaMiddleware(store: StateStore, activityLog?: ActivityLog, options?: ControlApiOptions): Middleware;
@@ -1,8 +1,10 @@
1
1
  import { ActivityLog } from "../core/activity-log.js";
2
+ import { resolveControlApiConfig } from "../core/control-api-env.js";
2
3
  import { isIgnoredPath } from "../core/ignore-paths.js";
4
+ import { warnOnPortCollision } from "../core/safe-listen.js";
3
5
  import { ScenarioEngine } from "../core/scenario-engine.js";
4
6
  import { StateStore } from "../core/state-store.js";
5
- import { createControlApi } from "../dashboard/server/control-api.js";
7
+ import { createControlApi, handleControlApiRequest, isControlApiRoute, } from "../dashboard/server/control-api.js";
6
8
  import { isBlockedByGuardrail } from "../guardrail.js";
7
9
  /** docs/PRD.md 6.6 — thin adapter over the shared core, same pattern as adapters/express.ts and adapters/fastify.ts. */
8
10
  export function chaosKoaMiddleware(options = {}) {
@@ -41,8 +43,28 @@ export function chaosKoaMiddleware(options = {}) {
41
43
  });
42
44
  middleware.store = store;
43
45
  middleware.activityLog = activityLog;
44
- if (options.controlPort) {
45
- middleware.controlApi = createControlApi(store, activityLog, { corsOrigin: options.corsOrigin }).listen(options.controlPort, options.controlHost ?? "127.0.0.1");
46
+ const controlApiConfig = resolveControlApiConfig(options);
47
+ if (controlApiConfig.port) {
48
+ const server = createControlApi(store, activityLog, { corsOrigin: controlApiConfig.corsOrigin }).listen(controlApiConfig.port, controlApiConfig.host);
49
+ middleware.controlApi = warnOnPortCollision(server, "control API", controlApiConfig.port, controlApiConfig.host);
46
50
  }
47
51
  return middleware;
48
52
  }
53
+ /**
54
+ * Koa-compatible middleware serving the same routes as `createControlApi`, mounted directly onto
55
+ * the host app's own Koa instance — no extra port, no port-collision risk. Requests that aren't
56
+ * ours fall through to Koa's normal middleware chain untouched.
57
+ */
58
+ export function createControlApiKoaMiddleware(store, activityLog, options = {}) {
59
+ const corsOrigin = options.corsOrigin ?? "*";
60
+ return async (ctx, next) => {
61
+ if (!isControlApiRoute(ctx.path)) {
62
+ await next();
63
+ return;
64
+ }
65
+ // We answer with the raw req/res directly (same as the standalone control API server) —
66
+ // take Koa out of the response-committing flow so it doesn't also try to write a reply.
67
+ ctx.respond = false;
68
+ await handleControlApiRequest(ctx.req, ctx.res, store, activityLog, corsOrigin);
69
+ };
70
+ }
@@ -1,5 +1,7 @@
1
1
  import { ActivityLog } from "../core/activity-log.js";
2
+ import { resolveControlApiConfig } from "../core/control-api-env.js";
2
3
  import { isIgnoredPath } from "../core/ignore-paths.js";
4
+ import { warnOnPortCollision } from "../core/safe-listen.js";
3
5
  import { ScenarioEngine } from "../core/scenario-engine.js";
4
6
  import { StateStore } from "../core/state-store.js";
5
7
  import { createControlApi } from "../dashboard/server/control-api.js";
@@ -46,8 +48,10 @@ export function createChaosNestMiddleware(options = {}) {
46
48
  });
47
49
  middleware.store = store;
48
50
  middleware.activityLog = activityLog;
49
- if (options.controlPort) {
50
- middleware.controlApi = createControlApi(store, activityLog, { corsOrigin: options.corsOrigin }).listen(options.controlPort, options.controlHost ?? "127.0.0.1");
51
+ const controlApiConfig = resolveControlApiConfig(options);
52
+ if (controlApiConfig.port) {
53
+ const server = createControlApi(store, activityLog, { corsOrigin: controlApiConfig.corsOrigin }).listen(controlApiConfig.port, controlApiConfig.host);
54
+ middleware.controlApi = warnOnPortCollision(server, "control API", controlApiConfig.port, controlApiConfig.host);
51
55
  }
52
56
  return middleware;
53
57
  }
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { ActivityLog } from "../core/activity-log.js";
3
+ import { resolveControlApiConfig } from "../core/control-api-env.js";
4
+ import { warnOnPortCollision } from "../core/safe-listen.js";
3
5
  import { StateStore } from "../core/state-store.js";
4
6
  import { createControlApi } from "../dashboard/server/control-api.js";
5
7
  import { startDashboard } from "../dashboard/server/index.js";
@@ -20,15 +22,20 @@ if (command === "dashboard") {
20
22
  const controlPortFlag = flagValue("--control-port");
21
23
  const controlHostFlag = flagValue("--control-host");
22
24
  const corsOriginFlag = flagValue("--cors-origin");
23
- const controlPort = controlPortFlag ? Number(controlPortFlag) : DEFAULT_CONTROL_PORT;
24
- const controlHost = controlHostFlag ?? "127.0.0.1";
25
+ const config = resolveControlApiConfig({
26
+ controlPort: controlPortFlag ? Number(controlPortFlag) : undefined,
27
+ controlHost: controlHostFlag,
28
+ corsOrigin: corsOriginFlag,
29
+ });
30
+ const controlPort = config.port ?? DEFAULT_CONTROL_PORT;
25
31
  const store = new StateStore();
26
32
  const activityLog = new ActivityLog();
27
- createControlApi(store, activityLog, { corsOrigin: corsOriginFlag }).listen(controlPort, controlHost, () => {
28
- console.log(`[chaos-api] standalone control API running at http://${controlHost}:${controlPort} ` +
33
+ const server = createControlApi(store, activityLog, { corsOrigin: config.corsOrigin }).listen(controlPort, config.host, () => {
34
+ console.log(`[chaos-api] standalone control API running at http://${config.host}:${controlPort} ` +
29
35
  "(demo StateStore, not wired to a real app — pass --no-control-api if your app " +
30
36
  "already runs chaos({ controlPort }) itself)");
31
37
  });
38
+ warnOnPortCollision(server, "standalone control API", controlPort, config.host);
32
39
  }
33
40
  }
34
41
  else {
@@ -0,0 +1,17 @@
1
+ export interface ControlApiEnvOptions {
2
+ controlPort?: number;
3
+ controlHost?: string;
4
+ corsOrigin?: string;
5
+ }
6
+ export interface ResolvedControlApiConfig {
7
+ port?: number;
8
+ host: string;
9
+ corsOrigin: string;
10
+ }
11
+ /**
12
+ * Resolves control-API bind config: explicit `chaos()` options win, then `CHAOS_CONTROL_PORT` /
13
+ * `CHAOS_CONTROL_HOST` / `CHAOS_CORS_ORIGIN` env vars, then hardcoded defaults. Lets ops move the
14
+ * control API to a different port per environment (e.g. to dodge the `chaos-api dashboard`
15
+ * CLI's demo control API, which defaults to the same 51820) without touching code.
16
+ */
17
+ export declare function resolveControlApiConfig(options: ControlApiEnvOptions): ResolvedControlApiConfig;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resolves control-API bind config: explicit `chaos()` options win, then `CHAOS_CONTROL_PORT` /
3
+ * `CHAOS_CONTROL_HOST` / `CHAOS_CORS_ORIGIN` env vars, then hardcoded defaults. Lets ops move the
4
+ * control API to a different port per environment (e.g. to dodge the `chaos-api dashboard`
5
+ * CLI's demo control API, which defaults to the same 51820) without touching code.
6
+ */
7
+ export function resolveControlApiConfig(options) {
8
+ const envPort = process.env.CHAOS_CONTROL_PORT;
9
+ return {
10
+ port: options.controlPort ?? (envPort ? Number(envPort) : undefined),
11
+ host: options.controlHost ?? process.env.CHAOS_CONTROL_HOST ?? "127.0.0.1",
12
+ corsOrigin: options.corsOrigin ?? process.env.CHAOS_CORS_ORIGIN ?? "*",
13
+ };
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { Server } from "node:http";
2
+ /**
3
+ * Attaches an `error` handler that turns EADDRINUSE into a clear, actionable log line instead of
4
+ * an unhandled-exception crash of the whole process. Two chaos-api control APIs easily end up on
5
+ * the same port (e.g. `chaos-api dashboard`'s demo control API defaults to 51820, same as the
6
+ * README's `chaos({ controlPort: 51820 })` example) — without this, whichever `.listen()` call
7
+ * loses the race silently kills its entire process, and the dashboard ends up talking to
8
+ * whichever StateStore *did* win the bind, with no indication anything went wrong.
9
+ */
10
+ export declare function warnOnPortCollision(server: Server, label: string, port: number, host: string): Server;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Attaches an `error` handler that turns EADDRINUSE into a clear, actionable log line instead of
3
+ * an unhandled-exception crash of the whole process. Two chaos-api control APIs easily end up on
4
+ * the same port (e.g. `chaos-api dashboard`'s demo control API defaults to 51820, same as the
5
+ * README's `chaos({ controlPort: 51820 })` example) — without this, whichever `.listen()` call
6
+ * loses the race silently kills its entire process, and the dashboard ends up talking to
7
+ * whichever StateStore *did* win the bind, with no indication anything went wrong.
8
+ */
9
+ export function warnOnPortCollision(server, label, port, host) {
10
+ server.on("error", (err) => {
11
+ if (err.code !== "EADDRINUSE")
12
+ throw err;
13
+ console.error(`[chaos-api] ${label} could not bind ${host}:${port} — address already in use. ` +
14
+ "Another chaos-api process is likely already bound there (e.g. `chaos-api dashboard` " +
15
+ "without --no-control-api running next to an app that already runs chaos({ controlPort }))." +
16
+ " Pick a different port (CHAOS_CONTROL_PORT env var or the controlPort/--control-port option).");
17
+ });
18
+ return server;
19
+ }
@@ -1,13 +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
4
  export interface ControlApiOptions {
5
5
  /** `Access-Control-Allow-Origin` value. Default `"*"` (dashboard-ui may run on any origin/port). */
6
6
  corsOrigin?: string;
7
7
  }
8
+ export type ControlApiNext = (err?: unknown) => void;
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
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>;
@@ -4,18 +4,62 @@ const CORS_HEADERS = {
4
4
  "Access-Control-Allow-Methods": "GET,POST,PATCH,DELETE,OPTIONS",
5
5
  "Access-Control-Allow-Headers": "Content-Type",
6
6
  };
7
+ /** Resource names this control API owns — anything else isn't ours (see `isControlApiRoute` below). */
8
+ const KNOWN_RESOURCES = ["scenarios", "activity", "presets", "config"];
7
9
  /**
8
10
  * Local control API for the scenario StateStore living inside the user's app process.
9
11
  * dashboard-ui talks to this directly (CORS-open, localhost-only by convention) — the
10
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.
11
17
  */
12
18
  export function createControlApi(store, activityLog, options = {}) {
13
19
  const corsOrigin = options.corsOrigin ?? "*";
14
20
  return createServer((req, res) => {
15
- void handleRequest(req, res, store, activityLog, corsOrigin);
21
+ void handleControlApiRequest(req, res, store, activityLog, corsOrigin);
16
22
  });
17
23
  }
18
- async function handleRequest(req, res, store, activityLog, corsOrigin) {
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
+ }
19
63
  res.setHeader("Access-Control-Allow-Origin", corsOrigin);
20
64
  for (const [key, value] of Object.entries(CORS_HEADERS))
21
65
  res.setHeader(key, value);
@@ -23,13 +67,11 @@ async function handleRequest(req, res, store, activityLog, corsOrigin) {
23
67
  res.writeHead(204).end();
24
68
  return;
25
69
  }
26
- const url = new URL(req.url ?? "/", "http://localhost");
27
70
  const allSegments = url.pathname.split("/").filter(Boolean);
28
- // Anchor on the *last* "api" segment, not position 0 — a host may mount this
29
- // control API (or proxy requests to it) underneath its own base path (e.g.
30
- // `/api/v1/notebooks/api/activity`). Anchoring on position 0 breaks in that
31
- // case; anchoring on the last "api" keeps the package route-independent of
32
- // wherever the host puts it.
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.
33
75
  const apiIndex = allSegments.lastIndexOf("api");
34
76
  try {
35
77
  if (apiIndex === -1) {
@@ -2,6 +2,7 @@ import { createServer } from "node:http";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { warnOnPortCollision } from "../../core/safe-listen.js";
5
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
7
  const UI_DIR = path.join(__dirname, "..", "ui");
7
8
  const MIME = {
@@ -23,7 +24,7 @@ export function startDashboard(options = {}) {
23
24
  server.listen(port, host, () => {
24
25
  console.log(`[chaos-api] dashboard running at http://${host}:${port}/dashboard`);
25
26
  });
26
- return server;
27
+ return warnOnPortCollision(server, "dashboard server", port, host);
27
28
  }
28
29
  async function serveStatic(url, res) {
29
30
  let pathname = url.split("?")[0];
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export { chaos } from "./adapters/express.js";
2
2
  export type { ChaosOptions, ChaosInstance } from "./adapters/express.js";
3
- export { chaosFastifyPlugin } from "./adapters/fastify.js";
4
- export type { ChaosFastifyPlugin } from "./adapters/fastify.js";
3
+ export { chaosFastifyPlugin, createControlApiFastifyPlugin } from "./adapters/fastify.js";
4
+ export type { ChaosFastifyPlugin, ChaosControlApiFastifyPlugin } from "./adapters/fastify.js";
5
5
  export { createChaosNestMiddleware } from "./adapters/nestjs.js";
6
6
  export type { ChaosNestMiddleware, NestLikeRequest, NestLikeResponse, NestMiddlewareFn } from "./adapters/nestjs.js";
7
- export { chaosKoaMiddleware } from "./adapters/koa.js";
7
+ export { chaosKoaMiddleware, createControlApiKoaMiddleware } from "./adapters/koa.js";
8
8
  export type { ChaosKoaMiddleware } from "./adapters/koa.js";
9
9
  export { StateStore, globToRegex } from "./core/state-store.js";
10
10
  export type { RegisterScenarioInput, UpdateScenarioInput } from "./core/state-store.js";
@@ -19,8 +19,10 @@ export { PRESET_CATALOG, applyPreset, findPreset, listPresets } from "./presets/
19
19
  export type { ApplyPresetOverrides, PresetCategory, PresetDefinition } from "./presets/index.js";
20
20
  export { createChaosFetch } from "./outbound/index.js";
21
21
  export type { ChaosFetchOptions } from "./outbound/index.js";
22
- export { createControlApi } from "./dashboard/server/control-api.js";
23
- export type { ControlApiOptions } from "./dashboard/server/control-api.js";
22
+ export { createControlApi, createControlApiMiddleware, handleControlApiRequest, isControlApiRoute, } from "./dashboard/server/control-api.js";
23
+ export type { ControlApiOptions, ControlApiMiddleware, ControlApiNext } from "./dashboard/server/control-api.js";
24
24
  export { startDashboard } from "./dashboard/server/index.js";
25
25
  export type { StartDashboardOptions } from "./dashboard/server/index.js";
26
26
  export { isBlockedByGuardrail, resetGuardrailWarning } from "./guardrail.js";
27
+ export { resolveControlApiConfig } from "./core/control-api-env.js";
28
+ export type { ControlApiEnvOptions, ResolvedControlApiConfig } from "./core/control-api-env.js";
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { chaos } from "./adapters/express.js";
2
- export { chaosFastifyPlugin } from "./adapters/fastify.js";
2
+ export { chaosFastifyPlugin, createControlApiFastifyPlugin } from "./adapters/fastify.js";
3
3
  export { createChaosNestMiddleware } from "./adapters/nestjs.js";
4
- export { chaosKoaMiddleware } from "./adapters/koa.js";
4
+ export { chaosKoaMiddleware, createControlApiKoaMiddleware } from "./adapters/koa.js";
5
5
  export { StateStore, globToRegex } from "./core/state-store.js";
6
6
  export { ActivityLog } from "./core/activity-log.js";
7
7
  export { isIgnoredPath } from "./core/ignore-paths.js";
@@ -9,6 +9,7 @@ export { ScenarioEngine } from "./core/scenario-engine.js";
9
9
  export * from "./scenarios/index.js";
10
10
  export { PRESET_CATALOG, applyPreset, findPreset, listPresets } from "./presets/index.js";
11
11
  export { createChaosFetch } from "./outbound/index.js";
12
- export { createControlApi } from "./dashboard/server/control-api.js";
12
+ export { createControlApi, createControlApiMiddleware, handleControlApiRequest, isControlApiRoute, } from "./dashboard/server/control-api.js";
13
13
  export { startDashboard } from "./dashboard/server/index.js";
14
14
  export { isBlockedByGuardrail, resetGuardrailWarning } from "./guardrail.js";
15
+ export { resolveControlApiConfig } from "./core/control-api-env.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henriquecosta/chaos-api",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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",
@@ -26,7 +26,8 @@
26
26
  "dev": "tsx watch src/bin/chaos-api.ts dashboard",
27
27
  "test": "vitest run",
28
28
  "test:watch": "vitest",
29
- "typecheck": "tsc --noEmit"
29
+ "typecheck": "tsc --noEmit",
30
+ "check": "npx qlscanner scan"
30
31
  },
31
32
  "peerDependencies": {
32
33
  "express": ">=4",