@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.
- package/README.md +66 -13
- package/dist/core/create-storage.d.ts +2 -0
- package/dist/core/create-storage.js +31 -0
- package/dist/core/memory-storage.d.ts +6 -0
- package/dist/core/memory-storage.js +14 -0
- package/dist/core/storage-signal.d.ts +2 -0
- package/dist/core/storage-signal.js +129 -0
- package/dist/create-storage.js +0 -18
- package/dist/features/heavy-storage/heavy-storage.d.ts +3 -0
- package/dist/features/heavy-storage/heavy-storage.js +50 -0
- package/dist/features/heavy-storage/heavy-storage.types.d.ts +16 -0
- package/dist/features/heavy-storage/heavy-storage.types.js +1 -0
- package/dist/features/heavy-storage/indexeddb-driver.d.ts +4 -0
- package/dist/features/heavy-storage/indexeddb-driver.js +40 -0
- package/dist/features/migrations.d.ts +1 -0
- package/dist/features/migrations.js +34 -0
- package/dist/features/xor.d.ts +3 -0
- package/dist/features/xor.js +14 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/storage-signal.js +23 -3
- package/dist/types.d.ts +1 -0
- package/dist/xor.d.ts +3 -0
- package/dist/xor.js +14 -0
- package/index.html +10 -31
- package/package.json +1 -1
- package/src/{create-storage.ts → core/create-storage.ts} +6 -21
- package/src/{storage-signal.test.ts → core/storage-signal.test.ts} +40 -0
- package/src/{storage-signal.ts → core/storage-signal.ts} +34 -4
- package/src/features/heavy-storage/heavy-storage.ts +89 -0
- package/src/features/heavy-storage/heavy-storage.types.ts +21 -0
- package/src/features/heavy-storage/indexeddb-driver.ts +50 -0
- package/src/features/xor.ts +18 -0
- package/src/index.ts +2 -2
- package/src/types.ts +1 -0
- package/src/heavy-storage.ts +0 -146
- /package/src/{memory-storage.ts → core/memory-storage.ts} +0 -0
- /package/src/{heavy-storage.test.ts → features/heavy-storage/heavy-storage.test.ts} +0 -0
- /package/src/{migrations.test.ts → features/migrations.test.ts} +0 -0
- /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` |
|
|
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
|
-
## 🔒
|
|
283
|
+
## 🔒 Encryption (XOR obfuscation)
|
|
283
284
|
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
336
|
+
httpOnly: true, // JavaScript cannot read this — ever
|
|
337
|
+
secure: true, // HTTPS only
|
|
338
|
+
sameSite: 'strict'
|
|
294
339
|
});
|
|
295
340
|
```
|
|
296
341
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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,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,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
|
+
}
|
package/dist/create-storage.js
CHANGED
|
@@ -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,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';
|
package/dist/storage-signal.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/dist/xor.d.ts
ADDED
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 {
|
|
8
|
+
import { createStorage } from './dist/index.js';
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
userPhotos: []
|
|
10
|
+
const secureStorage = createStorage({
|
|
11
|
+
token: ''
|
|
13
12
|
}, {
|
|
14
|
-
|
|
13
|
+
prefix: 'secure',
|
|
14
|
+
encrypt: true,
|
|
15
|
+
secret: 'mi-clave-secreta',
|
|
16
|
+
ttl: 3600000
|
|
15
17
|
});
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,11 +1,15 @@
|
|
|
1
1
|
// Types
|
|
2
|
-
import {
|
|
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 '
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/src/heavy-storage.ts
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|