# Extensions

An extension is a Python package installed beside the server that adds to it: tables in the account store, pages, routes, readiness checks, CLI commands, or a limit on what an account may do. The server finds it through an entry point and calls its hooks while it starts.

Import everything an extension builds on from `mindin.extension_api`. The names there stay put between releases; the modules they come from may move.

## Register the package

Name a factory that returns an `Extension` in the `mindin.extensions` entry point group:

```toml
[project.entry-points."mindin.extensions"]
orchard = "orchard_extension:Orchard"
```

```python
from mindin.extension_api import Extension, Server


class Orchard(Extension):
    def attach(self, server: Server) -> None:
        server.account_sections.append(lambda account, csrf: "<h2>Orchard</h2>")
```

Every hook has a default that does nothing, so a class overrides only what it needs. With several extensions installed, they run in the order of their entry point names.

## Hooks

| Hook | When it runs | What it returns |
| --- | --- | --- |
| `migrate(connection)` | Every time the account store opens, after the server's own tables. | Nothing. Create or upgrade this extension's own tables with `CREATE … IF NOT EXISTS`. |
| `database(directory)` | Once, when the account store opens. | A `Database` to keep the account store in, or `None` for `hosted.sqlite` in `directory`. The first extension that returns one wins. |
| `commands(subparsers)` | When the CLI builds its parser. | Nothing. Add subcommands with `subparsers.add_parser(...)`. |
| `attach(server)` | Once, while the HTTP server is built. | Nothing. Add to the lists on `server`. |
| `bus(server)` | Once, right after `attach`. | A `ChangeBus`, or `None` for none. The first extension that returns one wins. |
| `access(account)` | On every request an account makes. | `"full"`, `"read"` or `"none"`. The strictest answer of all extensions applies. |

## The server an extension attaches to

`attach` and `bus` receive a `Server`:

| Field | What it is |
| --- | --- |
| `config` | The server's `AuthConfig`: `path_prefix`, `issuer` and the rest of its settings. |
| `store` | The `HostedStore` holding accounts and vaults; `store.database` is the `Database` it runs on. |
| `clock` | The server's clock, seconds since the epoch. |
| `browser` | The `Browser` that signs people in on pages; `browser.signed_in(request)` gives the signed-in account and its CSRF token, or `None`. |
| `vaults` | The `SubjectVaults` that open each vault on first use. |
| `sealed` | The `SealedVaults` that seal vault storage; `sealed.prefix` is the storage prefix of all vaults, `sealed.facts(vault_id)` counts a vault's notes and bytes. |
| `deletion` | The `SelfDeletion` that erases an account and its vaults. |
| `smtp_factory` | The SMTP client factory to hand `Mailer`, or `None` for `smtplib`. |
| `routes` | Starlette routes to add. Put them under `config.path_prefix`. |
| `account_sections` | Functions `(account, csrf) -> html` that add a section to the Account page (`/account/settings`). |
| `vault_sections` | Functions `(account, vault, csrf) -> html` that add a section to a vault's page. |
| `health_checks` | Functions `() -> bool` that `/ready` runs; any that returns `False` or raises makes it answer 503 and name the check. |

Escape everything a section prints with `esc`, and wrap a route that takes a form or a secret in `limited` so it shares the server's rate limits.

## The account store

`Database` is what the account store runs on: `connect()` gives a `Connection` that commits when the block ends cleanly, and SQL uses `?` parameters. Write SQL that both SQLite and the database you supply run: `INSERT … ON CONFLICT … DO …` rather than `INSERT OR …`, and `database.tables(connection)` and `database.columns(connection, table)` rather than SQLite's catalogue. `has_table(connection, name)` checks a table on SQLite alone. `STORE_VERSION` is the number of the store's own migrations, so a tool that copies the store can check it is copying the version this server writes. `rewrite_slugs(connection)` gives every vault whose slug breaks the one-word rule the slug the server would make today, for a database that brings its own schema.

## The change bus

A `ChangeBus` (`publish`, `listen`, `close`) carries `Change(vault_id, kind, origin)` messages. The server publishes one after each write, layout change or vault deletion, and calls `listen` once with the function that applies a change it receives: `write` marks the vault's index stale, `schema` reloads its layout, `gone` drops the vault and its local index, `all` marks every open vault stale. A change carrying the server's own `origin` is ignored, so a bus may hand the server its own changes back. Without a bus, an open vault notices writes made to its storage outside the server by re-reading its listing.

## Names

`mindin.extension_api` exports:

- Hooks and the server: `Extension`, `Server`, `Access`, `Section`, `VaultSection`, `HealthCheck`, `Subcommands`.
- The account store: `Database`, `Connection`, `HostedStore`, `Account`, `Vault`, `Identities`, `STORE_VERSION`, `rewrite_slugs`, `has_table`, `account_of`, `digest`, `scope_label`, `MACHINE_KEY_PURPOSE`, `INVITATION_TTL`.
- Pages and mail: `Browser`, `page`, `esc`, `stamp`, `limited`, `Mailer`, `SmtpFactory`, `EMAIL_ADDRESS`.
- Vaults and storage: `SubjectVaults`, `SealedVaults`, `SelfDeletion`, `VaultFacts`, `VaultBackend`, `make_subject_backend`, `MUST_NOT_EXIST`, `VersionConflict`, `honours_conditions`.
- Changes: `ChangeBus`, `Change`, `Kind`, `Listener`.
- Settings: `AuthConfig`, `ConfigError`, `env`, `local_root`, `auth_dir`, `semantic_requested`.
