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