Declarative Cloudflare-Hosted Provider Plugins
Declarative Cloudflare-Hosted Provider Plugins
Status: Delivered
CAS: CAS-3499
Delivered: 2026-05-18
PRs: #932, #954, #955, #963, #964, #965, #966, #967, #968, #971, #973
What’s new
You can now add, update, and fix providers without shipping an App Store release. Provider plugins are hosted on Cloudflare, downloaded over the air, and activated inside the app — but every plugin is declarative data only, never executable code, so the App Store policy boundary is respected. The same plugin runs identically on desktop and iOS. Each plugin is signed with an Ed25519 key, hash-verified, schema-validated, and checked against a revocation list before it can run.
How to use it
The feature is wired up and backend-ready; the full catalog UI (Phase D) is in the codebase and will be surfaced in Settings once the distribution endpoint is live. From the board’s perspective, the flow will be:
- Authoring: the provider recorder (desktop tooling) captures a bank’s traffic and emits a
plugin-manifest.jsonalongside the provider/processor configs. A separate publish step signs the bundle and pushes it to Cloudflare. - Discovery: the app fetches a signed
index.jsoncatalog from Cloudflare on a schedule and whenever you manually refresh. - Install: open the Plugin Catalog in Settings. Each available plugin shows the publisher, publish date, capabilities, and a verified/unverified signature badge. You choose which providers to enable.
- Update policy: you can set per-app update policy to manual (default) or auto for the stable channel. Pending updates must pass a mandatory dry-run gate before they activate; an update that fails dry-run is staged and surfaced for review, never silently activated.
- Offline: if the catalog is unreachable, already-installed verified providers keep running. A stale-catalog banner appears in the catalog page.
- Revocation: if a version is revoked by the household, the app shows a warning during a grace window and a hard-disable surface for confirmed-malicious versions.
What changed under the hood
- Plugin schema v1 (
src-tauri/src/services/provider_plugins/schema_v1.rs): typed Rust structs for plugin manifest, catalog, and trust root, withdeny_unknown_fieldsand strict length/regex-safety bounds at validation time. - Trust layer (
trust.rs): Ed25519 signature verification against a pinned trust-root bundle; supports key rotation via superseded-key list; active key pinned at compile time. - Distribution service (
provider_plugin_distribution_service.rs): fetch catalog → hash+sig verify → schema validate → stage → activate lifecycle; revocation checking; DB provenance ledger viaprovider_plugin_db.rsand migration53_create_provider_plugin_tables.sql. - Execution hardening (
Phase B): regex length caps + restricted-construct validation on all provider and prefetch patterns; URL template constraints; per-row diagnostics (why a row was dropped, which mapping missed) surfaced to the UI. - Catalog UX (
src/pages/PluginCatalogPage.tsx,src/store/usePluginCatalogStore.ts,src/components/PluginCatalog/): full React frontend — install cards, signature badges, stale-catalog banner, revocation warning banner, update policy controls, dry-run result modal, per-plugin enable/disable. - Recorder publish metadata (
provider_recorder_service.rs): recorder session stop now emitsplugin-manifest.jsonwith derived plugin ID, engine constraints, and redaction hints — ready input for the publish toolchain. - CI gate (
workers/fake-providers/,playwright.fake-providers.config.ts): a local-only Miniflare fixture (wrangler dev --localon:8791) serving fake banks + fake signed catalog; it is not deployed to any Cloudflare subdomain. 14 Playwright e2e scenarios run the full trust→download→verify→scrape chain in CI/local checks without real banks or a Mac.
Why we built it
Every provider (SEB, AMEX, etc.) was compiled into the app binary. Adding a new bank or fixing a parse error required a full release cycle — build, TestFlight, App Store review, wait. The declarative plugin model moves provider knowledge out of the binary and into signed data blobs on Cloudflare. New providers ship in hours, not weeks, and the same update mechanism that fixes a broken bank parser also delivers new providers to users who haven’t updated their app. The architecture was designed from the start to be safe for iOS App Store distribution: there is no downloaded executable code, no eval, no WASM — only typed JSON interpreted by a bundled execution engine that has been in the app since the original provider implementation.
Known limitations / follow-on work
- iOS port (Phase F) not yet started. The architecture is designed for it and the design risks are fully documented (§11.8 of the architecture doc), but Phase F is gated on a verified desktop round-trip with a real provider. No implementation CASes have been created yet.
- Publish toolchain not yet built. The recorder emits
plugin-manifest.jsonbut the signing CLI and Cloudflare deployment tooling are not part of this epic. A separate CAS will cover the publisher side. - Distribution endpoint URL not yet live. The backend and UX are complete; activation is blocked on the CF Worker + R2 bucket being provisioned and the production trust root being generated and bundled.
- Engine gap strategies not yet implemented. XHR-capture (
extract://xhr-capture), DOM-table scraping, and multi-page pagination are designed and have red CI gate tests, but the bundled extractor capabilities are deferred to follow-on engine CASes. - Local imported configs remain supported (the existing
UntrustedConfigWarningModalsurface) but are not yet migrated to the plugin schema; that parity work is deferred.