super-productivity/docs/long-term-plans/sync-provider-plugins.md
Johannes Millan b6b51707d3 docs: clean up and organize project documentation
- Remove outdated feature requests from .github/CONTRIBUTING.md (GitLab
  support already exists) and add commit message format section
- Improve PR template with type-of-change checkboxes and checklist
- Update commit guideline links in README and CONTRIBUTING.md to
  reference the project's own format instead of external angular.js docs
- Add "only edit en.json" rule to TRANSLATING.md and clarify workflow
- Update add-new-integration.md provider list to match codebase (add
  Trello, ClickUp, Linear, Azure DevOps, Nextcloud Deck; note GitHub
  plugin migration; fix type name to BuiltInIssueProviderKey)
- Add cross-references between mac certificate docs and remove 240-line
  duplicate section from update-mac-certificates.md
- Clean up update-android-app.md (specify npm version args, collapse
  deprecated workflow, translate German UI labels to English)
- Add context to howto-refresh-snap-credentials.md
- Fix fine-grained token note in github-access-token-instructions.md
- Fix absolute URL to relative path in gitlab-access-token-instructions.md
- Fix grammar in i18n-script-usage.md
- Add status headers to all 19 long-term plan files (Planned, Completed,
  Archived with reason, Investigation Complete)
- Fix broken relative link in hybrid-manifest-architecture.md
- Delete supersync-scenarios-simplified.md (duplicate of
  supersync-scenarios.md; known issues already covered there)
- Rename vector-clock-pruning-research.md to
  vector-clock-history-and-alternatives.md for clarity
2026-03-10 10:22:56 +01:00

236 lines
8.7 KiB
Markdown

# Design: Sync Provider Plugins
> **Status: Planned**
## Goal
Enable community developers to build sync providers (Google Drive, OneDrive, S3, etc.) as plugins, using the existing plugin system's runtime loading and sandboxed execution.
## Design Decisions
- Plugins run in the **same sandbox** as regular plugins (no elevated trust)
- Plugins handle their **own auth UI** (OAuth flows, credential forms)
- Credentials stored via a **new `persistDataLocal()` API** (IndexedDB, never synced)
- **App manages encryption** — plugins only transport opaque bytes
- Built-in providers (Dropbox, WebDAV, LocalFile, SuperSync) **stay built-in for now**
- Plugin sync providers are always **file-based** (wrapped by `FileBasedSyncAdapterService`)
## Architecture
### 1. New Plugin API: `registerSyncProvider()`
Added to `PluginAPI` interface. A plugin calls this during initialization:
```javascript
plugin.registerSyncProvider({
id: 'google-drive',
label: 'Google Drive',
icon: 'cloud', // material icon name or inline SVG
// Core file operations
getFileRev: async (path, localRev) => {
// Return { rev: string } or throw if not found
},
downloadFile: async (path) => {
// Return { rev: string, dataStr: string }
},
uploadFile: async (path, dataStr, revToMatch, isForceOverwrite) => {
// Return { rev: string }
},
removeFile: async (path) => {},
// State
isReady: async () => true, // true if configured & authenticated
// Optional
listFiles: async (path) => [], // directory listing
isUploadForcePossible: true, // can force-overwrite on conflict
maxConcurrentRequests: 4, // concurrent upload/download limit
});
```
Only one sync provider per plugin. Calling `registerSyncProvider()` a second time replaces the first.
### 2. New Plugin API: `persistDataLocal()` / `loadLocalData()`
General-purpose local-only storage. Stored in IndexedDB, never synced.
```javascript
// Store credentials locally
await plugin.persistDataLocal(
JSON.stringify({
accessToken: '...',
refreshToken: '...',
}),
);
// Load on startup
const data = await plugin.loadLocalData();
const creds = data ? JSON.parse(data) : null;
```
Same constraints as `persistDataSynced()` (1 MB limit, rate limiting), but data stays on-device.
### 3. PluginSyncProviderAdapter
New class in `src/app/plugins/` that wraps plugin callbacks into `SyncProviderServiceInterface`:
```
src/app/plugins/plugin-sync-provider-adapter.ts
```
- Implements `SyncProviderServiceInterface<SyncProviderId>`
- Delegates file operations to plugin callbacks via `PluginBridgeService`
- `privateCfg` uses a no-op credential store (plugin manages its own creds)
- `isReady()` delegates to the plugin's `isReady()` callback
### 4. SyncProviderManager Changes
**File**: `src/app/op-log/sync-providers/provider-manager.service.ts`
Currently: static `SYNC_PROVIDERS` array populated at construction.
Changes:
- Add `registerPluginProvider(adapter: PluginSyncProviderAdapter)` method
- Add `unregisterPluginProvider(providerId: string)` method
- `SYNC_PROVIDERS` becomes a mutable list (or better: maintain a separate `pluginProviders` map)
- `SyncProviderId` enum extended with a dynamic/string approach for plugin IDs (e.g., `plugin:google-drive`)
- `activeProviderId$` and related observables react to plugin provider registration
### 5. Sync Settings UI Changes
**File**: `src/app/features/config/form-cfgs/sync-form.const.ts`
Currently: hardcoded provider dropdown options.
Changes:
- Provider dropdown dynamically includes registered plugin providers
- When a plugin provider is selected, instead of showing hardcoded form fields:
- Show a "Configure [Provider Name]" button
- Clicking it triggers the plugin's config UI (the plugin can use `plugin.openDialog()`, a side panel, or `plugin.showIndexHtmlAsView()`)
- Show connection status from the plugin's `isReady()` result
### 6. Lifecycle Handling
**Startup with plugin sync provider selected:**
1. App starts, loads sync config → selected provider is `plugin:google-drive`
2. `SyncProviderManager` sees unknown provider ID → `isProviderReady$` emits `false`
3. Plugin system loads and activates the Google Drive plugin
4. Plugin calls `registerSyncProvider(...)` → adapter registered with manager
5. Manager detects matching provider → `isProviderReady$` emits `true`
6. Sync begins
**Plugin disabled/uninstalled:**
1. `PluginService` calls cleanup → `unregisterPluginProvider('plugin:google-drive')`
2. `SyncProviderManager` removes the provider → `isProviderReady$` emits `false`
3. Sync stops
4. Settings UI shows warning: "Sync provider 'Google Drive' unavailable — enable the plugin or select another provider"
**Encryption:**
- Managed entirely by the app via `FileBasedSyncAdapterService`
- Encrypt key stored in app-level config (existing mechanism)
- Plugin never sees decrypted data and never handles the key
## Files to Modify
| File | Change |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `packages/plugin-api/src/types.ts` | Add `registerSyncProvider()` and `persistDataLocal()`/`loadLocalData()` to API types |
| `src/app/plugins/plugin-api.ts` | Implement new API methods |
| `src/app/plugins/plugin-bridge.service.ts` | Add bridge methods for sync provider registration and local data persistence |
| `src/app/plugins/plugin-cleanup.service.ts` | Unregister sync provider on plugin disable/unload |
| **New**: `src/app/plugins/plugin-sync-provider-adapter.ts` | Adapter wrapping plugin callbacks → `SyncProviderServiceInterface` |
| `src/app/op-log/sync-providers/provider-manager.service.ts` | Add `registerPluginProvider()` / `unregisterPluginProvider()`, dynamic provider list |
| `src/app/op-log/sync-providers/provider.const.ts` | Support dynamic plugin provider IDs alongside the enum |
| `src/app/features/config/form-cfgs/sync-form.const.ts` | Dynamic provider dropdown, "Configure" button for plugin providers |
| `src/app/plugins/store/` | Add reducer/actions for local plugin data persistence |
| `src/app/plugins/plugin-persistence.model.ts` | Add `PluginLocalData` model |
## New File
**`src/app/plugins/plugin-sync-provider-adapter.ts`**
Thin adapter that implements `SyncProviderServiceInterface` by delegating to plugin callbacks. ~50-80 lines.
## Verification Plan
1. **Unit test**: `PluginSyncProviderAdapter` correctly delegates all methods
2. **Unit test**: `SyncProviderManager` handles dynamic registration/unregistration
3. **Integration test**: Plugin registers → appears in settings dropdown → can be selected
4. **E2E test**: Build a test sync provider plugin that syncs to a local mock, verify full sync cycle works
5. **Edge case tests**: Plugin not loaded at startup, plugin disabled while active, plugin re-enabled
## Example Plugin
A minimal Google Drive sync plugin would look like:
```javascript
// manifest.json
{
"name": "Google Drive Sync",
"id": "google-drive-sync",
"version": "1.0.0",
"manifestVersion": 1,
"minSupVersion": "11.0.0",
"description": "Sync via Google Drive",
"hooks": [],
"permissions": ["syncProvider", "localData"]
}
// plugin.js
const GDRIVE_API = 'https://www.googleapis.com/drive/v3';
let credentials = null;
async function init() {
const data = await plugin.loadLocalData();
credentials = data ? JSON.parse(data) : null;
}
plugin.registerSyncProvider({
id: 'google-drive',
label: 'Google Drive',
icon: 'cloud',
maxConcurrentRequests: 4,
isUploadForcePossible: true,
isReady: async () => {
await init();
return !!credentials?.accessToken;
},
downloadFile: async (path) => {
// Use fetch() to call Google Drive API
// Return { rev, dataStr }
},
uploadFile: async (path, dataStr, revToMatch, isForceOverwrite) => {
// Upload to Google Drive
// Return { rev }
},
getFileRev: async (path, localRev) => {
// Check file metadata on Google Drive
// Return { rev }
},
removeFile: async (path) => {
// Delete file from Google Drive
},
});
// Auth UI via menu entry
plugin.registerMenuEntry({
label: 'Configure Google Drive Sync',
icon: 'settings',
onClick: async () => {
// Show auth dialog, store credentials
await plugin.persistDataLocal(JSON.stringify(credentials));
},
});
```