Implement DDD refactoring phases 1-4 with domain layer and ubiquitous language
Phase 1-3: Domain Layer Foundation - Add value objects: IdentityId, PermissionId, RelayId, WalletId, Nickname, NostrKeyPair - Add rich domain entities: Identity, Permission, Relay with behavior - Add domain events: IdentityCreated, IdentityRenamed, IdentitySelected, etc. - Add repository interfaces for Identity, Permission, Relay - Add infrastructure layer with repository implementations - Add EncryptionService abstraction Phase 4: Ubiquitous Language Cleanup - Rename BrowserSyncData → EncryptedVault (encrypted vault storage) - Rename BrowserSessionData → VaultSession (decrypted session state) - Rename SignerMetaData → ExtensionSettings (extension configuration) - Rename Identity_ENCRYPTED → StoredIdentity (storage DTO) - Rename Identity_DECRYPTED → IdentityData (session DTO) - Similar renames for Permission, Relay, NwcConnection, CashuMint - Add backwards compatibility aliases with @deprecated markers Test Coverage - Add comprehensive tests for all value objects - Add tests for domain entities and their behavior - Add tests for domain events - Fix PermissionChecker to prioritize kind-specific rules over blanket rules - Fix pre-existing component test issues (IconButton, Pubkey) All 113 tests pass. Both Chrome and Firefox builds succeed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,13 @@ import { Injectable } from '@angular/core';
|
||||
import { BrowserSyncHandler } from './browser-sync-handler';
|
||||
import { BrowserSessionHandler } from './browser-session-handler';
|
||||
import {
|
||||
BrowserSessionData,
|
||||
BrowserSyncData,
|
||||
BrowserSyncFlow,
|
||||
SignerMetaData,
|
||||
Relay_DECRYPTED,
|
||||
VaultSession,
|
||||
EncryptedVault,
|
||||
SyncFlow,
|
||||
ExtensionSettings,
|
||||
RelayData,
|
||||
CashuMintRecord,
|
||||
CashuProof,
|
||||
} from './types';
|
||||
import { SignerMetaHandler } from './signer-meta-handler';
|
||||
import { CryptoHelper } from '@common';
|
||||
@@ -30,7 +32,6 @@ import {
|
||||
deleteCashuMint,
|
||||
updateCashuMintProofs,
|
||||
} from './related/cashu';
|
||||
import { CashuMint_DECRYPTED, CashuProof } from './types';
|
||||
|
||||
export interface StorageServiceConfig {
|
||||
browserSessionHandler: BrowserSessionHandler;
|
||||
@@ -62,13 +63,13 @@ export class StorageService {
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
async enableBrowserSyncFlow(flow: BrowserSyncFlow): Promise<void> {
|
||||
async enableBrowserSyncFlow(flow: SyncFlow): Promise<void> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
this.#signerMetaHandler.setBrowserSyncFlow(flow);
|
||||
this.#signerMetaHandler.setSyncFlow(flow);
|
||||
}
|
||||
|
||||
async loadSignerMetaData(): Promise<SignerMetaData | undefined> {
|
||||
async loadExtensionSettings(): Promise<ExtensionSettings | undefined> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const data = await this.#signerMetaHandler.loadFullData();
|
||||
@@ -77,11 +78,16 @@ export class StorageService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.#signerMetaHandler.setFullData(data as SignerMetaData);
|
||||
return data as SignerMetaData;
|
||||
this.#signerMetaHandler.setFullData(data as ExtensionSettings);
|
||||
return data as ExtensionSettings;
|
||||
}
|
||||
|
||||
async loadBrowserSessionData(): Promise<BrowserSessionData | undefined> {
|
||||
/** @deprecated Use loadExtensionSettings instead */
|
||||
async loadSignerMetaData(): Promise<ExtensionSettings | undefined> {
|
||||
return this.loadExtensionSettings();
|
||||
}
|
||||
|
||||
async loadVaultSession(): Promise<VaultSession | undefined> {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const data = await this.#browserSessionHandler.loadFullData();
|
||||
@@ -91,22 +97,27 @@ export class StorageService {
|
||||
}
|
||||
|
||||
// Set the existing data for in-memory usage.
|
||||
this.#browserSessionHandler.setFullData(data as BrowserSessionData);
|
||||
return data as BrowserSessionData;
|
||||
this.#browserSessionHandler.setFullData(data as VaultSession);
|
||||
return data as VaultSession;
|
||||
}
|
||||
|
||||
/** @deprecated Use loadVaultSession instead */
|
||||
async loadBrowserSessionData(): Promise<VaultSession | undefined> {
|
||||
return this.loadVaultSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and migrate the browser sync data. If no data is available yet,
|
||||
* Load and migrate the encrypted vault data. If no data is available yet,
|
||||
* the returned object is undefined.
|
||||
*/
|
||||
async loadAndMigrateBrowserSyncData(): Promise<BrowserSyncData | undefined> {
|
||||
async loadAndMigrateEncryptedVault(): Promise<EncryptedVault | undefined> {
|
||||
this.assureIsInitialized();
|
||||
const unmigratedBrowserSyncData =
|
||||
const unmigratedEncryptedVault =
|
||||
await this.getBrowserSyncHandler().loadUnmigratedData();
|
||||
const { browserSyncData, migrationWasPerformed } =
|
||||
this.#migrateBrowserSyncData(unmigratedBrowserSyncData);
|
||||
const { encryptedVault, migrationWasPerformed } =
|
||||
this.#migrateEncryptedVault(unmigratedEncryptedVault);
|
||||
|
||||
if (!browserSyncData) {
|
||||
if (!encryptedVault) {
|
||||
// Nothing to do at this point.
|
||||
return undefined;
|
||||
}
|
||||
@@ -114,13 +125,18 @@ export class StorageService {
|
||||
// There is data. Check, if it was migrated.
|
||||
if (migrationWasPerformed) {
|
||||
// Persist the migrated data back to the browser sync storage.
|
||||
this.getBrowserSyncHandler().saveAndSetFullData(browserSyncData);
|
||||
this.getBrowserSyncHandler().saveAndSetFullData(encryptedVault);
|
||||
} else {
|
||||
// Set the data for in-memory usage.
|
||||
this.getBrowserSyncHandler().setFullData(browserSyncData);
|
||||
this.getBrowserSyncHandler().setFullData(encryptedVault);
|
||||
}
|
||||
|
||||
return browserSyncData;
|
||||
return encryptedVault;
|
||||
}
|
||||
|
||||
/** @deprecated Use loadAndMigrateEncryptedVault instead */
|
||||
async loadAndMigrateBrowserSyncData(): Promise<EncryptedVault | undefined> {
|
||||
return this.loadAndMigrateEncryptedVault();
|
||||
}
|
||||
|
||||
async deleteVault(doNotSetIsInitializedToFalse = false) {
|
||||
@@ -183,7 +199,7 @@ export class StorageService {
|
||||
await deleteRelay.call(this, relayId);
|
||||
}
|
||||
|
||||
async updateRelay(relayClone: Relay_DECRYPTED): Promise<void> {
|
||||
async updateRelay(relayClone: RelayData): Promise<void> {
|
||||
await updateRelay.call(this, relayClone);
|
||||
}
|
||||
|
||||
@@ -209,7 +225,7 @@ export class StorageService {
|
||||
name: string;
|
||||
mintUrl: string;
|
||||
unit?: string;
|
||||
}): Promise<CashuMint_DECRYPTED> {
|
||||
}): Promise<CashuMintRecord> {
|
||||
return await addCashuMint.call(this, data);
|
||||
}
|
||||
|
||||
@@ -227,36 +243,36 @@ export class StorageService {
|
||||
exportVault(): string {
|
||||
this.assureIsInitialized();
|
||||
const vaultJson = JSON.stringify(
|
||||
this.getBrowserSyncHandler().browserSyncData,
|
||||
this.getBrowserSyncHandler().encryptedVault,
|
||||
undefined,
|
||||
4
|
||||
);
|
||||
return vaultJson;
|
||||
}
|
||||
|
||||
async importVault(allegedBrowserSyncData: BrowserSyncData) {
|
||||
async importVault(allegedEncryptedVault: EncryptedVault) {
|
||||
this.assureIsInitialized();
|
||||
|
||||
const isValidData = this.#allegedBrowserSyncDataIsValid(
|
||||
allegedBrowserSyncData
|
||||
const isValidData = this.#allegedEncryptedVaultIsValid(
|
||||
allegedEncryptedVault
|
||||
);
|
||||
if (!isValidData) {
|
||||
throw new Error('The imported data is not valid.');
|
||||
}
|
||||
|
||||
await this.getBrowserSyncHandler().saveAndSetFullData(
|
||||
allegedBrowserSyncData
|
||||
allegedEncryptedVault
|
||||
);
|
||||
}
|
||||
|
||||
getBrowserSyncHandler(): BrowserSyncHandler {
|
||||
this.assureIsInitialized();
|
||||
|
||||
switch (this.#signerMetaHandler.signerMetaData?.syncFlow) {
|
||||
case BrowserSyncFlow.NO_SYNC:
|
||||
switch (this.#signerMetaHandler.extensionSettings?.syncFlow) {
|
||||
case SyncFlow.NO_SYNC:
|
||||
return this.#browserSyncNoHandler;
|
||||
|
||||
case BrowserSyncFlow.BROWSER_SYNC:
|
||||
case SyncFlow.BROWSER_SYNC:
|
||||
default:
|
||||
return this.#browserSyncYesHandler;
|
||||
}
|
||||
@@ -275,14 +291,14 @@ export class StorageService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current browser sync flow setting.
|
||||
* Get the current sync flow setting.
|
||||
* Returns NO_SYNC if not initialized or no setting found.
|
||||
*/
|
||||
getSyncFlow(): BrowserSyncFlow {
|
||||
if (!this.isInitialized || !this.#signerMetaHandler?.signerMetaData) {
|
||||
return BrowserSyncFlow.NO_SYNC;
|
||||
getSyncFlow(): SyncFlow {
|
||||
if (!this.isInitialized || !this.#signerMetaHandler?.extensionSettings) {
|
||||
return SyncFlow.NO_SYNC;
|
||||
}
|
||||
return this.#signerMetaHandler.signerMetaData.syncFlow ?? BrowserSyncFlow.NO_SYNC;
|
||||
return this.#signerMetaHandler.extensionSettings.syncFlow ?? SyncFlow.NO_SYNC;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,25 +313,24 @@ export class StorageService {
|
||||
}
|
||||
|
||||
async encrypt(value: string): Promise<string> {
|
||||
const browserSessionData =
|
||||
this.getBrowserSessionHandler().browserSessionData;
|
||||
if (!browserSessionData) {
|
||||
throw new Error('Browser session data is undefined.');
|
||||
const vaultSession = this.getBrowserSessionHandler().vaultSession;
|
||||
if (!vaultSession) {
|
||||
throw new Error('Vault session is undefined.');
|
||||
}
|
||||
|
||||
// v2: Use pre-derived key directly with AES-GCM
|
||||
if (browserSessionData.vaultKey) {
|
||||
return this.encryptV2(value, browserSessionData.iv, browserSessionData.vaultKey);
|
||||
if (vaultSession.vaultKey) {
|
||||
return this.encryptV2(value, vaultSession.iv, vaultSession.vaultKey);
|
||||
}
|
||||
|
||||
// v1: Use PBKDF2 with password
|
||||
if (!browserSessionData.vaultPassword) {
|
||||
if (!vaultSession.vaultPassword) {
|
||||
throw new Error('No vault password or key available.');
|
||||
}
|
||||
return CryptoHelper.encrypt(
|
||||
value,
|
||||
browserSessionData.iv,
|
||||
browserSessionData.vaultPassword
|
||||
vaultSession.iv,
|
||||
vaultSession.vaultPassword
|
||||
);
|
||||
}
|
||||
|
||||
@@ -347,31 +362,30 @@ export class StorageService {
|
||||
value: string,
|
||||
returnType: 'string' | 'number' | 'boolean'
|
||||
): Promise<any> {
|
||||
const browserSessionData =
|
||||
this.getBrowserSessionHandler().browserSessionData;
|
||||
if (!browserSessionData) {
|
||||
throw new Error('Browser session data is undefined.');
|
||||
const vaultSession = this.getBrowserSessionHandler().vaultSession;
|
||||
if (!vaultSession) {
|
||||
throw new Error('Vault session is undefined.');
|
||||
}
|
||||
|
||||
// v2: Use pre-derived key directly with AES-GCM
|
||||
if (browserSessionData.vaultKey) {
|
||||
if (vaultSession.vaultKey) {
|
||||
const decryptedValue = await this.decryptV2(
|
||||
value,
|
||||
browserSessionData.iv,
|
||||
browserSessionData.vaultKey
|
||||
vaultSession.iv,
|
||||
vaultSession.vaultKey
|
||||
);
|
||||
return this.parseDecryptedValue(decryptedValue, returnType);
|
||||
}
|
||||
|
||||
// v1: Use PBKDF2 with password
|
||||
if (!browserSessionData.vaultPassword) {
|
||||
if (!vaultSession.vaultPassword) {
|
||||
throw new Error('No vault password or key available.');
|
||||
}
|
||||
return this.decryptWithLockedVault(
|
||||
value,
|
||||
returnType,
|
||||
browserSessionData.iv,
|
||||
browserSessionData.vaultPassword
|
||||
vaultSession.iv,
|
||||
vaultSession.vaultPassword
|
||||
);
|
||||
}
|
||||
|
||||
@@ -445,28 +459,28 @@ export class StorageService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the browser sync data to the latest version.
|
||||
* Migrate the encrypted vault to the latest version.
|
||||
*/
|
||||
#migrateBrowserSyncData(browserSyncData: Partial<Record<string, any>>): {
|
||||
browserSyncData?: BrowserSyncData;
|
||||
#migrateEncryptedVault(encryptedVault: Partial<Record<string, any>>): {
|
||||
encryptedVault?: EncryptedVault;
|
||||
migrationWasPerformed: boolean;
|
||||
} {
|
||||
if (Object.keys(browserSyncData).length === 0) {
|
||||
// First run. There is no browser sync data yet.
|
||||
if (Object.keys(encryptedVault).length === 0) {
|
||||
// First run. There is no encrypted vault yet.
|
||||
return {
|
||||
browserSyncData: undefined,
|
||||
encryptedVault: undefined,
|
||||
migrationWasPerformed: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Will be implemented if migration is required.
|
||||
return {
|
||||
browserSyncData: browserSyncData as BrowserSyncData,
|
||||
encryptedVault: encryptedVault as EncryptedVault,
|
||||
migrationWasPerformed: false,
|
||||
};
|
||||
}
|
||||
|
||||
#allegedBrowserSyncDataIsValid(data: BrowserSyncData): boolean {
|
||||
#allegedEncryptedVaultIsValid(data: EncryptedVault): boolean {
|
||||
if (typeof data.iv === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user