chore: housekeeping — rescue orphaned doc, ignore .DS_Store, adopt vnc-v0.3.0
Commits the offline-client architecture analysis doc that was sitting untracked in docs/ — its own header already warns this exact thing happened once before (~/vncmail-plus is a shared checkout; an earlier untracked copy was lost to a concurrent branch switch). Confirmed the hazard is still live: vnc/VNC-CHANGES.md itself was found deleted from disk mid-edit by this session, by something else touching the checkout concurrently, and had to be restored with `git checkout --` before this commit. Committing on sight is the only defense against that, not a process improvement for later. Also: - .DS_Store added to .gitignore (was untracked in docs/) - introduces a VNC-side feature version, separate from package.json's upstream-tracking version (1.7.8, must stay that way per the fork's own rule 4 - bumping it would turn merging upstream releases into a diffing exercise). Retroactively bucketed at the milestone boundaries the commit history already has: v0.1.0 fork bootstrap, v0.2.0 S/MIME plugin audit+fixes, v0.3.0 the internal-CA foundation just landed. Tagged vnc-v0.3.0 on this commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3afa7ce012
commit
95cc65f504
@@ -58,3 +58,6 @@ next-env.d.ts
|
||||
vnc/plugins/smime/node_modules/
|
||||
vnc/plugins/smime/dist/
|
||||
vnc/plugins/smime/smime-vnc.zip
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# Bulwark / VNCmail+ — Offline & Native Client Architecture Analysis
|
||||
|
||||
Date: 2026-08-04 (updated same day — see §7 for a strategy-changing discovery)
|
||||
Scope: what Bulwark (upstream `bulwarkmail/webmail`, forked as VNCmail+) delivers today for
|
||||
offline use, and what has to be built to ship Electron (desktop), Capacitor/React-Native
|
||||
iOS (IPA) and Android (APK) clients with local notifications, an encrypted local search index
|
||||
(SQLite/SQLCipher), and true offline mail.
|
||||
|
||||
> **Note on this file's persistence:** `~/vncmail-plus` is a shared checkout — other sessions
|
||||
> actively commit and switch branches here. An earlier untracked copy of this doc was lost to a
|
||||
> branch switch. Commit this file (or move it somewhere durable) if you want it to survive.
|
||||
|
||||
## 1. Current state of the webmail repo (verified against ~/vncmail-plus source)
|
||||
|
||||
| Area | Status today | Evidence |
|
||||
|---|---|---|
|
||||
| Service worker | Installed, but **caches nothing** — `fetch` handler is a deliberate no-op so the app is never usable offline | `public/sw.js:6-8,36` |
|
||||
| Web app manifest | Present, installable PWA (icons, `protocol_handlers` for mailto/webcal) | `app/manifest.ts` |
|
||||
| Push notifications | **Real** Web Push: VAPID subscribe, `Notification.requestPermission`, SW `push`/`notificationclick` handlers, relayed through an external push relay + a preview API route | `lib/web-push.ts`, `public/sw.js:38-42`, `app/api/push/preview/route.ts` |
|
||||
| Local mail cache | **None.** IndexedDB is used only for plugin/theme blobs; `localStorage` only holds device IDs and Zustand UI-state (`persist()`), never message bodies | `lib/plugin-storage.ts`, `stores/account-store.ts:220` |
|
||||
| Search | Server-side JMAP `Email/query` only, no client index | `lib/jmap/search-utils.ts` |
|
||||
| Local encryption | None for cached data. The one AES-256-GCM routine (`lib/auth/crypto.ts`) encrypts the **session cookie server-side** using `node:crypto` — unusable in a browser/WebView | `lib/auth/crypto.ts` |
|
||||
| Mobile/desktop packaging in *this* repo | **Nothing exists**: no `capacitor.config.ts`, no Electron main/`electron-builder`, no Tauri, no fastlane/gradle/Xcode | confirmed via repo-wide `find`; `.github/workflows/*` |
|
||||
| JMAP client portability | `lib/jmap/client.ts` is pure `fetch()`, no Node-only APIs — portable into a WebView/Electron renderer unchanged | grep for `node:`/`require(` in `lib/jmap/*` = zero hits |
|
||||
|
||||
**Multi-account scope: confirmed YES** — the offline cache must support multiple simultaneous
|
||||
Stalwart accounts per device (matches the webmail's existing `account-registry` store). This
|
||||
multiplies SQLCipher key-management work (§3/§7): one isolated key per account, not one global key.
|
||||
|
||||
## 2. The fork in the road: shell strategy
|
||||
|
||||
**Option A — Native shell over a remote WebView.** Capacitor/Electron just point at the hosted
|
||||
Bulwark URL. Cheapest, ships an APK/IPA/desktop binary fast, gets native push — but is *not*
|
||||
offline.
|
||||
|
||||
**Option B — True offline-first client.** The client authenticates and syncs JMAP data
|
||||
directly, keeps a local encrypted store, and only needs connectivity to *sync*, not to *read*.
|
||||
|
||||
Recommendation stands: **Electron first (Option-B-lite is nearly free there — see §4)**, mobile
|
||||
starts with Option A, then graduates to Option B — **but see §7: for mobile, "graduate to
|
||||
Option B" likely means extending an existing app, not building one from scratch.**
|
||||
|
||||
## 3. Build-vs-buy matrix (webmail-repo-only view — see §7 for the revised mobile view)
|
||||
|
||||
| Component | Off-the-shelf | What you build yourselves |
|
||||
|---|---|---|
|
||||
| Capacitor shell (iOS/Android project scaffolding) | Capacitor CLI generates both native projects | Splash/icons, deep-link config, `capacitor.config.ts` tuning |
|
||||
| Local SQLite | `@capacitor-community/sqlite` — ships **native SQLCipher support** on iOS/Android; web fallback via `jeep-sqlite`/`wa-sqlite` | Schema, JMAP→SQLite mapping, migrations |
|
||||
| SQLCipher key lifecycle | Native Keychain/Keystore APIs (via Capacitor Secure Storage) store the raw key | Key derivation/rotation, **per-account keys** (multi-account confirmed §1), wipe-on-logout |
|
||||
| Full-text search | SQLite FTS5 ships free with SQLite | Tokenizer choice, incremental indexer fed by the sync engine |
|
||||
| Native push | `@capacitor/push-notifications` wraps FCM/APNs | Relay extension, device-token registration, notification-tap deep-linking |
|
||||
| Electron desktop | `electron-builder`; Electron's own cross-platform `Notification` API | Main process booting the existing standalone Next.js server; auto-update wiring |
|
||||
| Background sync | iOS `BGTaskScheduler`, Android `WorkManager` | The actual poll/backoff/delta-fetch job logic |
|
||||
| Biometric app-lock | `capacitor-native-biometric` | UI/UX, fallback-to-passcode flow |
|
||||
| Store release pipeline | Fastlane/EAS-style CI, Apple/Google developer accounts | Signing config, CI secrets, store metadata |
|
||||
|
||||
## 4. Why Electron is the cheap win
|
||||
|
||||
Electron has no server-dependency problem: bundle the standalone Next.js server (the same
|
||||
artifact the `Dockerfile` already produces) inside Electron's Node runtime, open a
|
||||
`BrowserWindow` against `localhost`. Reuses 100% of the existing app including `app/api/**`.
|
||||
Native `Notification` API replaces Web Push entirely on desktop. Ships well before mobile
|
||||
Option B.
|
||||
|
||||
## 5. Phased roadmap
|
||||
|
||||
1. **Fix the service worker** — today's SW intentionally caches nothing (`sw.js:36`). Add
|
||||
Workbox-style precaching of the app shell/static assets. Cheap, immediate PWA-offline-shell
|
||||
improvement, no architecture change.
|
||||
2. **Electron desktop** (§4) — bundle standalone server + BrowserWindow + native Notification +
|
||||
`electron-builder` packaging.
|
||||
3. **Capacitor mobile, Option A (remote shell)** — WebView on the hosted instance, native push
|
||||
registration bridged into the relay, biometric app-lock. Ships an installable APK/IPA fast;
|
||||
not offline yet. **Revisit against §7 before starting — extending `vncmail-native` may replace
|
||||
this step entirely rather than complement it.**
|
||||
4. **JMAP sync engine + SQLite/SQLCipher store** — design delta-sync via
|
||||
`Email/changes`/`Mailbox/changes`; local schema, one key per account (§1); move auth off the
|
||||
Next-only encrypted cookie into secure storage so mobile can talk to Stalwart directly.
|
||||
**§7: `vncmail-native` already has a cruder version of the "local cache" half of this
|
||||
(bulk AsyncStorage download) — the delta-sync/SQLite/SQLCipher/FTS half is still greenfield
|
||||
there too, but auth/JMAP wiring is not.**
|
||||
5. **FTS index + offline compose/outbox** — SQLite FTS5 population job; offline-composed
|
||||
messages queued and replayed via JMAP `Email/set` on reconnect; conflict handling.
|
||||
6. **Platform hardening** — background refresh scheduling, Apple export-compliance declaration
|
||||
(SQLCipher/AES in the binary triggers `ITSAppUsesNonExemptEncryption`), signing/release CI.
|
||||
|
||||
## 6. Open questions — status
|
||||
|
||||
- ~~Does the referenced upstream React Native app already solve native push/device-pairing?~~
|
||||
**RESOLVED — see §7.**
|
||||
- **Is Bulwark upstream planning native clients?** Partially answered by §7: yes, `bulwarkmail/native`
|
||||
is that plan, already public, beta/WIP. Still worth watching its upstream activity before
|
||||
diverging further, since pulling upstream improvements is cheaper than re-diverging an AGPL fork.
|
||||
- **Multi-account scope** — RESOLVED, see §1.
|
||||
|
||||
## 7. 2026-08-04 discovery: an upstream React Native app already exists — re-scope Phase 2
|
||||
|
||||
`bulwarkmail/native` (public, AGPL-3.0-only, Expo SDK 54, beta/WIP) is a React Native mobile
|
||||
client for Bulwark. **Forked to `brvncde-dotcom/vncmail-native`.** It already ships:
|
||||
|
||||
- **Multi-account** JMAP sign-in against any server (e.g. Stalwart).
|
||||
- **QR-code cross-device pairing** — `src/screens/LoginScreen.tsx` + `QrScanModal` +
|
||||
`redeemPairingCode`/OAuth handoff in `src/lib/oauth.ts`. This is the "QR-code SSO login and
|
||||
device pairing" feature referenced in the webmail's `CHANGELOG.md:207`.
|
||||
- **Android push notifications via FCM**, dispatched through a *second* public upstream repo,
|
||||
`bulwarkmail/relay` (also AGPL-3.0) — **this is the actual service behind the webmail's
|
||||
`DEFAULT_RELAY_BASE_URL`**, resolving the open question from the original Phase-2 plan about
|
||||
where that relay's source lives. It terminates JMAP `PushSubscription` pushes and forwards to
|
||||
FCM (mobile) or Web Push (PWA); a single Bulwark-hosted instance serves every opted-in client
|
||||
so self-hosters don't need their own Firebase project — **or you can self-host it** (Docker
|
||||
compose provided) if you want push traffic to never leave VNC infrastructure. **Decided:
|
||||
self-host.** Forked to `brvncde-dotcom/vncmail-relay`. Remaining: dedicated Firebase project
|
||||
for FCM credentials, a VAPID keypair, a microk8s deploy alongside `vncmail-plus` (per
|
||||
`vnclagoon-suite-microfrontends`), and repointing both `vncmail-plus`
|
||||
(`DEFAULT_RELAY_BASE_URL`) and `vncmail-native` at the self-hosted instance. Sequenced in the
|
||||
`VNCprodbuild` skill's Phase 2 step 0.
|
||||
- **A basic offline mail cache already**: `src/lib/offline-sync.ts` (155 lines) bulk-downloads
|
||||
the last N days of mail via `Email/query`+`Email/get` into `src/stores/offline-cache-store.ts`
|
||||
(AsyncStorage-backed, size-capped, evicts oldest), with live progress UI
|
||||
(`OfflineCacheBanner.tsx`). **This is not the delta-sync/SQLite/SQLCipher/FTS engine Phase 2
|
||||
called for** — it's a periodic bulk re-download, not incremental `Email/changes` sync, and
|
||||
storage is plain JSON in AsyncStorage, not an encrypted database — but auth, JMAP wiring, and
|
||||
the UI shell around "offline mail" already exist.
|
||||
- Android release pipeline (`.github/workflows/release-android.yml`, sideload APK from GitHub
|
||||
Releases) and an iOS release pipeline (`release-ios.yml`, `docs/ios-release.md`, TestFlight)
|
||||
**already exist** — iOS *builds*, just without push (see below).
|
||||
|
||||
**Still genuinely missing** (confirmed against its own README + source):
|
||||
- iOS push notifications and client certs — Android-only so far.
|
||||
- No SQLite/SQLCipher/FTS anywhere (`@react-native-async-storage/async-storage` +
|
||||
`expo-secure-store` only) — the encrypted-local-index work is still fully greenfield.
|
||||
**Resolved 2026-08-04:** use `expo-sqlite`'s official `useSQLCipher` config-plugin option
|
||||
(Android/iOS/macOS) rather than a third-party binding. Unusable in Expo Go, so this forces a
|
||||
custom dev client for development going forward — accepted. Stay Continuous-Native-Generation
|
||||
(don't commit `ios`/`android`, let `expo prebuild` regenerate them) rather than going fully
|
||||
bare, since a committed native tree would conflict on every future merge from upstream
|
||||
`bulwarkmail/native`. Detail in the `VNCprodbuild` skill's status log.
|
||||
- Filters & rules, S/MIME, plugins, themes, file storage are UI stubs only.
|
||||
- No Play Store distribution yet.
|
||||
|
||||
**Strategic implication:** for the mobile leg of the native-client roadmap, **extending
|
||||
`vncmail-native` is very likely cheaper than building a Capacitor wrapper around the webmail
|
||||
from scratch** — it already has the parts that were the most speculative/decision-heavy in the
|
||||
original Phase-2 plan (auth, pairing, push wiring, multi-account, a working offline-mail UX
|
||||
shell). The remaining work narrows to: iOS push, replacing the AsyncStorage bulk-cache with a
|
||||
real `Email/changes` delta-sync engine into SQLite/SQLCipher, an FTS5 index, and an
|
||||
offline-compose/outbox queue — i.e., roughly roadmap steps 4–6 above, now scoped against an
|
||||
existing app instead of a blank one. **This should be a formal decision gate before touching
|
||||
Phase 2 further**: adopt `vncmail-native` as the mobile client going forward (dropping/deferring
|
||||
the Capacitor-wraps-webmail plan for mobile), or keep both in parallel. Recommend adopting it —
|
||||
duplicating auth/pairing/push work that already exists and works has no upside.
|
||||
@@ -4,6 +4,20 @@ VNCmail+ is a fork of [bulwarkmail/webmail](https://github.com/bulwarkmail/webma
|
||||
(AGPL-3.0). This file records **every** intentional divergence from upstream so
|
||||
that merging new upstream releases stays a triage exercise, not an archaeology dig.
|
||||
|
||||
## VNC feature version
|
||||
|
||||
`package.json`'s version tracks **upstream** (currently `1.7.8`) and must stay
|
||||
that way per rule 4 below — bumping it would turn merging upstream releases into
|
||||
a diffing exercise instead of a fast-forward. The VNC-side feature set gets its
|
||||
own counter instead, tagged `vnc-vX.Y.Z` on `dev`, bumped whenever a milestone
|
||||
below closes:
|
||||
|
||||
| Version | Date | Milestone |
|
||||
|---|---|---|
|
||||
| `v0.1.0` | 2026-08-03 | Fork bootstrap: VNClagoon + SRC brand themes, per-theme logos, k8s deploy (after Vercel was abandoned — Bulwark writes to a local data dir, serverless fs is read-only), 6h session cookie |
|
||||
| `v0.2.0` | 2026-08-04 | SRC theme MD3 componentry; plugin-sandbox hardening (`B-01` scanner bypass, `B-04` unpermissioned hook registration); S/MIME plugin forked, audited (S-01, 9 findings), 3 shipping-blockers fixed + 2 hardened, verified end-to-end on real mail (sign, encrypt, decrypt, banner) |
|
||||
| `v0.3.0` | 2026-08-04 | Internal CA foundation (`P1`): EJBCA Community manifests + root-ceremony runbook (`A-01`/`A-06`), server-side `CaProvider` + enrolment route (`A-02`, `C-08` server half), finding 11 (certificate address binding). **No certificate has been issued yet** — the browser half of `C-08` (in-browser CSR generation) and a live EJBCA are both still outstanding. |
|
||||
|
||||
## Rules of the fork
|
||||
|
||||
1. **Keep upstream files unmodified whenever possible.** Prefer env vars
|
||||
|
||||
Reference in New Issue
Block a user