@vielzeug/codex 3.0.2 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +1899 -0
- package/data/llms-full.txt +30099 -0
- package/data/llms.txt +45 -0
- package/data/manifest.json +8 -0
- package/data/packages/arsenal.json +216 -0
- package/data/packages/assay.json +46 -0
- package/data/packages/clockwork.json +66 -0
- package/data/packages/codex.json +29 -0
- package/data/packages/coins.json +101 -0
- package/data/packages/conduit.json +68 -0
- package/data/packages/courier.json +55 -0
- package/data/packages/dnd.json +60 -0
- package/data/packages/familiar.json +17 -0
- package/data/packages/flux.json +94 -0
- package/data/packages/focus.json +38 -0
- package/data/packages/forge.json +59 -0
- package/data/packages/gesture.json +37 -0
- package/data/packages/herald.json +81 -0
- package/data/packages/illusionist.json +132 -0
- package/data/packages/keymap.json +51 -0
- package/data/packages/ledger.json +60 -0
- package/data/packages/lingua.json +70 -0
- package/data/packages/necromancer.json +50 -0
- package/data/packages/orbit.json +99 -0
- package/data/packages/ore.json +65 -0
- package/data/packages/postmaster.json +49 -0
- package/data/packages/prism.json +66 -0
- package/data/packages/pulse.json +71 -0
- package/data/packages/refine.json +12 -0
- package/data/packages/ripple.json +88 -0
- package/data/packages/rune.json +78 -0
- package/data/packages/sandbox.json +26 -0
- package/data/packages/scout.json +63 -0
- package/data/packages/scroll.json +110 -0
- package/data/packages/sentinel.json +35 -0
- package/data/packages/sourcerer.json +70 -0
- package/data/packages/spell.json +128 -0
- package/data/packages/tempo.json +81 -0
- package/data/packages/vault.json +93 -0
- package/data/packages/ward.json +80 -0
- package/data/packages/wayfinder.json +114 -0
- package/data/refine.json +12235 -0
- package/data/search.json +1534 -0
- package/package.json +4 -5
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "// Tempo keeps this re-export so all consumers share one Temporal implementation and version.\nexport { Temporal } from '@js-temporal/polyfill';\nexport { inTimeZone, toInstant } from './_convert';\nexport { endOf, startOf } from './boundary';\nexport { classifyExpiry, timeDiff } from './classify';\nexport { clamp, contains, isAfter, isBefore, isSame } from './compare';\nexport { difference, isValid, now, nowInstant, parse, shift } from './core';\nexport {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';\nexport {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';\nexport { dateRange, recurrence } from './range';\nexport type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';\n",
|
|
3
|
+
"docs": {
|
|
4
|
+
"index": "---\ntitle: Tempo — Temporal date and time utilities\ndescription: Explicit Temporal parsing, timezone-safe arithmetic, and localized date/time formatting for TypeScript.\npackage: tempo\ncategory: time\nkeywords: [temporal, date-time, timezone, formatting, arithmetic, dst, intl]\nrelated: [rune, vault]\nexports: [Temporal, parse, now, nowInstant, isValid, toInstant, inTimeZone, shift, difference, isBefore, isAfter, isSame, contains, clamp, startOf, endOf, dateRange, recurrence, format, formatParts, formatRange, formatRangeParts, formatInstant, formatZoned, formatRelative, parseDuration, formatDuration, classifyExpiry, timeDiff, humanize, TempoError, TempoInvalidInputError, TempoInvalidTzError, TempoMissingTzError, TempoUnsupportedInputError]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"tempo\" />\n\n## Why Tempo?\n\nDate/time bugs come from treating an instant and a wall-clock value as interchangeable. Tempo requires an explicit parse target and requires `timeZone` whenever a wall-clock value becomes an instant.\n\n```ts\n// Before\nconst reminder = new Date(meeting.getTime() - 15 * 60_000);\n\n// After\nimport { parse, shift } from '@vielzeug/tempo';\n\nconst localMeeting = parse('2026-03-21T10:30:00', { as: 'plainDateTime' });\nconst reminder = shift(localMeeting, { minutes: -15 }, { timeZone: 'America/New_York' });\n```\n\n| Feature | Tempo | date-fns | Native Date |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"tempo\" type=\"size\" /> | ~10 kB | 0 kB |\n| Zero dependencies | <ore-icon name=\"x\" size=\"16\"></ore-icon> `@js-temporal/polyfill` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Explicit wall-time conversion | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| DST-safe arithmetic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | Manual |\n| Localized formatting | <ore-icon name=\"check\" size=\"16\"></ore-icon> `Intl` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Tempo when** you need Temporal values, explicit timezone rules, and DST-safe operations.\n\n**Consider native `Date` when** your data is only elapsed milliseconds and you do not need calendar or timezone behavior.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/tempo\n```\n\n```sh [npm]\nnpm install @vielzeug/tempo\n```\n\n```sh [yarn]\nyarn add @vielzeug/tempo\n```\n\n:::\n\n## Quick Start\n\nParse a wall-clock input explicitly, attach its timezone, then format it for a user.\n\n```ts\nimport { format, parse, shift } from '@vielzeug/tempo';\n\nconst localMeeting = parse('2026-03-21T10:30:00', { as: 'plainDateTime' });\nconst reminder = shift(localMeeting, { minutes: -15 }, { timeZone: 'America/New_York' });\nconst text = format(reminder, {\n locale: 'en-US',\n pattern: 'short',\n});\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `parse()` — Requires an explicit ISO target: instant, zoned date-time, plain date-time, or plain date.\n- `toInstant()` / `inTimeZone()` — Convert wall-clock and absolute values with explicit timezone semantics.\n- `shift()` / `difference()` — Apply DST-safe arithmetic across instant, zoned, and wall-clock inputs.\n- `contains()` / `clamp()` — Compare normalized ranges with optional calendar-unit precision.\n- `startOf()` / `endOf()` — Resolve timezone-aware calendar boundaries, including configurable week starts.\n- `dateRange()` / `recurrence()` — Generate validated, lazy zoned calendar sequences.\n- `format()` / `formatRelative()` / `formatDuration()` — Render localized values through `Intl`, including calendar-aware relative months and years.\n- `classifyExpiry()` — Classify fixed elapsed-time thresholds without month or year approximation.\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- [Rune](/rune/) — format stable Temporal timestamps before writing structured log records.\n- [Vault](/vault/) — derive explicit expiry moments before storing records with TTL policies.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Tempo — API Reference\ndescription: Reference for Tempo Temporal parsing, conversion, arithmetic, comparison, sequence, formatting, and classification APIs.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `parse()` | Parse ISO text to an explicit Temporal kind | Sync | `as` is required |\n| `isValid()` | Narrow an unknown runtime value to `TimeInput` | Sync | Does not parse strings |\n| `toInstant()` | Resolve a value as an absolute instant | Sync | Plain values require `timeZone` |\n| `inTimeZone()` | Project a value to a zone | Sync | Preserves instant, changes wall-clock fields |\n| `shift()` / `difference()` | Timezone-aware arithmetic | Sync | Calendar operations require a timezone |\n| `isBefore()` / `isAfter()` / `isSame()` | Compare absolute values or calendar units | Sync | Plain values require `timeZone` |\n| `contains()` / `clamp()` | Compare normalized ranges | Sync | Bounds normalize automatically |\n| `startOf()` / `endOf()` | Resolve calendar boundaries | Sync | Weeks default to Monday |\n| `dateRange()` / `recurrence()` | Generate lazy zoned sequences | Sync | Steps and intervals must advance |\n| `classifyExpiry()` | Classify fixed elapsed-time thresholds | Sync | Months and years are rejected |\n| `format()` family | Localized and machine formatting | Sync | Use `timeZone`, not `tz` |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/tempo` | Tempo utilities, errors, types, and shared `Temporal` namespace |\n\n## Core Functions\n\n### `parse(input, { as })`\n\n```ts\nparse(input: string, options: { as: 'instant' }): Temporal.Instant;\nparse(input: string, options: { as: 'zonedDateTime' }): Temporal.ZonedDateTime;\nparse(input: string, options: { as: 'plainDateTime' }): Temporal.PlainDateTime;\nparse(input: string, options: { as: 'plainDate' }): Temporal.PlainDate;\n```\n\nParses an ISO 8601 string as the requested temporal kind.\n\n**Parameters**\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `input` | `string` | ISO 8601 input |\n| `options.as` | `ParseAs` | Required result kind |\n\n**Returns:** Requested Temporal value.\n\n**Example:**\n\n```ts\nimport { parse } from '@vielzeug/tempo';\n\nconst instant = parse('2026-03-21T10:15:30Z', { as: 'instant' });\n```\n\n---\n\n### `isValid(value)`\n\n```ts\nisValid(value: unknown): value is TimeInput;\n```\n\nReturns whether `value` is a Tempo-supported Temporal value. It does not parse ISO strings.\n\n**Example:**\n\n```ts\nimport { isValid, parse } from '@vielzeug/tempo';\n\nconst value: unknown = parse('2026-03-21T10:15:30Z', { as: 'instant' });\nconst valid = isValid(value); // true\n```\n\n---\n\n### `now({ timeZone })` / `nowInstant()`\n\n```ts\nnow(options: { timeZone: string }): Temporal.ZonedDateTime;\nnowInstant(): Temporal.Instant;\n```\n\nReturns current zoned or absolute time.\n\n**Example:**\n\n```ts\nimport { now, nowInstant } from '@vielzeug/tempo';\n\nnow({ timeZone: 'Europe/Berlin' });\nnowInstant();\n```\n\n---\n\n### `toInstant(input, options?)` / `inTimeZone(input, timeZone)`\n\n```ts\ntoInstant(input: AbsoluteTime): Temporal.Instant;\ntoInstant(input: WallTime, options: { timeZone: string } & DisambiguationOptions): Temporal.Instant;\ninTimeZone(input: TimeInput, timeZone: string): Temporal.ZonedDateTime;\n```\n\n`toInstant()` resolves wall-clock values. `inTimeZone()` projects a value into a requested timezone.\n\n**Example:**\n\n```ts\nimport { inTimeZone, parse, toInstant } from '@vielzeug/tempo';\n\nconst local = parse('2026-11-01T01:30:00', { as: 'plainDateTime' });\nconst instant = toInstant(local, { disambiguation: 'later', timeZone: 'America/New_York' });\ninTimeZone(instant, 'Europe/Berlin');\n```\n\n---\n\n## Arithmetic and Comparison\n\n### `shift(input, duration, options?)`\n\n```ts\nshift(input: Temporal.ZonedDateTime, duration: Temporal.DurationLike, options?: ShiftOptions): Temporal.ZonedDateTime;\nshift(input: Exclude<TimeInput, Temporal.ZonedDateTime>, duration: Temporal.DurationLike, options: ShiftOptions & { timeZone: string }): Temporal.ZonedDateTime;\n```\n\nAdds a duration through zoned calendar rules.\n\n**Returns:** `Temporal.ZonedDateTime`.\n\n---\n\n### `difference({ start, end, ...options })`\n\n```ts\ndifference(input: DifferenceInput): Temporal.Duration;\n```\n\nReturns the signed duration from `start` to `end` with optional Temporal rounding.\n\n**Returns:** `Temporal.Duration`.\n\n---\n\n### `isBefore()` / `isAfter()` / `isSame()`\n\n```ts\nisBefore(a: TimeInput, b: TimeInput, options?: CompareOptions): boolean;\nisAfter(a: TimeInput, b: TimeInput, options?: CompareOptions): boolean;\nisSame(a: TimeInput, b: TimeInput, options?: CompareOptions): boolean;\n```\n\nCompare absolute instants or their containing calendar units.\n\n**Returns:** A boolean comparison result.\n\n---\n\n### `contains({ value, start, end, ...options })` / `clamp({ value, start, end, ...options })`\n\n```ts\ncontains(input: ContainsInput): boolean;\nclamp(input: ClampInput & { value: Temporal.ZonedDateTime }): Temporal.ZonedDateTime;\nclamp(input: ClampInput): Temporal.Instant;\n```\n\nNormalizes reversed bounds, then checks or constrains the value.\n\n**Returns:** `contains()` returns `boolean`; `clamp()` returns the normalized value or nearest bound.\n\n---\n\n### `startOf()` / `endOf()`\n\n```ts\nstartOf(input: TimeInput, unit: BoundaryUnit, options?: BoundaryOptions): Temporal.ZonedDateTime;\nendOf(input: TimeInput, unit: BoundaryUnit, options?: BoundaryOptions): Temporal.ZonedDateTime;\n```\n\nReturns the first or last nanosecond of a timezone-aware calendar unit.\n\n**Returns:** `Temporal.ZonedDateTime`.\n\n## Calendar Sequences\n\n### `dateRange(start, end, step, options?)`\n\n```ts\ndateRange(start: TimeInput, end: TimeInput, step: Temporal.DurationLike, options?: TimeZoneOptions): Generator<Temporal.ZonedDateTime>;\n```\n\nReturns an inclusive lazy zoned range and rejects steps that do not advance time.\n\n**Returns:** `Generator<Temporal.ZonedDateTime>`.\n\n---\n\n### `recurrence(start, rule, options?)`\n\n```ts\nrecurrence(start: TimeInput, rule: RecurrenceRule, options?: TimeZoneOptions): Generator<Temporal.ZonedDateTime>;\n```\n\nReturns a count- or date-limited lazy recurrence. Intervals must be positive safe integers; counts must be non-negative safe integers.\n\n**Returns:** `Generator<Temporal.ZonedDateTime>`.\n\n## Formatting\n\n### `format(input, options?)`\n\n```ts\nformat(input: TimeInput, options?: FormatOptions): string;\n```\n\nFormats a value through `Intl.DateTimeFormat`.\n\n**Example:**\n\n```ts\nimport { format, parse } from '@vielzeug/tempo';\n\nformat(parse('2026-03-21T10:15:30Z', { as: 'instant' }), {\n locale: 'en-GB',\n pattern: 'short',\n timeZone: 'UTC',\n});\n```\n\n### `formatInstant()` / `formatZoned()` / `formatRelative()` / `formatDuration()`\n\n```ts\nformatInstant(input: TimeInput, options?: TimeZoneOptions): string;\nformatZoned(input: TimeInput, options?: TimeZoneOptions): string;\nformatRelative(input: RelativeTimeInput, options?: RelativeFormatOptions): string;\nformatDuration(input: string | Temporal.DurationLike, options?: DurationFormatOptions): string;\n```\n\n`formatInstant()` produces UTC transport text (`timeZone` needed for wall-time input, ignored for `Instant`). `formatZoned()` produces zoned ISO text (`timeZone` required for non-`ZonedDateTime` input). `formatRelative()` uses fixed units for short spans and complete calendar months or years in the requested or inferred timezone. Zoned inputs with different zones require `options.timeZone`. `formatDuration()` falls back to English when `Intl.DurationFormat` is unavailable.\n\n### `formatParts()` / `formatRange()` / `formatRangeParts()`\n\n```ts\nformatParts(input: TimeInput, options?: FormatOptions): Intl.DateTimeFormatPart[];\nformatRange(start: TimeInput, end: TimeInput, options?: FormatOptions): string;\nformatRangeParts(\n start: TimeInput,\n end: TimeInput,\n options?: FormatOptions,\n): ReturnType<Intl.DateTimeFormat['formatRangeToParts']>;\n```\n\nReturn `Intl` parts or localized range strings using `FormatOptions`.\n\n### `parseDuration()` / `humanize()`\n\n```ts\nparseDuration(input: string | Temporal.DurationLike): Temporal.Duration;\nhumanize(diff: TimeDiffResult, options?: { locale?: Intl.LocalesArgument }): string;\n```\n\n`humanize()` localizes numbers only. Unit names remain English.\n\n## Classification\n\n### `classifyExpiry({ value, thresholds, relativeTo?, timeZone? })`\n\n```ts\nclassifyExpiry<K extends string>(input: ClassifyExpiryInput<K>): K | null;\n```\n\nClassifies an expiry against fixed elapsed-time thresholds in milliseconds or larger units. Months and years throw `TempoInvalidInputError`.\n\n### `timeDiff(a, b?, options?)`\n\n```ts\ntimeDiff(a: TimeInput, b?: TimeInput, options?: TimeZoneOptions): TimeDiffResult;\n```\n\nReturns absolute calendar difference in its largest meaningful unit. Uses Temporal's calendar-aware `.since()` for correct month and year handling.\n\n## Types\n\n```ts\ntype AbsoluteTime = Temporal.Instant | Temporal.ZonedDateTime;\ntype WallTime = Temporal.PlainDate | Temporal.PlainDateTime;\ntype TimeInput = AbsoluteTime | WallTime;\ntype RelativeTimeInput = AbsoluteTime;\ntype ParseAs = 'instant' | 'plainDate' | 'plainDateTime' | 'zonedDateTime';\ntype Disambiguation = 'compatible' | 'earlier' | 'later' | 'reject';\ntype CalendarUnit = 'day' | 'month' | 'week' | 'year';\ntype BoundaryUnit = 'day' | 'hour' | 'minute' | 'month' | 'week' | 'year';\ntype WeekStartDay = 1 | 2 | 3 | 4 | 5 | 6 | 7;\ntype FormatPattern = 'date-only' | 'long' | 'medium' | 'short' | 'time-only';\ntype TempoUnit = 'day' | 'hour' | 'microsecond' | 'millisecond' | 'minute' | 'month' | 'nanosecond' | 'second' | 'week' | 'year';\ntype FixedDuration = Pick<Temporal.DurationLike, 'days' | 'hours' | 'microseconds' | 'milliseconds' | 'minutes' | 'nanoseconds' | 'seconds' | 'weeks'>;\ntype ExpiryThresholds<K extends string> = Record<K, FixedDuration>;\ntype TimeDiffUnit = Exclude<TempoUnit, 'microsecond' | 'nanosecond'>;\ntype TimeDiffResult = { unit: TimeDiffUnit; value: number };\n\ninterface TimeZoneOptions { timeZone?: string }\ninterface DisambiguationOptions { disambiguation?: Disambiguation }\ninterface ShiftOptions extends DisambiguationOptions, TimeZoneOptions {}\ninterface DifferenceInput extends DisambiguationOptions, TimeZoneOptions {\n end: TimeInput;\n largestUnit?: Temporal.DateTimeUnit;\n roundingIncrement?: number;\n roundingMode?: Temporal.RoundingMode;\n smallestUnit?: Temporal.DateTimeUnit;\n start: TimeInput;\n}\ninterface BoundaryOptions extends TimeZoneOptions { weekStartsOn?: WeekStartDay }\ninterface CompareOptions extends TimeZoneOptions { unit?: BoundaryUnit; weekStartsOn?: WeekStartDay }\ninterface ContainsInput extends CompareOptions { end: TimeInput; start: TimeInput; value: TimeInput }\ninterface ClampInput extends CompareOptions { end: TimeInput; start: TimeInput; value: TimeInput }\ntype FormatOptions =\n | { intl: Intl.DateTimeFormatOptions; locale?: Intl.LocalesArgument; pattern?: never; timeZone?: string }\n | { intl?: never; locale?: Intl.LocalesArgument; pattern?: FormatPattern; timeZone?: string };\ninterface RelativeFormatOptions extends TimeZoneOptions {\n base?: RelativeTimeInput;\n locale?: Intl.LocalesArgument;\n numeric?: Intl.RelativeTimeFormatNumeric;\n style?: Intl.RelativeTimeFormatStyle;\n}\ninterface DurationFormatOptions {\n locale?: Intl.LocalesArgument;\n style?: 'digital' | 'long' | 'narrow' | 'short';\n}\ninterface ClassifyExpiryInput<K extends string> extends TimeZoneOptions {\n value: TimeInput;\n thresholds: ExpiryThresholds<K>;\n relativeTo?: Temporal.Instant;\n}\ntype RecurrenceRule = { frequency: 'daily' | 'monthly' | 'weekly' | 'yearly'; interval?: number } &\n ({ count: number; until?: TimeInput } | { count?: number; until: TimeInput });\n```\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `TempoError` | Base Tempo error | `instanceof TempoError` narrows every subtype |\n| `TempoInvalidInputError` | Invalid parse, duration, or fixed-threshold input | Extends `TempoError` |\n| `TempoInvalidTzError` | Invalid IANA zone or offset | Extends `TempoError` |\n| `TempoMissingTzError` | Wall time without required `timeZone` | Extends `TempoError` |\n| `TempoUnsupportedInputError` | Non-Temporal input passed to conversion | Extends `TempoError` |\n\nEvery Tempo error thrown from `parse()`, `parseDuration()`, and timezone validation carries the original Temporal error on its `cause` property (ES2022). Inspect `err.cause` for root-cause details when debugging.\n",
|
|
6
|
+
"usage": "---\ntitle: Tempo — Usage Guide\ndescription: Parse explicit Temporal values, resolve wall-clock time, perform timezone-aware arithmetic, and format dates with Tempo.\n---\n\n[[toc]]\n\n## Basic Usage\n\nParse ISO input with a declared target. Convert plain values with `timeZone` before treating them as an instant.\n\n```ts\nimport { format, parse, shift } from '@vielzeug/tempo';\n\nconst local = parse('2026-03-21T10:15:30', { as: 'plainDateTime' });\nconst reminder = shift(local, { minutes: -15 }, { timeZone: 'America/New_York' });\n\nformat(reminder, { locale: 'en-US', pattern: 'short' });\n```\n\n## Parse ISO Values\n\nChoose the value your boundary actually represents. Tempo does not auto-detect ISO strings.\n\n```ts\nimport { parse } from '@vielzeug/tempo';\n\nconst occurredAt = parse('2026-03-21T10:15:30Z', { as: 'instant' });\nconst meeting = parse('2026-03-21T10:15:30+01:00[Europe/Berlin]', { as: 'zonedDateTime' });\nconst localStart = parse('2026-03-21T10:15:30', { as: 'plainDateTime' });\nconst birthday = parse('2026-03-21', { as: 'plainDate' });\n```\n\n## Convert Timezones\n\nUse `inTimeZone()` to project an absolute value. Use `toInstant()` only when resolving a wall-clock value.\n\n```ts\nimport { inTimeZone, parse, toInstant } from '@vielzeug/tempo';\n\nconst local = parse('2026-11-01T01:30:00', { as: 'plainDateTime' });\nconst firstOccurrence = toInstant(local, {\n disambiguation: 'earlier',\n timeZone: 'America/New_York',\n});\n\nconst berlin = inTimeZone(firstOccurrence, 'Europe/Berlin');\n```\n\n## Calculate and Compare\n\nUse Tempo helpers when an operation must resolve timezones, normalize ranges, or compare calendar units. Use Temporal methods directly when values are already the exact kind and timezone you need.\n\n```ts\nimport { clamp, contains, difference, parse, shift } from '@vielzeug/tempo';\n\nconst start = parse('2026-03-21T10:00:00Z', { as: 'instant' });\nconst end = parse('2026-03-21T12:00:00Z', { as: 'instant' });\nconst value = parse('2026-03-21T13:00:00Z', { as: 'instant' });\n\nconst duration = difference({ end, largestUnit: 'hour', start });\nconst isScheduled = contains({ end, start, value });\nconst bounded = clamp({ end, start, value });\n\nconst local = parse('2026-03-08T01:30:00', { as: 'plainDateTime' });\nconst tomorrow = shift(local, { days: 1 }, { timeZone: 'America/New_York' });\n```\n\n## Classify Expiry\n\nUse fixed elapsed-time thresholds in milliseconds or larger units. Handle `null` as the unclassified state instead of adding a far-future catch-all.\n\n```ts\nimport { classifyExpiry, parse } from '@vielzeug/tempo';\n\nconst status = classifyExpiry({\n relativeTo: parse('2026-06-01T00:00:00Z', { as: 'instant' }),\n thresholds: {\n expired: { days: 0 },\n critical: { days: 3 },\n warning: { days: 14 },\n },\n value: parse('2026-06-04T00:00:00Z', { as: 'instant' }),\n});\n\nconst label = status ?? 'safe';\n```\n\n## Format Values\n\nUse `format()` for UI, `formatInstant()` for transport, and `formatZoned()` for a zoned ISO string.\n\n```ts\nimport { format, formatInstant, formatRelative, formatZoned, parse } from '@vielzeug/tempo';\n\nconst instant = parse('2026-03-21T10:15:30Z', { as: 'instant' });\n\nformat(instant, { locale: 'en-GB', pattern: 'short', timeZone: 'UTC' });\nformatInstant(instant);\nformatZoned(instant, { timeZone: 'Europe/Berlin' });\nformatRelative(instant, { base: parse('2026-03-21T09:15:30Z', { as: 'instant' }) });\n```\n\n`formatRelative()` uses fixed elapsed-time units for short spans and complete calendar months or years for longer spans. Pass `timeZone` when calendar-relative output must use a specific regional calendar boundary.\n\n## Generate Calendar Sequences\n\nUse zoned values when the sequence already has a timezone. Plain and instant starts require `timeZone`.\n\n```ts\nimport { dateRange, parse, recurrence } from '@vielzeug/tempo';\n\nconst start = parse('2026-03-01T09:00:00[America/New_York]', { as: 'zonedDateTime' });\nconst end = parse('2026-03-03T09:00:00[America/New_York]', { as: 'zonedDateTime' });\n\nconst days = [...dateRange(start, end, { days: 1 })];\nconst meetings = [...recurrence(start, { count: 4, frequency: 'weekly', interval: 2 })];\n```\n\nBoth functions advance with zoned calendar arithmetic. Non-advancing range steps, invalid recurrence intervals, and negative or fractional counts throw `TempoInvalidInputError`.\n\n## Testing\n\nPin the reference instant for deterministic expiry tests.\n\n```ts\nimport { classifyExpiry, parse } from '@vielzeug/tempo';\n\nconst relativeTo = parse('2026-06-01T00:00:00Z', { as: 'instant' });\nconst value = parse('2026-05-31T00:00:00Z', { as: 'instant' });\n\nclassifyExpiry({ relativeTo, thresholds: { expired: { days: 0 } }, value });\n```\n\n## Framework Integration\n\nPass ISO strings through component props. Parse and format at the rendering boundary.\n\n::: code-group\n\n```tsx [React]\nimport { format, parse } from '@vielzeug/tempo';\n\nconst label = format(parse(iso, { as: 'instant' }), { locale: 'en-US', pattern: 'medium', timeZone: 'UTC' });\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport { format, parse } from '@vielzeug/tempo';\n\nconst props = defineProps<{ iso: string }>();\nconst label = format(parse(props.iso, { as: 'instant' }), { locale: 'en-US', pattern: 'medium', timeZone: 'UTC' });\n</script>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { format, parse } from '@vielzeug/tempo';\n export let iso: string;\n $: label = format(parse(iso, { as: 'instant' }), { locale: 'en-US', pattern: 'medium', timeZone: 'UTC' });\n</script>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### With Rune\n\nWrite stable UTC timestamps to structured logs.\n\n```ts\nimport { formatInstant, nowInstant } from '@vielzeug/tempo';\n\nlogger.info({ timestamp: formatInstant(nowInstant()) }, 'server started');\n```\n\n### With Vault\n\nCalculate an explicit instant before storing an expiring record.\n\n```ts\nimport { now } from '@vielzeug/tempo';\n\nconst expiresAt = now({ timeZone: 'UTC' }).add({ minutes: 30 }).toInstant();\n```\n\n## Best Practices\n\n- Parse strings with their actual temporal meaning and pass `timeZone` when resolving plain values.\n- Use `disambiguation` for DST overlap and gap handling.\n- Use Tempo helpers for timezone resolution, normalized ranges, and calendar units; use Temporal directly for already-normalized values.\n- Use fixed duration units for expiry thresholds.\n- Reject non-advancing range and recurrence configuration.\n- Store instants for transport and database values.\n- Use `formatInstant()` for machine output and `format()` for user-facing text.\n- Pass `timeZone` to `formatRelative()` when month and year boundaries must use a specific region.\n",
|
|
7
|
+
"examples": "---\ntitle: Tempo — Examples\ndescription: Practical examples and recipes for tempo.\n---\n\n## Examples\n\n- [DST-Safe Arithmetic](./examples/dst-safe-arithmetic.md)\n- [Date Ranges and Recurrence](./examples/date-ranges-and-recurrence.md)\n- [Locale Formatting](./examples/locale-formatting.md)\n- [Timezone Conversion](./examples/timezone-conversion.md)\n- [Expiry Classification](./examples/expiry-classification.md)\n"
|
|
8
|
+
},
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "meeting-duration",
|
|
12
|
+
"code": "import { classifyExpiry, contains, difference, format, parse, shift, toInstant } from '@vielzeug/tempo'\n\nconst local = parse('2026-03-21T10:00:00', { as: 'plainDateTime' })\nconst start = toInstant(local, { timeZone: 'America/New_York' })\nconst end = shift(start, { hours: 2 }, { timeZone: 'America/New_York' })\nconst check = parse('2026-03-21T15:00:00Z', { as: 'instant' })\n\nconsole.log('Duration:', difference({ end, largestUnit: 'hour', start }).toString())\nconsole.log('Contains check:', contains({ end, start, value: check }))\nconsole.log('New York:', format(end, { locale: 'en-US', pattern: 'short' }))\nconsole.log('Expiry:', classifyExpiry({ thresholds: { soon: { days: 3 } }, value: end }))",
|
|
13
|
+
"name": "Timezone-Aware Arithmetic and Ranges"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"typeSignatures": {
|
|
17
|
+
"Temporal": "export { Temporal } from '@js-temporal/polyfill';",
|
|
18
|
+
"inTimeZone": "export { inTimeZone, toInstant } from './_convert';",
|
|
19
|
+
"toInstant": "export { inTimeZone, toInstant } from './_convert';",
|
|
20
|
+
"endOf": "export { endOf, startOf } from './boundary';",
|
|
21
|
+
"startOf": "export { endOf, startOf } from './boundary';",
|
|
22
|
+
"classifyExpiry": "export { classifyExpiry, timeDiff } from './classify';",
|
|
23
|
+
"timeDiff": "export { classifyExpiry, timeDiff } from './classify';",
|
|
24
|
+
"clamp": "export { clamp, contains, isAfter, isBefore, isSame } from './compare';",
|
|
25
|
+
"contains": "export { clamp, contains, isAfter, isBefore, isSame } from './compare';",
|
|
26
|
+
"isAfter": "export { clamp, contains, isAfter, isBefore, isSame } from './compare';",
|
|
27
|
+
"isBefore": "export { clamp, contains, isAfter, isBefore, isSame } from './compare';",
|
|
28
|
+
"isSame": "export { clamp, contains, isAfter, isBefore, isSame } from './compare';",
|
|
29
|
+
"difference": "export { difference, isValid, now, nowInstant, parse, shift } from './core';",
|
|
30
|
+
"isValid": "export { difference, isValid, now, nowInstant, parse, shift } from './core';",
|
|
31
|
+
"now": "export { difference, isValid, now, nowInstant, parse, shift } from './core';",
|
|
32
|
+
"nowInstant": "export { difference, isValid, now, nowInstant, parse, shift } from './core';",
|
|
33
|
+
"parse": "export { difference, isValid, now, nowInstant, parse, shift } from './core';",
|
|
34
|
+
"shift": "export { difference, isValid, now, nowInstant, parse, shift } from './core';",
|
|
35
|
+
"TempoError": "export {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';",
|
|
36
|
+
"TempoInvalidInputError": "export {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';",
|
|
37
|
+
"TempoInvalidTzError": "export {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';",
|
|
38
|
+
"TempoMissingTzError": "export {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';",
|
|
39
|
+
"TempoUnsupportedInputError": "export {\n TempoError,\n TempoInvalidInputError,\n TempoInvalidTzError,\n TempoMissingTzError,\n TempoUnsupportedInputError,\n} from './errors';",
|
|
40
|
+
"format": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
41
|
+
"formatDuration": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
42
|
+
"formatInstant": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
43
|
+
"formatParts": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
44
|
+
"formatRange": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
45
|
+
"formatRangeParts": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
46
|
+
"formatRelative": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
47
|
+
"formatZoned": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
48
|
+
"humanize": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
49
|
+
"parseDuration": "export {\n format,\n formatDuration,\n formatInstant,\n formatParts,\n formatRange,\n formatRangeParts,\n formatRelative,\n formatZoned,\n humanize,\n parseDuration,\n} from './format';",
|
|
50
|
+
"dateRange": "export { dateRange, recurrence } from './range';",
|
|
51
|
+
"recurrence": "export { dateRange, recurrence } from './range';",
|
|
52
|
+
"AbsoluteTime": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
53
|
+
"BoundaryOptions": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
54
|
+
"BoundaryUnit": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
55
|
+
"CalendarUnit": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
56
|
+
"ClampInput": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
57
|
+
"ClassifyExpiryInput": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
58
|
+
"CompareOptions": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
59
|
+
"ContainsInput": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
60
|
+
"DifferenceInput": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
61
|
+
"Disambiguation": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
62
|
+
"DisambiguationOptions": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
63
|
+
"DurationFormatOptions": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
64
|
+
"ExpiryThresholds": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
65
|
+
"FixedDuration": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
66
|
+
"FormatOptions": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
67
|
+
"FormatPattern": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
68
|
+
"ParseAs": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
69
|
+
"RecurrenceRule": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
70
|
+
"RelativeFormatOptions": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
71
|
+
"RelativeTimeInput": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
72
|
+
"ShiftOptions": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
73
|
+
"TempoUnit": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
74
|
+
"TimeDiffResult": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
75
|
+
"TimeDiffUnit": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
76
|
+
"TimeInput": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
77
|
+
"TimeZoneOptions": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
78
|
+
"WallTime": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';",
|
|
79
|
+
"WeekStartDay": "export type {\n AbsoluteTime,\n BoundaryOptions,\n BoundaryUnit,\n CalendarUnit,\n ClampInput,\n ClassifyExpiryInput,\n CompareOptions,\n ContainsInput,\n DifferenceInput,\n Disambiguation,\n DisambiguationOptions,\n DurationFormatOptions,\n ExpiryThresholds,\n FixedDuration,\n FormatOptions,\n FormatPattern,\n ParseAs,\n RecurrenceRule,\n RelativeFormatOptions,\n RelativeTimeInput,\n ShiftOptions,\n TempoUnit,\n TimeDiffResult,\n TimeDiffUnit,\n TimeInput,\n TimeZoneOptions,\n WallTime,\n WeekStartDay,\n} from './types';"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "export {\n count,\n deleteMany,\n getMany,\n has,\n isEmpty,\n keys,\n update,\n upsert,\n} from './adapter-core';\nexport { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';\nexport type { QueryBuilder } from './query';\nexport { isExpired, ttl } from './ttl';\nexport type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';\nexport { table, validatorCodec } from './types';\n",
|
|
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, isExpired, validatorCodec, 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 `KeyValueVaultStore` for portable CRUD and observation; choose IndexedDB or the opt-in SQLite subpath when you need atomic transactions or lazy iteration. Durable adapters require codecs so invalid stored data never enters typed code.\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 key-value store 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 + SQLite capability | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Required durable codecs | <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| 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 honest storage capabilities and required durable codecs.\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 key-value store with codecs, and dispose it with its owner.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\nconst PreferenceSchema = s.object({ id: s.string(), theme: s.union('dark', 'light') });\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<{ id: string; theme: 'dark' | 'light' }>('id') },\n codecs: { preferences: PreferenceSchema },\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- Spell and other parser schemas work directly as codecs; `validatorCodec()` creates an explicit identity-encoding codec.\n- `/memory` returns a portable `KeyValueVaultStore` — codecs optional (in-memory, no trust boundary).\n- `/local-storage` and `/session-storage` return `KeyValueVaultStore` — codecs required.\n- `observe()` emits current and changed table snapshots.\n- `ttl` creates validated expiration durations.\n- `/indexeddb` and `/sqlite` return `DocumentVaultStore` with `batch()` and `iterate()` — codecs required.\n- `/indexeddb` also exports `defineMigration()` for schema upgrades.\n- `pruneExpired()` removes stale TTL entries on demand.\n- Common helpers are bound to stores and transaction contexts; standalone forms remain exported.\n- `query()` provides typed filtering, ordering, pagination, counting, first-match, and deletion.\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, codecs, 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 key-value store | Async API | Import from `/memory`; codecs optional |\n| `createLocalStorage()` / `createSessionStorage()` | Web Storage-backed key-value stores | Async API | Codecs required; available only where the corresponding Web API exists |\n| `createIndexedDB()` | Browser document store with transactions and cursor iteration | Async API | Import from `/indexeddb`; codecs required |\n| `createSQLite()` | Driver-neutral SQLite document store | Async API over a synchronous driver | Import from `/sqlite`; codecs required |\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| `validatorCodec()` | Create an identity-encoding codec from a parser | Sync | Spell schemas can also be passed directly |\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, codecs, derived helpers, 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, Codecs, 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### `validatorCodec()`\n\n```ts\nfunction validatorCodec<T>(validator: { parse(value: unknown): T }): RecordCodec<T>;\n```\n\nWraps any object with a `parse(value): T` method as an identity-encoding `RecordCodec`. Parser schemas can also be passed directly in `codecs`; a Spell schema requires no adapter.\n\n**Returns:** A `RecordCodec<T>`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst UserSchema = s.object({ id: s.number(), name: s.string() });\nconst store = createLocalStorage({\n name: 'app',\n schema: { users: table<{ id: number; name: string }>('id') },\n codecs: { users: UserSchema },\n});\n```\n\nCustom `RecordCodec` values validate writes by round-tripping `decode(encode(value))`. IndexedDB codecs must preserve declared indexed field names and values in their encoded object.\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\nDurable factories (LocalStorage, SessionStorage, IndexedDB, SQLite) require `codecs`. Memory may omit them.\n\n### `createMemory()`\n\n```ts\nfunction createMemory<S extends AnySchema>(options: MemoryStoreOptions<S>): KeyValueVaultStore<S>;\n```\n\nCreates an in-memory key-value store. Codecs are optional because memory values never cross a trust boundary.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables created by `table()` |\n| `codecs` | Optional per-table codecs |\n\n**Returns:** `KeyValueVaultStore<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: DurableStoreOptions<S> & {\n name: string;\n onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';\n}): KeyValueVaultStore<S>;\n```\n\nCreates a namespaced `localStorage` store. Codecs are required.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables created by `table()` |\n| `codecs` | Required per-table codecs |\n| `name` | Required storage namespace |\n| `onQuotaExceeded` | Handles a Web Storage quota error; returning `'ignore'` drops that write |\n\n**Returns:** `KeyValueVaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\nconst store = createLocalStorage({\n name: 'app',\n schema: { settings: table<{ id: string }>('id') },\n codecs: { settings: SettingsSchema },\n});\n```\n\n---\n\n### `createSessionStorage()`\n\n```ts\nfunction createSessionStorage<S extends AnySchema>(options: DurableStoreOptions<S> & {\n name: string;\n onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';\n}): KeyValueVaultStore<S>;\n```\n\nCreates a namespaced `sessionStorage` store. Its options and return type match `createLocalStorage()`.\n\n**Returns:** `KeyValueVaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createSessionStorage } from '@vielzeug/vault/session-storage';\n\nconst store = createSessionStorage({\n name: 'checkout',\n schema: { cart: table<{ id: string }>('id') },\n codecs: { cart: CartSchema },\n});\n```\n\n---\n\n### `createIndexedDB()`\n\n```ts\nfunction createIndexedDB<S extends AnySchema>(options: DurableStoreOptions<S> & {\n migrate?: MigrationFn;\n name: string;\n version?: number;\n}): IndexedDbVaultStore<S>;\n```\n\nCreates an IndexedDB document store with atomic batches, lazy cursor iteration, and optional schema migrations. Codecs are required.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables and IndexedDB secondary indexes |\n| `codecs` | Required per-table codecs |\n| `name` | Required database name |\n| `version` | Positive schema version; defaults to `1` |\n| `migrate` | Synchronous upgrade callback for version changes |\n\n**Returns:** `IndexedDbVaultStore<S>`. Its `getAllByIndex(table, field, value)` method queries a declared IndexedDB index without scanning the full table.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\n\nconst store = createIndexedDB({\n name: 'app',\n schema: { users: table<{ id: number; role: string }>('id', { indexes: ['role'] }) },\n codecs: { users: UserSchema },\n});\n\nconst admins = await store.getAllByIndex('users', 'role', 'admin');\n```\n\n---\n\n### `createSQLite()`\n\n```ts\nfunction createSQLite<S extends AnySchema>(options: SQLiteVaultOptions<S>): DocumentVaultStore<S>;\n```\n\nCreates a namespaced SQLite document store with atomic batches and keyset-paginated iteration. Codecs are required. 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| `codecs` | Required per-table codecs |\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:** `DocumentVaultStore<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 codecs: { users: UserSchema },\n});\n```\n\nNode `DatabaseSync`, Bun `Database`, and Deno `jsr:@db/sqlite` `Database` satisfy the protocol. Codec output may be any JSON-compatible value. During a `batch()` callback, calls on every Vault store sharing that connection reject; use `tx.*` instead.\n\n## Store Capabilities\n\n### `KeyValueVaultStore`\n\n```ts\ninterface KeyValueVaultStore<S extends AnySchema> {\n clear(table): Promise<void>;\n count(table): Promise<number>;\n delete(table, key): Promise<boolean>;\n deleteMany(table, keys): Promise<number>;\n get(table, key): Promise<Record | undefined>;\n getAll(table): Promise<Record[]>;\n getMany(table, keys): Promise<(Record | undefined)[]>;\n has(table, key): Promise<boolean>;\n isEmpty(table): Promise<boolean>;\n keys(table, filter?): Promise<VaultKey[]>;\n query(table): QueryBuilder<Record>;\n update(table, key, changes, ttl?): Promise<Record | undefined>;\n upsert(table, key, update, ttl?): Promise<Record>;\n observe(table, listener, options?): Unsubscribe;\n pruneExpired(): Promise<Record<string, number>>;\n put<K extends keyof S & string>(table: K, value: RecordOf<S, K>, ttl?: number): Promise<RecordOf<S, K>>;\n putAll(table, values, ttl?): Promise<void>;\n readonly disposalSignal: AbortSignal;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n [Symbol.asyncDispose](): Promise<void>;\n}\n```\n\nThe portable key-value store API is returned by Memory and Web Storage factories. `put()`, `update()`, and `upsert()` return the canonical value produced by codec validation. `observe()` emits the current table snapshot by default and then emits after mutations.\n\n---\n\n### `DocumentVaultStore`\n\n```ts\ninterface DocumentVaultStore<S extends AnySchema> extends KeyValueVaultStore<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## Bound and Standalone Helpers\n\nConvenience operations are bound to every store and transaction context. Standalone forms remain exported from `@vielzeug/vault` for functional composition. `store.query(table)` provides fluent in-memory filtering, ordering, pagination, counting, first-match, and deletion.\n\n| Helper | Signature | Description |\n| --- | --- | --- |\n| `has()` | `(store, table, key) => Promise<boolean>` | Check whether a record exists |\n| `count()` | `(store, table) => Promise<number>` | Count live records |\n| `isEmpty()` | `(store, table) => Promise<boolean>` | Check whether a table has no live records |\n| `getMany()` | `(store, table, keys) => Promise<(T \\| undefined)[]>` | Fetch multiple records by key |\n| `keys()` | `(store, table, filter?) => Promise<VaultKey[]>` | Fetch live primary keys, optionally filtered by record |\n| `deleteMany()` | `(store, table, keys) => Promise<number>` | Delete multiple records, return count |\n| `update()` | `(store, table, key, changes, ttl?) => Promise<T \\| undefined>` | Partially update an existing record |\n| `upsert()` | `(store, table, key, fn, ttl?) => Promise<T>` | Read-modify-write with callback |\n\n```ts\nconst exists = await store.has('users', 1);\nconst total = await store.count('users');\nconst [a, b] = await store.getMany('users', [1, 2]);\nconst updated = await store.update('users', 1, { name: 'Alice' });\nconst result = await store.upsert('users', 99, (existing) => ({ id: 99, name: existing?.name ?? 'Guest' }));\n\nconst active = await store.query('users').equals('status', 'active').orderBy('name').limit(20).toArray();\nconst activeCount = await store.query('users').equals('status', 'active').count();\n\n// Standalone form when functional composition is preferable:\nconst empty = await isEmpty(store, 'users');\n```\n\nBound `update()` and `upsert()` are atomic on document stores. Their key-value-store forms are non-atomic convenience operations.\n\n### `QueryBuilder`\n\n```ts\ninterface QueryBuilder<T extends object> {\n equals<K extends keyof T & string>(field: K, value: T[K]): QueryBuilder<T>;\n filter(predicate: (value: T, index: number, array: readonly T[]) => boolean): QueryBuilder<T>;\n orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): QueryBuilder<T>;\n offset(count: number): QueryBuilder<T>;\n limit(count: number): QueryBuilder<T>;\n count(): Promise<number>;\n first(): Promise<T | undefined>;\n delete(): Promise<number>;\n toArray(): Promise<T[]>;\n}\n```\n\nQueries materialize through `getAll()`. `count()` ignores ordering, offset, and limit. Document-store query deletion and bound bulk mutations execute atomically.\n\n## Migrations\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 defaultTtl?: number;\n indexes?: readonly (keyof T & string)[];\n key: Key;\n};\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 RecordCodec<T> = {\n decode(value: unknown): T;\n encode(value: T): unknown;\n};\n\ntype RecordParser<T> = { parse(value: unknown): T };\ntype CodecInput<T> = RecordCodec<T> | RecordParser<T>;\n\ntype TableCodecs<S extends AnySchema> = {\n [K in keyof S]: CodecInput<RecordOf<S, K>>;\n};\n\ntype MemoryStoreOptions<S extends AnySchema> = {\n schema: S;\n codecs?: TableCodecs<S>;\n};\n\ntype DurableStoreOptions<S extends AnySchema> = {\n schema: S;\n codecs: TableCodecs<S>;\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> | null | 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> = DurableStoreOptions<S> & {\n closeOnDispose?: boolean;\n database: SQLiteDatabase;\n name: string;\n};\n```\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 delete<T extends K>(table: T, key: KeyOf<S, T>): Promise<boolean>;\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 iterate<T extends K>(table: T): AsyncIterable<RecordOf<S, T>>;\n put<T extends K>(table: T, value: RecordOf<S, T>, ttl?: number): Promise<RecordOf<S, T>>;\n putAll<T extends K>(table: T, values: RecordOf<S, T>[], ttl?: number): Promise<void>;\n}\n```\n\n`TransactionContext` has core CRUD, `iterate()`, and TTL methods, 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 codec 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 with required durable codecs.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a key-value store with a schema and codecs, then write a typed row.\n\n```ts\nimport { s } from '@vielzeug/spell';\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 PreferenceSchema = s.object({ id: s.string(), theme: s.union('dark', 'light') });\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n codecs: { preferences: PreferenceSchema },\n});\n\nawait store.put('preferences', { id: 'theme', theme: 'dark' });\nconsole.log(await store.get('preferences', 'theme'));\n```\n\n## Create a Key-Value Store\n\nMemory, LocalStorage, and SessionStorage return `KeyValueVaultStore`. They share portable string/number keys, bound helpers, fluent queries, TTL, and `observe()`. Memory may omit codecs; Web Storage requires them. Malformed storage envelopes are evicted lazily, while codec validation failures are surfaced without deleting persisted data.\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 codecs: { preferences: PreferenceSchema },\n});\n```\n\n## Read and Change Records\n\nUse bound `update()` and `upsert()` methods for read-modify-write operations; no helper imports are required.\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. Document-store methods run atomically; key-value-store methods are non-atomic conveniences when multiple writers race.\n\n## Iterate and Filter Records\n\nUse `query()` for typed in-memory filtering, sorting, pagination, counting, and deletion. Use `iterate()` on a `DocumentVaultStore` for lazy cursor-based traversal, and `getAllByIndex()` on IndexedDB for declared equality indexes.\n\n```ts\n// Bound fluent query\nconst themed = await store.query('preferences').filter((preference) => preference.id.startsWith('theme')).toArray();\n\n// Document store: iterate lazily\nfor await (const pref of db.iterate('preferences')) {\n if (pref.id.startsWith('theme')) console.log(pref);\n}\n```\n\nFor large tables, prefer `iterate()` on IndexedDB or SQLite instead of materializing every record with `getAll()`.\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; tables with removed rows publish fresh observer snapshots.\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. Notifications are revision-ordered, and explicit TTL pruning publishes fresh snapshots for affected tables. 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 { s } from '@vielzeug/spell';\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\n\nconst EventSchema = s.object({ id: s.number(), type: s.string() });\nconst db = createIndexedDB({\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n codecs: { events: EventSchema },\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\nTransaction contexts expose the same bound helpers and fluent queries; `tx.update()`, `tx.upsert()`, `tx.deleteMany()`, and query deletion remain atomic. Only await `tx.*` operations inside a batch callback. Do not retain the transaction context after the callback; later use throws `VaultScopeError`. Do not await timers, fetches, or other external asynchronous work because 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 { s } from '@vielzeug/spell';\nimport { table } from '@vielzeug/vault';\nimport { createSQLite } from '@vielzeug/vault/sqlite';\n\nconst EventSchema = s.object({ id: s.number(), type: s.string() });\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 codecs: { events: EventSchema },\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 any JSON-compatible codec output. Circular values, `bigint`, class instances, functions, and non-finite numbers must be transformed by the codec 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. Codecs must preserve each indexed field name and value in their encoded object. Use `migrate` only for IndexedDB version upgrades and mirror Vault's fixed `value.<field>` index path.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB, type MigrationFn } from '@vielzeug/vault/indexeddb';\n\nconst UserSchema = s.object({ id: s.number(), name: s.string() });\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({\n name: 'app-v2',\n migrate,\n schema,\n version: 2,\n codecs: { users: UserSchema },\n});\n```\n\n## Framework Integration\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\n\nimport type { AnySchema, KeyValueVaultStore, RecordOf } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: KeyValueVaultStore<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, KeyValueVaultStore, RecordOf } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: KeyValueVaultStore<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, KeyValueVaultStore, RecordOf } from '@vielzeug/vault';\n\nexport function tableStore<S extends AnySchema, K extends keyof S & string>(store: KeyValueVaultStore<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\nKeep 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- Provide codecs for every durable adapter; pass Spell or another parser schema directly when identity encoding is sufficient.\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
|
+
"examples": "---\ntitle: Vault — Examples\ndescription: Practical Vault examples for browser storage, SQLite, TTL, transactions, observation, and framework integration.\n---\n\n## Browser Key-Value Store with TTL\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { table, ttl } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\ninterface Session {\n id: string;\n token: string;\n}\n\nconst SessionSchema = s.object({ id: s.string(), token: s.string() });\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { sessions: table<Session>('id', { defaultTtl: ttl.hours(1) }) },\n codecs: { sessions: SessionSchema },\n});\n\nawait store.put('sessions', { id: 'current', token: 'FAKE-TOKEN-FOR-DOCS' });\nconsole.log(await store.get('sessions', 'current'));\n```\n\n## IndexedDB with Transactions and Iteration\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\n\ninterface Event {\n id: number;\n type: string;\n createdAt: number;\n}\n\nconst EventSchema = s.object({ createdAt: s.number(), id: s.number(), type: s.string() });\nconst db = createIndexedDB({\n name: 'events',\n schema: { events: table<Event>('id', { indexes: ['type'] }) },\n codecs: { events: EventSchema },\n});\n\nawait db.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened', createdAt: Date.now() });\n await tx.put('events', { id: 2, type: 'saved', createdAt: Date.now() });\n});\n\nfor await (const event of db.iterate('events')) {\n console.log(event.type);\n}\n```\n\n## SQLite Document Store\n\n```ts\nimport { DatabaseSync } from 'node:sqlite';\n\nimport { s } from '@vielzeug/spell';\nimport { table } from '@vielzeug/vault';\nimport { createSQLite } from '@vielzeug/vault/sqlite';\n\ninterface Job {\n id: string;\n status: 'queued' | 'running' | 'done';\n}\n\nconst JobSchema = s.object({ id: s.string(), status: s.union('queued', 'running', 'done') });\nconst database = new DatabaseSync(':memory:');\nconst store = createSQLite({\n database,\n name: 'app',\n schema: { jobs: table<Job>('id') },\n codecs: { jobs: JobSchema },\n});\n\nawait store.batch(['jobs'], async (tx) => {\n await tx.put('jobs', { id: 'job-1', status: 'queued' });\n await tx.put('jobs', { id: 'job-2', status: 'running' });\n});\n\nfor await (const job of store.iterate('jobs')) {\n console.log(job.id, job.status);\n}\n```\n\n## Observe Table Changes\n\n```ts\nconst stop = store.observe('sessions', (sessions) => {\n console.log(`Active sessions: ${sessions.length}`);\n});\n\nstore.disposalSignal.addEventListener('abort', stop);\n```\n\n## Bound Helpers and Queries\n\n```ts\nconsole.log(await store.has('sessions', 'current'));\nconsole.log(await store.count('sessions'));\nconsole.log(await store.isEmpty('sessions'));\n\nconst [a, b] = await store.getMany('sessions', ['current', 'old']);\nconsole.log(await store.keys('sessions'));\nconsole.log(await store.deleteMany('sessions', ['old', 'expired']));\n\nawait store.update('sessions', 'current', { token: 'FAKE-UPDATED-TOKEN' });\nawait store.upsert('sessions', 'temp', (existing) => ({\n id: 'temp',\n token: existing?.token ?? 'generated',\n}));\n\nconst current = await store.query('sessions').equals('id', 'current').first();\n(void a, b, current);\n```\n\n## React Hook\n\n```tsx\nimport { useEffect, useState } from 'react';\n\nimport type { AnySchema, KeyValueVaultStore, RecordOf } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(\n store: KeyValueVaultStore<S>,\n table: K,\n): RecordOf<S, 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## Migration Example\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB, defineMigration } from '@vielzeug/vault/indexeddb';\n\nconst UserSchema = s.object({ email: s.string().email(), id: s.number() });\nconst migrate = defineMigration([\n { field: 'email', table: 'users', type: 'addIndex' },\n]);\n\nconst db = createIndexedDB({\n name: 'app-v2',\n version: 2,\n migrate,\n schema: { users: table<{ id: number; email: string }>('id', { indexes: ['email'] }) },\n codecs: { users: UserSchema },\n});\n```\n\n## Recipe Examples\n\n- [CRUD Operations](./examples/crud.md)\n- [Filtering and Pagination](./examples/querying.md)\n- [TTL and Pruning](./examples/ttl.md)\n- [IndexedDB Batch Transactions](./examples/batch.md)\n- [IndexedDB Lazy Iteration](./examples/iterate.md)\n- [SQLite Transactions and Iteration](./examples/sqlite.md)\n- [Reactive Tables with observe()](./examples/reactive.md)\n- [Validators and Error Handling](./examples/plugins.md)\n"
|
|
8
|
+
},
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "basic-setup",
|
|
12
|
+
"code": "import { s } from '@vielzeug/spell'\nimport { table } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst UserSchema = s.object({ email: s.string().email(), id: s.number(), name: s.string() })\nconst schema = {\n users: table('id'),\n}\n\nconst db = createLocalStorage({\n name: 'demo',\n schema,\n codecs: { users: UserSchema },\n})\n\nawait db.put('users', { id: 1, name: 'Alice', email: 'alice@example.com' })\nawait db.put('users', { id: 2, name: 'Bob', email: 'bob@example.com' })\n\nconsole.log('Get user 1:', await db.get('users', 1))\nconsole.log('All users:', await db.getAll('users'))\nconsole.log('Count:', await db.count('users'))",
|
|
13
|
+
"name": "Basic Setup - Initialize Vault"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "bulk-operations",
|
|
17
|
+
"code": "import { s } from '@vielzeug/spell'\nimport { table } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst ItemSchema = s.object({ id: s.number(), value: s.number() })\nconst schema = {\n items: table('id'),\n}\n\nconst db = createLocalStorage({\n name: 'bulk-demo',\n schema,\n codecs: { items: ItemSchema },\n})\n\nconst items = Array.from({ length: 10 }, (_, index) => ({\n id: index + 1,\n value: +(Math.random() * 1000).toFixed(2),\n}))\n\nawait db.putAll('items', items)\nconsole.log('Inserted', items.length, 'items')\n\n// getMany — fetch multiple by key in one call (missing keys return undefined)\nconst [first, missing, third] = await db.getMany('items', [1, 99, 3])\nconsole.log('getMany [1, 99, 3]:', first?.id, missing, third?.id)\n\n// deleteMany — remove multiple by key, returns count deleted\nconst deleted = await db.deleteMany('items', [1, 2, 3, 99])\nconsole.log('deleteMany [1,2,3,99] deleted:', deleted) // 3 (99 did not exist)\n\n// filter-driven removal: getAll + filter + deleteMany\nconst remaining = await db.getAll('items')\nconst toDelete = remaining.filter((item) => item.id <= 6).map((item) => item.id)\nconst queryDeleted = await db.deleteMany('items', toDelete)\nconsole.log('Deleted items with id ≤ 6:', queryDeleted)\n\nconsole.log('Remaining count:', await db.count('items'))\nconsole.log('First remaining item:', (await db.getAll('items')).sort((a, b) => a.id - b.id)[0])",
|
|
18
|
+
"name": "Bulk Operations"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "cache-first",
|
|
22
|
+
"code": "import { s } from '@vielzeug/spell'\nimport { table, ttl } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst CacheSchema = s.object({ data: s.string(), fetchedAt: s.number(), id: s.string() })\nconst db = createLocalStorage({\n name: 'cache-demo',\n schema: { cache: table('id') },\n codecs: { cache: CacheSchema },\n})\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
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "crud-operations",
|
|
27
|
+
"code": "import { s } from '@vielzeug/spell'\nimport { table } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst UserSchema = s.object({ age: s.number(), email: s.string().email(), id: s.number(), name: s.string() })\nconst schema = {\n users: table('id'),\n}\n\nconst db = createLocalStorage({\n name: 'demo',\n schema,\n codecs: { users: UserSchema },\n})\n\nawait db.put('users', { id: 1, name: 'Alice', email: 'alice@example.com', age: 25 })\nawait db.put('users', { id: 2, name: 'Bob', email: 'bob@example.com', age: 30 })\nconsole.log('Created 2 users')\n\nconsole.log('Get user 1:', await db.get('users', 1))\nconsole.log('Count:', await db.count('users'))\nconsole.log('isEmpty before clear:', await db.isEmpty('users')) // false\n\nawait db.update('users', 1, { age: 26, name: 'Alice Smith' })\nconsole.log('Updated user 1:', await db.get('users', 1))\n\nconsole.log('Deleted user 2:', await db.delete('users', 2))\nconsole.log('Remaining users:', await db.getAll('users'))\n\nawait db.clear('users')\nconsole.log('isEmpty after clear:', await db.isEmpty('users')) // true",
|
|
28
|
+
"name": "CRUD Operations"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "filtering",
|
|
32
|
+
"code": "import { s } from '@vielzeug/spell'\nimport { table } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst ProductSchema = s.object({ category: s.string(), id: s.number(), inStock: s.boolean(), name: s.string(), price: s.number() })\nconst schema = {\n products: table('id'),\n}\n\nconst db = createLocalStorage({\n name: 'shop',\n schema,\n codecs: { products: ProductSchema },\n})\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 products = db.query('products')\nconst electronics = products\n .filter((product) => product.category === 'electronics' && product.inStock)\n .orderBy('price')\n\nconst pageSize = 2\nconst pageIndex = 0\nconst page = await electronics.offset(pageIndex * pageSize).limit(pageSize).toArray()\nconst total = await electronics.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 products.filter((product) => product.name.toLowerCase().startsWith('m')).toArray()\nconsole.log('Starts with m:', mice.map((p) => p.name))\n\nconst removed = await products.filter((product) => !product.inStock).delete()\nconsole.log('Removed out-of-stock:', removed)\n\nconst cheapest = await products.orderBy('price').first()\nconsole.log('Cheapest:', cheapest?.name, cheapest?.price)\n\nconsole.log('Remaining count:', await db.count('products'))",
|
|
33
|
+
"name": "Filtering — fluent query, pagination, and deletion"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "indexed-db",
|
|
37
|
+
"code": "import { s } from '@vielzeug/spell'\nimport { table, ttl } from '@vielzeug/vault'\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb'\n\nconst LogSchema = s.object({ id: s.number(), level: s.string(), message: s.string(), ts: s.number() })\nconst schema = {\n logs: table('id'),\n}\n\n// createIndexedDB returns DocumentVaultStore with transactions and cursor iteration\nconst db = createIndexedDB({\n name: 'app-logs',\n schema,\n codecs: { logs: LogSchema },\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.delete('logs', 1)\n await tx.delete('logs', 2)\n})\n\n// iterate() — cursor-based streaming, only on DocumentVaultStore\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.count('logs'))\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()",
|
|
38
|
+
"name": "IndexedDB — Atomic Batch & iterate()"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "prune-schedule",
|
|
42
|
+
"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: 'FAKE-TOKEN-ONE', user: 1 }, ttl.ms(1))\nawait db.put('sessions', { token: 'FAKE-TOKEN-TWO', 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()",
|
|
43
|
+
"name": "TTL — pruneExpired with disposalSignal"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "reactive-observe",
|
|
47
|
+
"code": "import { table } from '@vielzeug/vault'\nimport { createMemory } from '@vielzeug/vault/memory'\n\nconst db = createMemory({ schema: { users: table('id') } })\nconst snapshots = []\nconst stop = db.observe('users', (users) => snapshots.push(users.map((user) => user.name)))\n\nawait Promise.resolve()\nawait db.put('users', { id: 1, name: 'Ada' })\nawait Promise.resolve()\n\nconsole.log(snapshots) // [[], ['Ada']]\nstop()\nawait db.dispose()",
|
|
48
|
+
"name": "Reactive — observe()"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "ttl-expiration",
|
|
52
|
+
"code": "import { s } from '@vielzeug/spell'\nimport { table, ttl } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst CacheSchema = s.object({ data: s.string(), id: s.string() })\nconst schema = {\n cache: table('id'),\n}\n\nconst db = createLocalStorage({\n name: 'cache-demo',\n schema,\n codecs: { cache: CacheSchema },\n})\n\n// ttl helpers produce finite, positive millisecond durations\nawait db.put('cache', { id: 'short', data: 'Expires in 1 second' }, ttl.seconds(1))\nawait db.put('cache', { id: 'long', data: 'Expires in 5 minutes' }, ttl.minutes(5))\nconsole.log('Stored records with TTL')\nconsole.log('Immediate read:', await db.get('cache', 'short'))\n\nawait new Promise((resolve) => setTimeout(resolve, 1500))\nconsole.log('After 1.5s:', await db.get('cache', 'short')) // expired — undefined\nconsole.log('Long-lived still here:', await db.get('cache', 'long'))\n\nconsole.log('ttl helpers:', {\n '100ms': ttl.ms(100),\n '5 minutes': ttl.minutes(5),\n '2 hours': ttl.hours(2),\n '7 days': ttl.days(7),\n})",
|
|
53
|
+
"name": "TTL & Expiration"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"typeSignatures": {
|
|
57
|
+
"count": "export {\n count,\n deleteMany,\n getMany,\n has,\n isEmpty,\n keys,\n update,\n upsert,\n} from './adapter-core';",
|
|
58
|
+
"deleteMany": "export {\n count,\n deleteMany,\n getMany,\n has,\n isEmpty,\n keys,\n update,\n upsert,\n} from './adapter-core';",
|
|
59
|
+
"getMany": "export {\n count,\n deleteMany,\n getMany,\n has,\n isEmpty,\n keys,\n update,\n upsert,\n} from './adapter-core';",
|
|
60
|
+
"has": "export {\n count,\n deleteMany,\n getMany,\n has,\n isEmpty,\n keys,\n update,\n upsert,\n} from './adapter-core';",
|
|
61
|
+
"isEmpty": "export {\n count,\n deleteMany,\n getMany,\n has,\n isEmpty,\n keys,\n update,\n upsert,\n} from './adapter-core';",
|
|
62
|
+
"keys": "export {\n count,\n deleteMany,\n getMany,\n has,\n isEmpty,\n keys,\n update,\n upsert,\n} from './adapter-core';",
|
|
63
|
+
"update": "export {\n count,\n deleteMany,\n getMany,\n has,\n isEmpty,\n keys,\n update,\n upsert,\n} from './adapter-core';",
|
|
64
|
+
"upsert": "export {\n count,\n deleteMany,\n getMany,\n has,\n isEmpty,\n keys,\n update,\n upsert,\n} from './adapter-core';",
|
|
65
|
+
"VaultDisposedError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
|
|
66
|
+
"VaultError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
|
|
67
|
+
"VaultMigrationError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
|
|
68
|
+
"VaultQuotaError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
|
|
69
|
+
"VaultScopeError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
|
|
70
|
+
"QueryBuilder": "export type { QueryBuilder } from './query';",
|
|
71
|
+
"isExpired": "export { isExpired, ttl } from './ttl';",
|
|
72
|
+
"ttl": "export { isExpired, ttl } from './ttl';",
|
|
73
|
+
"AnySchema": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
74
|
+
"CodecInput": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
75
|
+
"DocumentVaultStore": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
76
|
+
"DurableStoreOptions": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
77
|
+
"KeyOf": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
78
|
+
"KeyValueVaultStore": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
79
|
+
"MemoryStoreOptions": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
80
|
+
"Observer": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
81
|
+
"RecordCodec": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
82
|
+
"RecordOf": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
83
|
+
"RecordParser": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
84
|
+
"SchemaEntry": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
85
|
+
"TableCodecs": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
86
|
+
"TransactionContext": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
87
|
+
"Unsubscribe": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
88
|
+
"VaultConveniences": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
89
|
+
"VaultKey": "export type {\n AnySchema,\n CodecInput,\n DocumentVaultStore,\n DurableStoreOptions,\n KeyOf,\n KeyValueVaultStore,\n MemoryStoreOptions,\n Observer,\n RecordCodec,\n RecordOf,\n RecordParser,\n SchemaEntry,\n TableCodecs,\n TransactionContext,\n Unsubscribe,\n VaultConveniences,\n VaultKey,\n} from './types';",
|
|
90
|
+
"table": "export { table, validatorCodec } from './types';",
|
|
91
|
+
"validatorCodec": "export { table, validatorCodec } from './types';"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apiSource": "export { ANONYMOUS, WILDCARD } from './constants';\nexport { WardConditionError, WardConfigError, WardError } from './errors';\nexport { createWard } from './factory';\nexport { matchesPattern, patternCovers } from './resource';\nexport { allow, deny, predicate } from './rules';\nexport type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';\n",
|
|
3
|
+
"docs": {
|
|
4
|
+
"index": "---\ntitle: Ward — Ordered authorization rules\ndescription: Zero-dependency authorization decisions with ordered rules, role helpers, ownership predicates, and default deny.\npackage: ward\ncategory: security\nkeywords: [authorization, rbac, permissions, policy, roles, predicates]\nexports: [createWard, allow, deny, predicate, ANONYMOUS, WILDCARD, matchesPattern, patternCovers]\nrelated: [herald, postmaster, refine]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ward\" />\n\n## Why Ward?\n\nWard evaluates immutable ordered rules. The first matching rule wins; no match means deny. Typed action, resource, and attribute contracts keep role and ownership policies explicit.\n\n## Quick Start\n\n```ts\nimport { allow, createWard, deny, predicate, WILDCARD } from '@vielzeug/ward';\n\nconst ward = createWard([\n deny('blocked', WILDCARD, [WILDCARD]),\n allow('editor', 'posts', ['read', 'update'], { when: predicate.owns('authorId') }),\n allow('viewer', 'posts', ['read']),\n]);\n\nconst decision = ward.decide({\n action: 'update',\n attributes: { authorId: 'u1' },\n principal: { id: 'u1', roles: ['editor'] },\n resource: 'posts',\n});\n```\n\n| Feature | Ward | Inline conditionals |\n| --- | --- | --- |\n| Ordered first-match policy | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual |\n| Default deny | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual |\n| Typed actions | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n| Ownership predicates | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n## Features\n\n<div class=\"features-grid\">\n\n- Ordered `allow` and `deny` decisions\n- Concise role/action rule factories\n- `predicate.owns()`, `and()`, `or()`, and `not()`\n- Exact, namespace, and wildcard action/resource patterns\n- Immutable rules, principals, and JSON-compatible attributes\n- Typed batch, bound-principal, and allowed-action checks\n- `tap()` decision observability\n- Default deny when no rule matches\n\n</div>\n\n## Installation\n\n```sh\npnpm add @vielzeug/ward\n```\n\n## Documentation\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n## See Also\n\n- [Postmaster](/postmaster/) — authorize durable job actions at application boundaries.\n- [Refine](/refine/) — consume decisions in accessible UI components.\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Ward — API Reference\ndescription: Ordered authorization rules, typed decisions, immutable policies, and observability.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createWard()` | Compile immutable ordered rules | Sync | First match wins |\n| `allow()` / `deny()` | Create role-conditioned rules | Sync | One rule is produced per action |\n| `predicate` | Compose typed synchronous conditions | Sync | Async results throw |\n| `matchesPattern()` | Match exact and wildcard values | Sync | `posts:*` does not match `posts` |\n| `patternCovers()` | Compare pattern coverage | Sync | It does not inspect conditions |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/ward` | All runtime APIs, errors, constants, and public types |\n\n## Core API\n\n### `createWard()`\n\n```ts\nfunction createWard<Action extends string, Resource extends string, Attributes extends WardAttributes>(\n rules?: readonly (WardRule<Action, Resource, Attributes> | readonly WardRule<Action, Resource, Attributes>[])[],\n): Ward<Action, Resource, Attributes>\n```\n\nReturns an immutable ordered policy. Nested rule arrays are flattened once. Rules, declarative attributes, and bound principals are snapshotted.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `rules` | `readonly (WardRule \\| readonly WardRule[])[]` | Rules in first-match order |\n\n**Returns:** `Ward<Action, Resource, Attributes>`.\n\n```ts\nimport { allow, createWard } from '@vielzeug/ward';\n\nconst ward = createWard<'read', 'posts'>([allow('viewer', 'posts', ['read'])]);\n```\n\n| Method | Returns | Behavior |\n| --- | --- | --- |\n| `decide(input)` | `WardDecision` | Evaluates and emits one decision event |\n| `checkAll(inputs)` | `WardDecision[]` | Evaluates each input in order |\n| `allowedActions(input)` | `Action[]` | Deduplicates and filters known actions without emitting events |\n| `forPrincipal(principal?)` | `BoundWard` | Binds an immutable principal snapshot |\n| `tap(handler, options?)` | `() => void` | Observes decisions; supports `AbortSignal` |\n| `rules` | `readonly WardRule[]` | Immutable compiled rules |\n\n---\n\n### `allow()` and `deny()`\n\n```ts\nallow<Action, Resource, Attributes>(role, resource, actions, options?): WardRule[]\ndeny<Action, Resource, Attributes>(role, resource, actions, options?): WardRule[]\n```\n\nReturn one role-conditioned rule per action.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `role` | `string \\| readonly string[]` | Role, role alternatives, `ANONYMOUS`, or `WILDCARD` |\n| `resource` | `WardPattern<Resource>` | Exact or wildcard resource |\n| `actions` | `readonly WardPattern<Action>[]` | Exact or wildcard actions |\n| `options.when` | `WardCondition<Attributes>` | Additional synchronous condition |\n\n**Returns:** `WardRule<Action, Resource, Attributes>[]`.\n\n```ts\nimport { allow, deny, WILDCARD } from '@vielzeug/ward';\n\nconst rules = [deny('blocked', WILDCARD, [WILDCARD]), allow(['editor', 'admin'], 'posts', ['read'])];\n```\n\n---\n\n### `predicate`\n\n```ts\npredicate.hasRole<Attributes>(role)\npredicate.owns<Attributes>(attribute)\npredicate.and<Attributes>(...conditions)\npredicate.or<Attributes>(...conditions)\npredicate.not<Attributes>(condition)\n```\n\nReturns typed `WardCondition` functions. `owns()` accepts only a string key from the selected attribute type.\n\n---\n\n### Pattern helpers\n\n```ts\nmatchesPattern(pattern: string, value: string): boolean\npatternCovers(broad: string, narrow: string): boolean\n```\n\n`matchesPattern()` supports exact values, `*`, and namespace wildcards such as `posts:*`. `patternCovers()` compares those pattern sets.\n\n## Types\n\n```ts\ntype WardAttributeValue =\n | boolean\n | number\n | string\n | null\n | readonly WardAttributeValue[]\n | { readonly [key: string]: WardAttributeValue };\n\ntype WardAttributes = Readonly<Record<string, WardAttributeValue>>;\ntype WardPattern<Value extends string> = Value | typeof WILDCARD | `${string}:*`;\n```\n\n```ts\ntype UserPrincipal = Readonly<{\n attributes?: WardAttributes;\n id: string;\n roles: readonly string[];\n}>;\n\ntype Principal = UserPrincipal | null;\n```\n\n```ts\ntype WardConditionInput<Attributes extends WardAttributes> = Readonly<{\n attributes?: Attributes;\n principal: Principal;\n}>;\n\ntype WardCondition<Attributes extends WardAttributes> = (\n input: WardConditionInput<Attributes>,\n) => boolean;\n```\n\n```ts\ntype WardRule<Action extends string, Resource extends string, Attributes extends WardAttributes> = Readonly<{\n action: WardPattern<Action>;\n attributes?: Attributes;\n condition?: WardCondition<Attributes>;\n effect: 'allow' | 'deny';\n resource: WardPattern<Resource>;\n}>;\n```\n\n```ts\ntype WardDecisionInput<Action extends string, Resource extends string, Attributes extends WardAttributes> = Readonly<{\n action: Action;\n attributes?: Attributes;\n principal?: Principal;\n resource: Resource;\n}>;\n```\n\n```ts\ntype WardDecision<Action extends string, Resource extends string, Attributes extends WardAttributes> =\n | { effect: 'allow' | 'deny'; matched: true; reason: string; rule: WardRule<Action, Resource, Attributes> }\n | { effect: 'deny'; matched: false; reason: string; rule?: never };\n```\n\n```ts\ntype WardEvent<Action extends string, Resource extends string, Attributes extends WardAttributes> = Readonly<{\n decision: WardDecision<Action, Resource, Attributes>;\n input: WardDecisionInput<Action, Resource, Attributes> & { principal: Principal };\n type: 'decision';\n}>;\n```\n\n`BoundWard`, `WardAllowedActionsInput`, `BoundWardAllowedActionsInput`, and `BoundWardDecisionInput` expose the corresponding principal-bound method contracts.\n\n## Errors\n\n| Error | Trigger | Notable properties |\n| --- | --- | --- |\n| `WardError` | Base for Ward-originated errors | Standard `cause` support |\n| `WardConfigError` | Invalid rules, principals, inputs, or attribute values | Configuration message with field path |\n| `WardConditionError` | Thrown, async, or non-boolean condition result | `ruleIndex`, `cause` |\n",
|
|
6
|
+
"usage": "---\ntitle: Ward — Usage Guide\ndescription: Define ordered role, wildcard, attribute, and ownership authorization rules.\n---\n\n[[toc]]\n\n## Basic Usage\n\n```ts\nimport { allow, createWard, deny, WILDCARD } from '@vielzeug/ward';\n\nconst ward = createWard<'read' | 'update', 'posts'>([\n deny('blocked', WILDCARD, [WILDCARD]),\n allow('editor', 'posts', ['read', 'update']),\n allow('viewer', 'posts', ['read']),\n]);\n\nconst decision = ward.decide({\n action: 'update',\n principal: { id: 'u1', roles: ['editor'] },\n resource: 'posts',\n});\n```\n\nOrder is policy. Put exceptions and explicit denies before broad allows. No match returns a default-deny decision with `matched: false`.\n\n## Check ownership with typed attributes\n\n```ts\nimport { allow, createWard, predicate } from '@vielzeug/ward';\n\ntype Attributes = { authorId: string; status: 'draft' | 'published' };\n\nconst ward = createWard<'update', 'posts', Attributes>([\n allow<'update', 'posts', Attributes>('editor', 'posts', ['update'], {\n when: predicate.owns<Attributes>('authorId'),\n }),\n]);\n\nward.decide({\n action: 'update',\n attributes: { authorId: post.authorId, status: post.status },\n principal,\n resource: 'posts',\n});\n```\n\nAttribute values support finite numbers, strings, booleans, `null`, arrays, and plain objects. Ward rejects class instances, dates, maps, sets, functions, `undefined`, non-finite numbers, and circular values.\n\n## Combine conditions\n\n```ts\nconst canPublishOwnPost = predicate.and<Attributes>(\n predicate.hasRole<Attributes>('editor'),\n predicate.owns<Attributes>('authorId'),\n ({ attributes }) => attributes?.status === 'draft',\n);\n```\n\nConditions are synchronous. A thrown error, Promise, or non-boolean result becomes a `WardConditionError` with the original value in `cause`.\n\n## Bind a principal\n\n```ts\nconst permissions = ward.forPrincipal(principal);\n\npermissions.decide({ action: 'update', attributes, resource: 'posts' });\npermissions.checkAll([\n { action: 'read', attributes, resource: 'posts' },\n { action: 'update', attributes, resource: 'posts' },\n]);\npermissions.allowedActions({ attributes, knownActions: ['read', 'update'], resource: 'posts' });\n```\n\n`forPrincipal()` snapshots the principal, roles, and principal attributes. Later caller mutations cannot alter bound decisions.\n\n## Observe decisions\n\n```ts\nconst controller = new AbortController();\n\nward.tap((event) => audit.write(event), { signal: controller.signal });\ncontroller.abort();\n```\n\n`tap()` observes `decide()` and `checkAll()`. It does not observe the hypothetical checks performed by `allowedActions()`. Handler failures are swallowed.\n\n## Anonymous and wildcard roles\n\n- `ANONYMOUS` matches `null` and an omitted principal.\n- `WILDCARD` as a role matches any authenticated principal.\n- `WILDCARD` as an action or resource matches every value.\n\n## Testing\n\nConstruct a fresh Ward for each policy test. Assert decisions and ordering through the public API; rules are immutable snapshots.\n\n## Framework Integration\n\nKeep one Ward instance near the application boundary. Bind the current principal when rendering several related controls, and repeat authorization at the mutation boundary.\n\n## Working with Other Vielzeug Libraries\n\nSend `WardEvent` values to a Rune logger or Herald bus when an application needs centralized diagnostics. Keep durable audit persistence in the application.\n\n## Best Practices\n\n- Put narrow exceptions before broad rules.\n- Keep default deny.\n- Prefer `allow()` and `deny()` for role policies.\n- Prefer declarative attributes before custom conditions.\n- Type action, resource, and attribute generics at shared policy boundaries.\n- Pass only authorization-relevant attributes.\n- Treat `reason` as diagnostics, not user-facing prose.\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
|
+
},
|
|
9
|
+
"examples": [
|
|
10
|
+
{
|
|
11
|
+
"id": "basic-rules",
|
|
12
|
+
"code": "import { WILDCARD, createWard } from '@vielzeug/ward'\n\n// Ordered first-match rules — the first matching rule wins; default deny if none match\nconst ward = createWard([\n { action: WILDCARD, resource: WILDCARD, effect: 'deny', condition: ({ principal }) => principal?.roles.includes('blocked') ?? false },\n { action: 'read', resource: 'posts', effect: 'allow' },\n { action: 'update', resource: 'posts', effect: 'allow', condition: ({ principal }) => principal?.roles.includes('editor') ?? false },\n])\n\nconst viewer = { id: 'u1', roles: ['viewer'] }\nconst editor = { id: 'u2', roles: ['editor'] }\nconst blocked = { id: 'u3', roles: ['blocked'] }\n\nconsole.log('viewer read: ', ward.decide({ action: 'read', principal: viewer, resource: 'posts' }).effect) // allow\nconsole.log('viewer update:', ward.decide({ action: 'update', principal: viewer, resource: 'posts' }).effect) // deny\nconsole.log('editor update:', ward.decide({ action: 'update', principal: editor, resource: 'posts' }).effect) // allow\nconsole.log('blocked read: ', ward.decide({ action: 'read', principal: blocked, resource: 'posts' }).effect) // deny",
|
|
13
|
+
"name": "Basic Rules"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "basic-setup",
|
|
17
|
+
"code": "import { createWard } from '@vielzeug/ward'\n\n// Simple allow/deny rules evaluated in order — first match wins\nconst ward = createWard([\n { action: 'read', resource: 'posts', effect: 'allow' },\n { action: 'write', resource: 'posts', effect: 'allow', condition: ({ principal }) => principal?.roles.includes('writer') ?? false },\n { action: 'delete', resource: 'posts', effect: 'deny' },\n])\n\nconst user = { id: 'u1', roles: ['writer'] }\n\nconsole.log('read: ', ward.decide({ action: 'read', principal: user, resource: 'posts' }).effect) // allow\nconsole.log('write: ', ward.decide({ action: 'write', principal: user, resource: 'posts' }).effect) // allow\nconsole.log('delete:', ward.decide({ action: 'delete', principal: user, resource: 'posts' }).effect) // deny",
|
|
18
|
+
"name": "Basic Setup"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "dynamic-permissions",
|
|
22
|
+
"code": "import { createWard } from '@vielzeug/ward'\n\n// Use condition callbacks for dynamic checks (e.g. ownership)\nconst ward = createWard([\n {\n action: 'update',\n resource: 'posts',\n effect: 'allow',\n condition: ({ principal, attributes }) => attributes?.authorId === principal?.id,\n },\n { action: 'update', resource: 'posts', effect: 'deny' },\n])\n\nconst author = { id: 'u1', roles: ['user'] }\nconst other = { id: 'u2', roles: ['user'] }\n\nconsole.log('author edits own post:',\n ward.decide({ action: 'update', principal: author, resource: 'posts', attributes: { authorId: 'u1' } }).effect) // allow\nconsole.log('other edits author post:',\n ward.decide({ action: 'update', principal: other, resource: 'posts', attributes: { authorId: 'u1' } }).effect) // deny",
|
|
23
|
+
"name": "Dynamic Permissions"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "multi-role-rules",
|
|
27
|
+
"code": "import { createWard } from '@vielzeug/ward'\n\n// Use condition callbacks for multi-role checks (OR semantics)\nconst ward = createWard([\n {\n action: 'read',\n resource: 'articles',\n effect: 'allow',\n condition: ({ principal }) => (principal?.roles.some(r => ['editor', 'reviewer', 'reader'].includes(r)) ?? false),\n },\n { action: 'read', resource: 'articles', effect: 'deny' },\n])\n\nconst editor = { id: 'u1', roles: ['editor'] }\nconst reviewer = { id: 'u2', roles: ['reviewer'] }\nconst outsider = { id: 'u3', roles: ['guest'] }\n\nconsole.log('editor: ', ward.decide({ action: 'read', principal: editor, resource: 'articles' }).effect) // allow\nconsole.log('reviewer:', ward.decide({ action: 'read', principal: reviewer, resource: 'articles' }).effect) // allow\nconsole.log('outsider:', ward.decide({ action: 'read', principal: outsider, resource: 'articles' }).effect) // deny",
|
|
28
|
+
"name": "Multi-Role Rules"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "permission-checks",
|
|
32
|
+
"code": "import { createWard } from '@vielzeug/ward'\n\nconst ward = createWard([\n { action: 'read', resource: 'articles', effect: 'allow' },\n { action: 'create', resource: 'articles', effect: 'allow', condition: ({ principal }) => principal?.roles.includes('editor') ?? false },\n { action: 'update', resource: 'articles', effect: 'allow', condition: ({ principal }) => principal?.roles.includes('editor') ?? false },\n { action: 'delete', resource: 'articles', effect: 'deny' },\n])\n\nconst editor = { id: 'u1', roles: ['editor'] }\nconst guest = { id: 'u2', roles: ['guest'] }\n\nconsole.log('guest read: ', ward.decide({ action: 'read', principal: guest, resource: 'articles' }).effect) // allow\nconsole.log('guest create:', ward.decide({ action: 'create', principal: guest, resource: 'articles' }).effect) // deny\nconsole.log('editor create:', ward.decide({ action: 'create', principal: editor, resource: 'articles' }).effect) // allow\nconsole.log('editor delete:', ward.decide({ action: 'delete', principal: editor, resource: 'articles' }).effect) // deny",
|
|
33
|
+
"name": "Permission Checks"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "permission-management",
|
|
37
|
+
"code": "import { createWard } from '@vielzeug/ward'\n\n// Rules are plain objects — build them dynamically from config or a database\nconst rules = [\n { action: 'read', resource: 'comments', effect: 'allow' },\n { action: 'write', resource: 'comments', effect: 'allow', condition: ({ principal }) => principal?.roles.includes('user') ?? false },\n { action: 'delete', resource: 'comments', effect: 'allow', condition: ({ principal }) => principal?.roles.includes('moderator') ?? false },\n]\n\nconst ward = createWard(rules)\n\nconst user = { id: 'u1', roles: ['user'] }\nconst mod = { id: 'u2', roles: ['moderator'] }\nconst guest = { id: 'u3', roles: [] }\n\nconsole.log('guest read: ', ward.decide({ action: 'read', principal: guest, resource: 'comments' }).effect) // allow\nconsole.log('user write: ', ward.decide({ action: 'write', principal: user, resource: 'comments' }).effect) // allow\nconsole.log('guest write: ', ward.decide({ action: 'write', principal: guest, resource: 'comments' }).effect) // deny\nconsole.log('mod delete:', ward.decide({ action: 'delete', principal: mod, resource: 'comments' }).effect) // allow",
|
|
38
|
+
"name": "Permission Management"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "role-hierarchy",
|
|
42
|
+
"code": "import { createWard } from '@vielzeug/ward'\n\n// Simulate role hierarchy via condition callbacks\nconst hasRole = (role: string) => ({ principal }: { principal?: { roles: readonly string[] } | null }) =>\n principal?.roles.includes(role) ?? false\n\nconst ward = createWard([\n { action: 'read', resource: 'posts', effect: 'allow' },\n { action: 'update', resource: 'posts', effect: 'allow', condition: hasRole('editor') },\n { action: 'delete', resource: 'posts', effect: 'allow', condition: hasRole('admin') },\n])\n\nconst editor = { id: 'u1', roles: ['editor'] }\nconst admin = { id: 'u2', roles: ['admin'] }\nconst viewer = { id: 'u3', roles: ['viewer'] }\n\nconsole.log('viewer read: ', ward.decide({ action: 'read', principal: viewer, resource: 'posts' }).effect) // allow\nconsole.log('editor update:', ward.decide({ action: 'update', principal: editor, resource: 'posts' }).effect) // allow\nconsole.log('admin delete:', ward.decide({ action: 'delete', principal: admin, resource: 'posts' }).effect) // allow\nconsole.log('viewer delete:', ward.decide({ action: 'delete', principal: viewer, resource: 'posts' }).effect) // deny",
|
|
43
|
+
"name": "Role Hierarchy"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "wildcard-permissions",
|
|
47
|
+
"code": "import { WILDCARD, createWard } from '@vielzeug/ward'\n\n// Wildcards match any action or resource\nconst ward = createWard([\n { action: WILDCARD, resource: WILDCARD, effect: 'allow', condition: ({ principal }) => principal?.roles.includes('admin') ?? false },\n { action: 'read', resource: WILDCARD, effect: 'allow' },\n { action: WILDCARD, resource: WILDCARD, effect: 'deny' },\n])\n\nconst admin = { id: 'u1', roles: ['admin'] }\nconst viewer = { id: 'u2', roles: ['viewer'] }\n\nconsole.log('admin anything:', ward.decide({ action: 'delete', principal: admin, resource: 'users' }).effect) // allow\nconsole.log('viewer read: ', ward.decide({ action: 'read', principal: viewer, resource: 'posts' }).effect) // allow\nconsole.log('viewer delete: ', ward.decide({ action: 'delete', principal: viewer, resource: 'posts' }).effect) // deny",
|
|
48
|
+
"name": "Wildcard Permissions"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"typeSignatures": {
|
|
52
|
+
"ANONYMOUS": "export { ANONYMOUS, WILDCARD } from './constants';",
|
|
53
|
+
"WILDCARD": "export { ANONYMOUS, WILDCARD } from './constants';",
|
|
54
|
+
"WardConditionError": "export { WardConditionError, WardConfigError, WardError } from './errors';",
|
|
55
|
+
"WardConfigError": "export { WardConditionError, WardConfigError, WardError } from './errors';",
|
|
56
|
+
"WardError": "export { WardConditionError, WardConfigError, WardError } from './errors';",
|
|
57
|
+
"createWard": "export { createWard } from './factory';",
|
|
58
|
+
"matchesPattern": "export { matchesPattern, patternCovers } from './resource';",
|
|
59
|
+
"patternCovers": "export { matchesPattern, patternCovers } from './resource';",
|
|
60
|
+
"allow": "export { allow, deny, predicate } from './rules';",
|
|
61
|
+
"deny": "export { allow, deny, predicate } from './rules';",
|
|
62
|
+
"predicate": "export { allow, deny, predicate } from './rules';",
|
|
63
|
+
"BoundWard": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
64
|
+
"BoundWardAllowedActionsInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
65
|
+
"BoundWardDecisionInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
66
|
+
"Principal": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
67
|
+
"UserPrincipal": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
68
|
+
"Ward": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
69
|
+
"WardAllowedActionsInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
70
|
+
"WardAttributes": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
71
|
+
"WardAttributeValue": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
72
|
+
"WardCondition": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
73
|
+
"WardConditionInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
74
|
+
"WardDecision": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
75
|
+
"WardDecisionInput": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
76
|
+
"WardEvent": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
77
|
+
"WardPattern": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';",
|
|
78
|
+
"WardRule": "export type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardAttributes,\n WardAttributeValue,\n WardCondition,\n WardConditionInput,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardPattern,\n WardRule,\n} from './types';"
|
|
79
|
+
}
|
|
80
|
+
}
|