Patterns (L3)
The runtime contract crates (posthaste-contract-core, posthaste-runtime-api,
posthaste-client-link) are separate from the node crates: the near node lives
in posthaste-runtime and the far node in posthaste-authority-server
(crate topology). The wrapping of the legacy API path behind the handle is complete and its
dead residue removed (RFC-L2-architecture-cleanup D20); the surviving
service-graph handle and from_api_bridge_* entry points are deliberate
test-harness seams (testkit), not part of the durable contract.
1. Contract crate types¶
posthaste-contract-core holds the shared wire vocabulary and is wasm-pure:
allowed dependencies are posthaste-domain-model, posthaste-replica-core,
serde/serde_json, thiserror, and optional utoipa behind the openapi
feature — no tokio, reqwest, rusqlite, mail-parser, or axum. The
contract traits live in posthaste-runtime-api (typed, wire-free domain RPC
returning serde domain types) and posthaste-client-link (link ops: mutation
forward, RuntimeFrame stream, link/view-stream ops); they add async-trait
and futures-util. The former RuntimeCore god-trait splits across these two
traits. The mutation forward keeps its shape (named forward_mutation — one
up-channel verb across both seams, RFC D35b):
async fn forward_mutation(&self, caller: RuntimeCaller, request: MutationRequest)
-> Result<MutationReceipt, RuntimeError>;
where MutationRequest carries the typed MailOperation (parsed once at the
wire, dispatched by exhaustive match), not a stringly name/args pair.
Runtime identifiers are opaque #[serde(transparent)] newtypes over strings
(RuntimeLinkId, ViewId, SubscriptionId, ClientMutationId,
RuntimeMutationId), defined in posthaste-contract-core; they expose
new/as_str and no parsing that infers account, provider, mailbox, message, or
storage facts. Revisions are numeric ViewRevision/RuntimeLinkSeq newtypes.
Contract payloads serialize camelCase. Build-time failures use typed
RuntimeBuildError; runtime-call failures use the adapter-safe
RuntimeError(RuntimeAdapterError) — never a transport type.
2. Authority build order¶
build_authority_runtime(RuntimeBuildConfig) accepts pre-resolved
config_root/state_root/cache_root and assembles in this order:
- create state and cache roots
- open the TOML config repository (init defaults/smart mailboxes, or import
bootstrap_path, when empty) - open the SQLite store at
state_root/mail.sqlite - build
MailServiceand sync source projections - create the runtime event broadcast channel
- build and start the authority-server
AccountSupervisor - build
RuntimeHandle,RuntimeShutdownHandle, and initialRuntimeStatus
The builder binds no socket, builds no router, creates no webview, and reads no
renderer state. Production uses the default SystemSecretStore; tests inject a
mock SecretStore.
3. Runtime status and the liveness gate¶
RuntimeStatus reports only adapter-safe readiness: lifecycle state,
config/store/cache readiness booleans, and configured account count — never
filesystem roots, provider secrets, bearer tokens, SQLite handles, or keyring
references. runtime_status stays callable after shutdown and reports Stopped;
every other contract method (runtime-api and client-link alike) checks lifecycle
liveness first, allowing Ready/Degraded and rejecting
Starting/Stopping/Stopped with RuntimeNotReady. A separate
mutation-service-availability gate distinguishes "runtime stopped" from "mutation
runtime not wired".
4. Account, settings, and OAuth contracts¶
Account CRUD/lifecycle/reload/delete and settings/smart-mailbox mutations use
transport-neutral request/result structs in posthaste-contract-core (the
mutation_args families; shared domain-model types, but no Axum extractors or
HTTP-status concepts); posthaste-authority-server owns the side effects:
config/service writes, secret-store writes, automation backfill checks, and event
append/broadcast. Account delete methods own the full resource linkage — managed
secret deletion, supervisor removal, config deletion, and the account-delete
event — while adapters handle only transport-owned assets (e.g. deleting an
uploaded logo file after runtime deletion succeeds).
OAuth keeps HTTP concerns (validation, pending-flow state, authorization-code
exchange, redirect/HTML) in posthaste-server; after token exchange the adapter
calls handle methods to encode token sets, write managed secrets, create
provider-first IMAP/SMTP OAuth accounts or patch existing accounts to OAuth2,
publish account events, and drive supervisor lifecycle.
5. Provider reads and event subscription¶
Provider-backed reads use the authority-server LiveAccountRuntimeProvider seam
and the authority supervisor, not route-owned gateway lookup. Mail-list/search
reads use query_mail_page with MailQueryRequest { query, presentation,
visibility }; the query string is the universal selector (in:Inbox →
saved smart mailbox, in:Account/Archive → concrete source mailbox,
conversation:<id> → conversation members). The runtime owns query
parsing/evaluation, saved-query expansion, cache-maintenance triggers, and event
publication; adapters only shape the transport response.
subscribe_events owns the durable replay query and the live broadcast receiver:
it subscribes before querying backlog, returns RuntimeEventSubscription { replay,
live }, filters both with EventFilter, and suppresses live sequences already
covered by replay. Adapters map each DomainEvent to their wire format and never
own replay/subscription logic.
6. Security hardening¶
A loopback bridge in bundled mode is a local capability channel, not the authority
boundary. When present it: binds loopback/local-only; uses an unguessable
process-local token required in an authorization header (query-token auth
rejected); validates Host before route exemptions; validates origin/referer when
present; restricts CORS to the packaged app and configured dev origins; keeps
tokens in memory and writes no daemon port/token file in bundled mode; and serves
no provider secrets to JavaScript.
The desktop host exposes only the named Tauri commands the renderer needs — no generic filesystem, shell, process, network, or environment access — and every command validates input with deny-unknown-field semantics, treating descriptors, identifiers, and URL parameters as untrusted. External URLs open only for allowed schemes (http/https) in the system browser; untrusted in-app navigation is blocked; email body content stays sandboxed/sanitized. Logs and diagnostics exclude secrets, tokens, capability URLs, Authorization headers, sensitive query strings, bodies, and attachment bytes. Resource handles/capability URLs are link-scoped, expire, and cannot be broadened by editing path/query. Account deletion and cache cleanup run as runtime mutations/maintenance; renderer storage cleanup requests runtime cleanup rather than deleting runtime roots directly.
7. Validation¶
Tests cover that the bundled app starts without a separate authority server; that the shared contract compiles without Axum/Tauri/React types and the handle builds without binding a listener, with both adapters calling the same contract; that provider secrets and client remote-profile tokens are not persisted in renderer-owned files, profile JSON, localStorage, logs, URLs, or packaged JavaScript; and that loopback tokens are header/capability-only, host validation precedes route exemptions, query-token auth is rejected, Tauri input rejects unknown fields, external URL schemes are restricted, and generic filesystem/shell capabilities are not exposed.
8. Assertions¶
| ID | Sev. | Assertion |
|---|---|---|
| authority-build-order | MUST | Authority server assembly opens config, store, service, projections, event channel, account supervisor, handle, and shutdown in the documented order. |
| no-adapter-startup-in-builder | MUST | The authority server builder does not bind sockets, build routers, create webviews, or read renderer state. |
| status-no-sensitive-fields | MUST | Runtime status does not expose filesystem roots, provider secrets, bearer tokens, SQLite handles, or keyring references. |
| lifecycle-gate-before-operations | MUST | runtime_status remains callable after shutdown; all other operations check lifecycle liveness and return RuntimeNotReady outside Ready/Degraded. |
| account-mutation-contract-pattern | MUST | Account mutation contracts stay transport-neutral and concrete side effects live in authority server methods. |
| account-resource-linkage-runtime-owned | MUST | Authority server delete methods own managed secret deletion, account runtime removal, config deletion, and account-delete event publication. |
| oauth-materialization-runtime-owned | MUST | OAuth callback token-set persistence and provider-first materialization are authority-server handle methods; adapters keep redirect/query/HTML shaping. |
| live-account-provider-runtime-owned | MUST | Provider-backed runtime reads use the authority-server LiveAccountRuntimeProvider seam and supervisor, not route-owned gateway lookup. |
| message-operations-provider-owned | MUST | Message/query runtime methods own provider gateway lookup, query evaluation, cache triggers, and event publication; query_mail_page serves mail-list/search and adapters add no route-shaped list methods. |
| event-subscription-runtime-backed | MUST | Event backlog replay and live broadcast subscription are runtime-owned; adapters map returned domain events to their transport wire shape. |
| contract-dependencies-transport-free | MUST | The contract crates (posthaste-contract-core, posthaste-runtime-api, posthaste-client-link) stay limited to transport-free support crates and posthaste-domain-model/posthaste-replica-core, free of Axum, Tauri, provider-client, SQLite, store-row, and frontend packages; posthaste-contract-core stays wasm-pure. |
| loopback-local-auth | MUST | Loopback bridge access is local-only, capability-protected, host-validated, origin/referer-checked, header-token-only (query-token rejected), and writes no daemon port/token file in bundled mode. |
| tauri-command-allowlist | MUST | The desktop host exposes only named required Tauri commands with validated untrusted input and no generic filesystem/shell/process/network/environment capability. |
| external-navigation-restricted | MUST | Renderer-provided external URLs open only for allowed schemes such as http/https; untrusted in-app navigation is blocked. |
| no-runtime-root-deletion-from-renderer | MUST | Renderer storage cleanup requests runtime cleanup through named mutations/maintenance rather than deleting runtime-owned roots directly. |