578 lines
31 KiB
Markdown
578 lines
31 KiB
Markdown
# Phase 6: New Backends - Research
|
|
|
|
**Researched:** 2026-03-30
|
|
**Domain:** rclone backend registry extension — OneDrive, SFTP, GCS, Backblaze B2
|
|
**Confidence:** HIGH
|
|
|
|
<phase_requirements>
|
|
## Phase Requirements
|
|
|
|
| ID | Description | Research Support |
|
|
|----|-------------|-----------------|
|
|
| BACK-01 | User can configure an OneDrive remote (with OAuth token paste input and guidance) | OneDrive rclone type = `onedrive`; token field is a JSON blob; drive_id and drive_type required; SFTP-style toggle not needed (single auth path: paste token) |
|
|
| BACK-02 | User can configure an SFTP remote (host, user, password or key-based auth) | SFTP rclone type = `sftp`; auth method toggle needed (password vs key); CSS-hidden toggle follows AzureAuthToggle precedent; key field is `key_pem` for inline paste |
|
|
| BACK-03 | User can configure a Google Cloud Storage remote | GCS rclone type = `google cloud storage` (with spaces); service_account_credentials is the correct unattended-paste field (JSON blob); project_number needed for bucket listing |
|
|
| BACK-04 | User can configure a Backblaze B2 remote | B2 rclone type = `b2`; fields are `account` (applicationKeyId) and `key` (application key); simplest of the four — no toggle needed |
|
|
</phase_requirements>
|
|
|
|
## Summary
|
|
|
|
This phase adds four backend entries to `BACKEND_REGISTRY` and the corresponding `BACKEND_SCHEMAS` entries in `src/schemas/index.ts`. Because Phase 5 (TECH-03) wired `BackendSelectionStep` to `Object.entries(BACKEND_REGISTRY)`, adding an entry to the registry automatically surfaces a card in the UI — zero changes needed in `BackendSelectionStep`.
|
|
|
|
Three of the four backends (OneDrive, GCS, Backblaze B2) follow the existing "pure registry + FieldRenderer loop" pattern used for S3 and S3-compatible. SFTP is the only one that requires a custom auth-method toggle component (analogous to `AzureAuthToggle`) because users must choose between password and private-key authentication. The `SftpAuthToggle` component must follow the same CSS-hidden pattern as `AzureAuthToggle` to preserve both sets of field values in react-hook-form state when toggling.
|
|
|
|
`RemoteConfigStep` needs to be extended: the `backendLabel` object must be updated for the four new types, the Zod schema lookup must include the four new types, and SFTP needs a branch in the form rendering (similar to the `azureblob` branch today) to insert `SftpAuthToggle` instead of the plain FieldRenderer loop.
|
|
|
|
**Primary recommendation:** Implement in order: (1) registry entries for all four backends, (2) BACKEND_SCHEMAS extensions, (3) RCLONE_TYPE_MAP updates in rclone-conf.ts, (4) `SftpAuthToggle` component, (5) `RemoteConfigStep` extension. Write Wave 0 test stubs first for each.
|
|
|
|
## Standard Stack
|
|
|
|
### Core (already in use — no new installs)
|
|
|
|
| Library | Version | Purpose | Why Standard |
|
|
|---------|---------|---------|--------------|
|
|
| React | 18.3.1 | UI rendering, hooks, conditional render | Project baseline |
|
|
| TypeScript | 5.5.3 | Type safety for new BackendType union | Project baseline |
|
|
| react-hook-form | 7.72.0 | Form state, registry-driven field registration | Project baseline |
|
|
| zod | 4.3.6 | Schema derivation from registry fields | Project baseline |
|
|
| @hookform/resolvers | 5.2.2 | Connects Zod schemas to react-hook-form | Project baseline |
|
|
| Vitest | 4.1.1 | Test runner | Project baseline |
|
|
| @testing-library/react | 16.3.2 | Component testing | Project baseline |
|
|
| @testing-library/user-event | 14.6.1 | Async interaction simulation (act-safe) | Project baseline |
|
|
|
|
**Installation:** No new packages required.
|
|
|
|
## Architecture Patterns
|
|
|
|
### Recommended File Touch Map
|
|
|
|
```
|
|
src/
|
|
├── schemas/
|
|
│ ├── registry.ts # Add 'onedrive' | 'sftp' | 'gcs' | 'b2' to BackendType union; add 4 registry entries
|
|
│ └── index.ts # Add 4 entries to BACKEND_SCHEMAS; extend buildZodSchema calls
|
|
├── generators/
|
|
│ └── rclone-conf.ts # Add 4 entries to RCLONE_TYPE_MAP; gcs type has spaces
|
|
├── components/
|
|
│ ├── wizard/
|
|
│ │ ├── RemoteConfigStep.tsx # Extend backendLabel; add sftp branch with SftpAuthToggle; handle gcs/b2/onedrive via registry loop
|
|
│ │ └── SftpAuthToggle.tsx # NEW: password vs key-based auth toggle (follows AzureAuthToggle pattern)
|
|
│ └── ui/
|
|
│ └── (no new UI primitives needed — FieldRenderer + PasswordField cover all new fields)
|
|
├── tests/
|
|
│ ├── schemas/registry.test.ts # Update EXPECTED_BACKENDS; add 4 backend assertions
|
|
│ ├── generators/rclone-conf.test.ts # Add fixtures + assertions for all 4 new backends
|
|
│ └── components/wizard/RemoteConfigStep.test.tsx # Add describe blocks for 4 new backends
|
|
```
|
|
|
|
### Pattern 1: Registry Entry for a Simple Backend (B2 and SFTP non-toggle fields)
|
|
|
|
**What:** Add a `Record<BackendType, { displayName, description, fields: FieldDef[] }>` entry.
|
|
**When to use:** When all fields are rendered uniformly through the FieldRenderer loop (no custom toggle).
|
|
|
|
```typescript
|
|
// Source: Direct code audit of src/schemas/registry.ts (existing pattern)
|
|
b2: {
|
|
displayName: 'Backblaze B2',
|
|
description: 'Backblaze B2 Cloud Storage',
|
|
fields: [
|
|
{
|
|
key: 'account',
|
|
label: 'Application Key ID',
|
|
inputType: 'text',
|
|
required: true,
|
|
placeholder: 'your-application-key-id',
|
|
helpText: 'The applicationKeyId — not the master account ID',
|
|
},
|
|
{
|
|
key: 'key',
|
|
label: 'Application Key',
|
|
inputType: 'password',
|
|
required: true,
|
|
helpText: 'The application key (secret value from the B2 dashboard)',
|
|
},
|
|
],
|
|
},
|
|
```
|
|
|
|
### Pattern 2: Registry Entry for OneDrive (token as password field)
|
|
|
|
**What:** OneDrive requires a token JSON blob paste. No drive_id or drive_type in the form — they are part of the token JSON obtained via `rclone authorize`.
|
|
**Decision:** Per REQUIREMENTS.md Out of Scope: "OAuth flow in browser (OneDrive) — Requires backend proxy; paste pre-obtained token instead." The token field receives the full JSON output of `rclone authorize "onedrive"` and the drive_id / drive_type are embedded in that JSON or set interactively — however for unattended rclone config the minimal viable approach is token + drive_id + drive_type.
|
|
|
|
```typescript
|
|
// Source: https://rclone.org/onedrive/ — verified 2026-03-30
|
|
onedrive: {
|
|
displayName: 'OneDrive',
|
|
description: 'Microsoft OneDrive (paste pre-obtained rclone token)',
|
|
fields: [
|
|
{
|
|
key: 'token',
|
|
label: 'OAuth Token (JSON)',
|
|
inputType: 'password',
|
|
required: true,
|
|
placeholder: '{"access_token":"...","token_type":"Bearer","refresh_token":"...","expiry":"..."}',
|
|
helpText: 'Paste the JSON token from: rclone authorize "onedrive"',
|
|
},
|
|
{
|
|
key: 'drive_id',
|
|
label: 'Drive ID',
|
|
inputType: 'text',
|
|
required: true,
|
|
placeholder: 'b!...',
|
|
helpText: 'The drive ID from your OneDrive. Found in the rclone authorize output.',
|
|
},
|
|
{
|
|
key: 'drive_type',
|
|
label: 'Drive Type',
|
|
inputType: 'select',
|
|
required: true,
|
|
options: [
|
|
{ value: 'personal', label: 'Personal' },
|
|
{ value: 'business', label: 'Business' },
|
|
{ value: 'documentLibrary', label: 'SharePoint Document Library' },
|
|
],
|
|
helpText: 'Personal for consumer OneDrive, Business for Microsoft 365',
|
|
},
|
|
],
|
|
},
|
|
```
|
|
|
|
### Pattern 3: Registry Entry for Google Cloud Storage
|
|
|
|
**What:** GCS uses `service_account_credentials` (JSON blob) as the recommended unattended auth method, since `service_account_file` requires a file path on disk (not compatible with wizard paste flow).
|
|
|
|
```typescript
|
|
// Source: https://rclone.org/googlecloudstorage/ — verified 2026-03-30
|
|
// Note: rclone type string is exactly "google cloud storage" (with spaces)
|
|
gcs: {
|
|
displayName: 'Google Cloud Storage',
|
|
description: 'Google Cloud Storage',
|
|
fields: [
|
|
{
|
|
key: 'project_number',
|
|
label: 'Project Number',
|
|
inputType: 'text',
|
|
required: true,
|
|
placeholder: '123456789',
|
|
helpText: 'Your GCP project number (not project ID)',
|
|
},
|
|
{
|
|
key: 'service_account_credentials',
|
|
label: 'Service Account JSON',
|
|
inputType: 'password',
|
|
required: true,
|
|
placeholder: '{"type":"service_account","project_id":"..."}',
|
|
helpText: 'Paste the full content of your service account JSON key file',
|
|
},
|
|
],
|
|
},
|
|
```
|
|
|
|
### Pattern 4: SftpAuthToggle Component (new, follows AzureAuthToggle)
|
|
|
|
**What:** SFTP supports two auth methods: password (using the `pass` key) and private key (using `key_pem` for inline paste). The toggle follows the exact CSS-hidden pattern of `AzureAuthToggle`.
|
|
|
|
**Key design decisions:**
|
|
- Use `pass` for password field (rclone SFTP key name — not `password`)
|
|
- Use `key_pem` for inline key paste (avoids file path: `key_file` requires a path on the server, not pasteable in the wizard)
|
|
- CSS hidden (not conditional render) — preserves both field values when toggling, consistent with the `AzureAuthToggle` decision recorded in PROJECT.md
|
|
|
|
```typescript
|
|
// Source: https://rclone.org/sftp/ — verified 2026-03-30; pattern from AzureAuthToggle.tsx
|
|
// SftpAuthToggle.tsx
|
|
import { useState } from 'react';
|
|
import type { UseFormRegister, FieldError } from 'react-hook-form';
|
|
import { PasswordField } from '../ui/PasswordField';
|
|
|
|
type SftpAuthMethod = 'password' | 'key';
|
|
|
|
interface SftpAuthToggleProps {
|
|
register: UseFormRegister<any>;
|
|
errors: {
|
|
pass?: FieldError;
|
|
key_pem?: FieldError;
|
|
};
|
|
}
|
|
|
|
export function SftpAuthToggle({ register, errors }: SftpAuthToggleProps) {
|
|
const [authMethod, setAuthMethod] = useState<SftpAuthMethod>('password');
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
<div className="flex rounded-md border border-gray-300 overflow-hidden">
|
|
<button type="button" onClick={() => setAuthMethod('password')} ...>Password</button>
|
|
<button type="button" onClick={() => setAuthMethod('key')} ...>Private Key</button>
|
|
</div>
|
|
<div className={authMethod === 'password' ? 'block' : 'hidden'}>
|
|
<PasswordField id="pass" label="Password" error={errors.pass} registration={register('pass')} />
|
|
</div>
|
|
<div className={authMethod === 'key' ? 'block' : 'hidden'}>
|
|
<PasswordField id="key_pem" label="Private Key (PEM)" error={errors.key_pem}
|
|
registration={register('key_pem')}
|
|
helpText="Paste your private key in PEM format (-----BEGIN ... PRIVATE KEY-----)" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Pattern 5: SFTP Registry Entry (structural fields only — auth toggle handles pass/key_pem)
|
|
|
|
```typescript
|
|
// Source: https://rclone.org/sftp/ — verified 2026-03-30
|
|
sftp: {
|
|
displayName: 'SFTP',
|
|
description: 'SSH File Transfer Protocol',
|
|
fields: [
|
|
{
|
|
key: 'host',
|
|
label: 'Host',
|
|
inputType: 'text',
|
|
required: true,
|
|
placeholder: 'sftp.example.com',
|
|
helpText: 'The SSH server hostname or IP address',
|
|
},
|
|
{
|
|
key: 'user',
|
|
label: 'Username',
|
|
inputType: 'text',
|
|
required: true,
|
|
placeholder: 'admin',
|
|
},
|
|
// NOTE: pass and key_pem are NOT in the fields array.
|
|
// They are handled by SftpAuthToggle (both always registered via CSS-hidden toggle).
|
|
// This mirrors AzureAuthToggle's approach: key + sas_url not in registry fields array.
|
|
],
|
|
},
|
|
```
|
|
|
|
**Important:** Like `azureblob`, the `sftp` backend has fields not listed in `BACKEND_REGISTRY.sftp.fields` (`pass` and `key_pem`). These are registered by `SftpAuthToggle` directly. The Zod schema built from the registry (`buildZodSchema('sftp')`) will NOT include `pass`/`key_pem` — a custom Zod schema override for `sftp` is needed (like `azureblob` has its manual schema).
|
|
|
|
Wait — re-examine the current schema approach:
|
|
|
|
`buildZodSchema` makes required fields `.min(1)` and optional fields `.optional()`. For azureblob: `key` and `sas_url` are both in the registry (both optional). The schema covers them. So the toggle fields ARE in the registry for azureblob, just with `required: false`.
|
|
|
|
**Revised SFTP approach:** Include `pass` and `key_pem` in the registry fields as `required: false`. `SftpAuthToggle` still handles their visibility, but the registry/schema does cover them. This avoids needing a custom schema.
|
|
|
|
```typescript
|
|
sftp: {
|
|
displayName: 'SFTP',
|
|
description: 'SSH File Transfer Protocol',
|
|
fields: [
|
|
{ key: 'host', label: 'Host', inputType: 'text', required: true, placeholder: 'sftp.example.com' },
|
|
{ key: 'user', label: 'Username', inputType: 'text', required: true, placeholder: 'admin' },
|
|
{ key: 'pass', label: 'Password', inputType: 'password', required: false, helpText: 'SFTP password (leave blank if using private key)' },
|
|
{ key: 'key_pem', label: 'Private Key (PEM)', inputType: 'password', required: false, helpText: 'Paste PEM-encoded private key' },
|
|
],
|
|
},
|
|
```
|
|
|
|
### Pattern 6: RCLONE_TYPE_MAP Extension
|
|
|
|
**What:** `rclone-conf.ts` maps our `BackendType` keys to rclone's `type =` values. GCS is the special case — its rclone type string contains spaces.
|
|
|
|
```typescript
|
|
// Source: https://rclone.org/googlecloudstorage/ — verified 2026-03-30
|
|
const RCLONE_TYPE_MAP: Record<string, string> = {
|
|
azureblob: 'azureblob',
|
|
s3: 's3',
|
|
's3-compatible': 's3',
|
|
onedrive: 'onedrive',
|
|
sftp: 'sftp',
|
|
gcs: 'google cloud storage', // NOTE: spaces in rclone type value
|
|
b2: 'b2',
|
|
};
|
|
```
|
|
|
|
### Pattern 7: RemoteConfigStep Extension
|
|
|
|
**What:** The component needs to handle 4 new backends. Only `sftp` needs special treatment (SftpAuthToggle). The other three render via the registry loop.
|
|
|
|
```typescript
|
|
// Extend backendLabel record
|
|
const backendLabel: Record<NonNullable<typeof backendType>, string> = {
|
|
azureblob: 'Azure Blob Storage',
|
|
s3: 'Amazon S3',
|
|
's3-compatible': 'S3-Compatible Storage',
|
|
onedrive: 'OneDrive',
|
|
sftp: 'SFTP',
|
|
gcs: 'Google Cloud Storage',
|
|
b2: 'Backblaze B2',
|
|
};
|
|
|
|
// In the JSX — add sftp branch alongside the existing azureblob branch
|
|
{backendType === 'azureblob' ? (
|
|
<> ... existing AzureAuthToggle ... </>
|
|
) : backendType === 'sftp' ? (
|
|
<>
|
|
{/* host and user via FieldRenderer */}
|
|
<FieldRenderer field={BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'host')!} ... />
|
|
<FieldRenderer field={BACKEND_REGISTRY.sftp.fields.find(f => f.key === 'user')!} ... />
|
|
{/* Auth toggle handles pass + key_pem */}
|
|
<SftpAuthToggle register={register} errors={{ pass: errors.pass as FieldError | undefined, key_pem: errors.key_pem as FieldError | undefined }} />
|
|
</>
|
|
) : (
|
|
/* All others: full registry loop */
|
|
BACKEND_REGISTRY[backendType].fields.map(field => (
|
|
<FieldRenderer key={field.key} field={field} register={register} error={errors[field.key] as FieldError | undefined} />
|
|
))
|
|
)}
|
|
```
|
|
|
|
### Anti-Patterns to Avoid
|
|
|
|
- **Adding `sftp` to the generic FieldRenderer loop:** SFTP's `pass` and `key_pem` must be handled by a toggle (CSS-hidden), not two separate visible fields. If both render simultaneously, UX is broken.
|
|
- **Using `key_file` instead of `key_pem` for SFTP:** `key_file` expects a file system path (not pasteable in a browser wizard). `key_pem` accepts raw PEM content — correct for a paste workflow.
|
|
- **Using `service_account_file` for GCS:** Same problem — it expects a file path. Use `service_account_credentials` for the JSON paste approach.
|
|
- **Hardcoding `type = google cloud storage` in the output:** Do NOT hardcode this. The `RCLONE_TYPE_MAP` lookup handles it; `buildRcloneConf` already uses the map for all backends.
|
|
- **Forgetting to extend the `schema` fallback in RemoteConfigStep:** Line 26 has `const schema = backendType ? BACKEND_SCHEMAS[backendType] : BACKEND_SCHEMAS['azureblob']`. When new types are added to `BackendType`, TypeScript will flag the `BACKEND_SCHEMAS[backendType]` access if the new type is not in `BACKEND_SCHEMAS`.
|
|
|
|
## Don't Hand-Roll
|
|
|
|
| Problem | Don't Build | Use Instead | Why |
|
|
|---------|-------------|-------------|-----|
|
|
| SFTP auth toggle | New toggle pattern from scratch | SftpAuthToggle following `AzureAuthToggle.tsx` exactly | AzureAuthToggle is proven, CSS-hidden preserves form state, already tested pattern |
|
|
| Zod schema for new backends | Manual `z.object({ ... })` for each new backend | `buildZodSchema()` from registry fields | The project's entire schema layer is auto-derived from registry — maintain that invariant |
|
|
| rclone type string mapping | Conditional logic in generator | `RCLONE_TYPE_MAP` entry | GCS's spaced type string makes if/else brittle; the map is already the pattern |
|
|
| Field rendering for GCS/B2/OneDrive | Custom form components | `FieldRenderer` loop over registry fields | These three have no toggle — FieldRenderer handles text/password/select/hidden correctly |
|
|
|
|
## Common Pitfalls
|
|
|
|
### Pitfall 1: GCS type string has spaces
|
|
**What goes wrong:** Writing `type = googlecloudstorage` or `type = gcs` in the generated config — rclone rejects it. The correct rclone type string is `google cloud storage` (lowercase, with spaces).
|
|
**Why it happens:** The internal key in BACKEND_REGISTRY uses `gcs` (short, no spaces) but the rclone config value is the full string.
|
|
**How to avoid:** Add `gcs: 'google cloud storage'` to `RCLONE_TYPE_MAP` in `rclone-conf.ts`. Test: `buildRcloneConf(gcsState)` must contain `type = google cloud storage`.
|
|
**Warning signs:** rclone returns "didn't find section in config file" or "unknown backend type" for a GCS config.
|
|
|
|
### Pitfall 2: B2 account field naming collision
|
|
**What goes wrong:** Backblaze B2 uses `account` for the applicationKeyId — same key name as Azure Blob's `account` field. In WizardState, `params` is a flat `Record<string, string>`, so this is fine — no collision. But a test that reuses an Azure fixture and changes only `backendType` to `b2` would pass wrong params.
|
|
**Why it happens:** Field name overlap across two unrelated backends.
|
|
**How to avoid:** B2 test fixtures must explicitly set `params: { account: 'APP_KEY_ID', key: 'APP_KEY' }`. Do not reuse azureblob fixtures.
|
|
**Warning signs:** B2 rclone.conf test passes but generated config has `type = azureblob` (fixture mistake).
|
|
|
|
### Pitfall 3: BackendType union not extended in types.ts
|
|
**What goes wrong:** `src/store/types.ts` imports `BackendType` from `registry.ts` and re-exports it. Adding new types to `registry.ts` BackendType union automatically propagates. However, if any code has `Record<BackendType, ...>` exhaustive objects (like `backendLabel` in `RemoteConfigStep`), TypeScript will error until those are updated.
|
|
**Why it happens:** Exhaustive `Record<BackendType, string>` objects do not accept partial keys.
|
|
**How to avoid:** Update `backendLabel` and `BACKEND_SCHEMAS` when adding new BackendType values. Run `npx tsc --noEmit` after each registry addition.
|
|
**Warning signs:** TypeScript error "Type 'X' is not assignable to type 'never'" or "Property 'X' is missing in type".
|
|
|
|
### Pitfall 4: SFTP `pass` field vs rclone's obscure requirement
|
|
**What goes wrong:** rclone SFTP documentation notes that `pass` should be obscured using `rclone obscure`. The generated config will contain the plain-text password.
|
|
**Why it happens:** rclone's `pass` key for SFTP stores an obscured (not encrypted) password by default. When writing configs manually, plain text may or may not work depending on rclone version.
|
|
**How to avoid:** Add a `helpText` note to the `pass` field: "rclone may require the password to be obscured using `rclone obscure <password>`. If authentication fails, use the obscured value instead of the plain password."
|
|
**Important:** Per project constraints, plain text in generated files is acceptable — the security warning gate covers this. Do not block on this.
|
|
**Warning signs:** rclone connects but authentication fails silently (password not accepted as plain text).
|
|
|
|
### Pitfall 5: OneDrive token is a JSON blob — FieldRenderer handles it as a password
|
|
**What goes wrong:** The token field contains `{"access_token":"...","refresh_token":"...","expiry":"..."}` — a JSON string. Treating it as a text field shows it in plain text. Treating it as a password field hides it (preferred for credentials).
|
|
**How to avoid:** Set `inputType: 'password'` for the token field. FieldRenderer already handles `inputType: 'password'` via `PasswordField`.
|
|
**Warning signs:** Token value visible in browser without any masking.
|
|
|
|
### Pitfall 6: RemoteConfigStep's schema fallback must handle new types
|
|
**What goes wrong:** Line 26: `const schema = backendType ? BACKEND_SCHEMAS[backendType] : BACKEND_SCHEMAS['azureblob']`. If a new BackendType is added to `registry.ts` but not to `BACKEND_SCHEMAS` in `index.ts`, this lookup throws at runtime.
|
|
**Why it happens:** `BACKEND_SCHEMAS` is a manually-maintained object (not auto-derived from all registry keys in the current implementation).
|
|
**How to avoid:** After adding registry entries, immediately add the corresponding `buildZodSchema('newtype')` call to `BACKEND_SCHEMAS` in `index.ts`. TypeScript will flag the missing key in `BACKEND_SCHEMAS[backendType]` if the type is exhaustive.
|
|
|
|
## Code Examples
|
|
|
|
### rclone.conf output for each new backend
|
|
|
|
```ini
|
|
; OneDrive — Source: https://rclone.org/onedrive/ verified 2026-03-30
|
|
[my-onedrive]
|
|
type = onedrive
|
|
token = {"access_token":"...","token_type":"Bearer","refresh_token":"...","expiry":"..."}
|
|
drive_id = b!XXXXXXXXX
|
|
drive_type = business
|
|
|
|
; SFTP with password — Source: https://rclone.org/sftp/ verified 2026-03-30
|
|
[my-sftp]
|
|
type = sftp
|
|
host = sftp.example.com
|
|
user = admin
|
|
pass = mypassword
|
|
|
|
; SFTP with private key
|
|
[my-sftp-key]
|
|
type = sftp
|
|
host = sftp.example.com
|
|
user = admin
|
|
key_pem = -----BEGIN RSA PRIVATE KEY-----\n...
|
|
|
|
; GCS — Source: https://rclone.org/googlecloudstorage/ verified 2026-03-30
|
|
; Note: type value has spaces
|
|
[my-gcs]
|
|
type = google cloud storage
|
|
project_number = 123456789
|
|
service_account_credentials = {"type":"service_account","project_id":"..."}
|
|
|
|
; Backblaze B2 — Source: https://rclone.org/b2/ verified 2026-03-30
|
|
[my-b2]
|
|
type = b2
|
|
account = applicationKeyId123
|
|
key = secretApplicationKey456
|
|
```
|
|
|
|
### BackendType union extension
|
|
|
|
```typescript
|
|
// src/schemas/registry.ts — updated union
|
|
export type BackendType = 'azureblob' | 's3' | 's3-compatible' | 'onedrive' | 'sftp' | 'gcs' | 'b2';
|
|
```
|
|
|
|
### BACKEND_SCHEMAS extension
|
|
|
|
```typescript
|
|
// src/schemas/index.ts — extend the BACKEND_SCHEMAS object
|
|
export const BACKEND_SCHEMAS = {
|
|
azureblob: buildZodSchema('azureblob'),
|
|
s3: buildZodSchema('s3'),
|
|
's3-compatible': buildZodSchema('s3-compatible'),
|
|
onedrive: buildZodSchema('onedrive'),
|
|
sftp: buildZodSchema('sftp'),
|
|
gcs: buildZodSchema('gcs'),
|
|
b2: buildZodSchema('b2'),
|
|
} as const;
|
|
```
|
|
|
|
### Test fixture pattern for new backends (rclone-conf.test.ts)
|
|
|
|
```typescript
|
|
// Source: existing test pattern in src/generators/rclone-conf.test.ts
|
|
const b2State: WizardState = {
|
|
...INITIAL_STATE,
|
|
remote: {
|
|
name: 'my-b2',
|
|
backendType: 'b2',
|
|
params: { account: 'APP_KEY_ID', key: 'APP_KEY' },
|
|
},
|
|
};
|
|
|
|
describe('buildRcloneConf — b2', () => {
|
|
it('contains type = b2', () => {
|
|
expect(buildRcloneConf(b2State)).toContain('type = b2');
|
|
});
|
|
it('contains account value', () => {
|
|
expect(buildRcloneConf(b2State)).toContain('account = APP_KEY_ID');
|
|
});
|
|
});
|
|
```
|
|
|
|
### RemoteConfigStep test pattern for new backends
|
|
|
|
```typescript
|
|
// Source: existing pattern in RemoteConfigStep.test.tsx (renderWithBackend helper)
|
|
describe('BACK-04: Backblaze B2 form', () => {
|
|
it('renders Application Key ID field', async () => {
|
|
renderWithBackend('b2');
|
|
await waitFor(() => {
|
|
expect(screen.getByLabelText(/application key id/i)).toBeDefined();
|
|
});
|
|
});
|
|
it('renders Application Key field', async () => {
|
|
renderWithBackend('b2');
|
|
await waitFor(() => {
|
|
expect(screen.getByLabelText(/application key/i)).toBeDefined();
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
## State of the Art
|
|
|
|
| Old Approach | Current Approach | When Changed | Impact |
|
|
|--------------|------------------|--------------|--------|
|
|
| Hardcoded backend list in BackendSelectionStep | `Object.entries(BACKEND_REGISTRY)` loop | Phase 5 (TECH-03) | Adding registry entry auto-surfaces card in UI — zero additional BackendSelectionStep changes |
|
|
| `BACKEND_REGISTRY: Record<BackendType, FieldDef[]>` | `Record<BackendType, { displayName, description, fields }>` | Phase 5 (TECH-03) | New entries follow the enriched shape |
|
|
|
|
**Deprecated/outdated:**
|
|
- `BackendFormValues<T>` type: removed in Phase 5 (TECH-04), do not re-introduce
|
|
|
|
## Open Questions
|
|
|
|
1. **OneDrive drive_id and drive_type: should they be in the form, or embedded in the token?**
|
|
- What we know: The rclone authorize flow produces a token JSON, but `drive_id` and `drive_type` are separate config keys in rclone.conf.
|
|
- What's unclear: Whether IT pros who pre-obtain a token via `rclone authorize "onedrive"` will have drive_id readily available, or whether this creates friction.
|
|
- Recommendation: Include `drive_id` and `drive_type` as explicit form fields with guidance helpText. This is the explicit, transparent approach — avoids silent misconfiguration.
|
|
|
|
2. **SFTP `pass` field: warn about `rclone obscure` or silently output plain text?**
|
|
- What we know: rclone may require passwords in `pass` to be obscured. Plain text may work with some rclone versions but not others.
|
|
- What's unclear: Current rclone version behavior for plain-text `pass` in sftp configs.
|
|
- Recommendation: Add `helpText` noting the obscure requirement. Do not block generation. The security warning gate already covers plain-text credentials.
|
|
|
|
3. **GCS: should `project_number` be optional or required?**
|
|
- What we know: rclone docs say project_number is needed for bucket listing operations. GCS remotes may work for object access without it.
|
|
- What's unclear: Whether the wizard's target audience (IT pros deploying rclone) will always have the project number.
|
|
- Recommendation: Mark as `required: true` with a clear helpText. Better to require it upfront than produce a config that partially fails.
|
|
|
|
## Validation Architecture
|
|
|
|
### Test Framework
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Framework | Vitest 4.1.1 |
|
|
| Config file | `vitest.config.ts` (root) |
|
|
| Quick run command | `npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts src/components/wizard/RemoteConfigStep.test.tsx` |
|
|
| Full suite command | `npx vitest run` |
|
|
|
|
### Phase Requirements → Test Map
|
|
|
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
|
|--------|----------|-----------|-------------------|-------------|
|
|
| BACK-01 | OneDrive card appears in BackendSelectionStep | unit | `npx vitest run src/components/wizard/BackendSelectionStep.test.tsx` | ✅ (auto — BACKEND_REGISTRY drives the list; existing test now checks 7 backends) |
|
|
| BACK-01 | OneDrive form renders token + drive_id + drive_type fields | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
|
| BACK-01 | buildRcloneConf for onedrive produces `type = onedrive` | unit | `npx vitest run src/generators/rclone-conf.test.ts` | ❌ Wave 0 |
|
|
| BACK-02 | SFTP form renders host + user + password/key toggle | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
|
| BACK-02 | SFTP toggle CSS-hides inactive auth field (preserves value) | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
|
| BACK-02 | buildRcloneConf for sftp produces `type = sftp` with host/user | unit | `npx vitest run src/generators/rclone-conf.test.ts` | ❌ Wave 0 |
|
|
| BACK-03 | GCS form renders project_number + service_account_credentials | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
|
| BACK-03 | buildRcloneConf for gcs produces `type = google cloud storage` | unit | `npx vitest run src/generators/rclone-conf.test.ts` | ❌ Wave 0 |
|
|
| BACK-04 | B2 form renders account (Application Key ID) + key fields | unit | `npx vitest run src/components/wizard/RemoteConfigStep.test.tsx` | ❌ Wave 0 |
|
|
| BACK-04 | buildRcloneConf for b2 produces `type = b2` | unit | `npx vitest run src/generators/rclone-conf.test.ts` | ❌ Wave 0 |
|
|
| All | All 7 backends in registry have displayName, description, fields | unit | `npx vitest run src/schemas/registry.test.ts` | ❌ Wave 0 (update EXPECTED_BACKENDS) |
|
|
|
|
### Sampling Rate
|
|
- **Per task commit:** `npx vitest run` (full suite — fast, no reason not to run all)
|
|
- **Per wave merge:** `npx vitest run` + `npx tsc --noEmit`
|
|
- **Phase gate:** Full suite green + zero TypeScript errors before `/gsd:verify-work`
|
|
|
|
### Wave 0 Gaps
|
|
|
|
- [ ] `src/schemas/registry.test.ts` — update `EXPECTED_BACKENDS` array to include all 7 backends; add SFTP-specific assertions (host, user, pass, key_pem fields); add B2 assertions; add GCS assertions; add OneDrive assertions
|
|
- [ ] `src/generators/rclone-conf.test.ts` — add fixtures for onedrive, sftp (password), sftp (key), gcs, b2; assert type string and key fields for each; assert `gcs` produces `type = google cloud storage` (with spaces)
|
|
- [ ] `src/components/wizard/RemoteConfigStep.test.tsx` — add describe blocks for BACK-01 (OneDrive), BACK-02 (SFTP with toggle assertions), BACK-03 (GCS), BACK-04 (B2) using existing `renderWithBackend` helper
|
|
|
|
## Sources
|
|
|
|
### Primary (HIGH confidence)
|
|
- Direct code audit of `src/schemas/registry.ts` — confirmed BackendType union, BACKEND_REGISTRY shape, all existing field definitions
|
|
- Direct code audit of `src/schemas/index.ts` — confirmed `buildZodSchema` function, `BACKEND_SCHEMAS` structure
|
|
- Direct code audit of `src/generators/rclone-conf.ts` — confirmed `RCLONE_TYPE_MAP`, `buildRcloneConf` function
|
|
- Direct code audit of `src/components/wizard/RemoteConfigStep.tsx` — confirmed `backendLabel` record, azureblob branch pattern, registry loop pattern
|
|
- Direct code audit of `src/components/wizard/AzureAuthToggle.tsx` — confirmed CSS-hidden pattern, both fields always registered
|
|
- Direct code audit of `src/components/wizard/FieldRenderer.tsx` — confirmed all input types handled (text, password, select, hidden)
|
|
- `package.json` — confirmed all required libraries already installed, no new dependencies needed
|
|
- https://rclone.org/onedrive/ — token JSON structure, drive_id, drive_type fields (fetched 2026-03-30)
|
|
- https://rclone.org/sftp/ — host, user, pass, key_pem, key_file field names (fetched 2026-03-30)
|
|
- https://rclone.org/googlecloudstorage/ — type string "google cloud storage", service_account_credentials field (fetched 2026-03-30)
|
|
- https://rclone.org/b2/ — type "b2", account (applicationKeyId), key fields (fetched 2026-03-30)
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
- STATE.md project decisions — AzureAuthToggle CSS-hidden pattern noted as established for SFTP auth toggle
|
|
- PROJECT.md Out of Scope — "OAuth flow in browser (OneDrive)" confirms paste-token approach
|
|
|
|
### Tertiary (LOW confidence)
|
|
- SFTP `pass` plain-text behavior: rclone may require `rclone obscure` for the pass value. Documentation states it should be obscured but behavior with plain text varies. Recommend helpText warning rather than blocking.
|
|
|
|
## Metadata
|
|
|
|
**Confidence breakdown:**
|
|
- Standard stack: HIGH — no new libraries; all existing stack confirmed from package.json and code audit
|
|
- Architecture: HIGH — patterns derived from direct code audit of AzureAuthToggle, registry, and generator
|
|
- rclone field names: HIGH — verified against official rclone.org docs (fetched 2026-03-30)
|
|
- SFTP pass plain-text behavior: LOW — rclone version-dependent; add helpText but do not block
|
|
|
|
**Research date:** 2026-03-30
|
|
**Valid until:** 2026-06-30 (rclone backend config format is stable; React/Vitest versions unchanged)
|