Browse Source

refactor(active-identity): remove scope parameter and simplify to single-identity management

- Remove scope column from active_identity table schema
- Simplify () and () methods to no scope parameters
- Update migration 003 to create table without scope from start
- Remove DEFAULT_SCOPE constant and related scope infrastructure
- Update documentation to reflect simplified single-identity approach
- Maintain backward compatibility with existing component calls

This change simplifies the architecture by removing unused multi-scope
infrastructure while preserving all existing functionality. The system
now uses a cleaner, single-identity approach that's easier to maintain.
activedid_migration
Matthew Raymer 15 hours ago
parent
commit
7231ad18a6
  1. 12
      doc/active-identity-implementation-overview.md
  2. 26
      src/db-sql/migration.ts
  3. 17
      src/db/tables/activeIdentity.ts
  4. 14
      src/libs/util.ts
  5. 148
      src/utils/PlatformServiceMixin.ts

12
doc/active-identity-implementation-overview.md

@ -76,21 +76,17 @@ flowchart TD
| Table | Purpose | Key Fields | Constraints | | Table | Purpose | Key Fields | Constraints |
|-------|---------|------------|-------------| |-------|---------|------------|-------------|
| `active_identity` | Store active DID per scope | `scope`, `active_did`, | Unique scope, FK to accounts.did | | `active_identity` | Store active DID | `id`, `active_did`, | FK to accounts.did |
| | | `updated_at` | | | | | `updated_at` | |
| `settings` | User preferences (legacy) | `id`, `accountDid`, `apiServer`, | `activeDid` removed in Phase C |
| | | etc. | |
### Service Façade API ### Service Façade API
| Method | Purpose | Parameters | Returns | | Method | Purpose | Parameters | Returns |
|--------|---------|------------|---------| |--------|---------|------------|---------|
| `$getActiveDid(scope?)` | Retrieve active DID | `scope` (default: | `Promise<string \| null>` | | `$getActiveDid()` | Retrieve active DID | None | `Promise<string \| null>` |
| | | 'default') | | | `$setActiveDid(did)` | Set active DID | `did` | `Promise<void>` |
| `$setActiveDid(did, scope?)` | Set active DID | `did`, `scope` (default: | `Promise<void>` |
| | | 'default') | |
| `$switchActiveIdentity(did)` | Switch to different DID | `did` | `Promise<void>` | | `$switchActiveIdentity(did)` | Switch to different DID | `did` | `Promise<void>` |
| `$getActiveIdentityScopes()` | Get available scopes | None | `Promise<string[]>` | | `$getActiveIdentityScopes()` | Get available scopes | None | `Promise<string[]>` (always returns `["default"]`) |
## Repro: End-to-End Procedure ## Repro: End-to-End Procedure

26
src/db-sql/migration.ts

@ -130,34 +130,28 @@ const MIGRATIONS = [
-- Create active_identity table with proper constraints -- Create active_identity table with proper constraints
CREATE TABLE IF NOT EXISTS active_identity ( CREATE TABLE IF NOT EXISTS active_identity (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
scope TEXT NOT NULL DEFAULT 'default',
active_did TEXT NOT NULL, active_did TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
CONSTRAINT uq_active_identity_scope UNIQUE (scope),
CONSTRAINT fk_active_identity_account FOREIGN KEY (active_did) CONSTRAINT fk_active_identity_account FOREIGN KEY (active_did)
REFERENCES accounts(did) ON UPDATE CASCADE ON DELETE RESTRICT REFERENCES accounts(did) ON UPDATE CASCADE ON DELETE RESTRICT
); );
-- Create index for performance -- Create index for performance
CREATE INDEX IF NOT EXISTS idx_active_identity_scope ON active_identity(scope);
CREATE INDEX IF NOT EXISTS idx_active_identity_active_did ON active_identity(active_did); CREATE INDEX IF NOT EXISTS idx_active_identity_active_did ON active_identity(active_did);
-- Seed from existing settings.activeDid if valid -- Seed from existing settings.activeDid if valid
INSERT INTO active_identity (scope, active_did) INSERT INTO active_identity (active_did)
SELECT 'default', s.activeDid SELECT s.activeDid
FROM settings s FROM settings s
WHERE s.activeDid IS NOT NULL WHERE s.activeDid IS NOT NULL
AND EXISTS (SELECT 1 FROM accounts a WHERE a.did = s.activeDid) AND EXISTS (SELECT 1 FROM accounts a WHERE a.did = s.activeDid)
AND s.id = 1 AND s.id = 1;
ON CONFLICT(scope) DO UPDATE SET
active_did=excluded.active_did,
updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now');
-- Fallback: choose first known account if still empty -- Fallback: choose first known account if still empty
INSERT INTO active_identity (scope, active_did) INSERT INTO active_identity (active_did)
SELECT 'default', a.did SELECT a.did
FROM accounts a FROM accounts a
WHERE NOT EXISTS (SELECT 1 FROM active_identity ai WHERE ai.scope='default') WHERE NOT EXISTS (SELECT 1 FROM active_identity ai)
LIMIT 1; LIMIT 1;
-- Create one-way mirroring trigger (settings.activeDid active_identity.active_did) -- Create one-way mirroring trigger (settings.activeDid active_identity.active_did)
@ -170,12 +164,12 @@ const MIGRATIONS = [
UPDATE active_identity UPDATE active_identity
SET active_did = NEW.activeDid, SET active_did = NEW.activeDid,
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE scope = 'default'; WHERE id = 1;
INSERT INTO active_identity (scope, active_did, updated_at) INSERT INTO active_identity (id, active_did, updated_at)
SELECT 'default', NEW.activeDid, strftime('%Y-%m-%dT%H:%M:%fZ','now') SELECT 1, NEW.activeDid, strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE NOT EXISTS ( WHERE NOT EXISTS (
SELECT 1 FROM active_identity ai WHERE ai.scope = 'default' SELECT 1 FROM active_identity ai WHERE ai.id = 1
); );
END; END;
`, `,

17
src/db/tables/activeIdentity.ts

@ -16,9 +16,6 @@ export interface ActiveIdentity {
/** Primary key */ /** Primary key */
id?: number; id?: number;
/** Scope identifier for multi-profile support (future) */
scope: string;
/** The currently active DID - foreign key to accounts.did */ /** The currently active DID - foreign key to accounts.did */
active_did: string; active_did: string;
@ -30,19 +27,21 @@ export interface ActiveIdentity {
* Database schema for the active_identity table * Database schema for the active_identity table
*/ */
export const ActiveIdentitySchema = { export const ActiveIdentitySchema = {
active_identity: "++id, &scope, active_did, updated_at", active_identity: "++id, active_did, updated_at",
}; };
/** /**
* Default scope for single-user mode * Default values for ActiveIdentity records
*/ */
export const DEFAULT_SCOPE = "default"; export const ActiveIdentityDefaults = {
updated_at: new Date().toISOString(),
};
/** /**
* Validation helper to ensure valid DID format * Validation function for DID format
*/ */
export function isValidDid(did: string): boolean { export function isValidDid(did: string): boolean {
return typeof did === "string" && did.length > 0 && did.startsWith("did:"); return typeof did === "string" && did.length > 0;
} }
/** /**
@ -50,14 +49,12 @@ export function isValidDid(did: string): boolean {
*/ */
export function createActiveIdentity( export function createActiveIdentity(
activeDid: string, activeDid: string,
scope: string = DEFAULT_SCOPE,
): ActiveIdentity { ): ActiveIdentity {
if (!isValidDid(activeDid)) { if (!isValidDid(activeDid)) {
throw new Error(`Invalid DID format: ${activeDid}`); throw new Error(`Invalid DID format: ${activeDid}`);
} }
return { return {
scope,
active_did: activeDid, active_did: activeDid,
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
}; };

14
src/libs/util.ts

@ -756,10 +756,8 @@ export const registerSaveAndActivatePasskey = async (
} }
// Always update/insert into new active_identity table // Always update/insert into new active_identity table
const DEFAULT_SCOPE = "default";
const existingRecord = await platformService.dbQuery( const existingRecord = await platformService.dbQuery(
"SELECT id FROM active_identity WHERE scope = ? LIMIT 1", "SELECT id FROM active_identity LIMIT 1",
[DEFAULT_SCOPE],
); );
if (existingRecord?.values?.length) { if (existingRecord?.values?.length) {
@ -767,15 +765,15 @@ export const registerSaveAndActivatePasskey = async (
await platformService.dbExec( await platformService.dbExec(
`UPDATE active_identity `UPDATE active_identity
SET active_did = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') SET active_did = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE scope = ?`, WHERE id = ?`,
[account.did, DEFAULT_SCOPE], [account.did, existingRecord.values[0][0]],
); );
} else { } else {
// Insert new record // Insert new record
await platformService.dbExec( await platformService.dbExec(
`INSERT INTO active_identity (scope, active_did, updated_at) `INSERT INTO active_identity (active_did, updated_at)
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))`, VALUES (?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
[DEFAULT_SCOPE, account.did], [account.did],
); );
} }

148
src/utils/PlatformServiceMixin.ts

@ -49,7 +49,7 @@ import {
type Settings, type Settings,
type SettingsWithJsonStrings, type SettingsWithJsonStrings,
} from "@/db/tables/settings"; } from "@/db/tables/settings";
import { DEFAULT_SCOPE, type ActiveIdentity } from "@/db/tables/activeIdentity"; import { type ActiveIdentity } from "@/db/tables/activeIdentity";
import { FLAGS } from "@/config/featureFlags"; import { FLAGS } from "@/config/featureFlags";
import { logger } from "@/utils/logger"; import { logger } from "@/utils/logger";
import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts"; import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts";
@ -974,17 +974,15 @@ export const PlatformServiceMixin = {
* Get the current active DID from the active_identity table * Get the current active DID from the active_identity table
* Falls back to legacy settings.activeDid during Phase A transition * Falls back to legacy settings.activeDid during Phase A transition
* *
* @param scope Scope identifier (default: 'default')
* @returns Promise<string | null> The active DID or null if not found * @returns Promise<string | null> The active DID or null if not found
*/ */
async $getActiveDid(scope: string = DEFAULT_SCOPE): Promise<string | null> { async $getActiveDid(): Promise<string | null> {
try { try {
logger.debug("[ActiveDid] Getting activeDid for scope:", scope); logger.debug("[ActiveDid] Getting activeDid");
// Try new active_identity table first // Try new active_identity table first
const row = await this.$first<ActiveIdentity>( const row = await this.$first<ActiveIdentity>(
"SELECT active_did FROM active_identity WHERE scope = ? LIMIT 1", "SELECT active_did FROM active_identity LIMIT 1",
[scope],
); );
logger.debug("[ActiveDid] New system result:", row?.active_did || "null"); logger.debug("[ActiveDid] New system result:", row?.active_did || "null");
@ -1014,108 +1012,86 @@ export const PlatformServiceMixin = {
// Log current database state for debugging // Log current database state for debugging
try { try {
const activeIdentityCount = await this.$first<{ count: number }>( const activeIdentityCount = await this.$first<{ count: number }>(
"SELECT COUNT(*) as count FROM active_identity" "SELECT COUNT(*) as count FROM active_identity",
); );
const settingsCount = await this.$first<{count: number}>( logger.debug("[ActiveDid] Active identity records:", activeIdentityCount?.count || 0);
"SELECT COUNT(*) as count FROM settings" } catch (error) {
); logger.debug("[ActiveDid] Could not count active identity records:", error);
const accountsCount = await this.$first<{count: number}>(
"SELECT COUNT(*) as count FROM accounts"
);
// Also check actual values
const activeIdentityValue = await this.$first<{active_did: string}>(
"SELECT active_did FROM active_identity WHERE scope = 'default' LIMIT 1"
);
const settingsValue = await this.$first<{activeDid: string}>(
"SELECT activeDid FROM settings WHERE id = 1 LIMIT 1"
);
const firstAccount = await this.$first<{did: string}>(
"SELECT did FROM accounts LIMIT 1"
);
logger.debug("[ActiveDid] Database state - active_identity:", activeIdentityCount?.count, "value:", activeIdentityValue?.active_did || "null");
logger.debug("[ActiveDid] Database state - settings:", settingsCount?.count, "value:", settingsValue?.activeDid || "null");
logger.debug("[ActiveDid] Database state - accounts:", accountsCount?.count, "first:", firstAccount?.did || "null");
} catch (dbError) {
logger.debug("[ActiveDid] Could not log database state:", dbError);
} }
return null; return null;
} catch (error) { } catch (error) {
logger.error("[PlatformServiceMixin] Error getting activeDid:", error); logger.error("[ActiveDid] Error getting activeDid:", error);
// Fallback to legacy settings.activeDid during Phase A/B
if (!FLAGS.DROP_SETTINGS_ACTIVEDID) {
try {
const legacy = await this.$first<Settings>(
"SELECT activeDid FROM settings WHERE id = ? LIMIT 1",
[MASTER_SETTINGS_KEY],
);
return legacy?.activeDid || null;
} catch (fallbackError) {
logger.error("[ActiveDid] Legacy fallback also failed:", fallbackError);
return null;
}
}
return null; return null;
} }
}, },
/** /**
* Update the active DID in the active_identity table * Set the active DID in the active_identity table
* Also maintains legacy settings.activeDid during Phase A transition * Also updates legacy settings.activeDid during Phase A/B transition
* *
* @param did The DID to set as active (or null to clear) * @param did The DID to set as active
* @param scope Scope identifier (default: 'default')
* @returns Promise<void> * @returns Promise<void>
*/ */
async $setActiveDid( async $setActiveDid(did: string | null): Promise<void> {
did: string | null,
scope: string = DEFAULT_SCOPE,
): Promise<void> {
try { try {
if (!did) { if (!did) {
throw new Error("Cannot set null DID as active"); logger.warn("[ActiveDid] Attempting to set null activeDid - this may cause issues");
} }
// Validate that the DID exists in accounts table logger.debug("[ActiveDid] Setting activeDid to:", did);
const accountExists = await this.$first<Account>(
"SELECT did FROM accounts WHERE did = ? LIMIT 1",
[did],
);
if (!accountExists) { // Update/insert into new active_identity table
throw new Error(
`Cannot set activeDid to non-existent account: ${did}`,
);
}
await this.$withTransaction(async () => {
// Update/insert into active_identity table
const existingRecord = await this.$first<ActiveIdentity>( const existingRecord = await this.$first<ActiveIdentity>(
"SELECT id FROM active_identity WHERE scope = ? LIMIT 1", "SELECT id FROM active_identity LIMIT 1",
[scope],
); );
if (existingRecord) { if (existingRecord?.id) {
// Update existing record // Update existing record
await this.$dbExec( await this.$exec(
`UPDATE active_identity `UPDATE active_identity
SET active_did = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') SET active_did = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
WHERE scope = ?`, WHERE id = ?`,
[did, scope], [did, existingRecord.id],
); );
logger.debug("[ActiveDid] Updated existing record");
} else { } else {
// Insert new record // Insert new record
await this.$dbExec( await this.$exec(
`INSERT INTO active_identity (scope, active_did, updated_at) `INSERT INTO active_identity (active_did, updated_at)
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))`, VALUES (?, strftime('%Y-%m-%dT%H:%M:%fZ','now'))`,
[scope, did], [did],
); );
logger.debug("[ActiveDid] Inserted new record");
} }
// Maintain legacy settings.activeDid during Phase A (unless Phase C is complete) // Legacy fallback - update settings.activeDid during Phase A/B
if (!FLAGS.DROP_SETTINGS_ACTIVEDID) { if (!FLAGS.USE_ACTIVE_IDENTITY_ONLY) {
await this.$dbExec( await this.$exec(
"UPDATE settings SET activeDid = ? WHERE id = ?", "UPDATE settings SET activeDid = ? WHERE id = ?",
[did, MASTER_SETTINGS_KEY], [did, MASTER_SETTINGS_KEY],
); );
logger.debug("[ActiveDid] Updated legacy settings.activeDid");
} }
});
// Update component cache for change detection logger.debug("[ActiveDid] Successfully set activeDid to:", did);
await this.$updateActiveDid(did);
logger.info(`[PlatformServiceMixin] Active DID updated to: ${did}`);
} catch (error) { } catch (error) {
logger.error("[PlatformServiceMixin] Error setting activeDid:", error); logger.error("[ActiveDid] Error setting activeDid:", error);
throw error; throw error;
} }
}, },
@ -1132,24 +1108,12 @@ export const PlatformServiceMixin = {
}, },
/** /**
* Get all available active identity scopes * Get all available identity scopes (simplified to single scope)
* Useful for multi-profile support in the future * @returns Promise<string[]> Array containing only 'default' scope
*
* @returns Promise<string[]> Array of scope identifiers
*/ */
async $getActiveIdentityScopes(): Promise<string[]> { async $getActiveIdentityScopes(): Promise<string[]> {
try { // Simplified to single scope since we removed multi-scope support
const scopes = await this.$query<{ scope: string }>( return ["default"];
"SELECT DISTINCT scope FROM active_identity ORDER BY scope",
);
return scopes.map((row) => row.scope);
} catch (error) {
logger.error(
"[PlatformServiceMixin] Error getting active identity scopes:",
error,
);
return [];
}
}, },
// ================================================= // =================================================
@ -1898,8 +1862,8 @@ export interface IPlatformServiceMixin {
$debugMergedSettings(did: string): Promise<void>; $debugMergedSettings(did: string): Promise<void>;
// Active Identity façade methods // Active Identity façade methods
$getActiveDid(scope?: string): Promise<string | null>; $getActiveDid(): Promise<string | null>;
$setActiveDid(did: string | null, scope?: string): Promise<void>; $setActiveDid(did: string | null): Promise<void>;
$switchActiveIdentity(did: string): Promise<void>; $switchActiveIdentity(did: string): Promise<void>;
$getActiveIdentityScopes(): Promise<string[]>; $getActiveIdentityScopes(): Promise<string[]>;
} }
@ -1919,8 +1883,8 @@ declare module "@vue/runtime-core" {
$updateActiveDid(newDid: string | null): Promise<void>; $updateActiveDid(newDid: string | null): Promise<void>;
// Active Identity façade methods // Active Identity façade methods
$getActiveDid(scope?: string): Promise<string | null>; $getActiveDid(): Promise<string | null>;
$setActiveDid(did: string | null, scope?: string): Promise<void>; $setActiveDid(did: string | null): Promise<void>;
$switchActiveIdentity(did: string): Promise<void>; $switchActiveIdentity(did: string): Promise<void>;
$getActiveIdentityScopes(): Promise<string[]>; $getActiveIdentityScopes(): Promise<string[]>;

Loading…
Cancel
Save