Search

    MENU

    Core library

    Markdown

    @zinn-dev/core provides Zinn’s shared operations, input schemas, and SQLite storage. Install it in a Node.js project:

    			
    				
    						
    						npm i @zinn-dev/core
    					
    			
    		

    Core requires Node.js 24.15.0 or newer and also runs on Bun.

    Configure the database before the first operation. Supply an explicit actor to every mutation:

    			
    				
    						
    						import type { Actor } from "@zinn-dev/core";
    import { fileURLToPath } from "node:url";
    import { configureDatabase, project, task } from "@zinn-dev/core";
    
    configureDatabase({
    	client: "example",
    	version: "0.1.0",
    	executable: fileURLToPath(import.meta.url),
    	release: "source"
    });
    
    const actor: Actor = { name: "Alper", email: null, kind: "human" };
    const app = project.create({ key: "APP", name: "My project" }, actor);
    const createdTask = task.create({ project_id: app.id, title: "Ship the first feature" }, actor);
    
    console.log(createdTask.key); // APP-1
    console.log(task.getAll({ projectKey: app.key }));
    					
    			
    		

    Save the example as example.ts and run it with a separate Zinn directory:

    			
    				
    						
    						mkdir -p ./.zinn-example
    					
    						
    						ZINN_DIR=./.zinn-example node example.ts
    					
    			
    		

    Core opens the database on first use. project.create and label.create create it when the store has none. Other operations throw a CoreError with kind missing, entity database and the databasePath, and create nothing.

    By default it shares ~/.zinn/data/zinn.sqlite with the CLI, but ZINN_DIR and settings files can select another location. See Settings for directory selection.

    getStorage() returns a fresh Storage snapshot on each call. Settings and the selected directory are chosen on first use and stay cached. Each snapshot resolves the database destination through the current filesystem links. getBackupStorage() resolves the backup folder as well, for an operation that writes a backup. Keep one snapshot throughout an operation. An existing connection retains the destination it opened, including for its errors and search reindexing.

    The client supplies its own release identity: source for development and published for prepared release artifacts. Core identifies its own build independently. Core throws errors while the client handles output, prompts, and an optional database-wait callback.

    Actor clients

    Library callers may supply Actor.client as a string or null. Core preserves the supplied string exactly in actor_client on every recorded activity event. Omitting it stores null. Events recorded before schema version 4 also have a null client after migration.

    Clients can follow Zinn’s client chain convention when constructing their value. Core supplies no client token of its own.

    parseClientChain(client) returns an array of { name, version } objects in chain order. It splits on whitespace and separates each token at its first /. Tokens without / have a null version. Null or blank input returns an empty array. Names and versions preserve case and arbitrary text, including any further slashes in a version. Parsing does not validate tokens or change the stored string.

    Client chain convention

    Zinn’s preferred convention for its own clients and integrations is:

    • Use space-separated name/version tokens, without parenthesized comments. Names use lowercase letters, digits and -. Versions contain no whitespace or /.
    • Name a part of a program as program:part, such as zinn-opencode:panel/0.3.0. The version belongs to the program.
    • An intermediary appends its own token to its inherited declaration, separated by one space and trimmed. The declared chain reads outermost caller first.

    Other programs may declare any text. The client activity filter uses whitespace-separated tokens for every chain, so words inside a caller’s parenthesized comments can also match.

    Shared behavior

    project.getAllWithTaskCounts() returns project records with taskCounts: { active, archived }. Counts cover existing tasks in each project. Active tasks are unarchived tasks in any column, including Done. Both counts are always present, including zero.

    task.getAll, task.search and activity.getAll accept an optional numeric limit, an integer from 1 through Number.MAX_SAFE_INTEGER, inclusive. The cap applies after all filters and preserves the existing order. Omitting it returns every match. task.search counts all hidden archived matches before applying the cap.

    task.getAll({ sort: "activity", limit: 10 }) returns the ten most recently active tasks. Last activity is the newest recorded task event, including comments and relations, or updated_at when no events exist. Filters apply before sorting, and the limit applies afterward. Equal times keep board order. Omitting sort preserves board order.

    task.getAll returns an array. task.getPage accepts the same task-list filters plus optional startingAfter and returns { tasks, hasMore, nextCursor? }. nextCursor is an opaque token, present only when hasMore is true. Pass it as startingAfter to continue.

    Both take state: "open", "closed" or "all" (the default). A task is closed when its column’s category is done or cancelled. state combines with archive and every other filter.

    Continuation survives edits, moves, archiving, relabelling, deadline changes and deletion of the last listed task. Filters may change while the sort stays the same. No snapshot is kept between calls, so other tasks changing in between can still cause repeats or skips. The order breaks remaining ties by task ID. Omit limit to return every remaining task with hasMore: false.

    A refused cursor throws a CoreError with kind input and field: ["startingAfter"]. The reason is invalid_cursor (malformed), cursor_version (made by another version), different_sort (made for another sort) or task_id_cursor (a task ID).

    			
    				
    						
    						const firstPage = task.getPage({ projectKey: "APP", limit: 10 });
    if (firstPage.hasMore) {
    	const nextPage = task.getPage({
    		projectKey: "APP",
    		limit: 10,
    		startingAfter: firstPage.nextCursor
    	});
    	console.log(nextPage.tasks);
    }
    					
    			
    		

    Each call reads one current database snapshot. Separate calls do not share a snapshot. Moves, reorders, edits, new activity and other changes can change task positions between pages and cause repeats or skips.

    task.next({ projectKey?, columnName? }) returns the next ready task, or null when none is ready. A task is ready when it is active, in a column whose category is todo or backlog, and every task it depends on is archived or in a column whose category is done or cancelled. Ready tasks in todo columns come first, then those in backlog columns. Within each, they are ordered by priority (urgent first, unset last), then the earliest due_at with undated tasks last, then creation time. columnName takes ready tasks from that one column instead, of any category but done and cancelled. It matches by exact name, ignoring case, as in task.getAll. On an older database opened read-only, which has no categories, every column but a board’s last holds ready tasks. An unknown projectKey throws a CoreError with kind missing.

    Names and titles are trimmed and cannot be blank or contain line breaks or control characters.

    • Edits change only supplied fields. Valid edits that leave values unchanged preserve modification timestamps and record no activity.
    • task.edit, task.label.add, task.label.remove and task.order return a TaskChange: { task, changed }. The task field contains the resulting task, and changed is false when the requested state was already satisfied. For several label changes, it is true when any attachment changed.
    • project.edit, column.edit, label.edit and comment.edit return ProjectChange, ColumnChange, LabelChange and CommentChange: { project, changed }, { column, changed }, { label, changed } and { comment, changed }. The object field contains the resulting record; changed is false when all supplied values already matched.
    • column.order also returns a ColumnChange. Ordering a task or column to its current position, including before or after itself, returns changed: false and records no activity.
    • Every column has a category: backlog, todo, in_progress, review, done or cancelled (columnCategorySchema). column.create({ projectKey, name, category }, actor) requires it, and column.edit({ projectKey, columnName, name?, category? }, actor) takes a name, a category or both. Renaming keeps the category. Activity records the category on create and as a [previous, new] pair on change. project.create adds Backlog (backlog), TODO (todo), Doing (in_progress), Review (review), Done (done) and Cancelled (cancelled). column.getTaskCountsByProjectKey gives each column’s taskCount of unarchived tasks and archivedTaskCount. On an older database opened read-only, columns have no category.
    • column.getByName(projectKey, columnName) returns a column matched case-insensitively and throws if the project or column is missing. column.delete({ projectKey, columnName }, actor) returns the deleted column and records its project ID, project key and name in activity. It refuses columns holding any active or archived tasks. A project may have no columns. task.create then fails with reason no_columns without allocating a task number. Add a column with column.create before creating tasks.
    • Descriptions default to null. Passing null or blank text clears them. Non-blank text input keeps its whitespace.
    • Descriptions and comment text normalize CRLF line endings to LF, including append, find and replacement inputs. All other whitespace, including lone carriage returns, is preserved.
    • Descriptions and comment text are limited to MAX_TEXT_BYTES, exported by core: 1 MiB (1,048,576 UTF-8 bytes). Full text is checked before normalization and partial edits check the resulting text. Oversized input fails without writing changes. Editing other fields does not revalidate a stored description.
    • Entity mutations record the actor and activity in the same transaction as the change.
    • After a change commits, core runs the hooks from the global settings file in the calling process, with its working directory and environment. ZINN_HOOKS=0 saves changes without running them.
    • Bulk task moves, orders, archives, unarchives and deletes apply in input order and succeed or fail together.
    • task.move and task.moveMany take an optional fromColumn. A task that is no longer in that column fails the move with kind conflict, reason column_changed, and expectedColumn and actualColumn facts naming the two columns. Nothing is written. The check runs inside the write transaction, so of several callers moving one task from the same column, one succeeds.
    • task.deleteMany(taskKeys, actor) returns the deleted tasks in input order, including archived tasks. Task keys must be distinct, ignoring case. Each deletion preserves task activity and records removed relations on the other tasks, including tasks deleted later in the same batch. task.delete(taskKey, actor) returns one deleted task.
    • project.delete(key, actor) records aggregate project deletion history and one relation_delete event per removed cross-project relation on its surviving task, including archived tasks. Surviving task modification timestamps are preserved.
    • task.orderMany({ taskKeys, direction, targetTaskKey? }, actor) returns a TaskChange per key. It orders the first task as requested, then each next task after the previous one. Multiple tasks must be distinct active tasks in the same project and column. The target cannot be among them. up and down take one task. A single task ordered before or after itself remains unchanged.

    Task deadlines

    Task results include due_at: number | null, an exact deadline in integer Unix epoch milliseconds. task.create accepts optional due_at. Omission or null creates an undated task. task.edit also accepts due_at. Omission preserves the current value and null clears it. Repeating the current value or clearing an unset deadline is a no-op that preserves updated_at and records no activity.

    timestampSchema, exported by core, validates integer milliseconds in the JavaScript Date range from -8640000000000000 through 8640000000000000, inclusive. activityTimestampSchema remains exported as an alias to the same schema.

    task.getAll and task.search accept optional dueSince and dueUntil bounds. Both are inclusive. Use either bound alone or both together. Equal bounds select that exact millisecond, and a reversed range is invalid. A supplied deadline bound excludes tasks whose due_at is null. Deadline bounds intersect with the other filters before the result limit applies. Search retains its relevance and task-key order.

    task.getAll({ sort: "due" }) sorts by earliest deadline first, puts undated tasks last, and preserves board order for equal deadlines and undated tasks. This is opt-in and does not change task positions. task.search has no due sort option.

    Task relations

    relation.create({ sourceTaskKey, targetTaskKey, relation_type }, actor) creates a relation. Types are related, dependency, duplicate and subtask. A pair has at most one relation across all types and both directions. Directed types preserve their source and target. An exact existing relation is an error.

    relation.getConflicts({ sourceTaskKey, targetTaskKey, relation_type }) validates the requested creation and returns conflicting relation records with source and target task records. Conflicts include a different parent when creating a subtask. Self-relations, exact repeats and subtask cycles fail before confirmation.

    After obtaining approval, pass the returned relation IDs as replace to authorize exactly those removals:

    			
    				
    						
    						import { relation } from "@zinn-dev/core";
    
    const request = { sourceTaskKey: "APP-1", targetTaskKey: "APP-2", relation_type: "subtask" as const };
    const conflicts = relation.getConflicts(request);
    // Obtain approval in the calling interface before replacing these links.
    const created = relation.create({ ...request, replace: conflicts.map(({ id }) => id) }, actor);
    					
    			
    		

    If the conflicting IDs changed before the write, core throws a CoreError with kind conflict and reason relation_changed. replace: true explicitly authorizes replacing the current conflicting links without an expected-ID check. Omitting replace or passing false refuses conflicting links. Every removal and the creation share one transaction and activity timestamp. Failures preserve existing links and activity.

    task.create accepts relations: [{ relation_type, targetTaskKey }]. Identical type and resolved-target requests deduplicate. Different types for one target fail the whole creation without consuming a task number or recording activity.

    Task blame

    task.blame(taskKey) returns a TaskBlame for the current active or archived task. It resolves the key once and reads the task, current memberships and ID-scoped history in one database snapshot. A reused key selects only the current task lifetime. Unknown and deleted keys throw the ordinary missing-task CoreError. For the command and a worked example, see Task blame.

    			
    				
    						
    						import type { TaskBlame } from "@zinn-dev/core";
    
    const blame: TaskBlame = task.blame(createdTask.key);
    console.log(blame.fields.title.value);
    console.log(blame.fields.title.attribution?.actor_session_id ?? null);
    for (const line of blame.description) {
    	console.log(line.line, line.text, line.attribution?.id ?? null);
    }
    					
    			
    		

    The result contains task: { id, key }, blamed fields, current relations, and numbered description lines. Core exports TaskBlame, TaskBlamedValue<T> and TaskAttribution. See Task blame result for the complete shape. Attribution projects the recorded event ID, time and actor fields exactly as stored. Use activity.getById(attribution.id) to inspect its full event.

    Creation can attribute initial values. A scalar value recorded later is attributable only when its latest recorded new value matches the current value. Labels are one membership collection, including removals and label deletion, attributable only when the recorded creation and membership changes add up to the current number of labels. Current column and label names accompany the original membership attribution. Parent assignment or removal follows outgoing subtask events. Every current relation also carries its creation attribution, including parents and children, in the existing relation order.

    To attribute description lines, core compares each recorded description with the previous one, line by line. Lines that are unchanged keep the attribution they had. Lines that an edit inserts or replaces receive that edit’s attribution, and deleted lines disappear. The comparison keeps the shared lines at the start and the end in place, then matches the longest sequence of remaining lines that both versions share in the same order. When an edit could be read more than one way, it treats a line as removed before treating it as added. Each edit has a fixed search budget, enough for about 2,500 replaced lines. Past it, lines not yet matched receive that edit’s attribution, so a heavily reordered description reads as rewritten. Whitespace and internal blank lines are compared exactly. A terminal newline ends the last line without adding an empty one, and changing only that newline preserves attribution. Null or empty descriptions return an empty array.

    The replay follows per-task activity ID order. Missing or unrecognized history yields null attribution. When an edit’s recorded old description does not match the description carried from earlier records, unchanged old lines start unattributed while the lines that edit introduces receive its attribution. A later description record that does not hold both the old and new text discards the carried line attribution, so later edits cannot inherit it. If the final recorded description disagrees with the current text, all current lines are unattributed. The operation records no activity and does not mutate task data.

    Task blame result

    			
    				
    						
    						type TaskAttribution = {
    	id: string;
    	created_at: number;
    	actor_name: string | null;
    	actor_email: string | null;
    	actor_kind: ActorKind;
    	actor_source: string | null;
    	actor_session_id: string | null;
    	actor_thread_id: string | null;
    	actor_agent: string | null;
    	actor_model: string | null;
    	actor_effort: string | null;
    	actor_client: string | null;
    };
    type TaskBlamedValue<T> = { value: T; attribution: TaskAttribution | null };
    type TaskBlame = {
    	task: { id: string; key: string };
    	fields: {
    		title: TaskBlamedValue<string>;
    		column: TaskBlamedValue<{ id: string; name: string }>;
    		priority: TaskBlamedValue<TaskPriority | null>;
    		due_at: TaskBlamedValue<number | null>;
    		labels: TaskBlamedValue<Array<{ id: string; name: string }>>;
    		parent: TaskBlamedValue<{ id: string; key: string } | null>;
    	};
    	relations: Array<{
    		id: string;
    		type: TaskRelationType;
    		direction: "outgoing" | "incoming" | null;
    		task: { id: string; key: string };
    		attribution: TaskAttribution | null;
    	}>;
    	description: Array<{
    		line: number;
    		text: string;
    		attribution: TaskAttribution | null;
    	}>;
    };
    					
    			
    		

    TaskAttribution contains only the listed recorded event fields, without a changes payload. Timestamps are integer Unix epoch milliseconds. Labels use the existing label name order. Relations use creation time ascending, then relation ID ascending. direction is relative to the inspected task and is null for symmetric related links. Description line numbers start at one.

    Activity records

    Core exports the Activity type for a recorded event. Its fields are id, created_at (epoch milliseconds), actor_name, actor_email, actor_kind, actor_source, actor_session_id, actor_thread_id, actor_agent, actor_model, actor_effort, actor_client, entity_type, entity_id, action, and changes (parsed JSON or null).

    The id identifies the activity event itself. actor_name can be null for an unnamed agent. Optional actor metadata is null when absent. Payloads and IDs are preserved in full, including after deletion. Project deletion events keep the project’s entity_id, with removed task IDs in changes.tasks.

    Hooks receive these same records, with the task’s key added as task_key.

    Activity selection

    activity.getAll returns { events, hasMore, nextCursor? }. events contains full activity records, newest first by timestamp and then event ID. Without a project or task selector it reads across the database. hasMore is true only when another event matches after the returned page. Without limit, it is false.

    nextCursor is the last returned event’s ID and is present only when hasMore is true. Pass startingAfter with it to continue toward older events, excluding that event. It accepts the same full IDs and unique prefixes as activity.getById. The cursor event need not match the supplied filters. Every filter applies before the page cap, including changed and client. Omitting limit returns every remaining match.

    			
    				
    						
    						const firstPage = activity.getAll({ limit: 10 });
    if (firstPage.hasMore) {
    	const nextPage = activity.getAll({
    		limit: 10,
    		startingAfter: firstPage.events.at(-1)!.id
    	});
    	console.log(nextPage.events);
    }
    					
    			
    		

    activity.follow(input) returns a follower for the activity that other connections commit from then on. It accepts the fields of activity.getAll except limit and startingAfter, which raise an input error when supplied, and throws as activity.getAll does for a selection that does not exist. Each poll() returns { events } with the matching events committed since the previous call, oldest first in commit order, which is [] when nothing new matches. Polling detects commits through SQLite’s PRAGMA data_version, reads inside one short transaction and keeps none open between calls.

    			
    				
    						
    						const follower = activity.follow({ changed: [{ field: "column", value: "Review" }] });
    
    setInterval(() => {
    	const polled = follower.poll();
    	if ("stopped" in polled) {
    		throw new Error(`Following stopped: ${polled.stopped}`);
    	}
    
    	for (const event of polled.events) {
    		console.log(event.entity_id);
    	}
    }, 1000);
    					
    			
    		

    A poll returns { stopped, databasePath } once the follower can read no further. stopped is "schema_changed" when the database’s schema marker changes, including a restore, migration or reindex by another connection, and "file_replaced" when the database path no longer leads to the file the process opened. After file replacement, restart the hosting process before following again so it opens the replacement database. Core exports ActivityFollower, ActivityFollowInput and ActivityFollowStop.

    Optional numeric since and until fields filter the stored created_at, inclusively, before limit applies. Use either bound alone or both together. They accept integer epoch milliseconds between -8640000000000000 and 8640000000000000, inclusive. Equal bounds select events at exactly that millisecond. A reversed range raises an input CoreError whose facts.field is ["until"]. Historical identity resolution uses the full recorded history before time selection.

    • { taskKey: "APP-1", showDeleted: false } selects the existing active or archived task. A missing task raises a CoreError with kind missing; its facts identify entity task and the requested key.
    • { taskKey: "APP-1", showDeleted: true } includes every recoverable existing or deleted task identity sharing that key. Omitting showDeleted selects only the existing task, as with false.
    • { taskId: createdTask.id } selects one exact task identity, including a deleted task. IDs accept full, case-insensitive UUIDs with canonical hyphens or 32 hexadecimal digits. Task ID prefixes are rejected.
    • { projectKey: "APP" } selects the existing project’s events, its columns, and all its tasks, including archived and explicitly deleted tasks. A missing project raises a CoreError with kind missing, entity project, and the requested key.
    • { projectKey: "APP", showDeleted: true } includes every recorded existing or deleted project identity sharing that key. Omitting showDeleted or setting it to false selects only the existing project.
    • { projectId: createdProject.id } selects one exact project identity, including after deletion. Project IDs accept the same full UUID spellings as task IDs, without prefixes.

    projectKey, projectId, taskKey, and taskId are mutually exclusive. showDeleted requires projectKey or taskKey, including when it is false. A valid exact ID or explicit deleted-history query with no recorded matches returns { events: [], hasMore: false }.

    Project selection includes task comments, label attachments, reorders, and original project-deletion snapshots. Global label-definition events are excluded. Cross-project relations include only the selected project’s task event and within-project relations include both task events.

    Task selection includes original project-deletion events whose snapshots contain the selected task IDs. These events retain their project identity and full payloads. sessionId and threadId match complete, case-sensitive stored values; all supplied filters must match. limit applies after filtering.

    client selects a client name anywhere in the chain. It splits the stored string on whitespace, takes each token’s text before the first / and matches that name exactly or with a following :. Matching is case-sensitive. For example, { client: "zinn-opencode" } matches both zinn-opencode/0.3.0 and zinn-opencode:panel/0.3.0. Other supplied filters must also match. Events with a null client do not match.

    actions accepts an array of recorded action names, such as create, edit, move or comment_create. An event matches any listed action. Action names are case-sensitive and use the ActivityAction vocabulary across entity types.

    changed accepts an array of task field filters. Use { field } to match any change to column, priority, labels, title or description. Labels match additions and removals. Initial task fields also match, including null priority and description. Initial labels match when at least one label is recorded.

    Use { field: "due_at" } to match any recorded deadline change. Do not supply value for this field. Task creation snapshots include the initial deadline, including null when it is unset. Deadline edits record the previous and new values. Earlier activity payloads retain their recorded shape.

    Use { field, value } with column, priority or labels to select recorded new values or added labels. Initial values on task creation also match. String values compare case-insensitively, using the names recorded at the time of the event. Use { field: "priority", value: null } to match an unassigned priority on creation or a change that clears it. String values must be nonempty.

    Several filters for the same field match any of them, including a value-free filter alongside one with a value. Different fields must all match in the same event. For example, activity.getAll({ actions: ["edit"], changed: [{ field: "description" }, { field: "priority", value: "high" }] }) selects edits that change the description and set high priority. Selection and identity filters must also match, and limit applies afterwards. The returned records retain their full payloads.

    activity.getTaskIdentities accepts a task key string or an object containing taskKey, taskId, and showDeleted with the same selection rules. It returns each recoverable task’s ID, key, title, and active, archived, or deleted status.

    activity.getCurrentTaskIdentities({ taskIds, taskKeys }) returns the same identity for each existing task, active or archived, that matches one of the given full task IDs or task keys. Both arrays are optional. IDs and keys that match no existing task, including those of deleted tasks, are omitted.

    activity.getDeletedTaskIdentities({ taskIds, taskKeys }) returns the identity of each deleted task that matches one of the given full task IDs or task keys, read from task deletions and project deletions in one history read. The status is deleted and the key and title are those recorded at deletion. Both arrays are optional. Existing tasks are omitted.

    SQLite errors

    SQLite failures throw SqliteError on both Node.js and Bun. Its code is a symbolic name, errcode is the numeric extended result code, and cause retains the original driver error.

    Use isSqliteErrorCode(error, "SQLITE_BUSY") for an exact result code and isSqliteErrorFamily(error, "SQLITE_BUSY") for the primary code and all its subtypes. For example, SQLITE_BUSY_SNAPSHOT matches the BUSY family but does not exactly match SQLITE_BUSY. The exported SqliteCode and SqlitePrimaryCode types provide the accepted names.

    Both helpers accept unknown, narrow matching values to SqliteError, and inspect only that error. They do not inspect its cause. An unlisted extended code matches its known primary family. Its code falls back to the primary name for display, while exact matching still distinguishes it from the primary result code. A wholly unknown primary code displays as SQLITE_ERROR and matches neither helper.

    Database compatibility

    column.inspect({ taskKeys }) reads existing project column names and requested task identities without creating or upgrading storage. taskKeys is optional. It returns { projects: [{ key, columns }], taskKeys }. Note that task keys that do not identify an existing task are omitted. Empty databases and databases without a schema version return null and unavailable storage raises an error.

    Opening a database can upgrade it, even for a read operation. Upgrades save a backup first and apply completely or not at all. Upgrades and restores check that foreign keys reference existing tables and rows before committing.

    Each migration step also checks existing foreign-key definitions, including those in tables it does not rebuild. An unexpected change rolls back the upgrade. Intentional changes must match the migration’s declared starting and resulting definitions.

    • Older databases upgrade automatically when both the core package and client are published releases, or when ZINN_DIR explicitly selects a separate database. Source clients otherwise open older databases read-only until an explicit migrate.run.
    • Databases newer than core supports open read-only.
    • Databases without a schema version are refused.
    • A process that keeps the database open checks the schema version at the start of each write. When another process has changed it, such as a newer Zinn upgrading the database, the write throws a CoreError with kind conflict and reason newer_database or older_database and changes nothing. Reads keep working. Restart the process to write again.

    Core backup and restore operations use the selected store independently of the normal database-opening policy. See Data and backups for shared storage and restore behavior.

    backup.restore({ path, client, version, storage? }) accepts an optional BackupStorage snapshot from getBackupStorage(). Pass it when the caller confirms a destination before restoring. Restore uses that captured destination even if the configured symbolic link changes afterward. Without storage, restore captures a fresh snapshot when called. Source backups are validated again when restore runs.

    The public exports, input schemas, and operation implementations document the available API.