@vielzeug/codex 2.2.8 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  {
2
- "apiSource": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';\nexport { scheduleExpiredPrune } from './prune';\nexport type { QueryBuilder } from './query';\nexport { isExpired, ttl } from './ttl';\nexport type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';\nexport { table } from './types';\n",
2
+ "apiSource": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';\nexport type { QueryBuilder } from './query';\nexport { isExpired, ttl } from './ttl';\nexport type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';\nexport { table } from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Vault — Typed storage\ndescription: Typed browser storage and opt-in driver-neutral SQLite with portable keys, TTL, observation, and transactions.\npackage: vault\ncategory: Storage\nkeywords: [storage, indexeddb, localstorage, sessionstorage, sqlite, ttl, browser, node, deno]\nrelated: [courier, forge, ripple]\nexports: [table, ttl, scheduleExpiredPrune, isExpired, createMemory, createLocalStorage, createSessionStorage, createIndexedDB, createSQLite]\nenvironments: [browser, node, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"vault\" />\n\n## Why Vault?\n\nVault gives browser and SQLite persistence one typed schema while keeping backend guarantees explicit. Use `VaultStore` for portable CRUD and observation; choose IndexedDB or the opt-in SQLite subpath when you need atomic transactions or lazy iteration.\n\n```ts\n// Before\nlocalStorage.setItem('theme', JSON.stringify({ value: 'dark' }));\nconst theme = JSON.parse(localStorage.getItem('theme') ?? '{}').value;\n\n// After\nawait store.put('preferences', { id: 'theme', value: 'dark' });\nconst theme = await store.get('preferences', 'theme');\n```\n\n| Feature | Vault | Raw Web Storage | Dexie |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"vault\" type=\"size\" /> | Browser built-in | Extra dependency |\n| Runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed schema and keys | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Portable Memory/Web Storage API | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | IndexedDB only |\n| Explicit atomic transactions | IndexedDB capability | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Driver-neutral SQLite | Opt-in subpath | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Vault when** you need typed browser persistence or application-owned SQLite with one portable CRUD API and explicit storage capabilities.\n\n**Consider raw Web Storage when** you only persist one or two unstructured values. **Consider Dexie when** you need a broader IndexedDB ecosystem.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/vault\n```\n\n```sh [npm]\nnpm install @vielzeug/vault\n```\n\n```sh [yarn]\nyarn add @vielzeug/vault\n```\n\n:::\n\n## Quick Start\n\nDefine a schema, create a portable store, and dispose it with its owner.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<{ id: string; theme: 'dark' | 'light' }>('id') },\n});\n\ntry {\n await store.put('preferences', { id: 'theme', theme: 'dark' });\n console.log(await store.get('preferences', 'theme'));\n} finally {\n await store.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `table()` defines typed records with portable string or number keys.\n- `/memory`, `/local-storage`, and `/session-storage` return portable `VaultStore` instances without loading other adapters.\n- `observe()` emits current and changed table snapshots.\n- `ttl` creates validated expiration durations.\n- `/indexeddb` returns `IndexedDbVaultStore` with `batch()` and `iterate()`.\n- `createSQLite()` is an opt-in, driver-neutral subpath for Node, Bun, and Deno SQLite drivers.\n- `/indexeddb` also exports `defineMigration()` for schema upgrades.\n- `scheduleExpiredPrune()` removes stale TTL entries on an owned schedule.\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- [Forge](../forge/index.md) saves and restores form drafts through Vault stores.\n- [Ripple](../ripple/index.md) owns application state that can persist through Vault.\n- [Courier](../courier/index.md) can populate persistent cache data.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Vault — API Reference\ndescription: Reference for Vault schemas, adapter entry points, storage capabilities, SQLite drivers, and errors.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createMemory()` | In-memory portable store | Async API | Import from `/memory` |\n| `createLocalStorage()` / `createSessionStorage()` | Web Storage-backed portable stores | Async API | Available only where the corresponding Web API exists |\n| `createIndexedDB()` | Browser transactions and cursor iteration | Async API | Import from `/indexeddb` |\n| `createSQLite()` | Driver-neutral SQLite store | Async API over a synchronous driver | Import from `/sqlite` |\n| `table()` | Typed record schema | Sync | The key field must be a string or finite number |\n| `ttl` | Valid expiration durations | Sync | Durations must be positive |\n| `scheduleExpiredPrune()` | Periodic TTL cleanup | Sync setup, async work | Pass `disposalSignal` to auto-cancel |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/vault` | Adapter-free schemas, TTL, errors, pruning, queries, and shared types |\n| `@vielzeug/vault/memory` | `createMemory` |\n| `@vielzeug/vault/local-storage` | `createLocalStorage` |\n| `@vielzeug/vault/session-storage` | `createSessionStorage` |\n| `@vielzeug/vault/indexeddb` | `createIndexedDB`, `defineMigration`, migrations, and IndexedDB-only types |\n| `@vielzeug/vault/sqlite` | `createSQLite`, the SQLite driver protocol types, and `TransactionContext` |\n\n## Schemas and TTL\n\n### `table()`\n\n```ts\nfunction table<T extends object, Key extends keyof T & string = keyof T & string>(\n key: Key & (T[Key] extends VaultKey ? unknown : never),\n options?: { defaultTtl?: number; indexes?: readonly (keyof T & string)[] },\n): SchemaEntry<T, Key>;\n```\n\nDefines a typed table and its primary-key field.\n\n| Parameter | Description |\n| --- | --- |\n| `key` | A record field whose values are `string` or finite `number` keys |\n| `options.defaultTtl` | Per-table default TTL in milliseconds |\n| `options.indexes` | IndexedDB secondary index fields |\n\n**Returns:** A `SchemaEntry` describing the table.\n\n```ts\nimport { table, ttl } from '@vielzeug/vault';\n\nconst users = table<{ id: number; email: string }>('id', {\n indexes: ['email'],\n defaultTtl: ttl.days(7),\n});\n```\n\n---\n\n### `ttl`\n\n```ts\nconst ttl: {\n days(n: number): number;\n hours(n: number): number;\n minutes(n: number): number;\n ms(n: number): number;\n seconds(n: number): number;\n};\n```\n\nCreates a finite, positive duration in milliseconds for writes and table defaults.\n\n**Returns:** `number`.\n\n```ts\nimport { ttl } from '@vielzeug/vault';\n\nconst cacheLifetime = ttl.minutes(5);\n```\n\n---\n\n### `isExpired()`\n\n```ts\nfunction isExpired(expiresAt: number | undefined): boolean;\n```\n\nReports whether an expiration timestamp has passed.\n\n**Returns:** `true` when `expiresAt` is defined and no later than the current time.\n\n```ts\nimport { isExpired } from '@vielzeug/vault';\n\nif (isExpired(record.expiresAt)) console.log('expired');\n```\n\n## Factories\n\nAll factory options accept `schema`, plus optional `validators`, `logger`, and `onMetrics`. The root entry does not export any factory.\n\n### `createMemory()`\n\n```ts\nfunction createMemory<S extends AnySchema>(options: {\n name?: string;\n schema: S;\n} & BaseAdapterOptions<S>): VaultStore<S>;\n```\n\nCreates an in-memory portable store. A `name` enables same-origin `BroadcastChannel` observation between memory stores when the platform provides it.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables created by `table()` |\n| `name` | Optional shared memory-store namespace |\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createMemory } from '@vielzeug/vault/memory';\n\nconst store = createMemory({ schema: { users: table<{ id: number; name: string }>('id') } });\n```\n\n---\n\n### `createLocalStorage()`\n\n```ts\nfunction createLocalStorage<S extends AnySchema>(options: {\n name: string;\n onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';\n schema: S;\n} & BaseAdapterOptions<S>): VaultStore<S>;\n```\n\nCreates a namespaced `localStorage` store.\n\n| Parameter | Description |\n| --- | --- |\n| `name` | Required storage namespace |\n| `onQuotaExceeded` | Handles a Web Storage quota error; returning `'ignore'` drops that write |\n| `schema` | Tables created by `table()` |\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\nconst store = createLocalStorage({ name: 'app', schema: { settings: table<{ id: string }>('id') } });\n```\n\n---\n\n### `createSessionStorage()`\n\n```ts\nfunction createSessionStorage<S extends AnySchema>(options: {\n name: string;\n onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';\n schema: S;\n} & BaseAdapterOptions<S>): VaultStore<S>;\n```\n\nCreates a namespaced `sessionStorage` store. Its options and return type match `createLocalStorage()`.\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createSessionStorage } from '@vielzeug/vault/session-storage';\n\nconst store = createSessionStorage({ name: 'checkout', schema: { cart: table<{ id: string }>('id') } });\n```\n\n---\n\n### `createIndexedDB()`\n\n```ts\nfunction createIndexedDB<S extends AnySchema>(options: {\n migrate?: MigrationFn;\n name: string;\n schema: S;\n version?: number;\n} & BaseAdapterOptions<S>): IndexedDbVaultStore<S>;\n```\n\nCreates an IndexedDB store with atomic batches, lazy cursor iteration, and optional schema migrations.\n\n| Parameter | Description |\n| --- | --- |\n| `name` | Required database name |\n| `schema` | Tables and IndexedDB secondary indexes |\n| `version` | Positive schema version; defaults to `1` |\n| `migrate` | Synchronous upgrade callback for version changes |\n\n**Returns:** `IndexedDbVaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\n\nconst store = createIndexedDB({ name: 'app', schema: { users: table<{ id: number }>('id') } });\n```\n\n---\n\n### `createSQLite()`\n\n```ts\nfunction createSQLite<S extends AnySchema>(options: SQLiteVaultOptions<S>): SQLiteVaultStore<S>;\n```\n\nCreates a namespaced SQLite store with atomic batches and keyset-paginated iteration. It accepts an application-provided positional-parameter driver and never opens or imports a runtime driver.\n\n| Parameter | Description |\n| --- | --- |\n| `database` | Caller-provided `SQLiteDatabase` connection |\n| `name` | Namespace within the connection |\n| `schema`, `validators`, `logger`, `onMetrics` | Shared factory options |\n| `closeOnDispose` | Closes the connection during disposal; defaults to `false` |\n\n**Returns:** `SQLiteVaultStore<S>`.\n\n```ts\nimport { DatabaseSync } from 'node:sqlite';\n\nimport { table } from '@vielzeug/vault';\nimport { createSQLite } from '@vielzeug/vault/sqlite';\n\nconst store = createSQLite({\n database: new DatabaseSync(':memory:'),\n name: 'tests',\n schema: { users: table<{ id: number; name: string }>('id') },\n});\n```\n\nNode `DatabaseSync`, Bun `Database`, and Deno `jsr:@db/sqlite` `Database` satisfy the protocol. Values must be JSON-compatible plain objects. During a `batch()` callback, calls on every Vault store sharing that connection reject; use `tx.*` instead.\n\n## Store Capabilities\n\n### `VaultStore`\n\n```ts\ninterface VaultStore<S extends AnySchema> {\n clear<K extends keyof S & string>(table: K): Promise<void>;\n count<K extends keyof S & string>(table: K): Promise<number>;\n delete<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n deleteMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<number>;\n entries<K extends keyof S & string>(table: K): Promise<Array<[KeyOf<S, K>, RecordOf<S, K>]>>;\n get<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<RecordOf<S, K> | undefined>;\n getAll<K extends keyof S & string>(table: K): Promise<RecordOf<S, K>[]>;\n getMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<Array<RecordOf<S, K> | undefined>>;\n getOrDefault<K extends keyof S & string>(table: K, key: KeyOf<S, K>, defaultFn: () => RecordOf<S, K>, ttl?: number): Promise<RecordOf<S, K>>;\n has<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n isEmpty<K extends keyof S & string>(table: K): Promise<boolean>;\n keys<K extends keyof S & string>(table: K, filter?: (record: RecordOf<S, K>) => boolean): Promise<KeyOf<S, K>[]>;\n put<K extends keyof S & string>(table: K, value: RecordOf<S, K>, ttl?: number): Promise<void>;\n putAll<K extends keyof S & string>(table: K, values: RecordOf<S, K>[], ttl?: number): Promise<void>;\n query<K extends keyof S & string>(table: K): QueryBuilder<RecordOf<S, K>>;\n update<K extends keyof S & string>(table: K, key: KeyOf<S, K>, changes: Partial<RecordOf<S, K>>, ttl?: number): Promise<RecordOf<S, K> | undefined>;\n upsert<K extends keyof S & string>(table: K, key: KeyOf<S, K>, fn: (existing: RecordOf<S, K> | undefined) => RecordOf<S, K>, ttl?: number): Promise<RecordOf<S, K>>;\n pruneExpired(): Promise<Record<keyof S & string, number>>;\n debug(): Promise<DebugInfo<S>>;\n observe<K extends keyof S & string>(table: K, listener: Observer<RecordOf<S, K>>, options?: { immediate?: boolean; signal?: AbortSignal }): Unsubscribe;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n [Symbol.asyncDispose](): Promise<void>;\n}\n```\n\nThe portable store API is returned by every factory. `observe()` emits the current table snapshot by default and then emits after mutations.\n\n---\n\n### `batch()`\n\n```ts\ninterface TransactionalVaultStore<S extends AnySchema> extends VaultStore<S> {\n batch<K extends keyof S & string, R>(\n tables: readonly K[],\n fn: (tx: TransactionContext<S, K>) => Promise<R>,\n ): Promise<R>;\n}\n```\n\nRuns a scoped atomic callback. `IndexedDbVaultStore` and `SQLiteVaultStore` provide it.\n\n| Parameter | Description |\n| --- | --- |\n| `tables` | Tables the transaction may access |\n| `fn` | Async callback that uses only the supplied `tx` context |\n\n**Returns:** The callback result after commit.\n\n```ts\nawait store.batch(['users'], async (tx) => {\n await tx.put('users', { id: 1, name: 'Ada' });\n});\n```\n\n---\n\n### `iterate()`\n\n```ts\ninterface IterableVaultStore<S extends AnySchema> extends VaultStore<S> {\n iterate<K extends keyof S & string>(table: K): AsyncIterable<RecordOf<S, K>>;\n}\n```\n\nLazily yields table records. `IndexedDbVaultStore` uses a cursor; `SQLiteVaultStore` uses keyset pagination.\n\n**Returns:** An `AsyncIterable` of records.\n\n```ts\nfor await (const user of store.iterate('users')) console.log(user);\n```\n\n## Queries, Pruning, and Migrations\n\n### `QueryBuilder`\n\n```ts\ninterface QueryBuilder<T extends object, N extends T = T> {\n between(field: string, lower: number | string, upper: number | string): QueryBuilder<T, N>;\n count(): Promise<number>;\n delete(): Promise<number>;\n equals<K extends keyof T & string, V extends T[K]>(field: K, value: V): QueryBuilder<T & Record<K, V>>;\n exists(): Promise<boolean>;\n filter(fn: (value: N, index: number, array: N[]) => boolean): QueryBuilder<T, N>;\n first(): Promise<N | undefined>;\n limit(n: number): QueryBuilder<T, N>;\n offset(n: number): QueryBuilder<T, N>;\n orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): QueryBuilder<T, N>;\n startsWith(field: keyof T, prefix: string, options?: { ignoreCase?: boolean }): QueryBuilder<T, N>;\n toArray(): Promise<N[]>;\n}\n```\n\nBuilds a lazy table query. `count()` ignores `limit()`, `offset()`, and `orderBy()` — it always returns the full filtered-set size.\n\n```ts\nconst page = await store.query('users').startsWith('name', 'A').orderBy('name').limit(20).toArray();\n```\n\n---\n\n### `scheduleExpiredPrune()`\n\n```ts\nfunction scheduleExpiredPrune<S extends AnySchema>(\n adapter: Pick<VaultStore<S>, 'pruneExpired'>,\n options: {\n interval: number;\n onError?: (error: unknown) => void;\n signal?: AbortSignal;\n },\n): () => void;\n```\n\nSchedules `pruneExpired()` at a finite, positive interval. Pass `signal: store.disposalSignal` to auto-cancel when the store is torn down.\n\n**Returns:** A stop function.\n\n```ts\nimport { scheduleExpiredPrune, ttl } from '@vielzeug/vault';\n\nconst stop = scheduleExpiredPrune(store, {\n interval: ttl.hours(1),\n signal: store.disposalSignal,\n});\nstop();\n```\n\n---\n\n### `defineMigration()`\n\n```ts\nfunction defineMigration(steps: MigrationStep[]): MigrationFn;\n```\n\nBuilds an idempotent IndexedDB migration callback from schema-change steps.\n\n**Returns:** An IndexedDB `MigrationFn`.\n\n```ts\nimport { defineMigration } from '@vielzeug/vault/indexeddb';\n\nconst migrate = defineMigration([{ field: 'email', table: 'users', type: 'addIndex' }]);\n```\n\n## Types\n\n```ts\ntype VaultKey = number | string;\ntype Unsubscribe = () => void;\ntype Observer<T> = (records: T[]) => void;\ntype AnySchema = Record<string, {\n defaultTtl?: number;\n indexes?: readonly string[];\n key: string;\n}>;\ntype SchemaEntry<T extends object, Key extends keyof T & string = keyof T & string> =\n T[Key] extends VaultKey ? {\n defaultTtl?: number;\n indexes?: readonly (keyof T & string)[];\n key: Key;\n } : never;\ntype RecordOf<S extends AnySchema, K extends keyof S> =\n S[K] extends SchemaEntry<infer R, infer _Key> ? R : never;\ntype KeyOf<S extends AnySchema, K extends keyof S> =\n Extract<S[K] extends SchemaEntry<infer R, infer Key> ? R[Key] : never, VaultKey>;\n```\n\n```ts\ntype BaseAdapterOptions<S extends AnySchema> = {\n logger?: VaultLogger;\n onMetrics?: (event: MetricsEvent) => void;\n schema: S;\n validators?: TableValidators<S>;\n};\n\ntype VaultLogger = {\n error(message: string, context?: Error | Record<string, unknown>): void;\n};\n\ntype RecordValidator<T> = {\n parse(value: unknown): T;\n};\n\ntype TableValidators<S extends AnySchema> = {\n [K in keyof S]?: RecordValidator<RecordOf<S, K>>;\n};\n\ntype MetricsEvent = {\n duration: number;\n operation: 'batch' | 'clear' | 'count' | 'delete' | 'deleteMany' | 'entries' | 'get' | 'getAll' |\n 'getMany' | 'getOrDefault' | 'has' | 'isEmpty' | 'keys' | 'put' | 'putAll' | 'query' |\n 'queryDelete' | 'update' | 'upsert';\n table: string;\n};\n\ntype DebugStats = { expiredCount: number; recordCount: number };\ntype DebugInfo<S extends AnySchema> = { tables: Array<{ name: keyof S & string } & DebugStats> };\n```\n\n```ts\ninterface IndexedDbVaultStore<S extends AnySchema>\n extends TransactionalVaultStore<S>, IterableVaultStore<S> {}\n\ntype MigrationContext = {\n db: IDBDatabase;\n newVersion: number | null;\n oldVersion: number;\n tx: IDBTransaction;\n};\n\ntype MigrationFn = (ctx: MigrationContext) => void;\n\ntype MigrationStep =\n | { field: string; table: string; type: 'addIndex' }\n | { field: string; table: string; type: 'removeIndex' }\n | { name: string; type: 'addTable' }\n | { name: string; type: 'removeTable' };\n```\n\nImport `MigrationContext`, `MigrationFn`, and `MigrationStep` from `@vielzeug/vault/indexeddb`.\n\n```ts\ntype SQLiteParameter = null | number | string;\n\ninterface SQLiteStatement {\n all(...parameters: SQLiteParameter[]): readonly Record<string, unknown>[];\n finalize?(): void;\n get(...parameters: SQLiteParameter[]): Record<string, unknown> | undefined;\n run(...parameters: SQLiteParameter[]): unknown;\n}\n\ninterface SQLiteDatabase {\n close?(): void;\n exec(sql: string): void;\n prepare(sql: string): SQLiteStatement;\n}\n\ntype SQLiteVaultOptions<S extends AnySchema> = BaseAdapterOptions<S> & {\n closeOnDispose?: boolean;\n database: SQLiteDatabase;\n name: string;\n};\n\ninterface SQLiteVaultStore<S extends AnySchema>\n extends TransactionalVaultStore<S>, IterableVaultStore<S> {}\n```\n\n`TransactionContext` has the same CRUD, query, and TTL methods as `VaultStore`, narrowed to the tables declared in `batch()`. Import it from `@vielzeug/vault/indexeddb` or `@vielzeug/vault/sqlite`.\n\n## Errors\n\n| Error | Trigger |\n| --- | --- |\n| `VaultError` | Any Vault-originated validation, serialization, storage, or query error |\n| `VaultDisposedError` | An operation after the store or observer hub is disposed |\n| `VaultScopeError` | An IndexedDB transaction accesses a table outside its declared batch scope |\n| `VaultQuotaError` | A LocalStorage or SessionStorage write exceeds the browser quota |\n| `VaultMigrationError` | An IndexedDB migration callback throws |\n\nEvery listed error extends `VaultError`.\n",
6
- "usage": "---\ntitle: Vault — Usage Guide\ndescription: Persist typed browser or SQLite data, observe table snapshots, and use atomic transactions.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a portable store with one schema and write a typed row.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\ninterface Preference {\n id: string;\n theme: 'dark' | 'light';\n}\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n\nawait store.put('preferences', { id: 'theme', theme: 'dark' });\nconsole.log(await store.get('preferences', 'theme'));\n```\n\n## Create a Portable Store\n\nMemory, LocalStorage, and SessionStorage return `VaultStore`. They share portable string/number keys, CRUD methods, queries, TTL, and `observe()`. Vault keeps record values and expiry metadata separate; the physical storage layout is adapter-specific.\n\nThe root entry is adapter-free. Import `createMemory` from `@vielzeug/vault/memory`, `createLocalStorage` from `@vielzeug/vault/local-storage`, or `createSessionStorage` from `@vielzeug/vault/session-storage`. Import each adapter from its focused subpath so unused backends stay out of the bundle.\n\nUse a new storage name when upgrading from Vault 1. Old key and envelope formats are not read by Vault 2.\n\n```ts\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n```\n\n## Read and Change Records\n\nUse `update()` for an existing row and `upsert()` when the row may not exist.\n\n```ts\nconst updated = await store.update('preferences', 'theme', { theme: 'light' });\n\nawait store.upsert('preferences', 'locale', (current) => ({\n id: 'locale',\n theme: current?.theme ?? 'dark',\n}));\n\nconsole.log(updated);\n```\n\n`update()` returns `undefined` for a missing key. `upsert()` always writes the record returned by its callback.\n\n## Query Records\n\nBuild a query from a table, then finish it with a terminal method. `count()` ignores pagination, which makes it suitable for page controls.\n\n```ts\nconst query = store.query('preferences').startsWith('id', 'theme');\nconst preferences = await query.orderBy('id').limit(10).toArray();\nconst total = await query.count();\n\nconsole.log({ preferences, total });\n```\n\nMemory and Web Storage queries scan the table. IndexedDB can use declared secondary indexes, while SQLite pushes primary-key equality, range, and case-sensitive prefix filters to the database.\n\n## Use TTL and Pruning\n\nUse `ttl.*` helpers for expiring rows. Schedule pruning when stale rows can accumulate without reads.\n\n```ts\nimport { scheduleExpiredPrune, ttl } from '@vielzeug/vault';\n\nawait store.put('preferences', { id: 'temporary', theme: 'dark' }, ttl.hours(1));\nconst stopPrune = scheduleExpiredPrune(store, {\n interval: ttl.hours(6),\n signal: store.disposalSignal,\n});\n\nstopPrune();\n```\n\n## Observe a Table\n\nUse `observe()` for current and future snapshots. Tie subscription lifetime to an `AbortSignal` when a component or request owns it.\n\n```ts\nconst controller = new AbortController();\n\nstore.observe('preferences', (preferences) => {\n console.log(preferences);\n}, { signal: controller.signal });\n\ncontroller.abort();\n```\n\n## Use IndexedDB for Browser Transactions\n\nChoose IndexedDB when browser storage needs multiple writes to commit together or cursor iteration.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\n\nconst db = createIndexedDB({\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n});\n\nawait db.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened' });\n await tx.put('events', { id: 2, type: 'saved' });\n});\n```\n\nOnly await `tx.*` operations inside a batch callback. Do not await timers, fetches, or other external asynchronous work; IndexedDB can commit an inactive transaction.\n\n## Use SQLite Outside the Browser\n\nImport SQLite from the opt-in subpath so the browser root stays free of runtime drivers. Vault never opens a connection or configures its SQLite process behavior for you.\n\n```ts\nimport { DatabaseSync } from 'node:sqlite';\n\nimport { table } from '@vielzeug/vault';\nimport { createSQLite } from '@vielzeug/vault/sqlite';\n\nconst database = new DatabaseSync('app.db', { timeout: 5_000 });\nconst store = createSQLite({\n database,\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n});\n\nawait store.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened' });\n await tx.put('events', { id: 2, type: 'saved' });\n});\n```\n\nNode's `node:sqlite` API is experimental. Bun's `bun:sqlite` `Database` satisfies the same positional `exec()` and `prepare()` contract; configure WAL from your application when the deployment needs it. Deno does not include SQLite, but `jsr:@db/sqlite`'s `Database` satisfies the same contract when its FFI, filesystem, and environment permissions are granted.\n\nSQLite stores serialize all access through the injected connection. `batch()` starts `BEGIN IMMEDIATE` and rolls back callback failures. While its callback runs, calls on any store sharing that connection reject rather than waiting behind the transaction; use `tx.*` instead. The underlying drivers are synchronous, so move large scans and writes to a worker or isolate when event-loop latency matters.\n\n## Store SQLite Values and Observe Changes\n\nSQLite accepts JSON-compatible plain-object records only. Circular values, `bigint`, dates, class instances, functions, and non-finite numbers are rejected before writing. Number and string primary keys remain distinct.\n\n`observe()` sees mutations written through Vault stores sharing the same injected connection after a commit. It cannot detect direct SQL changes, writes from another process, or writes through another connection. The connection belongs to the caller by default; use `closeOnDispose: true` only when the store owns it.\n\n## Handle IndexedDB Schema Migrations\n\nDeclare IndexedDB indexes in the schema. Use `migrate` only for IndexedDB version upgrades and mirror Vault’s fixed `value.<field>` index path.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB, type MigrationFn } from '@vielzeug/vault/indexeddb';\n\nconst schema = { users: table<{ id: number; name: string }>('id', { indexes: ['name'] }) };\nconst migrate: MigrationFn = ({ db, oldVersion, tx }) => {\n if (oldVersion < 2 && db.objectStoreNames.contains('users')) {\n tx.objectStore('users').createIndex('name', 'value.name');\n }\n};\n\ncreateIndexedDB({ name: 'app-v2', migrate, schema, version: 2 });\n```\n\n## Framework Integration\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const [rows, setRows] = useState<RecordOf<S, K>[]>([]);\n\n useEffect(() => store.observe(table, setRows), [store, table]);\n return rows;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const rows = shallowRef<RecordOf<S, K>[]>([]);\n const stop = store.observe(table, (next) => (rows.value = next));\n\n onUnmounted(stop);\n return rows;\n}\n```\n\n```ts [Svelte]\nimport { readable } from 'svelte/store';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function tableStore<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n return readable<RecordOf<S, K>[]>([], (set) => store.observe(table, set));\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Forge’s Vault helpers for explicit form-draft persistence. Keep Ripple signals as application state and persist selected changes through Vault writes.\n\n## Best Practices\n\n- Define one schema per storage namespace.\n- Use string or finite-number primary keys only.\n- Choose a new namespace for Vault 1 storage unless you migrate it yourself.\n- Use `observe()` for table snapshots.\n- Use IndexedDB or SQLite for atomic work.\n- Keep external asynchronous work outside `batch()` callbacks.\n- Use `ttl.*` instead of raw durations.\n- Keep SQLite scans and writes off latency-sensitive event loops, and dispose stores with their owner.\n- Dispose stores when their owner ends.\n",
4
+ "index": "---\ntitle: Vault — Typed storage\ndescription: Typed browser storage and opt-in driver-neutral SQLite with portable keys, TTL, observation, and transactions.\npackage: vault\ncategory: Storage\nkeywords: [storage, indexeddb, localstorage, sessionstorage, sqlite, ttl, browser, node, deno]\nrelated: [courier, forge, ripple]\nexports: [table, ttl, isExpired, createMemory, createLocalStorage, createSessionStorage, createIndexedDB, createSQLite, defineMigration]\nenvironments: [browser, node, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"vault\" />\n\n## Why Vault?\n\nVault gives browser and SQLite persistence one typed schema while keeping backend guarantees explicit. Use `VaultStore` for portable CRUD and observation; choose IndexedDB or the opt-in SQLite subpath when you need atomic transactions or lazy iteration.\n\n```ts\n// Before\nlocalStorage.setItem('theme', JSON.stringify({ value: 'dark' }));\nconst theme = JSON.parse(localStorage.getItem('theme') ?? '{}').value;\n\n// After\nawait store.put('preferences', { id: 'theme', value: 'dark' });\nconst theme = await store.get('preferences', 'theme');\n```\n\n| Feature | Vault | Raw Web Storage | Dexie |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"vault\" type=\"size\" /> | Browser built-in | Extra dependency |\n| Runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed schema and keys | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Portable Memory/Web Storage API | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | IndexedDB only |\n| Explicit atomic transactions | IndexedDB capability | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Driver-neutral SQLite | Opt-in subpath | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Vault when** you need typed browser persistence or application-owned SQLite with one portable CRUD API and explicit storage capabilities.\n\n**Consider raw Web Storage when** you only persist one or two unstructured values. **Consider Dexie when** you need a broader IndexedDB ecosystem.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/vault\n```\n\n```sh [npm]\nnpm install @vielzeug/vault\n```\n\n```sh [yarn]\nyarn add @vielzeug/vault\n```\n\n:::\n\n## Quick Start\n\nDefine a schema, create a portable store, and dispose it with its owner.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<{ id: string; theme: 'dark' | 'light' }>('id') },\n});\n\ntry {\n await store.put('preferences', { id: 'theme', theme: 'dark' });\n console.log(await store.get('preferences', 'theme'));\n} finally {\n await store.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `table()` defines typed records with portable string or number keys.\n- `/memory`, `/local-storage`, and `/session-storage` return portable `VaultStore` instances without loading other adapters.\n- `observe()` emits current and changed table snapshots.\n- `ttl` creates validated expiration durations.\n- `/indexeddb` returns `TransactionalVaultStore` with `batch()` and `iterate()`.\n- `createSQLite()` is an opt-in, driver-neutral subpath for Node, Bun, and Deno SQLite drivers.\n- `/indexeddb` also exports `defineMigration()` for schema upgrades.\n- `pruneExpired()` removes stale TTL entries on demand.\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- [Forge](../forge/index.md) saves and restores form drafts through Vault stores.\n- [Ripple](../ripple/index.md) owns application state that can persist through Vault.\n- [Courier](../courier/index.md) can populate persistent cache data.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Vault — API Reference\ndescription: Reference for Vault schemas, adapter entry points, storage capabilities, SQLite drivers, and errors.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createMemory()` | In-memory portable store | Async API | Import from `/memory` |\n| `createLocalStorage()` / `createSessionStorage()` | Web Storage-backed portable stores | Async API | Available only where the corresponding Web API exists |\n| `createIndexedDB()` | Browser transactions and cursor iteration | Async API | Import from `/indexeddb` |\n| `createSQLite()` | Driver-neutral SQLite store | Async API over a synchronous driver | Import from `/sqlite` |\n| `defineMigration()` | Declarative IndexedDB schema upgrade | Sync | Import from `/indexeddb` |\n| `table()` | Typed record schema | Sync | The key field must be a string or finite number |\n| `ttl` | Valid expiration durations | Sync | Durations must be positive |\n| `isExpired()` | Check an expiration timestamp | Sync | Returns `false` when no expiry is set |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/vault` | Adapter-free schemas, TTL, errors, pruning, queries, and shared types |\n| `@vielzeug/vault/memory` | `createMemory` |\n| `@vielzeug/vault/local-storage` | `createLocalStorage` |\n| `@vielzeug/vault/session-storage` | `createSessionStorage` |\n| `@vielzeug/vault/indexeddb` | `createIndexedDB`, `defineMigration`, migrations, and IndexedDB-only types |\n| `@vielzeug/vault/sqlite` | `createSQLite`, the SQLite driver protocol types, and `TransactionContext` |\n\n## Schemas and TTL\n\n### `table()`\n\n```ts\nfunction table<T extends object, Key extends keyof T & string = keyof T & string>(\n key: Key & (T[Key] extends VaultKey ? unknown : never),\n options?: { defaultTtl?: number; indexes?: readonly (keyof T & string)[] },\n): SchemaEntry<T, Key>;\n```\n\nDefines a typed table and its primary-key field.\n\n| Parameter | Description |\n| --- | --- |\n| `key` | A record field whose values are `string` or finite `number` keys |\n| `options.defaultTtl` | Per-table default TTL in milliseconds |\n| `options.indexes` | IndexedDB secondary index fields |\n\n**Returns:** A `SchemaEntry` describing the table.\n\n```ts\nimport { table, ttl } from '@vielzeug/vault';\n\nconst users = table<{ id: number; email: string }>('id', {\n indexes: ['email'],\n defaultTtl: ttl.days(7),\n});\n```\n\n---\n\n### `ttl`\n\n```ts\nconst ttl: {\n days(n: number): number;\n hours(n: number): number;\n minutes(n: number): number;\n ms(n: number): number;\n seconds(n: number): number;\n};\n```\n\nCreates a finite, positive duration in milliseconds for writes and table defaults.\n\n**Returns:** `number`.\n\n```ts\nimport { ttl } from '@vielzeug/vault';\n\nconst cacheLifetime = ttl.minutes(5);\n```\n\n---\n\n### `isExpired()`\n\n```ts\nfunction isExpired(expiresAt: number | undefined): boolean;\n```\n\nReports whether an expiration timestamp has passed.\n\n**Returns:** `true` when `expiresAt` is defined and no later than the current time.\n\n```ts\nimport { isExpired } from '@vielzeug/vault';\n\nif (isExpired(record.expiresAt)) console.log('expired');\n```\n\n## Factories\n\nAll factory options accept `schema` and optional `validators`. The root entry does not export any factory.\n\n### `createMemory()`\n\n```ts\nfunction createMemory<S extends AnySchema>(options: BaseAdapterOptions<S>): VaultStore<S>;\n```\n\nCreates an in-memory portable store.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables created by `table()` |\n| `validators` | Optional per-table validators with a `parse(value): T` method |\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createMemory } from '@vielzeug/vault/memory';\n\nconst store = createMemory({ schema: { users: table<{ id: number; name: string }>('id') } });\n```\n\n---\n\n### `createLocalStorage()`\n\n```ts\nfunction createLocalStorage<S extends AnySchema>(options: BaseAdapterOptions<S> & {\n name: string;\n onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';\n}): VaultStore<S>;\n```\n\nCreates a namespaced `localStorage` store.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables created by `table()` |\n| `validators` | Optional per-table validators |\n| `name` | Required storage namespace |\n| `onQuotaExceeded` | Handles a Web Storage quota error; returning `'ignore'` drops that write |\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\nconst store = createLocalStorage({ name: 'app', schema: { settings: table<{ id: string }>('id') } });\n```\n\n---\n\n### `createSessionStorage()`\n\n```ts\nfunction createSessionStorage<S extends AnySchema>(options: BaseAdapterOptions<S> & {\n name: string;\n onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';\n}): VaultStore<S>;\n```\n\nCreates a namespaced `sessionStorage` store. Its options and return type match `createLocalStorage()`.\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createSessionStorage } from '@vielzeug/vault/session-storage';\n\nconst store = createSessionStorage({ name: 'checkout', schema: { cart: table<{ id: string }>('id') } });\n```\n\n---\n\n### `createIndexedDB()`\n\n```ts\nfunction createIndexedDB<S extends AnySchema>(options: BaseAdapterOptions<S> & {\n migrate?: MigrationFn;\n name: string;\n version?: number;\n}): TransactionalVaultStore<S>;\n```\n\nCreates an IndexedDB store with atomic batches, lazy cursor iteration, and optional schema migrations.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables and IndexedDB secondary indexes |\n| `validators` | Optional per-table validators |\n| `name` | Required database name |\n| `version` | Positive schema version; defaults to `1` |\n| `migrate` | Synchronous upgrade callback for version changes |\n\n**Returns:** `TransactionalVaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\n\nconst store = createIndexedDB({ name: 'app', schema: { users: table<{ id: number }>('id') } });\n```\n\n---\n\n### `createSQLite()`\n\n```ts\nfunction createSQLite<S extends AnySchema>(options: SQLiteVaultOptions<S>): TransactionalVaultStore<S>;\n```\n\nCreates a namespaced SQLite store with atomic batches and keyset-paginated iteration. It accepts an application-provided positional-parameter driver and never opens or imports a runtime driver.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables created by `table()` |\n| `validators` | Optional per-table validators |\n| `database` | Caller-provided `SQLiteDatabase` connection |\n| `name` | Namespace within the connection |\n| `closeOnDispose` | Closes the connection during disposal; defaults to `false` |\n\n**Returns:** `TransactionalVaultStore<S>`.\n\n```ts\nimport { DatabaseSync } from 'node:sqlite';\n\nimport { table } from '@vielzeug/vault';\nimport { createSQLite } from '@vielzeug/vault/sqlite';\n\nconst store = createSQLite({\n database: new DatabaseSync(':memory:'),\n name: 'tests',\n schema: { users: table<{ id: number; name: string }>('id') },\n});\n```\n\nNode `DatabaseSync`, Bun `Database`, and Deno `jsr:@db/sqlite` `Database` satisfy the protocol. Values must be JSON-compatible plain objects. During a `batch()` callback, calls on every Vault store sharing that connection reject; use `tx.*` instead.\n\n## Store Capabilities\n\n### `VaultStore`\n\n```ts\ninterface VaultStore<S extends AnySchema> {\n clear<K extends keyof S & string>(table: K): Promise<void>;\n count<K extends keyof S & string>(table: K): Promise<number>;\n delete<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n deleteMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<number>;\n get<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<RecordOf<S, K> | undefined>;\n getAll<K extends keyof S & string>(table: K): Promise<RecordOf<S, K>[]>;\n getMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<Array<RecordOf<S, K> | undefined>>;\n has<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n isEmpty<K extends keyof S & string>(table: K): Promise<boolean>;\n keys<K extends keyof S & string>(table: K, filter?: (record: RecordOf<S, K>) => boolean): Promise<KeyOf<S, K>[]>;\n put<K extends keyof S & string>(table: K, value: RecordOf<S, K>, ttl?: number): Promise<void>;\n putAll<K extends keyof S & string>(table: K, values: RecordOf<S, K>[], ttl?: number): Promise<void>;\n query<K extends keyof S & string>(table: K): QueryBuilder<RecordOf<S, K>>;\n update<K extends keyof S & string>(table: K, key: KeyOf<S, K>, changes: Partial<RecordOf<S, K>>, ttl?: number): Promise<RecordOf<S, K> | undefined>;\n upsert<K extends keyof S & string>(table: K, key: KeyOf<S, K>, fn: (existing: RecordOf<S, K> | undefined) => RecordOf<S, K>, ttl?: number): Promise<RecordOf<S, K>>;\n pruneExpired(): Promise<Record<keyof S & string, number>>;\n observe<K extends keyof S & string>(table: K, listener: Observer<RecordOf<S, K>>, options?: { immediate?: boolean; signal?: AbortSignal }): Unsubscribe;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n [Symbol.asyncDispose](): Promise<void>;\n}\n```\n\nThe portable store API is returned by every factory. `observe()` emits the current table snapshot by default and then emits after mutations.\n\n---\n\n### `batch()` and `iterate()`\n\n```ts\ninterface TransactionalVaultStore<S extends AnySchema> extends VaultStore<S> {\n batch<K extends keyof S & string, R>(\n tables: readonly K[],\n fn: (tx: TransactionContext<S, K>) => Promise<R>,\n ): Promise<R>;\n iterate<K extends keyof S & string>(table: K): AsyncIterable<RecordOf<S, K>>;\n}\n```\n\n`batch()` runs a scoped atomic callback. `iterate()` lazily yields table records — IndexedDB uses a cursor, SQLite uses keyset pagination. Both are provided by `createIndexedDB()` and `createSQLite()`.\n\n| Parameter | Description |\n| --- | --- |\n| `tables` | Tables the transaction may access |\n| `fn` | Async callback that uses only the supplied `tx` context |\n\n**Returns:** The callback result after commit.\n\n```ts\nawait store.batch(['users'], async (tx) => {\n await tx.put('users', { id: 1, name: 'Ada' });\n});\n\nfor await (const user of store.iterate('users')) console.log(user);\n```\n\n## Queries and Migrations\n\n### `QueryBuilder`\n\n```ts\ninterface QueryBuilder<T extends object> {\n count(): Promise<number>;\n delete(): Promise<number>;\n equals<K extends keyof T & string, V extends T[K]>(field: K, value: V): QueryBuilder<T>;\n filter(fn: (value: T, index: number, array: T[]) => boolean): QueryBuilder<T>;\n first(): Promise<T | undefined>;\n limit(n: number): QueryBuilder<T>;\n offset(n: number): QueryBuilder<T>;\n orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): QueryBuilder<T>;\n toArray(): Promise<T[]>;\n}\n```\n\nBuilds a lazy table query. `count()` ignores `limit()`, `offset()`, and `orderBy()` — it always returns the full filtered-set size.\n\n```ts\nconst page = await store.query('users').equals('role', 'admin').orderBy('name').limit(20).toArray();\n```\n\n---\n\n### `defineMigration()`\n\n```ts\nfunction defineMigration(steps: MigrationStep[]): MigrationFn;\n```\n\nBuilds an idempotent IndexedDB migration callback from schema-change steps.\n\n**Returns:** An IndexedDB `MigrationFn`.\n\n```ts\nimport { defineMigration } from '@vielzeug/vault/indexeddb';\n\nconst migrate = defineMigration([{ field: 'email', table: 'users', type: 'addIndex' }]);\n```\n\n## Types\n\n```ts\ntype VaultKey = number | string;\ntype Unsubscribe = () => void;\ntype Observer<T> = (records: T[]) => void;\ntype AnySchema = Record<string, {\n defaultTtl?: number;\n indexes?: readonly string[];\n key: string;\n}>;\ntype SchemaEntry<T extends object, Key extends keyof T & string = keyof T & string> =\n T[Key] extends VaultKey ? {\n defaultTtl?: number;\n indexes?: readonly (keyof T & string)[];\n key: Key;\n } : never;\ntype RecordOf<S extends AnySchema, K extends keyof S> =\n S[K] extends SchemaEntry<infer R, infer _Key> ? R : never;\ntype KeyOf<S extends AnySchema, K extends keyof S> =\n Extract<S[K] extends SchemaEntry<infer R, infer Key> ? R[Key] : never, VaultKey>;\n```\n\n```ts\ntype BaseAdapterOptions<S extends AnySchema> = {\n schema: S;\n validators?: TableValidators<S>;\n};\n\ntype RecordValidator<T> = {\n parse(value: unknown): T;\n};\n\ntype TableValidators<S extends AnySchema> = {\n [K in keyof S]?: RecordValidator<RecordOf<S, K>>;\n};\n```\n\n```ts\ntype MigrationContext = {\n db: IDBDatabase;\n newVersion: number | null;\n oldVersion: number;\n tx: IDBTransaction;\n};\n\ntype MigrationFn = (ctx: MigrationContext) => void;\n\ntype MigrationStep =\n | { field: string; table: string; type: 'addIndex' }\n | { field: string; table: string; type: 'removeIndex' }\n | { name: string; type: 'addTable' }\n | { name: string; type: 'removeTable' };\n```\n\nImport `MigrationContext`, `MigrationFn`, and `MigrationStep` from `@vielzeug/vault/indexeddb`.\n\n```ts\ntype SQLiteParameter = null | number | string;\n\ninterface SQLiteStatement {\n all(...parameters: SQLiteParameter[]): readonly Record<string, unknown>[];\n finalize?(): void;\n get(...parameters: SQLiteParameter[]): Record<string, unknown> | undefined;\n run(...parameters: SQLiteParameter[]): unknown;\n}\n\ninterface SQLiteDatabase {\n close?(): void;\n exec(sql: string): void;\n prepare(sql: string): SQLiteStatement;\n}\n\ntype SQLiteVaultOptions<S extends AnySchema> = BaseAdapterOptions<S> & {\n closeOnDispose?: boolean;\n database: SQLiteDatabase;\n name: string;\n};\n```\n\n```ts\ninterface TransactionalVaultStore<S extends AnySchema> extends VaultStore<S> {\n batch<K extends keyof S & string, R>(\n tables: readonly K[],\n fn: (tx: TransactionContext<S, K>) => Promise<R>,\n ): Promise<R>;\n iterate<K extends keyof S & string>(table: K): AsyncIterable<RecordOf<S, K>>;\n}\n```\n\nImport `TransactionalVaultStore` from `@vielzeug/vault`.\n\n```ts\ninterface TransactionContext<S extends AnySchema, K extends keyof S & string = keyof S & string> {\n clear<T extends K>(table: T): Promise<void>;\n count<T extends K>(table: T): Promise<number>;\n delete<T extends K>(table: T, key: KeyOf<S, T>): Promise<boolean>;\n deleteMany<T extends K>(table: T, keys: KeyOf<S, T>[]): Promise<number>;\n get<T extends K>(table: T, key: KeyOf<S, T>): Promise<RecordOf<S, T> | undefined>;\n getAll<T extends K>(table: T): Promise<RecordOf<S, T>[]>;\n getMany<T extends K>(table: T, keys: KeyOf<S, T>[]): Promise<Array<RecordOf<S, T> | undefined>>;\n has<T extends K>(table: T, key: KeyOf<S, T>): Promise<boolean>;\n isEmpty<T extends K>(table: T): Promise<boolean>;\n keys<T extends K>(table: T, filter?: (record: RecordOf<S, T>) => boolean): Promise<KeyOf<S, T>[]>;\n put<T extends K>(table: T, value: RecordOf<S, T>, ttl?: number): Promise<void>;\n putAll<T extends K>(table: T, values: RecordOf<S, T>[], ttl?: number): Promise<void>;\n query<T extends K>(table: T): QueryBuilder<RecordOf<S, T>>;\n update<T extends K>(table: T, key: KeyOf<S, T>, changes: Partial<RecordOf<S, T>>, ttl?: number): Promise<RecordOf<S, T> | undefined>;\n upsert<T extends K>(table: T, key: KeyOf<S, T>, fn: (existing: RecordOf<S, T> | undefined) => RecordOf<S, T>, ttl?: number): Promise<RecordOf<S, T>>;\n}\n```\n\n`TransactionContext` has the same CRUD, query, and TTL methods as `VaultStore`, narrowed to the tables declared in `batch()`. Import it from `@vielzeug/vault/indexeddb` or `@vielzeug/vault/sqlite`.\n\n```ts\n// Adapter-specific type aliases — both resolve to TransactionalVaultStore.\ntype SQLiteVaultStore<S extends AnySchema> = TransactionalVaultStore<S>;\ntype IndexedDbVaultStore<S extends AnySchema> = TransactionalVaultStore<S>;\n```\n\n`SQLiteVaultStore` is exported from `@vielzeug/vault/sqlite`. `IndexedDbVaultStore` is exported from `@vielzeug/vault/indexeddb`.\n\n## Errors\n\n| Error | Trigger |\n| --- | --- |\n| `VaultError` | Any Vault-originated validation, serialization, storage, or query error |\n| `VaultDisposedError` | An operation after the store or observer hub is disposed |\n| `VaultScopeError` | A `batch()` callback accesses a table outside its declared scope |\n| `VaultQuotaError` | A LocalStorage or SessionStorage write exceeds the browser quota |\n| `VaultMigrationError` | An IndexedDB migration callback throws |\n\nEvery listed error extends `VaultError`.\n",
6
+ "usage": "---\ntitle: Vault — Usage Guide\ndescription: Persist typed browser or SQLite data, observe table snapshots, and use atomic transactions.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a portable store with one schema and write a typed row.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\ninterface Preference {\n id: string;\n theme: 'dark' | 'light';\n}\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n\nawait store.put('preferences', { id: 'theme', theme: 'dark' });\nconsole.log(await store.get('preferences', 'theme'));\n```\n\n## Create a Portable Store\n\nMemory, LocalStorage, and SessionStorage return `VaultStore`. They share portable string/number keys, CRUD methods, queries, TTL, and `observe()`. Vault keeps record values and expiry metadata separate; the physical storage layout is adapter-specific.\n\nThe root entry is adapter-free. Import `createMemory` from `@vielzeug/vault/memory`, `createLocalStorage` from `@vielzeug/vault/local-storage`, or `createSessionStorage` from `@vielzeug/vault/session-storage`. Import each adapter from its focused subpath so unused backends stay out of the bundle.\n\nUse a new storage name when upgrading from Vault 1. Old key and envelope formats are not read by Vault 2.\n\n```ts\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n```\n\n## Read and Change Records\n\nUse `update()` for an existing row and `upsert()` when the row may not exist.\n\n```ts\nconst updated = await store.update('preferences', 'theme', { theme: 'light' });\n\nawait store.upsert('preferences', 'locale', (current) => ({\n id: 'locale',\n theme: current?.theme ?? 'dark',\n}));\n\nconsole.log(updated);\n```\n\n`update()` returns `undefined` for a missing key. `upsert()` always writes the record returned by its callback.\n\n## Query Records\n\nBuild a query from a table, then finish it with a terminal method. `count()` ignores pagination, which makes it suitable for page controls.\n\n```ts\nconst query = store.query('preferences').filter((p) => p.id.startsWith('theme'));\nconst preferences = await query.orderBy('id').limit(10).toArray();\nconst total = await query.count();\n\nconsole.log({ preferences, total });\n```\n\nQueries scan the table in memory. Use `equals()` for exact field matches and `filter()` for custom predicates. For large tables, prefer `iterate()` on IndexedDB or SQLite instead of materializing every record.\n\n## Use TTL and Pruning\n\nUse `ttl.*` helpers for expiring rows. Call `pruneExpired()` to reclaim storage from stale rows that accumulate without reads.\n\n```ts\nimport { ttl } from '@vielzeug/vault';\n\nawait store.put('preferences', { id: 'temporary', theme: 'dark' }, ttl.hours(1));\n\n// Reclaim expired rows on a schedule owned by the application.\nconst pruneInterval = setInterval(() => store.pruneExpired(), ttl.hours(6));\nstore.disposalSignal.addEventListener('abort', () => clearInterval(pruneInterval));\n```\n\n## Observe a Table\n\nUse `observe()` for current and future snapshots. Tie subscription lifetime to an `AbortSignal` when a component or request owns it.\n\n```ts\nconst controller = new AbortController();\n\nstore.observe('preferences', (preferences) => {\n console.log(preferences);\n}, { signal: controller.signal });\n\ncontroller.abort();\n```\n\n## Use IndexedDB for Browser Transactions\n\nChoose IndexedDB when browser storage needs multiple writes to commit together or cursor iteration.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\n\nconst db = createIndexedDB({\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n});\n\nawait db.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened' });\n await tx.put('events', { id: 2, type: 'saved' });\n});\n```\n\nOnly await `tx.*` operations inside a batch callback. Do not await timers, fetches, or other external asynchronous work; IndexedDB can commit an inactive transaction.\n\n## Use SQLite Outside the Browser\n\nImport SQLite from the opt-in subpath so the browser root stays free of runtime drivers. Vault never opens a connection or configures its SQLite process behavior for you.\n\n```ts\nimport { DatabaseSync } from 'node:sqlite';\n\nimport { table } from '@vielzeug/vault';\nimport { createSQLite } from '@vielzeug/vault/sqlite';\n\nconst database = new DatabaseSync('app.db', { timeout: 5_000 });\nconst store = createSQLite({\n database,\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n});\n\nawait store.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened' });\n await tx.put('events', { id: 2, type: 'saved' });\n});\n```\n\nNode's `node:sqlite` API is experimental. Bun's `bun:sqlite` `Database` satisfies the same positional `exec()` and `prepare()` contract; configure WAL from your application when the deployment needs it. Deno does not include SQLite, but `jsr:@db/sqlite`'s `Database` satisfies the same contract when its FFI, filesystem, and environment permissions are granted.\n\nSQLite stores serialize all access through the injected connection. `batch()` starts `BEGIN IMMEDIATE` and rolls back callback failures. While its callback runs, calls on any store sharing that connection reject rather than waiting behind the transaction; use `tx.*` instead. The underlying drivers are synchronous, so move large scans and writes to a worker or isolate when event-loop latency matters.\n\n## Store SQLite Values and Observe Changes\n\nSQLite accepts JSON-compatible plain-object records only. Circular values, `bigint`, dates, class instances, functions, and non-finite numbers are rejected before writing. Number and string primary keys remain distinct.\n\n`observe()` sees mutations written through Vault stores sharing the same injected connection after a commit. It cannot detect direct SQL changes, writes from another process, or writes through another connection. The connection belongs to the caller by default; use `closeOnDispose: true` only when the store owns it.\n\n## Handle IndexedDB Schema Migrations\n\nDeclare IndexedDB indexes in the schema. Use `migrate` only for IndexedDB version upgrades and mirror Vault’s fixed `value.<field>` index path.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB, type MigrationFn } from '@vielzeug/vault/indexeddb';\n\nconst schema = { users: table<{ id: number; name: string }>('id', { indexes: ['name'] }) };\nconst migrate: MigrationFn = ({ db, oldVersion, tx }) => {\n if (oldVersion < 2 && db.objectStoreNames.contains('users')) {\n tx.objectStore('users').createIndex('name', 'value.name');\n }\n};\n\ncreateIndexedDB({ name: 'app-v2', migrate, schema, version: 2 });\n```\n\n## Framework Integration\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const [rows, setRows] = useState<RecordOf<S, K>[]>([]);\n\n useEffect(() => store.observe(table, setRows), [store, table]);\n return rows;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const rows = shallowRef<RecordOf<S, K>[]>([]);\n const stop = store.observe(table, (next) => (rows.value = next));\n\n onUnmounted(stop);\n return rows;\n}\n```\n\n```ts [Svelte]\nimport { readable } from 'svelte/store';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function tableStore<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n return readable<RecordOf<S, K>[]>([], (set) => store.observe(table, set));\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Forge’s Vault helpers for explicit form-draft persistence. Keep Ripple signals as application state and persist selected changes through Vault writes.\n\n## Best Practices\n\n- Define one schema per storage namespace.\n- Use string or finite-number primary keys only.\n- Choose a new namespace for Vault 1 storage unless you migrate it yourself.\n- Use `observe()` for table snapshots.\n- Use IndexedDB or SQLite for atomic work.\n- Keep external asynchronous work outside `batch()` callbacks.\n- Use `ttl.*` instead of raw durations.\n- Keep SQLite scans and writes off latency-sensitive event loops.\n- Dispose stores when their owner ends.\n",
7
7
  "examples": "---\ntitle: Vault — Examples\ndescription: Portable storage, observation, transactions, iteration, and SQLite.\n---\n\n- [CRUD](./examples/crud.md)\n- [TTL](./examples/ttl.md)\n- [Querying](./examples/querying.md)\n- [Reactive observation](./examples/reactive.md)\n- [IndexedDB iteration](./examples/iterate.md)\n- [IndexedDB batch transactions](./examples/batch.md)\n- [SQLite transactions and iteration](./examples/sqlite.md)\n- [Plugin validation](./examples/plugins.md)\n"
8
8
  },
9
9
  "examples": [
@@ -19,8 +19,8 @@
19
19
  },
20
20
  {
21
21
  "id": "cache-first",
22
- "code": "import { table, ttl } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst db = createLocalStorage({ name: 'cache-demo', schema: { cache: table('id') } })\n\nasync function getOrComputeConfig() {\n return db.getOrDefault('cache', 'config', () => ({\n id: 'config',\n data: 'computed value',\n fetchedAt: Date.now(),\n }), ttl.minutes(5))\n}\n\nconst first = await getOrComputeConfig()\nconst second = await getOrComputeConfig()\nconsole.log('Same cached record:', first.fetchedAt === second.fetchedAt)",
23
- "name": "Cache-First with getOrDefault"
22
+ "code": "import { table, ttl } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst db = createLocalStorage({ name: 'cache-demo', schema: { cache: table('id') } })\n\nasync function getOrComputeConfig() {\n const existing = await db.get('cache', 'config')\n if (existing) return existing\n\n const record = {\n id: 'config',\n data: 'computed value',\n fetchedAt: Date.now(),\n }\n await db.put('cache', record, ttl.minutes(5))\n return record\n}\n\nconst first = await getOrComputeConfig()\nconst second = await getOrComputeConfig()\nconsole.log('Same cached record:', first.fetchedAt === second.fetchedAt)",
23
+ "name": "Cache-First with get + put"
24
24
  },
25
25
  {
26
26
  "id": "crud-operations",
@@ -29,17 +29,17 @@
29
29
  },
30
30
  {
31
31
  "id": "indexed-db",
32
- "code": "import { table, ttl } from '@vielzeug/vault'\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb'\n\nconst schema = {\n logs: table('id'),\n}\n\n// createIndexedDB returns IndexedDbVaultStore with transactions and cursor iteration\nconst db = createIndexedDB({\n name: 'app-logs',\n schema,\n version: 1,\n})\n\nawait db.putAll('logs', [\n { id: 1, level: 'info', message: 'App started', ts: Date.now() - 3000 },\n { id: 2, level: 'warn', message: 'Slow query detected', ts: Date.now() - 2000 },\n { id: 3, level: 'error', message: 'Request failed', ts: Date.now() - 1000 },\n { id: 4, level: 'info', message: 'Request succeeded', ts: Date.now() },\n], ttl.hours(1))\n\n// batch() is atomic on IndexedDB — all writes commit or none do\nawait db.batch(['logs'], async (tx) => {\n await tx.put('logs', { id: 5, level: 'info', message: 'Batch committed', ts: Date.now() })\n await tx.deleteMany('logs', [1, 2]) // remove old entries in the same transaction\n})\n\n// iterate() — cursor-based streaming, only on IndexedDbVaultStore\n// the full table is never loaded into memory at once\nconst messages = []\nfor await (const entry of db.iterate('logs')) {\n messages.push(entry.message)\n}\nconsole.log('Streamed via iterate():', messages)\n\nconst errors = await db.query('logs').equals('level', 'error').toArray()\nconsole.log('Errors:', errors.map((e) => e.message))\nconsole.log('Total logs:', await db.query('logs').count())\n\nconst info = await db.debug()\nfor (const t of info.tables) {\n console.log(t.name + ':', t.recordCount, 'live,', t.expiredCount, 'expired')\n}\n\nawait db.dispose()",
32
+ "code": "import { table, ttl } from '@vielzeug/vault'\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb'\n\nconst schema = {\n logs: table('id'),\n}\n\n// createIndexedDB returns TransactionalVaultStore with transactions and cursor iteration\nconst db = createIndexedDB({\n name: 'app-logs',\n schema,\n version: 1,\n})\n\nawait db.putAll('logs', [\n { id: 1, level: 'info', message: 'App started', ts: Date.now() - 3000 },\n { id: 2, level: 'warn', message: 'Slow query detected', ts: Date.now() - 2000 },\n { id: 3, level: 'error', message: 'Request failed', ts: Date.now() - 1000 },\n { id: 4, level: 'info', message: 'Request succeeded', ts: Date.now() },\n], ttl.hours(1))\n\n// batch() is atomic on IndexedDB — all writes commit or none do\nawait db.batch(['logs'], async (tx) => {\n await tx.put('logs', { id: 5, level: 'info', message: 'Batch committed', ts: Date.now() })\n await tx.deleteMany('logs', [1, 2]) // remove old entries in the same transaction\n})\n\n// iterate() — cursor-based streaming, only on TransactionalVaultStore\n// the full table is never loaded into memory at once\nconst messages = []\nfor await (const entry of db.iterate('logs')) {\n messages.push(entry.message)\n}\nconsole.log('Streamed via iterate():', messages)\n\nconst errors = await db.query('logs').equals('level', 'error').toArray()\nconsole.log('Errors:', errors.map((e) => e.message))\nconsole.log('Total logs:', await db.query('logs').count())\n\n// pruneExpired() reclaims storage from TTL-expired records that haven't been read\nconst pruned = await db.pruneExpired()\nconsole.log('Pruned:', pruned)\n\nawait db.dispose()",
33
33
  "name": "IndexedDB — Atomic Batch & iterate()"
34
34
  },
35
35
  {
36
36
  "id": "prune-schedule",
37
- "code": "import { scheduleExpiredPrune, table, ttl } from '@vielzeug/vault'\nimport { createMemory } from '@vielzeug/vault/memory'\n\n// scheduleExpiredPrune runs pruneExpired() on an interval.\n// Pass disposalSignal to auto-cancel when the store is torn down.\n\nconst schema = { sessions: table('token') }\nconst db = createMemory({ schema })\n\nconst stop = scheduleExpiredPrune(db, {\n interval: ttl.minutes(15),\n signal: db.disposalSignal,\n onError: (err) => console.error('[vault] prune failed:', err),\n})\n\n// Write a session that expires in 1 ms\nawait db.put('sessions', { token: 'abc', user: 1 }, ttl.ms(1))\nawait db.put('sessions', { token: 'def', user: 2 }) // no TTL — permanent\n\nconsole.log('before prune:', await db.count('sessions')) // 2 (lazy eviction: both exist physically)\n\n// Manual prune to demonstrate the API\nawait new Promise((resolve) => setTimeout(resolve, 5))\nconst pruned = await db.pruneExpired()\nconsole.log('pruned:', pruned.sessions) // 1 (the expired session)\nconsole.log('after prune:', await db.count('sessions')) // 1\n\n// stop() before dispose, or rely on disposalSignal auto-cancel\nstop()\nawait db.dispose()",
38
- "name": "TTL — scheduleExpiredPrune with disposalSignal"
37
+ "code": "import { table, ttl } from '@vielzeug/vault'\nimport { createMemory } from '@vielzeug/vault/memory'\n\n// pruneExpired() sweeps all tables and removes expired records.\n// Schedule it with setInterval and cancel on disposalSignal.\n\nconst schema = { sessions: table('token') }\nconst db = createMemory({ schema })\n\nconst pruneInterval = setInterval(() => db.pruneExpired(), ttl.minutes(15))\ndb.disposalSignal.addEventListener('abort', () => clearInterval(pruneInterval))\n\n// Write a session that expires in 1 ms\nawait db.put('sessions', { token: 'abc', user: 1 }, ttl.ms(1))\nawait db.put('sessions', { token: 'def', user: 2 }) // no TTL — permanent\n\nconsole.log('before prune:', await db.count('sessions')) // 2 (lazy eviction: both exist physically)\n\n// Manual prune to demonstrate the API\nawait new Promise((resolve) => setTimeout(resolve, 5))\nconst pruned = await db.pruneExpired()\nconsole.log('pruned:', pruned.sessions) // 1 (the expired session)\nconsole.log('after prune:', await db.count('sessions')) // 1\n\nawait db.dispose()",
38
+ "name": "TTL — pruneExpired with disposalSignal"
39
39
  },
40
40
  {
41
41
  "id": "query-builder",
42
- "code": "import { table } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst schema = {\n products: table('id'),\n}\n\nconst db = createLocalStorage({ name: 'shop', schema })\n\nawait db.putAll('products', [\n { id: 1, name: 'Laptop', price: 999, category: 'electronics', inStock: true },\n { id: 2, name: 'Mouse', price: 29, category: 'electronics', inStock: true },\n { id: 3, name: 'Desk', price: 299, category: 'furniture', inStock: false },\n { id: 4, name: 'Chair', price: 199, category: 'furniture', inStock: true },\n { id: 5, name: 'Monitor', price: 399, category: 'electronics', inStock: true },\n])\n\nconst pageSize = 2\nconst pageIndex = 0\n\n// Build a base query — reuse it for both the page slice and the total count\nconst q = db\n .query('products')\n .equals('category', 'electronics')\n .filter((p) => p.inStock)\n .orderBy('price', 'asc')\n\n// count() ignores limit/offset/orderBy — returns the full filtered set size\nconst page = await q.limit(pageSize).offset(pageIndex * pageSize).toArray()\nconst total = await q.count()\n\nconsole.log('Page:', page.map((p) => p.name))\nconsole.log('Total matching:', total)\nconsole.log('Page 1 of', Math.ceil(total / pageSize))\n\n// startsWith with case-insensitive flag\nconst mice = await db.query('products').startsWith('name', 'm', { ignoreCase: true }).toArray()\nconsole.log('Starts with m:', mice.map((p) => p.name))\n\n// predicate delete\nconst removed = await db.query('products').filter((p) => !p.inStock).delete()\nconsole.log('Removed out-of-stock:', removed)\n\n// first()\nconst cheapest = await db.query('products').orderBy('price', 'asc').first()\nconsole.log('Cheapest:', cheapest?.name, cheapest?.price)",
42
+ "code": "import { table } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst schema = {\n products: table('id'),\n}\n\nconst db = createLocalStorage({ name: 'shop', schema })\n\nawait db.putAll('products', [\n { id: 1, name: 'Laptop', price: 999, category: 'electronics', inStock: true },\n { id: 2, name: 'Mouse', price: 29, category: 'electronics', inStock: true },\n { id: 3, name: 'Desk', price: 299, category: 'furniture', inStock: false },\n { id: 4, name: 'Chair', price: 199, category: 'furniture', inStock: true },\n { id: 5, name: 'Monitor', price: 399, category: 'electronics', inStock: true },\n])\n\nconst pageSize = 2\nconst pageIndex = 0\n\n// Build a base query — reuse it for both the page slice and the total count\nconst q = db\n .query('products')\n .equals('category', 'electronics')\n .filter((p) => p.inStock)\n .orderBy('price', 'asc')\n\n// count() ignores limit/offset/orderBy — returns the full filtered set size\nconst page = await q.limit(pageSize).offset(pageIndex * pageSize).toArray()\nconst total = await q.count()\n\nconsole.log('Page:', page.map((p) => p.name))\nconsole.log('Total matching:', total)\nconsole.log('Page 1 of', Math.ceil(total / pageSize))\n\n// prefix match via filter()\nconst mice = await db\n .query('products')\n .filter((p) => p.name.toLowerCase().startsWith('m'))\n .toArray()\nconsole.log('Starts with m:', mice.map((p) => p.name))\n\n// predicate delete\nconst removed = await db.query('products').filter((p) => !p.inStock).delete()\nconsole.log('Removed out-of-stock:', removed)\n\n// first()\nconst cheapest = await db.query('products').orderBy('price', 'asc').first()\nconsole.log('Cheapest:', cheapest?.name, cheapest?.price)",
43
43
  "name": "Query Builder — Filters, Pagination, count"
44
44
  },
45
45
  {
@@ -59,27 +59,21 @@
59
59
  "VaultMigrationError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
60
60
  "VaultQuotaError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
61
61
  "VaultScopeError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
62
- "scheduleExpiredPrune": "export { scheduleExpiredPrune } from './prune';",
63
62
  "QueryBuilder": "export type { QueryBuilder } from './query';",
64
63
  "isExpired": "export { isExpired, ttl } from './ttl';",
65
64
  "ttl": "export { isExpired, ttl } from './ttl';",
66
- "AnySchema": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
67
- "BaseAdapterOptions": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
68
- "DebugInfo": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
69
- "DebugStats": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
70
- "IterableVaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
71
- "KeyOf": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
72
- "MetricsEvent": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
73
- "Observer": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
74
- "RecordOf": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
75
- "RecordValidator": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
76
- "SchemaEntry": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
77
- "TableValidators": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
78
- "TransactionalVaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
79
- "Unsubscribe": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
80
- "VaultKey": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
81
- "VaultLogger": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
82
- "VaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
65
+ "AnySchema": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
66
+ "BaseAdapterOptions": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
67
+ "KeyOf": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
68
+ "Observer": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
69
+ "RecordOf": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
70
+ "RecordValidator": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
71
+ "SchemaEntry": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
72
+ "TableValidators": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
73
+ "TransactionalVaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
74
+ "Unsubscribe": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
75
+ "VaultKey": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
76
+ "VaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
83
77
  "table": "export { table } from './types';"
84
78
  }
85
79
  }
@@ -1,10 +1,10 @@
1
1
  {
2
- "apiSource": "export { allow, deny, owns, predicate, ruleFor } from './builder';\nexport { ANONYMOUS, WILDCARD } from './constants';\nexport { WardConfigError, WardError, WardPredicateError } from './errors';\nexport { createWard } from './factory';\nexport { matchesPattern, patternCovers } from './resource';\nexport type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n",
2
+ "apiSource": "export { allow, deny, owns, predicate, ruleFor } from './builder';\nexport { ANONYMOUS, WILDCARD } from './constants';\nexport { WardConfigError, WardError, WardPredicateError } from './errors';\nexport { createWard } from './factory';\nexport { matchesPattern, patternCovers } from './resource';\nexport type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Ward — Deterministic authorization for TypeScript\ndescription: Typed authorization policies with wildcard matching, deterministic precedence, and decision tracing.\npackage: ward\ncategory: auth\nkeywords: [authorization, rbac, permissions, policy, roles, wildcard, predicates]\nrelated: [wayfinder, conduit, herald]\nexports: [createWard, allow, deny, ruleFor, owns, predicate, ANONYMOUS, WILDCARD, WardError, WardConfigError, WardPredicateError, NormalizedWardRule, matchesPattern, patternCovers]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ward\" />\n\n## Why Ward?\n\nWard keeps authorization policies declarative and decision ordering deterministic. Define rules once, then explain or trace every permission decision without embedding role checks across handlers.\n\n```ts\n// Before\nconst canUpdate = user.roles.includes('editor') && post.authorId === user.id;\n\n// After\nimport { allow, createWard, owns } from '@vielzeug/ward';\n\nconst ward = createWard([\n allow('editor', 'posts', ['update'], { when: owns('authorId') }),\n]);\n\nconst decision = ward.explain({ principal: user, resource: 'posts', action: 'update', data: post });\nconst canUpdate = decision.allowed;\n```\n\n| Feature | Ward | CASL | AccessControl |\n| ------------------------ | -------------------------------------------- | ---------------------------------------- | ---------------------------------------- |\n| Bundle size | <PackageInfo package=\"ward\" type=\"size\" /> | Larger policy engine | Larger policy engine |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Deterministic precedence | Priority, specificity, deny, order | Rule-dependent | Role-grant dependent |\n| Decision tracing | `trace()` candidates and winner | Manual inspection | Manual inspection |\n\n<div class=\"decision-callout\">\n\n**Use Ward when** your application needs typed role/resource/action policies with explainable, deterministic outcomes.\n\n**Consider framework-specific authorization when** your application only needs one framework's built-in route or component guard layer.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ward\n```\n\n```sh [npm]\nnpm install @vielzeug/ward\n```\n\n```sh [yarn]\nyarn add @vielzeug/ward\n```\n\n:::\n\n## Quick Start\n\nCreate a small policy and handle both allowed and denied decisions at the request boundary.\n\n```ts\nimport { allow, createWard } from '@vielzeug/ward';\n\nconst ward = createWard([\n allow('viewer', 'posts', ['read']),\n allow('editor', 'posts', ['update']),\n]);\n\nconst decision = ward.explain({\n principal: { id: 'u1', roles: ['editor'] },\n resource: 'posts',\n action: 'update',\n});\n\nif (decision.allowed) console.log('Update post');\nelse console.log(decision.reason);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createWard()` creates immutable typed policy instances. Accepts `allow()`/`deny()` results directly — no spread needed.\n- `allow()`, `deny()`, and `ruleFor()` build role/resource/action rules.\n- `WILDCARD` and `ANONYMOUS` model broad or unauthenticated access explicitly.\n- `owns()` and `predicate` constrain rules with synchronous request data.\n- `explain()`, `trace()`, and `detectConflicts()` make policy decisions diagnosable.\n- `forUser()` creates a principal-bound view for repeated checks.\n- `checkAll()` evaluates multiple resource/action pairs in one call.\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- [Wayfinder](/wayfinder/) — route middleware can enforce Ward decisions during navigation.\n- [Conduit](/conduit/) — inject a Ward policy into application services.\n- [Herald](/herald/) — publish authorization outcomes as typed application events.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Ward — API Reference\ndescription: Complete API reference for @vielzeug/ward.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createWard` | Creates immutable policy | Sync | Rules cannot be mutated after creation |\n| `allow` / `deny` / `ruleFor` | Builds policy rules | Sync | Priority wins before specificity |\n| `Ward.explain` | Returns one decision | Sync | Pass resource data for predicate rules |\n| `Ward.trace` | Inspects decision candidates | Sync | Does not invoke the logger |\n| `Ward.forUser` | Binds a principal | Sync | Rebind when identity or roles change |\n| `Ward.checkAll` | Batch permission checks | Sync | Pass resource data for predicate rules |\n| `Ward.allowedActions` | Filters known actions to allowed set | Sync | Does not invoke the logger |\n| `Ward.rulesInScope` | Lists rules matching a principal/resource | Sync | Pass data to evaluate predicates |\n| `Ward.detectConflicts` | Detects duplicate/shadowed rules | Sync | O(n²) — use `maxConflicts` for large policies |\n| `predicate.owns` / `owns` | Ownership predicate on resource data | Sync | Skipped for anonymous principals |\n| `predicate.and` / `or` / `not` | Combine predicates | Sync | All inputs must be synchronous |\n| `matchesPattern` / `patternCovers` | Test resource pattern coverage | Sync | `'*'` is the only wildcard |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/ward` | Rules, factory, predicates, pattern helpers, errors, and public types |\n| `@vielzeug/ward/devtools` | `debugWard()` diagnostic factory |\n\n## Core Factory\n\n### `createWard(rules, options?)`\n\n```ts\ncreateWard<TAction extends string = string, TData = unknown>(\n rules: readonly (WardRule<TAction, TData> | readonly WardRule<TAction, TData>[])[] = [],\n options?: WardOptions<TAction, TData>,\n): Ward<TAction, TData>;\n```\n\nCreates an immutable ward instance. `rules` accepts a flat mix of single rules and rule arrays — `allow()`/`deny()`/`ruleFor()` results can be passed directly without spread. Validates `logger`, `onConflict`, and `maxConflicts` options before compiling rules; invalid values throw `WardConfigError`.\n\n**Parameters:**\n\n| Name | Type | Description |\n| --- | --- | --- |\n| `rules` | `readonly (WardRule \\| readonly WardRule[])[]` | Rule list. Single rules and rule arrays can be mixed. |\n| `options.logger` | `(ctx: WardLoggerContext) => void` | Called for `explain()` and `checkAll()` decisions. |\n| `options.onConflict` | `(conflict: WardConflict) => void` | Called synchronously per conflict at creation time. |\n| `options.strict` | `boolean` | Throws `WardConfigError` on the first conflict. |\n| `options.maxConflicts` | `number` | Caps the number of conflicts returned by `detectConflicts()`. |\n\n**Returns:** `Ward<TAction, TData>` — an immutable policy instance.\n\n**Example:**\n\n```ts\nimport { allow, createWard, deny, WILDCARD } from '@vielzeug/ward';\n\nconst ward = createWard([\n allow('viewer', 'posts', ['read']),\n allow('editor', 'posts', ['update']),\n deny('blocked', WILDCARD, [WILDCARD], { priority: 100 }),\n]);\n```\n\n---\n\n## Rule Builders\n\n### `allow(role, resource, actions, options?)`\n\n```ts\nallow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: { priority?: number; when?: WardPredicate<TData> },\n): WardRule<TAction, TData>[];\n```\n\nCreates one `WardRule` per action with `effect: 'allow'`. Reads naturally: \"allow editor to read/update posts\".\n\n**Returns:** `WardRule[]` — one rule per action.\n\n---\n\n### `deny(role, resource, actions, options?)`\n\n```ts\ndeny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: { priority?: number; when?: WardPredicate<TData> },\n): WardRule<TAction, TData>[];\n```\n\nCreates one `WardRule` per action with `effect: 'deny'`. Reads naturally: \"deny blocked from reading posts\".\n\n**Returns:** `WardRule[]` — one rule per action.\n\n---\n\n### `ruleFor(effect, role, resource, actions, options?)`\n\n```ts\nruleFor<TAction extends string = string, TData = unknown>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: { priority?: number; when?: WardPredicate<TData> },\n): WardRule<TAction, TData>[];\n```\n\nLow-level factory. Prefer `allow()` or `deny()` for ergonomic rule authoring.\n\n**Returns:** `WardRule[]` — one rule per action.\n\n---\n\n## Ward Methods\n\n### `checkAll(principal, checks)`\n\n```ts\ncheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n): WardDecisionResult<TAction, TData>[];\n```\n\nEvaluates multiple resource/action pairs for one principal. Invokes the logger for each decision.\n\n**Returns:** `WardDecisionResult[]` — each entry carries `action`, `resource`, and the decision.\n\n---\n\n### `explain(input)`\n\n```ts\nexplain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData>;\n```\n\n`WardDecisionInput`:\n\n```ts\n{\n principal: Principal;\n resource: string;\n action: TAction;\n data?: TData;\n}\n```\n\nReturns one decision. Invokes the logger.\n\n**Returns:** `WardDecision` — `{ allowed: true; rule }` or `{ allowed: false; reason: 'explicit-deny'; rule }` or `{ allowed: false; reason: 'no-matching-rule' }`.\n\n---\n\n### `trace(input)`\n\n```ts\ntrace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData>;\n```\n\nSame request shape as `explain()`. Returns winner + candidate list. Does not fire the logger.\n\n**Returns:** `WardTrace` — `{ candidates: WardTraceCandidate[]; decision: WardDecision }`.\n\n---\n\n### `allowedActions(input)`\n\n```ts\nallowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[];\n```\n\nInput shape:\n\n```ts\n{\n principal: Principal;\n resource: string;\n knownActions: readonly TAction[];\n data?: TData;\n}\n```\n\nFilters the provided `knownActions` list to those the principal may perform. Does not invoke the logger.\n\n**Returns:** `TAction[]` — the subset of `knownActions` that `explain()` would allow.\n\n---\n\n### `rulesInScope(input)`\n\n```ts\nrulesInScope(input: WardRulesInScopeInput<TData>): ReadonlyArray<Readonly<NormalizedWardRule<TAction, TData>>>;\n```\n\nInput shape:\n\n```ts\n{\n principal: Principal;\n resource: string;\n data?: TData;\n}\n```\n\nLists rules matching the principal/resource pair. Pass `data` to evaluate predicate-gated matches; without it, predicate rules are skipped.\n\n**Returns:** `ReadonlyArray<Readonly<NormalizedWardRule>>` — rules in their normalized form (`role` always array, `priority` always number).\n\n---\n\n### `detectConflicts()`\n\n```ts\ndetectConflicts(): readonly WardConflict<TAction, TData>[];\n```\n\nLazily computes and caches duplicate/shadowed rule conflicts. O(n²) — use `maxConflicts` for large policies.\n\n**Returns:** `readonly WardConflict[]` — `{ kind: 'duplicate'; indexA; indexB; ruleA; ruleB }` or `{ kind: 'shadowed'; shadowedIndex; shadowedRule; shadowingIndex; shadowingRule }`.\n\n---\n\n### `forUser(principal)`\n\n```ts\nforUser(principal: UserPrincipal): BoundWard<TAction, TData>;\n```\n\nReturns a principal-bound view. `UserPrincipal` (not nullable — use `null` directly with `explain()` for anonymous).\n\n**Returns:** `BoundWard` — same methods without the `principal` argument.\n\n---\n\n## `BoundWard` Methods\n\n```ts\ntype BoundWard<TAction extends string = string, TData = unknown> = {\n allowedActions(input: BoundWardAllowedActionsInput<TAction, TData>): TAction[];\n checkAll(checks: readonly WardCheck<TAction, TData>[]): WardDecisionResult<TAction, TData>[];\n explain(input: BoundWardDecisionInput<TAction, TData>): WardDecision<TAction, TData>;\n rulesInScope(input: BoundWardRulesInScopeInput<TData>): ReadonlyArray<Readonly<NormalizedWardRule<TAction, TData>>>;\n trace(input: BoundWardDecisionInput<TAction, TData>): WardTrace<TAction, TData>;\n};\n```\n\nBound input shapes remove `principal`:\n\n```ts\n{ resource: string; action: TAction; data?: TData } // explain/trace\n{ resource: string; knownActions: readonly TAction[]; data?: TData } // allowedActions\n{ resource: string; data?: TData } // rulesInScope\n```\n\n---\n\n## Predicate Helpers\n\n### `predicate.owns(attributeKey)`\n\n```ts\npredicate.owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n): WardPredicate<TData>;\n```\n\nReturns a `WardPredicate` that checks whether `data[attributeKey]` matches `principal.id`. Skipped for anonymous principals — pairing `owns` with an `ANONYMOUS`-role rule produces a rule that can never match.\n\n**Returns:** `WardPredicate<TData>`.\n\n---\n\n### `predicate.and(...predicates)`\n\n```ts\npredicate.and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData>;\n```\n\nAll predicates must return `true`.\n\n---\n\n### `predicate.or(...predicates)`\n\n```ts\npredicate.or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData>;\n```\n\nAt least one predicate must return `true`.\n\n---\n\n### `predicate.not(predicate)`\n\n```ts\npredicate.not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData>;\n```\n\nInverts the given predicate.\n\n---\n\n### `owns(attributeKey)` (alias)\n\n```ts\nowns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n): WardPredicate<TData>;\n```\n\nTop-level re-export of `predicate.owns`.\n\nPredicates run synchronously. Returning a Promise throws `WardPredicateError`.\n\n---\n\n## Pattern Helpers\n\n### `matchesPattern(pattern, value): boolean`\n\n```ts\nmatchesPattern(pattern: string, value: string): boolean;\n```\n\nTests whether `value` matches a `'*'`-wildcard `pattern`. `'*'` matches any value; an exact string matches only itself.\n\n---\n\n### `patternCovers(broad, narrow): boolean`\n\n```ts\npatternCovers(broad: string, narrow: string): boolean;\n```\n\nTests whether the `broad` pattern covers the `narrow` pattern. `'*'` covers everything; an exact string covers only itself.\n\n---\n\n## Devtools\n\n### `debugWard(rules, options?)`\n\nSub-path import: `@vielzeug/ward/devtools`.\n\n```ts\nimport { debugWard } from '@vielzeug/ward/devtools';\n```\n\nDiagnostic factory for development inspection.\n\n---\n\n## Types\n\n```ts\nexport type UserPrincipal = {\n attributes?: Record<string, unknown>;\n id: string;\n roles: readonly string[];\n};\n\nexport type Principal = UserPrincipal | null;\n\nexport type RuleContext<TData = unknown> = {\n data?: TData;\n principal: UserPrincipal;\n};\n\nexport type WardPredicate<TData = unknown> = (ctx: RuleContext<TData>) => boolean;\n\nexport type WardRule<TAction extends string = string, TData = unknown> = {\n action: TAction | typeof WILDCARD;\n effect: 'allow' | 'deny';\n priority?: number;\n resource: string | typeof WILDCARD;\n role: string | readonly string[];\n when?: WardPredicate<TData>;\n};\n\nexport type NormalizedWardRule<TAction extends string = string, TData = unknown> = Readonly<{\n action: TAction | typeof WILDCARD;\n effect: 'allow' | 'deny';\n priority: number;\n resource: string | typeof WILDCARD;\n role: readonly string[];\n when?: WardPredicate<TData>;\n}>;\n\nexport type WardDecision<TAction extends string = string, TData = unknown> =\n | { allowed: true; rule: Readonly<NormalizedWardRule<TAction, TData>> }\n | { allowed: false; reason: 'explicit-deny'; rule: Readonly<NormalizedWardRule<TAction, TData>> }\n | { allowed: false; reason: 'no-matching-rule' };\n\nexport type WardCheck<TAction extends string = string, TData = unknown> = {\n action: TAction;\n data?: TData;\n resource: string;\n};\n\nexport type WardDecisionResult<TAction extends string = string, TData = unknown> = WardDecision<TAction, TData> & {\n action: TAction;\n resource: string;\n};\n\nexport type WardDecisionInput<TAction extends string = string, TData = unknown> = {\n action: TAction;\n data?: TData;\n principal: Principal;\n resource: string;\n};\n\nexport type WardAllowedActionsInput<TAction extends string = string, TData = unknown> = {\n data?: TData;\n knownActions: readonly TAction[];\n principal: Principal;\n resource: string;\n};\n\nexport type WardRulesInScopeInput<TData = unknown> = {\n data?: TData;\n principal: Principal;\n resource: string;\n};\n\nexport type BoundWardDecisionInput<TAction extends string = string, TData = unknown> = {\n action: TAction;\n data?: TData;\n resource: string;\n};\n\nexport type BoundWardAllowedActionsInput<TAction extends string = string, TData = unknown> = {\n data?: TData;\n knownActions: readonly TAction[];\n resource: string;\n};\n\nexport type BoundWardRulesInScopeInput<TData = unknown> = {\n data?: TData;\n resource: string;\n};\n\nexport type ConflictKind = 'duplicate' | 'shadowed';\n\nexport type WardConflict<TAction extends string = string, TData = unknown> =\n | {\n indexA: number;\n indexB: number;\n kind: 'duplicate';\n ruleA: Readonly<NormalizedWardRule<TAction, TData>>;\n ruleB: Readonly<NormalizedWardRule<TAction, TData>>;\n }\n | {\n kind: 'shadowed';\n shadowedIndex: number;\n shadowedRule: Readonly<NormalizedWardRule<TAction, TData>>;\n shadowingIndex: number;\n shadowingRule: Readonly<NormalizedWardRule<TAction, TData>>;\n };\n\nexport type WardTraceCandidate<TAction extends string = string, TData = unknown> = {\n index: number;\n priority: number;\n rule: Readonly<NormalizedWardRule<TAction, TData>>;\n score: number;\n won: boolean;\n};\n\nexport type WardTrace<TAction extends string = string, TData = unknown> = {\n candidates: WardTraceCandidate<TAction, TData>[];\n decision: WardDecision<TAction, TData>;\n};\n\nexport type Ward<TAction extends string = string, TData = unknown> = {\n allowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[];\n checkAll(principal: Principal, checks: readonly WardCheck<TAction, TData>[]): WardDecisionResult<TAction, TData>[];\n detectConflicts(): readonly WardConflict<TAction, TData>[];\n explain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData>;\n forUser(principal: UserPrincipal): BoundWard<TAction, TData>;\n rulesInScope(input: WardRulesInScopeInput<TData>): ReadonlyArray<Readonly<NormalizedWardRule<TAction, TData>>>;\n trace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData>;\n};\n\nexport type BoundWard<TAction extends string = string, TData = unknown> = {\n allowedActions(input: BoundWardAllowedActionsInput<TAction, TData>): TAction[];\n checkAll(checks: readonly WardCheck<TAction, TData>[]): WardDecisionResult<TAction, TData>[];\n explain(input: BoundWardDecisionInput<TAction, TData>): WardDecision<TAction, TData>;\n rulesInScope(input: BoundWardRulesInScopeInput<TData>): ReadonlyArray<Readonly<NormalizedWardRule<TAction, TData>>>;\n trace(input: BoundWardDecisionInput<TAction, TData>): WardTrace<TAction, TData>;\n};\n\nexport type WardLoggerContext<TAction extends string = string, TData = unknown> = WardDecision<TAction, TData> & {\n action: TAction;\n data?: TData;\n principal: Principal;\n resource: string;\n};\n\nexport type WardOptions<TAction extends string = string, TData = unknown> = {\n logger?: (context: WardLoggerContext<TAction, TData>) => void;\n maxConflicts?: number;\n onConflict?: (conflict: WardConflict<TAction, TData>) => void;\n strict?: boolean;\n};\n```\n\n`WardDecision`, `WardDecisionResult`, `WardTrace`, `WardTraceCandidate`, and `WardConflict` reference `NormalizedWardRule` (always-array `role`, always-number `priority`).\n\n`Ward`, `BoundWard`, `WardDecision`, `WardDecisionResult`, `WardTrace`, `WardTraceCandidate`, `WardConflict`,\n`NormalizedWardRule`, `WardOptions`, `WardCheck`, `WardAllowedActionsInput`, `WardRulesInScopeInput`, `RuleContext`,\n`WardLoggerContext`, `WardPredicate`, and `ConflictKind` are exported from the root entry point.\n\n## Errors\n\n- `WardError` is the base error class; use `WardError.is(value)` for narrowing.\n- `WardConfigError` reports malformed rules, invalid `createWard` options (`logger`, `onConflict`, `maxConflicts`), invalid principals, and strict conflict initialization.\n- `WardPredicateError` reports a throwing synchronous predicate and includes its `ruleIndex` and cause.\n",
6
- "usage": "---\ntitle: Ward — Usage Guide\ndescription: Build deterministic authorization policies with immutable rule sets, wildcard support, and runtime predicates.\n---\n\n[[toc]]\n\n## Basic Usage\n\n```ts\nimport { WILDCARD, allow, createWard, deny } from '@vielzeug/ward';\n\nconst ward = createWard([\n allow('viewer', 'posts', ['read']),\n allow('editor', 'posts', ['update']),\n deny('blocked', 'posts', [WILDCARD], { priority: 100 }),\n]);\n```\n\n`allow()`, `deny()`, and `ruleFor()` return `WardRule[]` (one rule per action). Pass them directly to `createWard` — no spread needed. Rules are immutable after creation. Create a new ward to update policy.\n\n## Explain a Decision\n\n```ts\nconst decision = ward.explain({\n principal: { id: 'u1', roles: ['editor'] },\n resource: 'posts',\n action: 'update',\n data: { authorId: 'u1' },\n});\n\nif (decision.allowed) {\n console.log(decision.rule);\n} else {\n console.log(decision.reason); // 'no-matching-rule' | 'explicit-deny'\n}\n```\n\n## Batch Decisions\n\n```ts\nconst results = ward.checkAll({ id: 'u1', roles: ['editor'] }, [\n { resource: 'posts', action: 'read' },\n { resource: 'posts', action: 'update', data: { authorId: 'u1' } },\n]);\n```\n\n## Bound Ward (`forUser`)\n\n```ts\nconst bound = ward.forUser({ id: 'u1', roles: ['editor'] });\n\nbound.explain({ resource: 'posts', action: 'read' });\nbound.trace({ resource: 'posts', action: 'update', data: { authorId: 'u1' } });\nbound.rulesInScope({ resource: 'posts' });\nbound.allowedActions({ resource: 'posts', knownActions: ['read', 'update', 'delete'] as const });\n```\n\n`forUser()` snapshots the principal. Re-bind when roles/identity change.\n\n## Allowed Actions\n\n`allowedActions()` evaluates a provided action set:\n\n```ts\nconst actions = ward.allowedActions({\n principal: { id: 'u1', roles: ['admin'] },\n resource: 'posts',\n knownActions: ['read', 'update', 'delete'] as const,\n});\n```\n\nIt does not fire the logger.\n\n## Rule Introspection\n\n```ts\nconst scoped = ward.rulesInScope({\n principal: { id: 'u1', roles: ['editor'] },\n resource: 'posts',\n});\n```\n\nUse optional `data` to filter predicate-gated matches.\n\n## Trace Candidates\n\n```ts\nconst trace = ward.trace({\n principal: { id: 'u1', roles: ['editor', 'blocked'] },\n resource: 'posts',\n action: 'read',\n});\n\ntrace.candidates.forEach((c) => {\n console.log(c.index, c.priority, c.score, c.won);\n});\n```\n\n`trace()` does not fire the logger.\n\n## Predicate Helpers\n\n```ts\nimport { owns, predicate } from '@vielzeug/ward';\n\nconst isOwner = owns('authorId');\nconst canEdit = predicate.and(isOwner, ({ principal }) => principal.id !== '');\n```\n\nAsync predicates are rejected at runtime with `WardPredicateError`.\n\n## Request Guards\n\nUse `explain()` directly at request boundaries. Extract the principal from your framework's request object and pass it to Ward:\n\n```ts\nconst principal = await extractPrincipal(req);\nconst decision = ward.explain({ principal, resource: 'posts', action: 'read' });\n\nif (!decision.allowed) {\n return res.status(403).json({ error: decision.reason });\n}\n```\n\n## Testing\n\nTest policy outcomes through `explain()` so each test captures an allowed, explicit-deny, or no-match result.\n\n```ts\nimport { expect, it } from 'vitest';\n\nit('denies an action with no matching rule', () => {\n expect(\n ward.explain({ principal: { id: 'u1', roles: ['viewer'] }, resource: 'posts', action: 'delete' }).allowed,\n ).toBe(false);\n});\n```\n\n## Framework Integration\n\nKeep Ward independent from rendering frameworks. Obtain a current principal from framework state, bind it with `forUser()`, and rebind whenever identity or roles change.\n\n::: code-group\n\n```tsx [React]\nconst actions = ward.forUser(user).allowedActions({ resource: 'posts', knownActions: ['read', 'update'] as const });\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nconst actions = ward\n .forUser(user.value)\n .allowedActions({ resource: 'posts', knownActions: ['read', 'update'] as const });\n</script>\n```\n\n```ts [Svelte]\nconst actions = ward.forUser(user).allowedActions({ resource: 'posts', knownActions: ['read', 'update'] as const });\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### With Wayfinder\n\nEnforce Ward decisions in Wayfinder route guards by calling `explain()` inside the guard callback:\n\n```ts\nconst decision = ward.explain({ principal, resource: route.meta.resource, action: 'read' });\n\nif (!decision.allowed) return '/forbidden';\n```\n\n### With Conduit\n\nInject a Ward instance into Conduit-managed services so authorization checks share a single compiled policy:\n\n```ts\nconst ward = createWard(rules);\ncontainer.register('ward', ward);\n```\n\n## Best Practices\n\n- Model default-deny by adding only explicit allow rules.\n- Keep predicates synchronous and provide required resource data.\n- Assign priority deliberately before relying on specificity.\n- Rebind `forUser()` when identity or roles change.\n- Use `trace()` and `detectConflicts()` to diagnose policy behavior.\n- Enforce authorization again at request and mutation boundaries.\n",
7
- "examples": "---\ntitle: Ward — Examples\ndescription: Practical examples and recipes for ward.\n---\n\n## Examples\n\n- [Blog Roles](./examples/blog-roles.md)\n- [Multi-Role Rules](./examples/multi-role-rules.md)\n- [Wildcard Action](./examples/wildcard-action.md)\n- [Priority and Overrides](./examples/inheritance-and-overrides.md)\n- [Bound Guard in UI Layer](./examples/bound-guard-in-ui-layer.md)\n- [Rule Specificity](./examples/disabling-wildcard-fallback.md)\n- [Logger for Auditing](./examples/logger-for-auditing.md)\n- [Fresh Ward Per Test](./examples/snapshot-restore-for-test-isolation.md)\n- [Conflict Detection](./examples/conflict-detection.md)\n- [Trace a Decision](./examples/trace-decision.md)\n"
4
+ "index": "---\ntitle: Ward — Deterministic authorization for TypeScript\ndescription: Typed authorization policies with wildcard matching, deterministic precedence, and decision tracing.\npackage: ward\ncategory: auth\nkeywords: [authorization, rbac, permissions, policy, roles, wildcard, predicates]\nrelated: [wayfinder, conduit, herald]\nexports: [createWard, allow, deny, ruleFor, owns, predicate, ANONYMOUS, WILDCARD, WardError, WardConfigError, WardPredicateError, NormalizedWardRule, matchesPattern, patternCovers]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ward\" />\n\n## Why Ward?\n\nWard keeps authorization policies declarative and decision ordering deterministic. Define rules once, then explain or trace every permission decision without embedding role checks across handlers.\n\n```ts\n// Before\nconst canUpdate = user.roles.includes('editor') && post.authorId === user.id;\n\n// After\nimport { allow, createWard, owns } from '@vielzeug/ward';\n\nconst ward = createWard([\n allow('editor', 'posts', ['update'], { when: owns('authorId') }),\n]);\n\nconst decision = ward.explain({ principal: user, resource: 'posts', action: 'update', data: post });\nconst canUpdate = decision.allowed;\n```\n\n| Feature | Ward | CASL | AccessControl |\n| ------------------------ | -------------------------------------------- | ---------------------------------------- | ---------------------------------------- |\n| Bundle size | <PackageInfo package=\"ward\" type=\"size\" /> | Larger policy engine | Larger policy engine |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Deterministic precedence | Priority, specificity, deny, order | Rule-dependent | Role-grant dependent |\n| Decision tracing | `trace()` candidates and winner | Manual inspection | Manual inspection |\n\n<div class=\"decision-callout\">\n\n**Use Ward when** your application needs typed role/resource/action policies with explainable, deterministic outcomes.\n\n**Consider framework-specific authorization when** your application only needs one framework's built-in route or component guard layer.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ward\n```\n\n```sh [npm]\nnpm install @vielzeug/ward\n```\n\n```sh [yarn]\nyarn add @vielzeug/ward\n```\n\n:::\n\n## Quick Start\n\nCreate a small policy and handle both allowed and denied decisions at the request boundary.\n\n```ts\nimport { allow, createWard } from '@vielzeug/ward';\n\nconst ward = createWard([\n allow('viewer', 'posts', ['read']),\n allow('editor', 'posts', ['update']),\n]);\n\nconst decision = ward.explain({\n principal: { id: 'u1', roles: ['editor'] },\n resource: 'posts',\n action: 'update',\n});\n\nif (decision.allowed) console.log('Update post');\nelse console.log(decision.reason);\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `createWard()` creates immutable typed policy instances. Accepts `allow()`/`deny()` results directly — no spread needed.\n- `allow()`, `deny()`, and `ruleFor()` build role/resource/action rules.\n- `WILDCARD` and `ANONYMOUS` model broad or unauthenticated access explicitly.\n- `owns()` and `predicate` constrain rules with synchronous request data.\n- `explain()`, `trace()`, and `detectConflicts()` make policy decisions diagnosable.\n- `tap()` subscribes to decision events for logging and diagnostics.\n- `forUser()` creates a principal-bound view for repeated checks.\n- `checkAll()` evaluates multiple resource/action pairs in one call.\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- [Wayfinder](/wayfinder/) — route middleware can enforce Ward decisions during navigation.\n- [Conduit](/conduit/) — inject a Ward policy into application services.\n- [Herald](/herald/) — publish authorization outcomes as typed application events.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Ward — API Reference\ndescription: Complete API reference for @vielzeug/ward.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createWard` | Creates immutable policy | Sync | Rules cannot be mutated after creation |\n| `allow` / `deny` / `ruleFor` | Builds policy rules | Sync | Priority wins before specificity |\n| `Ward.explain` | Returns one decision | Sync | Pass resource data for predicate rules |\n| `Ward.trace` | Inspects decision candidates | Sync | Does not fire a `decision` event |\n| `Ward.forUser` | Binds a principal | Sync | Rebind when identity or roles change |\n| `Ward.checkAll` | Batch permission checks | Sync | Pass resource data for predicate rules |\n| `Ward.allowedActions` | Filters known actions to allowed set | Sync | Does not fire a `decision` event |\n| `Ward.rulesInScope` | Lists rules matching a principal/resource | Sync | Pass data to evaluate predicates |\n| `Ward.detectConflicts` | Detects duplicate/shadowed rules | Sync | O(n²) — use `maxConflicts` for large policies |\n| `predicate.owns` / `owns` | Ownership predicate on resource data | Sync | Skipped for anonymous principals |\n| `predicate.and` / `or` / `not` | Combine predicates | Sync | All inputs must be synchronous |\n| `matchesPattern` / `patternCovers` | Test resource pattern coverage | Sync | `'*'` is the only wildcard |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/ward` | Rules, factory, predicates, pattern helpers, errors, and public types |\n\n## Core Factory\n\n### `createWard(rules, options?)`\n\n```ts\ncreateWard<TAction extends string = string, TData = unknown>(\n rules: readonly (WardRule<TAction, TData> | readonly WardRule<TAction, TData>[])[] = [],\n options?: WardOptions<TAction, TData>,\n): Ward<TAction, TData>;\n```\n\nCreates an immutable ward instance. `rules` accepts a flat mix of single rules and rule arrays — `allow()`/`deny()`/`ruleFor()` results can be passed directly without spread. Validates `onConflict` and `maxConflicts` options before compiling rules; invalid values throw `WardConfigError`.\n\n**Parameters:**\n\n| Name | Type | Description |\n| --- | --- | --- |\n| `rules` | `readonly (WardRule \\| readonly WardRule[])[]` | Rule list. Single rules and rule arrays can be mixed. |\n| `options.onConflict` | `(conflict: WardConflict) => void` | Called synchronously per conflict at creation time. |\n| `options.strict` | `boolean` | Throws `WardConfigError` on the first conflict. |\n| `options.maxConflicts` | `number` | Caps the number of conflicts returned by `detectConflicts()`. |\n\n**Returns:** `Ward<TAction, TData>` — an immutable policy instance.\n\n**Example:**\n\n```ts\nimport { allow, createWard, deny, WILDCARD } from '@vielzeug/ward';\n\nconst ward = createWard([\n allow('viewer', 'posts', ['read']),\n allow('editor', 'posts', ['update']),\n deny('blocked', WILDCARD, [WILDCARD], { priority: 100 }),\n]);\n```\n\n---\n\n## Rule Builders\n\n### `allow(role, resource, actions, options?)`\n\n```ts\nallow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: { priority?: number; when?: WardPredicate<TData> },\n): WardRule<TAction, TData>[];\n```\n\nCreates one `WardRule` per action with `effect: 'allow'`. Reads naturally: \"allow editor to read/update posts\".\n\n**Returns:** `WardRule[]` — one rule per action.\n\n---\n\n### `deny(role, resource, actions, options?)`\n\n```ts\ndeny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: { priority?: number; when?: WardPredicate<TData> },\n): WardRule<TAction, TData>[];\n```\n\nCreates one `WardRule` per action with `effect: 'deny'`. Reads naturally: \"deny blocked from reading posts\".\n\n**Returns:** `WardRule[]` — one rule per action.\n\n---\n\n### `ruleFor(effect, role, resource, actions, options?)`\n\n```ts\nruleFor<TAction extends string = string, TData = unknown>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: { priority?: number; when?: WardPredicate<TData> },\n): WardRule<TAction, TData>[];\n```\n\nLow-level factory. Prefer `allow()` or `deny()` for ergonomic rule authoring.\n\n**Returns:** `WardRule[]` — one rule per action.\n\n---\n\n## Ward Methods\n\n### `checkAll(principal, checks)`\n\n```ts\ncheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n): WardDecisionResult<TAction, TData>[];\n```\n\nEvaluates multiple resource/action pairs for one principal. Fires a `decision` event for each result via `tap()`.\n\n**Returns:** `WardDecisionResult[]` — each entry carries `action`, `resource`, and the decision.\n\n---\n\n### `explain(input)`\n\n```ts\nexplain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData>;\n```\n\n`WardDecisionInput`:\n\n```ts\n{\n principal: Principal;\n resource: string;\n action: TAction;\n data?: TData;\n}\n```\n\nReturns one decision. Fires a `decision` event via `tap()`.\n\n**Returns:** `WardDecision` — `{ allowed: true; rule }` or `{ allowed: false; reason: 'explicit-deny'; rule }` or `{ allowed: false; reason: 'no-matching-rule' }`.\n\n---\n\n### `trace(input)`\n\n```ts\ntrace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData>;\n```\n\nSame request shape as `explain()`. Returns winner + candidate list. Does not fire a `decision` event.\n\n**Returns:** `WardTrace` — `{ candidates: WardTraceCandidate[]; decision: WardDecision }`.\n\n---\n\n### `allowedActions(input)`\n\n```ts\nallowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[];\n```\n\nInput shape:\n\n```ts\n{\n principal: Principal;\n resource: string;\n knownActions: readonly TAction[];\n data?: TData;\n}\n```\n\nFilters the provided `knownActions` list to those the principal may perform. Does not fire a `decision` event.\n\n**Returns:** `TAction[]` — the subset of `knownActions` that `explain()` would allow.\n\n---\n\n### `rulesInScope(input)`\n\n```ts\nrulesInScope(input: WardRulesInScopeInput<TData>): ReadonlyArray<Readonly<NormalizedWardRule<TAction, TData>>>;\n```\n\nInput shape:\n\n```ts\n{\n principal: Principal;\n resource: string;\n data?: TData;\n}\n```\n\nLists rules matching the principal/resource pair. Pass `data` to evaluate predicate-gated matches; without it, predicate rules are skipped.\n\n**Returns:** `ReadonlyArray<Readonly<NormalizedWardRule>>` — rules in their normalized form (`role` always array, `priority` always number).\n\n---\n\n### `detectConflicts()`\n\n```ts\ndetectConflicts(): readonly WardConflict<TAction, TData>[];\n```\n\nLazily computes and caches duplicate/shadowed rule conflicts. O(n²) — use `maxConflicts` for large policies.\n\n**Returns:** `readonly WardConflict[]` — `{ kind: 'duplicate'; indexA; indexB; ruleA; ruleB }` or `{ kind: 'shadowed'; shadowedIndex; shadowedRule; shadowingIndex; shadowingRule }`.\n\n---\n\n### `forUser(principal)`\n\n```ts\nforUser(principal: UserPrincipal): BoundWard<TAction, TData>;\n```\n\nReturns a principal-bound view. `UserPrincipal` (not nullable — use `null` directly with `explain()` for anonymous).\n\n**Returns:** `BoundWard` — same methods without the `principal` argument.\n\n---\n\n## `BoundWard` Methods\n\n```ts\ntype BoundWard<TAction extends string = string, TData = unknown> = {\n allowedActions(input: BoundWardAllowedActionsInput<TAction, TData>): TAction[];\n checkAll(checks: readonly WardCheck<TAction, TData>[]): WardDecisionResult<TAction, TData>[];\n explain(input: BoundWardDecisionInput<TAction, TData>): WardDecision<TAction, TData>;\n rulesInScope(input: BoundWardRulesInScopeInput<TData>): ReadonlyArray<Readonly<NormalizedWardRule<TAction, TData>>>;\n trace(input: BoundWardDecisionInput<TAction, TData>): WardTrace<TAction, TData>;\n};\n```\n\nBound input shapes remove `principal`:\n\n```ts\n{ resource: string; action: TAction; data?: TData } // explain/trace\n{ resource: string; knownActions: readonly TAction[]; data?: TData } // allowedActions\n{ resource: string; data?: TData } // rulesInScope\n```\n\n---\n\n## Predicate Helpers\n\n### `predicate.owns(attributeKey)`\n\n```ts\npredicate.owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n): WardPredicate<TData>;\n```\n\nReturns a `WardPredicate` that checks whether `data[attributeKey]` matches `principal.id`. Skipped for anonymous principals — pairing `owns` with an `ANONYMOUS`-role rule produces a rule that can never match.\n\n**Returns:** `WardPredicate<TData>`.\n\n---\n\n### `predicate.and(...predicates)`\n\n```ts\npredicate.and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData>;\n```\n\nAll predicates must return `true`.\n\n---\n\n### `predicate.or(...predicates)`\n\n```ts\npredicate.or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData>;\n```\n\nAt least one predicate must return `true`.\n\n---\n\n### `predicate.not(predicate)`\n\n```ts\npredicate.not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData>;\n```\n\nInverts the given predicate.\n\n---\n\n### `owns(attributeKey)` (alias)\n\n```ts\nowns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n): WardPredicate<TData>;\n```\n\nTop-level re-export of `predicate.owns`.\n\nPredicates run synchronously. Returning a Promise throws `WardPredicateError`.\n\n---\n\n## Pattern Helpers\n\n### `matchesPattern(pattern, value): boolean`\n\n```ts\nmatchesPattern(pattern: string, value: string): boolean;\n```\n\nTests whether `value` matches a `'*'`-wildcard `pattern`. `'*'` matches any value; an exact string matches only itself.\n\n---\n\n### `patternCovers(broad, narrow): boolean`\n\n```ts\npatternCovers(broad: string, narrow: string): boolean;\n```\n\nTests whether the `broad` pattern covers the `narrow` pattern. `'*'` covers everything; an exact string covers only itself.\n\n---\n\n## Observability\n\n### `tap(handler, options?)`\n\n```ts\ntap(\n handler: (event: WardEvent<TAction, TData>) => void,\n options?: { signal?: AbortSignal },\n): () => void;\n```\n\nSubscribes a handler to ward events. Each `explain()` and `checkAll()` decision fires a `decision` event. `trace()` and `allowedActions()` do not fire events.\n\nPass an `AbortSignal` to unsubscribe automatically; the returned function unsubscribes manually.\n\n**Returns:** `() => void` — call to unsubscribe the handler.\n\n**Example:**\n\n```ts\nconst ward = createWard(rules);\nward.tap((event) => console.debug(`ward:${event.type}`, event.decision));\n```\n\nWith a logger from `@vielzeug/rune`:\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\nconst log = createLogger({ name: 'ward' });\nward.tap((event) => log.debug(event, 'ward:decision'));\n```\n\n---\n\n## Types\n\n```ts\nexport type UserPrincipal = {\n attributes?: Record<string, unknown>;\n id: string;\n roles: readonly string[];\n};\n\nexport type Principal = UserPrincipal | null;\n\nexport type RuleContext<TData = unknown> = {\n data?: TData;\n principal: UserPrincipal;\n};\n\nexport type WardPredicate<TData = unknown> = (ctx: RuleContext<TData>) => boolean;\n\nexport type WardRule<TAction extends string = string, TData = unknown> = {\n action: TAction | typeof WILDCARD;\n effect: 'allow' | 'deny';\n priority?: number;\n resource: string | typeof WILDCARD;\n role: string | readonly string[];\n when?: WardPredicate<TData>;\n};\n\nexport type NormalizedWardRule<TAction extends string = string, TData = unknown> = Readonly<{\n action: TAction | typeof WILDCARD;\n effect: 'allow' | 'deny';\n priority: number;\n resource: string | typeof WILDCARD;\n role: readonly string[];\n when?: WardPredicate<TData>;\n}>;\n\nexport type WardDecision<TAction extends string = string, TData = unknown> =\n | { allowed: true; rule: Readonly<NormalizedWardRule<TAction, TData>> }\n | { allowed: false; reason: 'explicit-deny'; rule: Readonly<NormalizedWardRule<TAction, TData>> }\n | { allowed: false; reason: 'no-matching-rule' };\n\nexport type WardCheck<TAction extends string = string, TData = unknown> = {\n action: TAction;\n data?: TData;\n resource: string;\n};\n\nexport type WardDecisionResult<TAction extends string = string, TData = unknown> = WardDecision<TAction, TData> & {\n action: TAction;\n resource: string;\n};\n\nexport type WardDecisionInput<TAction extends string = string, TData = unknown> = {\n action: TAction;\n data?: TData;\n principal: Principal;\n resource: string;\n};\n\nexport type WardAllowedActionsInput<TAction extends string = string, TData = unknown> = {\n data?: TData;\n knownActions: readonly TAction[];\n principal: Principal;\n resource: string;\n};\n\nexport type WardRulesInScopeInput<TData = unknown> = {\n data?: TData;\n principal: Principal;\n resource: string;\n};\n\nexport type BoundWardDecisionInput<TAction extends string = string, TData = unknown> = {\n action: TAction;\n data?: TData;\n resource: string;\n};\n\nexport type BoundWardAllowedActionsInput<TAction extends string = string, TData = unknown> = {\n data?: TData;\n knownActions: readonly TAction[];\n resource: string;\n};\n\nexport type BoundWardRulesInScopeInput<TData = unknown> = {\n data?: TData;\n resource: string;\n};\n\nexport type ConflictKind = 'duplicate' | 'shadowed';\n\nexport type WardConflict<TAction extends string = string, TData = unknown> =\n | {\n indexA: number;\n indexB: number;\n kind: 'duplicate';\n ruleA: Readonly<NormalizedWardRule<TAction, TData>>;\n ruleB: Readonly<NormalizedWardRule<TAction, TData>>;\n }\n | {\n kind: 'shadowed';\n shadowedIndex: number;\n shadowedRule: Readonly<NormalizedWardRule<TAction, TData>>;\n shadowingIndex: number;\n shadowingRule: Readonly<NormalizedWardRule<TAction, TData>>;\n };\n\nexport type WardTraceCandidate<TAction extends string = string, TData = unknown> = {\n index: number;\n priority: number;\n rule: Readonly<NormalizedWardRule<TAction, TData>>;\n score: number;\n won: boolean;\n};\n\nexport type WardTrace<TAction extends string = string, TData = unknown> = {\n candidates: WardTraceCandidate<TAction, TData>[];\n decision: WardDecision<TAction, TData>;\n};\n\nexport type Ward<TAction extends string = string, TData = unknown> = {\n allowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[];\n checkAll(principal: Principal, checks: readonly WardCheck<TAction, TData>[]): WardDecisionResult<TAction, TData>[];\n detectConflicts(): readonly WardConflict<TAction, TData>[];\n explain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData>;\n forUser(principal: UserPrincipal): BoundWard<TAction, TData>;\n rulesInScope(input: WardRulesInScopeInput<TData>): ReadonlyArray<Readonly<NormalizedWardRule<TAction, TData>>>;\n tap(handler: (event: WardEvent<TAction, TData>) => void, options?: { signal?: AbortSignal }): () => void;\n trace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData>;\n};\n\nexport type BoundWard<TAction extends string = string, TData = unknown> = {\n allowedActions(input: BoundWardAllowedActionsInput<TAction, TData>): TAction[];\n checkAll(checks: readonly WardCheck<TAction, TData>[]): WardDecisionResult<TAction, TData>[];\n explain(input: BoundWardDecisionInput<TAction, TData>): WardDecision<TAction, TData>;\n rulesInScope(input: BoundWardRulesInScopeInput<TData>): ReadonlyArray<Readonly<NormalizedWardRule<TAction, TData>>>;\n trace(input: BoundWardDecisionInput<TAction, TData>): WardTrace<TAction, TData>;\n};\n\nexport type WardEvent<TAction extends string = string, TData = unknown> = {\n type: 'decision';\n decision: WardDecision<TAction, TData>;\n action: TAction;\n data?: TData;\n principal: Principal;\n resource: string;\n};\n\nexport type WardOptions<TAction extends string = string, TData = unknown> = {\n maxConflicts?: number;\n onConflict?: (conflict: WardConflict<TAction, TData>) => void;\n strict?: boolean;\n};\n```\n\n`WardDecision`, `WardDecisionResult`, `WardTrace`, `WardTraceCandidate`, and `WardConflict` reference `NormalizedWardRule` (always-array `role`, always-number `priority`).\n\n`Ward`, `BoundWard`, `WardDecision`, `WardDecisionResult`, `WardTrace`, `WardTraceCandidate`, `WardConflict`,\n`NormalizedWardRule`, `WardOptions`, `WardCheck`, `WardAllowedActionsInput`, `WardRulesInScopeInput`, `RuleContext`,\n`WardEvent`, `WardPredicate`, and `ConflictKind` are exported from the root entry point.\n\n## Errors\n\n- `WardError` is the base error class; use `instanceof WardError` for narrowing.\n- `WardConfigError` reports malformed rules, invalid `createWard` options (`onConflict`, `maxConflicts`), invalid principals, and strict conflict initialization.\n- `WardPredicateError` reports a throwing synchronous predicate and includes its `ruleIndex` and cause.\n",
6
+ "usage": "---\ntitle: Ward — Usage Guide\ndescription: Build deterministic authorization policies with immutable rule sets, wildcard support, and runtime predicates.\n---\n\n[[toc]]\n\n## Basic Usage\n\n```ts\nimport { WILDCARD, allow, createWard, deny } from '@vielzeug/ward';\n\nconst ward = createWard([\n allow('viewer', 'posts', ['read']),\n allow('editor', 'posts', ['update']),\n deny('blocked', 'posts', [WILDCARD], { priority: 100 }),\n]);\n```\n\n`allow()`, `deny()`, and `ruleFor()` return `WardRule[]` (one rule per action). Pass them directly to `createWard` — no spread needed. Rules are immutable after creation. Create a new ward to update policy.\n\n## Explain a Decision\n\n```ts\nconst decision = ward.explain({\n principal: { id: 'u1', roles: ['editor'] },\n resource: 'posts',\n action: 'update',\n data: { authorId: 'u1' },\n});\n\nif (decision.allowed) {\n console.log(decision.rule);\n} else {\n console.log(decision.reason); // 'no-matching-rule' | 'explicit-deny'\n}\n```\n\n## Batch Decisions\n\n```ts\nconst results = ward.checkAll({ id: 'u1', roles: ['editor'] }, [\n { resource: 'posts', action: 'read' },\n { resource: 'posts', action: 'update', data: { authorId: 'u1' } },\n]);\n```\n\n## Bound Ward (`forUser`)\n\n```ts\nconst bound = ward.forUser({ id: 'u1', roles: ['editor'] });\n\nbound.explain({ resource: 'posts', action: 'read' });\nbound.trace({ resource: 'posts', action: 'update', data: { authorId: 'u1' } });\nbound.rulesInScope({ resource: 'posts' });\nbound.allowedActions({ resource: 'posts', knownActions: ['read', 'update', 'delete'] as const });\n```\n\n`forUser()` snapshots the principal. Re-bind when roles/identity change.\n\n## Allowed Actions\n\n`allowedActions()` evaluates a provided action set:\n\n```ts\nconst actions = ward.allowedActions({\n principal: { id: 'u1', roles: ['admin'] },\n resource: 'posts',\n knownActions: ['read', 'update', 'delete'] as const,\n});\n```\n\nIt does not fire a `decision` event.\n\n## Rule Introspection\n\n```ts\nconst scoped = ward.rulesInScope({\n principal: { id: 'u1', roles: ['editor'] },\n resource: 'posts',\n});\n```\n\nUse optional `data` to filter predicate-gated matches.\n\n## Trace Candidates\n\n```ts\nconst trace = ward.trace({\n principal: { id: 'u1', roles: ['editor', 'blocked'] },\n resource: 'posts',\n action: 'read',\n});\n\ntrace.candidates.forEach((c) => {\n console.log(c.index, c.priority, c.score, c.won);\n});\n```\n\n`trace()` does not fire a `decision` event.\n\n## Observing Decisions\n\n`tap()` subscribes a handler to ward events. Each `explain()` and `checkAll()` decision fires a `decision` event; `trace()` and `allowedActions()` do not.\n\n```ts\nconst ward = createWard(rules);\nward.tap((event) => console.debug(`ward:${event.type}`, event.decision));\n```\n\nPass an `AbortSignal` to unsubscribe automatically, or call the returned function to unsubscribe manually:\n\n```ts\nconst controller = new AbortController();\nconst unsubscribe = ward.tap((event) => console.debug(event), { signal: controller.signal });\n\n// later\nunsubscribe(); // or controller.abort();\n```\n\nFor structured logging, forward events to a `@vielzeug/rune` logger:\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\nconst log = createLogger({ name: 'ward' });\nward.tap((event) => log.debug(event, 'ward:decision'));\n```\n\n## Predicate Helpers\n\n```ts\nimport { owns, predicate } from '@vielzeug/ward';\n\nconst isOwner = owns('authorId');\nconst canEdit = predicate.and(isOwner, ({ principal }) => principal.id !== '');\n```\n\nAsync predicates are rejected at runtime with `WardPredicateError`.\n\n## Request Guards\n\nUse `explain()` directly at request boundaries. Extract the principal from your framework's request object and pass it to Ward:\n\n```ts\nconst principal = await extractPrincipal(req);\nconst decision = ward.explain({ principal, resource: 'posts', action: 'read' });\n\nif (!decision.allowed) {\n return res.status(403).json({ error: decision.reason });\n}\n```\n\n## Testing\n\nTest policy outcomes through `explain()` so each test captures an allowed, explicit-deny, or no-match result.\n\n```ts\nimport { expect, it } from 'vitest';\n\nit('denies an action with no matching rule', () => {\n expect(\n ward.explain({ principal: { id: 'u1', roles: ['viewer'] }, resource: 'posts', action: 'delete' }).allowed,\n ).toBe(false);\n});\n```\n\n## Framework Integration\n\nKeep Ward independent from rendering frameworks. Obtain a current principal from framework state, bind it with `forUser()`, and rebind whenever identity or roles change.\n\n::: code-group\n\n```tsx [React]\nconst actions = ward.forUser(user).allowedActions({ resource: 'posts', knownActions: ['read', 'update'] as const });\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nconst actions = ward\n .forUser(user.value)\n .allowedActions({ resource: 'posts', knownActions: ['read', 'update'] as const });\n</script>\n```\n\n```ts [Svelte]\nconst actions = ward.forUser(user).allowedActions({ resource: 'posts', knownActions: ['read', 'update'] as const });\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### With Wayfinder\n\nEnforce Ward decisions in Wayfinder route guards by calling `explain()` inside the guard callback:\n\n```ts\nconst decision = ward.explain({ principal, resource: route.meta.resource, action: 'read' });\n\nif (!decision.allowed) return '/forbidden';\n```\n\n### With Conduit\n\nInject a Ward instance into Conduit-managed services so authorization checks share a single compiled policy:\n\n```ts\nconst ward = createWard(rules);\ncontainer.register('ward', ward);\n```\n\n## Best Practices\n\n- Model default-deny by adding only explicit allow rules.\n- Keep predicates synchronous and provide required resource data.\n- Assign priority deliberately before relying on specificity.\n- Rebind `forUser()` when identity or roles change.\n- Use `trace()` and `detectConflicts()` to diagnose policy behavior.\n- Enforce authorization again at request and mutation boundaries.\n",
7
+ "examples": "---\ntitle: Ward — Examples\ndescription: Practical examples and recipes for ward.\n---\n\n## Examples\n\n- [Blog Roles](./examples/blog-roles.md)\n- [Multi-Role Rules](./examples/multi-role-rules.md)\n- [Wildcard Action](./examples/wildcard-action.md)\n- [Priority and Overrides](./examples/inheritance-and-overrides.md)\n- [Bound Guard in UI Layer](./examples/bound-guard-in-ui-layer.md)\n- [Rule Specificity](./examples/disabling-wildcard-fallback.md)\n- [Auditing Decisions](./examples/logger-for-auditing.md)\n- [Fresh Ward Per Test](./examples/snapshot-restore-for-test-isolation.md)\n- [Conflict Detection](./examples/conflict-detection.md)\n- [Trace a Decision](./examples/trace-decision.md)\n"
8
8
  },
9
9
  "examples": [
10
10
  {
@@ -87,28 +87,28 @@
87
87
  "createWard": "export { createWard } from './factory';",
88
88
  "matchesPattern": "export { matchesPattern, patternCovers } from './resource';",
89
89
  "patternCovers": "export { matchesPattern, patternCovers } from './resource';",
90
- "BoundWard": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
91
- "BoundWardAllowedActionsInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
92
- "BoundWardDecisionInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
93
- "BoundWardRulesInScopeInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
94
- "ConflictKind": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
95
- "NormalizedWardRule": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
96
- "Principal": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
97
- "RuleContext": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
98
- "UserPrincipal": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
99
- "Ward": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
100
- "WardAllowedActionsInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
101
- "WardCheck": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
102
- "WardConflict": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
103
- "WardDecision": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
104
- "WardDecisionInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
105
- "WardDecisionResult": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
106
- "WardLoggerContext": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
107
- "WardOptions": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
108
- "WardPredicate": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
109
- "WardRule": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
110
- "WardRulesInScopeInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
111
- "WardTrace": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
112
- "WardTraceCandidate": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardLoggerContext,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';"
90
+ "BoundWard": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
91
+ "BoundWardAllowedActionsInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
92
+ "BoundWardDecisionInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
93
+ "BoundWardRulesInScopeInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
94
+ "ConflictKind": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
95
+ "NormalizedWardRule": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
96
+ "Principal": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
97
+ "RuleContext": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
98
+ "UserPrincipal": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
99
+ "Ward": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
100
+ "WardAllowedActionsInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
101
+ "WardCheck": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
102
+ "WardConflict": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
103
+ "WardDecision": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
104
+ "WardDecisionInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
105
+ "WardDecisionResult": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
106
+ "WardEvent": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
107
+ "WardOptions": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
108
+ "WardPredicate": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
109
+ "WardRule": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
110
+ "WardRulesInScopeInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
111
+ "WardTrace": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';",
112
+ "WardTraceCandidate": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n ConflictKind,\n NormalizedWardRule,\n Principal,\n RuleContext,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardPredicate,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';"
113
113
  }
114
114
  }