permission-repository.impl.ts raw

   1  import type {
   2    PermissionRepository,
   3    PermissionSnapshot,
   4    PermissionQuery,
   5    ExtensionMethod,
   6  } from '../../domain/repositories/permission-repository';
   7  import { IdentityId, PermissionId } from '../../domain/value-objects';
   8  import { EncryptionService } from '../encryption';
   9  
  10  /**
  11   * Encrypted permission as stored in browser sync storage.
  12   */
  13  interface EncryptedPermission {
  14    id: string;
  15    identityId: string;
  16    host: string;
  17    method: string;
  18    methodPolicy: string;
  19    kind?: string;
  20  }
  21  
  22  /**
  23   * Storage adapter interface for permissions.
  24   */
  25  export interface PermissionStorageAdapter {
  26    // Session (in-memory, decrypted) operations
  27    getSessionPermissions(): PermissionSnapshot[];
  28    setSessionPermissions(permissions: PermissionSnapshot[]): void;
  29    saveSessionData(): Promise<void>;
  30  
  31    // Sync (persistent, encrypted) operations
  32    getSyncPermissions(): EncryptedPermission[];
  33    saveSyncPermissions(permissions: EncryptedPermission[]): Promise<void>;
  34  }
  35  
  36  /**
  37   * Implementation of PermissionRepository using browser storage.
  38   */
  39  export class BrowserPermissionRepository implements PermissionRepository {
  40    constructor(
  41      private readonly storage: PermissionStorageAdapter,
  42      private readonly encryption: EncryptionService
  43    ) {}
  44  
  45    async findById(id: PermissionId): Promise<PermissionSnapshot | undefined> {
  46      const permissions = this.storage.getSessionPermissions();
  47      return permissions.find((p) => p.id === id.value);
  48    }
  49  
  50    async find(query: PermissionQuery): Promise<PermissionSnapshot[]> {
  51      let permissions = this.storage.getSessionPermissions();
  52  
  53      if (query.identityId) {
  54        const identityIdValue = query.identityId.value;
  55        permissions = permissions.filter((p) => p.identityId === identityIdValue);
  56      }
  57      if (query.host) {
  58        const host = query.host;
  59        permissions = permissions.filter((p) => p.host === host);
  60      }
  61      if (query.method) {
  62        const method = query.method;
  63        permissions = permissions.filter((p) => p.method === method);
  64      }
  65      if (query.kind !== undefined) {
  66        const kind = query.kind;
  67        permissions = permissions.filter((p) => p.kind === kind);
  68      }
  69  
  70      return permissions;
  71    }
  72  
  73    async findExact(
  74      identityId: IdentityId,
  75      host: string,
  76      method: ExtensionMethod,
  77      kind?: number
  78    ): Promise<PermissionSnapshot | undefined> {
  79      const permissions = this.storage.getSessionPermissions();
  80      return permissions.find(
  81        (p) =>
  82          p.identityId === identityId.value &&
  83          p.host === host &&
  84          p.method === method &&
  85          (kind === undefined ? p.kind === undefined : p.kind === kind)
  86      );
  87    }
  88  
  89    async findByIdentity(identityId: IdentityId): Promise<PermissionSnapshot[]> {
  90      const permissions = this.storage.getSessionPermissions();
  91      return permissions.filter((p) => p.identityId === identityId.value);
  92    }
  93  
  94    async findAll(): Promise<PermissionSnapshot[]> {
  95      return this.storage.getSessionPermissions();
  96    }
  97  
  98    async save(permission: PermissionSnapshot): Promise<void> {
  99      const sessionPermissions = this.storage.getSessionPermissions();
 100      const existingIndex = sessionPermissions.findIndex((p) => p.id === permission.id);
 101  
 102      if (existingIndex >= 0) {
 103        sessionPermissions[existingIndex] = permission;
 104      } else {
 105        sessionPermissions.push(permission);
 106      }
 107  
 108      this.storage.setSessionPermissions(sessionPermissions);
 109      await this.storage.saveSessionData();
 110  
 111      // Encrypt and save to sync storage
 112      const encryptedPermission = await this.encryptPermission(permission);
 113      const syncPermissions = this.storage.getSyncPermissions();
 114  
 115      // Find by decrypting IDs (expensive but necessary for updates)
 116      let syncIndex = -1;
 117      for (let i = 0; i < syncPermissions.length; i++) {
 118        try {
 119          const decryptedId = await this.encryption.decryptString(syncPermissions[i].id);
 120          if (decryptedId === permission.id) {
 121            syncIndex = i;
 122            break;
 123          }
 124        } catch {
 125          // Skip corrupted entries
 126        }
 127      }
 128  
 129      if (syncIndex >= 0) {
 130        syncPermissions[syncIndex] = encryptedPermission;
 131      } else {
 132        syncPermissions.push(encryptedPermission);
 133      }
 134  
 135      await this.storage.saveSyncPermissions(syncPermissions);
 136    }
 137  
 138    async delete(id: PermissionId): Promise<boolean> {
 139      const sessionPermissions = this.storage.getSessionPermissions();
 140      const initialLength = sessionPermissions.length;
 141      const filtered = sessionPermissions.filter((p) => p.id !== id.value);
 142  
 143      if (filtered.length === initialLength) {
 144        return false;
 145      }
 146  
 147      this.storage.setSessionPermissions(filtered);
 148      await this.storage.saveSessionData();
 149  
 150      // Remove from sync storage
 151      const encryptedId = await this.encryption.encryptString(id.value);
 152      const syncPermissions = this.storage.getSyncPermissions();
 153      const filteredSync = syncPermissions.filter((p) => p.id !== encryptedId);
 154      await this.storage.saveSyncPermissions(filteredSync);
 155  
 156      return true;
 157    }
 158  
 159    async deleteByIdentity(identityId: IdentityId): Promise<number> {
 160      const sessionPermissions = this.storage.getSessionPermissions();
 161      const initialLength = sessionPermissions.length;
 162      const filtered = sessionPermissions.filter((p) => p.identityId !== identityId.value);
 163      const deletedCount = initialLength - filtered.length;
 164  
 165      if (deletedCount === 0) {
 166        return 0;
 167      }
 168  
 169      this.storage.setSessionPermissions(filtered);
 170      await this.storage.saveSessionData();
 171  
 172      // Remove from sync storage
 173      const encryptedIdentityId = await this.encryption.encryptString(identityId.value);
 174      const syncPermissions = this.storage.getSyncPermissions();
 175      const filteredSync = syncPermissions.filter((p) => p.identityId !== encryptedIdentityId);
 176      await this.storage.saveSyncPermissions(filteredSync);
 177  
 178      return deletedCount;
 179    }
 180  
 181    async count(query?: PermissionQuery): Promise<number> {
 182      if (query) {
 183        const results = await this.find(query);
 184        return results.length;
 185      }
 186      return this.storage.getSessionPermissions().length;
 187    }
 188  
 189    // ─────────────────────────────────────────────────────────────────────────
 190    // Private helpers
 191    // ─────────────────────────────────────────────────────────────────────────
 192  
 193    private async encryptPermission(permission: PermissionSnapshot): Promise<EncryptedPermission> {
 194      const encrypted: EncryptedPermission = {
 195        id: await this.encryption.encryptString(permission.id),
 196        identityId: await this.encryption.encryptString(permission.identityId),
 197        host: await this.encryption.encryptString(permission.host),
 198        method: await this.encryption.encryptString(permission.method),
 199        methodPolicy: await this.encryption.encryptString(permission.methodPolicy),
 200      };
 201  
 202      if (permission.kind !== undefined) {
 203        encrypted.kind = await this.encryption.encryptNumber(permission.kind);
 204      }
 205  
 206      return encrypted;
 207    }
 208  }
 209  
 210  /**
 211   * Factory function to create a BrowserPermissionRepository.
 212   */
 213  export function createPermissionRepository(
 214    storage: PermissionStorageAdapter,
 215    encryption: EncryptionService
 216  ): PermissionRepository {
 217    return new BrowserPermissionRepository(storage, encryption);
 218  }
 219