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:
@@ -0,0 +1,228 @@
|
||||
import {
|
||||
IdentityRepositoryError,
|
||||
IdentityErrorCode,
|
||||
} from '../../domain/repositories/identity-repository';
|
||||
import type {
|
||||
IdentityRepository,
|
||||
IdentitySnapshot,
|
||||
} from '../../domain/repositories/identity-repository';
|
||||
import { IdentityId } from '../../domain/value-objects';
|
||||
import { EncryptionService } from '../encryption';
|
||||
import { NostrHelper } from '../../helpers/nostr-helper';
|
||||
|
||||
/**
|
||||
* Encrypted identity as stored in browser sync storage.
|
||||
*/
|
||||
interface EncryptedIdentity {
|
||||
id: string;
|
||||
nick: string;
|
||||
privkey: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage adapter interface - abstracts browser storage operations.
|
||||
* Implementations provided by Chrome/Firefox specific code.
|
||||
*/
|
||||
export interface IdentityStorageAdapter {
|
||||
// Session (in-memory, decrypted) operations
|
||||
getSessionIdentities(): IdentitySnapshot[];
|
||||
setSessionIdentities(identities: IdentitySnapshot[]): void;
|
||||
saveSessionData(): Promise<void>;
|
||||
|
||||
getSessionSelectedId(): string | null;
|
||||
setSessionSelectedId(id: string | null): void;
|
||||
|
||||
// Sync (persistent, encrypted) operations
|
||||
getSyncIdentities(): EncryptedIdentity[];
|
||||
saveSyncIdentities(identities: EncryptedIdentity[]): Promise<void>;
|
||||
|
||||
getSyncSelectedId(): string | null;
|
||||
saveSyncSelectedId(id: string | null): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of IdentityRepository using browser storage.
|
||||
* Handles encryption/decryption transparently.
|
||||
*/
|
||||
export class BrowserIdentityRepository implements IdentityRepository {
|
||||
constructor(
|
||||
private readonly storage: IdentityStorageAdapter,
|
||||
private readonly encryption: EncryptionService
|
||||
) {}
|
||||
|
||||
async findById(id: IdentityId): Promise<IdentitySnapshot | undefined> {
|
||||
const identities = this.storage.getSessionIdentities();
|
||||
return identities.find((i) => i.id === id.value);
|
||||
}
|
||||
|
||||
async findByPublicKey(publicKey: string): Promise<IdentitySnapshot | undefined> {
|
||||
const identities = this.storage.getSessionIdentities();
|
||||
return identities.find((i) => {
|
||||
try {
|
||||
const derivedPubkey = NostrHelper.pubkeyFromPrivkey(i.privkey);
|
||||
return derivedPubkey === publicKey;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async findByPrivateKey(privateKey: string): Promise<IdentitySnapshot | undefined> {
|
||||
// Normalize the private key to hex format
|
||||
let privkeyHex: string;
|
||||
try {
|
||||
privkeyHex = NostrHelper.getNostrPrivkeyObject(privateKey.toLowerCase()).hex;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const identities = this.storage.getSessionIdentities();
|
||||
return identities.find((i) => i.privkey === privkeyHex);
|
||||
}
|
||||
|
||||
async findAll(): Promise<IdentitySnapshot[]> {
|
||||
return this.storage.getSessionIdentities();
|
||||
}
|
||||
|
||||
async save(identity: IdentitySnapshot): Promise<void> {
|
||||
// Check for duplicate private key (excluding self)
|
||||
const existing = await this.findByPrivateKey(identity.privkey);
|
||||
if (existing && existing.id !== identity.id) {
|
||||
throw new IdentityRepositoryError(
|
||||
`An identity with the same private key already exists: ${existing.nick}`,
|
||||
IdentityErrorCode.DUPLICATE_PRIVATE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
// Update session storage
|
||||
const sessionIdentities = this.storage.getSessionIdentities();
|
||||
const existingIndex = sessionIdentities.findIndex((i) => i.id === identity.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// Update existing
|
||||
sessionIdentities[existingIndex] = identity;
|
||||
} else {
|
||||
// Add new
|
||||
sessionIdentities.push(identity);
|
||||
|
||||
// Auto-select if first identity
|
||||
if (sessionIdentities.length === 1) {
|
||||
this.storage.setSessionSelectedId(identity.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.storage.setSessionIdentities(sessionIdentities);
|
||||
await this.storage.saveSessionData();
|
||||
|
||||
// Encrypt and save to sync storage
|
||||
const encryptedIdentity = await this.encryptIdentity(identity);
|
||||
const syncIdentities = this.storage.getSyncIdentities();
|
||||
const syncIndex = syncIdentities.findIndex(
|
||||
async (i) => (await this.encryption.decryptString(i.id)) === identity.id
|
||||
);
|
||||
|
||||
if (syncIndex >= 0) {
|
||||
syncIdentities[syncIndex] = encryptedIdentity;
|
||||
} else {
|
||||
syncIdentities.push(encryptedIdentity);
|
||||
}
|
||||
|
||||
await this.storage.saveSyncIdentities(syncIdentities);
|
||||
|
||||
// Update selected ID in sync if this was the first identity
|
||||
if (sessionIdentities.length === 1) {
|
||||
const encryptedId = await this.encryption.encryptString(identity.id);
|
||||
await this.storage.saveSyncSelectedId(encryptedId);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: IdentityId): Promise<boolean> {
|
||||
const sessionIdentities = this.storage.getSessionIdentities();
|
||||
const initialLength = sessionIdentities.length;
|
||||
const filtered = sessionIdentities.filter((i) => i.id !== id.value);
|
||||
|
||||
if (filtered.length === initialLength) {
|
||||
return false; // Nothing was deleted
|
||||
}
|
||||
|
||||
// Update selected identity if needed
|
||||
const currentSelectedId = this.storage.getSessionSelectedId();
|
||||
if (currentSelectedId === id.value) {
|
||||
const newSelectedId = filtered.length > 0 ? filtered[0].id : null;
|
||||
this.storage.setSessionSelectedId(newSelectedId);
|
||||
}
|
||||
|
||||
this.storage.setSessionIdentities(filtered);
|
||||
await this.storage.saveSessionData();
|
||||
|
||||
// Remove from sync storage
|
||||
const encryptedId = await this.encryption.encryptString(id.value);
|
||||
const syncIdentities = this.storage.getSyncIdentities();
|
||||
const filteredSync = syncIdentities.filter((i) => i.id !== encryptedId);
|
||||
await this.storage.saveSyncIdentities(filteredSync);
|
||||
|
||||
// Update selected ID in sync
|
||||
const newSelectedId = this.storage.getSessionSelectedId();
|
||||
const encryptedSelectedId = newSelectedId
|
||||
? await this.encryption.encryptString(newSelectedId)
|
||||
: null;
|
||||
await this.storage.saveSyncSelectedId(encryptedSelectedId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async getSelectedId(): Promise<IdentityId | null> {
|
||||
const selectedId = this.storage.getSessionSelectedId();
|
||||
return selectedId ? IdentityId.from(selectedId) : null;
|
||||
}
|
||||
|
||||
async setSelectedId(id: IdentityId | null): Promise<void> {
|
||||
if (id) {
|
||||
// Verify the identity exists
|
||||
const exists = await this.findById(id);
|
||||
if (!exists) {
|
||||
throw new IdentityRepositoryError(
|
||||
`Identity not found: ${id.value}`,
|
||||
IdentityErrorCode.NOT_FOUND
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.storage.setSessionSelectedId(id?.value ?? null);
|
||||
await this.storage.saveSessionData();
|
||||
|
||||
// Update sync storage
|
||||
const encryptedId = id
|
||||
? await this.encryption.encryptString(id.value)
|
||||
: null;
|
||||
await this.storage.saveSyncSelectedId(encryptedId);
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
return this.storage.getSessionIdentities().length;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Private helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private async encryptIdentity(identity: IdentitySnapshot): Promise<EncryptedIdentity> {
|
||||
return {
|
||||
id: await this.encryption.encryptString(identity.id),
|
||||
nick: await this.encryption.encryptString(identity.nick),
|
||||
privkey: await this.encryption.encryptString(identity.privkey),
|
||||
createdAt: await this.encryption.encryptString(identity.createdAt),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a BrowserIdentityRepository.
|
||||
*/
|
||||
export function createIdentityRepository(
|
||||
storage: IdentityStorageAdapter,
|
||||
encryption: EncryptionService
|
||||
): IdentityRepository {
|
||||
return new BrowserIdentityRepository(storage, encryption);
|
||||
}
|
||||
17
projects/common/src/lib/infrastructure/repositories/index.ts
Normal file
17
projects/common/src/lib/infrastructure/repositories/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export {
|
||||
BrowserIdentityRepository,
|
||||
createIdentityRepository,
|
||||
} from './identity-repository.impl';
|
||||
export type { IdentityStorageAdapter } from './identity-repository.impl';
|
||||
|
||||
export {
|
||||
BrowserPermissionRepository,
|
||||
createPermissionRepository,
|
||||
} from './permission-repository.impl';
|
||||
export type { PermissionStorageAdapter } from './permission-repository.impl';
|
||||
|
||||
export {
|
||||
BrowserRelayRepository,
|
||||
createRelayRepository,
|
||||
} from './relay-repository.impl';
|
||||
export type { RelayStorageAdapter } from './relay-repository.impl';
|
||||
@@ -0,0 +1,218 @@
|
||||
import type {
|
||||
PermissionRepository,
|
||||
PermissionSnapshot,
|
||||
PermissionQuery,
|
||||
ExtensionMethod,
|
||||
} from '../../domain/repositories/permission-repository';
|
||||
import { IdentityId, PermissionId } from '../../domain/value-objects';
|
||||
import { EncryptionService } from '../encryption';
|
||||
|
||||
/**
|
||||
* Encrypted permission as stored in browser sync storage.
|
||||
*/
|
||||
interface EncryptedPermission {
|
||||
id: string;
|
||||
identityId: string;
|
||||
host: string;
|
||||
method: string;
|
||||
methodPolicy: string;
|
||||
kind?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage adapter interface for permissions.
|
||||
*/
|
||||
export interface PermissionStorageAdapter {
|
||||
// Session (in-memory, decrypted) operations
|
||||
getSessionPermissions(): PermissionSnapshot[];
|
||||
setSessionPermissions(permissions: PermissionSnapshot[]): void;
|
||||
saveSessionData(): Promise<void>;
|
||||
|
||||
// Sync (persistent, encrypted) operations
|
||||
getSyncPermissions(): EncryptedPermission[];
|
||||
saveSyncPermissions(permissions: EncryptedPermission[]): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of PermissionRepository using browser storage.
|
||||
*/
|
||||
export class BrowserPermissionRepository implements PermissionRepository {
|
||||
constructor(
|
||||
private readonly storage: PermissionStorageAdapter,
|
||||
private readonly encryption: EncryptionService
|
||||
) {}
|
||||
|
||||
async findById(id: PermissionId): Promise<PermissionSnapshot | undefined> {
|
||||
const permissions = this.storage.getSessionPermissions();
|
||||
return permissions.find((p) => p.id === id.value);
|
||||
}
|
||||
|
||||
async find(query: PermissionQuery): Promise<PermissionSnapshot[]> {
|
||||
let permissions = this.storage.getSessionPermissions();
|
||||
|
||||
if (query.identityId) {
|
||||
const identityIdValue = query.identityId.value;
|
||||
permissions = permissions.filter((p) => p.identityId === identityIdValue);
|
||||
}
|
||||
if (query.host) {
|
||||
const host = query.host;
|
||||
permissions = permissions.filter((p) => p.host === host);
|
||||
}
|
||||
if (query.method) {
|
||||
const method = query.method;
|
||||
permissions = permissions.filter((p) => p.method === method);
|
||||
}
|
||||
if (query.kind !== undefined) {
|
||||
const kind = query.kind;
|
||||
permissions = permissions.filter((p) => p.kind === kind);
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
async findExact(
|
||||
identityId: IdentityId,
|
||||
host: string,
|
||||
method: ExtensionMethod,
|
||||
kind?: number
|
||||
): Promise<PermissionSnapshot | undefined> {
|
||||
const permissions = this.storage.getSessionPermissions();
|
||||
return permissions.find(
|
||||
(p) =>
|
||||
p.identityId === identityId.value &&
|
||||
p.host === host &&
|
||||
p.method === method &&
|
||||
(kind === undefined ? p.kind === undefined : p.kind === kind)
|
||||
);
|
||||
}
|
||||
|
||||
async findByIdentity(identityId: IdentityId): Promise<PermissionSnapshot[]> {
|
||||
const permissions = this.storage.getSessionPermissions();
|
||||
return permissions.filter((p) => p.identityId === identityId.value);
|
||||
}
|
||||
|
||||
async findAll(): Promise<PermissionSnapshot[]> {
|
||||
return this.storage.getSessionPermissions();
|
||||
}
|
||||
|
||||
async save(permission: PermissionSnapshot): Promise<void> {
|
||||
const sessionPermissions = this.storage.getSessionPermissions();
|
||||
const existingIndex = sessionPermissions.findIndex((p) => p.id === permission.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
sessionPermissions[existingIndex] = permission;
|
||||
} else {
|
||||
sessionPermissions.push(permission);
|
||||
}
|
||||
|
||||
this.storage.setSessionPermissions(sessionPermissions);
|
||||
await this.storage.saveSessionData();
|
||||
|
||||
// Encrypt and save to sync storage
|
||||
const encryptedPermission = await this.encryptPermission(permission);
|
||||
const syncPermissions = this.storage.getSyncPermissions();
|
||||
|
||||
// Find by decrypting IDs (expensive but necessary for updates)
|
||||
let syncIndex = -1;
|
||||
for (let i = 0; i < syncPermissions.length; i++) {
|
||||
try {
|
||||
const decryptedId = await this.encryption.decryptString(syncPermissions[i].id);
|
||||
if (decryptedId === permission.id) {
|
||||
syncIndex = i;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip corrupted entries
|
||||
}
|
||||
}
|
||||
|
||||
if (syncIndex >= 0) {
|
||||
syncPermissions[syncIndex] = encryptedPermission;
|
||||
} else {
|
||||
syncPermissions.push(encryptedPermission);
|
||||
}
|
||||
|
||||
await this.storage.saveSyncPermissions(syncPermissions);
|
||||
}
|
||||
|
||||
async delete(id: PermissionId): Promise<boolean> {
|
||||
const sessionPermissions = this.storage.getSessionPermissions();
|
||||
const initialLength = sessionPermissions.length;
|
||||
const filtered = sessionPermissions.filter((p) => p.id !== id.value);
|
||||
|
||||
if (filtered.length === initialLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.storage.setSessionPermissions(filtered);
|
||||
await this.storage.saveSessionData();
|
||||
|
||||
// Remove from sync storage
|
||||
const encryptedId = await this.encryption.encryptString(id.value);
|
||||
const syncPermissions = this.storage.getSyncPermissions();
|
||||
const filteredSync = syncPermissions.filter((p) => p.id !== encryptedId);
|
||||
await this.storage.saveSyncPermissions(filteredSync);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteByIdentity(identityId: IdentityId): Promise<number> {
|
||||
const sessionPermissions = this.storage.getSessionPermissions();
|
||||
const initialLength = sessionPermissions.length;
|
||||
const filtered = sessionPermissions.filter((p) => p.identityId !== identityId.value);
|
||||
const deletedCount = initialLength - filtered.length;
|
||||
|
||||
if (deletedCount === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.storage.setSessionPermissions(filtered);
|
||||
await this.storage.saveSessionData();
|
||||
|
||||
// Remove from sync storage
|
||||
const encryptedIdentityId = await this.encryption.encryptString(identityId.value);
|
||||
const syncPermissions = this.storage.getSyncPermissions();
|
||||
const filteredSync = syncPermissions.filter((p) => p.identityId !== encryptedIdentityId);
|
||||
await this.storage.saveSyncPermissions(filteredSync);
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
async count(query?: PermissionQuery): Promise<number> {
|
||||
if (query) {
|
||||
const results = await this.find(query);
|
||||
return results.length;
|
||||
}
|
||||
return this.storage.getSessionPermissions().length;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Private helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private async encryptPermission(permission: PermissionSnapshot): Promise<EncryptedPermission> {
|
||||
const encrypted: EncryptedPermission = {
|
||||
id: await this.encryption.encryptString(permission.id),
|
||||
identityId: await this.encryption.encryptString(permission.identityId),
|
||||
host: await this.encryption.encryptString(permission.host),
|
||||
method: await this.encryption.encryptString(permission.method),
|
||||
methodPolicy: await this.encryption.encryptString(permission.methodPolicy),
|
||||
};
|
||||
|
||||
if (permission.kind !== undefined) {
|
||||
encrypted.kind = await this.encryption.encryptNumber(permission.kind);
|
||||
}
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a BrowserPermissionRepository.
|
||||
*/
|
||||
export function createPermissionRepository(
|
||||
storage: PermissionStorageAdapter,
|
||||
encryption: EncryptionService
|
||||
): PermissionRepository {
|
||||
return new BrowserPermissionRepository(storage, encryption);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import {
|
||||
RelayRepositoryError,
|
||||
RelayErrorCode,
|
||||
} from '../../domain/repositories/relay-repository';
|
||||
import type {
|
||||
RelayRepository,
|
||||
RelaySnapshot,
|
||||
RelayQuery,
|
||||
} from '../../domain/repositories/relay-repository';
|
||||
import { IdentityId, RelayId } from '../../domain/value-objects';
|
||||
import { EncryptionService } from '../encryption';
|
||||
|
||||
/**
|
||||
* Encrypted relay as stored in browser sync storage.
|
||||
*/
|
||||
interface EncryptedRelay {
|
||||
id: string;
|
||||
identityId: string;
|
||||
url: string;
|
||||
read: string;
|
||||
write: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage adapter interface for relays.
|
||||
*/
|
||||
export interface RelayStorageAdapter {
|
||||
// Session (in-memory, decrypted) operations
|
||||
getSessionRelays(): RelaySnapshot[];
|
||||
setSessionRelays(relays: RelaySnapshot[]): void;
|
||||
saveSessionData(): Promise<void>;
|
||||
|
||||
// Sync (persistent, encrypted) operations
|
||||
getSyncRelays(): EncryptedRelay[];
|
||||
saveSyncRelays(relays: EncryptedRelay[]): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of RelayRepository using browser storage.
|
||||
*/
|
||||
export class BrowserRelayRepository implements RelayRepository {
|
||||
constructor(
|
||||
private readonly storage: RelayStorageAdapter,
|
||||
private readonly encryption: EncryptionService
|
||||
) {}
|
||||
|
||||
async findById(id: RelayId): Promise<RelaySnapshot | undefined> {
|
||||
const relays = this.storage.getSessionRelays();
|
||||
return relays.find((r) => r.id === id.value);
|
||||
}
|
||||
|
||||
async find(query: RelayQuery): Promise<RelaySnapshot[]> {
|
||||
let relays = this.storage.getSessionRelays();
|
||||
|
||||
if (query.identityId) {
|
||||
const identityIdValue = query.identityId.value;
|
||||
relays = relays.filter((r) => r.identityId === identityIdValue);
|
||||
}
|
||||
if (query.url) {
|
||||
const urlLower = query.url.toLowerCase();
|
||||
relays = relays.filter((r) => r.url.toLowerCase() === urlLower);
|
||||
}
|
||||
if (query.read !== undefined) {
|
||||
const read = query.read;
|
||||
relays = relays.filter((r) => r.read === read);
|
||||
}
|
||||
if (query.write !== undefined) {
|
||||
const write = query.write;
|
||||
relays = relays.filter((r) => r.write === write);
|
||||
}
|
||||
|
||||
return relays;
|
||||
}
|
||||
|
||||
async findByUrl(identityId: IdentityId, url: string): Promise<RelaySnapshot | undefined> {
|
||||
const relays = this.storage.getSessionRelays();
|
||||
return relays.find(
|
||||
(r) =>
|
||||
r.identityId === identityId.value &&
|
||||
r.url.toLowerCase() === url.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
async findByIdentity(identityId: IdentityId): Promise<RelaySnapshot[]> {
|
||||
const relays = this.storage.getSessionRelays();
|
||||
return relays.filter((r) => r.identityId === identityId.value);
|
||||
}
|
||||
|
||||
async findAll(): Promise<RelaySnapshot[]> {
|
||||
return this.storage.getSessionRelays();
|
||||
}
|
||||
|
||||
async save(relay: RelaySnapshot): Promise<void> {
|
||||
// Check for duplicate URL for the same identity (excluding self)
|
||||
const existing = await this.findByUrl(
|
||||
IdentityId.from(relay.identityId),
|
||||
relay.url
|
||||
);
|
||||
if (existing && existing.id !== relay.id) {
|
||||
throw new RelayRepositoryError(
|
||||
'A relay with the same URL already exists for this identity',
|
||||
RelayErrorCode.DUPLICATE_URL
|
||||
);
|
||||
}
|
||||
|
||||
const sessionRelays = this.storage.getSessionRelays();
|
||||
const existingIndex = sessionRelays.findIndex((r) => r.id === relay.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
sessionRelays[existingIndex] = relay;
|
||||
} else {
|
||||
sessionRelays.push(relay);
|
||||
}
|
||||
|
||||
this.storage.setSessionRelays(sessionRelays);
|
||||
await this.storage.saveSessionData();
|
||||
|
||||
// Encrypt and save to sync storage
|
||||
const encryptedRelay = await this.encryptRelay(relay);
|
||||
const syncRelays = this.storage.getSyncRelays();
|
||||
|
||||
// Find by decrypting IDs
|
||||
let syncIndex = -1;
|
||||
for (let i = 0; i < syncRelays.length; i++) {
|
||||
try {
|
||||
const decryptedId = await this.encryption.decryptString(syncRelays[i].id);
|
||||
if (decryptedId === relay.id) {
|
||||
syncIndex = i;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Skip corrupted entries
|
||||
}
|
||||
}
|
||||
|
||||
if (syncIndex >= 0) {
|
||||
syncRelays[syncIndex] = encryptedRelay;
|
||||
} else {
|
||||
syncRelays.push(encryptedRelay);
|
||||
}
|
||||
|
||||
await this.storage.saveSyncRelays(syncRelays);
|
||||
}
|
||||
|
||||
async delete(id: RelayId): Promise<boolean> {
|
||||
const sessionRelays = this.storage.getSessionRelays();
|
||||
const initialLength = sessionRelays.length;
|
||||
const filtered = sessionRelays.filter((r) => r.id !== id.value);
|
||||
|
||||
if (filtered.length === initialLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.storage.setSessionRelays(filtered);
|
||||
await this.storage.saveSessionData();
|
||||
|
||||
// Remove from sync storage
|
||||
const encryptedId = await this.encryption.encryptString(id.value);
|
||||
const syncRelays = this.storage.getSyncRelays();
|
||||
const filteredSync = syncRelays.filter((r) => r.id !== encryptedId);
|
||||
await this.storage.saveSyncRelays(filteredSync);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteByIdentity(identityId: IdentityId): Promise<number> {
|
||||
const sessionRelays = this.storage.getSessionRelays();
|
||||
const initialLength = sessionRelays.length;
|
||||
const filtered = sessionRelays.filter((r) => r.identityId !== identityId.value);
|
||||
const deletedCount = initialLength - filtered.length;
|
||||
|
||||
if (deletedCount === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.storage.setSessionRelays(filtered);
|
||||
await this.storage.saveSessionData();
|
||||
|
||||
// Remove from sync storage
|
||||
const encryptedIdentityId = await this.encryption.encryptString(identityId.value);
|
||||
const syncRelays = this.storage.getSyncRelays();
|
||||
const filteredSync = syncRelays.filter((r) => r.identityId !== encryptedIdentityId);
|
||||
await this.storage.saveSyncRelays(filteredSync);
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
async count(query?: RelayQuery): Promise<number> {
|
||||
if (query) {
|
||||
const results = await this.find(query);
|
||||
return results.length;
|
||||
}
|
||||
return this.storage.getSessionRelays().length;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Private helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private async encryptRelay(relay: RelaySnapshot): Promise<EncryptedRelay> {
|
||||
return {
|
||||
id: await this.encryption.encryptString(relay.id),
|
||||
identityId: await this.encryption.encryptString(relay.identityId),
|
||||
url: await this.encryption.encryptString(relay.url),
|
||||
read: await this.encryption.encryptBoolean(relay.read),
|
||||
write: await this.encryption.encryptBoolean(relay.write),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create a BrowserRelayRepository.
|
||||
*/
|
||||
export function createRelayRepository(
|
||||
storage: RelayStorageAdapter,
|
||||
encryption: EncryptionService
|
||||
): RelayRepository {
|
||||
return new BrowserRelayRepository(storage, encryption);
|
||||
}
|
||||
Reference in New Issue
Block a user