Storage, Network & Secrets
Persisting config, making host-proxied HTTP calls, and storing credentials in the Keychain — and why each is a separate capability.
Beyond the file system, a module often needs to remember settings, talk to a server, or keep a password. Each is its own capability with its own permission, so a module asks for exactly what it needs and nothing more.
Config — per-module persisted settings
storage permission. Small key/value persistence, namespaced per module so
two modules can't read or clobber each other's settings (the key is
mutka.modcfg.<moduleId>.<key> under the hood).
permissions: ["storage"],
setup(host) {
await host.config.set("accounts", JSON.stringify(list));
const raw = await host.config.get("accounts"); // string | null
}Values are strings — serialize structured data yourself (JSON). This is for
settings, not secrets: it's plain localStorage, readable by anyone with the
file. Passwords go in the Keychain (below).
Secrets — credentials in the macOS Keychain
secrets permission. Per-module credential storage backed by the macOS
Keychain — never plaintext on disk. Each module gets its own Keychain service
(mutka.<moduleId>), so modules are isolated from each other's secrets.
permissions: ["secrets"],
setup(host) {
await host.secrets.set(accountId, password);
const password = (await host.secrets.get(accountId)) ?? "";
await host.secrets.delete(accountId);
}Why split config and secrets
They have different threat models. Config is convenient but inspectable;
secrets are protected by the OS. Splitting them means a module that only needs
to remember a window size (storage) never gets to ask for Keychain access,
and a credential never accidentally lands in localStorage. WebDAV uses both:
account metadata in config, each password in secrets.
Network — host-proxied HTTP
network:public or network:local permission. Outbound HTTP that runs in
Rust, not the WebView, which means it bypasses CORS — essential for talking
to arbitrary servers (WebDAV, S3, an API) that a browser fetch would block. There
is no blanket network: declare network:public for HTTPS to public domains, or
network:local for a self-hosted server / NAS on a private IP or localhost
(both if you need both). The URL is tier-checked in Rust on every call.
| Method | Does |
|---|---|
host.net.request(opts) | A general request (url, method, headers, body) |
host.net.download(opts) | Saves a URL to a temp file, returns its path |
host.net.upload(opts) | PUTs a local file's bytes to a URL |
permissions: ["network:public"],
setup(host) {
const res = await host.net.request({
url: "https://dav.example.com/remote.php/dav/",
method: "PROPFIND",
headers: { Authorization: `Basic ${btoa(`${user}:${pass}`)}` },
});
}download and upload stream through the file system rather than the JS heap,
so they handle large files without loading them into memory.
No `database` capability — by design
There is deliberately no SQL or database capability in the core. A
.sqlite file is the database: a module reads its raw bytes with fs:read
(host.fs.readBytes → a Uint8Array) and decodes the format itself, entirely
inside its sandboxed worker — no Rust, no server, no extra permission. See
com.sqlite-browser,
which parses the SQLite on-disk format in the worker and renders its tables and
rows with declarative UI. This is the pattern
any format viewer (zip, pdf, parquet, …) should follow: read bytes, parse in
the worker. Keeping it out of the core means the attack surface stays small
and the byte-reading already covered by fs:read does the job.
Putting it together
These four capabilities are why a single ~300-line WebDAV module can offer
multi-account remote storage: network:public to reach the server, secrets for each
password, config for the account list, ui for its settings section, and
fs:read to read bytes — each requested explicitly, each independently denied if
the module asked for more than it should. See the
capability reference for the full list.
Events & File Watching
The narrow set of app events a module may subscribe to, and how directory watching works (and why it's current-directory-only).
Security Model
The security guarantees that protect users when running community modules — six layers from worker isolation to event whitelisting, and the residual risks you should understand.