feat: add mobile vault config model
This commit is contained in:
82
apps/mobile/src/mobileVaultConfig.test.ts
Normal file
82
apps/mobile/src/mobileVaultConfig.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createMobileVaultConfig } from './mobileVaultConfig'
|
||||
|
||||
describe('mobile vault config', () => {
|
||||
it('creates an app-local vault without sync when no remote is provided', () => {
|
||||
expect(createMobileVaultConfig({ id: 'personal', name: 'Personal Journal' })).toEqual({
|
||||
ok: true,
|
||||
config: {
|
||||
id: 'personal',
|
||||
name: 'Personal Journal',
|
||||
storage: {
|
||||
kind: 'appLocalGit',
|
||||
directoryName: 'personal-journal',
|
||||
},
|
||||
sync: {
|
||||
state: 'localOnly',
|
||||
remote: null,
|
||||
authRequirement: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('uses GitHub OAuth for GitHub-backed vaults', () => {
|
||||
const result = createMobileVaultConfig({
|
||||
id: 'tolaria',
|
||||
name: 'Tolaria MVP',
|
||||
remoteUrl: 'https://github.com/refactoringhq/tolaria.git',
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
config: {
|
||||
sync: {
|
||||
state: 'remoteReady',
|
||||
remote: {
|
||||
host: 'github.com',
|
||||
owner: 'refactoringhq',
|
||||
repository: 'tolaria',
|
||||
},
|
||||
authRequirement: {
|
||||
strategy: 'githubOAuth',
|
||||
host: 'github.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('requires SSH keys for arbitrary Git remotes', () => {
|
||||
const result = createMobileVaultConfig({
|
||||
id: 'work',
|
||||
name: 'Work',
|
||||
remoteUrl: 'ssh://git@git.example.com/acme/notes.git',
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
config: {
|
||||
sync: {
|
||||
authRequirement: {
|
||||
strategy: 'sshKey',
|
||||
host: 'git.example.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects invalid remote URLs before persisting a config', () => {
|
||||
expect(
|
||||
createMobileVaultConfig({
|
||||
id: 'broken',
|
||||
name: 'Broken',
|
||||
remoteUrl: '/Users/luca/Laputa',
|
||||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error: 'invalidRemoteUrl',
|
||||
})
|
||||
})
|
||||
})
|
||||
100
apps/mobile/src/mobileVaultConfig.ts
Normal file
100
apps/mobile/src/mobileVaultConfig.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { parseMobileGitRemote, type MobileGitAuthStrategy, type MobileGitRemote } from './mobileGitRemote'
|
||||
|
||||
export type MobileVaultStorage = {
|
||||
kind: 'appLocalGit'
|
||||
directoryName: string
|
||||
}
|
||||
|
||||
export type MobileVaultAuthRequirement = {
|
||||
strategy: MobileGitAuthStrategy
|
||||
host: string
|
||||
}
|
||||
|
||||
export type MobileVaultSync =
|
||||
| {
|
||||
state: 'localOnly'
|
||||
remote: null
|
||||
authRequirement: null
|
||||
}
|
||||
| {
|
||||
state: 'remoteReady'
|
||||
remote: MobileGitRemote
|
||||
authRequirement: MobileVaultAuthRequirement
|
||||
}
|
||||
|
||||
export type MobileVaultConfig = {
|
||||
id: string
|
||||
name: string
|
||||
storage: MobileVaultStorage
|
||||
sync: MobileVaultSync
|
||||
}
|
||||
|
||||
export type CreateMobileVaultConfigInput = {
|
||||
id: string
|
||||
name: string
|
||||
remoteUrl?: string
|
||||
}
|
||||
|
||||
export type MobileVaultConfigError = 'invalidRemoteUrl'
|
||||
|
||||
export type CreateMobileVaultConfigResult =
|
||||
| {
|
||||
ok: true
|
||||
config: MobileVaultConfig
|
||||
}
|
||||
| {
|
||||
ok: false
|
||||
error: MobileVaultConfigError
|
||||
}
|
||||
|
||||
export function createMobileVaultConfig(input: CreateMobileVaultConfigInput): CreateMobileVaultConfigResult {
|
||||
const sync = createSyncConfig(input.remoteUrl)
|
||||
if (!sync) {
|
||||
return { ok: false, error: 'invalidRemoteUrl' }
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
config: {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
storage: createStorageConfig(input.name),
|
||||
sync,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createSyncConfig(remoteUrl: string | undefined): MobileVaultSync | null {
|
||||
if (!remoteUrl?.trim()) {
|
||||
return { state: 'localOnly', remote: null, authRequirement: null }
|
||||
}
|
||||
|
||||
const remote = parseMobileGitRemote(remoteUrl)
|
||||
return remote ? createRemoteSync(remote) : null
|
||||
}
|
||||
|
||||
function createRemoteSync(remote: MobileGitRemote): MobileVaultSync {
|
||||
return {
|
||||
state: 'remoteReady',
|
||||
remote,
|
||||
authRequirement: {
|
||||
strategy: remote.authStrategy,
|
||||
host: remote.host,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createStorageConfig(name: string): MobileVaultStorage {
|
||||
return {
|
||||
kind: 'appLocalGit',
|
||||
directoryName: createDirectoryName(name),
|
||||
}
|
||||
}
|
||||
|
||||
function createDirectoryName(name: string) {
|
||||
return name
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
}
|
||||
@@ -8,7 +8,7 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
|
||||
|
||||
- Branch: `codex/mobile`
|
||||
- Active phase: Phase 2 - Mobile Shell
|
||||
- Active slice: Add mobile Git remote auth strategy parsing
|
||||
- Active slice: Connect parsed Git remotes to mobile vault configuration
|
||||
- Push policy: commit locally; do not push unless explicitly requested
|
||||
- Validation target: iPad/iOS simulator first
|
||||
|
||||
@@ -44,14 +44,15 @@ This file is the resumable working log for Tolaria mobile. The strategy and road
|
||||
- Added a tested compact gesture mapper and `SwipeSurface` wrapper so phone panels can transition through swipe gestures while keeping panel behavior in the reducer.
|
||||
- Re-tested the iPad simulator path; the app now launches in Expo Go on `iPad Pro 13-inch (M4)`.
|
||||
- Added a pure mobile Git remote parser that codifies the auth choice: GitHub remotes use the GitHub OAuth App path, arbitrary Git remotes use the SSH-key path.
|
||||
- Added a pure mobile vault configuration model that keeps vault storage app-local, distinguishes local-only vs remote-backed sync, and derives the required Git auth path from the parsed remote.
|
||||
|
||||
## Next Action
|
||||
|
||||
Continue Phase 2 with the next mobile shell slice:
|
||||
|
||||
1. Dismiss or suppress Expo Go's first-run tools modal during simulator QA so screenshots capture the app without the overlay.
|
||||
2. Connect parsed Git remotes to a mobile vault configuration model.
|
||||
3. Begin the TenTap editor spike behind a `MobileEditorAdapter` once the shell/storage boundary is stable.
|
||||
2. Begin the TenTap editor spike behind a `MobileEditorAdapter` once the shell/storage boundary is stable.
|
||||
3. Add the first native storage adapter around the vault config/repository contracts after the editor spike confirms the app shape.
|
||||
|
||||
## Verification Log
|
||||
|
||||
@@ -119,6 +120,10 @@ Continue Phase 2 with the next mobile shell slice:
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after Git remote auth parsing.
|
||||
- CodeScene after Git remote auth parsing: `apps/mobile/src/mobileGitRemote.ts` and `apps/mobile/src/mobileGitRemote.test.ts` scored `10`.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after Git remote auth parsing.
|
||||
- `pnpm --filter @tolaria/mobile test -- src/mobileVaultConfig.test.ts` passed after vault config extraction: 7 files / 24 tests.
|
||||
- `pnpm --filter @tolaria/mobile typecheck` passed after vault config extraction.
|
||||
- CodeScene after vault config extraction: `apps/mobile/src/mobileVaultConfig.ts` and `apps/mobile/src/mobileVaultConfig.test.ts` scored `10`.
|
||||
- `pnpm --filter @tolaria/mobile exec expo export --platform ios --output-dir /tmp/tolaria-mobile-export` passed after vault config extraction.
|
||||
|
||||
## Risks / Watch Items
|
||||
|
||||
|
||||
Reference in New Issue
Block a user