@nwire/hooks 0.7.0 → 0.8.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/dist/record.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Recording + replay.
3
+ *
4
+ * Recording a hook run produces a {@link Recording} — the full step trace
5
+ * plus user-supplied ctx snapshots taken before and after. Replays the
6
+ * recording later (or in another process) and assert step-by-step shape
7
+ * matches. This is the substrate Studio's "replay this trace" button and
8
+ * the deterministic debug flow are built on.
9
+ *
10
+ * Two scopes:
11
+ *
12
+ * record(hook, ctx, opts?) one-shot: run the hook, snapshot ctx, return Recording
13
+ * replay(hook, recording, opts) re-run with the recorded ctxIn; compare shapes
14
+ *
15
+ * The clone strategy is pluggable; default is `structuredClone`. Pass
16
+ * `clone: (x) => JSON.parse(JSON.stringify(x))` for non-clonable shapes.
17
+ */
18
+ const defaultClone = (ctx) => {
19
+ if (typeof structuredClone === "function")
20
+ return structuredClone(ctx);
21
+ // Last-resort fallback for environments without structuredClone.
22
+ return JSON.parse(JSON.stringify(ctx));
23
+ };
24
+ /**
25
+ * Run the hook once, capture ctx-in/out + every observation. Result is a
26
+ * self-contained {@link Recording} that can be persisted (JSON-safe if the
27
+ * caller's clone yields JSON-safe shapes), shipped to Studio, or fed back
28
+ * into `replay()`.
29
+ */
30
+ export async function record(hook, ctx, opts = {}) {
31
+ const clone = opts.clone ?? defaultClone;
32
+ const ctxIn = clone(ctx);
33
+ const result = await hook.runDetailed(ctx, opts);
34
+ const ctxOut = clone(result.ctx);
35
+ return {
36
+ hookId: "id" in hook ? hook.id : "unknown",
37
+ hookName: hook.name,
38
+ runId: result.runId,
39
+ outcome: result.outcome,
40
+ steps: result.steps,
41
+ ctxIn,
42
+ ctxOut,
43
+ capturedAt: new Date().toISOString(),
44
+ };
45
+ }
46
+ /**
47
+ * Replay a recording. Re-runs the hook with `recording.ctxIn` (cloned),
48
+ * compares the resulting step sequence + outcome against the recording,
49
+ * and reports drift. This is observational — it does not stub side effects.
50
+ * Pure / idempotent chains should match; non-deterministic chains report
51
+ * their drift so a human can decide whether it's expected.
52
+ */
53
+ export async function replay(hook, recording, opts = {}) {
54
+ const clone = opts.clone ?? defaultClone;
55
+ const inputCtx = clone(recording.ctxIn);
56
+ const result = await hook.runDetailed(inputCtx, opts);
57
+ const drift = [];
58
+ if (result.outcome !== recording.outcome) {
59
+ drift.push(`outcome: recorded=${recording.outcome} replayed=${result.outcome}`);
60
+ }
61
+ if (result.steps.length !== recording.steps.length) {
62
+ drift.push(`step count: recorded=${recording.steps.length} replayed=${result.steps.length}`);
63
+ }
64
+ // Compare step sequences by (stepKind, stepName, phase). Durations + ts
65
+ // are inherently non-deterministic — we exclude them from the compare.
66
+ const lim = Math.min(result.steps.length, recording.steps.length);
67
+ for (let i = 0; i < lim; i++) {
68
+ const r = recording.steps[i];
69
+ const p = result.steps[i];
70
+ if (r.stepKind !== p.stepKind || r.stepName !== p.stepName || r.phase !== p.phase) {
71
+ drift.push(`step[${i}]: recorded=${r.stepKind}/${r.stepName ?? ""}/${r.phase} ` +
72
+ `replayed=${p.stepKind}/${p.stepName ?? ""}/${p.phase}`);
73
+ }
74
+ }
75
+ return {
76
+ matches: drift.length === 0,
77
+ drift,
78
+ recorded: recording,
79
+ replayed: {
80
+ ctx: result.ctx,
81
+ stepCount: result.steps.length,
82
+ outcome: result.outcome,
83
+ },
84
+ };
85
+ }
86
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.js","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Process-wide registry + topology helpers.
3
+ *
4
+ * Everything that needs to be ambient — the "current run" context that
5
+ * lets nested `.run()` calls link as children, plus the introspection
6
+ * list `nwire scan` and Studio consume — lives here. One module, no
7
+ * deps beyond `node:async_hooks` (stdlib).
8
+ */
9
+ import type { Hook } from "./hook.js";
10
+ import type { SourceLocation } from "./types.js";
11
+ /** Internal — called by `hook()` after instantiation. */
12
+ export declare function registerHook(hook: Hook<unknown>, source?: SourceLocation): void;
13
+ /**
14
+ * Metadata for every hook created in this process. Scan + Studio call
15
+ * this to enumerate the world; it returns a snapshot, mutating it has
16
+ * no effect on the registry.
17
+ */
18
+ export declare function listHooks(): ReadonlyArray<{
19
+ readonly id: string;
20
+ readonly name: string;
21
+ readonly chain: number;
22
+ readonly listeners: number;
23
+ readonly source?: SourceLocation;
24
+ }>;
25
+ /** Clear the registry. Test-only — never call in production paths. */
26
+ export declare function __resetRegistryForTests(): void;
27
+ /**
28
+ * Walk the V8 stack, skip frames that originate inside `@nwire/*` packages,
29
+ * return the first user-code frame. `skipFrames` lets call sites that are
30
+ * one or two helpers deep aim past their own wrapper.
31
+ */
32
+ export declare function captureSourceLocation(skipFrames?: number): SourceLocation | undefined;
33
+ interface RunContext {
34
+ readonly runId: string;
35
+ readonly hookId: string;
36
+ readonly parentRunId?: string;
37
+ }
38
+ /** Monotonic id for the next `.run()`. Shared across module instances. */
39
+ export declare function nextRunId(): string;
40
+ /** Monotonic id assigned to each `hook()` at construction. */
41
+ export declare function nextHookId(): string;
42
+ /**
43
+ * Run a function with a {@link RunContext} active for the duration. Nested
44
+ * `.run()` calls inside `fn` see this context via {@link currentRun} and
45
+ * pick up `runId` as their `parentRunId`.
46
+ */
47
+ export declare function withRunContext<T>(ctx: RunContext, fn: () => Promise<T>): Promise<T>;
48
+ /**
49
+ * The active run context, if any. Returns undefined when called outside
50
+ * a hook run — that's the normal case for top-level dispatch.
51
+ */
52
+ export declare function currentRun(): RunContext | undefined;
53
+ export {};
54
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AA6B9C,yDAAyD;AACzD,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAE/E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,aAAa,CAAC;IACzC,QAAQ,CAAC,EAAE,EAAQ,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAM,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAK,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAC,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAG,cAAc,CAAC;CACnC,CAAC,CAQD;AAED,sEAAsE;AACtE,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAaD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,UAAU,SAAI,GAAG,cAAc,GAAG,SAAS,CAehF;AAID,UAAU,UAAU;IAClB,QAAQ,CAAC,KAAK,EAAS,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAQ,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAID,0EAA0E;AAC1E,wBAAgB,SAAS,IAAI,MAAM,CAGlC;AAED,8DAA8D;AAC9D,wBAAgB,UAAU,IAAI,MAAM,CAGnC;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAEnF;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,UAAU,GAAG,SAAS,CAEnD"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Process-wide registry + topology helpers.
3
+ *
4
+ * Everything that needs to be ambient — the "current run" context that
5
+ * lets nested `.run()` calls link as children, plus the introspection
6
+ * list `nwire scan` and Studio consume — lives here. One module, no
7
+ * deps beyond `node:async_hooks` (stdlib).
8
+ */
9
+ import { AsyncLocalStorage } from "node:async_hooks";
10
+ // Pin to globalThis so every instance of @nwire/hooks loaded into this
11
+ // process (Node loader + jiti + tsx + …) shares one registry. Without
12
+ // this, scan-time `listHooks()` ran against a different module graph
13
+ // than user code and saw an empty list.
14
+ const GLOBAL_KEY = "__nwire_hooks_registry__";
15
+ const g = globalThis;
16
+ const slot = (g[GLOBAL_KEY] ??= {
17
+ registry: [],
18
+ hookCounter: { value: 0 },
19
+ runCounter: { value: 0 },
20
+ });
21
+ const registry = slot.registry;
22
+ /** Internal — called by `hook()` after instantiation. */
23
+ export function registerHook(hook, source) {
24
+ registry.push({ hook, source });
25
+ }
26
+ /**
27
+ * Metadata for every hook created in this process. Scan + Studio call
28
+ * this to enumerate the world; it returns a snapshot, mutating it has
29
+ * no effect on the registry.
30
+ */
31
+ export function listHooks() {
32
+ return registry.map(({ hook, source }) => ({
33
+ id: hook.id,
34
+ name: hook.name,
35
+ chain: hook.stepCounts().chain,
36
+ listeners: hook.stepCounts().listeners,
37
+ source,
38
+ }));
39
+ }
40
+ /** Clear the registry. Test-only — never call in production paths. */
41
+ export function __resetRegistryForTests() {
42
+ registry.length = 0;
43
+ }
44
+ // ─── Source-location capture ──────────────────────────────────────────
45
+ const PAREN_RE = /\(([^()]+):(\d+):(\d+)\)\s*$/;
46
+ const PLAIN_RE = /\s+at\s+([^()\s]+):(\d+):(\d+)\s*$/;
47
+ // Skip frames inside installed @nwire packages (node_modules) and inside
48
+ // framework-source paths — but NOT inside test files under those packages,
49
+ // so package-local tests can assert on captured user-code locations.
50
+ // `[\\/]` accepts both POSIX `/` and Windows `\` separators because V8
51
+ // stack frames emit native path syntax per-platform.
52
+ const NWIRE_FRAME_RE = /(node_modules[\\/](@nwire[\\/]|\.pnpm[\\/]@nwire\+)|packages[\\/]nwire-[^\\/]+[\\/](src|dist)[\\/](?!__tests__[\\/]))/;
53
+ /**
54
+ * Walk the V8 stack, skip frames that originate inside `@nwire/*` packages,
55
+ * return the first user-code frame. `skipFrames` lets call sites that are
56
+ * one or two helpers deep aim past their own wrapper.
57
+ */
58
+ export function captureSourceLocation(skipFrames = 1) {
59
+ const err = new Error();
60
+ const stack = err.stack;
61
+ if (!stack)
62
+ return undefined;
63
+ const lines = stack.split("\n");
64
+ // First line is "Error" header; the +1 skips it.
65
+ for (let i = skipFrames + 1; i < lines.length; i++) {
66
+ const line = lines[i];
67
+ if (NWIRE_FRAME_RE.test(line))
68
+ continue;
69
+ const m = PAREN_RE.exec(line) ?? PLAIN_RE.exec(line);
70
+ if (!m)
71
+ continue;
72
+ const [, file, lineStr, colStr] = m;
73
+ return { file: file, line: Number(lineStr), column: Number(colStr) };
74
+ }
75
+ return undefined;
76
+ }
77
+ const runContext = new AsyncLocalStorage();
78
+ /** Monotonic id for the next `.run()`. Shared across module instances. */
79
+ export function nextRunId() {
80
+ slot.runCounter.value += 1;
81
+ return `r${slot.runCounter.value.toString(36)}-${Date.now().toString(36)}`;
82
+ }
83
+ /** Monotonic id assigned to each `hook()` at construction. */
84
+ export function nextHookId() {
85
+ slot.hookCounter.value += 1;
86
+ return `h${slot.hookCounter.value.toString(36)}`;
87
+ }
88
+ /**
89
+ * Run a function with a {@link RunContext} active for the duration. Nested
90
+ * `.run()` calls inside `fn` see this context via {@link currentRun} and
91
+ * pick up `runId` as their `parentRunId`.
92
+ */
93
+ export function withRunContext(ctx, fn) {
94
+ return runContext.run(ctx, fn);
95
+ }
96
+ /**
97
+ * The active run context, if any. Returns undefined when called outside
98
+ * a hook run — that's the normal case for top-level dispatch.
99
+ */
100
+ export function currentRun() {
101
+ return runContext.getStore();
102
+ }
103
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -1,23 +1,52 @@
1
1
  /**
2
- * Public types for `@nwire/hooks`. See architecture-sketch.html §06–§07.
2
+ * Public types for `@nwire/hooks`.
3
+ *
4
+ * The two function shapes:
5
+ * - ChainFn — koa-compose middleware. Wraps next(); can short-circuit.
6
+ * - ListenerFn — parallel observer. Reads the final ctx; never mutates.
7
+ *
8
+ * The observability surface (StepObservation / RunResult / Recording) is the
9
+ * contract every consumer relies on for tracing, scan, Studio, OTel, replay.
3
10
  */
4
11
  /** A single link in a chain — koa-compose-shaped middleware. */
5
12
  export type ChainFn<Ctx> = (ctx: Ctx, next: () => Promise<void>) => Promise<void> | void;
6
13
  /** A listener — observes the final ctx state, never mutates it. */
7
14
  export type ListenerFn<Ctx> = (ctx: Readonly<Ctx>) => Promise<void> | void;
8
- /** Options for {@link createHook}/{@link hook}. */
15
+ /** Where this hook (or step) was authored. Captured at attach time. */
16
+ export interface SourceLocation {
17
+ readonly file: string;
18
+ readonly line: number;
19
+ readonly column?: number;
20
+ }
21
+ /** Options for `hook()` / `createHook()`. */
9
22
  export interface HookOptions<Ctx> {
10
23
  /**
11
24
  * If true, listener errors are re-thrown by `.run()` instead of being
12
- * collected and surfaced via `onListenerError`. Default: false (conservative).
25
+ * collected and surfaced via `onListenerError`. Default: false.
13
26
  */
14
27
  strictListeners?: boolean;
15
28
  /**
16
- * Called once per listener error. Defaults to `console.error`. A host
17
- * (e.g. `createHooks(...)`) wires its `lifecycle.error` hook here.
29
+ * Called once per listener error. Defaults to console.error. Hosts can
30
+ * route this through their own logger / framework-event bus.
18
31
  */
19
32
  onListenerError?: (err: unknown, ctx: Readonly<Ctx>, hookName: string) => void;
20
33
  }
34
+ /** Optional per-step metadata supplied at `.use()` / `.on()` time. */
35
+ export interface UseOptions {
36
+ /** Human label — shown in tap events, OTel spans, Studio. */
37
+ readonly name?: string;
38
+ /** Higher runs first (= outermost in chain). Default 0. */
39
+ readonly priority?: number;
40
+ }
41
+ export interface OnOptions extends UseOptions {
42
+ /**
43
+ * Filter when this listener fires relative to the chain outcome.
44
+ * "always" — fire on completed + prevented + failed (default).
45
+ * "success" — fire only when the chain completed without throw.
46
+ * "failure" — fire only when the chain threw.
47
+ */
48
+ readonly when?: "always" | "success" | "failure";
49
+ }
21
50
  /** Retry policy for {@link withRetry}. */
22
51
  export interface RetryOpts {
23
52
  /** Total attempts (including the first). Must be >= 1. */
@@ -27,4 +56,67 @@ export interface RetryOpts {
27
56
  /** Optional predicate — return false to stop retrying a given error. */
28
57
  shouldRetry?: (err: unknown, attempt: number) => boolean;
29
58
  }
59
+ export type StepKind = "chain" | "listener";
60
+ export type StepPhase = "start" | "end" | "error";
61
+ export type RunOutcome = "completed" | "prevented" | "failed";
62
+ /** One observation emitted to taps. Tagged union by `phase`. */
63
+ export interface StepObservation {
64
+ /** Hook name, stable across runs. */
65
+ readonly hookName: string;
66
+ /** Hook instance id — unique per `hook()` call in this process. */
67
+ readonly hookId: string;
68
+ /** Run id — unique per `.run()` invocation. */
69
+ readonly runId: string;
70
+ /** Parent run id when this `.run()` was started inside another. */
71
+ readonly parentRunId?: string;
72
+ /** Monotonic step id within the hook (insertion order). */
73
+ readonly stepId: number;
74
+ readonly stepKind: StepKind;
75
+ /** Optional human label from {@link UseOptions.name} / {@link OnOptions.name}. */
76
+ readonly stepName?: string;
77
+ readonly phase: StepPhase;
78
+ /** performance.now() timestamp at observation. */
79
+ readonly ts: number;
80
+ /** Set on `end` and `error`. Milliseconds since matching `start`. */
81
+ readonly durationMs?: number;
82
+ /** Set on `error`. */
83
+ readonly error?: unknown;
84
+ }
85
+ /** Subscriber to a hook's per-step observations. */
86
+ export type TapFn = (obs: StepObservation) => void;
87
+ /** Per-run options. */
88
+ export interface RunOptions {
89
+ /** Abort the run. Listeners may inspect; chains may bail mid-flight. */
90
+ readonly signal?: AbortSignal;
91
+ /** Explicit parent. When omitted, picks up the ambient parent. */
92
+ readonly parentRunId?: string;
93
+ }
94
+ /** Detailed result returned by {@link Hook.runDetailed}. */
95
+ export interface RunResult<Ctx> {
96
+ readonly ctx: Ctx;
97
+ readonly outcome: RunOutcome;
98
+ readonly runId: string;
99
+ readonly parentRunId?: string;
100
+ readonly steps: ReadonlyArray<StepObservation>;
101
+ readonly durationMs: number;
102
+ /** Set when outcome === "failed". */
103
+ readonly error?: unknown;
104
+ }
105
+ /**
106
+ * A recorded run, complete enough to replay step-by-step.
107
+ *
108
+ * `ctxIn` / `ctxOut` are snapshots provided by the caller — the recorder
109
+ * does not serialize ctx itself (ctx shapes are domain-specific). Callers
110
+ * usually pass `structuredClone(ctx)` before and after.
111
+ */
112
+ export interface Recording {
113
+ readonly hookId: string;
114
+ readonly hookName: string;
115
+ readonly runId: string;
116
+ readonly outcome: RunOutcome;
117
+ readonly steps: ReadonlyArray<StepObservation>;
118
+ readonly ctxIn: unknown;
119
+ readonly ctxOut: unknown;
120
+ readonly capturedAt: string;
121
+ }
30
122
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,gEAAgE;AAChE,MAAM,MAAM,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEzF,mEAAmE;AACnE,MAAM,MAAM,UAAU,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAE3E,mDAAmD;AACnD,MAAM,WAAW,WAAW,CAAC,GAAG;IAC9B;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAChF;AAED,0CAA0C;AAC1C,MAAM,WAAW,SAAS;IACxB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;CAC1D"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js CHANGED
@@ -1,5 +1,12 @@
1
1
  /**
2
- * Public types for `@nwire/hooks`. See architecture-sketch.html §06–§07.
2
+ * Public types for `@nwire/hooks`.
3
+ *
4
+ * The two function shapes:
5
+ * - ChainFn — koa-compose middleware. Wraps next(); can short-circuit.
6
+ * - ListenerFn — parallel observer. Reads the final ctx; never mutates.
7
+ *
8
+ * The observability surface (StepObservation / RunResult / Recording) is the
9
+ * contract every consumer relies on for tracing, scan, Studio, OTel, replay.
3
10
  */
4
11
  export {};
5
12
  //# sourceMappingURL=types.js.map
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nwire/hooks",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Nwire — the universal dispatch primitive. One hook() accepts both .use() (sequential koa-compose chain) and .on() (parallel listener) attachments. Built on emittery + a ~30 LOC composer. Standalone, in-process only, ~100 LOC of surface.",
5
5
  "keywords": [
6
6
  "dispatch",
@@ -11,9 +11,11 @@
11
11
  "middleware",
12
12
  "nwire"
13
13
  ],
14
+ "license": "MIT",
14
15
  "files": [
15
16
  "dist",
16
- "README.md"
17
+ "README.md",
18
+ "LICENSE"
17
19
  ],
18
20
  "type": "module",
19
21
  "main": "./dist/index.js",