@angular-helpers/storage 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,20 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Injectable, signal, inject, DestroyRef, computed, Inject, Optional } from '@angular/core';
2
+ import { InjectionToken, Inject, Injectable, signal, inject, DestroyRef, computed, Optional } from '@angular/core';
3
3
 
4
4
  /**
5
5
  * Token de inyección para el Transporte de Almacenamiento activo
6
6
  */
7
7
  const STORAGE_TRANSPORT = new InjectionToken('STORAGE_TRANSPORT');
8
8
 
9
+ /**
10
+ * Injection token to configure the AES-GCM encryption passphrase for StorageTransports.
11
+ * Defaults to the legacy string for seamless backward compatibility.
12
+ */
13
+ const SECURE_STORAGE_PASSPHRASE = new InjectionToken('SECURE_STORAGE_PASSPHRASE', {
14
+ providedIn: 'root',
15
+ factory: () => 'angular-helpers-secure-storage-passphrase',
16
+ });
17
+
9
18
  const ENCRYPTION_SALT = new Uint8Array([7, 21, 14, 9, 3, 18, 5, 12, 1, 20, 16, 2, 8, 15, 6, 11]);
10
19
  function getCrypto() {
11
20
  if (typeof crypto !== 'undefined')
@@ -98,47 +107,54 @@ async function deserializeData(text, useToon = false) {
98
107
  return JSON.parse(text);
99
108
  }
100
109
  class LocalStorageTransport {
110
+ secretPassphrase;
101
111
  VIRTUAL_BASE_URL = 'https://angular-helpers.local/storage-cache/';
102
- SECRET_PASSPHRASE = 'angular-helpers-secure-storage-passphrase';
103
112
  storageType = 'local';
104
113
  encrypt = false;
105
114
  dbName = 'ah_db';
106
115
  storeName = 'kv';
107
116
  cacheName = 'ah_cache';
108
- constructor() {
117
+ constructor(secretPassphrase = 'fallback-passphrase-angular-helpers-default-key-sec') {
118
+ this.secretPassphrase = secretPassphrase;
109
119
  // If running in worker context, fall back to indexeddb as default L2
110
120
  if (typeof window === 'undefined') {
111
121
  this.storageType = 'indexeddb';
112
122
  }
113
123
  }
114
- async openDB() {
124
+ async openDB(dbName, storeName) {
115
125
  return new Promise((resolve, reject) => {
116
- const request = indexedDB.open(this.dbName, 1);
126
+ const request = indexedDB.open(dbName, 1);
117
127
  request.onupgradeneeded = () => {
118
128
  const db = request.result;
119
- if (!db.objectStoreNames.contains(this.storeName)) {
120
- db.createObjectStore(this.storeName);
129
+ if (!db.objectStoreNames.contains(storeName)) {
130
+ db.createObjectStore(storeName);
121
131
  }
122
132
  };
123
133
  request.onsuccess = () => resolve(request.result);
124
134
  request.onerror = () => reject(request.error);
125
135
  });
126
136
  }
127
- async read(key, useToon) {
137
+ async read(key, options) {
128
138
  try {
129
- if (this.storageType === 'cacheapi') {
139
+ const storageType = options?.storageType ?? this.storageType;
140
+ const encryptData = options?.encrypt ?? this.encrypt;
141
+ const dbName = options?.dbName ?? this.dbName;
142
+ const storeName = options?.storeName ?? this.storeName;
143
+ const cacheName = options?.cacheName ?? this.cacheName;
144
+ const useToon = options?.serializer === 'toon';
145
+ if (storageType === 'cacheapi') {
130
146
  const cacheContext = getCaches();
131
147
  if (!cacheContext) {
132
148
  throw new Error('Cache API is not supported in this environment');
133
149
  }
134
- const cache = await cacheContext.open(this.cacheName);
150
+ const cache = await cacheContext.open(cacheName);
135
151
  const url = `${this.VIRTUAL_BASE_URL}${key}`;
136
152
  const response = await cache.match(url);
137
153
  if (!response)
138
154
  return undefined;
139
- if (this.encrypt) {
155
+ if (encryptData) {
140
156
  const cipherText = await response.text();
141
- const plainText = await decrypt(cipherText, this.SECRET_PASSPHRASE);
157
+ const plainText = await decrypt(cipherText, this.secretPassphrase);
142
158
  return await deserializeData(plainText, useToon);
143
159
  }
144
160
  if (useToon) {
@@ -149,11 +165,11 @@ class LocalStorageTransport {
149
165
  return (await response.json());
150
166
  }
151
167
  }
152
- if (this.storageType === 'indexeddb') {
153
- const db = await this.openDB();
168
+ if (storageType === 'indexeddb') {
169
+ const db = await this.openDB(dbName, storeName);
154
170
  return new Promise((resolve, reject) => {
155
- const transaction = db.transaction(this.storeName, 'readonly');
156
- const store = transaction.objectStore(this.storeName);
171
+ const transaction = db.transaction(storeName, 'readonly');
172
+ const store = transaction.objectStore(storeName);
157
173
  const request = store.get(key);
158
174
  request.onsuccess = async () => {
159
175
  const rawVal = request.result;
@@ -162,8 +178,8 @@ class LocalStorageTransport {
162
178
  return;
163
179
  }
164
180
  try {
165
- if (this.encrypt) {
166
- const plainText = await decrypt(rawVal, this.SECRET_PASSPHRASE);
181
+ if (encryptData) {
182
+ const plainText = await decrypt(rawVal, this.secretPassphrase);
167
183
  resolve(await deserializeData(plainText, useToon));
168
184
  }
169
185
  else {
@@ -179,14 +195,14 @@ class LocalStorageTransport {
179
195
  }
180
196
  // Local or Session Storage (Main Thread Only)
181
197
  if (typeof window === 'undefined') {
182
- throw new Error(`Storage type '${this.storageType}' is not supported in Worker context`);
198
+ throw new Error(`Storage type '${storageType}' is not supported in Worker context`);
183
199
  }
184
- const storage = this.storageType === 'session' ? window.sessionStorage : window.localStorage;
200
+ const storage = storageType === 'session' ? window.sessionStorage : window.localStorage;
185
201
  const raw = storage.getItem(key);
186
202
  if (raw === null)
187
203
  return undefined;
188
- if (this.encrypt) {
189
- const plainText = await decrypt(raw, this.SECRET_PASSPHRASE);
204
+ if (encryptData) {
205
+ const plainText = await decrypt(raw, this.secretPassphrase);
190
206
  return await deserializeData(plainText, useToon);
191
207
  }
192
208
  return await deserializeData(raw, useToon);
@@ -196,33 +212,39 @@ class LocalStorageTransport {
196
212
  return undefined;
197
213
  }
198
214
  }
199
- async write(key, data, useToon) {
215
+ async write(key, data, options) {
200
216
  try {
217
+ const storageType = options?.storageType ?? this.storageType;
218
+ const encryptData = options?.encrypt ?? this.encrypt;
219
+ const dbName = options?.dbName ?? this.dbName;
220
+ const storeName = options?.storeName ?? this.storeName;
221
+ const cacheName = options?.cacheName ?? this.cacheName;
222
+ const useToon = options?.serializer === 'toon';
201
223
  let payload = await serializeData(data, useToon);
202
- if (this.encrypt) {
203
- payload = await encrypt(payload, this.SECRET_PASSPHRASE);
224
+ if (encryptData) {
225
+ payload = await encrypt(payload, this.secretPassphrase);
204
226
  }
205
- if (this.storageType === 'cacheapi') {
227
+ if (storageType === 'cacheapi') {
206
228
  const cacheContext = getCaches();
207
229
  if (!cacheContext) {
208
230
  throw new Error('Cache API is not supported in this environment');
209
231
  }
210
- const cache = await cacheContext.open(this.cacheName);
232
+ const cache = await cacheContext.open(cacheName);
211
233
  const url = `${this.VIRTUAL_BASE_URL}${key}`;
212
234
  const response = new Response(payload, {
213
235
  headers: {
214
- 'Content-Type': useToon && !this.encrypt ? 'application/toon' : 'application/json',
236
+ 'Content-Type': useToon && !encryptData ? 'application/toon' : 'application/json',
215
237
  'X-Storage-Date': new Date().toISOString(),
216
238
  },
217
239
  });
218
240
  await cache.put(url, response);
219
241
  return;
220
242
  }
221
- if (this.storageType === 'indexeddb') {
222
- const db = await this.openDB();
243
+ if (storageType === 'indexeddb') {
244
+ const db = await this.openDB(dbName, storeName);
223
245
  return new Promise((resolve, reject) => {
224
- const transaction = db.transaction(this.storeName, 'readwrite');
225
- const store = transaction.objectStore(this.storeName);
246
+ const transaction = db.transaction(storeName, 'readwrite');
247
+ const store = transaction.objectStore(storeName);
226
248
  const request = store.put(payload, key);
227
249
  request.onsuccess = () => resolve();
228
250
  request.onerror = () => reject(request.error);
@@ -230,32 +252,36 @@ class LocalStorageTransport {
230
252
  }
231
253
  // Local or Session Storage (Main Thread Only)
232
254
  if (typeof window === 'undefined') {
233
- throw new Error(`Storage type '${this.storageType}' is not supported in Worker context`);
255
+ throw new Error(`Storage type '${storageType}' is not supported in Worker context`);
234
256
  }
235
- const storage = this.storageType === 'session' ? window.sessionStorage : window.localStorage;
257
+ const storage = storageType === 'session' ? window.sessionStorage : window.localStorage;
236
258
  storage.setItem(key, payload);
237
259
  }
238
260
  catch (error) {
239
261
  console.error(`[LocalStorageTransport] Error al escribir clave: ${key}`, error);
240
262
  }
241
263
  }
242
- async delete(key) {
264
+ async delete(key, options) {
243
265
  try {
244
- if (this.storageType === 'cacheapi') {
266
+ const storageType = options?.storageType ?? this.storageType;
267
+ const dbName = options?.dbName ?? this.dbName;
268
+ const storeName = options?.storeName ?? this.storeName;
269
+ const cacheName = options?.cacheName ?? this.cacheName;
270
+ if (storageType === 'cacheapi') {
245
271
  const cacheContext = getCaches();
246
272
  if (!cacheContext) {
247
273
  throw new Error('Cache API is not supported in this environment');
248
274
  }
249
- const cache = await cacheContext.open(this.cacheName);
275
+ const cache = await cacheContext.open(cacheName);
250
276
  const url = `${this.VIRTUAL_BASE_URL}${key}`;
251
277
  await cache.delete(url);
252
278
  return;
253
279
  }
254
- if (this.storageType === 'indexeddb') {
255
- const db = await this.openDB();
280
+ if (storageType === 'indexeddb') {
281
+ const db = await this.openDB(dbName, storeName);
256
282
  return new Promise((resolve, reject) => {
257
- const transaction = db.transaction(this.storeName, 'readwrite');
258
- const store = transaction.objectStore(this.storeName);
283
+ const transaction = db.transaction(storeName, 'readwrite');
284
+ const store = transaction.objectStore(storeName);
259
285
  const request = store.delete(key);
260
286
  request.onsuccess = () => resolve();
261
287
  request.onerror = () => reject(request.error);
@@ -263,9 +289,9 @@ class LocalStorageTransport {
263
289
  }
264
290
  // Local or Session Storage (Main Thread Only)
265
291
  if (typeof window === 'undefined') {
266
- throw new Error(`Storage type '${this.storageType}' is not supported in Worker context`);
292
+ throw new Error(`Storage type '${storageType}' is not supported in Worker context`);
267
293
  }
268
- const storage = this.storageType === 'session' ? window.sessionStorage : window.localStorage;
294
+ const storage = storageType === 'session' ? window.sessionStorage : window.localStorage;
269
295
  storage.removeItem(key);
270
296
  }
271
297
  catch (error) {
@@ -289,13 +315,16 @@ class LocalStorageTransport {
289
315
  window.addEventListener('storage', listener);
290
316
  return () => window.removeEventListener('storage', listener);
291
317
  }
292
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: LocalStorageTransport, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
318
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: LocalStorageTransport, deps: [{ token: SECURE_STORAGE_PASSPHRASE }], target: i0.ɵɵFactoryTarget.Injectable });
293
319
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: LocalStorageTransport, providedIn: 'root' });
294
320
  }
295
321
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: LocalStorageTransport, decorators: [{
296
322
  type: Injectable,
297
323
  args: [{ providedIn: 'root' }]
298
- }], ctorParameters: () => [] });
324
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
325
+ type: Inject,
326
+ args: [SECURE_STORAGE_PASSPHRASE]
327
+ }] }] });
299
328
 
300
329
  class SafeReadonlyMap {
301
330
  _map;
@@ -340,21 +369,9 @@ function injectStorageSignal(key, defaultValue, options) {
340
369
  if (!transport) {
341
370
  transport = inject(LocalStorageTransport);
342
371
  }
343
- // Configurar las propiedades de persistencia si es transporte local
344
- if (transport instanceof LocalStorageTransport) {
345
- transport.storageType = options.storageType;
346
- transport.encrypt = !!options.encrypt;
347
- if (options.dbName)
348
- transport.dbName = options.dbName;
349
- if (options.storeName)
350
- transport.storeName = options.storeName;
351
- if (options.cacheName)
352
- transport.cacheName = options.cacheName;
353
- }
354
- const useToon = options.serializer === 'toon';
355
372
  // 1. Cargar valor inicial de L2
356
373
  transport
357
- .read(key, useToon)
374
+ .read(key, options)
358
375
  .then((value) => {
359
376
  state.set({
360
377
  data: value !== undefined ? value : defaultValue,
@@ -374,7 +391,7 @@ function injectStorageSignal(key, defaultValue, options) {
374
391
  const originalUpdate = state.update.bind(state);
375
392
  const persist = (newData) => {
376
393
  transport
377
- .write(key, newData, useToon)
394
+ .write(key, newData, options)
378
395
  .catch((err) => console.error(`[injectStorageSignal] Error escribiendo clave: ${key}`, err));
379
396
  };
380
397
  const customSignal = state;
@@ -498,21 +515,9 @@ class EntityStore {
498
515
  }
499
516
  initPersistence(key) {
500
517
  const transport = this._resolveTransport();
501
- const useToon = this.options.storageOptions?.serializer === 'toon';
502
- if (transport instanceof LocalStorageTransport && this.options.storageOptions) {
503
- const opts = this.options.storageOptions;
504
- transport.storageType = opts.storageType;
505
- transport.encrypt = !!opts.encrypt;
506
- if (opts.dbName)
507
- transport.dbName = opts.dbName;
508
- if (opts.storeName)
509
- transport.storeName = opts.storeName;
510
- if (opts.cacheName)
511
- transport.cacheName = opts.cacheName;
512
- }
513
518
  this._isRestoring = true;
514
519
  transport
515
- .read(key, useToon)
520
+ .read(key, this.options.storageOptions)
516
521
  .then((data) => {
517
522
  if (data && Array.isArray(data)) {
518
523
  this.setMany(data);
@@ -527,20 +532,8 @@ class EntityStore {
527
532
  if (this._isRestoring || !this.options.persistKey)
528
533
  return;
529
534
  const transport = this._resolveTransport();
530
- const useToon = this.options.storageOptions?.serializer === 'toon';
531
- if (transport instanceof LocalStorageTransport && this.options.storageOptions) {
532
- const opts = this.options.storageOptions;
533
- transport.storageType = opts.storageType;
534
- transport.encrypt = !!opts.encrypt;
535
- if (opts.dbName)
536
- transport.dbName = opts.dbName;
537
- if (opts.storeName)
538
- transport.storeName = opts.storeName;
539
- if (opts.cacheName)
540
- transport.cacheName = opts.cacheName;
541
- }
542
535
  transport
543
- .write(this.options.persistKey, this.list(), useToon)
536
+ .write(this.options.persistKey, this.list(), this.options.storageOptions)
544
537
  .catch((err) => console.error(`[EntityStore] Error guardando entidades persistidas:`, err));
545
538
  }
546
539
  }
@@ -604,17 +597,19 @@ class WorkerStorageTransport {
604
597
  this.pendingRequests.clear();
605
598
  };
606
599
  }
607
- read(key, useToon) {
600
+ read(key, options) {
608
601
  this.ensureWorker();
609
- return this.postRequest('read', key, undefined, { useToon });
602
+ const useToon = options?.serializer === 'toon';
603
+ return this.postRequest('read', key, undefined, { ...options, useToon });
610
604
  }
611
- write(key, data, useToon) {
605
+ write(key, data, options) {
612
606
  this.ensureWorker();
613
- return this.postRequest('write', key, data, { useToon });
607
+ const useToon = options?.serializer === 'toon';
608
+ return this.postRequest('write', key, data, { ...options, useToon });
614
609
  }
615
- delete(key) {
610
+ delete(key, options) {
616
611
  this.ensureWorker();
617
- return this.postRequest('delete', key);
612
+ return this.postRequest('delete', key, undefined, options);
618
613
  }
619
614
  onChange(key, callback) {
620
615
  if (!this.changeCallbacks.has(key)) {
@@ -672,4 +667,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
672
667
  * Generated bundle index. Do not edit.
673
668
  */
674
669
 
675
- export { EntityStore, LocalStorageTransport, STORAGE_TRANSPORT, STORAGE_WORKER_FACTORY, SafeReadonlyMap, WorkerStorageTransport, injectEntityStore, injectStorageSignal };
670
+ export { EntityStore, LocalStorageTransport, SECURE_STORAGE_PASSPHRASE, STORAGE_TRANSPORT, STORAGE_WORKER_FACTORY, SafeReadonlyMap, WorkerStorageTransport, injectEntityStore, injectStorageSignal };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-helpers/storage",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Sistema de almacenamiento reactivo premium para Angular con soporte para Cache API, IndexedDB, compresión TOON y blindaje en runtime.",
5
5
  "homepage": "https://gaspar1992.github.io/angular-helpers/docs/storage",
6
6
  "repository": {
@@ -27,15 +27,15 @@ interface StorageTransport {
27
27
  /**
28
28
  * Lee un valor persistido asíncronamente
29
29
  */
30
- read<T>(key: string, useToon?: boolean): Promise<T | undefined>;
30
+ read<T>(key: string, options?: StorageSignalOptions): Promise<T | undefined>;
31
31
  /**
32
32
  * Escribe un valor persistido asíncronamente
33
33
  */
34
- write<T>(key: string, data: T, useToon?: boolean): Promise<void>;
34
+ write<T>(key: string, data: T, options?: StorageSignalOptions): Promise<void>;
35
35
  /**
36
36
  * Elimina un valor persistido
37
37
  */
38
- delete(key: string): Promise<void>;
38
+ delete(key: string, options?: StorageSignalOptions): Promise<void>;
39
39
  /**
40
40
  * Suscribe un callback para cambios externos (sincronización multi-pestaña)
41
41
  * Devuelve una función para des-suscribirse.
@@ -48,18 +48,18 @@ interface StorageTransport {
48
48
  declare const STORAGE_TRANSPORT: InjectionToken<StorageTransport>;
49
49
 
50
50
  declare class LocalStorageTransport implements StorageTransport {
51
+ private readonly secretPassphrase;
51
52
  private readonly VIRTUAL_BASE_URL;
52
- private readonly SECRET_PASSPHRASE;
53
53
  storageType: 'local' | 'session' | 'indexeddb' | 'cacheapi';
54
54
  encrypt: boolean;
55
55
  dbName: string;
56
56
  storeName: string;
57
57
  cacheName: string;
58
- constructor();
58
+ constructor(secretPassphrase?: string);
59
59
  private openDB;
60
- read<T>(key: string, useToon?: boolean): Promise<T | undefined>;
61
- write<T>(key: string, data: T, useToon?: boolean): Promise<void>;
62
- delete(key: string): Promise<void>;
60
+ read<T>(key: string, options?: StorageSignalOptions): Promise<T | undefined>;
61
+ write<T>(key: string, data: T, options?: StorageSignalOptions): Promise<void>;
62
+ delete(key: string, options?: StorageSignalOptions): Promise<void>;
63
63
  onChange<T>(key: string, callback: (value: T) => void): () => void;
64
64
  static ɵfac: i0.ɵɵFactoryDeclaration<LocalStorageTransport, never>;
65
65
  static ɵprov: i0.ɵɵInjectableDeclaration<LocalStorageTransport>;
@@ -141,6 +141,12 @@ interface WorkerStorageResponse {
141
141
  */
142
142
  declare const STORAGE_WORKER_FACTORY: InjectionToken<() => Worker>;
143
143
 
144
+ /**
145
+ * Injection token to configure the AES-GCM encryption passphrase for StorageTransports.
146
+ * Defaults to the legacy string for seamless backward compatibility.
147
+ */
148
+ declare const SECURE_STORAGE_PASSPHRASE: InjectionToken<string>;
149
+
144
150
  declare class WorkerStorageTransport implements StorageTransport {
145
151
  private readonly workerFactory?;
146
152
  private worker?;
@@ -148,9 +154,9 @@ declare class WorkerStorageTransport implements StorageTransport {
148
154
  private readonly changeCallbacks;
149
155
  constructor(workerFactory?: () => Worker);
150
156
  private initWorker;
151
- read<T>(key: string, useToon?: boolean): Promise<T | undefined>;
152
- write<T>(key: string, data: T, useToon?: boolean): Promise<void>;
153
- delete(key: string): Promise<void>;
157
+ read<T>(key: string, options?: StorageSignalOptions): Promise<T | undefined>;
158
+ write<T>(key: string, data: T, options?: StorageSignalOptions): Promise<void>;
159
+ delete(key: string, options?: StorageSignalOptions): Promise<void>;
154
160
  onChange<T>(key: string, callback: (value: T) => void): () => void;
155
161
  private ensureWorker;
156
162
  private generateId;
@@ -159,5 +165,5 @@ declare class WorkerStorageTransport implements StorageTransport {
159
165
  static ɵprov: i0.ɵɵInjectableDeclaration<WorkerStorageTransport>;
160
166
  }
161
167
 
162
- export { EntityStore, LocalStorageTransport, STORAGE_TRANSPORT, STORAGE_WORKER_FACTORY, SafeReadonlyMap, WorkerStorageTransport, injectEntityStore, injectStorageSignal };
168
+ export { EntityStore, LocalStorageTransport, SECURE_STORAGE_PASSPHRASE, STORAGE_TRANSPORT, STORAGE_WORKER_FACTORY, SafeReadonlyMap, WorkerStorageTransport, injectEntityStore, injectStorageSignal };
163
169
  export type { EntityStoreOptions, StorageSignalOptions, StorageSignalState, StorageTransport, WorkerStorageAction, WorkerStorageRequest, WorkerStorageResponse };