@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.
- package/README.md +91 -9
- package/dist/chaos-config.schema.json +1314 -0
- package/dist/chaos-config.schema.notes.md +32 -0
- package/dist/chaos-maker.cjs +23 -5
- package/dist/chaos-maker.js +3476 -2247
- package/dist/chaos-maker.umd.js +23 -5
- package/dist/sw.js +1 -1
- package/dist/sw.mjs +808 -454
- package/dist/types/ChaosMaker.d.ts +41 -0
- package/dist/types/builder.d.ts +24 -0
- package/dist/types/config.d.ts +82 -14
- package/dist/types/debug.d.ts +58 -0
- package/dist/types/errors.d.ts +14 -2
- package/dist/types/events.d.ts +59 -1
- package/dist/types/groups.d.ts +71 -0
- package/dist/types/index.d.ts +30 -5
- package/dist/types/interceptors/domAssailant.d.ts +2 -1
- package/dist/types/interceptors/eventSource.d.ts +2 -1
- package/dist/types/interceptors/networkFetch.d.ts +2 -1
- package/dist/types/interceptors/networkXHR.d.ts +2 -1
- package/dist/types/interceptors/websocket.d.ts +2 -1
- package/dist/types/presets.d.ts +63 -2
- package/dist/types/sw-bridge-source.d.ts +3 -1
- package/dist/types/sw.d.ts +7 -0
- package/dist/types/utils.d.ts +46 -1
- package/dist/types/validation-deprecation.d.ts +7 -0
- package/dist/types/validation-format.d.ts +10 -0
- package/dist/types/validation-strip.d.ts +11 -0
- package/dist/types/validation-types.d.ts +34 -0
- package/dist/types/validation.d.ts +52 -0
- package/package.json +5 -3
package/dist/types/presets.d.ts
CHANGED
|
@@ -1,2 +1,63 @@
|
|
|
1
|
-
import { ChaosConfig } from './config';
|
|
2
|
-
|
|
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";
|
package/dist/types/sw.d.ts
CHANGED
|
@@ -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;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -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.
|
|
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
|
}
|