---
phase: 06-new-backends
plan: "02"
type: execute
wave: 1
depends_on:
- "06-00"
files_modified:
- src/schemas/registry.ts
- src/schemas/index.ts
- src/generators/rclone-conf.ts
- src/components/wizard/SftpAuthToggle.tsx
autonomous: true
requirements:
- BACK-02
must_haves:
truths:
- "BACKEND_REGISTRY has sftp entry with host (required), user (required), pass (optional), key_pem (optional)"
- "SftpAuthToggle renders a Password tab and Private Key tab; inactive tab's field is CSS-hidden (not unmounted)"
- "Switching SFTP auth tabs does not clear the hidden field value (CSS-hidden, not conditional render)"
- "buildRcloneConf(sftpPasswordState) outputs 'type = sftp' with pass, omits key_pem"
- "buildRcloneConf(sftpKeyState) outputs 'type = sftp' with key_pem, omits pass"
artifacts:
- path: "src/schemas/registry.ts"
provides: "sftp entry in BACKEND_REGISTRY"
contains: "key_pem"
- path: "src/components/wizard/SftpAuthToggle.tsx"
provides: "SFTP password vs private-key toggle following AzureAuthToggle CSS-hidden pattern"
exports: ["SftpAuthToggle"]
- path: "src/generators/rclone-conf.ts"
provides: "RCLONE_TYPE_MAP sftp entry"
contains: "sftp.*sftp"
key_links:
- from: "src/components/wizard/SftpAuthToggle.tsx"
to: "src/components/ui/PasswordField.tsx"
via: "PasswordField component for both pass and key_pem fields"
pattern: "PasswordField.*pass|PasswordField.*key_pem"
- from: "src/schemas/registry.ts"
to: "BACKEND_REGISTRY sftp entry"
via: "pass and key_pem fields required: false — SftpAuthToggle registers them directly"
pattern: "key_pem.*required.*false"
---
Add SFTP to the data layer and create the SftpAuthToggle component.
Purpose: SFTP is the only new backend requiring a custom auth-method toggle (password vs private key). Isolating SFTP work in its own plan keeps Plan 01 clean and allows parallel execution.
Output: sftp entry in registry.ts/index.ts/rclone-conf.ts; SftpAuthToggle.tsx component following the AzureAuthToggle CSS-hidden pattern.
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/06-new-backends/06-RESEARCH.md
@.planning/phases/06-new-backends/06-00-SUMMARY.md
From src/components/wizard/AzureAuthToggle.tsx:
```typescript
import { useState } from 'react';
import type { UseFormRegister, FieldError } from 'react-hook-form';
import { PasswordField } from '../ui/PasswordField';
type AuthMethod = 'sas' | 'key';
interface AzureAuthToggleProps {
register: UseFormRegister;
errors: { key?: FieldError; sas_url?: FieldError; };
}
export function AzureAuthToggle({ register, errors }: AzureAuthToggleProps) {
const [authMethod, setAuthMethod] = useState('sas');
return (
{/* Both fields always registered — CSS toggling only */}
);
}
```
From src/schemas/registry.ts (current sftp fields — to add):
```typescript
// sftp registry entry (add to BACKEND_REGISTRY):
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' },
{ key: 'pass', label: 'Password', inputType: 'password', required: false, helpText: 'SFTP password. Note: rclone may require the password to be obscured using `rclone obscure `. If authentication fails, use the obscured value.' },
{ key: 'key_pem', label: 'Private Key (PEM)', inputType: 'password', required: false, helpText: 'Paste your private key in PEM format (-----BEGIN ... PRIVATE KEY-----)' },
],
},
```
Task 1: Add sftp entry to registry.ts, index.ts, and rclone-conf.tssrc/schemas/registry.ts, src/schemas/index.ts, src/generators/rclone-conf.ts
- BACKEND_REGISTRY gains sftp entry with host (text, required), user (text, required), pass (password, optional), key_pem (password, optional)
- pass helpText mentions rclone obscure requirement (security note, does not block generation)
- BACKEND_SCHEMAS gains sftp entry via buildZodSchema('sftp')
- RCLONE_TYPE_MAP gains sftp → 'sftp'
- `npx vitest run src/schemas/registry.test.ts` passes all sftp assertions (host, user, pass, key_pem)
- `npx vitest run src/generators/rclone-conf.test.ts` passes sftp password and sftp key describe blocks
- `npx tsc --noEmit` is clean (no more "sftp missing from BACKEND_REGISTRY" error)
**src/schemas/registry.ts:** Add sftp entry to BACKEND_REGISTRY (after 's3-compatible', before or after the entries added by Plan 01 — order does not matter):
```typescript
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',
},
{
key: 'pass',
label: 'Password',
inputType: 'password',
required: false,
helpText: 'SFTP password. Note: rclone may require the password to be obscured using `rclone obscure `. If authentication fails, use the obscured value instead of the plain password.',
},
{
key: 'key_pem',
label: 'Private Key (PEM)',
inputType: 'password',
required: false,
helpText: 'Paste your private key in PEM format (-----BEGIN ... PRIVATE KEY-----)',
},
],
},
```
**src/schemas/index.ts:** Add sftp to BACKEND_SCHEMAS:
```typescript
sftp: buildZodSchema('sftp'),
```
**src/generators/rclone-conf.ts:** Add sftp to RCLONE_TYPE_MAP:
```typescript
sftp: 'sftp',
```
Note: BackendType union already includes 'sftp' (added by Plan 01 Task 1). This plan only adds the registry/schema/map entries. If Plan 01 and Plan 02 run in parallel on separate branches, merge Plan 01 first then add sftp entries on top.
npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts 2>&1 | tail -20sftp entry in registry with all four field keys; sftp in BACKEND_SCHEMAS and RCLONE_TYPE_MAP; registry and rclone-conf tests fully green for all 7 backends; zero TypeScript errorsTask 2: Create SftpAuthToggle.tsxsrc/components/wizard/SftpAuthToggle.tsx
- Component renders a segmented control with two buttons: "Password" (left) and "Private Key" (right)
- Default auth method is 'password' — Password tab is active on first render
- Password tab active: pass field wrapper has class 'block', key_pem field wrapper has class 'hidden'
- Private Key tab active: pass field wrapper has class 'hidden', key_pem field wrapper has class 'block'
- Both fields are ALWAYS registered with react-hook-form (CSS-hidden, not conditional render)
- Pass field: id="pass", label="Password", uses PasswordField
- Key field: id="key_pem", label="Private Key (PEM)", uses PasswordField with multiline hint in helpText
- Active button has class 'bg-blue-600 text-white', inactive has 'bg-white text-gray-700 hover:bg-gray-50'
- Component accepts register and errors props matching SftpAuthToggleProps interface
Create src/components/wizard/SftpAuthToggle.tsx following AzureAuthToggle.tsx exactly, with these substitutions:
- AuthMethod type: 'password' | 'key' (was 'sas' | 'key')
- Initial state: 'password' (was 'sas')
- Tab 1 button text: "Password" (was "SAS URL"), onClick: setAuthMethod('password')
- Tab 2 button text: "Private Key" (was "Access Key"), onClick: setAuthMethod('key')
- Active condition for tab 1: authMethod === 'password'
- Active condition for tab 2: authMethod === 'key'
- Field 1 wrapper: className={authMethod === 'password' ? 'block' : 'hidden'}
PasswordField: id="pass", label="Password", error={errors.pass}, registration={register('pass')},
helpText="SFTP password. Note: rclone may require the password to be obscured using `rclone obscure `. If authentication fails, use the obscured value."
- Field 2 wrapper: 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-----)"
Props interface:
```typescript
interface SftpAuthToggleProps {
register: UseFormRegister;
errors: {
pass?: FieldError;
key_pem?: FieldError;
};
}
```
The SFTP toggle test in RemoteConfigStep.test.tsx asserts:
- getByText(/^password$/i, { selector: 'button' }) — button text must be exactly "Password"
- getByText(/private key/i, { selector: 'button' }) — button text must contain "Private Key"
- getByLabelText(/^password$/i) — PasswordField label must be exactly "Password"
- getByLabelText(/private key.*pem/i) — PasswordField label must match "Private Key (PEM)"
Ensure button text and label text match these patterns exactly.
npx vitest run src/components/wizard/RemoteConfigStep.test.tsx 2>&1 | grep -E "(BACK-02|SFTP|PASS|FAIL)" | head -20SftpAuthToggle.tsx created; component matches AzureAuthToggle pattern; SFTP toggle tests in RemoteConfigStep.test.tsx will pass after Plan 03 wires it into RemoteConfigStep (component exists and is importable)
`npx vitest run src/schemas/registry.test.ts src/generators/rclone-conf.test.ts`
Expected: fully green — all 7 backends pass in both files.
`npx tsc --noEmit`
Expected: zero errors (sftp now present in BACKEND_REGISTRY satisfying the exhaustive Record type).
- sftp registry entry with host, user, pass, key_pem (correct rclone key names)
- SftpAuthToggle.tsx exists, exports SftpAuthToggle, follows AzureAuthToggle CSS-hidden pattern
- sftp in BACKEND_SCHEMAS and RCLONE_TYPE_MAP
- registry and rclone-conf tests fully green for all 7 backends
- Zero TypeScript errors