@jeanharo98/typed-storage 0.1.6 → 0.1.8

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.
Files changed (40) hide show
  1. package/README.md +66 -13
  2. package/dist/core/create-storage.d.ts +2 -0
  3. package/dist/core/create-storage.js +31 -0
  4. package/dist/core/memory-storage.d.ts +6 -0
  5. package/dist/core/memory-storage.js +14 -0
  6. package/dist/core/storage-signal.d.ts +2 -0
  7. package/dist/core/storage-signal.js +129 -0
  8. package/dist/create-storage.js +0 -18
  9. package/dist/features/heavy-storage/heavy-storage.d.ts +3 -0
  10. package/dist/features/heavy-storage/heavy-storage.js +50 -0
  11. package/dist/features/heavy-storage/heavy-storage.types.d.ts +16 -0
  12. package/dist/features/heavy-storage/heavy-storage.types.js +1 -0
  13. package/dist/features/heavy-storage/indexeddb-driver.d.ts +4 -0
  14. package/dist/features/heavy-storage/indexeddb-driver.js +40 -0
  15. package/dist/features/migrations.d.ts +1 -0
  16. package/dist/features/migrations.js +34 -0
  17. package/dist/features/xor.d.ts +3 -0
  18. package/dist/features/xor.js +14 -0
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +2 -2
  21. package/dist/storage-signal.js +23 -3
  22. package/dist/types.d.ts +1 -0
  23. package/dist/xor.d.ts +3 -0
  24. package/dist/xor.js +14 -0
  25. package/index.html +10 -31
  26. package/package.json +1 -1
  27. package/src/{create-storage.ts → core/create-storage.ts} +6 -21
  28. package/src/{storage-signal.test.ts → core/storage-signal.test.ts} +40 -0
  29. package/src/{storage-signal.ts → core/storage-signal.ts} +34 -4
  30. package/src/features/heavy-storage/heavy-storage.ts +89 -0
  31. package/src/features/heavy-storage/heavy-storage.types.ts +21 -0
  32. package/src/features/heavy-storage/indexeddb-driver.ts +50 -0
  33. package/src/features/xor.ts +18 -0
  34. package/src/index.ts +2 -2
  35. package/src/types.ts +1 -0
  36. package/src/heavy-storage.ts +0 -146
  37. /package/src/{memory-storage.ts → core/memory-storage.ts} +0 -0
  38. /package/src/{heavy-storage.test.ts → features/heavy-storage/heavy-storage.test.ts} +0 -0
  39. /package/src/{migrations.test.ts → features/migrations.test.ts} +0 -0
  40. /package/src/{migrations.ts → features/migrations.ts} +0 -0
package/README.md CHANGED
@@ -224,7 +224,8 @@ const appStorage = createStorage(schema, options);
224
224
  | `version` | `number` | — | Current schema version — required for migrations |
225
225
  | `migrations` | `Record<number, (data) => data>` | — | Migration functions per version |
226
226
  | `compress` | `boolean` | `false` | Compresses data with LZ-string before storing |
227
- | `encrypt` | `boolean` | `false` | Shows a security warning — see note below |
227
+ | `encrypt` | `boolean` | `false` | Obfuscates data with XOR + Base64 — see security note below |
228
+ | `secret` | `string` | — | Required when `encrypt: true` — the obfuscation key |
228
229
 
229
230
  ---
230
231
 
@@ -279,27 +280,79 @@ appStorage.theme.remove(); // → 'theme changed to: dark' (initialValue)
279
280
 
280
281
  ---
281
282
 
282
- ## 🔒 A note on encryption
283
+ ## 🔒 Encryption (XOR obfuscation)
283
284
 
284
- If you pass `encrypt: true`, typed-storage will display a warning explaining why encrypting values in `localStorage` is not a secure practice the encryption key must live in the frontend and is accessible to anyone who inspects your code.
285
+ `typed-storage` can obfuscate values using XOR + Base64 before storing them. This is **not real cryptography**read this section carefully before using it.
285
286
 
286
- For sensitive data such as auth tokens or personal information, use **httpOnly cookies** set by your server:
287
+ ```typescript
288
+ const secureStorage = createStorage({
289
+ token: ''
290
+ }, {
291
+ encrypt: true,
292
+ secret: 'your-secret-key', // required when encrypt is true
293
+ ttl: 3600000 // recommended — expire alongside your real token
294
+ });
295
+
296
+ secureStorage.token.set('eyJhbGciOiJIUzI1NiJ9.xxx.yyy');
297
+ // Stored in localStorage as obfuscated text, not the readable JWT
298
+
299
+ const token = secureStorage.token();
300
+ // Automatically decrypted — returns the real JWT
301
+ ```
302
+
303
+ ### ⚠️ What this actually protects against
304
+
305
+ ```
306
+ ✅ Hides the value from casual inspection in DevTools/Application/Storage
307
+ ✅ Discourages non-technical users from reading or copying the value
308
+ ✅ Combined with ttl, expires alongside your backend token
309
+
310
+ ❌ Does NOT protect against a technical attacker
311
+ ❌ Does NOT protect against debugger breakpoints — the secret and
312
+ decrypted value are visible in memory while the app runs
313
+ ❌ Is NOT equivalent to real cryptography (AES, etc.)
314
+ ```
315
+
316
+ ### Why this limitation exists — and why no frontend library can fix it
317
+
318
+ The `secret` you pass lives in your JavaScript code, which runs in the user's browser. No matter the algorithm used (XOR, AES, anything), **the key must be present in the frontend to decrypt the value**, which means it's always inspectable:
319
+
320
+ ```
321
+ 1. The secret travels safely over HTTPS — that's not the problem
322
+ 2. Once it reaches the browser, it must be used by your JS to decrypt
323
+ 3. Anyone with DevTools open can set a breakpoint where decryption
324
+ happens and read the secret and the decrypted value directly
325
+ 4. This is true even with industry-standard encryption (Web Crypto AES) —
326
+ the algorithm's strength doesn't matter if the key is exposed
327
+ ```
328
+
329
+ This is a fundamental limitation of any frontend-only encryption — not a flaw specific to typed-storage's XOR implementation.
330
+
331
+ ### For real security with auth tokens
287
332
 
288
333
  ```typescript
289
334
  // In your backend (Express / NestJS):
290
335
  res.cookie('authToken', token, {
291
- httpOnly: true, // not accessible from JavaScript
292
- secure: true, // HTTPS only
293
- sameSite: 'strict'
336
+ httpOnly: true, // JavaScript cannot read this — ever
337
+ secure: true, // HTTPS only
338
+ sameSite: 'strict'
294
339
  });
295
340
  ```
296
341
 
297
- typed-storage is designed for:
298
- - ✅ UI preferences (theme, language, font size)
299
- - Navigation state (last visited, sidebar open)
300
- - ✅ Non-sensitive user settings
301
- - ❌ Auth tokens → use httpOnly cookies
302
- - Passwords or financial data → never in localStorage
342
+ httpOnly cookies are the only approach where the token never becomes accessible to JavaScript running in the browser — because the browser itself enforces the restriction, not your code.
343
+
344
+ ### When `encrypt` is still worth using
345
+
346
+ ```
347
+ You understand it's obfuscation, not security
348
+ ✅ You want to deter casual inspection, not block determined attackers
349
+ ✅ You're combining it with ttl so values expire predictably
350
+ ✅ The data isn't critical enough to justify httpOnly cookie infrastructure
351
+ (e.g. you're prototyping, or it's a low-stakes internal tool)
352
+
353
+ ❌ Don't rely on this alone for banking, healthcare, or any data where
354
+ a breach has real consequences — use httpOnly cookies on the backend
355
+ ```
303
356
 
304
357
  ---
305
358
 
@@ -0,0 +1,2 @@
1
+ import { StorageSchema, StorageResult, StorageSignalOptions } from '../types.js';
2
+ export declare function createStorage<T extends StorageSchema>(schema: T, options?: StorageSignalOptions): StorageResult<T>;
@@ -0,0 +1,31 @@
1
+ import { createStorageSignal } from './storage-signal.js';
2
+ import { applyMigrations } from '../features/migrations.js';
3
+ function registerPrefix(prefix, sto) {
4
+ const registryKey = '__typed-storage__';
5
+ const existing = sto.getItem(registryKey);
6
+ const prefixes = existing ? JSON.parse(existing) : [];
7
+ if (prefix && !prefixes.includes(prefix)) {
8
+ prefixes.push(prefix);
9
+ sto.setItem(registryKey, JSON.stringify(prefixes));
10
+ }
11
+ }
12
+ export function createStorage(schema, options) {
13
+ if (options?.version && options.migrations) {
14
+ const sto = options.storage === 'session' ? sessionStorage : localStorage;
15
+ const prefix = options.prefix ?? '';
16
+ applyMigrations(prefix, options.version, options.migrations, sto);
17
+ }
18
+ const sto = options?.storage === 'session' ? sessionStorage : localStorage;
19
+ registerPrefix(options?.prefix ?? '', sto);
20
+ const result = [];
21
+ let keys = Object.keys(schema);
22
+ for (let key of keys) {
23
+ result[key] = createStorageSignal(key, schema[key], options);
24
+ }
25
+ result.clear = () => {
26
+ for (let key of keys) {
27
+ result[key].reset();
28
+ }
29
+ };
30
+ return result;
31
+ }
@@ -0,0 +1,6 @@
1
+ export declare class MemoryStorage {
2
+ private data;
3
+ getItem(key: string): string | null;
4
+ setItem(key: string, value: string): void;
5
+ removeItem(key: string): void;
6
+ }
@@ -0,0 +1,14 @@
1
+ export class MemoryStorage {
2
+ constructor() {
3
+ this.data = new Map();
4
+ }
5
+ getItem(key) {
6
+ return this.data.get(key) ?? null;
7
+ }
8
+ setItem(key, value) {
9
+ this.data.set(key, value);
10
+ }
11
+ removeItem(key) {
12
+ this.data.delete(key);
13
+ }
14
+ }
@@ -0,0 +1,2 @@
1
+ import { StorageSignal, StorageSignalOptions } from "../types.js";
2
+ export declare function createStorageSignal<T>(key: string, initialValue: T, options?: StorageSignalOptions): StorageSignal<T>;
@@ -0,0 +1,129 @@
1
+ import LZString from 'lz-string';
2
+ import { MemoryStorage } from "./memory-storage.js";
3
+ import { xorEncrypt, xorDecrypt } from '../features/xor.js';
4
+ function safeParseJSON(value, fallback) {
5
+ if (!value)
6
+ return { value: fallback };
7
+ try {
8
+ const parsed = JSON.parse(value);
9
+ if (parsed && typeof parsed === 'object' && 'value' in parsed) {
10
+ return parsed;
11
+ }
12
+ return { value: JSON.parse(value) };
13
+ }
14
+ catch (error) {
15
+ console.warn(`Error al parsear JSON de localStorage. Usando valor por defecto.`, error);
16
+ return { value: fallback };
17
+ }
18
+ }
19
+ function getStorage(type) {
20
+ try {
21
+ const sto = type === 'session' ? sessionStorage : localStorage;
22
+ sto.setItem('__typed_storage_test__', '1');
23
+ sto.removeItem('__typed_storage_test__');
24
+ return sto;
25
+ }
26
+ catch {
27
+ console.warn('Storage no disponible, usando memoria como fallback');
28
+ return new MemoryStorage();
29
+ }
30
+ }
31
+ export function createStorageSignal(key, initialValue, options) {
32
+ let sto = getStorage(options?.storage ?? 'local');
33
+ if (options?.prefix) {
34
+ key = `${options.prefix}:${key}`;
35
+ }
36
+ const rawData = sto.getItem(key);
37
+ let savedData = options?.compress && rawData
38
+ ? LZString.decompress(rawData)
39
+ : rawData;
40
+ if (options?.encrypt && options?.secret && savedData) {
41
+ try {
42
+ savedData = xorDecrypt(savedData, options.secret);
43
+ }
44
+ catch {
45
+ savedData = null;
46
+ }
47
+ }
48
+ let currentValue;
49
+ const listeners = [];
50
+ function notify(value) {
51
+ listeners.forEach(cb => cb(value));
52
+ }
53
+ const item = safeParseJSON(savedData, initialValue);
54
+ if (item.expiresAt === undefined) {
55
+ currentValue = !savedData ? initialValue : item.value;
56
+ }
57
+ else {
58
+ if (Date.now() <= item.expiresAt) {
59
+ currentValue = item.value;
60
+ }
61
+ else {
62
+ sto.removeItem(key);
63
+ currentValue = initialValue;
64
+ }
65
+ }
66
+ const signalBase = function () {
67
+ return currentValue;
68
+ };
69
+ if (options?.sync) {
70
+ window.addEventListener('storage', (event) => {
71
+ if (event.key === key) {
72
+ if (event.newValue === null) {
73
+ notify(initialValue);
74
+ return currentValue = initialValue;
75
+ }
76
+ let rawNewValue = options?.compress
77
+ ? LZString.decompress(event.newValue)
78
+ : event.newValue;
79
+ if (options?.encrypt && options?.secret) {
80
+ try {
81
+ rawNewValue = xorDecrypt(rawNewValue, options.secret);
82
+ }
83
+ catch {
84
+ rawNewValue = '';
85
+ }
86
+ }
87
+ const item = safeParseJSON(rawNewValue, initialValue);
88
+ notify(item.value);
89
+ return currentValue = item.value;
90
+ }
91
+ });
92
+ }
93
+ signalBase.set = function (newValue) {
94
+ currentValue = newValue;
95
+ notify(currentValue);
96
+ const dataToStore = JSON.stringify({
97
+ value: newValue,
98
+ expiresAt: options?.ttl ? Date.now() + options.ttl : undefined
99
+ });
100
+ let finalData = options?.compress
101
+ ? LZString.compress(dataToStore)
102
+ : dataToStore;
103
+ if (options?.encrypt && options?.secret) {
104
+ finalData = xorEncrypt(finalData, options.secret);
105
+ }
106
+ sto.setItem(key, finalData);
107
+ };
108
+ signalBase.reset = function () {
109
+ currentValue = initialValue;
110
+ notify(currentValue);
111
+ const dataToStore = JSON.stringify(initialValue);
112
+ const finalData = options?.compress
113
+ ? LZString.compress(dataToStore)
114
+ : dataToStore;
115
+ sto.setItem(key, finalData);
116
+ };
117
+ signalBase.has = function () {
118
+ return !!sto.getItem(key);
119
+ };
120
+ signalBase.remove = function () {
121
+ sto.removeItem(key);
122
+ currentValue = initialValue;
123
+ notify(currentValue);
124
+ };
125
+ signalBase.onChange = function (callback) {
126
+ listeners.push(callback);
127
+ };
128
+ return signalBase;
129
+ }
@@ -17,24 +17,6 @@ export function createStorage(schema, options) {
17
17
  }
18
18
  const sto = options?.storage === 'session' ? sessionStorage : localStorage;
19
19
  registerPrefix(options?.prefix ?? '', sto);
20
- if (options?.encrypt) {
21
- console.warn(`
22
- ⚠️ typed-storage: la opción encrypt está activada.
23
-
24
- Encriptar valores en localStorage no es seguro —
25
- la clave vive en el frontend y cualquier dev puede accederla.
26
-
27
- Para datos sensibles usa:
28
- ✅ httpOnly cookies (tokens, sesiones)
29
- ✅ Variables de entorno en el servidor
30
-
31
- typed-storage es ideal para:
32
- ✅ Preferencias de UI (theme, language)
33
- ✅ Estado de navegación
34
- ❌ Tokens de autenticación
35
- ❌ Datos financieros o personales sensibles
36
- `);
37
- }
38
20
  const result = [];
39
21
  let keys = Object.keys(schema);
40
22
  for (let key of keys) {
@@ -0,0 +1,3 @@
1
+ import { HeavySignal, HeavyStorageOptions, HeavyStorageResult, HeavyStorageSchema } from "./heavy-storage.types";
2
+ export declare function createHeavySignal<T>(key: string, initialValue: T, options?: HeavyStorageOptions): HeavySignal<T>;
3
+ export declare function createHeavyStorage<T extends HeavyStorageSchema>(schema: T, options?: HeavyStorageOptions): HeavyStorageResult<T>;
@@ -0,0 +1,50 @@
1
+ import { dbDelete, dbGet, dbSet, openDB } from "./indexeddb-driver";
2
+ export function createHeavySignal(key, initialValue, options) {
3
+ const dbName = options?.dbName ?? 'typed-storage-heavy';
4
+ const listeners = [];
5
+ function notify(value) {
6
+ listeners.forEach(cb => cb(value));
7
+ }
8
+ const signal = {};
9
+ signal.get = async function () {
10
+ const db = await openDB(dbName);
11
+ const stored = await dbGet(db, key);
12
+ if (stored === undefined)
13
+ return initialValue;
14
+ if (stored.expiresAt && Date.now() > stored.expiresAt) {
15
+ await dbDelete(db, key);
16
+ return initialValue;
17
+ }
18
+ return stored.value ?? initialValue;
19
+ };
20
+ signal.set = async function (value) {
21
+ const db = await openDB(dbName);
22
+ await dbSet(db, key, {
23
+ value,
24
+ expiresAt: options?.ttl ? Date.now() + options.ttl : undefined
25
+ });
26
+ notify(value);
27
+ };
28
+ signal.remove = async function () {
29
+ const db = await openDB(dbName);
30
+ await dbDelete(db, key);
31
+ notify(initialValue);
32
+ };
33
+ signal.onChange = function (callback) {
34
+ listeners.push(callback);
35
+ };
36
+ return signal;
37
+ }
38
+ export function createHeavyStorage(schema, options) {
39
+ const result = {};
40
+ const keys = Object.keys(schema);
41
+ for (const key of keys) {
42
+ result[key] = createHeavySignal(key, schema[key], options);
43
+ }
44
+ result.clear = async () => {
45
+ for (const key of keys) {
46
+ await result[key].remove();
47
+ }
48
+ };
49
+ return result;
50
+ }
@@ -0,0 +1,16 @@
1
+ export interface HeavyStorageOptions {
2
+ dbName?: string;
3
+ ttl?: number;
4
+ }
5
+ export interface HeavySignal<T> {
6
+ get(): Promise<T>;
7
+ set(value: T): Promise<void>;
8
+ remove(): Promise<void>;
9
+ onChange(callback: (value: T) => void): void;
10
+ }
11
+ export type HeavyStorageSchema = Record<string, any>;
12
+ export type HeavyStorageResult<T extends HeavyStorageSchema> = {
13
+ [K in keyof T]: HeavySignal<T[K]>;
14
+ } & {
15
+ clear(): Promise<void>;
16
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ export declare function openDB(dbName: string): Promise<IDBDatabase>;
2
+ export declare function dbGet(db: IDBDatabase, key: string): Promise<any>;
3
+ export declare function dbSet(db: IDBDatabase, key: string, value: any): Promise<void>;
4
+ export declare function dbDelete(db: IDBDatabase, key: string): Promise<void>;
@@ -0,0 +1,40 @@
1
+ export function openDB(dbName) {
2
+ return new Promise((resolve, reject) => {
3
+ const request = indexedDB.open(dbName, 1);
4
+ request.onupgradeneeded = (event) => {
5
+ const db = event.target.result;
6
+ if (!db.objectStoreNames.contains('storage')) {
7
+ db.createObjectStore('storage');
8
+ }
9
+ };
10
+ request.onsuccess = () => resolve(request.result);
11
+ request.onerror = () => reject(request.error);
12
+ });
13
+ }
14
+ export function dbGet(db, key) {
15
+ return new Promise((resolve, reject) => {
16
+ const transaction = db.transaction('storage', 'readonly');
17
+ const store = transaction.objectStore('storage');
18
+ const request = store.get(key);
19
+ request.onsuccess = () => resolve(request.result);
20
+ request.onerror = () => reject(request.error);
21
+ });
22
+ }
23
+ export function dbSet(db, key, value) {
24
+ return new Promise((resolve, reject) => {
25
+ const transaction = db.transaction('storage', 'readwrite');
26
+ const store = transaction.objectStore('storage');
27
+ const request = store.put(value, key);
28
+ request.onsuccess = () => resolve();
29
+ request.onerror = () => reject(request.error);
30
+ });
31
+ }
32
+ export function dbDelete(db, key) {
33
+ return new Promise((resolve, reject) => {
34
+ const transaction = db.transaction('storage', 'readwrite');
35
+ const store = transaction.objectStore('storage');
36
+ const request = store.delete(key);
37
+ request.onsuccess = () => resolve();
38
+ request.onerror = () => reject(request.error);
39
+ });
40
+ }
@@ -0,0 +1 @@
1
+ export declare function applyMigrations(prefix: string, currentVersion: number, migrations: Record<number, (data: any) => any>, storage: Storage): void;
@@ -0,0 +1,34 @@
1
+ export function applyMigrations(prefix, currentVersion, migrations, storage) {
2
+ const versionKey = `${prefix}__version__`;
3
+ const savedVersion = storage.getItem(versionKey);
4
+ if (!savedVersion) {
5
+ storage.setItem(versionKey, String(currentVersion));
6
+ return;
7
+ }
8
+ let version = parseInt(savedVersion);
9
+ if (version >= currentVersion)
10
+ return;
11
+ const currentData = {};
12
+ for (let i = 0; i < storage.length; i++) {
13
+ const key = storage.key(i);
14
+ if (key && key.startsWith(prefix) && key !== versionKey) {
15
+ const value = storage.getItem(key);
16
+ if (value) {
17
+ const cleanKey = key.replace(`${prefix}:`, '');
18
+ currentData[cleanKey] = JSON.parse(value);
19
+ }
20
+ }
21
+ }
22
+ while (version < currentVersion) {
23
+ const migration = migrations[version];
24
+ if (migration) {
25
+ const migrated = migration(currentData);
26
+ Object.assign(currentData, migrated);
27
+ }
28
+ version++;
29
+ }
30
+ for (const [key, value] of Object.entries(currentData)) {
31
+ storage.setItem(`${prefix}:${key}`, JSON.stringify(value));
32
+ }
33
+ storage.setItem(versionKey, String(currentVersion));
34
+ }
@@ -0,0 +1,3 @@
1
+ export declare function xorTransform(text: string, secret: string): string;
2
+ export declare function xorEncrypt(text: string, secret: string): string;
3
+ export declare function xorDecrypt(text: string, secret: string): string;
@@ -0,0 +1,14 @@
1
+ export function xorTransform(text, secret) {
2
+ return text.split('').map((char, i) => {
3
+ const keyChar = secret[i % secret.length];
4
+ return String.fromCharCode(char.charCodeAt(0) ^ keyChar.charCodeAt(0));
5
+ }).join('');
6
+ }
7
+ export function xorEncrypt(text, secret) {
8
+ const xored = xorTransform(text, secret);
9
+ return btoa(xored);
10
+ }
11
+ export function xorDecrypt(text, secret) {
12
+ const xored = atob(text);
13
+ return xorTransform(xored, secret);
14
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { createStorage } from './create-storage.js';
2
- export { createHeavyStorage } from './heavy-storage.js';
1
+ export { createStorage } from './core/create-storage.js';
2
+ export { createHeavyStorage } from './features/heavy-storage/heavy-storage.js';
3
3
  export type { StorageSignal, StorageSchema, StorageResult, StorageSignalOptions } from './types.js';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { createStorage } from './create-storage.js';
2
- export { createHeavyStorage } from './heavy-storage.js';
1
+ export { createStorage } from './core/create-storage.js';
2
+ export { createHeavyStorage } from './features/heavy-storage/heavy-storage.js';
@@ -1,5 +1,6 @@
1
1
  import LZString from 'lz-string';
2
2
  import { MemoryStorage } from "./memory-storage.js";
3
+ import { xorEncrypt, xorDecrypt } from './xor.js';
3
4
  function safeParseJSON(value, fallback) {
4
5
  if (!value)
5
6
  return { value: fallback };
@@ -33,9 +34,17 @@ export function createStorageSignal(key, initialValue, options) {
33
34
  key = `${options.prefix}:${key}`;
34
35
  }
35
36
  const rawData = sto.getItem(key);
36
- const savedData = options?.compress && rawData
37
+ let savedData = options?.compress && rawData
37
38
  ? LZString.decompress(rawData)
38
39
  : rawData;
40
+ if (options?.encrypt && options?.secret && savedData) {
41
+ try {
42
+ savedData = xorDecrypt(savedData, options.secret);
43
+ }
44
+ catch {
45
+ savedData = null;
46
+ }
47
+ }
39
48
  let currentValue;
40
49
  const listeners = [];
41
50
  function notify(value) {
@@ -64,9 +73,17 @@ export function createStorageSignal(key, initialValue, options) {
64
73
  notify(initialValue);
65
74
  return currentValue = initialValue;
66
75
  }
67
- const rawNewValue = options?.compress
76
+ let rawNewValue = options?.compress
68
77
  ? LZString.decompress(event.newValue)
69
78
  : event.newValue;
79
+ if (options?.encrypt && options?.secret) {
80
+ try {
81
+ rawNewValue = xorDecrypt(rawNewValue, options.secret);
82
+ }
83
+ catch {
84
+ rawNewValue = '';
85
+ }
86
+ }
70
87
  const item = safeParseJSON(rawNewValue, initialValue);
71
88
  notify(item.value);
72
89
  return currentValue = item.value;
@@ -80,9 +97,12 @@ export function createStorageSignal(key, initialValue, options) {
80
97
  value: newValue,
81
98
  expiresAt: options?.ttl ? Date.now() + options.ttl : undefined
82
99
  });
83
- const finalData = options?.compress
100
+ let finalData = options?.compress
84
101
  ? LZString.compress(dataToStore)
85
102
  : dataToStore;
103
+ if (options?.encrypt && options?.secret) {
104
+ finalData = xorEncrypt(finalData, options.secret);
105
+ }
86
106
  sto.setItem(key, finalData);
87
107
  };
88
108
  signalBase.reset = function () {
package/dist/types.d.ts CHANGED
@@ -4,6 +4,7 @@ export interface StorageSignalOptions {
4
4
  ttl?: number;
5
5
  sync?: boolean;
6
6
  encrypt?: boolean;
7
+ secret?: string;
7
8
  version?: number;
8
9
  migrations?: Record<number, (data: any) => any>;
9
10
  compress?: boolean;
package/dist/xor.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function xorTransform(text: string, secret: string): string;
2
+ export declare function xorEncrypt(text: string, secret: string): string;
3
+ export declare function xorDecrypt(text: string, secret: string): string;
package/dist/xor.js ADDED
@@ -0,0 +1,14 @@
1
+ export function xorTransform(text, secret) {
2
+ return text.split('').map((char, i) => {
3
+ const keyChar = secret[i % secret.length];
4
+ return String.fromCharCode(char.charCodeAt(0) ^ keyChar.charCodeAt(0));
5
+ }).join('');
6
+ }
7
+ export function xorEncrypt(text, secret) {
8
+ const xored = xorTransform(text, secret);
9
+ return btoa(xored);
10
+ }
11
+ export function xorDecrypt(text, secret) {
12
+ const xored = atob(text);
13
+ return xorTransform(xored, secret);
14
+ }
package/index.html CHANGED
@@ -5,41 +5,20 @@
5
5
  </head>
6
6
  <body>
7
7
  <script type="module">
8
- import { createHeavyStorage } from './dist/index.js';
8
+ import { createStorage } from './dist/index.js';
9
9
 
10
- const heavyStorage = createHeavyStorage({
11
- documents: [],
12
- userPhotos: []
10
+ const secureStorage = createStorage({
11
+ token: ''
13
12
  }, {
14
- dbName: 'test-heavy-storage'
13
+ prefix: 'secure',
14
+ encrypt: true,
15
+ secret: 'mi-clave-secreta',
16
+ ttl: 3600000
15
17
  });
16
18
 
17
- async function test() {
18
- // Set
19
- await heavyStorage.documents.set([
20
- { id: 1, name: 'Documento 1' },
21
- { id: 2, name: 'Documento 2' }
22
- ]);
23
- console.log('✅ Documentos guardados');
24
-
25
- // Get
26
- const docs = await heavyStorage.documents.get();
27
- console.log('📄 Documentos leídos:', docs);
28
-
29
- // onChange
30
- heavyStorage.documents.onChange((newValue) => {
31
- console.log('🔔 documents cambió a:', newValue);
32
- });
33
-
34
- await heavyStorage.documents.set([{ id: 3, name: 'Documento nuevo' }]);
35
-
36
- // Remove
37
- await heavyStorage.userPhotos.remove();
38
- const photos = await heavyStorage.userPhotos.get();
39
- console.log('🗑️ userPhotos después de remove:', photos);
40
- }
41
-
42
- test();
19
+ secureStorage.token.set('eyJhbGciOiJIUzI1NiJ9.test.jwt');
20
+ console.log('Valor leído:', secureStorage.token());
21
+ console.log('Valor en localStorage (debe estar ofuscado):', localStorage.getItem('secure:token'));
43
22
  </script>
44
23
  </body>
45
24
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeanharo98/typed-storage",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Type-safe localStorage with reactive signals",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,11 +1,15 @@
1
1
  // Types
2
- import { StorageSchema, StorageResult, StorageSignalOptions } from './types.js';
2
+ import {
3
+ StorageSchema,
4
+ StorageResult,
5
+ StorageSignalOptions
6
+ } from '../types.js';
3
7
 
4
8
  // Storage Signal
5
9
  import { createStorageSignal } from './storage-signal.js';
6
10
 
7
11
  // Migraciones
8
- import { applyMigrations } from './migrations.js';
12
+ import { applyMigrations } from '../features/migrations.js';
9
13
 
10
14
  function registerPrefix (
11
15
  prefix: string,
@@ -42,25 +46,6 @@ export function createStorage<T extends StorageSchema>(
42
46
  const sto = options?.storage === 'session' ? sessionStorage : localStorage;
43
47
  registerPrefix(options?.prefix ?? '', sto);
44
48
 
45
- if ( options?.encrypt ) {
46
- console.warn(`
47
- ⚠️ typed-storage: la opción encrypt está activada.
48
-
49
- Encriptar valores en localStorage no es seguro —
50
- la clave vive en el frontend y cualquier dev puede accederla.
51
-
52
- Para datos sensibles usa:
53
- ✅ httpOnly cookies (tokens, sesiones)
54
- ✅ Variables de entorno en el servidor
55
-
56
- typed-storage es ideal para:
57
- ✅ Preferencias de UI (theme, language)
58
- ✅ Estado de navegación
59
- ❌ Tokens de autenticación
60
- ❌ Datos financieros o personales sensibles
61
- `);
62
- }
63
-
64
49
  const result: any = [];
65
50
 
66
51
  let keys = Object.keys(schema);
@@ -118,4 +118,44 @@ describe('compress option', () => {
118
118
 
119
119
  expect(compressedSize).toBeLessThan(uncompressedSize);
120
120
  });
121
+ });
122
+
123
+ describe('encrypt option', () => {
124
+ beforeEach(() => localStorage.clear());
125
+
126
+ it('debe ofuscar el valor guardado en localStorage', () => {
127
+ const signal = createStorageSignal('token', '', {
128
+ encrypt: true,
129
+ secret: 'mi-clave'
130
+ });
131
+
132
+ signal.set('eyJhbGciOiJIUzI1NiJ9.test.jwt');
133
+
134
+ const rawStored = localStorage.getItem('token');
135
+ // El valor crudo NO debe contener el JWT en texto plano
136
+ expect(rawStored).not.toContain('eyJhbGciOiJIUzI1NiJ9');
137
+ });
138
+
139
+ it('debe desencriptar correctamente al leer', () => {
140
+ const signal = createStorageSignal('token', '', {
141
+ encrypt: true,
142
+ secret: 'mi-clave'
143
+ });
144
+
145
+ const originalValue = 'eyJhbGciOiJIUzI1NiJ9.test.jwt';
146
+ signal.set(originalValue);
147
+
148
+ expect(signal()).toBe(originalValue);
149
+ });
150
+
151
+ it('debe funcionar junto con TTL', () => {
152
+ const signal = createStorageSignal('token', '', {
153
+ encrypt: true,
154
+ secret: 'mi-clave',
155
+ ttl: 1000
156
+ });
157
+
158
+ signal.set('mi-token');
159
+ expect(signal()).toBe('mi-token');
160
+ });
121
161
  });
@@ -1,11 +1,20 @@
1
1
  import LZString from 'lz-string';
2
2
 
3
3
  // Tipos
4
- import { StorageSignal, StorageSignalOptions } from "./types.js";
4
+ import {
5
+ StorageSignal,
6
+ StorageSignalOptions
7
+ } from "../types.js";
5
8
 
6
9
  // Memory
7
10
  import { MemoryStorage } from "./memory-storage.js";
8
11
 
12
+ // Xor
13
+ import {
14
+ xorEncrypt,
15
+ xorDecrypt
16
+ } from '../features/xor.js';
17
+
9
18
  // Interface
10
19
  interface StoredValue<T> {
11
20
  value: T;
@@ -66,9 +75,18 @@ export function createStorageSignal<T>(
66
75
  }
67
76
 
68
77
  const rawData = sto.getItem(key);
69
- const savedData = options?.compress && rawData
78
+ let savedData = options?.compress && rawData
70
79
  ? LZString.decompress(rawData)
71
80
  : rawData;
81
+
82
+ // Desencripta si encrypt está activo
83
+ if ( options?.encrypt && options?.secret && savedData ) {
84
+ try {
85
+ savedData = xorDecrypt(savedData, options.secret);
86
+ } catch {
87
+ savedData = null; // si falla desencriptar, trata como vacío
88
+ }
89
+ }
72
90
  let currentValue: T;
73
91
 
74
92
  const listeners: Array<(value: T) => void> = [];
@@ -106,9 +124,17 @@ export function createStorageSignal<T>(
106
124
  }
107
125
 
108
126
  // Parseamos el nuevo valor
109
- const rawNewValue = options?.compress
127
+ let rawNewValue = options?.compress
110
128
  ? LZString.decompress(event.newValue)
111
129
  : event.newValue;
130
+
131
+ if ( options?.encrypt && options?.secret ) {
132
+ try {
133
+ rawNewValue = xorDecrypt(rawNewValue, options.secret);
134
+ } catch {
135
+ rawNewValue = '';
136
+ }
137
+ }
112
138
  const item = safeParseJSON(rawNewValue, initialValue);
113
139
 
114
140
  notify(item.value as T);
@@ -126,10 +152,14 @@ export function createStorageSignal<T>(
126
152
  expiresAt: options?.ttl ? Date.now() + options.ttl : undefined
127
153
  });
128
154
 
129
- const finalData = options?.compress
155
+ let finalData = options?.compress
130
156
  ? LZString.compress(dataToStore)
131
157
  : dataToStore;
132
158
 
159
+ if ( options?.encrypt && options?.secret ) {
160
+ finalData = xorEncrypt(finalData, options.secret);
161
+ }
162
+
133
163
  sto.setItem( key, finalData );
134
164
  }
135
165
 
@@ -0,0 +1,89 @@
1
+ // Indexed
2
+ import {
3
+ dbDelete,
4
+ dbGet,
5
+ dbSet,
6
+ openDB
7
+ } from "./indexeddb-driver";
8
+
9
+ // Types
10
+ import {
11
+ HeavySignal,
12
+ HeavyStorageOptions,
13
+ HeavyStorageResult,
14
+ HeavyStorageSchema
15
+ } from "./heavy-storage.types";
16
+
17
+ export function createHeavySignal<T>(
18
+ key: string,
19
+ initialValue: T,
20
+ options?: HeavyStorageOptions
21
+ ): HeavySignal<T> {
22
+ const dbName = options?.dbName ?? 'typed-storage-heavy';
23
+ const listeners: Array<(value: T) => void> = [];
24
+
25
+ function notify ( value: T ): void {
26
+ listeners.forEach(cb => cb(value));
27
+ }
28
+
29
+ const signal: any = {};
30
+
31
+ signal.get = async function(): Promise<T> {
32
+ const db = await openDB(dbName);
33
+ const stored = await dbGet(db, key);
34
+
35
+ if (stored === undefined) return initialValue;
36
+
37
+ // Verifica TTL si existe
38
+ if (stored.expiresAt && Date.now() > stored.expiresAt) {
39
+ await dbDelete(db, key);
40
+ return initialValue;
41
+ }
42
+
43
+ return stored.value ?? initialValue;
44
+ };
45
+
46
+ signal.set = async function(value: T): Promise<void> {
47
+ const db = await openDB(dbName);
48
+ await dbSet(db, key, {
49
+ value,
50
+ expiresAt: options?.ttl ? Date.now() + options.ttl : undefined
51
+ });
52
+ notify(value);
53
+ };
54
+
55
+ signal.remove = async function(): Promise<void> {
56
+ const db = await openDB(dbName);
57
+ await dbDelete(db, key);
58
+ notify(initialValue);
59
+ };
60
+
61
+ signal.onChange = function ( callback: (value: T) => void ): void {
62
+ listeners.push(callback);
63
+ };
64
+
65
+ return signal as HeavySignal<T>;
66
+ }
67
+
68
+ // ===========================================
69
+ // Iteramos el schema completo
70
+
71
+ export function createHeavyStorage<T extends HeavyStorageSchema>(
72
+ schema: T,
73
+ options?: HeavyStorageOptions
74
+ ): HeavyStorageResult<T> {
75
+ const result: any = {};
76
+
77
+ const keys = Object.keys(schema);
78
+ for (const key of keys) {
79
+ result[key] = createHeavySignal(key, schema[key], options);
80
+ }
81
+
82
+ result.clear = async () => {
83
+ for (const key of keys) {
84
+ await result[key].remove();
85
+ }
86
+ };
87
+
88
+ return result as HeavyStorageResult<T>;
89
+ }
@@ -0,0 +1,21 @@
1
+ export interface HeavyStorageOptions {
2
+ dbName?: string;
3
+ ttl?: number;
4
+ }
5
+
6
+ export interface HeavySignal<T> {
7
+ get(): Promise<T>;
8
+ set(value: T): Promise<void>;
9
+ remove(): Promise<void>;
10
+ onChange(callback: (value: T) => void): void;
11
+ }
12
+
13
+ // ==============================================
14
+
15
+ export type HeavyStorageSchema = Record<string, any>;
16
+
17
+ export type HeavyStorageResult<T extends HeavyStorageSchema> = {
18
+ [K in keyof T]: HeavySignal<T[K]>;
19
+ } & {
20
+ clear(): Promise<void>;
21
+ };
@@ -0,0 +1,50 @@
1
+ // Abrir/crear la base de datos
2
+ export function openDB(dbName: string): Promise<IDBDatabase> {
3
+ return new Promise((resolve, reject) => {
4
+ const request = indexedDB.open(dbName, 1);
5
+
6
+ request.onupgradeneeded = (event) => {
7
+ const db = (event.target as IDBOpenDBRequest).result;
8
+ // Crea el "object store" — equivalente a una tabla
9
+ if (!db.objectStoreNames.contains('storage')) {
10
+ db.createObjectStore('storage');
11
+ }
12
+ };
13
+
14
+ request.onsuccess = () => resolve(request.result);
15
+ request.onerror = () => reject(request.error);
16
+ });
17
+ }
18
+
19
+ export function dbGet(db: IDBDatabase, key: string): Promise<any> {
20
+ return new Promise((resolve, reject) => {
21
+ const transaction = db.transaction('storage', 'readonly');
22
+ const store = transaction.objectStore('storage');
23
+ const request = store.get(key);
24
+
25
+ request.onsuccess = () => resolve(request.result);
26
+ request.onerror = () => reject(request.error);
27
+ });
28
+ }
29
+
30
+ export function dbSet(db: IDBDatabase, key: string, value: any): Promise<void> {
31
+ return new Promise((resolve, reject) => {
32
+ const transaction = db.transaction('storage', 'readwrite');
33
+ const store = transaction.objectStore('storage');
34
+ const request = store.put(value, key);
35
+
36
+ request.onsuccess = () => resolve();
37
+ request.onerror = () => reject(request.error);
38
+ });
39
+ }
40
+
41
+ export function dbDelete(db: IDBDatabase, key: string): Promise<void> {
42
+ return new Promise((resolve, reject) => {
43
+ const transaction = db.transaction('storage', 'readwrite');
44
+ const store = transaction.objectStore('storage');
45
+ const request = store.delete(key);
46
+
47
+ request.onsuccess = () => resolve();
48
+ request.onerror = () => reject(request.error);
49
+ });
50
+ }
@@ -0,0 +1,18 @@
1
+ export function xorTransform ( text: string, secret: string ): string {
2
+ return text.split('').map((char, i) => {
3
+ const keyChar = secret[i % secret.length];
4
+ return String.fromCharCode(
5
+ char.charCodeAt(0) ^ keyChar.charCodeAt(0)
6
+ );
7
+ }).join('');
8
+ }
9
+
10
+ export function xorEncrypt ( text: string, secret: string ): string {
11
+ const xored = xorTransform(text, secret);
12
+ return btoa(xored);
13
+ }
14
+
15
+ export function xorDecrypt ( text: string, secret: string ): string {
16
+ const xored = atob(text);
17
+ return xorTransform(xored, secret);
18
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { createStorage } from './create-storage.js';
2
- export { createHeavyStorage } from './heavy-storage.js';
1
+ export { createStorage } from './core/create-storage.js';
2
+ export { createHeavyStorage } from './features/heavy-storage/heavy-storage.js';
3
3
  export type {
4
4
  StorageSignal,
5
5
  StorageSchema,
package/src/types.ts CHANGED
@@ -5,6 +5,7 @@ export interface StorageSignalOptions {
5
5
  ttl?: number;
6
6
  sync?: boolean;
7
7
  encrypt?: boolean;
8
+ secret?: string; // Requerido si el encrypt es true
8
9
  version?: number;
9
10
  migrations?: Record<number, ( data: any ) => any>;
10
11
  compress?: boolean;
@@ -1,146 +0,0 @@
1
- // Abrir/crear la base de datos
2
- function openDB(dbName: string): Promise<IDBDatabase> {
3
- return new Promise((resolve, reject) => {
4
- const request = indexedDB.open(dbName, 1);
5
-
6
- request.onupgradeneeded = (event) => {
7
- const db = (event.target as IDBOpenDBRequest).result;
8
- // Crea el "object store" — equivalente a una tabla
9
- if (!db.objectStoreNames.contains('storage')) {
10
- db.createObjectStore('storage');
11
- }
12
- };
13
-
14
- request.onsuccess = () => resolve(request.result);
15
- request.onerror = () => reject(request.error);
16
- });
17
- }
18
-
19
- function dbGet(db: IDBDatabase, key: string): Promise<any> {
20
- return new Promise((resolve, reject) => {
21
- const transaction = db.transaction('storage', 'readonly');
22
- const store = transaction.objectStore('storage');
23
- const request = store.get(key);
24
-
25
- request.onsuccess = () => resolve(request.result);
26
- request.onerror = () => reject(request.error);
27
- });
28
- }
29
-
30
- function dbSet(db: IDBDatabase, key: string, value: any): Promise<void> {
31
- return new Promise((resolve, reject) => {
32
- const transaction = db.transaction('storage', 'readwrite');
33
- const store = transaction.objectStore('storage');
34
- const request = store.put(value, key);
35
-
36
- request.onsuccess = () => resolve();
37
- request.onerror = () => reject(request.error);
38
- });
39
- }
40
-
41
- function dbDelete(db: IDBDatabase, key: string): Promise<void> {
42
- return new Promise((resolve, reject) => {
43
- const transaction = db.transaction('storage', 'readwrite');
44
- const store = transaction.objectStore('storage');
45
- const request = store.delete(key);
46
-
47
- request.onsuccess = () => resolve();
48
- request.onerror = () => reject(request.error);
49
- });
50
- }
51
-
52
- // ===========================================
53
-
54
- interface HeavyStorageOptions {
55
- dbName?: string;
56
- ttl?: number;
57
- }
58
-
59
- interface HeavySignal<T> {
60
- get(): Promise<T>;
61
- set(value: T): Promise<void>;
62
- remove(): Promise<void>;
63
- onChange(callback: (value: T) => void): void;
64
- }
65
-
66
- export function createHeavySignal<T>(
67
- key: string,
68
- initialValue: T,
69
- options?: HeavyStorageOptions
70
- ): HeavySignal<T> {
71
- const dbName = options?.dbName ?? 'typed-storage-heavy';
72
- const listeners: Array<(value: T) => void> = [];
73
-
74
- function notify(value: T): void {
75
- listeners.forEach(cb => cb(value));
76
- }
77
-
78
- const signal: any = {};
79
-
80
- signal.get = async function(): Promise<T> {
81
- const db = await openDB(dbName);
82
- const stored = await dbGet(db, key);
83
-
84
- if (stored === undefined) return initialValue;
85
-
86
- // Verifica TTL si existe
87
- if (stored.expiresAt && Date.now() > stored.expiresAt) {
88
- await dbDelete(db, key);
89
- return initialValue;
90
- }
91
-
92
- return stored.value ?? initialValue;
93
- };
94
-
95
- signal.set = async function(value: T): Promise<void> {
96
- const db = await openDB(dbName);
97
- await dbSet(db, key, {
98
- value,
99
- expiresAt: options?.ttl ? Date.now() + options.ttl : undefined
100
- });
101
- notify(value);
102
- };
103
-
104
- signal.remove = async function(): Promise<void> {
105
- const db = await openDB(dbName);
106
- await dbDelete(db, key);
107
- notify(initialValue);
108
- };
109
-
110
- signal.onChange = function(callback: (value: T) => void): void {
111
- listeners.push(callback);
112
- };
113
-
114
- return signal as HeavySignal<T>;
115
- }
116
-
117
- // ===========================================
118
- // Iteramos el schema completo
119
-
120
- type HeavyStorageSchema = Record<string, any>;
121
-
122
- type HeavyStorageResult<T extends HeavyStorageSchema> = {
123
- [K in keyof T]: HeavySignal<T[K]>;
124
- } & {
125
- clear(): Promise<void>;
126
- };
127
-
128
- export function createHeavyStorage<T extends HeavyStorageSchema>(
129
- schema: T,
130
- options?: HeavyStorageOptions
131
- ): HeavyStorageResult<T> {
132
- const result: any = {};
133
-
134
- const keys = Object.keys(schema);
135
- for (const key of keys) {
136
- result[key] = createHeavySignal(key, schema[key], options);
137
- }
138
-
139
- result.clear = async () => {
140
- for (const key of keys) {
141
- await result[key].remove();
142
- }
143
- };
144
-
145
- return result as HeavyStorageResult<T>;
146
- }
File without changes