@vielzeug/codex 26.10.0 → 26.10.1

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.
Files changed (59) hide show
  1. package/README.md +1 -1
  2. package/data/catalog.json +53 -59
  3. package/data/llms-full.txt +1322 -1069
  4. package/data/llms.txt +6 -6
  5. package/data/manifest.json +1 -1
  6. package/data/packages/arsenal.json +13 -13
  7. package/data/packages/assay.json +4 -4
  8. package/data/packages/clockwork.json +4 -4
  9. package/data/packages/codex.json +3 -3
  10. package/data/packages/coins.json +4 -4
  11. package/data/packages/conduit.json +4 -4
  12. package/data/packages/courier.json +4 -4
  13. package/data/packages/dnd.json +10 -10
  14. package/data/packages/familiar.json +4 -4
  15. package/data/packages/flux.json +4 -4
  16. package/data/packages/focus.json +4 -4
  17. package/data/packages/forge.json +4 -4
  18. package/data/packages/gesture.json +4 -4
  19. package/data/packages/herald.json +12 -12
  20. package/data/packages/illusionist.json +5 -5
  21. package/data/packages/keymap.json +9 -9
  22. package/data/packages/ledger.json +4 -4
  23. package/data/packages/lingua.json +4 -4
  24. package/data/packages/mesh.json +5 -5
  25. package/data/packages/necromancer.json +5 -5
  26. package/data/packages/orbit.json +7 -7
  27. package/data/packages/ore.json +5 -5
  28. package/data/packages/postmaster.json +4 -4
  29. package/data/packages/prism.json +50 -42
  30. package/data/packages/pulse.json +8 -8
  31. package/data/packages/refine.json +3 -3
  32. package/data/packages/ripple.json +4 -4
  33. package/data/packages/rune.json +9 -9
  34. package/data/packages/sandbox.json +4 -4
  35. package/data/packages/scout.json +7 -7
  36. package/data/packages/scroll.json +10 -10
  37. package/data/packages/sentinel.json +4 -4
  38. package/data/packages/sigil.json +6 -6
  39. package/data/packages/sourcerer.json +4 -4
  40. package/data/packages/spell.json +9 -9
  41. package/data/packages/tandem.json +5 -5
  42. package/data/packages/tavern.json +12 -12
  43. package/data/packages/tempo.json +4 -4
  44. package/data/packages/vault.json +13 -13
  45. package/data/packages/ward.json +9 -9
  46. package/data/packages/wayfinder.json +17 -17
  47. package/data/refine.json +259 -87
  48. package/data/search.json +246 -246
  49. package/dist/http.js +2 -2
  50. package/dist/http.js.map +1 -1
  51. package/dist/index.d.ts +1 -1
  52. package/dist/index.js +1 -1
  53. package/dist/refine.d.ts +1 -1
  54. package/dist/refine.js +1 -1
  55. package/dist/server.d.ts +1 -1
  56. package/dist/server.js +1 -1
  57. package/dist/tools/refine.js +1 -1
  58. package/package.json +2 -2
  59. package/skills/vielzeug/SKILL.md +57 -57
@@ -1,35 +1,35 @@
1
1
  {
2
2
  "apiSource": "export type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';\nexport { consoleTransport, DEFAULT_THEME, resolveTheme } from './console';\nexport { RuneConfigError, RuneError } from './errors';\nexport type { LazyBinding } from './lazy';\nexport { lazy } from './lazy';\nexport { createLogger } from './logger';\nexport { batchTransport, jsonTransport, redactTransport, remoteTransport, sampleTransport } from './transports';\nexport type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n Logger,\n LogLevel,\n LogMethod,\n LogMiddleware,\n LogType,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';\nexport { isLevelEnabled, PRIORITY } from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Rune — Structured logging for TypeScript\ndescription: Browser/Node logger with levels, namespaces, pluggable transports, lazy bindings, and timing helpers.\npackage: rune\ncategory: logging\nkeywords: [logging, console, structured, scoped, transports, remote-logging, levels, namespaces, lazy-bindings]\nrelated: [courier, herald, familiar]\nexports:\n [\n createLogger,\n consoleTransport,\n jsonTransport,\n remoteTransport,\n batchTransport,\n sampleTransport,\n redactTransport,\n lazy,\n isLevelEnabled,\n resolveTheme,\n DEFAULT_THEME,\n PRIORITY,\n RuneError,\n RuneConfigError,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"rune\" />\n\n## Why Rune?\n\nPlain `console.log` lacks structure: no log levels, no namespacing, no remote delivery, no way to silence logs in production.\n\n```ts\n// Before — manual approach\nconst path = '/users';\nconsole.log(`[api] GET ${path}`);\n\n// After — Rune\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst api = createLogger({\n namespace: 'api',\n transports: [\n consoleTransport({ level: 'debug' }),\n jsonTransport({ level: 'error' }),\n ],\n});\n\napi.info('request', { method: 'GET', path });\n```\n\n| Feature | Rune | Winston | Pino | console |\n| -------------------- | ------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"rune\" type=\"size\" /> | ~44 kB | ~4 kB | 0 kB |\n| Browser support | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Scoped loggers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | Child | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Pluggable transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Built-in factories | <ore-icon name=\"check\" size=\"16\"></ore-icon> Transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Streams | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Structured log entry | <ore-icon name=\"check\" size=\"16\"></ore-icon> `LogEntry` type | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Lazy bindings | <ore-icon name=\"check\" size=\"16\"></ore-icon> `lazy(fn)` | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Styled output | <ore-icon name=\"check\" size=\"16\"></ore-icon> CSS badges | Text only | Text only | Manual |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> (15+) | <ore-icon name=\"x\" size=\"16\"></ore-icon> (5+) | N/A |\n\n<div class=\"decision-callout\">\n\n**Use Rune when** you need isomorphic logging (browser + Node.js), namespaced module loggers, or remote error delivery without a heavy dependency chain.\n\n**Consider alternatives when** you need high-throughput file-based logging (Pino), file rotation (Winston), or your team already uses a logging framework.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/rune\n```\n\n```sh [npm]\nnpm install @vielzeug/rune\n```\n\n```sh [yarn]\nyarn add @vielzeug/rune\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport, lazy } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'server',\n transports: [\n consoleTransport({ timestamp: true }),\n jsonTransport({ level: 'error' }),\n ],\n});\n\nconst requestLog = log.withBindings({\n diagnostics: lazy(() => ({ queueDepth: 0 })),\n requestId: 'abc-123',\n});\n\nrequestLog.info('request', { method: 'GET', path: '/users' });\nconst users = await requestLog.time('load users', () => Promise.resolve(['user-1']));\nconsole.log(users);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Level filtering (`debug` to `off`) with `enabled()` checks, including `fatal` above `error`\n- Immutable config after construction — use `child()` or `withBindings()` to scope\n- Message-first, context-first, and Error-first log calls for application code and adapters\n- `Error` values are auto-serialized to `{ message, name, stack }` — survives JSON.stringify\n- Pinned context bindings via `withBindings({ requestId })` — fields on every line\n- Lazy bindings via `lazy(fn)` — expensive computations gated behind the level check\n- Namespaced child loggers via `createLogger('name')` or `logger.child({ namespace })`\n- Pluggable transport pipeline: console, JSON, remote, batching, sampling, and fail-closed redaction\n- Immutable middleware transforms and filters applied before every transport\n- Structured `time()` wrapper: emits the label as message with `{ duration_ms }` in context\n- `group()` and `groupCollapsed()` wrappers that auto-close on throw/reject\n- `LogEntry.data` — single merged flat object for transports; no manual merging needed\n- Zero dependencies — <PackageInfo package=\"rune\" type=\"size\" /> gzipped\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Courier](/courier/) — HTTP client with built-in request/response interception; pipe Rune as a transport to log every API call with structured context\n- [Herald](/herald/) — typed event bus; emit log-level change or flush events across modules without coupling loggers directly\n- [Familiar](/familiar/) — Web Worker pool; use Rune inside task functions to surface structured worker-side logs back to the main thread\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Rune — API Reference\ndescription: API reference for @vielzeug/rune exports, logger methods, configuration types, and transport factories.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------- | ------------------------------------------------ | -------------- | ------------------------------------------------------------ |\n| `createLogger()` | Create an isolated `Logger` instance | Sync | Omitting `transports` defaults to `consoleTransport()` |\n| `lazy(fn)` | Defer a binding value past the level check | Sync | Factory runs on every emit, not once |\n| `isLevelEnabled()` | Utility: test whether a level passes a threshold | Sync | `'off'` always returns `false` |\n| `PRIORITY` | Numeric priority table backing `isLevelEnabled()`| — | Lower number = more verbose |\n| `resolveTheme()` | Merge a partial theme onto the default | Sync | Returns a fully-populated `ResolvedTheme` |\n| `consoleTransport()` | Styled console output | Sync | Theme is resolved once at factory call, not per entry |\n| `jsonTransport()` | NDJSON to stdout or a custom sink | Sync | `process.stdout` is unavailable in browsers |\n| `remoteTransport()` | Asynchronous remote delivery | Async | Pass `onError` for production failure observability |\n| `batchTransport()` | Serialized buffered delivery | Async | Await `dispose()` during shutdown |\n| `sampleTransport()` | Probabilistic forwarding | Sync | Validate the downstream delivery path independently |\n| `redactTransport()` | Recursive sensitive-field masking | Sync | Deeper subtrees are replaced when `maxDepth` is exceeded |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------- | -------------------------------------------------------- |\n| `@vielzeug/rune` | All exports — logger, transport factories, `lazy`, types |\n\n## createLogger(initial?, options?)\n\nCreates an isolated logger instance.\n\n```ts\ncreateLogger(namespace: string, options?: Omit<RuneOptions, 'namespace'>): Logger\ncreateLogger(options?: RuneOptions): Logger\n```\n\n- `string` shorthand sets namespace: `createLogger('api')` or `createLogger('api', { logLevel: 'warn' })`.\n- Each call produces a fully independent instance — no shared mutable state.\n- Default transport is `consoleTransport()` when `transports` is omitted.\n\n> **Note — disposed loggers:** after `dispose()` is called, all log methods (`debug`, `info`, `warn`, `error`, `fatal`), `time()`, and `group()` / `groupCollapsed()` silently no-op. The `fn` callback in `group()` still runs — only the group header is suppressed.\n\n> **Note — fault isolation:** if middleware throws, that entry is dropped; if a transport throws synchronously, sibling transports still run. Asynchronous delivery failures belong to the transport and should be observed through options such as `remoteTransport({ onError })`.\n\n**Returns:** `Logger`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({ logLevel: 'warn', namespace: 'app' });\n\nconst serverLog = createLogger({\n namespace: 'server',\n transports: [\n consoleTransport(),\n jsonTransport({ level: 'error' }),\n ],\n});\n```\n\n## lazy(fn)\n\nDefers evaluation of an expensive binding value until after the level check passes.\nThe factory function is never called when the log level suppresses the entry.\n\n```ts\nlazy(fn: () => unknown): LazyBinding\n```\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst reqLog = log.withBindings({\n diagnostics: lazy(() => buildExpensiveDiagnostics()),\n});\n\nreqLog.debug('trace'); // diagnostics() only called when debug is enabled\n```\n\n**Returns:** `LazyBinding`\n\n## Logger Methods\n\n### Logging\n\nAll five methods use `LogMethod`:\n\n```ts\n(message: string, context?: Bindings): void\n(context: Bindings, message?: string): void\n(error: Error, message?: string): void\n(error: Error, context: Bindings, message?: string): void\n```\n\nMessage-first is preferred for ordinary logs. Context-first supports structured events and adapters. Error-first stores the serialized error under `err`.\n\n```ts\nlog.info('request started', { requestId: 'abc' });\nlog.debug({ type: 'dispatch' }, 'bus:dispatch');\nlog.error(new Error('timeout'), { requestId: 'abc' }, 'request failed');\n```\n\n### Composition\n\n| Method | Returns | What it does |\n| ---------------------- | -------- | ---------------------------------------------------------------- |\n| `child(overrides?)` | `Logger` | Clones config, applies overrides, inherits bindings |\n| `use(middleware)` | `Logger` | Adds an immutable pre-dispatch transform or filter |\n| `withBindings(fields)` | `Logger` | Pins fields to every subsequent call, returns a new child logger |\n\n`child()` transport inheritance:\n\n- Omit `transports` → inherit parent transports (default).\n- Pass `transports: []` → disable all transports on the child.\n- Pass `transports: [...]` → replace entirely with the given list.\n\n`child()` namespace joining:\n\n- `parent.child({ namespace: 'auth' })` on a logger with namespace `'api'` produces `'api.auth'`.\n- Omit `namespace` → inherits parent namespace unchanged.\n\n### Utilities\n\n| Method | Returns | Description |\n| ----------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `enabled(level)` | `boolean` | True if entries at this level pass the configured threshold |\n| `time(label, fn, level?)` | `T` | Measures sync/async execution; emits at `level` (default `'debug'`), label as message, `{ duration_ms }` in `data`. When `fn` throws or rejects, `{ err }` is also included. |\n| `group(label, fn, level?)` | `T` | Wraps callback in `console.group`; closes even on throw/reject. Pass `level` to gate the group header on the configured threshold (e.g. `'debug'` suppresses when `logLevel` is `'warn'`). |\n| `groupCollapsed(label, fn, level?)` | `T` | Same as `group`, using `console.groupCollapsed`. |\n| `dispose()` | `void` | Silences all subsequent log calls on this logger instance. Idempotent. |\n\n### Properties\n\n| Property | Type | Description |\n| ------------------ | -------------------------- | ------------------------------------------------------------------ |\n| `logLevel` | `LogLevel` | Active log level threshold |\n| `namespace` | `string` | Effective namespace string |\n| `transports` | `readonly Transport[]` | Transport pipeline snapshot |\n| `middleware` | `readonly LogMiddleware[]` | Middleware pipeline snapshot |\n| `bindings` | `Readonly<Bindings>` | Snapshot of currently pinned fields |\n| `disposalSignal` | `AbortSignal` | Aborted when `dispose()` is called. Use to tie external lifetimes. |\n| `disposed` | `boolean` | `true` after `dispose()` has been called |\n| `[Symbol.dispose]` | `() => void` | Delegates to `dispose()`. Enables `using` declarations. |\n\n## Transport Factories\n\n### consoleTransport(options?)\n\n```ts\nconsoleTransport(options?: ConsoleTransportOptions): Transport\n```\n\nWrites styled output to the browser console (CSS badges) or Node terminal (plain text). This is the default transport.\n\n| Option | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\nimport { inspect } from 'node:util';\n\nconst log = createLogger({\n transports: [consoleTransport({ level: 'info', timestamp: true, inspectFn: inspect })],\n});\n```\n\n### jsonTransport(options?)\n\n```ts\njsonTransport(options?: JsonTransportOptions): Transport\n```\n\nOutputs newline-delimited JSON (NDJSON) to `stdout` or a custom function. Useful for server-side log aggregation pipelines (ELK, Datadog, etc.).\n\nEach line is a flat JSON object with `level`, `time` (ISO), and optional `ns`, `msg`, plus all merged context fields.\n\n| Option | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | -------------------------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names for aggregator compatibility (e.g. `'severity'` for Datadog) |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info('request', { path: '/users', status: 200 });\n// {\"path\":\"/users\",\"status\":200,\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"msg\":\"request\"}\n```\n\n### remoteTransport(options)\n\n```ts\nremoteTransport(options: RemoteTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| --------- | ------------------------------------------------------------- | ------------- | ------------------------------ |\n| `handler` | `(level, data) => void \\| Promise<unknown>` | required | Remote delivery function |\n| `level` | `LogLevel` | `'debug'` | Minimum forwarded level |\n| `env` | `'development' \\| 'production'` | auto-detected | Payload environment |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | dev warning | Delivery failure observer |\n\n**Returns:** A fire-and-forget `Transport`. Pass `onError` whenever production delivery failures require monitoring.\n\n```ts\nimport { remoteTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({\n handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' }),\n onError: (error) => console.error('log delivery failed', error),\n});\n```\n\n### batchTransport(options)\n\n```ts\nbatchTransport(options: BatchTransportOptions): BatchHandle\n```\n\n| Option | Type | Default | Description |\n| -------------- | --------------------------------------------- | --------- | ----------------------------------- |\n| `onFlush` | `(entries) => void \\| Promise<void>` | required | Serialized batch delivery |\n| `onFlushError` | `(entries, error) => void` | — | Delivery failure observer |\n| `interval` | `number` | `5000` | Flush interval in milliseconds |\n| `maxSize` | `number` | `50` | Entries that trigger an early flush |\n| `maxBuffer` | `number` | unbounded | Hard in-memory limit |\n| `level` | `LogLevel` | `'debug'` | Minimum buffered level |\n\n**Returns:** `BatchHandle` with `transport`, `flush()`, `dispose()`, `disposed`, and `[Symbol.asyncDispose]`. Await `dispose()` during shutdown.\n\n```ts\nimport { batchTransport, createLogger } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n onFlush: (entries) => fetch('/api/log-batches', { body: JSON.stringify(entries), method: 'POST' }),\n});\nconst log = createLogger({ transports: [batch.transport] });\nawait batch.dispose();\n```\n\n### sampleTransport(options)\n\n```ts\nsampleTransport(options: SampleTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| ----------- | ----------- | --------- | ------------------------------ |\n| `rate` | `number` | required | Forwarding fraction from 0–1 |\n| `transport` | `Transport` | required | Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level before sampling |\n\n**Returns:** A sampling `Transport`.\n\n```ts\nimport { remoteTransport, sampleTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({ handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data) }) });\nconst sampled = sampleTransport({ rate: 0.1, transport: remote });\n```\n\n### redactTransport(options)\n\n```ts\nredactTransport(options: RedactTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| ------------- | ------------------- | -------------- | ------------------------------------------------ |\n| `keys` | `readonly string[]` | required | Exact field names redacted at any inspected depth |\n| `transport` | `Transport` | required | Downstream transport |\n| `maxDepth` | `number` | `20` | Maximum nested object depth inspected |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement for keys and deeper subtrees |\n\n**Returns:** A non-mutating redaction `Transport`. When `maxDepth` is exceeded, the whole deeper subtree is replaced; uninspected values are never forwarded.\n\n```ts\nimport { jsonTransport, redactTransport } from '@vielzeug/rune';\n\nconst safeJson = redactTransport({ keys: ['password', 'token'], transport: jsonTransport() });\n```\n\n## Utilities\n\n### isLevelEnabled(threshold, level)\n\n```ts\nisLevelEnabled(threshold: LogLevel, level: LogLevel): boolean\n```\n\nReturns `true` when `level` is at or above `threshold`. Always returns `false` when `level` is `'off'`. Useful for building custom transports that respect level filtering.\n\n```ts\nimport { isLevelEnabled } from '@vielzeug/rune';\n\nisLevelEnabled('warn', 'error'); // true\nisLevelEnabled('warn', 'info'); // false\nisLevelEnabled('debug', 'off'); // false\n```\n\n### resolveTheme(override?)\n\n```ts\nresolveTheme(override: ConsoleTheme | undefined): ResolvedTheme\n```\n\nDeep-merges a partial `ConsoleTheme` override onto `DEFAULT_THEME`. Returns a fully-populated `ResolvedTheme` where every level and every field is present. Used internally by `consoleTransport()` — call directly when building a custom transport that needs to honour theme overrides.\n\n```ts\nimport { resolveTheme } from '@vielzeug/rune';\n\nconst theme = resolveTheme({ warn: { badge: '⚡' } });\n// theme.warn.badge === '⚡', theme.warn.bg === DEFAULT_THEME.warn.bg (unchanged)\n```\n\n### DEFAULT_THEME\n\nThe built-in badge and namespace colour definitions used by `consoleTransport()`. Override per-transport via `ConsoleTransportOptions.theme`.\n\n### PRIORITY\n\n```ts\nPRIORITY: Record<LogLevel, number>\n```\n\nNumeric priority for each level (`debug: 0`, `info: 1`, `warn: 2`, `error: 3`, `fatal: 4`, `off: 5`) — lower is more verbose. Exported for transport authors building custom level-comparison logic; `isLevelEnabled()` is built directly on top of it.\n\n## Types\n\n### LogType\n\n`'debug' | 'error' | 'fatal' | 'info' | 'warn'`\n\n### LogLevel\n\n`LogType | 'off'` — threshold order: `debug < info < warn < error < fatal < off`\n\n### Bindings\n\n`Record<string, unknown>` — Key-value context pinned via `withBindings()` or passed per-call.\n\n### LogEntry\n\nThe structured record produced by every log call and dispatched to all transports.\n\n| Field | Type | Description |\n| ----------- | -------------------- | ------------------------------------------------------------------------ |\n| `data` | `Readonly<Bindings>` | Merged result of pinned bindings and per-call context — already resolved |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string` | Effective namespace at time of call |\n| `timestamp` | `Date` | Exact moment of the call, shared across transports |\n\n### Transport\n\n```ts\ntype Transport = (entry: LogEntry) => void;\n```\n\nReceives every `LogEntry` that passes the logger's level threshold. Responsible for its own formatting, delivery, and per-transport level filtering.\n\n### ConsoleThemeEntry\n\n```ts\ntype ConsoleThemeEntry = {\n badge: string;\n bg: string;\n border: string;\n color: string;\n};\n```\n\nPer-level style definition for the console transport. All fields are optional when providing a level override — unspecified fields fall back to the default theme.\n\n### ConsoleTheme\n\n```ts\ntype ConsoleTheme = Partial<Record<LogType | 'group' | 'ns', Partial<ConsoleThemeEntry>>>;\n```\n\nPartial theme overrides merged on top of the default theme. Each level entry is also partial — only specify the fields you want to change.\n\n### ResolvedTheme\n\n`Record<LogType | 'group' | 'ns', ConsoleThemeEntry>` — fully resolved theme with all fields populated.\n\n### RuneOptions\n\n| Field | Type | Default | Description |\n| ------------ | -------------- | ---------------------- | ---------------------------- |\n| `logLevel` | `LogLevel?` | `'debug'` | Logger level threshold |\n| `namespace` | `string?` | `''` | Namespace prefix |\n| `transports` | `Transport[]?` | `[consoleTransport()]` | Transport pipeline |\n| `middleware` | `LogMiddleware[]?` | `[]` | Pre-dispatch transform/filter pipeline |\n| `bindings` | `Bindings?` | `{}` | Initial pinned bindings |\n\n### LogMethod\n\n```ts\ntype LogMethod = {\n (message: string, context?: Bindings): void;\n (context: Bindings, message?: string): void;\n (error: Error, message?: string): void;\n (error: Error, context: Bindings, message?: string): void;\n};\n```\n\nMessage-first, context-first, and Error-first calls are supported. Top-level `Error` values are serialized to `{ message, name, stack }`.\n\n### LazyBinding\n\nOpaque type returned by `lazy()`. Pass as a value inside `withBindings()`. The factory is only called when the entry is actually emitted (after the level check passes).\n\n### Logger\n\nThe full interface returned by `createLogger()`:\n\n```ts\ntype Logger = {\n [Symbol.dispose]: () => void;\n readonly bindings: Readonly<Bindings>;\n child: (overrides?: RuneOptions) => Logger;\n debug: LogMethod;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n enabled: (type: LogLevel) => boolean;\n error: LogMethod;\n fatal: LogMethod;\n group: <T>(label: string, fn: () => T, level?: LogType) => T;\n groupCollapsed: <T>(label: string, fn: () => T, level?: LogType) => T;\n info: LogMethod;\n readonly logLevel: LogLevel;\n readonly middleware: readonly LogMiddleware[];\n readonly namespace: string;\n time: <T>(label: string, fn: () => T, level?: LogType) => T;\n readonly transports: readonly Transport[];\n use: (middleware: LogMiddleware) => Logger;\n warn: LogMethod;\n withBindings: (bindings: Bindings) => Logger;\n};\n```\n\n### ConsoleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n### JsonTransportOptions\n\n| Field | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | ------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names (e.g. `level: 'severity'` for Datadog) |\n\n### LogMiddleware\n\n```ts\ntype LogMiddleware = (entry: LogEntry) => LogEntry | null;\n```\n\nTransforms an entry before all transports or returns `null` to drop it.\n\n### RemoteLogData and RemoteTransportOptions\n\n```ts\ntype RemoteLogData = {\n data?: Readonly<Bindings>;\n env: 'development' | 'production';\n level: LogType;\n message?: string;\n namespace?: string;\n timestamp: string;\n};\n\ntype RemoteTransportOptions = {\n env?: 'development' | 'production';\n handler: (type: LogType, data: RemoteLogData) => void | Promise<unknown>;\n level?: LogLevel;\n onError?: (error: unknown, data: RemoteLogData) => void;\n};\n```\n\n### BatchHandle and BatchTransportOptions\n\n```ts\ntype BatchHandle = {\n [Symbol.asyncDispose]: () => Promise<void>;\n dispose: () => Promise<void>;\n readonly disposed: boolean;\n flush: () => Promise<void>;\n transport: Transport;\n};\n\ntype BatchTransportOptions = {\n interval?: number;\n level?: LogLevel;\n maxBuffer?: number;\n maxSize?: number;\n onFlush: (entries: LogEntry[]) => void | Promise<void>;\n onFlushError?: (entries: LogEntry[], error: unknown) => void;\n};\n```\n\n### SampleTransportOptions\n\n```ts\ntype SampleTransportOptions = {\n level?: LogLevel;\n rate: number;\n transport: Transport;\n};\n```\n\n### RedactTransportOptions\n\n```ts\ntype RedactTransportOptions = {\n keys: readonly string[];\n maxDepth?: number;\n replacement?: string;\n transport: Transport;\n};\n```\n\nDepth-limited subtrees are replaced entirely so sensitive values are never forwarded uninspected.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `RuneError` | Base class for all rune-originated errors | `instanceof RuneError` narrows all Rune errors |\n| `RuneConfigError` | Logger or transport configured with invalid options | — |\n",
6
- "usage": "---\ntitle: Rune — Usage Guide\ndescription: Configuration, transports, scoped loggers, lazy bindings, timers, groups, and best practices for Rune.\n---\n\n[[toc]]\n\n::: tip New to Rune?\nStart with the [Overview](./index.md), then use this page for detailed usage patterns.\n:::\n\n## Basic Usage\n\nUse `createLogger()` to create an isolated logger instance.\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\n\nconst appLog = createLogger();\nconst apiLog = createLogger({ namespace: 'api' });\nconst authLog = createLogger('auth'); // shorthand namespace\n```\n\nEach `createLogger()` call is fully independent with its own transport pipeline.\n\nThe two-arg shorthand combines namespace and options cleanly:\n\n```ts\nconst log = createLogger('api', { logLevel: 'warn', transports: [transport] });\n```\n\n## Transports\n\nTransports are the delivery layer. Every `LogEntry` that passes the logger's level threshold is dispatched to each transport in order. Transports handle their own formatting, level filtering, and delivery.\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n transports: [\n consoleTransport({ timestamp: true }),\n jsonTransport({ level: 'error' }),\n ],\n});\n```\n\nWhen `transports` is omitted, `consoleTransport()` is used automatically.\n\n### Built-in Transport Factories\n\n| Factory | Use case |\n| -------------------- | --------------------------------------------- |\n| `consoleTransport()` | Styled console output (default) |\n| `jsonTransport()` | NDJSON for server-side log aggregation |\n| `remoteTransport()` | Asynchronous browser or server delivery |\n| `batchTransport()` | Serialized buffered delivery with flush |\n| `sampleTransport()` | Probabilistic volume reduction |\n| `redactTransport()` | Recursive sensitive-field and subtree masking |\n\n### Node.js: Structured JSON Logging\n\nFor server-side log pipelines (ELK, Datadog, CloudWatch), `jsonTransport` emits NDJSON to stdout:\n\n```ts\nimport { jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info('request', { path: '/users', status: 200 });\n// Outputs: {\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"path\":\"/users\",\"status\":200,\"msg\":\"request\"}\n```\n\n### Remote Delivery, Batching, and Redaction\n\nCompose transport wrappers around the delivery boundary. Handle remote failures explicitly in production and dispose the batch during shutdown.\n\n```ts\nimport { batchTransport, createLogger, redactTransport, remoteTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({\n handler: (_level, payload) =>\n fetch('/api/logs', { body: JSON.stringify(payload), method: 'POST' }).then((response) => {\n if (!response.ok) throw new Error(`log delivery failed: ${response.status}`);\n }),\n level: 'warn',\n onError: (error) => console.error('log delivery failed', error),\n});\nconst batch = batchTransport({\n onFlush: async (entries) => {\n const response = await fetch('/api/log-batches', { body: JSON.stringify(entries), method: 'POST' });\n if (!response.ok) throw new Error(`batch delivery failed: ${response.status}`);\n },\n});\nconst log = createLogger({\n transports: [redactTransport({ keys: ['password', 'token'], transport: remote })],\n});\nconst batchedLog = createLogger({\n transports: [redactTransport({ keys: ['password', 'token'], transport: batch.transport })],\n});\n\nlog.error('direct delivery');\nbatchedLog.error('buffered delivery');\nawait batch.dispose();\n```\n\n`redactTransport()` fails closed: when `maxDepth` is exceeded, the entire deeper subtree is replaced rather than forwarded uninspected.\n\n## Configuration\n\nUse `child()` to derive immutable logger variants.\n\n```ts\nconst log = createLogger({ namespace: 'app' });\nconst AppLog = log.child({\n logLevel: 'warn',\n namespace: 'App',\n // transports inherited from parent by default\n // pass transports: [] to disable all, or transports: [...] to replace\n});\n\n// Individual getters — no config snapshot\nconsole.log(AppLog.logLevel); // 'warn'\nconsole.log(AppLog.namespace); // 'App'\nconsole.log(AppLog.transports); // [...]\n```\n\nLevel threshold order: `debug` < `info` < `warn` < `error` < `fatal` < `off`\n\n## Middleware\n\nUse middleware for transformations or filters that must apply once before every transport. Middleware runs in order; returning `null` drops the entry.\n\n```ts\nconst log = createLogger({\n middleware: [(entry) => ({ ...entry, data: { ...entry.data, environment: 'production' } })],\n transports: [consoleTransport(), jsonTransport()],\n});\n\nconst errorsOnly = log.use((entry) => (entry.level === 'error' || entry.level === 'fatal' ? entry : null));\n```\n\n`use()` returns a new logger and leaves its parent unchanged. A throwing middleware is isolated and drops only the affected entry.\n\n## Call Signature\n\nAll log methods support message-first, context-first, and error-first calls:\n\n```ts\nlog.info('message');\nlog.info('message', { key: 'value' });\nlog.debug({ type: 'dispatch', id: 42 }, 'bus:dispatch');\nlog.error(new Error('boom'), { requestId: 'abc' }, 'request failed');\n```\n\nUse message-first for ordinary application logs. Context-first avoids synthetic messages for structured events and matches observation callbacks such as `tap((event) => log.debug(event))`. Error-first automatically stores the serialized error under `err`.\n\nPer-call context is shallow-merged with `withBindings()` bindings into `entry.data`. `Error` values are serialized to `{ message, name, stack }`.\n\n## Logging Methods\n\n```ts\nconst log = createLogger();\nlog.debug('debug details');\nlog.info('server started', { port: 3000 });\nlog.warn('cache stale');\nlog.error('request failed', { err: new Error('timeout') }); // Error auto-serialized in context\nlog.fatal('terminating', { service: 'db' }); // above error, use for unrecoverable state\n```\n\nUse `enabled()` to avoid expensive payload construction before the level check:\n\n```ts\nif (log.enabled('debug')) {\n log.debug('diagnostics', { diagnostics: buildLargePayload() });\n}\n```\n\nOr use `lazy()` to let Rune gate it automatically:\n\n```ts\nconst reqLog = log.withBindings({ diagnostics: lazy(() => buildLargePayload()) });\nreqLog.debug('diagnostics'); // buildLargePayload() only called when debug is enabled\n```\n\n## Pinned Bindings\n\n`withBindings(fields)` returns a child logger where the given fields are merged into every log call. This is the idiomatic way to attach per-request or per-user context.\n\n```ts\nconst api = createLogger({ namespace: 'api' });\n\nconst reqLog = api.withBindings({ requestId: 'abc-123', userId: 42 });\nreqLog.info('GET /users'); // always includes requestId and userId\nreqLog.warn('query took 2s', { slow: true }); // call-site fields merged in\n```\n\nThe parent logger is not affected. Bindings stack additively through chained `withBindings()` calls:\n\n```ts\nconst base = log.withBindings({ service: 'api' });\nconst req = base.withBindings({ requestId: 'xyz' });\n// req emits both service and requestId on every call\n```\n\nThe `bindings` getter returns a defensive snapshot:\n\n```ts\nconsole.log(reqLog.bindings); // { requestId: 'abc-123', userId: 42 }\n```\n\n## Lazy Bindings\n\n`lazy(fn)` defers evaluation of a binding value until after the level check passes. The factory is never called when the entry would be suppressed.\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst log = createLogger().withBindings({\n // Only called when debug entries are emitted\n snapshot: lazy(() => JSON.stringify(getFullAppState())),\n // Regular values are always included as-is\n service: 'api',\n});\n\nlog.debug('state trace'); // snapshot() only called here\nlog.warn('cache miss'); // snapshot() NOT called — warn doesn't need it\n```\n\nLazy bindings are resolved on every emitted call, not cached:\n\n```ts\nconst counter = { n: 0 };\nconst log = createLogger().withBindings({ tick: lazy(() => ++counter.n) });\n\nlog.info('a'); // tick: 1\nlog.info('b'); // tick: 2\n```\n\n## Child Loggers\n\n`child(overrides?)` creates a new logger scoped to a namespace, level, or transport set. Use it to create module-level or service-level loggers.\n\n```ts\nconst api = createLogger({ namespace: 'api' });\nconst auth = api.child({ namespace: 'auth' }); // → 'api.auth' (dot-joined automatically)\n\napi.info('GET /users');\nauth.warn('token expiring');\n```\n\n`child(overrides?)` clones current config and applies overrides. Transports are inherited by default.\n\n```ts\nconst base = createLogger({ logLevel: 'info', namespace: 'app' });\nconst verbose = base.child({ logLevel: 'debug' }); // inherits transports\n\n// Replace transports entirely on the child\nconst silent = base.child({ transports: [] }); // no output\n\n// Override with a different transport set\nconst jsonChild = base.child({ transports: [jsonTransport()] });\n```\n\nChild and parent configs remain independent after creation.\n\n## Timing\n\n`time(label, fn, level?)` measures execution time of sync or async functions. Emits a structured entry with `{ duration_ms }` in `data` and `label` as the message. When `fn` throws or rejects, the entry also includes `{ err }` with the serialized error.\n\n```ts\n// Sync\nconst result = log.time('parse', () => parseDocument(input));\n// Emits: { level: 'debug', message: 'parse', data: { duration_ms: 2.4 } }\n\n// Async\nconst users = await log.time('db.users', () => db.query('SELECT * FROM users'));\n// Emits even on rejection, with { err } included in data\n\n// Custom level\nlog.time('health-check', () => ping(), 'info');\n\n// Skipped when logLevel is 'off', but fn still executes\n```\n\n## Groups\n\n`group(label, fn, level?)` and `groupCollapsed(label, fn, level?)` wrap a callback in a console group, ensuring `groupEnd` is called even when the callback throws or rejects.\n\n```ts\nawait log.groupCollapsed('Job', async () => {\n await log.time('process', () => runJob());\n log.info('Done');\n});\n\n// Gate the group header on a log level — suppresses when logLevel is above 'debug'\nlog.group(\n 'verbose trace',\n () => {\n log.debug('internal state', { state });\n },\n 'debug',\n);\n```\n\nWhen `logLevel` is `'off'`, the group wrapper is bypassed but the callback still executes. When a `level` is provided and it is below the configured threshold, the group header is skipped but the callback still runs.\n\n## Testing\n\nUse a test transport to assert log entries without mocking `console`. This approach is more robust and does not require spy cleanup:\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createLogger } from '@vielzeug/rune';\nimport type { LogEntry, Transport } from '@vielzeug/rune';\n\nfunction createTestTransport() {\n const entries: LogEntry[] = [];\n const transport: Transport = (entry) => entries.push(entry);\n return { entries, transport };\n}\n\nit('logs errors when enabled', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'error', transports: [transport] });\n\n log.error('boom');\n\n expect(entries).toHaveLength(1);\n expect(entries[0].level).toBe('error');\n expect(entries[0].message).toBe('boom');\n});\n\nit('suppresses debug when logLevel is warn', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'warn', transports: [transport] });\n\n log.debug('silent');\n log.warn('loud');\n\n expect(entries).toHaveLength(1);\n});\n```\n\nYou can still spy on `console` methods when testing `consoleTransport` output directly:\n\n```ts\nimport { afterEach, expect, it, vi } from 'vitest';\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\n\nafterEach(() => vi.restoreAllMocks());\n\nit('writes error to console.error', () => {\n const spy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const log = createLogger({ logLevel: 'error', transports: [consoleTransport({ timestamp: false })] });\n\n log.error('boom');\n\n expect(spy).toHaveBeenCalled();\n});\n```\n\n## Framework Integration\n\nRune is framework-agnostic and works as a module-level singleton or a context-injected instance.\n\n::: code-group\n\n```tsx [React]\nimport { createContext, useState, useContext } from 'react';\nimport { createLogger } from '@vielzeug/rune';\n\nconst LogContext = createContext(createLogger({ namespace: 'app' }));\n\nfunction useLogger() {\n return useContext(LogContext);\n}\n\nfunction App() {\n const [requestLogger] = useState(() => createLogger({ namespace: 'app' }).withBindings({ userId: '42' }));\n return (\n <LogContext.Provider value={requestLogger}>\n <Dashboard />\n </LogContext.Provider>\n );\n}\n\nfunction Dashboard() {\n const log = useLogger();\n log.info('Dashboard mounted');\n return <div>Dashboard</div>;\n}\n```\n\n```ts [Vue 3]\nimport { inject, provide } from 'vue';\nimport { createLogger, type Logger } from '@vielzeug/rune';\n\nconst LoggerKey = Symbol('logger');\n\nfunction provideLogger(namespace: string) {\n const logger = createLogger({ namespace });\n provide(LoggerKey, logger);\n return logger;\n}\n\nfunction useLogger(): Logger {\n const logger = inject<Logger>(LoggerKey);\n if (!logger) throw new Error('Logger not provided');\n return logger;\n}\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { setContext, getContext } from 'svelte';\n import { createLogger } from '@vielzeug/rune';\n\n const logger = createLogger({ namespace: 'app' });\n setContext('logger', logger);\n</script>\n\n<!-- Child component -->\n<script lang=\"ts\">\n import { getContext } from 'svelte';\n import type { Logger } from '@vielzeug/rune';\n\n const logger = getContext<Logger>('logger');\n logger.info('component mounted');\n</script>\n```\n\n:::\n\n### Pitfalls\n\n- **React:** Creating the logger without a stable initializer recreates it on every re-render. Use `useState(() => createLogger(...))`.\n- **Vue 3:** `inject()` must be called at the top level of `setup()`, not inside callbacks.\n- **Svelte:** `getContext()` must be called synchronously during component initialization.\n\n## Working with Other Vielzeug Libraries\n\n### With Courier\n\n```ts\nimport { createCourier, withLogging } from '@vielzeug/courier';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'courier' });\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withLogging({ logger: (message, meta) => log.debug(message, meta) })],\n});\n```\n\n### With Herald\n\n```ts\nimport { createBus } from '@vielzeug/herald';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'bus' });\nconst bus = createBus<AppEvents>({\n onDispatch: (event, payload) => log.debug('dispatched', { event, payload }),\n onError: (err, event) => log.error(`handler error in \"${event}\"`, { err }),\n});\n```\n\n## Best Practices\n\n- Create one child logger per module boundary using `createLogger('module.name')` or `log.child({ namespace: 'module.name' })`.\n- Use `withBindings()` to pin request/session context instead of repeating fields on each call.\n- Use `lazy()` for expensive diagnostics bindings only needed at `debug` level.\n- Set `logLevel` from environment (`'debug'` in dev, `'warn'` or `'error'` in prod).\n- Use `enabled()` before expensive payload construction that `lazy()` cannot defer.\n- Configure transports at the application root; pass scoped loggers via DI or context.\n- To style console output, pass `consoleTransport({ theme })` explicitly in `transports`.\n- Use `fatal()` only for genuinely unrecoverable states.\n",
7
- "examples": "---\ntitle: Rune — Examples\ndescription: Practical examples and recipes for rune.\n---\n\n## Examples\n\n- [Module Logger Pattern](./examples/module-logger-pattern.md)\n- [Child Logger Overrides](./examples/child-logger-overrides.md)\n- [Production Setup](./examples/production-setup.md)\n- [Timing And Grouping](./examples/timing-and-grouping.md)\n- [React Integration](./examples/react-integration.md)\n- [Request Middleware](./examples/request-middleware.md)\n- [Testing](./examples/testing.md)\n"
4
+ "index": "---\ntitle: 'Rune: Structured logging for TypeScript'\ndescription: Browser/Node logger with levels, namespaces, pluggable transports, lazy bindings, and timing helpers.\npackage: rune\ncategory: logging\nkeywords: [logging, console, structured, scoped, transports, remote-logging, levels, namespaces, lazy-bindings]\nrelated: [courier, herald, familiar]\nexports:\n [\n createLogger,\n consoleTransport,\n jsonTransport,\n remoteTransport,\n batchTransport,\n sampleTransport,\n redactTransport,\n lazy,\n isLevelEnabled,\n resolveTheme,\n DEFAULT_THEME,\n PRIORITY,\n RuneError,\n RuneConfigError,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"rune\" />\n\n## Why Rune?\n\nPlain `console.log` lacks structure: no log levels, no namespacing, no remote delivery, no way to silence logs in production.\n\n```ts\n// Before: manual approach\nconst path = '/users';\nconsole.log(`[api] GET ${path}`);\n\n// After: Rune\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst api = createLogger({\n namespace: 'api',\n transports: [\n consoleTransport({ level: 'debug' }),\n jsonTransport({ level: 'error' }),\n ],\n});\n\napi.info('request', { method: 'GET', path });\n```\n\n| Feature | Rune | Winston | Pino | console |\n| -------------------- | ------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"rune\" type=\"size\" /> | ~44 kB | ~4 kB | 0 kB |\n| Browser support | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Scoped loggers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | Child | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Pluggable transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Built-in factories | <ore-icon name=\"check\" size=\"16\"></ore-icon> Transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Streams | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Structured log entry | <ore-icon name=\"check\" size=\"16\"></ore-icon> `LogEntry` type | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Lazy bindings | <ore-icon name=\"check\" size=\"16\"></ore-icon> `lazy(fn)` | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Styled output | <ore-icon name=\"check\" size=\"16\"></ore-icon> CSS badges | Text only | Text only | Manual |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> (15+) | <ore-icon name=\"x\" size=\"16\"></ore-icon> (5+) | N/A |\n\n<div class=\"decision-callout\">\n\n**Use Rune when** you need isomorphic logging (browser + Node.js), namespaced module loggers, or remote error delivery without a heavy dependency chain.\n\n**Consider alternatives when** you need high-throughput file-based logging (Pino), file rotation (Winston), or your team already uses a logging framework.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/rune\n```\n\n```sh [npm]\nnpm install @vielzeug/rune\n```\n\n```sh [yarn]\nyarn add @vielzeug/rune\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport, lazy } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'server',\n transports: [\n consoleTransport({ timestamp: true }),\n jsonTransport({ level: 'error' }),\n ],\n});\n\nconst requestLog = log.withBindings({\n diagnostics: lazy(() => ({ queueDepth: 0 })),\n requestId: 'abc-123',\n});\n\nrequestLog.info('request', { method: 'GET', path: '/users' });\nconst users = await requestLog.time('load users', () => Promise.resolve(['user-1']));\nconsole.log(users);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Level filtering (`debug` to `off`) with `enabled()` checks, including `fatal` above `error`\n- Immutable config after construction: use `child()` or `withBindings()` to scope\n- Message-first, context-first, and Error-first log calls for application code and adapters\n- `Error` values are auto-serialized to `{ message, name, stack }`: survives JSON.stringify\n- Pinned context bindings via `withBindings({ requestId })`: fields on every line\n- Lazy bindings via `lazy(fn)`: expensive computations gated behind the level check\n- Namespaced child loggers via `createLogger('name')` or `logger.child({ namespace })`\n- Pluggable transport pipeline: console, JSON, remote, batching, sampling, and fail-closed redaction\n- Immutable middleware transforms and filters applied before every transport\n- Structured `time()` wrapper: emits the label as message with `{ duration_ms }` in context\n- `group()` and `groupCollapsed()` wrappers that auto-close on throw/reject\n- `LogEntry.data`: single merged flat object for transports; no manual merging needed\n- Zero dependencies: <PackageInfo package=\"rune\" type=\"size\" /> gzipped\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Courier](/courier/): HTTP client with built-in request/response interception; pipe Rune as a transport to log every API call with structured context\n- [Herald](/herald/): typed event bus; emit log-level change or flush events across modules without coupling loggers directly\n- [Familiar](/familiar/): Web Worker pool; use Rune inside task functions to surface structured worker-side logs back to the main thread\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: 'Rune: API Reference'\ndescription: API reference for @vielzeug/rune exports, logger methods, configuration types, and transport factories.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------- | ------------------------------------------------ | -------------- | ------------------------------------------------------------ |\n| `createLogger()` | Create an isolated `Logger` instance | Sync | Omitting `transports` defaults to `consoleTransport()` |\n| `lazy(fn)` | Defer a binding value past the level check | Sync | Factory runs on every emit, not once |\n| `isLevelEnabled()` | Utility: test whether a level passes a threshold | Sync | `'off'` always returns `false` |\n| `PRIORITY` | Numeric priority table backing `isLevelEnabled()`| N/A | Lower number = more verbose |\n| `resolveTheme()` | Merge a partial theme onto the default | Sync | Returns a fully-populated `ResolvedTheme` |\n| `consoleTransport()` | Styled console output | Sync | Theme is resolved once at factory call, not per entry |\n| `jsonTransport()` | NDJSON to stdout or a custom sink | Sync | `process.stdout` is unavailable in browsers |\n| `remoteTransport()` | Asynchronous remote delivery | Async | Pass `onError` for production failure observability |\n| `batchTransport()` | Serialized buffered delivery | Async | Await `dispose()` during shutdown |\n| `sampleTransport()` | Probabilistic forwarding | Sync | Validate the downstream delivery path independently |\n| `redactTransport()` | Recursive sensitive-field masking | Sync | Deeper subtrees are replaced when `maxDepth` is exceeded |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------- | -------------------------------------------------------- |\n| `@vielzeug/rune` | All exports: logger, transport factories, `lazy`, types |\n\n## createLogger(initial?, options?)\n\nCreates an isolated logger instance.\n\n```ts\ncreateLogger(namespace: string, options?: Omit<RuneOptions, 'namespace'>): Logger\ncreateLogger(options?: RuneOptions): Logger\n```\n\n- `string` shorthand sets namespace: `createLogger('api')` or `createLogger('api', { logLevel: 'warn' })`.\n- Each call produces a fully independent instance: no shared mutable state.\n- Default transport is `consoleTransport()` when `transports` is omitted.\n\n> **Note: disposed loggers:** after `dispose()` is called, all log methods (`debug`, `info`, `warn`, `error`, `fatal`), `time()`, and `group()` / `groupCollapsed()` silently no-op. The `fn` callback in `group()` still runs: only the group header is suppressed.\n\n> **Note: fault isolation:** if middleware throws, that entry is dropped; if a transport throws synchronously, sibling transports still run. Asynchronous delivery failures belong to the transport and should be observed through options such as `remoteTransport({ onError })`.\n\n**Returns:** `Logger`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({ logLevel: 'warn', namespace: 'app' });\n\nconst serverLog = createLogger({\n namespace: 'server',\n transports: [\n consoleTransport(),\n jsonTransport({ level: 'error' }),\n ],\n});\n```\n\n## lazy(fn)\n\nDefers evaluation of an expensive binding value until after the level check passes.\nThe factory function is never called when the log level suppresses the entry.\n\n```ts\nlazy(fn: () => unknown): LazyBinding\n```\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst reqLog = log.withBindings({\n diagnostics: lazy(() => buildExpensiveDiagnostics()),\n});\n\nreqLog.debug('trace'); // diagnostics() only called when debug is enabled\n```\n\n**Returns:** `LazyBinding`\n\n## Logger Methods\n\n### Logging\n\nAll five methods use `LogMethod`:\n\n```ts\n(message: string, context?: Bindings): void\n(context: Bindings, message?: string): void\n(error: Error, message?: string): void\n(error: Error, context: Bindings, message?: string): void\n```\n\nMessage-first is preferred for ordinary logs. Context-first supports structured events and adapters. Error-first stores the serialized error under `err`.\n\n```ts\nlog.info('request started', { requestId: 'abc' });\nlog.debug({ type: 'dispatch' }, 'bus:dispatch');\nlog.error(new Error('timeout'), { requestId: 'abc' }, 'request failed');\n```\n\n### Composition\n\n| Method | Returns | What it does |\n| ---------------------- | -------- | ---------------------------------------------------------------- |\n| `child(overrides?)` | `Logger` | Clones config, applies overrides, inherits bindings |\n| `use(middleware)` | `Logger` | Adds an immutable pre-dispatch transform or filter |\n| `withBindings(fields)` | `Logger` | Pins fields to every subsequent call, returns a new child logger |\n\n`child()` transport inheritance:\n\n- Omit `transports` → inherit parent transports (default).\n- Pass `transports: []` → disable all transports on the child.\n- Pass `transports: [...]` → replace entirely with the given list.\n\n`child()` namespace joining:\n\n- `parent.child({ namespace: 'auth' })` on a logger with namespace `'api'` produces `'api.auth'`.\n- Omit `namespace` → inherits parent namespace unchanged.\n\n### Utilities\n\n| Method | Returns | Description |\n| ----------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `enabled(level)` | `boolean` | True if entries at this level pass the configured threshold |\n| `time(label, fn, level?)` | `T` | Measures sync/async execution; emits at `level` (default `'debug'`), label as message, `{ duration_ms }` in `data`. When `fn` throws or rejects, `{ err }` is also included. |\n| `group(label, fn, level?)` | `T` | Wraps callback in `console.group`; closes even on throw/reject. Pass `level` to gate the group header on the configured threshold (e.g. `'debug'` suppresses when `logLevel` is `'warn'`). |\n| `groupCollapsed(label, fn, level?)` | `T` | Same as `group`, using `console.groupCollapsed`. |\n| `dispose()` | `void` | Silences all subsequent log calls on this logger instance. Idempotent. |\n\n### Properties\n\n| Property | Type | Description |\n| ------------------ | -------------------------- | ------------------------------------------------------------------ |\n| `logLevel` | `LogLevel` | Active log level threshold |\n| `namespace` | `string` | Effective namespace string |\n| `transports` | `readonly Transport[]` | Transport pipeline snapshot |\n| `middleware` | `readonly LogMiddleware[]` | Middleware pipeline snapshot |\n| `bindings` | `Readonly<Bindings>` | Snapshot of currently pinned fields |\n| `disposalSignal` | `AbortSignal` | Aborted when `dispose()` is called. Use to tie external lifetimes. |\n| `disposed` | `boolean` | `true` after `dispose()` has been called |\n| `[Symbol.dispose]` | `() => void` | Delegates to `dispose()`. Enables `using` declarations. |\n\n## Transport Factories\n\n### consoleTransport(options?)\n\n```ts\nconsoleTransport(options?: ConsoleTransportOptions): Transport\n```\n\nWrites styled output to the browser console (CSS badges) or Node terminal (plain text). This is the default transport.\n\n| Option | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | N/A | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | N/A | Override default badge colours for this transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\nimport { inspect } from 'node:util';\n\nconst log = createLogger({\n transports: [consoleTransport({ level: 'info', timestamp: true, inspectFn: inspect })],\n});\n```\n\n### jsonTransport(options?)\n\n```ts\njsonTransport(options?: JsonTransportOptions): Transport\n```\n\nOutputs newline-delimited JSON (NDJSON) to `stdout` or a custom function. Useful for server-side log aggregation pipelines (ELK, Datadog, etc.).\n\nEach line is a flat JSON object with `level`, `time` (ISO), and optional `ns`, `msg`, plus all merged context fields.\n\n| Option | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | -------------------------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | N/A | Custom output field names for aggregator compatibility (e.g. `'severity'` for Datadog) |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info('request', { path: '/users', status: 200 });\n// {\"path\":\"/users\",\"status\":200,\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"msg\":\"request\"}\n```\n\n### remoteTransport(options)\n\n```ts\nremoteTransport(options: RemoteTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| --------- | ------------------------------------------------------------- | ------------- | ------------------------------ |\n| `handler` | `(level, data) => void \\| Promise<unknown>` | required | Remote delivery function |\n| `level` | `LogLevel` | `'debug'` | Minimum forwarded level |\n| `env` | `'development' \\| 'production'` | auto-detected | Payload environment |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | dev warning | Delivery failure observer |\n\n**Returns:** A fire-and-forget `Transport`. Pass `onError` whenever production delivery failures require monitoring.\n\n```ts\nimport { remoteTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({\n handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' }),\n onError: (error) => console.error('log delivery failed', error),\n});\n```\n\n### batchTransport(options)\n\n```ts\nbatchTransport(options: BatchTransportOptions): BatchHandle\n```\n\n| Option | Type | Default | Description |\n| -------------- | --------------------------------------------- | --------- | ----------------------------------- |\n| `onFlush` | `(entries) => void \\| Promise<void>` | required | Serialized batch delivery |\n| `onFlushError` | `(entries, error) => void` | N/A | Delivery failure observer |\n| `interval` | `number` | `5000` | Flush interval in milliseconds |\n| `maxSize` | `number` | `50` | Entries that trigger an early flush |\n| `maxBuffer` | `number` | unbounded | Hard in-memory limit |\n| `level` | `LogLevel` | `'debug'` | Minimum buffered level |\n\n**Returns:** `BatchHandle` with `transport`, `flush()`, `dispose()`, `disposed`, and `[Symbol.asyncDispose]`. Await `dispose()` during shutdown.\n\n```ts\nimport { batchTransport, createLogger } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n onFlush: (entries) => fetch('/api/log-batches', { body: JSON.stringify(entries), method: 'POST' }),\n});\nconst log = createLogger({ transports: [batch.transport] });\nawait batch.dispose();\n```\n\n### sampleTransport(options)\n\n```ts\nsampleTransport(options: SampleTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| ----------- | ----------- | --------- | ------------------------------ |\n| `rate` | `number` | required | Forwarding fraction from 0–1 |\n| `transport` | `Transport` | required | Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level before sampling |\n\n**Returns:** A sampling `Transport`.\n\n```ts\nimport { remoteTransport, sampleTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({ handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data) }) });\nconst sampled = sampleTransport({ rate: 0.1, transport: remote });\n```\n\n### redactTransport(options)\n\n```ts\nredactTransport(options: RedactTransportOptions): Transport\n```\n\n| Option | Type | Default | Description |\n| ------------- | ------------------- | -------------- | ------------------------------------------------ |\n| `keys` | `readonly string[]` | required | Exact field names redacted at any inspected depth |\n| `transport` | `Transport` | required | Downstream transport |\n| `maxDepth` | `number` | `20` | Maximum nested object depth inspected |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement for keys and deeper subtrees |\n\n**Returns:** A non-mutating redaction `Transport`. When `maxDepth` is exceeded, the whole deeper subtree is replaced; uninspected values are never forwarded.\n\n```ts\nimport { jsonTransport, redactTransport } from '@vielzeug/rune';\n\nconst safeJson = redactTransport({ keys: ['password', 'token'], transport: jsonTransport() });\n```\n\n## Utilities\n\n### isLevelEnabled(threshold, level)\n\n```ts\nisLevelEnabled(threshold: LogLevel, level: LogLevel): boolean\n```\n\nReturns `true` when `level` is at or above `threshold`. Always returns `false` when `level` is `'off'`. Useful for building custom transports that respect level filtering.\n\n```ts\nimport { isLevelEnabled } from '@vielzeug/rune';\n\nisLevelEnabled('warn', 'error'); // true\nisLevelEnabled('warn', 'info'); // false\nisLevelEnabled('debug', 'off'); // false\n```\n\n### resolveTheme(override?)\n\n```ts\nresolveTheme(override: ConsoleTheme | undefined): ResolvedTheme\n```\n\nDeep-merges a partial `ConsoleTheme` override onto `DEFAULT_THEME`. Returns a fully-populated `ResolvedTheme` where every level and every field is present. Used internally by `consoleTransport()`: call directly when building a custom transport that needs to honour theme overrides.\n\n```ts\nimport { resolveTheme } from '@vielzeug/rune';\n\nconst theme = resolveTheme({ warn: { badge: '⚡' } });\n// theme.warn.badge === '⚡', theme.warn.bg === DEFAULT_THEME.warn.bg (unchanged)\n```\n\n### DEFAULT_THEME\n\nThe built-in badge and namespace colour definitions used by `consoleTransport()`. Override per-transport via `ConsoleTransportOptions.theme`.\n\n### PRIORITY\n\n```ts\nPRIORITY: Record<LogLevel, number>\n```\n\nNumeric priority for each level (`debug: 0`, `info: 1`, `warn: 2`, `error: 3`, `fatal: 4`, `off: 5`): lower is more verbose. Exported for transport authors building custom level-comparison logic; `isLevelEnabled()` is built directly on top of it.\n\n## Types\n\n### LogType\n\n`'debug' | 'error' | 'fatal' | 'info' | 'warn'`\n\n### LogLevel\n\n`LogType | 'off'`: threshold order: `debug < info < warn < error < fatal < off`\n\n### Bindings\n\n`Record<string, unknown>`: Key-value context pinned via `withBindings()` or passed per-call.\n\n### LogEntry\n\nThe structured record produced by every log call and dispatched to all transports.\n\n| Field | Type | Description |\n| ----------- | -------------------- | ------------------------------------------------------------------------ |\n| `data` | `Readonly<Bindings>` | Merged result of pinned bindings and per-call context: already resolved |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string` | Effective namespace at time of call |\n| `timestamp` | `Date` | Exact moment of the call, shared across transports |\n\n### Transport\n\n```ts\ntype Transport = (entry: LogEntry) => void;\n```\n\nReceives every `LogEntry` that passes the logger's level threshold. Responsible for its own formatting, delivery, and per-transport level filtering.\n\n### ConsoleThemeEntry\n\n```ts\ntype ConsoleThemeEntry = {\n badge: string;\n bg: string;\n border: string;\n color: string;\n};\n```\n\nPer-level style definition for the console transport. All fields are optional when providing a level override: unspecified fields fall back to the default theme.\n\n### ConsoleTheme\n\n```ts\ntype ConsoleTheme = Partial<Record<LogType | 'group' | 'ns', Partial<ConsoleThemeEntry>>>;\n```\n\nPartial theme overrides merged on top of the default theme. Each level entry is also partial: only specify the fields you want to change.\n\n### ResolvedTheme\n\n`Record<LogType | 'group' | 'ns', ConsoleThemeEntry>`: fully resolved theme with all fields populated.\n\n### RuneOptions\n\n| Field | Type | Default | Description |\n| ------------ | -------------- | ---------------------- | ---------------------------- |\n| `logLevel` | `LogLevel?` | `'debug'` | Logger level threshold |\n| `namespace` | `string?` | `''` | Namespace prefix |\n| `transports` | `Transport[]?` | `[consoleTransport()]` | Transport pipeline |\n| `middleware` | `LogMiddleware[]?` | `[]` | Pre-dispatch transform/filter pipeline |\n| `bindings` | `Bindings?` | `{}` | Initial pinned bindings |\n\n### LogMethod\n\n```ts\ntype LogMethod = {\n (message: string, context?: Bindings): void;\n (context: Bindings, message?: string): void;\n (error: Error, message?: string): void;\n (error: Error, context: Bindings, message?: string): void;\n};\n```\n\nMessage-first, context-first, and Error-first calls are supported. Top-level `Error` values are serialized to `{ message, name, stack }`.\n\n### LazyBinding\n\nOpaque type returned by `lazy()`. Pass as a value inside `withBindings()`. The factory is only called when the entry is actually emitted (after the level check passes).\n\n### Logger\n\nThe full interface returned by `createLogger()`:\n\n```ts\ntype Logger = {\n [Symbol.dispose]: () => void;\n readonly bindings: Readonly<Bindings>;\n child: (overrides?: RuneOptions) => Logger;\n debug: LogMethod;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n enabled: (type: LogLevel) => boolean;\n error: LogMethod;\n fatal: LogMethod;\n group: <T>(label: string, fn: () => T, level?: LogType) => T;\n groupCollapsed: <T>(label: string, fn: () => T, level?: LogType) => T;\n info: LogMethod;\n readonly logLevel: LogLevel;\n readonly middleware: readonly LogMiddleware[];\n readonly namespace: string;\n time: <T>(label: string, fn: () => T, level?: LogType) => T;\n readonly transports: readonly Transport[];\n use: (middleware: LogMiddleware) => Logger;\n warn: LogMethod;\n withBindings: (bindings: Bindings) => Logger;\n};\n```\n\n### ConsoleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | N/A | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | N/A | Override default badge colours for this transport |\n\n### JsonTransportOptions\n\n| Field | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | ------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | N/A | Custom output field names (e.g. `level: 'severity'` for Datadog) |\n\n### LogMiddleware\n\n```ts\ntype LogMiddleware = (entry: LogEntry) => LogEntry | null;\n```\n\nTransforms an entry before all transports or returns `null` to drop it.\n\n### RemoteLogData and RemoteTransportOptions\n\n```ts\ntype RemoteLogData = {\n data?: Readonly<Bindings>;\n env: 'development' | 'production';\n level: LogType;\n message?: string;\n namespace?: string;\n timestamp: string;\n};\n\ntype RemoteTransportOptions = {\n env?: 'development' | 'production';\n handler: (type: LogType, data: RemoteLogData) => void | Promise<unknown>;\n level?: LogLevel;\n onError?: (error: unknown, data: RemoteLogData) => void;\n};\n```\n\n### BatchHandle and BatchTransportOptions\n\n```ts\ntype BatchHandle = {\n [Symbol.asyncDispose]: () => Promise<void>;\n dispose: () => Promise<void>;\n readonly disposed: boolean;\n flush: () => Promise<void>;\n transport: Transport;\n};\n\ntype BatchTransportOptions = {\n interval?: number;\n level?: LogLevel;\n maxBuffer?: number;\n maxSize?: number;\n onFlush: (entries: LogEntry[]) => void | Promise<void>;\n onFlushError?: (entries: LogEntry[], error: unknown) => void;\n};\n```\n\n### SampleTransportOptions\n\n```ts\ntype SampleTransportOptions = {\n level?: LogLevel;\n rate: number;\n transport: Transport;\n};\n```\n\n### RedactTransportOptions\n\n```ts\ntype RedactTransportOptions = {\n keys: readonly string[];\n maxDepth?: number;\n replacement?: string;\n transport: Transport;\n};\n```\n\nDepth-limited subtrees are replaced entirely so sensitive values are never forwarded uninspected.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `RuneError` | Base class for all rune-originated errors | `instanceof RuneError` narrows all Rune errors |\n| `RuneConfigError` | Logger or transport configured with invalid options | N/A |\n",
6
+ "usage": "---\ntitle: 'Rune: Usage Guide'\ndescription: Configuration, transports, scoped loggers, lazy bindings, timers, groups, and best practices for Rune.\n---\n\n[[toc]]\n\n::: tip New to Rune?\nStart with the [Overview](./index.md), then use this page for detailed usage patterns.\n:::\n\n## Basic Usage\n\nUse `createLogger()` to create an isolated logger instance.\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\n\nconst appLog = createLogger();\nconst apiLog = createLogger({ namespace: 'api' });\nconst authLog = createLogger('auth'); // shorthand namespace\n```\n\nEach `createLogger()` call is fully independent with its own transport pipeline.\n\nThe two-arg shorthand combines namespace and options cleanly:\n\n```ts\nconst log = createLogger('api', { logLevel: 'warn', transports: [transport] });\n```\n\n## Transports\n\nTransports are the delivery layer. Every `LogEntry` that passes the logger's level threshold is dispatched to each transport in order. Transports handle their own formatting, level filtering, and delivery.\n\n```ts\nimport { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n transports: [\n consoleTransport({ timestamp: true }),\n jsonTransport({ level: 'error' }),\n ],\n});\n```\n\nWhen `transports` is omitted, `consoleTransport()` is used automatically.\n\n### Built-in Transport Factories\n\n| Factory | Use case |\n| -------------------- | --------------------------------------------- |\n| `consoleTransport()` | Styled console output (default) |\n| `jsonTransport()` | NDJSON for server-side log aggregation |\n| `remoteTransport()` | Asynchronous browser or server delivery |\n| `batchTransport()` | Serialized buffered delivery with flush |\n| `sampleTransport()` | Probabilistic volume reduction |\n| `redactTransport()` | Recursive sensitive-field and subtree masking |\n\n### Node.js: Structured JSON Logging\n\nFor server-side log pipelines (ELK, Datadog, CloudWatch), `jsonTransport` emits NDJSON to stdout:\n\n```ts\nimport { jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info('request', { path: '/users', status: 200 });\n// Outputs: {\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"path\":\"/users\",\"status\":200,\"msg\":\"request\"}\n```\n\n### Remote Delivery, Batching, and Redaction\n\nCompose transport wrappers around the delivery boundary. Handle remote failures explicitly in production and dispose the batch during shutdown.\n\n```ts\nimport { batchTransport, createLogger, redactTransport, remoteTransport } from '@vielzeug/rune';\n\nconst remote = remoteTransport({\n handler: (_level, payload) =>\n fetch('/api/logs', { body: JSON.stringify(payload), method: 'POST' }).then((response) => {\n if (!response.ok) throw new Error(`log delivery failed: ${response.status}`);\n }),\n level: 'warn',\n onError: (error) => console.error('log delivery failed', error),\n});\nconst batch = batchTransport({\n onFlush: async (entries) => {\n const response = await fetch('/api/log-batches', { body: JSON.stringify(entries), method: 'POST' });\n if (!response.ok) throw new Error(`batch delivery failed: ${response.status}`);\n },\n});\nconst log = createLogger({\n transports: [redactTransport({ keys: ['password', 'token'], transport: remote })],\n});\nconst batchedLog = createLogger({\n transports: [redactTransport({ keys: ['password', 'token'], transport: batch.transport })],\n});\n\nlog.error('direct delivery');\nbatchedLog.error('buffered delivery');\nawait batch.dispose();\n```\n\n`redactTransport()` fails closed: when `maxDepth` is exceeded, the entire deeper subtree is replaced rather than forwarded uninspected.\n\n## Configuration\n\nUse `child()` to derive immutable logger variants.\n\n```ts\nconst log = createLogger({ namespace: 'app' });\nconst AppLog = log.child({\n logLevel: 'warn',\n namespace: 'App',\n // transports inherited from parent by default\n // pass transports: [] to disable all, or transports: [...] to replace\n});\n\n// Individual getters: no config snapshot\nconsole.log(AppLog.logLevel); // 'warn'\nconsole.log(AppLog.namespace); // 'App'\nconsole.log(AppLog.transports); // [...]\n```\n\nLevel threshold order: `debug` < `info` < `warn` < `error` < `fatal` < `off`\n\n## Middleware\n\nUse middleware for transformations or filters that must apply once before every transport. Middleware runs in order; returning `null` drops the entry.\n\n```ts\nconst log = createLogger({\n middleware: [(entry) => ({ ...entry, data: { ...entry.data, environment: 'production' } })],\n transports: [consoleTransport(), jsonTransport()],\n});\n\nconst errorsOnly = log.use((entry) => (entry.level === 'error' || entry.level === 'fatal' ? entry : null));\n```\n\n`use()` returns a new logger and leaves its parent unchanged. A throwing middleware is isolated and drops only the affected entry.\n\n## Call Signature\n\nAll log methods support message-first, context-first, and error-first calls:\n\n```ts\nlog.info('message');\nlog.info('message', { key: 'value' });\nlog.debug({ type: 'dispatch', id: 42 }, 'bus:dispatch');\nlog.error(new Error('boom'), { requestId: 'abc' }, 'request failed');\n```\n\nUse message-first for ordinary application logs. Context-first avoids synthetic messages for structured events and matches observation callbacks such as `tap((event) => log.debug(event))`. Error-first automatically stores the serialized error under `err`.\n\nPer-call context is shallow-merged with `withBindings()` bindings into `entry.data`. `Error` values are serialized to `{ message, name, stack }`.\n\n## Logging Methods\n\n```ts\nconst log = createLogger();\nlog.debug('debug details');\nlog.info('server started', { port: 3000 });\nlog.warn('cache stale');\nlog.error('request failed', { err: new Error('timeout') }); // Error auto-serialized in context\nlog.fatal('terminating', { service: 'db' }); // above error, use for unrecoverable state\n```\n\nUse `enabled()` to avoid expensive payload construction before the level check:\n\n```ts\nif (log.enabled('debug')) {\n log.debug('diagnostics', { diagnostics: buildLargePayload() });\n}\n```\n\nOr use `lazy()` to let Rune gate it automatically:\n\n```ts\nconst reqLog = log.withBindings({ diagnostics: lazy(() => buildLargePayload()) });\nreqLog.debug('diagnostics'); // buildLargePayload() only called when debug is enabled\n```\n\n## Pinned Bindings\n\n`withBindings(fields)` returns a child logger where the given fields are merged into every log call. This is the idiomatic way to attach per-request or per-user context.\n\n```ts\nconst api = createLogger({ namespace: 'api' });\n\nconst reqLog = api.withBindings({ requestId: 'abc-123', userId: 42 });\nreqLog.info('GET /users'); // always includes requestId and userId\nreqLog.warn('query took 2s', { slow: true }); // call-site fields merged in\n```\n\nThe parent logger is not affected. Bindings stack additively through chained `withBindings()` calls:\n\n```ts\nconst base = log.withBindings({ service: 'api' });\nconst req = base.withBindings({ requestId: 'xyz' });\n// req emits both service and requestId on every call\n```\n\nThe `bindings` getter returns a defensive snapshot:\n\n```ts\nconsole.log(reqLog.bindings); // { requestId: 'abc-123', userId: 42 }\n```\n\n## Lazy Bindings\n\n`lazy(fn)` defers evaluation of a binding value until after the level check passes. The factory is never called when the entry would be suppressed.\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst log = createLogger().withBindings({\n // Only called when debug entries are emitted\n snapshot: lazy(() => JSON.stringify(getFullAppState())),\n // Regular values are always included as-is\n service: 'api',\n});\n\nlog.debug('state trace'); // snapshot() only called here\nlog.warn('cache miss'); // snapshot() NOT called: warn doesn't need it\n```\n\nLazy bindings are resolved on every emitted call, not cached:\n\n```ts\nconst counter = { n: 0 };\nconst log = createLogger().withBindings({ tick: lazy(() => ++counter.n) });\n\nlog.info('a'); // tick: 1\nlog.info('b'); // tick: 2\n```\n\n## Child Loggers\n\n`child(overrides?)` creates a new logger scoped to a namespace, level, or transport set. Use it to create module-level or service-level loggers.\n\n```ts\nconst api = createLogger({ namespace: 'api' });\nconst auth = api.child({ namespace: 'auth' }); // → 'api.auth' (dot-joined automatically)\n\napi.info('GET /users');\nauth.warn('token expiring');\n```\n\n`child(overrides?)` clones current config and applies overrides. Transports are inherited by default.\n\n```ts\nconst base = createLogger({ logLevel: 'info', namespace: 'app' });\nconst verbose = base.child({ logLevel: 'debug' }); // inherits transports\n\n// Replace transports entirely on the child\nconst silent = base.child({ transports: [] }); // no output\n\n// Override with a different transport set\nconst jsonChild = base.child({ transports: [jsonTransport()] });\n```\n\nChild and parent configs remain independent after creation.\n\n## Timing\n\n`time(label, fn, level?)` measures execution time of sync or async functions. Emits a structured entry with `{ duration_ms }` in `data` and `label` as the message. When `fn` throws or rejects, the entry also includes `{ err }` with the serialized error.\n\n```ts\n// Sync\nconst result = log.time('parse', () => parseDocument(input));\n// Emits: { level: 'debug', message: 'parse', data: { duration_ms: 2.4 } }\n\n// Async\nconst users = await log.time('db.users', () => db.query('SELECT * FROM users'));\n// Emits even on rejection, with { err } included in data\n\n// Custom level\nlog.time('health-check', () => ping(), 'info');\n\n// Skipped when logLevel is 'off', but fn still executes\n```\n\n## Groups\n\n`group(label, fn, level?)` and `groupCollapsed(label, fn, level?)` wrap a callback in a console group, ensuring `groupEnd` is called even when the callback throws or rejects.\n\n```ts\nawait log.groupCollapsed('Job', async () => {\n await log.time('process', () => runJob());\n log.info('Done');\n});\n\n// Gate the group header on a log level: suppresses when logLevel is above 'debug'\nlog.group(\n 'verbose trace',\n () => {\n log.debug('internal state', { state });\n },\n 'debug',\n);\n```\n\nWhen `logLevel` is `'off'`, the group wrapper is bypassed but the callback still executes. When a `level` is provided and it is below the configured threshold, the group header is skipped but the callback still runs.\n\n## Testing\n\nUse a test transport to assert log entries without mocking `console`. This approach is more robust and does not require spy cleanup:\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createLogger } from '@vielzeug/rune';\nimport type { LogEntry, Transport } from '@vielzeug/rune';\n\nfunction createTestTransport() {\n const entries: LogEntry[] = [];\n const transport: Transport = (entry) => entries.push(entry);\n return { entries, transport };\n}\n\nit('logs errors when enabled', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'error', transports: [transport] });\n\n log.error('boom');\n\n expect(entries).toHaveLength(1);\n expect(entries[0].level).toBe('error');\n expect(entries[0].message).toBe('boom');\n});\n\nit('suppresses debug when logLevel is warn', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'warn', transports: [transport] });\n\n log.debug('silent');\n log.warn('loud');\n\n expect(entries).toHaveLength(1);\n});\n```\n\nYou can still spy on `console` methods when testing `consoleTransport` output directly:\n\n```ts\nimport { afterEach, expect, it, vi } from 'vitest';\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\n\nafterEach(() => vi.restoreAllMocks());\n\nit('writes error to console.error', () => {\n const spy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const log = createLogger({ logLevel: 'error', transports: [consoleTransport({ timestamp: false })] });\n\n log.error('boom');\n\n expect(spy).toHaveBeenCalled();\n});\n```\n\n## Framework Integration\n\nRune is framework-agnostic and works as a module-level singleton or a context-injected instance.\n\n::: code-group\n\n```tsx [React]\nimport { createContext, useState, useContext } from 'react';\nimport { createLogger } from '@vielzeug/rune';\n\nconst LogContext = createContext(createLogger({ namespace: 'app' }));\n\nfunction useLogger() {\n return useContext(LogContext);\n}\n\nfunction App() {\n const [requestLogger] = useState(() => createLogger({ namespace: 'app' }).withBindings({ userId: '42' }));\n return (\n <LogContext.Provider value={requestLogger}>\n <Dashboard />\n </LogContext.Provider>\n );\n}\n\nfunction Dashboard() {\n const log = useLogger();\n log.info('Dashboard mounted');\n return <div>Dashboard</div>;\n}\n```\n\n```ts [Vue 3]\nimport { inject, provide } from 'vue';\nimport { createLogger, type Logger } from '@vielzeug/rune';\n\nconst LoggerKey = Symbol('logger');\n\nfunction provideLogger(namespace: string) {\n const logger = createLogger({ namespace });\n provide(LoggerKey, logger);\n return logger;\n}\n\nfunction useLogger(): Logger {\n const logger = inject<Logger>(LoggerKey);\n if (!logger) throw new Error('Logger not provided');\n return logger;\n}\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { setContext, getContext } from 'svelte';\n import { createLogger } from '@vielzeug/rune';\n\n const logger = createLogger({ namespace: 'app' });\n setContext('logger', logger);\n</script>\n\n<!-- Child component -->\n<script lang=\"ts\">\n import { getContext } from 'svelte';\n import type { Logger } from '@vielzeug/rune';\n\n const logger = getContext<Logger>('logger');\n logger.info('component mounted');\n</script>\n```\n\n:::\n\n### Pitfalls\n\n- **React:** Creating the logger without a stable initializer recreates it on every re-render. Use `useState(() => createLogger(...))`.\n- **Vue 3:** `inject()` must be called at the top level of `setup()`, not inside callbacks.\n- **Svelte:** `getContext()` must be called synchronously during component initialization.\n\n## Working with Other Vielzeug Libraries\n\n### With Courier\n\n```ts\nimport { createCourier, withLogging } from '@vielzeug/courier';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'courier' });\nconst courier = createCourier({\n baseUrl: 'https://api.example.com',\n middleware: [withLogging({ logger: (message, meta) => log.debug(message, meta) })],\n});\n```\n\n### With Herald\n\n```ts\nimport { createBus } from '@vielzeug/herald';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'bus' });\nconst bus = createBus<AppEvents>({\n onDispatch: (event, payload) => log.debug('dispatched', { event, payload }),\n onError: (err, event) => log.error(`handler error in \"${event}\"`, { err }),\n});\n```\n\n## Best Practices\n\n- Create one child logger per module boundary using `createLogger('module.name')` or `log.child({ namespace: 'module.name' })`.\n- Use `withBindings()` to pin request/session context instead of repeating fields on each call.\n- Use `lazy()` for expensive diagnostics bindings only needed at `debug` level.\n- Set `logLevel` from environment (`'debug'` in dev, `'warn'` or `'error'` in prod).\n- Use `enabled()` before expensive payload construction that `lazy()` cannot defer.\n- Configure transports at the application root; pass scoped loggers via DI or context.\n- To style console output, pass `consoleTransport({ theme })` explicitly in `transports`.\n- Use `fatal()` only for genuinely unrecoverable states.\n",
7
+ "examples": "---\ntitle: 'Rune: Examples'\ndescription: Practical examples and recipes for rune.\n---\n\n## Examples\n\n- [Module Logger Pattern](./examples/module-logger-pattern.md)\n- [Child Logger Overrides](./examples/child-logger-overrides.md)\n- [Production Setup](./examples/production-setup.md)\n- [Timing And Grouping](./examples/timing-and-grouping.md)\n- [React Integration](./examples/react-integration.md)\n- [Request Middleware](./examples/request-middleware.md)\n- [Testing](./examples/testing.md)\n"
8
8
  },
9
9
  "examples": [
10
10
  {
11
11
  "id": "basic-logging",
12
- "code": "import { createLogger } from '@vielzeug/rune'\n\nconst log = createLogger()\n\n// message-only\nlog.debug('app starting')\nlog.info('ready')\n\n// message + structured context\nlog.info('server listening', { port: 3000 })\nlog.warn('retrying request', { retries: 3 })\n\n// Pass Error as a context field — auto-serialized to { message, name, stack }\nconst err = new Error('connection refused')\nlog.error('service unavailable', { err })\nlog.error('request failed', { err, requestId: 'r-001' })\n\nconsole.log('(Open DevTools console to see styled output)')",
12
+ "code": "import { createLogger } from '@vielzeug/rune'\n\nconst log = createLogger()\n\n// message-only\nlog.debug('app starting')\nlog.info('ready')\n\n// message + structured context\nlog.info('server listening', { port: 3000 })\nlog.warn('retrying request', { retries: 3 })\n\n// Pass Error as a context field: auto-serialized to { message, name, stack }\nconst err = new Error('connection refused')\nlog.error('service unavailable', { err })\nlog.error('request failed', { err, requestId: 'r-001' })\n\nconsole.log('(Open DevTools console to see styled output)')",
13
13
  "name": "Basic Logging"
14
14
  },
15
15
  {
16
16
  "id": "lazy-and-timing",
17
- "code": "import { createLogger, lazy } from '@vielzeug/rune'\n\nconst entries = []\nconst log = createLogger({ transports: [(e) => entries.push(e)] })\n\n// lazy() defers factory evaluation until after the level check\nlet callCount = 0\nconst reqLog = log.withBindings({\n snapshot: lazy(() => ({ n: ++callCount, size: 1024 })),\n})\n\nreqLog.debug('trace') // snapshot() called — callCount becomes 1\nreqLog.info('step 2') // snapshot() called — callCount becomes 2\n\n// Suppress debug: lazy factory is never called\nconst quietLog = log.child({ logLevel: 'warn' })\nconst quietReq = quietLog.withBindings({ val: lazy(() => ++callCount) })\nquietReq.debug('not emitted') // factory skipped\n\nconsole.log('Factory calls:', callCount) // 2, not 3\n\n// time() measures execution and emits { duration_ms } in data\nconst parsed = log.time('parse', () => JSON.parse('[1,2,3]'))\nconsole.log('Parsed:', parsed)\nconsole.log('Timer data:', entries[entries.length - 1].data)\n\n// When the timed fn throws, { err } is also included in data\ntry {\n log.time('risky', () => { throw new Error('oops') })\n} catch {}\nconsole.log('Error data:', entries[entries.length - 1].data)\n\n// dispose() marks the logger as disposed; subsequent calls become no-ops\nlog.dispose()\nlog.info('silenced') // no-op\nconsole.log('disposed:', log.disposed) // true",
17
+ "code": "import { createLogger, lazy } from '@vielzeug/rune'\n\nconst entries = []\nconst log = createLogger({ transports: [(e) => entries.push(e)] })\n\n// lazy() defers factory evaluation until after the level check\nlet callCount = 0\nconst reqLog = log.withBindings({\n snapshot: lazy(() => ({ n: ++callCount, size: 1024 })),\n})\n\nreqLog.debug('trace') // snapshot() called: callCount becomes 1\nreqLog.info('step 2') // snapshot() called: callCount becomes 2\n\n// Suppress debug: lazy factory is never called\nconst quietLog = log.child({ logLevel: 'warn' })\nconst quietReq = quietLog.withBindings({ val: lazy(() => ++callCount) })\nquietReq.debug('not emitted') // factory skipped\n\nconsole.log('Factory calls:', callCount) // 2, not 3\n\n// time() measures execution and emits { duration_ms } in data\nconst parsed = log.time('parse', () => JSON.parse('[1,2,3]'))\nconsole.log('Parsed:', parsed)\nconsole.log('Timer data:', entries[entries.length - 1].data)\n\n// When the timed fn throws, { err } is also included in data\ntry {\n log.time('risky', () => { throw new Error('oops') })\n} catch {}\nconsole.log('Error data:', entries[entries.length - 1].data)\n\n// dispose() marks the logger as disposed; subsequent calls become no-ops\nlog.dispose()\nlog.info('silenced') // no-op\nconsole.log('disposed:', log.disposed) // true",
18
18
  "name": "Lazy Bindings & Timing"
19
19
  },
20
20
  {
21
21
  "id": "level-filtering",
22
- "code": "import { createLogger } from '@vielzeug/rune'\n\nconst entries = []\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'app',\n transports: [(e) => entries.push(e)],\n})\n\n// Threshold order: debug < info < warn < error < fatal < off\nlog.debug('msg')\nlog.info('msg')\nlog.warn('msg')\nlog.error('msg')\nlog.fatal('msg')\nconsole.log('All levels:', entries.map((e) => e.level))\nentries.length = 0\n\n// child() with raised threshold — debug and info are suppressed\nconst prodLog = log.child({ logLevel: 'warn' })\nprodLog.debug('suppressed')\nprodLog.info('suppressed')\nprodLog.warn('passes')\nprodLog.error('passes')\nconsole.log('Threshold warn:', entries.map((e) => e.level))\nentries.length = 0\n\n// Individual getters expose config without a snapshot object\nconsole.log('log.logLevel:', log.logLevel) // 'debug'\nconsole.log('prodLog.logLevel:', prodLog.logLevel) // 'warn'\nconsole.log('log.namespace:', log.namespace) // 'app'\n\n// enabled() guards expensive payload construction\nconsole.log('debug enabled:', log.enabled('debug')) // true\nconsole.log('debug enabled (prod):', prodLog.enabled('debug')) // false",
22
+ "code": "import { createLogger } from '@vielzeug/rune'\n\nconst entries = []\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'app',\n transports: [(e) => entries.push(e)],\n})\n\n// Threshold order: debug < info < warn < error < fatal < off\nlog.debug('msg')\nlog.info('msg')\nlog.warn('msg')\nlog.error('msg')\nlog.fatal('msg')\nconsole.log('All levels:', entries.map((e) => e.level))\nentries.length = 0\n\n// child() with raised threshold: debug and info are suppressed\nconst prodLog = log.child({ logLevel: 'warn' })\nprodLog.debug('suppressed')\nprodLog.info('suppressed')\nprodLog.warn('passes')\nprodLog.error('passes')\nconsole.log('Threshold warn:', entries.map((e) => e.level))\nentries.length = 0\n\n// Individual getters expose config without a snapshot object\nconsole.log('log.logLevel:', log.logLevel) // 'debug'\nconsole.log('prodLog.logLevel:', prodLog.logLevel) // 'warn'\nconsole.log('log.namespace:', log.namespace) // 'app'\n\n// enabled() guards expensive payload construction\nconsole.log('debug enabled:', log.enabled('debug')) // true\nconsole.log('debug enabled (prod):', prodLog.enabled('debug')) // false",
23
23
  "name": "Level Filtering"
24
24
  },
25
25
  {
26
26
  "id": "lifecycle",
27
- "code": "import { createLogger } from '@vielzeug/rune';\n\n// Two-arg shorthand: namespace + options\nconst log = createLogger('api', { logLevel: 'debug' });\n\nlog.info('logger created');\nlog.debug('request start', { url: '/health' });\n\n// disposed logger silences all subsequent calls\nlog.dispose();\nlog.info('this is silenced — no output');\n\nconsole.log('log.disposed:', log.disposed);\n\n// using-declaration auto-disposes at end of scope\n{\n using scoped = createLogger('scoped', { logLevel: 'debug' });\n scoped.info('scoped logger active');\n} // scoped.dispose() called automatically here\n\n// disposalSignal aborts when dispose() is called\nconst monitored = createLogger('monitored');\nmonitored.disposalSignal.addEventListener('abort', () => {\n console.log('monitored logger was disposed');\n});\nmonitored.dispose();\n",
27
+ "code": "import { createLogger } from '@vielzeug/rune';\n\n// Two-arg shorthand: namespace + options\nconst log = createLogger('api', { logLevel: 'debug' });\n\nlog.info('logger created');\nlog.debug('request start', { url: '/health' });\n\n// disposed logger silences all subsequent calls\nlog.dispose();\nlog.info('this is silenced: no output');\n\nconsole.log('log.disposed:', log.disposed);\n\n// using-declaration auto-disposes at end of scope\n{\n using scoped = createLogger('scoped', { logLevel: 'debug' });\n scoped.info('scoped logger active');\n} // scoped.dispose() called automatically here\n\n// disposalSignal aborts when dispose() is called\nconst monitored = createLogger('monitored');\nmonitored.disposalSignal.addEventListener('abort', () => {\n console.log('monitored logger was disposed');\n});\nmonitored.dispose();\n",
28
28
  "name": "Logger Lifecycle & Disposal"
29
29
  },
30
30
  {
31
31
  "id": "scoped-loggers",
32
- "code": "import { createLogger } from '@vielzeug/rune'\n\nconst log = createLogger()\n\n// child() dot-joins namespaces automatically\nconst api = log.child({ namespace: 'api' }) // 'api'\nconst auth = api.child({ namespace: 'auth' }) // 'api.auth'\nconst worker = api.child({ namespace: 'worker' }) // 'api.worker'\n\n// Individual getters — no config snapshot\nconsole.log('root:', log.namespace) // ''\nconsole.log('api:', api.namespace) // 'api'\nconsole.log('auth:', auth.namespace) // 'api.auth'\nconsole.log('worker:', worker.namespace) // 'api.worker'\n\n// withBindings() pins fields to every call\nconst reqLog = auth.withBindings({ requestId: 'r-001', userId: 'u-42' })\nreqLog.info('token check')\nreqLog.warn('token refreshed', { expired: false })\n\nconsole.log('bindings snapshot:', reqLog.bindings)\n\n// enabled() checks whether a level passes the logger threshold\nconsole.log('debug enabled:', api.enabled('debug')) // true (default level is debug)\nconst warnLog = api.child({ logLevel: 'warn' })\nconsole.log('warnLog debug:', warnLog.enabled('debug')) // false\n\nconsole.log('(Open DevTools console to see styled output)')",
32
+ "code": "import { createLogger } from '@vielzeug/rune'\n\nconst log = createLogger()\n\n// child() dot-joins namespaces automatically\nconst api = log.child({ namespace: 'api' }) // 'api'\nconst auth = api.child({ namespace: 'auth' }) // 'api.auth'\nconst worker = api.child({ namespace: 'worker' }) // 'api.worker'\n\n// Individual getters: no config snapshot\nconsole.log('root:', log.namespace) // ''\nconsole.log('api:', api.namespace) // 'api'\nconsole.log('auth:', auth.namespace) // 'api.auth'\nconsole.log('worker:', worker.namespace) // 'api.worker'\n\n// withBindings() pins fields to every call\nconst reqLog = auth.withBindings({ requestId: 'r-001', userId: 'u-42' })\nreqLog.info('token check')\nreqLog.warn('token refreshed', { expired: false })\n\nconsole.log('bindings snapshot:', reqLog.bindings)\n\n// enabled() checks whether a level passes the logger threshold\nconsole.log('debug enabled:', api.enabled('debug')) // true (default level is debug)\nconst warnLog = api.child({ logLevel: 'warn' })\nconsole.log('warnLog debug:', warnLog.enabled('debug')) // false\n\nconsole.log('(Open DevTools console to see styled output)')",
33
33
  "name": "Scoped Loggers"
34
34
  },
35
35
  {
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "apiSource": "export { createSandbox } from './_runtime.js';\nexport { SandboxConfigurationError, SandboxError, SandboxTimeoutError } from './errors.js';\nexport type { SandboxBridge, SandboxHandle, SandboxMessage, SandboxOptions } from './types.js';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Sandbox — Sandboxed iframe runtime\ndescription: Isolated iframe runtime with a typed state bridge and explicit message trust boundary for safe execution of untrusted HTML — component previews, playgrounds, plugin sandboxes, and more.\npackage: sandbox\ncategory: ui-primitives\nkeywords: [sandbox, iframe, isolation, playground, csp, postmessage, security, components]\nexports:\n [\n createSandbox,\n SandboxConfigurationError,\n SandboxError,\n SandboxTimeoutError,\n SandboxHandle,\n SandboxOptions,\n SandboxBridge,\n SandboxMessage,\n ]\nrelated: [codex, refine]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"sandbox\" />\n\n## Why Sandbox?\n\nRunning untrusted HTML in the main window is unsafe — arbitrary code can access the DOM, cookies, and user data. Sandbox creates an isolated `<iframe sandbox=\"allow-scripts\">` that receives content over a typed state bridge and explicit message trust boundary. The sandbox cannot reach the host page.\n\n```ts\n// Before\ncontainer.innerHTML = untrustedHtml;\n\n// After\nconst sandbox = createSandbox(container);\nawait sandbox.render(untrustedHtml);\n```\n\nCommon use cases:\n\n- **Component previews** — render isolated HTML/CSS examples in documentation or design tools\n- **Code playgrounds** — execute user-provided code with full error forwarding and state injection\n- **Plugin sandboxes** — host third-party or user-authored plugin UI without granting host access\n- **User-generated content** — display untrusted HTML (emails, form output, external widgets) safely\n- **Widget embedding** — wrap third-party widgets with strict CSP and bidirectional messaging\n- **AI-generated UI** — render LLM-produced HTML components with guaranteed isolation\n\n| Feature | Raw `<iframe>` | Sandbox |\n| -------------------------- | -------------------------------------------- | --------------------------------------------- |\n| Bundle size | 0 B (built-in) | <PackageInfo package=\"sandbox\" type=\"size\" /> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Content-Security-Policy | Manual | Auto-generated, strict by default |\n| Managed message protocol | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Typed host state; untrusted inbound details |\n| Error forwarding | <ore-icon name=\"x\" size=\"16\"></ore-icon> | `onerror` + `unhandledrejection` → host |\n| Dispose / `using` | Manual `remove()` | `dispose()` + `[Symbol.dispose]` |\n\n<div class=\"decision-callout\">\n\n**Use Sandbox when** you need to render untrusted or user-provided HTML in the browser with iframe isolation, CSP enforcement, and explicit host/sandbox messaging.\n\n**Consider a raw `<iframe>` when** you only need to embed a known third-party URL — Sandbox is for programmatic `srcdoc` content, not URL-based embedding.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/sandbox\n```\n\n```sh [npm]\nnpm install @vielzeug/sandbox\n```\n\n```sh [yarn]\nyarn add @vielzeug/sandbox\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createSandbox } from '@vielzeug/sandbox';\n\nconst container = document.getElementById('preview')!;\nconst sandbox = createSandbox<{ theme: 'dark' | 'light' }>(container);\n\nsandbox.onMessage((message) => {\n if (message.type === 'custom') console.log(message.event, message.detail);\n if (message.type === 'error') console.error(message.message);\n if (message.type === 'resize') console.log('height:', message.height);\n});\n\ntry {\n await sandbox.render('<ore-button variant=\"primary\">Click me</ore-button>');\n sandbox.setState({ theme: 'dark' });\n} catch (error) {\n console.error('Sandbox render failed', error);\n} finally {\n sandbox.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createSandbox<State>()` — Creates an isolated `<iframe sandbox=\"allow-scripts\">` with typed outbound state\n- `render(html)` — Creates or replaces the document and resolves when its bridge reports ready\n- `setState(update)` — Pushes one or more typed state values in one message\n- `replaceBody(html)` — Updates streamed body content without resetting head scripts and styles\n- `updateStyle(id, css)` — Patches a named style without replacing the document\n- `SandboxMessage` — Keeps sandbox-controlled custom details typed as `unknown` on the host\n- `SandboxBridge<State, Events>` — Types authored sandbox-side state subscriptions and event emission\n- `readyTimeout` — Rejects blocked document initialization with `SandboxTimeoutError`\n- Strict CSP — Uses `default-src 'none'` and blocks network requests by default\n- Error forwarding — Installs before user scripts and forwards uncaught errors and rejections\n- Stale-message protection — Rejects messages from superseded render generations\n- Disposable — Supports `dispose()`, `disposalSignal`, and `[Symbol.dispose]`\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Codex](/codex/) — MCP server with `generate-sandbox-document` and `get-state-bridge-spec` tools; generates document templates for use with Sandbox\n- [Refine](/refine/) — Web component library; renders correctly inside the sandbox via `<script>` injection and `allowedScriptOrigins`\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Sandbox — API Reference\ndescription: API reference for the @vielzeug/sandbox managed iframe runtime, bridge, errors, and testing utilities.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createSandbox()` | Create a managed sandbox iframe | Sync | The iframe is created lazily by `render()` |\n| `SandboxHandle.render()` | Replace the document and await readiness | Async | Injected scripts can trigger a timeout |\n| `SandboxHandle` | Update, observe, and dispose the iframe | Sync methods | State/body updates require a ready document |\n| `SandboxOptions` | Configure metadata, CSP sources, scripts, styles, and timeout | Sync validation | Invalid values throw during creation |\n| `SandboxMessage` | Host-side message union | Sync callback | Custom details are always `unknown` |\n| `SandboxBridge` | Typed sandbox-side `window.__sandbox__` API | Sync | Its generics do not validate host-side input |\n| `SandboxError` | Base class for package errors | Thrown/rejected | Narrow caught values with `instanceof` |\n\n## Package Entry Point\n\n| Import | Exports |\n| --- | --- |\n| `@vielzeug/sandbox` | Runtime, errors, and public types |\n| `@vielzeug/sandbox/testing` | `createSandboxTestHelpers()` |\n\n```ts\nimport {\n createSandbox,\n SandboxConfigurationError,\n SandboxError,\n SandboxTimeoutError,\n} from '@vielzeug/sandbox';\nimport type {\n SandboxBridge,\n SandboxHandle,\n SandboxMessage,\n SandboxOptions,\n} from '@vielzeug/sandbox';\n```\n\n## Runtime\n\n### `createSandbox<State>(container, options?)`\n\n```ts\nfunction createSandbox<State extends object = Record<string, unknown>>(\n container: HTMLElement,\n options?: SandboxOptions,\n): SandboxHandle<State>;\n```\n\nCreates a managed `<iframe sandbox=\"allow-scripts\">` in `container`. Construction validates options but does not create DOM. The first `render()` creates the iframe with `referrerpolicy=\"no-referrer\"` and a generated CSP.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `container` | `HTMLElement` | Host element that owns the iframe |\n| `options` | `SandboxOptions` | Optional document and runtime configuration |\n\n**Returns:** `SandboxHandle<State>`.\n\n`State` describes values sent from the trusted host to the sandbox through `setState()`. Ordinary interfaces are supported.\n\n```ts\ninterface PreviewState {\n locale: string;\n theme: 'dark' | 'light';\n}\n\nconst sandbox = createSandbox<PreviewState>(container, {\n styles: { theme: 'body { color-scheme: light; }' },\n title: 'Preview',\n});\n\nawait sandbox.render('<main>Ready</main>');\nsandbox.setState({ locale: 'en', theme: 'dark' });\n```\n\n### `SandboxHandle<State>`\n\n```ts\ninterface SandboxHandle<State extends object = Record<string, unknown>> {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n dispose(): void;\n onMessage(handler: (message: SandboxMessage) => void): () => void;\n render(html: string): Promise<void>;\n replaceBody(html: string): void;\n setState(update: Partial<State>): void;\n updateStyle(id: string, css: string): void;\n [Symbol.dispose](): void;\n}\n```\n\n| Method/property | Description |\n| --- | --- |\n| `render(html)` | Replace the complete document and await readiness |\n| `replaceBody(html)` | Replace body descendants without navigation |\n| `setState(update)` | Push typed host state |\n| `updateStyle(id, css)` | Patch a named style |\n| `onMessage(handler)` | Subscribe to untrusted iframe messages |\n| `disposed` / `disposalSignal` | Observe lifecycle state |\n| `dispose()` / `[Symbol.dispose]()` | Release the iframe and listeners |\n\n#### `render(html)`\n\nReplaces the complete `srcdoc` document. The returned Promise resolves when that document emits its internal ready message and rejects with `SandboxTimeoutError` after `readyTimeout`.\n\nA newer render supersedes and resolves the previous pending render. Disposal also resolves a pending render. Messages from superseded generations are ignored.\n\nThe generated bridge is installed in `<head>` before injected scripts and user HTML, so it can capture script errors during document initialization. Its ready message is sent on `DOMContentLoaded`.\n\n#### `replaceBody(html)`\n\nReplaces `document.body.innerHTML` without navigating. Head scripts, document/window listeners, and named styles survive. Body descendants, descendant listeners, references, form state, and scripts in replacement HTML do not.\n\nCall only after `render()` resolves. The method warns in development and does nothing when no ready document exists.\n\n#### `setState(update)`\n\nSends one object containing one or more state values. Inside the iframe, each key dispatches its own `sandbox:state-update` `CustomEvent` and reaches matching `SandboxBridge.onState()` subscribers.\n\n```ts\nsandbox.setState({ locale: 'en', theme: 'dark' });\n```\n\nCalls made before the document is ready are accumulated instead of dropped: the newest value per key is delivered in one message when the bridge reports ready, and again after each later `render()` so a re-rendered document starts with current state.\n\n#### `updateStyle(id, css)`\n\nUpdates a style declared in `options.styles`. Before the first render it updates only the render baseline. After readiness it also patches the live `<style>` element. Unknown IDs emit a development warning.\n\n#### `onMessage(handler)`\n\nSubscribes to application messages and returns an idempotent unsubscribe function. Internal ready messages are not forwarded.\n\n```ts\nsandbox.onMessage((message) => {\n if (message.type === 'resize') container.style.height = `${message.height}px`;\n});\n```\n\n#### Lifecycle\n\n`dispose()` removes the iframe, removes the host `message` listener, clears subscribers, resolves a pending render, and aborts `disposalSignal`. It is idempotent. `[Symbol.dispose]()` provides the same behavior for `using` declarations.\n\n## Types\n\n### `SandboxOptions`\n\n```ts\ninterface SandboxOptions {\n allowedFontOrigins?: string[];\n allowedImageOrigins?: string[];\n allowedScriptOrigins?: string[];\n allowedStyleOrigins?: string[];\n lang?: string;\n nonce?: string;\n readyTimeout?: number;\n scripts?: string[];\n styles?: Record<string, string>;\n title?: string;\n}\n```\n\n| Option | Default | Description |\n| --- | --- | --- |\n| `allowedFontOrigins` | `[]` | HTTP(S) origins added to `font-src`; otherwise `'none'` |\n| `allowedImageOrigins` | `[]` | HTTP(S) origins added to `img-src`; `data:` is always included |\n| `allowedScriptOrigins` | `[]` | HTTP(S) origins added to `script-src` |\n| `allowedStyleOrigins` | `[]` | HTTP(S) origins added to `style-src` |\n| `lang` | `'en'` | Basic BCP 47 language tag for `<html lang>` |\n| `nonce` | `undefined` | Base64/base64url-style nonce for the bridge script and CSP |\n| `readyTimeout` | `5000` | Positive finite timeout in milliseconds, at most `2^31 - 1` |\n| `scripts` | `[]` | Absolute HTTP(S) scripts injected before user HTML |\n| `styles` | `{}` | Named style blocks; IDs start with a letter and contain letters, digits, `_`, or `-` |\n| `title` | `''` | Escaped document title |\n\nAllowed origins reject paths, credentials, query strings, fragments, unsupported schemes, and CSP syntax. Script URLs must be absolute HTTP(S) URLs.\n\n### `SandboxMessage`\n\n```ts\ntype SandboxMessage =\n | { detail: unknown; event: string; type: 'custom' }\n | { message: string; stack?: string; type: 'error' }\n | { height: number; type: 'resize' };\n```\n\nAll fields originate in sandboxed code and are untrusted. Validate custom `detail`, and render text through escaping APIs rather than HTML injection.\n\n### `SandboxBridge<State, Events>`\n\nThe generated document exposes `window.__sandbox__`:\n\n```ts\ninterface SandboxBridge<\n State extends object = Record<string, unknown>,\n Events extends object = Record<string, unknown>,\n> {\n emit<K extends keyof Events & string>(\n event: K,\n ...detail: undefined extends Events[K] ? [detail?: Events[K]] : [detail: Events[K]]\n ): void;\n onState<K extends keyof State & string>(\n key: K,\n handler: (value: State[K]) => void,\n ): () => void;\n}\n```\n\nUse an ambient declaration in authored sandbox-side TypeScript:\n\n```ts\ninterface State {\n theme: 'dark' | 'light';\n}\n\ninterface Events {\n saved: { id: string };\n}\n\ndeclare interface Window {\n __sandbox__: import('@vielzeug/sandbox').SandboxBridge<State, Events>;\n}\n```\n\nThese generics check authored sandbox code only. They do not make host-side messages trustworthy.\n\n## Errors\n\n### `SandboxError`\n\nBase class for package errors. Accepts `opts?: ErrorOptions` for cause chaining.\n\n### `SandboxConfigurationError`\n\nThrown synchronously by `createSandbox()` when an option cannot produce a valid document or CSP.\n\n### `SandboxTimeoutError`\n\nRejected by `render()` when no ready message arrives before `readyTimeout`. An injected script may have blocked document initialization.\n\n## Testing\n\n```ts\nimport { createSandboxTestHelpers } from '@vielzeug/sandbox/testing';\n```\n\n`createSandboxTestHelpers(container)` returns `fireReady()`, `fireCustom()`, `fireError()`, and `fireResize()` helpers. Call `render()` first so the helper can read the iframe channel and generation metadata.\n",
6
- "usage": "---\ntitle: Sandbox — Usage Guide\ndescription: Render untrusted HTML, push typed state, receive sandbox messages, update live content, and manage iframe lifecycle.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a sandbox, subscribe before rendering, await readiness, and dispose it when finished.\n\n```ts\nimport { createSandbox } from '@vielzeug/sandbox';\n\nconst container = document.getElementById('preview')!;\nconst sandbox = createSandbox(container);\n\nsandbox.onMessage((message) => {\n if (message.type === 'error') console.error(message.message);\n if (message.type === 'resize') container.style.height = `${message.height}px`;\n});\n\ntry {\n await sandbox.render('<main><h1>Preview</h1></main>');\n} finally {\n sandbox.dispose();\n}\n```\n\nThe iframe is created by the first `render()` call. Each render creates a fresh document and resolves after its generated bridge reports ready.\n\n## Rendering HTML\n\nUse `render()` when scripts, styles, globals, and DOM state should reset together.\n\n```ts\nawait sandbox.render(`\n <style>body { font-family: system-ui; }</style>\n <button id=\"save\">Save</button>\n <script>\n document.querySelector('#save').addEventListener('click', () => {\n window.__sandbox__.emit('save');\n });\n </script>\n`);\n```\n\nA newer render supersedes a pending one. Messages from the old document are ignored through the per-render generation marker.\n\n## Pushing Typed State\n\nPass a state interface to `createSandbox()` and send partial updates with one object.\n\n```ts\ninterface PreviewState {\n locale: string;\n theme: 'dark' | 'light';\n user: { name: string };\n}\n\nconst sandbox = createSandbox<PreviewState>(container);\nawait sandbox.render('<main id=\"app\"></main>');\n\nsandbox.setState({\n locale: 'en',\n theme: 'dark',\n user: { name: 'Ada' },\n});\n```\n\nSubscribe inside the iframe through the typed bridge.\n\n```ts\ninterface PreviewState {\n locale: string;\n theme: 'dark' | 'light';\n}\n\ndeclare interface Window {\n __sandbox__: import('@vielzeug/sandbox').SandboxBridge<PreviewState>;\n}\n\nwindow.__sandbox__.onState('theme', (theme) => {\n document.documentElement.dataset.theme = theme;\n});\n```\n\n`setState()` is safe to call at any point in the lifecycle. Updates made before `render()` resolves are accumulated and delivered in one message when the bridge reports ready, so an early call never loses state.\n\n## Receiving Sandbox Messages\n\nSubscribe with `onMessage()` before rendering. The unsubscribe function detaches only that handler.\n\n```ts\nconst unsubscribe = sandbox.onMessage((message) => {\n if (message.type === 'custom' && message.event === 'saved') {\n if (isSavedDetail(message.detail)) persist(message.detail.id);\n }\n});\n\nfunction isSavedDetail(value: unknown): value is { id: string } {\n return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';\n}\n```\n\nCustom event details remain `unknown` on the host. The iframe is isolated, but its code still controls message payloads.\n\nUse `SandboxBridge<State, Events>` only to check authored sandbox-side calls.\n\n```ts\ninterface Events {\n saved: { id: string };\n}\n\ndeclare interface Window {\n __sandbox__: import('@vielzeug/sandbox').SandboxBridge<Record<string, never>, Events>;\n}\n\nwindow.__sandbox__.emit('saved', { id: '42' });\n```\n\n## Updating Live Content\n\n### Replace the body\n\nUse `replaceBody()` when streamed HTML should preserve head scripts, styles, and global listeners.\n\n```ts\nawait sandbox.render('<main>Waiting…</main>');\n\nlet html = '';\nfor await (const chunk of stream) {\n html += chunk;\n sandbox.replaceBody(html);\n}\n```\n\nReplacement uses `document.body.innerHTML`. Scripts in replacement HTML do not execute, and body descendant state and listeners are discarded.\n\n### Update a named style\n\nDeclare styles at construction and patch them without replacing the document.\n\n```ts\nconst sandbox = createSandbox(container, {\n styles: { theme: 'body { color-scheme: light; }' },\n});\n\nawait sandbox.render('<main>Preview</main>');\nsandbox.updateStyle('theme', 'body { color-scheme: dark; }');\n```\n\nCalling `updateStyle()` before the first render updates the render baseline. Unknown IDs emit a development warning.\n\n## Injecting Scripts and Configuring CSP\n\nList absolute external scripts in `scripts`. Their origins are added to `script-src` automatically.\n\n```ts\nconst sandbox = createSandbox(container, {\n allowedFontOrigins: ['https://fonts.gstatic.com'],\n allowedStyleOrigins: ['https://fonts.googleapis.com'],\n scripts: ['https://cdn.example.com/widget.js'],\n});\n```\n\nNetwork requests remain blocked by `connect-src 'none'`. The package does not expose arbitrary CSP directives.\n\nA nonce authorizes the generated bridge while preventing un-nonced inline user scripts in CSP Level 3 browsers.\n\n```ts\nconst sandbox = createSandbox(container, {\n nonce: crypto.randomUUID(),\n});\n```\n\n## Handling Errors and Timeouts\n\n`render()` rejects if the document does not become ready before `readyTimeout`.\n\n```ts\nimport { SandboxError, SandboxTimeoutError } from '@vielzeug/sandbox';\n\ntry {\n await sandbox.render(html);\n} catch (error) {\n if (error instanceof SandboxTimeoutError) {\n showError('A script blocked preview initialization.');\n } else if (error instanceof SandboxError) {\n showError(error.message);\n }\n}\n```\n\nUncaught iframe errors and unhandled rejections arrive through `onMessage()` as untrusted `error` messages.\n\n## Disposal\n\nUse `dispose()` or a `using` declaration to remove the iframe and host listeners.\n\n```ts\nusing sandbox = createSandbox(container);\nawait sandbox.render('<p>Temporary preview</p>');\n```\n\nTie other asynchronous work to `disposalSignal`.\n\n```ts\nconst response = await fetch('/preview-data', {\n signal: sandbox.disposalSignal,\n});\n```\n\n## Testing\n\nUse the testing subpath to complete `render()` and simulate messages in jsdom.\n\n```ts\nimport { createSandbox } from '@vielzeug/sandbox';\nimport { createSandboxTestHelpers } from '@vielzeug/sandbox/testing';\nimport { expect, it } from 'vitest';\n\nit('receives resize messages', async () => {\n const container = document.createElement('div');\n const sandbox = createSandbox(container);\n const helpers = createSandboxTestHelpers(container);\n const heights: number[] = [];\n\n sandbox.onMessage((message) => {\n if (message.type === 'resize') heights.push(message.height);\n });\n\n const render = sandbox.render('<p>Preview</p>');\n helpers.fireReady();\n await render;\n helpers.fireResize(240);\n\n expect(heights).toEqual([240]);\n sandbox.dispose();\n});\n```\n\n## Framework Integration\n\nOwn one sandbox per mounted component and dispose it during unmount.\n\n::: code-group\n\n```tsx [React]\nimport { createSandbox, type SandboxHandle } from '@vielzeug/sandbox';\nimport { useEffect, useRef } from 'react';\n\nexport function Preview({ html }: { html: string }) {\n const container = useRef<HTMLDivElement>(null);\n const sandbox = useRef<SandboxHandle>();\n\n useEffect(() => {\n sandbox.current = createSandbox(container.current!);\n return () => sandbox.current?.dispose();\n }, []);\n\n useEffect(() => {\n void sandbox.current?.render(html).catch(console.error);\n }, [html]);\n\n return <div ref={container} />;\n}\n```\n\n```vue [Vue]\n<script setup lang=\"ts\">\nimport { createSandbox, type SandboxHandle } from '@vielzeug/sandbox';\nimport { onMounted, onUnmounted, ref, watch } from 'vue';\n\nconst props = defineProps<{ html: string }>();\nconst container = ref<HTMLElement>();\nlet sandbox: SandboxHandle | undefined;\n\nonMounted(() => {\n sandbox = createSandbox(container.value!);\n void sandbox.render(props.html).catch(console.error);\n});\nwatch(() => props.html, (html) => void sandbox?.render(html).catch(console.error));\nonUnmounted(() => sandbox?.dispose());\n</script>\n\n<template><div ref=\"container\" /></template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createSandbox, type SandboxHandle } from '@vielzeug/sandbox';\n import { onMount } from 'svelte';\n\n export let html: string;\n let container: HTMLElement;\n let sandbox: SandboxHandle | undefined;\n\n onMount(() => {\n sandbox = createSandbox(container);\n return () => sandbox?.dispose();\n });\n\n $: if (sandbox) void sandbox.render(html).catch(console.error);\n</script>\n\n<div bind:this={container}></div>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Sandbox to isolate generated markup that loads browser bundles of DOM-output packages such as Refine or Ore. Use Rune in the host to record validated message details; do not pass untrusted payloads directly to structured logs.\n\n## Best Practices\n\n- Await `render()` before sending state or replacing body content.\n- Narrow every custom message detail at the host boundary.\n- Prefer `setState()` for data changes and `updateStyle()` for CSS changes.\n- Use `render()` when scripts or global state must reset.\n- Handle `SandboxTimeoutError` at every non-test render boundary.\n- Dispose one sandbox with each owning component.\n- Tie related asynchronous work to `disposalSignal`.\n- Keep network access blocked unless the application requires a different isolation model.\n",
7
- "examples": "---\ntitle: Sandbox — Examples\ndescription: Recipes for common Sandbox use cases — component previews, user script sandboxes, and embedded widgets.\n---\n\n## Examples\n\n- [Component Preview](./examples/component-preview.md)\n- [User Script Sandbox](./examples/user-script-sandbox.md)\n- [Embedded Widget](./examples/embedded-widget.md)\n- [AI UI Renderer](./examples/ai-ui-renderer.md)\n"
4
+ "index": "---\ntitle: 'Sandbox: Sandboxed iframe runtime'\ndescription: 'Isolated iframe runtime with a typed state bridge and explicit message trust boundary for safe execution of untrusted HTML: component previews, playgrounds, plugin sandboxes, and more.'\npackage: sandbox\ncategory: ui-primitives\nkeywords: [sandbox, iframe, isolation, playground, csp, postmessage, security, components]\nexports:\n [\n createSandbox,\n SandboxConfigurationError,\n SandboxError,\n SandboxTimeoutError,\n SandboxHandle,\n SandboxOptions,\n SandboxBridge,\n SandboxMessage,\n ]\nrelated: [codex, refine]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"sandbox\" />\n\n## Why Sandbox?\n\nRunning untrusted HTML in the main window is unsafe: arbitrary code can access the DOM, cookies, and user data. Sandbox creates an isolated `<iframe sandbox=\"allow-scripts\">` that receives content over a typed state bridge and explicit message trust boundary. The sandbox cannot reach the host page.\n\n```ts\n// Before\ncontainer.innerHTML = untrustedHtml;\n\n// After\nconst sandbox = createSandbox(container);\nawait sandbox.render(untrustedHtml);\n```\n\nCommon use cases:\n\n- **Component previews**: render isolated HTML/CSS examples in documentation or design tools\n- **Code playgrounds**: execute user-provided code with full error forwarding and state injection\n- **Plugin sandboxes**: host third-party or user-authored plugin UI without granting host access\n- **User-generated content**: display untrusted HTML (emails, form output, external widgets) safely\n- **Widget embedding**: wrap third-party widgets with strict CSP and bidirectional messaging\n- **AI-generated UI**: render LLM-produced HTML components with guaranteed isolation\n\n| Feature | Raw `<iframe>` | Sandbox |\n| -------------------------- | -------------------------------------------- | --------------------------------------------- |\n| Bundle size | 0 B (built-in) | <PackageInfo package=\"sandbox\" type=\"size\" /> |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Content-Security-Policy | Manual | Auto-generated, strict by default |\n| Managed message protocol | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Typed host state; untrusted inbound details |\n| Error forwarding | <ore-icon name=\"x\" size=\"16\"></ore-icon> | `onerror` + `unhandledrejection` → host |\n| Dispose / `using` | Manual `remove()` | `dispose()` + `[Symbol.dispose]` |\n\n<div class=\"decision-callout\">\n\n**Use Sandbox when** you need to render untrusted or user-provided HTML in the browser with iframe isolation, CSP enforcement, and explicit host/sandbox messaging.\n\n**Consider a raw `<iframe>` when** you only need to embed a known third-party URL: Sandbox is for programmatic `srcdoc` content, not URL-based embedding.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/sandbox\n```\n\n```sh [npm]\nnpm install @vielzeug/sandbox\n```\n\n```sh [yarn]\nyarn add @vielzeug/sandbox\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { createSandbox } from '@vielzeug/sandbox';\n\nconst container = document.getElementById('preview')!;\nconst sandbox = createSandbox<{ theme: 'dark' | 'light' }>(container);\n\nsandbox.onMessage((message) => {\n if (message.type === 'custom') console.log(message.event, message.detail);\n if (message.type === 'error') console.error(message.message);\n if (message.type === 'resize') console.log('height:', message.height);\n});\n\ntry {\n await sandbox.render('<ore-button variant=\"primary\">Click me</ore-button>');\n sandbox.setState({ theme: 'dark' });\n} catch (error) {\n console.error('Sandbox render failed', error);\n} finally {\n sandbox.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createSandbox<State>()`: Creates an isolated `<iframe sandbox=\"allow-scripts\">` with typed outbound state\n- `render(html)`: Creates or replaces the document and resolves when its bridge reports ready\n- `setState(update)`: Pushes one or more typed state values in one message\n- `replaceBody(html)`: Updates streamed body content without resetting head scripts and styles\n- `updateStyle(id, css)`: Patches a named style without replacing the document\n- `SandboxMessage`: Keeps sandbox-controlled custom details typed as `unknown` on the host\n- `SandboxBridge<State, Events>`: Types authored sandbox-side state subscriptions and event emission\n- `readyTimeout`: Rejects blocked document initialization with `SandboxTimeoutError`\n- Strict CSP: Uses `default-src 'none'` and blocks network requests by default\n- Error forwarding: Installs before user scripts and forwards uncaught errors and rejections\n- Stale-message protection: Rejects messages from superseded render generations\n- Disposable: Supports `dispose()`, `disposalSignal`, and `[Symbol.dispose]`\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Codex](/codex/): MCP server with `generate-sandbox-document` and `get-state-bridge-spec` tools; generates document templates for use with Sandbox\n- [Refine](/refine/): Web component library; renders correctly inside the sandbox via `<script>` injection and `allowedScriptOrigins`\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: 'Sandbox: API Reference'\ndescription: API reference for the @vielzeug/sandbox managed iframe runtime, bridge, errors, and testing utilities.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createSandbox()` | Create a managed sandbox iframe | Sync | The iframe is created lazily by `render()` |\n| `SandboxHandle.render()` | Replace the document and await readiness | Async | Injected scripts can trigger a timeout |\n| `SandboxHandle` | Update, observe, and dispose the iframe | Sync methods | State/body updates require a ready document |\n| `SandboxOptions` | Configure metadata, CSP sources, scripts, styles, and timeout | Sync validation | Invalid values throw during creation |\n| `SandboxMessage` | Host-side message union | Sync callback | Custom details are always `unknown` |\n| `SandboxBridge` | Typed sandbox-side `window.__sandbox__` API | Sync | Its generics do not validate host-side input |\n| `SandboxError` | Base class for package errors | Thrown/rejected | Narrow caught values with `instanceof` |\n\n## Package Entry Point\n\n| Import | Exports |\n| --- | --- |\n| `@vielzeug/sandbox` | Runtime, errors, and public types |\n| `@vielzeug/sandbox/testing` | `createSandboxTestHelpers()` |\n\n```ts\nimport {\n createSandbox,\n SandboxConfigurationError,\n SandboxError,\n SandboxTimeoutError,\n} from '@vielzeug/sandbox';\nimport type {\n SandboxBridge,\n SandboxHandle,\n SandboxMessage,\n SandboxOptions,\n} from '@vielzeug/sandbox';\n```\n\n## Runtime\n\n### `createSandbox<State>(container, options?)`\n\n```ts\nfunction createSandbox<State extends object = Record<string, unknown>>(\n container: HTMLElement,\n options?: SandboxOptions,\n): SandboxHandle<State>;\n```\n\nCreates a managed `<iframe sandbox=\"allow-scripts\">` in `container`. Construction validates options but does not create DOM. The first `render()` creates the iframe with `referrerpolicy=\"no-referrer\"` and a generated CSP.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `container` | `HTMLElement` | Host element that owns the iframe |\n| `options` | `SandboxOptions` | Optional document and runtime configuration |\n\n**Returns:** `SandboxHandle<State>`.\n\n`State` describes values sent from the trusted host to the sandbox through `setState()`. Ordinary interfaces are supported.\n\n```ts\ninterface PreviewState {\n locale: string;\n theme: 'dark' | 'light';\n}\n\nconst sandbox = createSandbox<PreviewState>(container, {\n styles: { theme: 'body { color-scheme: light; }' },\n title: 'Preview',\n});\n\nawait sandbox.render('<main>Ready</main>');\nsandbox.setState({ locale: 'en', theme: 'dark' });\n```\n\n### `SandboxHandle<State>`\n\n```ts\ninterface SandboxHandle<State extends object = Record<string, unknown>> {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n dispose(): void;\n onMessage(handler: (message: SandboxMessage) => void): () => void;\n render(html: string): Promise<void>;\n replaceBody(html: string): void;\n setState(update: Partial<State>): void;\n updateStyle(id: string, css: string): void;\n [Symbol.dispose](): void;\n}\n```\n\n| Method/property | Description |\n| --- | --- |\n| `render(html)` | Replace the complete document and await readiness |\n| `replaceBody(html)` | Replace body descendants without navigation |\n| `setState(update)` | Push typed host state |\n| `updateStyle(id, css)` | Patch a named style |\n| `onMessage(handler)` | Subscribe to untrusted iframe messages |\n| `disposed` / `disposalSignal` | Observe lifecycle state |\n| `dispose()` / `[Symbol.dispose]()` | Release the iframe and listeners |\n\n#### `render(html)`\n\nReplaces the complete `srcdoc` document. The returned Promise resolves when that document emits its internal ready message and rejects with `SandboxTimeoutError` after `readyTimeout`.\n\nA newer render supersedes and resolves the previous pending render. Disposal also resolves a pending render. Messages from superseded generations are ignored.\n\nThe generated bridge is installed in `<head>` before injected scripts and user HTML, so it can capture script errors during document initialization. Its ready message is sent on `DOMContentLoaded`.\n\n#### `replaceBody(html)`\n\nReplaces `document.body.innerHTML` without navigating. Head scripts, document/window listeners, and named styles survive. Body descendants, descendant listeners, references, form state, and scripts in replacement HTML do not.\n\nCall only after `render()` resolves. The method warns in development and does nothing when no ready document exists.\n\n#### `setState(update)`\n\nSends one object containing one or more state values. Inside the iframe, each key dispatches its own `sandbox:state-update` `CustomEvent` and reaches matching `SandboxBridge.onState()` subscribers.\n\n```ts\nsandbox.setState({ locale: 'en', theme: 'dark' });\n```\n\nCalls made before the document is ready are accumulated instead of dropped: the newest value per key is delivered in one message when the bridge reports ready, and again after each later `render()` so a re-rendered document starts with current state.\n\n#### `updateStyle(id, css)`\n\nUpdates a style declared in `options.styles`. Before the first render it updates only the render baseline. After readiness it also patches the live `<style>` element. Unknown IDs emit a development warning.\n\n#### `onMessage(handler)`\n\nSubscribes to application messages and returns an idempotent unsubscribe function. Internal ready messages are not forwarded.\n\n```ts\nsandbox.onMessage((message) => {\n if (message.type === 'resize') container.style.height = `${message.height}px`;\n});\n```\n\n#### Lifecycle\n\n`dispose()` removes the iframe, removes the host `message` listener, clears subscribers, resolves a pending render, and aborts `disposalSignal`. It is idempotent. `[Symbol.dispose]()` provides the same behavior for `using` declarations.\n\n## Types\n\n### `SandboxOptions`\n\n```ts\ninterface SandboxOptions {\n allowedFontOrigins?: string[];\n allowedImageOrigins?: string[];\n allowedScriptOrigins?: string[];\n allowedStyleOrigins?: string[];\n lang?: string;\n nonce?: string;\n readyTimeout?: number;\n scripts?: string[];\n styles?: Record<string, string>;\n title?: string;\n}\n```\n\n| Option | Default | Description |\n| --- | --- | --- |\n| `allowedFontOrigins` | `[]` | HTTP(S) origins added to `font-src`; otherwise `'none'` |\n| `allowedImageOrigins` | `[]` | HTTP(S) origins added to `img-src`; `data:` is always included |\n| `allowedScriptOrigins` | `[]` | HTTP(S) origins added to `script-src` |\n| `allowedStyleOrigins` | `[]` | HTTP(S) origins added to `style-src` |\n| `lang` | `'en'` | Basic BCP 47 language tag for `<html lang>` |\n| `nonce` | `undefined` | Base64/base64url-style nonce for the bridge script and CSP |\n| `readyTimeout` | `5000` | Positive finite timeout in milliseconds, at most `2^31 - 1` |\n| `scripts` | `[]` | Absolute HTTP(S) scripts injected before user HTML |\n| `styles` | `{}` | Named style blocks; IDs start with a letter and contain letters, digits, `_`, or `-` |\n| `title` | `''` | Escaped document title |\n\nAllowed origins reject paths, credentials, query strings, fragments, unsupported schemes, and CSP syntax. Script URLs must be absolute HTTP(S) URLs.\n\n### `SandboxMessage`\n\n```ts\ntype SandboxMessage =\n | { detail: unknown; event: string; type: 'custom' }\n | { message: string; stack?: string; type: 'error' }\n | { height: number; type: 'resize' };\n```\n\nAll fields originate in sandboxed code and are untrusted. Validate custom `detail`, and render text through escaping APIs rather than HTML injection.\n\n### `SandboxBridge<State, Events>`\n\nThe generated document exposes `window.__sandbox__`:\n\n```ts\ninterface SandboxBridge<\n State extends object = Record<string, unknown>,\n Events extends object = Record<string, unknown>,\n> {\n emit<K extends keyof Events & string>(\n event: K,\n ...detail: undefined extends Events[K] ? [detail?: Events[K]] : [detail: Events[K]]\n ): void;\n onState<K extends keyof State & string>(\n key: K,\n handler: (value: State[K]) => void,\n ): () => void;\n}\n```\n\nUse an ambient declaration in authored sandbox-side TypeScript:\n\n```ts\ninterface State {\n theme: 'dark' | 'light';\n}\n\ninterface Events {\n saved: { id: string };\n}\n\ndeclare interface Window {\n __sandbox__: import('@vielzeug/sandbox').SandboxBridge<State, Events>;\n}\n```\n\nThese generics check authored sandbox code only. They do not make host-side messages trustworthy.\n\n## Errors\n\n### `SandboxError`\n\nBase class for package errors. Accepts `opts?: ErrorOptions` for cause chaining.\n\n### `SandboxConfigurationError`\n\nThrown synchronously by `createSandbox()` when an option cannot produce a valid document or CSP.\n\n### `SandboxTimeoutError`\n\nRejected by `render()` when no ready message arrives before `readyTimeout`. An injected script may have blocked document initialization.\n\n## Testing\n\n```ts\nimport { createSandboxTestHelpers } from '@vielzeug/sandbox/testing';\n```\n\n`createSandboxTestHelpers(container)` returns `fireReady()`, `fireCustom()`, `fireError()`, and `fireResize()` helpers. Call `render()` first so the helper can read the iframe channel and generation metadata.\n",
6
+ "usage": "---\ntitle: 'Sandbox: Usage Guide'\ndescription: Render untrusted HTML, push typed state, receive sandbox messages, update live content, and manage iframe lifecycle.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a sandbox, subscribe before rendering, await readiness, and dispose it when finished.\n\n```ts\nimport { createSandbox } from '@vielzeug/sandbox';\n\nconst container = document.getElementById('preview')!;\nconst sandbox = createSandbox(container);\n\nsandbox.onMessage((message) => {\n if (message.type === 'error') console.error(message.message);\n if (message.type === 'resize') container.style.height = `${message.height}px`;\n});\n\ntry {\n await sandbox.render('<main><h1>Preview</h1></main>');\n} finally {\n sandbox.dispose();\n}\n```\n\nThe iframe is created by the first `render()` call. Each render creates a fresh document and resolves after its generated bridge reports ready.\n\n## Rendering HTML\n\nUse `render()` when scripts, styles, globals, and DOM state should reset together.\n\n```ts\nawait sandbox.render(`\n <style>body { font-family: system-ui; }</style>\n <button id=\"save\">Save</button>\n <script>\n document.querySelector('#save').addEventListener('click', () => {\n window.__sandbox__.emit('save');\n });\n </script>\n`);\n```\n\nA newer render supersedes a pending one. Messages from the old document are ignored through the per-render generation marker.\n\n## Pushing Typed State\n\nPass a state interface to `createSandbox()` and send partial updates with one object.\n\n```ts\ninterface PreviewState {\n locale: string;\n theme: 'dark' | 'light';\n user: { name: string };\n}\n\nconst sandbox = createSandbox<PreviewState>(container);\nawait sandbox.render('<main id=\"app\"></main>');\n\nsandbox.setState({\n locale: 'en',\n theme: 'dark',\n user: { name: 'Ada' },\n});\n```\n\nSubscribe inside the iframe through the typed bridge.\n\n```ts\ninterface PreviewState {\n locale: string;\n theme: 'dark' | 'light';\n}\n\ndeclare interface Window {\n __sandbox__: import('@vielzeug/sandbox').SandboxBridge<PreviewState>;\n}\n\nwindow.__sandbox__.onState('theme', (theme) => {\n document.documentElement.dataset.theme = theme;\n});\n```\n\n`setState()` is safe to call at any point in the lifecycle. Updates made before `render()` resolves are accumulated and delivered in one message when the bridge reports ready, so an early call never loses state.\n\n## Receiving Sandbox Messages\n\nSubscribe with `onMessage()` before rendering. The unsubscribe function detaches only that handler.\n\n```ts\nconst unsubscribe = sandbox.onMessage((message) => {\n if (message.type === 'custom' && message.event === 'saved') {\n if (isSavedDetail(message.detail)) persist(message.detail.id);\n }\n});\n\nfunction isSavedDetail(value: unknown): value is { id: string } {\n return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';\n}\n```\n\nCustom event details remain `unknown` on the host. The iframe is isolated, but its code still controls message payloads.\n\nUse `SandboxBridge<State, Events>` only to check authored sandbox-side calls.\n\n```ts\ninterface Events {\n saved: { id: string };\n}\n\ndeclare interface Window {\n __sandbox__: import('@vielzeug/sandbox').SandboxBridge<Record<string, never>, Events>;\n}\n\nwindow.__sandbox__.emit('saved', { id: '42' });\n```\n\n## Updating Live Content\n\n### Replace the body\n\nUse `replaceBody()` when streamed HTML should preserve head scripts, styles, and global listeners.\n\n```ts\nawait sandbox.render('<main>Waiting…</main>');\n\nlet html = '';\nfor await (const chunk of stream) {\n html += chunk;\n sandbox.replaceBody(html);\n}\n```\n\nReplacement uses `document.body.innerHTML`. Scripts in replacement HTML do not execute, and body descendant state and listeners are discarded.\n\n### Update a named style\n\nDeclare styles at construction and patch them without replacing the document.\n\n```ts\nconst sandbox = createSandbox(container, {\n styles: { theme: 'body { color-scheme: light; }' },\n});\n\nawait sandbox.render('<main>Preview</main>');\nsandbox.updateStyle('theme', 'body { color-scheme: dark; }');\n```\n\nCalling `updateStyle()` before the first render updates the render baseline. Unknown IDs emit a development warning.\n\n## Injecting Scripts and Configuring CSP\n\nList absolute external scripts in `scripts`. Their origins are added to `script-src` automatically.\n\n```ts\nconst sandbox = createSandbox(container, {\n allowedFontOrigins: ['https://fonts.gstatic.com'],\n allowedStyleOrigins: ['https://fonts.googleapis.com'],\n scripts: ['https://cdn.example.com/widget.js'],\n});\n```\n\nNetwork requests remain blocked by `connect-src 'none'`. The package does not expose arbitrary CSP directives.\n\nA nonce authorizes the generated bridge while preventing un-nonced inline user scripts in CSP Level 3 browsers.\n\n```ts\nconst sandbox = createSandbox(container, {\n nonce: crypto.randomUUID(),\n});\n```\n\n## Handling Errors and Timeouts\n\n`render()` rejects if the document does not become ready before `readyTimeout`.\n\n```ts\nimport { SandboxError, SandboxTimeoutError } from '@vielzeug/sandbox';\n\ntry {\n await sandbox.render(html);\n} catch (error) {\n if (error instanceof SandboxTimeoutError) {\n showError('A script blocked preview initialization.');\n } else if (error instanceof SandboxError) {\n showError(error.message);\n }\n}\n```\n\nUncaught iframe errors and unhandled rejections arrive through `onMessage()` as untrusted `error` messages.\n\n## Disposal\n\nUse `dispose()` or a `using` declaration to remove the iframe and host listeners.\n\n```ts\nusing sandbox = createSandbox(container);\nawait sandbox.render('<p>Temporary preview</p>');\n```\n\nTie other asynchronous work to `disposalSignal`.\n\n```ts\nconst response = await fetch('/preview-data', {\n signal: sandbox.disposalSignal,\n});\n```\n\n## Testing\n\nUse the testing subpath to complete `render()` and simulate messages in jsdom.\n\n```ts\nimport { createSandbox } from '@vielzeug/sandbox';\nimport { createSandboxTestHelpers } from '@vielzeug/sandbox/testing';\nimport { expect, it } from 'vitest';\n\nit('receives resize messages', async () => {\n const container = document.createElement('div');\n const sandbox = createSandbox(container);\n const helpers = createSandboxTestHelpers(container);\n const heights: number[] = [];\n\n sandbox.onMessage((message) => {\n if (message.type === 'resize') heights.push(message.height);\n });\n\n const render = sandbox.render('<p>Preview</p>');\n helpers.fireReady();\n await render;\n helpers.fireResize(240);\n\n expect(heights).toEqual([240]);\n sandbox.dispose();\n});\n```\n\n## Framework Integration\n\nOwn one sandbox per mounted component and dispose it during unmount.\n\n::: code-group\n\n```tsx [React]\nimport { createSandbox, type SandboxHandle } from '@vielzeug/sandbox';\nimport { useEffect, useRef } from 'react';\n\nexport function Preview({ html }: { html: string }) {\n const container = useRef<HTMLDivElement>(null);\n const sandbox = useRef<SandboxHandle>();\n\n useEffect(() => {\n sandbox.current = createSandbox(container.current!);\n return () => sandbox.current?.dispose();\n }, []);\n\n useEffect(() => {\n void sandbox.current?.render(html).catch(console.error);\n }, [html]);\n\n return <div ref={container} />;\n}\n```\n\n```vue [Vue]\n<script setup lang=\"ts\">\nimport { createSandbox, type SandboxHandle } from '@vielzeug/sandbox';\nimport { onMounted, onUnmounted, ref, watch } from 'vue';\n\nconst props = defineProps<{ html: string }>();\nconst container = ref<HTMLElement>();\nlet sandbox: SandboxHandle | undefined;\n\nonMounted(() => {\n sandbox = createSandbox(container.value!);\n void sandbox.render(props.html).catch(console.error);\n});\nwatch(() => props.html, (html) => void sandbox?.render(html).catch(console.error));\nonUnmounted(() => sandbox?.dispose());\n</script>\n\n<template><div ref=\"container\" /></template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { createSandbox, type SandboxHandle } from '@vielzeug/sandbox';\n import { onMount } from 'svelte';\n\n export let html: string;\n let container: HTMLElement;\n let sandbox: SandboxHandle | undefined;\n\n onMount(() => {\n sandbox = createSandbox(container);\n return () => sandbox?.dispose();\n });\n\n $: if (sandbox) void sandbox.render(html).catch(console.error);\n</script>\n\n<div bind:this={container}></div>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Sandbox to isolate generated markup that loads browser bundles of DOM-output packages such as Refine or Ore. Use Rune in the host to record validated message details; do not pass untrusted payloads directly to structured logs.\n\n## Best Practices\n\n- Await `render()` before sending state or replacing body content.\n- Narrow every custom message detail at the host boundary.\n- Prefer `setState()` for data changes and `updateStyle()` for CSS changes.\n- Use `render()` when scripts or global state must reset.\n- Handle `SandboxTimeoutError` at every non-test render boundary.\n- Dispose one sandbox with each owning component.\n- Tie related asynchronous work to `disposalSignal`.\n- Keep network access blocked unless the application requires a different isolation model.\n",
7
+ "examples": "---\ntitle: 'Sandbox: Examples'\ndescription: 'Recipes for common Sandbox use cases: component previews, user script sandboxes, and embedded widgets.'\n---\n\n## Examples\n\n- [Component Preview](./examples/component-preview.md)\n- [User Script Sandbox](./examples/user-script-sandbox.md)\n- [Embedded Widget](./examples/embedded-widget.md)\n- [AI UI Renderer](./examples/ai-ui-renderer.md)\n"
8
8
  },
9
9
  "examples": [
10
10
  {