@ -223,148 +223,162 @@ To support **one active per workspace/tenant**:
## TimeSafari Implementation Guide
### Clean Implementation Path (Following Directive )
### Current State Analysis (2025-01-27 )
If implementing this pattern from scratch or reverting to reapply the directive, follow this clean implementation:
**Status**: ⚠️ **PARTIAL COMPLIANCE** - Smart deletion logic implemented correctly, but critical security issues remain.
#### 1) Schema Implementation
**Compliance Score**: 67% (4/6 components compliant)
**Initial Migration (001_initial):**
```sql
-- Enable foreign key constraints for data integrity
PRAGMA foreign_keys = ON;
-- Create accounts table with UNIQUE constraint on did
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dateCreated TEXT NOT NULL,
derivationPath TEXT,
did TEXT NOT NULL UNIQUE, -- UNIQUE constraint for foreign key support
identityEncrBase64 TEXT,
mnemonicEncrBase64 TEXT,
passkeyCredIdHex TEXT,
publicKeyHex TEXT NOT NULL
);
-- Create active_identity table with foreign key constraint
CREATE TABLE IF NOT EXISTS active_identity (
id INTEGER PRIMARY KEY CHECK (id = 1),
activeDid TEXT DEFAULT NULL, -- NULL instead of empty string
lastUpdated TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (activeDid) REFERENCES accounts(did) ON DELETE RESTRICT
);
#### ✅ **What's Already Working**
- **Smart Deletion Logic** : `IdentitySwitcherView.vue` implements atomic transaction-safe deletion
- **Data Access API** : All required DAL methods exist in `PlatformServiceMixin.ts`
- **Schema Structure** : `active_identity` table follows singleton pattern correctly
- **Bootstrapping** : `$ensureActiveSelected()` method implemented
-- Seed singleton row
INSERT INTO active_identity (id, activeDid, lastUpdated) VALUES (1, NULL, datetime('now'));
```
#### ❌ **Critical Issues Requiring Fix**
1. **Foreign Key Constraint** : Currently `ON DELETE SET NULL` (allows accidental deletion)
2. **Settings Table Cleanup** : Orphaned records with `accountDid=null` exist
#### 2) Data Access API Implementatio n
### Updated Implementation Plan
**Add to PlatformServiceMixin.ts:**
```typescript
// Required DAL methods following the pattern
async $getAllAccountDids(): Promise< string [ ] > {
const result = await this.$dbQuery("SELECT did FROM accounts ORDER BY dateCreated, did");
return result?.values?.map(row => row[0] as string) || [];
}
**Note**: Smart deletion logic is already implemented correctly. Focus on fixing security issues and cleanup.
async $getAccountDidById(id: number): Promise< string > {
const result = await this.$dbQuery("SELECT did FROM accounts WHERE id = ?", [id]);
return result?.values?.[0]?.[0] as string;
}
#### 1) Critical Security Fix (Migration 005)
async $getActiveDid(): Promise< string | null > {
const result = await this.$dbQuery("SELECT activeDid FROM active_identity WHERE id = 1");
return result?.values?.[0]?.[0] as string || null;
}
**Fix Foreign Key Constraint:**
```sql
-- Migration 005: Fix foreign key constraint to ON DELETE RESTRICT
{
name: "005_active_identity_constraint_fix",
sql: `
PRAGMA foreign_keys = ON;
async $setActiveDid(did: string | null): Promise< void > {
await this.$dbExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[did]
-- Recreate table with ON DELETE RESTRICT constraint (SECURITY FIX)
CREATE TABLE active_identity_new (
id INTEGER PRIMARY KEY CHECK (id = 1),
activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT,
lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
);
}
async $countAccounts(): Promise< number > {
const result = await this.$dbQuery("SELECT COUNT(*) FROM accounts");
return result?.values?.[0]?.[0] as number || 0;
}
-- Copy existing data
INSERT INTO active_identity_new (id, activeDid, lastUpdated)
SELECT id, activeDid, lastUpdated FROM active_identity;
// Deterministic "next" picker
$pickNextAccountDid(all: string[], current?: string): string {
const sorted = [...all].sort();
if (!current) return sorted[0];
const i = sorted.indexOf(current);
return sorted[(i + 1) % sorted.length];
}
```
-- Replace old table
DROP TABLE active_identity;
ALTER TABLE active_identity_new RENAME TO active_identity;
#### 3) Smart Deletion Implementation
**Replace deleteAccount in IdentitySwitcherView.vue:**
```typescript
async smartDeleteAccount(id: string) {
await this.$withTransaction(async () => {
const total = await this.$countAccounts();
if (total < = 1) {
this.notify.warning("Cannot delete the last account. Keep at least one.");
throw new Error("blocked:last-item");
-- Recreate indexes
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id);
`
}
```
const accountDid = await this.$getAccountDidById(parseInt(id));
const activeDid = await this.$getActiveDid();
#### 2) Settings Table Cleanup (Migration 006)
if (activeDid === accountDid) {
const allDids = await this.$getAllAccountDids();
const nextDid = this.$pickNextAccountDid(allDids.filter(d => d !== accountDid), accountDid);
await this.$setActiveDid(nextDid);
this.notify.success(`Switched active to ${nextDid} before deletion.`);
}
await this.$exec("DELETE FROM accounts WHERE id = ?", [id]) ;
});
// Update UI
this.otherIdentities = this.otherIdentities.filter(ident => ident.id !== id);
**Remove Orphaned Records:**
```sql
-- Migration 006: Settings cleanup
{
name: "006_settings_cleanup",
sql: `
-- Remove orphaned settings records (accountDid is null)
DELETE FROM settings WHERE accountDid IS NULL ;
-- Clear any remaining activeDid values in settings
UPDATE settings SET activeDid = NULL;
`
}
```
#### 4) Bootstrapping Implementation
#### 3) Optional Future Enhancement (Migration 007)
**Add to PlatformServiceMixin.ts:**
```typescript
async $ensureActiveSelected() {
const active = await this.$getActiveDid();
const all = await this.$getAllAccountDids();
if (active === null & & all.length > 0) {
await this.$setActiveDid(this.$pickNextAccountDid(all));
}
**Remove Legacy activeDid Column:**
```sql
-- Migration 007: Remove activeDid column entirely (future task)
{
name: "007_remove_activeDid_column",
sql: `
-- Remove the legacy activeDid column from settings table
ALTER TABLE settings DROP COLUMN activeDid;
`
}
```
**Call after migrations:**
```typescript
// In migration completion or app startup
await this.$ensureActiveSelected();
```
### Current Implementation Status
#### ✅ **Already Implemented Correctly**
- **Smart Deletion Logic** : `IdentitySwitcherView.vue` lines 285-315
- **Data Access API** : All methods exist in `PlatformServiceMixin.ts`
- **Transaction Safety** : Uses `$withTransaction()` for atomicity
- **Last Account Protection** : Blocks deletion when `total <= 1`
- **Deterministic Selection** : `$pickNextAccountDid()` method
- **Bootstrapping** : `$ensureActiveSelected()` method
#### ❌ **Requires Immediate Fix**
1. **Foreign Key Constraint** : Change from `ON DELETE SET NULL` to `ON DELETE RESTRICT`
2. **Settings Cleanup** : Remove orphaned records with `accountDid=null`
### Implementation Priority
#### **Phase 1: Critical Security Fix (IMMEDIATE)**
- **Migration 005** : Fix foreign key constraint to `ON DELETE RESTRICT`
- **Impact** : Prevents accidental account deletion
- **Risk** : HIGH - Current implementation allows data loss
#### **Phase 2: Settings Cleanup (HIGH PRIORITY)**
- **Migration 006** : Remove orphaned settings records
- **Impact** : Cleaner architecture, reduced confusion
- **Risk** : LOW - Only removes obsolete data
#### **Phase 3: Future Enhancement (OPTIONAL)**
- **Migration 007** : Remove `activeDid` column from settings
- **Impact** : Complete separation of concerns
- **Risk** : LOW - Architectural cleanup
### Updated Compliance Assessment
#### **Current Status** : ⚠️ **PARTIAL COMPLIANCE** (67%)
| Component | Status | Compliance |
|-----------|--------|------------|
| Smart Deletion Logic | ✅ Complete | 100% |
| Data Access API | ✅ Complete | 100% |
| Schema Structure | ✅ Complete | 100% |
| Foreign Key Constraint | ❌ Wrong (`SET NULL`) | 0% |
| Settings Cleanup | ❌ Missing | 0% |
| **Overall** | ⚠️ **Partial** | **67%** |
#### **After Fixes** : ✅ **FULL COMPLIANCE** (100%)
| Component | Status | Compliance |
|-----------|--------|------------|
| Smart Deletion Logic | ✅ Complete | 100% |
| Data Access API | ✅ Complete | 100% |
| Schema Structure | ✅ Complete | 100% |
| Foreign Key Constraint | ✅ Fixed (`RESTRICT`) | 100% |
| Settings Cleanup | ✅ Cleaned | 100% |
| **Overall** | ✅ **Complete** | **100%** |
### Implementation Benefits
**Following this clean path provides:**
**Current implementation already provides:**
- ✅ **Atomic Operations** : Transaction-safe account deletion
- ✅ **Last Account Protection** : Prevents deletion of final account
- ✅ **Smart Switching** : Auto-switches active account before deletion
- ✅ **Data Integrity** : Foreign key constraints prevent orphaned references
- ✅ **Deterministic Behavior** : Predictable "next account" selection
- ✅ **NULL Handling** : Proper empty state management
### Migration Strategy
**After fixes will add:**
- ✅ **Data Integrity** : Foreign key constraints prevent orphaned references
- ✅ **Clean Architecture** : Complete separation of identity vs. settings
- ✅ **Production Safety** : No accidental account deletion possible
### Next Steps
**For Existing Databases:**
1. **Revert** current foreign key implementation
2. **Apply** clean migration sequence above
3. **Convert** existing `activeDid = ''` to `NULL`
4. **Implement** smart deletion logic
5. **Test** all scenarios from test matrix
1. **IMMEDIATE** : Implement Migration 005 (foreign key fix)
2. **HIGH PRIORITY** : Implement Migration 006 (settings cleanup)
3. **OPTIONAL** : Implement Migration 007 (remove legacy column)
4. **TEST** : Run directive test matrix to verify compliance
This clean implementation follows the directive exactly and provides **complete pattern compliance** with **production-grade safeguards** .
This updated plan focuses on **fixing the critical security issue** while preserving the **already-working smart deletion logic** .