From 165ec6f2b02220be68ebc8653435322dfa29c439 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 4 May 2026 09:41:06 +0200 Subject: [PATCH] feat: add mobile vault config model --- apps/mobile/src/mobileVaultConfig.test.ts | 82 ++++++++++++++++++ apps/mobile/src/mobileVaultConfig.ts | 100 ++++++++++++++++++++++ docs/MOBILE_PROGRESS.md | 11 ++- 3 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 apps/mobile/src/mobileVaultConfig.test.ts create mode 100644 apps/mobile/src/mobileVaultConfig.ts diff --git a/apps/mobile/src/mobileVaultConfig.test.ts b/apps/mobile/src/mobileVaultConfig.test.ts new file mode 100644 index 00000000..8489ae72 --- /dev/null +++ b/apps/mobile/src/mobileVaultConfig.test.ts @@ -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', + }) + }) +}) diff --git a/apps/mobile/src/mobileVaultConfig.ts b/apps/mobile/src/mobileVaultConfig.ts new file mode 100644 index 00000000..13d86c25 --- /dev/null +++ b/apps/mobile/src/mobileVaultConfig.ts @@ -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, '') +} diff --git a/docs/MOBILE_PROGRESS.md b/docs/MOBILE_PROGRESS.md index 89f0b6f7..6d2cbfd2 100644 --- a/docs/MOBILE_PROGRESS.md +++ b/docs/MOBILE_PROGRESS.md @@ -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