Skip to content

feat: Cloudflare Workers + R2 encrypted sync (BYO) - #268

Closed
mrchatam wants to merge 15 commits into
VoltiusApp:devfrom
mrchatam:feat/cloudflare-sync
Closed

mrchatam wants to merge 15 commits into
VoltiusApp:devfrom
mrchatam:feat/cloudflare-sync

Conversation

@mrchatam

Copy link
Copy Markdown

Summary

Adds a bring-your-own Cloudflare Worker + R2 encrypted sync path, complementary to GitHub Gist Sync and official Voltius cloud sync.

Architecture

Voltius client → local vault crypto → cloudflare-sync plugin → user Worker → R2
  • Encryption: api.crypto.deriveKey(passphrase, salt) + exportState / importStates (same as Gist)
  • Passphrase required — transport SYNC_TOKEN is never used as the KDF secret
  • Sync algorithm: pull changed remotes → importStates → push local (poll default 60s)
  • No changes to SSH core or src/services/sync.ts

Worker API

Method Path Auth
GET /health no
GET/PUT /v1/manifest Bearer
GET/PUT/DELETE /v1/devices/:id Bearer

R2 keys: manifest.json, devices/{id}.b64

Test plan

  • pnpm exec vitest run src/plugins/cloudflare-sync
  • pnpm exec vitest run tests/pluginCatalogPublish.test.ts (7 seeded plugins)
  • pnpm exec node scripts/build-plugins.mjs cloudflare-sync
  • Worker: pnpm --ignore-workspace test in examples/cloudflare-sync-worker
  • CI on this PR (Windows seven-plugin assert, full pnpm test / cargo)

Notes for maintainers

  1. Seeded vs marketplace: implemented as seeded (like Gist) so CI builds the bundle. Happy to move to marketplace-only if preferred.
  2. Host SyncDropdown is still Gist-specific; Cloudflare sync is driven from the plugin settings page (syncNow is exposed via plugins.expose).
  3. runtime.exportState currently stamps accountId: "gist-sync" into the blob header for all plugin exports — pre-existing; CF reuses the same API without changing that host path in this PR.

Add examples/cloudflare-sync-worker as a clean-room MIT Worker template
for BYO R2 vault sync (VoltiusApp#267). Phase 1: GET /health,
wrangler/vitest harness, and docs. Auth and R2 routes come next.
Require SYNC_TOKEN on /v1/* with timing-safe compare. Leave /health
public. Return misconfigured when the secret is unset. Refs VoltiusApp#267.
Implement /v1/manifest with schema validation (salt, devices).
Disable flaky vitest isolated R2 storage; tests clear the bucket.
Refs VoltiusApp#267.
GET/PUT/DELETE /v1/devices/:id with JSON {content,label,pushedAt},
manifest upsert, and path-safe device ids. Serialize vitest files to
avoid shared R2 races. Refs VoltiusApp#267.
OPTIONS preflight and Access-Control-* on all responses. Document the
MVP HTTP API in the worker README. Refs VoltiusApp#267.
Add plugin-cloudflare-sync mirrored on gist-sync: manifest, stub
sync-engine, settings placeholder, i18n, and register cleanup tests.
Refs VoltiusApp#267.
Typed helpers for health, manifest, and device blob routes with
WorkerApiError mapping. Refs VoltiusApp#267.
Require passphrase for KDF (never the transport token). Setup/link,
disconnect, device remove, pull-then-push syncNow, poll helpers, and
unit tests with mocked worker-api. Refs VoltiusApp#267.
Connection fields (Worker URL, token, passphrase), create/link/health,
sync now, poll interval, and disconnect. Refs VoltiusApp#267.
Register cloudflare-sync in build-plugins, Tauri SEEDED_IDS, and CI's
Windows plugin bundle assert (six → seven). Update catalogue publish
expectations. Refs VoltiusApp#267.
Show manifest devices with labels/timestamps and allow removing other
devices from the Worker vault. Refs VoltiusApp#267.
Document one-click deploy for examples/cloudflare-sync-worker, plus
setup:buckets / secret:token / deploy:easy scripts for CLI users.
Refs VoltiusApp#267.
@mrchatam

Copy link
Copy Markdown
Author

Deploy UX follow-up

Added a Deploy to Cloudflare button and a 3-command Wrangler path in examples/cloudflare-sync-worker/README.md (also linked from the plugin README).

Happy to adjust the button URL to `VoltiusApp/voltius`/`dev` once this lands.

Refuse Create vault when a remote vault already exists unless the user
confirms overwrite, require https Worker URLs, start poll after
configure, document the Worker threat model, and compare SYNC_TOKEN via
SHA-256 digests to avoid length leaks.
@mrchatam

Copy link
Copy Markdown
Author

Follow-up review (post live-test)

Answers

  • Voltius UI: yes — Cloudflare Sync settings page (Worker URL, sync token, passphrase, Create/Link, Sync now, poll, Disconnect, remote devices). Not wired into the host Gist Sync dropdown yet (known gap; plugin exposes syncNow).
  • How the client adds a Worker: deploy Worker → enable plugin → paste Worker URL + SYNC_TOKEN + separate encryption passphrase → Create vault (first device) or Link existing.
  • Worker UI for key management: intentionally none. Worker stays an opaque ciphertext store. Import/export/delete/add/edit of vault material + devices belongs in the client; rotate SYNC_TOKEN via Wrangler/dashboard. See examples/cloudflare-sync-worker/SECURITY.md.

Security review notes

  • E2EE: passphrase-only KDF; never derive from transport token.
  • Bearer auth on /v1/*; /health public.
  • Token compare: SHA-256 digests + constant-time byte compare (no raw-secret length early-exit).
  • Create vault refuses overwrite unless confirmed (avoids silent salt/manifest clobber).
  • Client requires https:// Worker URLs (http only for localhost).
  • Residual: shared SYNC_TOKEN = full R/W of ciphertext; manifest last-writer-wins races (same class as Gist); host SyncDropdown still Gist-specific; wrong passphrase fails at import time.

Fixes in latest commit (ec93ad86)

  • Overwrite guard + confirm on Create vault
  • HTTPS URL normalization
  • Start poll + sync after Create/Link
  • Worker SECURITY.md + auth harden
  • Extra unit tests

Ready for maintainer review when convenient. Happy to follow up on SyncDropdown host wiring if desired as a follow-up PR.

Wire plugin-cloudflare-sync into the titlebar, dropdown, settings, and
effective-status selector (Voltius > Cloudflare > Gist). Tag plugin
exportState blobs with the plugin id instead of hardcoded gist-sync.
Reject a wrong passphrase before leaving cloudflare-sync configured, and
CAS manifest RMW with ETag/If-Match (412 on mismatch) plus bounded
client pull+push retries.
@mrchatam

Copy link
Copy Markdown
Author

Follow-up: remaining gaps addressed

Pushed to feat/cloudflare-sync (HEAD d23fec95):

  1. Host Sync UI — TitleBar / Sync dropdown / Settings Sync section surface Cloudflare Sync. Sync Now calls the plugin’s exposed syncNow. Configure → plugin-cloudflare-sync:cloudflare-sync-settings. Precedence for the status indicator: Voltius Pro account sync → Cloudflare Sync → Gist Sync. Dropdown still lists all sections.
  2. Link passphrase probelinkExistingVault decrypts one remote device blob when any exist; wrong passphrase errors with a clear message and rolls back stored URL/token/passphrase. Empty vault skips the probe.
  3. Manifest concurrency — GET /v1/manifest returns ETag; PUT manifest / device RMW honor If-Match (412 on mismatch; missing manifest still 409). Client syncNow retries pull+push up to 3 times on 412/409.

Also: runtime.exportState now tags blobs with the plugin id (no longer hardcoded gist-sync).

Local verification: client targeted suites 67 passed; Worker vitest 20 passed.

Note: GitHub Actions ci.yml is configured for pull_requestdev, but this fork PR currently only shows GitGuardian. If Actions are pending first-time-contributor approval, a maintainer “Approve and run workflows” on this PR would kick off the repo CI.

@mrchatam

Copy link
Copy Markdown
Author

Closing per maintainer guidance on #267: prefer marketplace-only, not seeded in-app, and keep the Worker template in our repo rather than upstream examples/.

We’ll resubmit via VoltiusApp/marketplace once the standalone plugin + Worker are ready. Thanks for the review bandwidth — sorry for the misaligned delivery shape.

@mrchatam

Copy link
Copy Markdown
Author

Closing: pivoting to marketplace-only per #267 (kipavy).

@mrchatam mrchatam closed this Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant