docs: add research docs for iOS app, Mac desktop, plugin security
- iOS: Capacitor vs WKWebView vs React Native WebView analysis - Mac: Tauri v2 vs Electron comparison with menu bar app patterns - Plugins: Signature validation, sandboxed iframes, permission system Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
290aa047d7
commit
4ac18cedb6
@@ -0,0 +1,113 @@
|
||||
# iOS App Research — AIUI
|
||||
|
||||
## Overview
|
||||
|
||||
Three approaches for shipping AIUI (Vue 3 + Vite SPA) as an iOS app.
|
||||
|
||||
## Approach 1: Capacitor (Recommended)
|
||||
|
||||
Capacitor wraps the Vite build output (`dist/`) in a native iOS Xcode project. The web app runs inside WKWebView with a JavaScript bridge to native device APIs.
|
||||
|
||||
```bash
|
||||
pnpm add @capacitor/core @capacitor/cli @capacitor/ios
|
||||
npx cap init && npx cap add ios
|
||||
pnpm build && npx cap sync
|
||||
npx cap open ios # opens Xcode
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Near-zero code changes to existing Vue 3 app — one codebase for web + iOS + Android
|
||||
- Large, mature plugin ecosystem (camera, biometrics, push, geolocation, haptics)
|
||||
- Hot reload during dev via `npx cap run ios --livereload`
|
||||
- OTA live updates possible via Capgo, bypassing App Store review for JS changes
|
||||
- `@capacitor/push-notifications` wraps APNs natively
|
||||
|
||||
**Cons:**
|
||||
- Service workers do NOT work in WKWebView on iOS (capacitor:// protocol breaks SW registration)
|
||||
- Performance ceiling is WebKit JS engine (not V8)
|
||||
- Each iOS SDK bump requires Capacitor + plugin updates
|
||||
|
||||
**Push Notifications:** Full support via `@capacitor/push-notifications` (APNs). Production-grade.
|
||||
|
||||
**Offline:** Entire app bundle ships inside .ipa — available offline. Dynamic data must use `@capacitor/preferences` or local SQLite. Workbox/SW caching does not work.
|
||||
|
||||
**Performance:** Modern WKWebView uses Nitro JS engine (same as Safari). For a chat UI like AIUI, indistinguishable from Safari. GPU-accelerated CSS transforms work well.
|
||||
|
||||
## Approach 2: Custom WKWebView Swift Wrapper
|
||||
|
||||
Write a native Swift/SwiftUI app embedding WKWebView. Use `WKScriptMessageHandler` for JS↔Swift communication.
|
||||
|
||||
**Pros:**
|
||||
- Maximum native control — own the shell, native navigation, gestures
|
||||
- Can implement App Clips, Share Extensions, Widgets alongside web content
|
||||
- Full access to all iOS APIs at the native layer
|
||||
|
||||
**Cons:**
|
||||
- Requires Swift knowledge — adds second language + build system
|
||||
- JS↔Swift bridge must be hand-written for every integration
|
||||
- No structured plugin community; each integration is bespoke
|
||||
- More setup friction vs Capacitor
|
||||
|
||||
**Push/Offline/Performance:** Same as Capacitor (all use WKWebView). More manual setup.
|
||||
|
||||
## Approach 3: React Native WebView
|
||||
|
||||
Create a React Native app with `react-native-webview` rendering the Vite build output.
|
||||
|
||||
**Pros:**
|
||||
- RN has deep native API access and large ecosystem
|
||||
- Surrounding shell can be fully native
|
||||
|
||||
**Cons:**
|
||||
- Two separate tech stacks (Vue + RN) — highest maintenance burden
|
||||
- No code sharing between Vue app and RN shell
|
||||
- Performance often worse (full RN runtime + WebView engine)
|
||||
- RN's own breaking changes cadence adds risk
|
||||
|
||||
**Verdict:** Only justified if an existing RN app is already in production.
|
||||
|
||||
## App Store Risk: Guideline 4.2
|
||||
|
||||
Apple's Guideline 4.2 (Minimum Functionality) is the primary risk for all webview-based apps. Apps that pass share these traits:
|
||||
- Native tab bar or navigation (not web-based menus)
|
||||
- At least one native API integration (push, biometrics, camera, Apple Pay)
|
||||
- Offline functionality beyond what a browser bookmark offers
|
||||
- UI formatted for iOS, not a desktop website in a phone frame
|
||||
|
||||
For AIUI: the chat interface, push notifications, and offline message history constitute sufficient native functionality.
|
||||
|
||||
## Service Workers in WKWebView
|
||||
|
||||
**SWs do not run inside WKWebView** — this is a fundamental WebKit limitation, not framework-specific. The correct offline strategy for all three approaches: ship assets in app bundle + implement dynamic caching via native storage APIs.
|
||||
|
||||
## Deep Linking
|
||||
|
||||
All three support iOS Universal Links via AASA file + Associated Domains capability:
|
||||
- **Capacitor:** `@capacitor/app` `appUrlOpen` event → Vue Router
|
||||
- **Custom WKWebView:** `AppDelegate.application(_:continue:...)` → JS evaluation
|
||||
- **RN:** React Navigation linking config → WebView `postMessage`
|
||||
|
||||
## Comparison
|
||||
|
||||
| Dimension | Capacitor | Custom WKWebView | RN WebView |
|
||||
|---|---|---|---|
|
||||
| Vue code reuse | 100% | 100% | 100% |
|
||||
| Native shell effort | Low | High | Very high |
|
||||
| Push notifications | First-class | Manual APNs | Via RN layer |
|
||||
| App Store risk | Moderate* | Moderate* | Moderate* |
|
||||
| Performance | Good | Good | Adequate |
|
||||
| Maintenance burden | Low-moderate | High | Very high |
|
||||
| Team fit (web-first) | Best | Poor | Poor |
|
||||
|
||||
*All face identical Guideline 4.2 scrutiny — framework choice is irrelevant to reviewers.
|
||||
|
||||
## Concrete Next Steps
|
||||
|
||||
1. Add `@capacitor/core`, `@capacitor/cli`, `@capacitor/ios` to `packages/app`
|
||||
2. Set Vite `base: './'` for the Capacitor build config
|
||||
3. Disable PWA service worker for native builds (partially done already)
|
||||
4. Add `@capacitor/push-notifications` for APNs
|
||||
5. Implement native splash screen and app icon
|
||||
6. Test on iOS Simulator via `npx cap run ios`
|
||||
7. Set up Apple Developer account + code signing
|
||||
8. Submit TestFlight build for internal testing
|
||||
@@ -0,0 +1,119 @@
|
||||
# Mac Desktop App Research — AIUI
|
||||
|
||||
## Overview
|
||||
|
||||
Two approaches for shipping AIUI as a Mac desktop app: Tauri v2 (Rust-based, system WebView) vs Electron (Chromium-based).
|
||||
|
||||
## Tauri v2 (Recommended)
|
||||
|
||||
Released stable October 2024. Uses OS-native WebView (WKWebView on macOS). The Vue 3 + Vite frontend runs inside the WebView unchanged. JS calls into Rust via typed IPC bridge.
|
||||
|
||||
**Binary Size:** 2–8 MB installer (no bundled runtime)
|
||||
**Memory Usage:** ~30–40 MB idle
|
||||
**Startup Time:** < 500ms
|
||||
|
||||
### Menu Bar App Pattern (Raycast-style)
|
||||
|
||||
Fully supported via `tauri-plugin-positioner` + tray + window APIs. Frameless popover window anchored to tray icon with `decorations: false`, `skip_taskbar: true`. Community examples exist (`ahkohd/tauri-macos-menubar-app-example` v2-popover branch).
|
||||
|
||||
### Global Hotkey
|
||||
|
||||
Built-in via `@tauri-apps/plugin-global-shortcut`. Register accelerators (e.g., `CmdOrCtrl+Space`) that fire even when background/minimized. First-class plugin.
|
||||
|
||||
### System Tray
|
||||
|
||||
First-class support. `AppHandle::tray()` with native menus and click event handling from Rust or frontend.
|
||||
|
||||
### Auto-Update
|
||||
|
||||
`@tauri-apps/plugin-updater` — signed updates required (Ed25519 keypair). Host a static JSON endpoint with version metadata and signed artifact URLs.
|
||||
|
||||
### macOS Code Signing / Notarization
|
||||
|
||||
Automated via Tauri CLI environment variables (`APPLE_CERTIFICATE`, `APPLE_SIGNING_IDENTITY`, `APPLE_ID`, `APPLE_TEAM_ID`). Notarization adds ~2–5 min per build.
|
||||
|
||||
### Build Pipeline
|
||||
|
||||
- Prerequisites: Rust toolchain + Xcode CLI tools
|
||||
- First build: 5–15 min (Cargo compiles Rust deps)
|
||||
- Incremental builds: Fast with caching
|
||||
- Config: `tauri.conf.json` + `Cargo.toml`
|
||||
- Complexity: Medium-High (Rust requirement is the barrier)
|
||||
|
||||
### Mobile Support
|
||||
|
||||
Tauri v2 has **first-class iOS/Android support** in the same codebase (WKWebView on iOS, Android System WebView on Android). HMR extends to physical devices. This is a genuine differentiator — Electron is desktop-only.
|
||||
|
||||
## Electron
|
||||
|
||||
Mature since 2013. Bundles full Chromium + Node.js runtime. Used by VS Code, Slack, Discord, Obsidian.
|
||||
|
||||
**Binary Size:** 80–150 MB installer
|
||||
**Memory Usage:** 200–350 MB idle
|
||||
**Startup Time:** 1–2s
|
||||
|
||||
### Menu Bar App
|
||||
|
||||
Well-established via `menubar` npm package. Creates BrowserWindow positioned below tray icon, manages show/hide on tray click. Very mature.
|
||||
|
||||
### Global Hotkey
|
||||
|
||||
`globalShortcut` module in Electron core. System-wide even when hidden.
|
||||
|
||||
### System Tray
|
||||
|
||||
`Tray` class in Electron core with context menus and click events.
|
||||
|
||||
### Auto-Update
|
||||
|
||||
`electron-updater` (S3/GitHub Releases) or `update.electronjs.org` (free for open-source).
|
||||
|
||||
### macOS Code Signing / Notarization
|
||||
|
||||
Via `@electron/osx-sign` + `@electron/notarize`, integrated into `electron-builder` / Electron Forge.
|
||||
|
||||
### Build Pipeline
|
||||
|
||||
- Prerequisites: Node.js only — no additional runtimes
|
||||
- Build tools: `electron-vite` for Vue 3 + Vite integration
|
||||
- Build times: 2–5 min (no Rust compilation) + 2–5 min notarization
|
||||
- Complexity: Medium (main/renderer process split requires understanding)
|
||||
|
||||
## Comparison
|
||||
|
||||
| Dimension | Tauri v2 | Electron |
|
||||
|---|---|---|
|
||||
| Installer size | 2–8 MB | 80–150 MB |
|
||||
| Idle RAM | 30–40 MB | 200–350 MB |
|
||||
| Startup time | < 500ms | 1–2s |
|
||||
| Menu bar app | Supported | Supported |
|
||||
| Global hotkey | Built-in plugin | Built-in API |
|
||||
| System tray | Built-in | Built-in |
|
||||
| Auto-update | Built-in (signed) | electron-updater |
|
||||
| New language | Rust | None (JS/TS) |
|
||||
| iOS/Android | Yes (same codebase) | No |
|
||||
| WebView | WKWebView (varies by OS) | Chromium (pinned, consistent) |
|
||||
| Ecosystem maturity | Growing fast | Very mature |
|
||||
| Security model | Capability-based, opt-in | Opt-out, manual discipline |
|
||||
| Debug tools | Safari Web Inspector | Chrome DevTools |
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Tauri v2 is the stronger choice for AIUI:**
|
||||
|
||||
1. **Memory advantage is decisive.** Users running local LLMs or managing API streaming need resources for the AI workload, not the shell. 30 MB vs 300 MB matters.
|
||||
2. **Menu bar pattern fits naturally** for a chat/AI assistant (Raycast-style quick invoke).
|
||||
3. **iOS/Android support** from the same codebase aligns with AIUI's multi-surface vision.
|
||||
4. **Capability-based security** is appropriate for handling API keys and sensitive chat data.
|
||||
5. **Binary size matters** — 5 MB download vs 120 MB affects distribution trust.
|
||||
|
||||
## Concrete Next Steps
|
||||
|
||||
1. Scaffold Tauri v2 project: `npm create tauri-app@latest` with Vite template
|
||||
2. Point dev server to existing `packages/app` Vite config
|
||||
3. Implement tray icon + menu bar popover window
|
||||
4. Register global hotkey (e.g., `Cmd+Shift+Space`) to invoke chat
|
||||
5. Write Rust commands for: file I/O, tray management, updater config
|
||||
6. Set up macOS code signing + notarization pipeline
|
||||
7. Distribute via Homebrew cask or direct download
|
||||
8. Evaluate Tauri mobile targets for iOS/Android convergence
|
||||
@@ -0,0 +1,209 @@
|
||||
# Plugin System Hardening Research — AIUI
|
||||
|
||||
## Current State
|
||||
|
||||
The plugin system has these existing components:
|
||||
- `packages/core/src/types/plugin.ts` — `AIUIPlugin` interface, `PluginContext`, `PluginType`
|
||||
- `packages/core/src/plugins/registry.ts` — in-memory Vue ref-based registry
|
||||
- `packages/app/src/stores/pluginMarketplace.ts` — `InstalledPlugin`, `PluginPermission`, `installPlugin`, `hasPermission`
|
||||
- `packages/app/src/components/settings/PluginMarketplace.vue` — permissions dialog
|
||||
- `packages/app/src/components/renderers/CodeRunner.vue` — existing `<iframe sandbox="allow-scripts">` + postMessage pattern
|
||||
|
||||
**Gaps:** No cryptographic signature verification, no runtime permission enforcement in sandbox, no CSP on plugin iframes.
|
||||
|
||||
## 1. Signature Validation for Community Plugins
|
||||
|
||||
### Problem
|
||||
|
||||
`importFromUrl` fetches arbitrary `aiui-plugin.json` from any URL with no integrity check. A compromised URL grants arbitrary code execution.
|
||||
|
||||
### Recommended: Ed25519 Detached Signatures
|
||||
|
||||
Aligns with existing crypto posture (tweetnacl.js for E2E, Web Crypto for storage).
|
||||
|
||||
**Manifest format:**
|
||||
```json
|
||||
{
|
||||
"id": "my-plugin",
|
||||
"name": "My Plugin",
|
||||
"version": "1.2.0",
|
||||
"signature": {
|
||||
"algorithm": "ed25519",
|
||||
"publicKey": "base64-public-key",
|
||||
"value": "base64-signature-over-canonical-manifest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verification:** Canonical payload = manifest JSON minus `signature` field, sorted keys. Verify via `crypto.subtle.verify('Ed25519', ...)` (Chrome 113+, Firefox 130+, Safari 17+) with `tweetnacl.js` fallback.
|
||||
|
||||
**Integration point:** Gate `installPlugin()` on signature verification for Tier 2+ plugins.
|
||||
|
||||
### Secondary: SRI Hash
|
||||
|
||||
`bundleUrl` + `integrity` field enables browser-native subresource integrity enforcement at load time.
|
||||
|
||||
### Trust Model
|
||||
|
||||
| Tier | Who | Signing | Sandbox | Verification |
|
||||
|---|---|---|---|---|
|
||||
| 1 — Built-in | AIUI maintainers | Bundled in app | None (trusted) | None needed |
|
||||
| 2 — Verified | Reviewed by AIUI team | Ed25519 by author | Full iframe sandbox | Signature + hash |
|
||||
| 3 — Sideloaded | User-imported URL | Optional | Full iframe sandbox, stricter CSP | Warn prominently |
|
||||
|
||||
## 2. Sandboxed iframe Execution
|
||||
|
||||
### Architecture
|
||||
|
||||
Each community plugin runs in an isolated iframe. AIUI manages a `PluginBridge` service:
|
||||
|
||||
```
|
||||
AIUI App (parent)
|
||||
│
|
||||
│ postMessage (structured protocol)
|
||||
│
|
||||
└── Plugin Sandbox iframe (origin: null, sandbox="allow-scripts")
|
||||
└── Plugin code (no DOM, no storage, no cross-origin network)
|
||||
```
|
||||
|
||||
### Sandbox Configuration
|
||||
|
||||
```html
|
||||
<!-- Tier 2 plugin (headless, no UI) -->
|
||||
<iframe sandbox="allow-scripts" srcdoc="..." style="display: none" />
|
||||
|
||||
<!-- Tier 2 plugin with UI panel -->
|
||||
<iframe sandbox="allow-scripts allow-popups-to-escape-sandbox" srcdoc="..." />
|
||||
```
|
||||
|
||||
**Never grant:** `allow-same-origin` (breaks isolation), `allow-forms`, `allow-top-navigation`, `allow-modals` unless explicitly user-approved.
|
||||
|
||||
### postMessage Protocol
|
||||
|
||||
Typed message envelopes following the archyBridge pattern:
|
||||
|
||||
```typescript
|
||||
// Plugin → Host
|
||||
interface PluginRequest {
|
||||
type: 'plugin:request'
|
||||
id: string // correlation ID
|
||||
pluginId: string
|
||||
capability: string // e.g. 'storage:get', 'network:fetch'
|
||||
payload: unknown
|
||||
}
|
||||
|
||||
// Host → Plugin
|
||||
interface PluginResponse {
|
||||
type: 'plugin:response'
|
||||
id: string
|
||||
success: boolean
|
||||
data?: unknown
|
||||
error?: string
|
||||
}
|
||||
```
|
||||
|
||||
### Host-side Validation
|
||||
|
||||
1. `event.origin` must be `null` (sandboxed srcdoc iframes)
|
||||
2. `pluginId` must match the iframe→plugin mapping
|
||||
3. Requested capability must be in `grantedPermissions`
|
||||
4. Rate-limit: reject if > N requests/second (DoS prevention)
|
||||
|
||||
### Network Restriction
|
||||
|
||||
With `sandbox="allow-scripts"` alone, iframes can still `fetch()`. Block direct network via CSP in srcdoc:
|
||||
|
||||
```html
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'unsafe-inline'; connect-src 'none'">
|
||||
```
|
||||
|
||||
Plugins requiring network use the `network:fetch` capability — host proxies the request after validating the URL.
|
||||
|
||||
### Storage Isolation
|
||||
|
||||
Sandboxed iframes without `allow-same-origin` cannot access parent's localStorage/IndexedDB. Plugin storage goes through the `storage` capability, namespaced under `plugin::{id}::`.
|
||||
|
||||
## 3. Permission System Per Plugin
|
||||
|
||||
### Expanded Permissions
|
||||
|
||||
```typescript
|
||||
export type PluginPermission =
|
||||
| 'chat-read' // read chat history
|
||||
| 'chat-inject' // inject messages (high risk)
|
||||
| 'chat-messages' // read + inject (legacy, maps to both)
|
||||
| 'network' // outbound HTTPS via host proxy
|
||||
| 'favorites' // read/write favorites
|
||||
| 'storage' // namespaced plugin storage
|
||||
| 'nostr' // access Nostr identity (high risk)
|
||||
| 'wallet' // deep-link to wallet (high risk)
|
||||
| 'clipboard' // read/write clipboard
|
||||
| 'notifications' // send notifications
|
||||
| 'media-playback' // control media player
|
||||
| 'renderer' // register content renderer
|
||||
| 'settings-read' // read AIUI settings
|
||||
```
|
||||
|
||||
### Risk Classification
|
||||
|
||||
```typescript
|
||||
const permissionRisk: Record<PluginPermission, 'low' | 'medium' | 'high'> = {
|
||||
'network': 'low',
|
||||
'storage': 'low',
|
||||
'chat-read': 'low',
|
||||
'favorites': 'low',
|
||||
'notifications': 'medium',
|
||||
'clipboard': 'medium',
|
||||
'media-playback': 'medium',
|
||||
'renderer': 'medium',
|
||||
'settings-read': 'medium',
|
||||
'chat-messages': 'high',
|
||||
'chat-inject': 'high',
|
||||
'nostr': 'high',
|
||||
'wallet': 'high',
|
||||
}
|
||||
```
|
||||
|
||||
### User Consent Flow
|
||||
|
||||
1. High-risk permissions default to unchecked in consent dialog
|
||||
2. Show risk badges (low/medium/high) next to each permission
|
||||
3. Unverified plugins (Tier 3) show prominent warning before permissions dialog
|
||||
4. Progressive disclosure: summary before detailed checkboxes
|
||||
|
||||
### Permission Revocation
|
||||
|
||||
1. Terminate plugin iframe immediately (`iframe.remove()`)
|
||||
2. Host handler rechecks `hasPermission()` on every capability call
|
||||
3. Emit `plugin:permission-revoked` event so plugin can react gracefully
|
||||
|
||||
### Least Privilege
|
||||
|
||||
- Plugins declare minimum permissions in manifest
|
||||
- All capabilities gated on `hasPermission()` — no admin override
|
||||
- Storage namespaced under `plugin::{id}::`
|
||||
- Optional `allowedOrigins[]` in manifest restricts network targets
|
||||
- Audit log: capability invocations logged with plugin ID
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Files to modify:
|
||||
|
||||
**`packages/core/src/types/plugin.ts`** — Add `tier`, `signature`, `sandbox` fields to manifest type.
|
||||
|
||||
**`packages/app/src/stores/pluginMarketplace.ts`** — Add `verifyManifestSignature()` in `installPlugin()`. Add `revokePermission()`. Move `grantedPermissions` to encrypted storage.
|
||||
|
||||
**`packages/core/src/plugins/registry.ts`** — Evolve into `PluginHost` service that manages sandbox iframe lifecycle and routes postMessage capability requests.
|
||||
|
||||
**`packages/app/src/plugins/index.ts`** — Tier 1 plugins use `registerPlugin()` directly. Tier 2+ load through `PluginHost.loadSandboxed(manifest)`.
|
||||
|
||||
## Concrete Next Steps
|
||||
|
||||
1. Define `PluginSignature` type and `verifyManifestSignature()` using tweetnacl.js
|
||||
2. Create `PluginSandbox` service to manage iframe lifecycle + postMessage routing
|
||||
3. Add CSP meta tag to plugin srcdoc template
|
||||
4. Implement capability handlers (storage, network, chat) with permission checks
|
||||
5. Update consent dialog with risk classification badges
|
||||
6. Add `revokePermission()` with live iframe termination
|
||||
7. Create CLI tool for plugin authors to sign manifests
|
||||
Reference in New Issue
Block a user