@chaos-maker/core 0.4.0 → 0.5.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.
@@ -1,2 +1,63 @@
1
- import { ChaosConfig } from './config';
2
- export declare const presets: Readonly<Record<string, ChaosConfig>>;
1
+ import type { ChaosConfig } from './config';
2
+ /** ChaosConfig slice a preset is allowed to carry. Auto-includes any new
3
+ * rule category added to ChaosConfig — the `Omit` is bounded to fields that
4
+ * are explicitly forbidden inside a preset (`presets`, `customPresets`,
5
+ * `seed`, `debug`). */
6
+ export type PresetConfigSlice = Omit<ChaosConfig, 'presets' | 'customPresets' | 'seed' | 'debug' | 'schemaVersion'>;
7
+ /** A named preset packaged for registry registration. */
8
+ export interface Preset {
9
+ readonly name: string;
10
+ readonly config: PresetConfigSlice;
11
+ }
12
+ /** All built-in presets including kebab aliases.
13
+ * Aliases are EXTRA registry entries pointing at the SAME config object
14
+ * identity as the camelCase entry — so
15
+ * `registry.get('slow-api') === presets.slowNetwork`.
16
+ *
17
+ * Both the array AND each `{ name, config }` descriptor are frozen so that
18
+ * `BUILT_IN_PRESETS[0].name = 'x'` or `BUILT_IN_PRESETS[0].config = {}` cannot
19
+ * poison future `PresetRegistry` constructions. Configs are already deep-
20
+ * frozen above, so the descriptor freeze is the missing layer. */
21
+ export declare const BUILT_IN_PRESETS: ReadonlyArray<Preset>;
22
+ /** Per-instance registry of presets. Constructor seeds the built-ins
23
+ * by default; pass an empty iterable to start from scratch. The slice shape
24
+ * is type-enforced for built-ins and Zod-validated for `customPresets`, so
25
+ * `register` does not re-check structure. */
26
+ export declare class PresetRegistry {
27
+ private map;
28
+ constructor(initial?: Iterable<Preset>);
29
+ register(preset: Preset): void;
30
+ registerAll(entries: Record<string, PresetConfigSlice> | undefined): void;
31
+ has(name: string): boolean;
32
+ get(name: string): PresetConfigSlice;
33
+ list(): string[];
34
+ }
35
+ /** Expand `config.presets` against `registry`. Identity contract:
36
+ *
37
+ * - ALWAYS returns a fresh `ChaosConfig`. Callers own the returned object
38
+ * and may mutate it without affecting the input. Built-in slices stay
39
+ * deep-frozen because each preset is deep-cloned at append time.
40
+ * - The output ALWAYS has `presets` and `customPresets` stripped, even if
41
+ * `presets[]` was empty. Prevents stale `customPresets` from leaking into
42
+ * the post-expansion config.
43
+ * - Append order: preset rules first (in the order they appear in
44
+ * `presets[]`), user rules last. Same rule for `groups`.
45
+ * - Throws when a name in `presets[]` is not registered. Plain `Error` —
46
+ * `prepareChaosConfig` wraps to `ChaosConfigError`.
47
+ *
48
+ * Defensive deduplication on `presets[]` runs here as well as in the Zod
49
+ * transform, because `expandPresets` is exported and a contributor could
50
+ * call it directly on an un-validated config. */
51
+ export declare function expandPresets(config: ChaosConfig, registry: PresetRegistry): ChaosConfig;
52
+ /** Backward-compat: the v0.4.0 frozen-record export. **CamelCase keys ONLY.**
53
+ * kebab aliases (`slow-api`, `flaky-api`, `offline-mode`,
54
+ * `high-latency`) live exclusively on `PresetRegistry` — they are NOT keys
55
+ * on this record. By design:
56
+ *
57
+ * presets['slow-api'] === undefined
58
+ * presets.slowNetwork === new PresetRegistry().get('slow-api') // same identity
59
+ * presets.slowNetwork === new PresetRegistry().get('slowNetwork')
60
+ *
61
+ * Use the camelCase key when reading from this record; use the registry (or
62
+ * the declarative `presets: ['slow-api']` config field) for kebab lookups. */
63
+ export declare const presets: Readonly<Record<string, PresetConfigSlice>>;
@@ -9,10 +9,12 @@
9
9
  * Exposes `window.__chaosMakerSWBridge`:
10
10
  * - `apply(cfg, timeoutMs)` — post config over MessageChannel, wait for ack.
11
11
  * - `stop(timeoutMs)` — stop chaos in the SW.
12
+ * - `toggleGroup(name, enabled, timeoutMs)` — flip a rule group inside the SW
13
+ * via `__chaosMakerToggleGroup`; resolves on ack with no engine restart.
12
14
  * - `getLocalLog()` / `clearLocalLog()` — page-side buffered event log.
13
15
  * - `getRemoteLog(timeoutMs)` — fetch SW's in-memory log.
14
16
  *
15
17
  * The bridge auto-wires a `controllerchange` listener so SW updates inherit
16
18
  * the most recent config. Install is idempotent.
17
19
  */
18
- export declare const SW_BRIDGE_SOURCE = "\n(function installChaosMakerSWBridge() {\n if (typeof window === 'undefined') return;\n if (window.__chaosMakerSWBridgeInstalled) return;\n window.__chaosMakerSWBridgeInstalled = true;\n\n var log = [];\n var lastConfig = null;\n\n function addSwListeners() {\n if (!('serviceWorker' in navigator)) return;\n navigator.serviceWorker.addEventListener('message', function (evt) {\n var data = evt && evt.data;\n if (data && data.__chaosMakerSWEvent && data.event) {\n log.push(data.event);\n }\n });\n navigator.serviceWorker.addEventListener('controllerchange', function () {\n if (lastConfig) {\n postConfig(lastConfig, 5000).catch(function (err) {\n try { console.warn('[chaos-maker] re-apply after controllerchange failed', err); } catch (_) {}\n });\n }\n });\n }\n\n function waitForController(timeoutMs) {\n return new Promise(function (resolve, reject) {\n if (!('serviceWorker' in navigator)) {\n reject(new Error('[chaos-maker] service worker API not available on this page'));\n return;\n }\n var start = Date.now();\n (function poll() {\n if (navigator.serviceWorker.controller) {\n resolve(navigator.serviceWorker.controller);\n return;\n }\n if (Date.now() - start >= timeoutMs) {\n reject(new Error('[chaos-maker] no SW controller after ' + timeoutMs + 'ms \u2014 did you register the SW?'));\n return;\n }\n setTimeout(poll, 50);\n })();\n });\n }\n\n function postViaPort(controller, message, timeoutMs) {\n return new Promise(function (resolve, reject) {\n var channel = new MessageChannel();\n var settled = false;\n var timer = setTimeout(function () {\n if (settled) return;\n settled = true;\n try { channel.port1.close(); } catch (_) {}\n reject(new Error('[chaos-maker] SW ack timeout after ' + timeoutMs + 'ms'));\n }, timeoutMs);\n channel.port1.onmessage = function (evt) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(evt && evt.data);\n };\n controller.postMessage(message, [channel.port2]);\n });\n }\n\n function postConfig(cfg, timeoutMs) {\n return waitForController(timeoutMs).then(function (ctrl) {\n return postViaPort(ctrl, { __chaosMakerConfig: cfg }, timeoutMs);\n });\n }\n\n window.__chaosMakerSWBridge = {\n apply: function (cfg, timeoutMs) {\n // Stash cfg BEFORE awaiting ack \u2014 intentional. If the first ack times\n // out (e.g. SW still installing), the controllerchange listener above\n // will retry with this config once the new SW claims the page. Caller\n // still sees the rejection from this attempt and can re-throw.\n lastConfig = cfg;\n return postConfig(cfg, timeoutMs).then(function (ack) {\n return { seed: ack && typeof ack.seed === 'number' ? ack.seed : null };\n });\n },\n stop: function (timeoutMs) {\n lastConfig = null;\n if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) {\n return Promise.resolve({ running: false });\n }\n return postViaPort(navigator.serviceWorker.controller, { __chaosMakerStop: true }, timeoutMs);\n },\n getLocalLog: function () { return log.slice(); },\n clearLocalLog: function () { log.length = 0; },\n getRemoteLog: function (timeoutMs) {\n if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) {\n return Promise.resolve([]);\n }\n return postViaPort(navigator.serviceWorker.controller, { __chaosMakerGetLog: true }, timeoutMs)\n .then(function (reply) {\n return (reply && Array.isArray(reply.log)) ? reply.log : [];\n });\n },\n };\n\n addSwListeners();\n})();\n";
20
+ export declare const SW_BRIDGE_SOURCE = "\n(function installChaosMakerSWBridge() {\n if (typeof window === 'undefined') return;\n if (window.__chaosMakerSWBridgeInstalled) return;\n window.__chaosMakerSWBridgeInstalled = true;\n\n var log = [];\n var lastConfig = null;\n\n function addSwListeners() {\n if (!('serviceWorker' in navigator)) return;\n navigator.serviceWorker.addEventListener('message', function (evt) {\n var data = evt && evt.data;\n if (data && data.__chaosMakerSWEvent && data.event) {\n log.push(data.event);\n }\n });\n navigator.serviceWorker.addEventListener('controllerchange', function () {\n if (lastConfig) {\n postConfig(lastConfig, 5000).catch(function (err) {\n try { console.warn('[chaos-maker] re-apply after controllerchange failed', err); } catch (_) {}\n });\n }\n });\n }\n\n function waitForController(timeoutMs) {\n return new Promise(function (resolve, reject) {\n if (!('serviceWorker' in navigator)) {\n reject(new Error('[chaos-maker] service worker API not available on this page'));\n return;\n }\n var start = Date.now();\n (function poll() {\n if (navigator.serviceWorker.controller) {\n resolve(navigator.serviceWorker.controller);\n return;\n }\n if (Date.now() - start >= timeoutMs) {\n reject(new Error('[chaos-maker] no SW controller after ' + timeoutMs + 'ms \u2014 did you register the SW?'));\n return;\n }\n setTimeout(poll, 50);\n })();\n });\n }\n\n function postViaPort(controller, message, timeoutMs) {\n return new Promise(function (resolve, reject) {\n var channel = new MessageChannel();\n var settled = false;\n var timer = setTimeout(function () {\n if (settled) return;\n settled = true;\n try { channel.port1.close(); } catch (_) {}\n reject(new Error('[chaos-maker] SW ack timeout after ' + timeoutMs + 'ms'));\n }, timeoutMs);\n channel.port1.onmessage = function (evt) {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n resolve(evt && evt.data);\n };\n controller.postMessage(message, [channel.port2]);\n });\n }\n\n function postConfig(cfg, timeoutMs) {\n return waitForController(timeoutMs).then(function (ctrl) {\n return postViaPort(ctrl, { __chaosMakerConfig: cfg }, timeoutMs);\n });\n }\n\n window.__chaosMakerSWBridge = {\n apply: function (cfg, timeoutMs) {\n // Defensive: page-side adapters validate before calling apply. The\n // page-side bundle deliberately excludes Zod, so a null/primitive/\n // array/wildly-wrong payload throws here instead of silently posting\n // garbage to the SW. `typeof [] === 'object'` so reject arrays too.\n if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {\n return Promise.reject(new Error('[chaos-maker] bridge.apply: config must be an object'));\n }\n // Stash cfg BEFORE awaiting ack \u2014 intentional. If the first ack times\n // out (e.g. SW still installing), the controllerchange listener above\n // will retry with this config once the new SW claims the page. Caller\n // still sees the rejection from this attempt and can re-throw.\n lastConfig = cfg;\n return postConfig(cfg, timeoutMs).then(function (ack) {\n return { seed: ack && typeof ack.seed === 'number' ? ack.seed : null };\n });\n },\n stop: function (timeoutMs) {\n lastConfig = null;\n if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) {\n return Promise.resolve({ running: false });\n }\n return postViaPort(navigator.serviceWorker.controller, { __chaosMakerStop: true }, timeoutMs);\n },\n toggleGroup: function (name, enabled, timeoutMs) {\n if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) {\n return Promise.reject(new Error('[chaos-maker] no SW controller \u2014 call injectSWChaos before toggleGroup'));\n }\n var t = (typeof timeoutMs === 'number' && timeoutMs > 0) ? timeoutMs : 2000;\n return postViaPort(\n navigator.serviceWorker.controller,\n { __chaosMakerToggleGroup: { name: String(name), enabled: !!enabled } },\n t,\n );\n },\n getLocalLog: function () { return log.slice(); },\n clearLocalLog: function () { log.length = 0; },\n getRemoteLog: function (timeoutMs) {\n if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) {\n return Promise.resolve([]);\n }\n return postViaPort(navigator.serviceWorker.controller, { __chaosMakerGetLog: true }, timeoutMs)\n .then(function (reply) {\n return (reply && Array.isArray(reply.log)) ? reply.log : [];\n });\n },\n };\n\n addSwListeners();\n})();\n";
@@ -32,6 +32,13 @@ export interface ChaosSWGetLogMessage {
32
32
  export interface ChaosSWClearLogMessage {
33
33
  __chaosMakerClearLog: true;
34
34
  }
35
+ /** Message sent by page -> SW to toggle a rule group without restarting chaos. */
36
+ export interface ChaosSWToggleGroupMessage {
37
+ __chaosMakerToggleGroup: {
38
+ name: string;
39
+ enabled: boolean;
40
+ };
41
+ }
35
42
  /** Ack sent SW → port (or broadcast) after a config / stop message lands. */
36
43
  export interface ChaosSWAck {
37
44
  __chaosMakerAck: true;
@@ -1,4 +1,11 @@
1
- import type { CorruptionStrategy, RequestCountingOptions } from './config';
1
+ import type { ChaosConfig, CorruptionStrategy, RequestCountingOptions } from './config';
2
+ import { RuleGroupRegistry } from './groups';
3
+ import type { ChaosEvent, ChaosEventEmitter } from './events';
4
+ /** Recursive deep clone that preserves `RegExp` instances literally.
5
+ * `JSON.parse(JSON.stringify(...))` would silently drop `RegExp`, breaking
6
+ * matchers like `graphqlOperation: /^Get/`. Shared by builder + presets so a
7
+ * single implementation owns RegExp survival. */
8
+ export declare function cloneValue<T>(value: T): T;
2
9
  export declare function shouldApplyChaos(probability: number, random: () => number): boolean;
3
10
  /**
4
11
  * Increment the per-rule request counter and return the new count.
@@ -14,3 +21,41 @@ export declare function incrementCounter(rule: object, counters: Map<object, num
14
21
  export declare function checkCountingCondition(rule: RequestCountingOptions, count: number): boolean;
15
22
  export declare function matchUrl(url: string, pattern: string): boolean;
16
23
  export declare function corruptText(text: string, strategy: CorruptionStrategy): string;
24
+ /**
25
+ * Group-active gate. Sits between `gateRule` and `shouldApplyChaos`
26
+ * inside every interceptor.
27
+ *
28
+ * - `registry === undefined` (legacy / direct interceptor caller): returns
29
+ * `true` so the interceptor proceeds without group checks.
30
+ * - When the rule's group is enabled: returns `true`.
31
+ * - When disabled: emits at most one `rule-group:gated` event per group per
32
+ * toggle cycle (deduped via `RuleGroupRegistry.shouldEmitGated`) and
33
+ * returns `false`. The emitted detail merges `baseDetail` (e.g. url +
34
+ * method) with `groupName`.
35
+ *
36
+ * Counting (`onNth` / `everyNth` / `afterN`) runs *before* this gate inside
37
+ * `gateRule`, so toggling a group does not desync per-rule counters.
38
+ */
39
+ export declare function gateGroup(rule: {
40
+ group?: string;
41
+ }, registry: RuleGroupRegistry | undefined, emitter: ChaosEventEmitter | undefined, baseDetail: ChaosEvent['detail']): boolean;
42
+ /** Minimal rule shape walked by `forEachRule`. Carries the optional `group`
43
+ * field every rule type now supports plus an open index for the
44
+ * remaining per-rule fields the walker doesn't care about. */
45
+ export type AnyRule = {
46
+ group?: string;
47
+ [k: string]: unknown;
48
+ };
49
+ /**
50
+ * Iterate every rule across every chaos category in the config exactly once.
51
+ *
52
+ * IMPORTANT: any new rule array added to `ChaosConfig` (e.g. `websocket.timeouts`,
53
+ * `sse.reconnects`) MUST be registered here. The matching test in
54
+ * `forEachRule.test.ts` asserts the visited count against a sample config; that
55
+ * test will fail loudly if a new array is added without updating this walker.
56
+ *
57
+ * Single source of truth for rule iteration. Used by `seedGroupsFromRules`
58
+ * and `collectReferencedGroups` in `ChaosMaker`, plus any future feature
59
+ * that needs to walk all rules.
60
+ */
61
+ export declare function forEachRule(config: ChaosConfig, fn: (rule: AnyRule) => void): void;
@@ -0,0 +1,7 @@
1
+ import type { ChaosConfig } from './config';
2
+ import type { DeprecationEntry, ValidationIssue } from './validation-types';
3
+ /** Empty for v0.5.0 — rails only. First real deprecation lands in v0.5.x by
4
+ * adding an entry here keyed by a dot-notation path the walker can detect.
5
+ * Top-level paths only for now (e.g. `'someTopLevelField'`). */
6
+ export declare const DEPRECATED_FIELDS: Map<string, DeprecationEntry>;
7
+ export declare function checkDeprecations(config: ChaosConfig, onDeprecation?: (issue: ValidationIssue) => void): void;
@@ -0,0 +1,10 @@
1
+ import type { ZodIssue } from 'zod';
2
+ import type { ValidationIssue } from './validation-types';
3
+ export declare function formatZodIssue(issue: ZodIssue): ValidationIssue;
4
+ export interface RenderOptions {
5
+ maxIssues?: number;
6
+ }
7
+ /** Deterministic sort: lex on path, then lex on code. Pure (returns a new
8
+ * array). Modern `Array#sort` is stable so equal pairs preserve input order. */
9
+ export declare function sortIssues(issues: ValidationIssue[]): ValidationIssue[];
10
+ export declare function renderValidationIssues(issues: ValidationIssue[], opts?: RenderOptions): string;
@@ -0,0 +1,11 @@
1
+ /** Known-key projection for `unknownFields: 'warn' | 'ignore'`. The walker is
2
+ * a small recursive helper, NOT a third Zod schema. Returns a fresh object
3
+ * detached from the passthrough parse result so subsequent stages cannot leak
4
+ * unknown fields back through aliasing. */
5
+ import type { ChaosConfig } from './config';
6
+ /** Project `input` to a fresh object containing only the keys recognized by
7
+ * the strict schema. Never mutates the input. */
8
+ export declare function stripUnknownKeys(input: unknown): ChaosConfig;
9
+ /** Collect dot-notation paths of unknown keys. Deterministic sorted output.
10
+ * Does not mutate the input. */
11
+ export declare function collectUnknownPaths(input: unknown): string[];
@@ -0,0 +1,34 @@
1
+ /** Public types for the structured validation surface.
2
+ * Pure types — no runtime. Imported by `errors.ts`, `validation.ts`, and the
3
+ * format / strip / deprecation helpers. */
4
+ export type RuleType = 'network.failure' | 'network.latency' | 'network.abort' | 'network.corruption' | 'network.cors' | 'ui.assault' | 'websocket.drop' | 'websocket.delay' | 'websocket.corrupt' | 'websocket.close' | 'sse.drop' | 'sse.delay' | 'sse.corrupt' | 'sse.close' | 'group' | 'preset' | 'top-level';
5
+ export type ValidationIssueCode = 'unknown_field' | 'missing_field' | 'invalid_type' | 'value_too_small' | 'value_too_large' | 'invalid_enum' | 'invalid_string' | 'invalid_regex' | 'mutually_exclusive' | 'duplicate' | 'unknown_preset' | 'preset_chain' | 'preset_collision' | 'unknown_schema_version' | 'deprecated' | 'custom' | 'legacy';
6
+ export interface ValidationIssue {
7
+ /** Dot-notation path: 'network.failures[0].statusCode'. Empty string for top-level. */
8
+ path: string;
9
+ code: ValidationIssueCode;
10
+ ruleType: RuleType;
11
+ message: string;
12
+ /** JSON-stringifiable. e.g. 'number 0..1', "'truncate'|'malformed-json'|...". */
13
+ expected?: string;
14
+ /** Received value via JSON.stringify (clipped to 80 chars). */
15
+ received?: string;
16
+ }
17
+ /** Custom validator hook. Rule passed by reference as `unknown`; callers
18
+ * narrow via type guards. Concrete `Readonly<NetworkFailureConfig>`-style
19
+ * typings per rule type are deferred post-v0.5.0.
20
+ *
21
+ * Mutation of the rule arg is undefined behavior. The engine deep-clones
22
+ * the canonical config at expansion time, so mutations made here may be
23
+ * observable on the validator's input but not by the running engine. */
24
+ export type CustomRuleValidator = (rule: unknown, ctx: Readonly<{
25
+ ruleType: RuleType;
26
+ path: string;
27
+ }>) => ValidationIssue[] | void;
28
+ export type CustomValidatorMap = Readonly<Partial<Record<RuleType, CustomRuleValidator>>>;
29
+ export interface DeprecationEntry {
30
+ since: string;
31
+ replacement?: string;
32
+ removeIn?: string;
33
+ message: string;
34
+ }
@@ -1,2 +1,54 @@
1
+ import { z } from 'zod';
1
2
  import type { ChaosConfig } from './config';
3
+ import type { CustomValidatorMap, ValidationIssue } from './validation-types';
4
+ /** Prebuilt strict variant. Default for `unknownFields: 'reject'` and for the
5
+ * post-preset-expansion second pass. Built once at module load. Typed as
6
+ * `z.ZodTypeAny` to keep DTS output tractable; runtime keeps the full
7
+ * schema graph. */
8
+ export declare const chaosConfigSchemaStrict: z.ZodTypeAny;
9
+ /** Prebuilt passthrough variant. Used for `unknownFields: 'warn' | 'ignore'`
10
+ * before `stripUnknownKeys` projects the result to known keys only. */
11
+ export declare const chaosConfigSchemaPassthrough: z.ZodTypeAny;
12
+ /** Bumped whenever the validator's invariants change semantically. Stale
13
+ * brands fail the strict-equality check inside the short-circuit and re-
14
+ * validate. Do NOT bump for cosmetic / refactor-only changes. */
15
+ export declare const VALIDATOR_BRAND_VERSION = 1;
16
+ /** Schema-only validation. Does NOT expand presets and does NOT run the
17
+ * post-merge re-validation pass. Calling this in a runtime path silently
18
+ * bypasses preset expansion. For runtime preparation, call
19
+ * `prepareChaosConfig` (or `validateChaosConfig` for the full structured
20
+ * pipeline). */
2
21
  export declare function validateConfig(config: unknown): ChaosConfig;
22
+ export interface PrepareChaosConfigOptions {
23
+ unknownFields?: 'reject' | 'warn' | 'ignore';
24
+ }
25
+ /** Canonical runtime preparation entry point for a `ChaosConfig`.
26
+ *
27
+ * Composes:
28
+ * 1. Zod pass 1 (strict OR passthrough+strip per `opts.unknownFields`).
29
+ * 2. Build per-instance `PresetRegistry`, register customs.
30
+ * 3. `expandPresets` — append rule arrays + groups, strip preset fields.
31
+ * 4. Zod pass 2 (strict, on the merged config).
32
+ *
33
+ * v0.4.x callers pass no opts and get strict-by-default behavior identical
34
+ * to before. */
35
+ export declare function prepareChaosConfig(input: unknown, opts?: PrepareChaosConfigOptions): ChaosConfig;
36
+ export interface ValidateChaosConfigOptions {
37
+ unknownFields?: 'reject' | 'warn' | 'ignore';
38
+ onDeprecation?: (issue: ValidationIssue) => void;
39
+ customValidators?: CustomValidatorMap;
40
+ }
41
+ /** Canonical validation entry point for adapters and the engine.
42
+ *
43
+ * Pipeline:
44
+ * 1. Schema-version gate (BEFORE Zod, unambiguous message).
45
+ * 2. Brand short-circuit (only when brand-version matches AND opts empty).
46
+ * 3-5. `prepareChaosConfig` — Zod pass 1 + preset expansion + Zod pass 2.
47
+ * 6. Deprecation walk.
48
+ * 7. Custom validators.
49
+ * 8. Issue sort (inside ChaosConfigError construction).
50
+ * 9. Brand stamp — final step only.
51
+ *
52
+ * Throws `ChaosConfigError` aggregating all issues from the first failing
53
+ * layer. Subsequent layers are skipped on failure. */
54
+ export declare function validateChaosConfig(input: unknown, opts?: ValidateChaosConfigOptions): ChaosConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaos-maker/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "A lightweight, framework-agnostic toolkit for injecting chaos into web applications to test frontend resilience",
6
6
  "keywords": [
@@ -58,11 +58,13 @@
58
58
  "jsdom": "^27.0.1",
59
59
  "typescript": "^5.4.5",
60
60
  "vite": "^6.4.2",
61
- "vitest": "^3.2.4"
61
+ "vitest": "^3.2.4",
62
+ "zod-to-json-schema": "3.24.5"
62
63
  },
63
64
  "scripts": {
64
65
  "dev": "vite build --watch",
65
- "build": "vite build && vite build -c vite.config.sw.ts && tsc -p tsconfig.build.json",
66
+ "build": "vite build && vite build -c vite.config.sw.ts && tsc -p tsconfig.build.json && pnpm run build:schema",
67
+ "build:schema": "node scripts/build-schema.mjs",
66
68
  "test": "vitest run",
67
69
  "test:watch": "vitest"
68
70
  }