Skip to content

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:

  1. Authoring: the provider recorder (desktop tooling) captures a bank’s traffic and emits a plugin-manifest.json alongside the provider/processor configs. A separate publish step signs the bundle and pushes it to Cloudflare.
  2. Discovery: the app fetches a signed index.json catalog from Cloudflare on a schedule and whenever you manually refresh.
  3. 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.
  4. 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.
  5. Offline: if the catalog is unreachable, already-installed verified providers keep running. A stale-catalog banner appears in the catalog page.
  6. 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, with deny_unknown_fields and 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 via provider_plugin_db.rs and migration 53_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 emits plugin-manifest.json with 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 --local on :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.json but 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 UntrustedConfigWarningModal surface) but are not yet migrated to the plugin schema; that parity work is deferred.