# Plugin API reference

Plugin API `1.0.0` is injected into activation; it is not obtained by
importing application modules. Import public types/helpers from the matching
runtime entry:

- `@thortiq/plugin-api/native` for a trusted native bundle;
- `@thortiq/plugin-api/sandbox` or `@thortiq/plugin-sdk/browser` for a
  sandboxed page; and
- `@thortiq/plugin-api/live` and `@thortiq/plugin-sdk/live` for managed live
  source.

The complete declarations are under `types/plugin-api/`. The generated
manifest, live-bundle, and detached signed-package schemas are under `schemas/`. Limits are in
`docs/limits.md`. Those generated artifacts are authoritative when this guide
and the type/schema contract appear to differ.

API additions within major 1 are optional. Feature-detect optional namespaces
or `api.features`. Manifest `apiVersion` is a supported range; this kit and its
npm packages use exact release versions.

Returned values are immutable serializable DTOs, opaque handles, or
lifecycle-owned registrations. The public API never exposes the data layer,
SQLite/index objects, filesystem handles, Yjs/CodeMirror objects, sync state,
credentials, or application classes.

## Note actions and bounded file viewers

Native note actions require `ui.note.action` and an exact declaration:

```ts
const archiveAction = {
  id: "archive-note",
  title: "Archive note",
  runtime: "native",
  permissions: ["ui.note.action", "notes.properties.write"],
  surfaces: ["editor", "search"],
  appliesTo: { anyTags: ["complete"], requiredProperties: ["status"] },
} as const;

api.noteActions?.register({
  ...archiveAction,
  async run({ note }, { api, signal }) {
    if (signal.aborted) return;
    await api.notes.updateProperties(note.id, { archived: true });
  },
});
```

The host evaluates only indexed metadata while opening menus, then rechecks
the current personal workspace, permissions, note existence/privacy,
applicability, user gesture, and plugin generation before invoking the
handler. The callback receives no note body.

File viewers require `ui.file.viewer` plus `assets.read`. Declare exact
lowercase extensions/MIME types and a byte cap—wildcards are rejected:

```ts
const csvViewer = {
  id: "csv-table",
  title: "CSV table",
  runtime: "native",
  rendererMode: "native-imperative",
  permissions: ["ui.file.viewer", "assets.read"],
  extensions: ["csv"],
  mediaTypes: ["text/csv"],
  maxBytes: 1_048_576,
  content: "text",
} as const;

api.fileViewers?.register({
  ...csvViewer,
  mount(container, input) {
    const pre = document.createElement("pre");
    pre.textContent = input.content.kind === "text" ? input.content.text : "";
    container.replaceChildren(pre);
    return { destroy: () => pre.remove() };
  },
});
```

Built-in PDF/image/generic viewers keep precedence until the user explicitly
selects a matching plugin or stores an exact type default. The host reads only
after navigation, verifies indexed path plus extension/signature/MIME/cap, and
supplies a copied DTO—never a filesystem handle. Sandboxed-page declarations
use `rendererMode: "sandboxed-page"` and an opaque viewer lifecycle; live
declarations use `"live-declarative"` and publish only the validated UI tree.
Live file viewers use `content: "text"` in API v1.

## Trusted editor extensions

Native/full-trust plugins may declare `contributes.editorExtensions` together
with `ui.editor.extension` and `editorExtensionBuild`. Register the identical
declaration during activation. Its synchronous `create(modules, context)`
factory must construct the extension only from the frozen host-supplied
CodeMirror cohort. Import public contracts with `import type`; do not import
CodeMirror, Yjs, y-codemirror, or lib0 at runtime.

The immutable context contains bounded note/workspace identity, edit/read
state, theme, validated scalar configuration, same-plugin command invocation,
and pane-scoped context-change events. It contains no editor view/state,
Y.Doc/Y.Text, live-doc/sync/filesystem handle, policy compartment, or
persistence callback. The generated build emits the required digest-bound
metafile proof and fails if a prohibited runtime was bundled or left imported.
This cohort rule does not change native code's full-trust status.

## Declared Markdown blocks

Declare a block with metadata shared by the manifest and registration:

```json
{
  "contributes": {
    "markdownBlocks": [{
      "id": "summary",
      "title": "Summary",
      "runtime": "native",
      "rendererMode": "native-imperative",
      "permissions": ["notes.read"],
      "configSchema": {
        "version": 1,
        "type": "object",
        "properties": {
          "label": { "type": "string", "maxLength": 80 }
        },
        "required": ["label"],
        "additionalProperties": false
      }
    }]
  }
}
```

Native activation registers the identical declaration:

```ts
api.markdownBlocks.register({
  ...manifest.contributes.markdownBlocks[0],
  render(container, block) {
    const heading = document.createElement("strong");
    heading.textContent = block.config.label;
    container.replaceChildren(heading);
    return { destroy: () => heading.remove() };
  },
});
```

The note anchor and owning sidecar are deliberately separate:

````markdown
```thortiq-plugin
{"id":"summary-1"}
```
````

```json
{
  "version": 1,
  "blocks": {
    "summary-1": {
      "id": "summary-1",
      "type": "my-plugin:summary",
      "props": { "version": 1, "config": { "label": "This week" } }
    }
  }
}
```

The type is host-namespaced. Config is scalar, closed by the declared schema,
at most 64 KiB/64 properties, and refreshed at most ten times per second. The
renderer receives an immutable DTO with explicit source-note, workspace,
read-only, and interaction state. Return a disposable handle; do not return
HTML or a DOM node.

Live declarations use `runtime: "live-sandboxed"` and
`rendererMode: "live-declarative"`. Handle the
`markdown-block-<local-id>` mount and publish only validated UI:

```js
thortiq.lifecycle.onMount(async event => {
  if (event.viewId !== "markdown-block-summary") return;
  const ui = thortiq.forLifecycle(event.instanceId).ui;
  await ui.setView({
    version: 1,
    root: { id: "label", type: "text", text: event.initialState.config.label },
  });
  return () => ui.clearView();
});
```

`sandboxed-page` block declarations are unsupported in API v1 and return
`markdown-block.sandboxed-page-unsupported`; no per-block iframe is created.
Shared/unavailable workspaces never run plugin code and instead show escaped
source/config.
