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:
[project.entry-points."mindin.extensions"]
orchard = "orchard_extension:Orchard"
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.