oto-storage 0.4.2 → 0.4.3
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/dist/index.d.mts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +450 -0
- package/dist/index.mjs +424 -0
- package/package.json +1 -1
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface StorageOptions {
|
|
2
|
+
prefix?: string;
|
|
3
|
+
type?: "local" | "session";
|
|
4
|
+
defaults?: Record<string, any>;
|
|
5
|
+
ttl?: number;
|
|
6
|
+
encryption?: StorageEncryptionOptions;
|
|
7
|
+
}
|
|
8
|
+
interface StorageEncryptionOptions {
|
|
9
|
+
encrypt: (plainText: string) => string;
|
|
10
|
+
decrypt: (cipherText: string) => string;
|
|
11
|
+
migrate?: boolean;
|
|
12
|
+
}
|
|
13
|
+
declare function oto<T extends object>(options?: StorageOptions): T;
|
|
14
|
+
declare const version = "0.4.2";
|
|
15
|
+
|
|
16
|
+
export { type StorageEncryptionOptions, type StorageOptions, oto, version };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface StorageOptions {
|
|
2
|
+
prefix?: string;
|
|
3
|
+
type?: "local" | "session";
|
|
4
|
+
defaults?: Record<string, any>;
|
|
5
|
+
ttl?: number;
|
|
6
|
+
encryption?: StorageEncryptionOptions;
|
|
7
|
+
}
|
|
8
|
+
interface StorageEncryptionOptions {
|
|
9
|
+
encrypt: (plainText: string) => string;
|
|
10
|
+
decrypt: (cipherText: string) => string;
|
|
11
|
+
migrate?: boolean;
|
|
12
|
+
}
|
|
13
|
+
declare function oto<T extends object>(options?: StorageOptions): T;
|
|
14
|
+
declare const version = "0.4.2";
|
|
15
|
+
|
|
16
|
+
export { type StorageEncryptionOptions, type StorageOptions, oto, version };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
oto: () => oto,
|
|
24
|
+
version: () => version
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
function deepMerge(target, source) {
|
|
28
|
+
if (source === null || source === void 0) return target;
|
|
29
|
+
if (target === null || target === void 0) return source;
|
|
30
|
+
if (typeof target !== "object" || typeof source !== "object") return source;
|
|
31
|
+
if (Array.isArray(target) || Array.isArray(source)) return source;
|
|
32
|
+
const result = { ...target };
|
|
33
|
+
for (const key in source) {
|
|
34
|
+
if (source.hasOwnProperty(key)) {
|
|
35
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function wrapWithTTL(value, ttl) {
|
|
41
|
+
return {
|
|
42
|
+
__oto_value: value,
|
|
43
|
+
__oto_expires: Date.now() + ttl
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function unwrapWithTTL(wrapped) {
|
|
47
|
+
if (wrapped && typeof wrapped === "object" && "__oto_expires" in wrapped && "__oto_value" in wrapped && typeof wrapped.__oto_expires === "number") {
|
|
48
|
+
const expired = Date.now() > wrapped.__oto_expires;
|
|
49
|
+
return { value: wrapped.__oto_value, expired };
|
|
50
|
+
}
|
|
51
|
+
return { value: wrapped, expired: false };
|
|
52
|
+
}
|
|
53
|
+
function isEncryptedWrapper(value) {
|
|
54
|
+
return value && typeof value === "object" && value.__oto_encrypted === true && typeof value.__oto_payload === "string";
|
|
55
|
+
}
|
|
56
|
+
function serializeForStorage(value, ttl, encryption) {
|
|
57
|
+
let valueToStore = value;
|
|
58
|
+
if (ttl && ttl > 0) {
|
|
59
|
+
valueToStore = wrapWithTTL(valueToStore, ttl);
|
|
60
|
+
}
|
|
61
|
+
if (!encryption) {
|
|
62
|
+
return JSON.stringify(valueToStore);
|
|
63
|
+
}
|
|
64
|
+
const plainText = JSON.stringify(valueToStore);
|
|
65
|
+
const cipherText = encryption.encrypt(plainText);
|
|
66
|
+
return JSON.stringify({
|
|
67
|
+
__oto_encrypted: true,
|
|
68
|
+
__oto_payload: cipherText
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function migrateStoredValueToEncrypted(safeStorage, fullKey, rawParsed, encryption) {
|
|
72
|
+
if (!encryption?.migrate || isEncryptedWrapper(rawParsed)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const cipherText = encryption.encrypt(JSON.stringify(rawParsed));
|
|
77
|
+
const wrapped = {
|
|
78
|
+
__oto_encrypted: true,
|
|
79
|
+
__oto_payload: cipherText
|
|
80
|
+
};
|
|
81
|
+
safeStorage.setItem(fullKey, JSON.stringify(wrapped));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(`TypedProxy: Failed to migrate ${fullKey} to encrypted format`, error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function maybeMigrate(safeStorage, fullKey, readResult, encryption) {
|
|
87
|
+
if (encryption?.migrate && !readResult.parseError && !readResult.wasEncrypted && readResult.rawParsed !== void 0) {
|
|
88
|
+
migrateStoredValueToEncrypted(safeStorage, fullKey, readResult.rawParsed, encryption);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function readStoredValue(storedValue, ttl, encryption) {
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(storedValue);
|
|
95
|
+
} catch {
|
|
96
|
+
return {
|
|
97
|
+
value: storedValue,
|
|
98
|
+
parseError: true,
|
|
99
|
+
decryptionError: false,
|
|
100
|
+
expired: false,
|
|
101
|
+
wasEncrypted: false
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
let payload = parsed;
|
|
105
|
+
let wasEncrypted = false;
|
|
106
|
+
if (encryption && isEncryptedWrapper(parsed)) {
|
|
107
|
+
wasEncrypted = true;
|
|
108
|
+
try {
|
|
109
|
+
const plainText = encryption.decrypt(parsed.__oto_payload);
|
|
110
|
+
payload = JSON.parse(plainText);
|
|
111
|
+
} catch {
|
|
112
|
+
return {
|
|
113
|
+
value: void 0,
|
|
114
|
+
rawParsed: parsed,
|
|
115
|
+
parseError: false,
|
|
116
|
+
decryptionError: true,
|
|
117
|
+
expired: false,
|
|
118
|
+
wasEncrypted
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (ttl && ttl > 0) {
|
|
123
|
+
const unwrapped = unwrapWithTTL(payload);
|
|
124
|
+
if (unwrapped.expired) {
|
|
125
|
+
return {
|
|
126
|
+
value: void 0,
|
|
127
|
+
rawParsed: parsed,
|
|
128
|
+
parseError: false,
|
|
129
|
+
decryptionError: false,
|
|
130
|
+
expired: true,
|
|
131
|
+
wasEncrypted
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
payload = unwrapped.value;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
value: payload,
|
|
138
|
+
rawParsed: parsed,
|
|
139
|
+
parseError: false,
|
|
140
|
+
decryptionError: false,
|
|
141
|
+
expired: false,
|
|
142
|
+
wasEncrypted
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function getValueAtPath(safeStorage, prefix, rootKey, path = [], ttl, encryption) {
|
|
146
|
+
const fullKey = `${prefix}${rootKey}`;
|
|
147
|
+
const storedValue = safeStorage.getItem(fullKey);
|
|
148
|
+
if (storedValue === null) return void 0;
|
|
149
|
+
const readResult = readStoredValue(storedValue, ttl, encryption);
|
|
150
|
+
if (readResult.expired || readResult.decryptionError) {
|
|
151
|
+
safeStorage.removeItem(fullKey);
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
maybeMigrate(safeStorage, fullKey, readResult, encryption);
|
|
155
|
+
let current = readResult.value;
|
|
156
|
+
for (const key of path) {
|
|
157
|
+
if (current === null || current === void 0) return void 0;
|
|
158
|
+
current = current[key];
|
|
159
|
+
}
|
|
160
|
+
return current;
|
|
161
|
+
}
|
|
162
|
+
function createNestedProxy(safeStorage, prefix, rootKey, path = [], defaultsAtRoot, ttl, encryption) {
|
|
163
|
+
return new Proxy({}, {
|
|
164
|
+
get(_target, prop) {
|
|
165
|
+
if (typeof prop === "symbol") {
|
|
166
|
+
return getValueAtPath(safeStorage, prefix, rootKey, path, ttl, encryption)?.[prop];
|
|
167
|
+
}
|
|
168
|
+
const current = getValueAtPath(
|
|
169
|
+
safeStorage,
|
|
170
|
+
prefix,
|
|
171
|
+
rootKey,
|
|
172
|
+
path,
|
|
173
|
+
ttl,
|
|
174
|
+
encryption
|
|
175
|
+
);
|
|
176
|
+
let defaultsAtPath;
|
|
177
|
+
if (defaultsAtRoot !== void 0) {
|
|
178
|
+
defaultsAtPath = defaultsAtRoot;
|
|
179
|
+
for (const key of path) {
|
|
180
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object") {
|
|
181
|
+
defaultsAtPath = defaultsAtPath[key];
|
|
182
|
+
} else {
|
|
183
|
+
defaultsAtPath = void 0;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if ((current === null || current === void 0) && !defaultsAtPath) {
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
let value = current?.[prop];
|
|
192
|
+
if (defaultsAtPath && defaultsAtPath[prop] !== void 0) {
|
|
193
|
+
value = deepMerge(defaultsAtPath[prop], value);
|
|
194
|
+
}
|
|
195
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
196
|
+
return createNestedProxy(
|
|
197
|
+
safeStorage,
|
|
198
|
+
prefix,
|
|
199
|
+
rootKey,
|
|
200
|
+
[...path, prop],
|
|
201
|
+
defaultsAtRoot,
|
|
202
|
+
ttl,
|
|
203
|
+
encryption
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
},
|
|
208
|
+
set(_target, prop, value) {
|
|
209
|
+
const fullKey = `${prefix}${rootKey}`;
|
|
210
|
+
const storedValue = safeStorage.getItem(fullKey);
|
|
211
|
+
let rootObj = {};
|
|
212
|
+
if (storedValue !== null) {
|
|
213
|
+
const readResult = readStoredValue(storedValue, ttl, encryption);
|
|
214
|
+
if (readResult.expired || readResult.decryptionError) {
|
|
215
|
+
safeStorage.removeItem(fullKey);
|
|
216
|
+
rootObj = {};
|
|
217
|
+
} else {
|
|
218
|
+
rootObj = readResult.value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (rootObj === null || typeof rootObj !== "object" || Array.isArray(rootObj)) {
|
|
222
|
+
rootObj = {};
|
|
223
|
+
}
|
|
224
|
+
let current = rootObj;
|
|
225
|
+
for (const key of path) {
|
|
226
|
+
if (current[key] === null || typeof current[key] !== "object") {
|
|
227
|
+
current[key] = {};
|
|
228
|
+
}
|
|
229
|
+
current = current[key];
|
|
230
|
+
}
|
|
231
|
+
current[prop] = value;
|
|
232
|
+
try {
|
|
233
|
+
safeStorage.setItem(fullKey, serializeForStorage(rootObj, ttl, encryption));
|
|
234
|
+
return true;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error(`TypedProxy: Failed to save ${rootKey}`, error);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
ownKeys() {
|
|
241
|
+
const current = getValueAtPath(
|
|
242
|
+
safeStorage,
|
|
243
|
+
prefix,
|
|
244
|
+
rootKey,
|
|
245
|
+
path,
|
|
246
|
+
ttl,
|
|
247
|
+
encryption
|
|
248
|
+
);
|
|
249
|
+
const keys = current ? Object.keys(current) : [];
|
|
250
|
+
if (defaultsAtRoot !== void 0) {
|
|
251
|
+
let defaultsAtPath = defaultsAtRoot;
|
|
252
|
+
for (const key of path) {
|
|
253
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object") {
|
|
254
|
+
defaultsAtPath = defaultsAtPath[key];
|
|
255
|
+
} else {
|
|
256
|
+
defaultsAtPath = void 0;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object") {
|
|
261
|
+
const defaultKeys = Object.keys(defaultsAtPath);
|
|
262
|
+
for (const key of defaultKeys) {
|
|
263
|
+
if (!keys.includes(key)) {
|
|
264
|
+
keys.push(key);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return keys;
|
|
270
|
+
},
|
|
271
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
272
|
+
if (typeof prop === "symbol") return void 0;
|
|
273
|
+
const current = getValueAtPath(
|
|
274
|
+
safeStorage,
|
|
275
|
+
prefix,
|
|
276
|
+
rootKey,
|
|
277
|
+
path,
|
|
278
|
+
ttl,
|
|
279
|
+
encryption
|
|
280
|
+
);
|
|
281
|
+
let defaultsAtPath;
|
|
282
|
+
if (defaultsAtRoot !== void 0) {
|
|
283
|
+
defaultsAtPath = defaultsAtRoot;
|
|
284
|
+
for (const key of path) {
|
|
285
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object") {
|
|
286
|
+
defaultsAtPath = defaultsAtPath[key];
|
|
287
|
+
} else {
|
|
288
|
+
defaultsAtPath = void 0;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const inCurrent = current && typeof current === "object" && prop in current;
|
|
294
|
+
const inDefaults = defaultsAtPath && typeof defaultsAtPath === "object" && prop in defaultsAtPath;
|
|
295
|
+
if (!inCurrent && !inDefaults) return void 0;
|
|
296
|
+
let value = current?.[prop];
|
|
297
|
+
if (defaultsAtPath && defaultsAtPath[prop] !== void 0) {
|
|
298
|
+
value = deepMerge(defaultsAtPath[prop], value);
|
|
299
|
+
}
|
|
300
|
+
const descriptorValue = value !== null && typeof value === "object" && !Array.isArray(value) ? createNestedProxy(
|
|
301
|
+
safeStorage,
|
|
302
|
+
prefix,
|
|
303
|
+
rootKey,
|
|
304
|
+
[...path, prop],
|
|
305
|
+
defaultsAtRoot,
|
|
306
|
+
ttl,
|
|
307
|
+
encryption
|
|
308
|
+
) : value;
|
|
309
|
+
return {
|
|
310
|
+
value: descriptorValue,
|
|
311
|
+
writable: true,
|
|
312
|
+
enumerable: true,
|
|
313
|
+
configurable: true
|
|
314
|
+
};
|
|
315
|
+
},
|
|
316
|
+
has(_target, prop) {
|
|
317
|
+
if (typeof prop === "symbol") return false;
|
|
318
|
+
const current = getValueAtPath(
|
|
319
|
+
safeStorage,
|
|
320
|
+
prefix,
|
|
321
|
+
rootKey,
|
|
322
|
+
path,
|
|
323
|
+
ttl,
|
|
324
|
+
encryption
|
|
325
|
+
);
|
|
326
|
+
if (current && typeof current === "object" && prop in current) {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
if (defaultsAtRoot !== void 0) {
|
|
330
|
+
let defaultsAtPath = defaultsAtRoot;
|
|
331
|
+
for (const key of path) {
|
|
332
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object") {
|
|
333
|
+
defaultsAtPath = defaultsAtPath[key];
|
|
334
|
+
} else {
|
|
335
|
+
defaultsAtPath = void 0;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object" && prop in defaultsAtPath) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function oto(options = {}) {
|
|
348
|
+
const { prefix = "", type = "local", defaults = {}, ttl, encryption } = options;
|
|
349
|
+
const storage = typeof window !== "undefined" ? type === "session" ? window.sessionStorage : window.localStorage : null;
|
|
350
|
+
const safeStorage = storage || {
|
|
351
|
+
getItem: () => null,
|
|
352
|
+
setItem: () => {
|
|
353
|
+
},
|
|
354
|
+
removeItem: () => {
|
|
355
|
+
},
|
|
356
|
+
clear: () => {
|
|
357
|
+
},
|
|
358
|
+
key: () => null,
|
|
359
|
+
get length() {
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
return new Proxy({}, {
|
|
364
|
+
get(_target, prop) {
|
|
365
|
+
if (prop === "clearAll") {
|
|
366
|
+
return () => {
|
|
367
|
+
const keys = [];
|
|
368
|
+
for (let i = 0; i < safeStorage.length; i++) {
|
|
369
|
+
const key = safeStorage.key(i);
|
|
370
|
+
if (key) keys.push(key);
|
|
371
|
+
}
|
|
372
|
+
keys.forEach((key) => {
|
|
373
|
+
if (key.startsWith(prefix)) safeStorage.removeItem(key);
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const fullKey = `${prefix}${prop}`;
|
|
378
|
+
const storedValue = safeStorage.getItem(fullKey);
|
|
379
|
+
let parsed;
|
|
380
|
+
if (storedValue === null) {
|
|
381
|
+
parsed = void 0;
|
|
382
|
+
} else {
|
|
383
|
+
const readResult = readStoredValue(storedValue, ttl, encryption);
|
|
384
|
+
if (readResult.expired || readResult.decryptionError) {
|
|
385
|
+
safeStorage.removeItem(fullKey);
|
|
386
|
+
parsed = void 0;
|
|
387
|
+
} else {
|
|
388
|
+
maybeMigrate(safeStorage, fullKey, readResult, encryption);
|
|
389
|
+
parsed = readResult.value;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (defaults && defaults[prop] !== void 0) {
|
|
393
|
+
parsed = deepMerge(defaults[prop], parsed);
|
|
394
|
+
}
|
|
395
|
+
if (parsed === void 0) return void 0;
|
|
396
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
397
|
+
return createNestedProxy(
|
|
398
|
+
safeStorage,
|
|
399
|
+
prefix,
|
|
400
|
+
prop,
|
|
401
|
+
[],
|
|
402
|
+
defaults[prop],
|
|
403
|
+
ttl,
|
|
404
|
+
encryption
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return parsed;
|
|
408
|
+
},
|
|
409
|
+
set(_target, prop, value) {
|
|
410
|
+
try {
|
|
411
|
+
safeStorage.setItem(`${prefix}${prop}`, serializeForStorage(value, ttl, encryption));
|
|
412
|
+
return true;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
console.error(`TypedProxy: Failed to save ${prop}`, error);
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
has(_target, prop) {
|
|
419
|
+
if (defaults && prop in defaults) {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
const fullKey = `${prefix}${prop}`;
|
|
423
|
+
const storedValue = safeStorage.getItem(fullKey);
|
|
424
|
+
if (storedValue === null) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const readResult = readStoredValue(storedValue, ttl, encryption);
|
|
429
|
+
if (readResult.expired || readResult.decryptionError) {
|
|
430
|
+
safeStorage.removeItem(fullKey);
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
return true;
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
deleteProperty(_target, prop) {
|
|
439
|
+
const key = `${prefix}${String(prop)}`;
|
|
440
|
+
safeStorage.removeItem(key);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
var version = "0.4.2";
|
|
446
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
447
|
+
0 && (module.exports = {
|
|
448
|
+
oto,
|
|
449
|
+
version
|
|
450
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function deepMerge(target, source) {
|
|
3
|
+
if (source === null || source === void 0) return target;
|
|
4
|
+
if (target === null || target === void 0) return source;
|
|
5
|
+
if (typeof target !== "object" || typeof source !== "object") return source;
|
|
6
|
+
if (Array.isArray(target) || Array.isArray(source)) return source;
|
|
7
|
+
const result = { ...target };
|
|
8
|
+
for (const key in source) {
|
|
9
|
+
if (source.hasOwnProperty(key)) {
|
|
10
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
function wrapWithTTL(value, ttl) {
|
|
16
|
+
return {
|
|
17
|
+
__oto_value: value,
|
|
18
|
+
__oto_expires: Date.now() + ttl
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function unwrapWithTTL(wrapped) {
|
|
22
|
+
if (wrapped && typeof wrapped === "object" && "__oto_expires" in wrapped && "__oto_value" in wrapped && typeof wrapped.__oto_expires === "number") {
|
|
23
|
+
const expired = Date.now() > wrapped.__oto_expires;
|
|
24
|
+
return { value: wrapped.__oto_value, expired };
|
|
25
|
+
}
|
|
26
|
+
return { value: wrapped, expired: false };
|
|
27
|
+
}
|
|
28
|
+
function isEncryptedWrapper(value) {
|
|
29
|
+
return value && typeof value === "object" && value.__oto_encrypted === true && typeof value.__oto_payload === "string";
|
|
30
|
+
}
|
|
31
|
+
function serializeForStorage(value, ttl, encryption) {
|
|
32
|
+
let valueToStore = value;
|
|
33
|
+
if (ttl && ttl > 0) {
|
|
34
|
+
valueToStore = wrapWithTTL(valueToStore, ttl);
|
|
35
|
+
}
|
|
36
|
+
if (!encryption) {
|
|
37
|
+
return JSON.stringify(valueToStore);
|
|
38
|
+
}
|
|
39
|
+
const plainText = JSON.stringify(valueToStore);
|
|
40
|
+
const cipherText = encryption.encrypt(plainText);
|
|
41
|
+
return JSON.stringify({
|
|
42
|
+
__oto_encrypted: true,
|
|
43
|
+
__oto_payload: cipherText
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function migrateStoredValueToEncrypted(safeStorage, fullKey, rawParsed, encryption) {
|
|
47
|
+
if (!encryption?.migrate || isEncryptedWrapper(rawParsed)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const cipherText = encryption.encrypt(JSON.stringify(rawParsed));
|
|
52
|
+
const wrapped = {
|
|
53
|
+
__oto_encrypted: true,
|
|
54
|
+
__oto_payload: cipherText
|
|
55
|
+
};
|
|
56
|
+
safeStorage.setItem(fullKey, JSON.stringify(wrapped));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`TypedProxy: Failed to migrate ${fullKey} to encrypted format`, error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function maybeMigrate(safeStorage, fullKey, readResult, encryption) {
|
|
62
|
+
if (encryption?.migrate && !readResult.parseError && !readResult.wasEncrypted && readResult.rawParsed !== void 0) {
|
|
63
|
+
migrateStoredValueToEncrypted(safeStorage, fullKey, readResult.rawParsed, encryption);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function readStoredValue(storedValue, ttl, encryption) {
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(storedValue);
|
|
70
|
+
} catch {
|
|
71
|
+
return {
|
|
72
|
+
value: storedValue,
|
|
73
|
+
parseError: true,
|
|
74
|
+
decryptionError: false,
|
|
75
|
+
expired: false,
|
|
76
|
+
wasEncrypted: false
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
let payload = parsed;
|
|
80
|
+
let wasEncrypted = false;
|
|
81
|
+
if (encryption && isEncryptedWrapper(parsed)) {
|
|
82
|
+
wasEncrypted = true;
|
|
83
|
+
try {
|
|
84
|
+
const plainText = encryption.decrypt(parsed.__oto_payload);
|
|
85
|
+
payload = JSON.parse(plainText);
|
|
86
|
+
} catch {
|
|
87
|
+
return {
|
|
88
|
+
value: void 0,
|
|
89
|
+
rawParsed: parsed,
|
|
90
|
+
parseError: false,
|
|
91
|
+
decryptionError: true,
|
|
92
|
+
expired: false,
|
|
93
|
+
wasEncrypted
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (ttl && ttl > 0) {
|
|
98
|
+
const unwrapped = unwrapWithTTL(payload);
|
|
99
|
+
if (unwrapped.expired) {
|
|
100
|
+
return {
|
|
101
|
+
value: void 0,
|
|
102
|
+
rawParsed: parsed,
|
|
103
|
+
parseError: false,
|
|
104
|
+
decryptionError: false,
|
|
105
|
+
expired: true,
|
|
106
|
+
wasEncrypted
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
payload = unwrapped.value;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
value: payload,
|
|
113
|
+
rawParsed: parsed,
|
|
114
|
+
parseError: false,
|
|
115
|
+
decryptionError: false,
|
|
116
|
+
expired: false,
|
|
117
|
+
wasEncrypted
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function getValueAtPath(safeStorage, prefix, rootKey, path = [], ttl, encryption) {
|
|
121
|
+
const fullKey = `${prefix}${rootKey}`;
|
|
122
|
+
const storedValue = safeStorage.getItem(fullKey);
|
|
123
|
+
if (storedValue === null) return void 0;
|
|
124
|
+
const readResult = readStoredValue(storedValue, ttl, encryption);
|
|
125
|
+
if (readResult.expired || readResult.decryptionError) {
|
|
126
|
+
safeStorage.removeItem(fullKey);
|
|
127
|
+
return void 0;
|
|
128
|
+
}
|
|
129
|
+
maybeMigrate(safeStorage, fullKey, readResult, encryption);
|
|
130
|
+
let current = readResult.value;
|
|
131
|
+
for (const key of path) {
|
|
132
|
+
if (current === null || current === void 0) return void 0;
|
|
133
|
+
current = current[key];
|
|
134
|
+
}
|
|
135
|
+
return current;
|
|
136
|
+
}
|
|
137
|
+
function createNestedProxy(safeStorage, prefix, rootKey, path = [], defaultsAtRoot, ttl, encryption) {
|
|
138
|
+
return new Proxy({}, {
|
|
139
|
+
get(_target, prop) {
|
|
140
|
+
if (typeof prop === "symbol") {
|
|
141
|
+
return getValueAtPath(safeStorage, prefix, rootKey, path, ttl, encryption)?.[prop];
|
|
142
|
+
}
|
|
143
|
+
const current = getValueAtPath(
|
|
144
|
+
safeStorage,
|
|
145
|
+
prefix,
|
|
146
|
+
rootKey,
|
|
147
|
+
path,
|
|
148
|
+
ttl,
|
|
149
|
+
encryption
|
|
150
|
+
);
|
|
151
|
+
let defaultsAtPath;
|
|
152
|
+
if (defaultsAtRoot !== void 0) {
|
|
153
|
+
defaultsAtPath = defaultsAtRoot;
|
|
154
|
+
for (const key of path) {
|
|
155
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object") {
|
|
156
|
+
defaultsAtPath = defaultsAtPath[key];
|
|
157
|
+
} else {
|
|
158
|
+
defaultsAtPath = void 0;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if ((current === null || current === void 0) && !defaultsAtPath) {
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
let value = current?.[prop];
|
|
167
|
+
if (defaultsAtPath && defaultsAtPath[prop] !== void 0) {
|
|
168
|
+
value = deepMerge(defaultsAtPath[prop], value);
|
|
169
|
+
}
|
|
170
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
171
|
+
return createNestedProxy(
|
|
172
|
+
safeStorage,
|
|
173
|
+
prefix,
|
|
174
|
+
rootKey,
|
|
175
|
+
[...path, prop],
|
|
176
|
+
defaultsAtRoot,
|
|
177
|
+
ttl,
|
|
178
|
+
encryption
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
},
|
|
183
|
+
set(_target, prop, value) {
|
|
184
|
+
const fullKey = `${prefix}${rootKey}`;
|
|
185
|
+
const storedValue = safeStorage.getItem(fullKey);
|
|
186
|
+
let rootObj = {};
|
|
187
|
+
if (storedValue !== null) {
|
|
188
|
+
const readResult = readStoredValue(storedValue, ttl, encryption);
|
|
189
|
+
if (readResult.expired || readResult.decryptionError) {
|
|
190
|
+
safeStorage.removeItem(fullKey);
|
|
191
|
+
rootObj = {};
|
|
192
|
+
} else {
|
|
193
|
+
rootObj = readResult.value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (rootObj === null || typeof rootObj !== "object" || Array.isArray(rootObj)) {
|
|
197
|
+
rootObj = {};
|
|
198
|
+
}
|
|
199
|
+
let current = rootObj;
|
|
200
|
+
for (const key of path) {
|
|
201
|
+
if (current[key] === null || typeof current[key] !== "object") {
|
|
202
|
+
current[key] = {};
|
|
203
|
+
}
|
|
204
|
+
current = current[key];
|
|
205
|
+
}
|
|
206
|
+
current[prop] = value;
|
|
207
|
+
try {
|
|
208
|
+
safeStorage.setItem(fullKey, serializeForStorage(rootObj, ttl, encryption));
|
|
209
|
+
return true;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error(`TypedProxy: Failed to save ${rootKey}`, error);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
ownKeys() {
|
|
216
|
+
const current = getValueAtPath(
|
|
217
|
+
safeStorage,
|
|
218
|
+
prefix,
|
|
219
|
+
rootKey,
|
|
220
|
+
path,
|
|
221
|
+
ttl,
|
|
222
|
+
encryption
|
|
223
|
+
);
|
|
224
|
+
const keys = current ? Object.keys(current) : [];
|
|
225
|
+
if (defaultsAtRoot !== void 0) {
|
|
226
|
+
let defaultsAtPath = defaultsAtRoot;
|
|
227
|
+
for (const key of path) {
|
|
228
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object") {
|
|
229
|
+
defaultsAtPath = defaultsAtPath[key];
|
|
230
|
+
} else {
|
|
231
|
+
defaultsAtPath = void 0;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object") {
|
|
236
|
+
const defaultKeys = Object.keys(defaultsAtPath);
|
|
237
|
+
for (const key of defaultKeys) {
|
|
238
|
+
if (!keys.includes(key)) {
|
|
239
|
+
keys.push(key);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return keys;
|
|
245
|
+
},
|
|
246
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
247
|
+
if (typeof prop === "symbol") return void 0;
|
|
248
|
+
const current = getValueAtPath(
|
|
249
|
+
safeStorage,
|
|
250
|
+
prefix,
|
|
251
|
+
rootKey,
|
|
252
|
+
path,
|
|
253
|
+
ttl,
|
|
254
|
+
encryption
|
|
255
|
+
);
|
|
256
|
+
let defaultsAtPath;
|
|
257
|
+
if (defaultsAtRoot !== void 0) {
|
|
258
|
+
defaultsAtPath = defaultsAtRoot;
|
|
259
|
+
for (const key of path) {
|
|
260
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object") {
|
|
261
|
+
defaultsAtPath = defaultsAtPath[key];
|
|
262
|
+
} else {
|
|
263
|
+
defaultsAtPath = void 0;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const inCurrent = current && typeof current === "object" && prop in current;
|
|
269
|
+
const inDefaults = defaultsAtPath && typeof defaultsAtPath === "object" && prop in defaultsAtPath;
|
|
270
|
+
if (!inCurrent && !inDefaults) return void 0;
|
|
271
|
+
let value = current?.[prop];
|
|
272
|
+
if (defaultsAtPath && defaultsAtPath[prop] !== void 0) {
|
|
273
|
+
value = deepMerge(defaultsAtPath[prop], value);
|
|
274
|
+
}
|
|
275
|
+
const descriptorValue = value !== null && typeof value === "object" && !Array.isArray(value) ? createNestedProxy(
|
|
276
|
+
safeStorage,
|
|
277
|
+
prefix,
|
|
278
|
+
rootKey,
|
|
279
|
+
[...path, prop],
|
|
280
|
+
defaultsAtRoot,
|
|
281
|
+
ttl,
|
|
282
|
+
encryption
|
|
283
|
+
) : value;
|
|
284
|
+
return {
|
|
285
|
+
value: descriptorValue,
|
|
286
|
+
writable: true,
|
|
287
|
+
enumerable: true,
|
|
288
|
+
configurable: true
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
has(_target, prop) {
|
|
292
|
+
if (typeof prop === "symbol") return false;
|
|
293
|
+
const current = getValueAtPath(
|
|
294
|
+
safeStorage,
|
|
295
|
+
prefix,
|
|
296
|
+
rootKey,
|
|
297
|
+
path,
|
|
298
|
+
ttl,
|
|
299
|
+
encryption
|
|
300
|
+
);
|
|
301
|
+
if (current && typeof current === "object" && prop in current) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
if (defaultsAtRoot !== void 0) {
|
|
305
|
+
let defaultsAtPath = defaultsAtRoot;
|
|
306
|
+
for (const key of path) {
|
|
307
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object") {
|
|
308
|
+
defaultsAtPath = defaultsAtPath[key];
|
|
309
|
+
} else {
|
|
310
|
+
defaultsAtPath = void 0;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (defaultsAtPath && typeof defaultsAtPath === "object" && prop in defaultsAtPath) {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function oto(options = {}) {
|
|
323
|
+
const { prefix = "", type = "local", defaults = {}, ttl, encryption } = options;
|
|
324
|
+
const storage = typeof window !== "undefined" ? type === "session" ? window.sessionStorage : window.localStorage : null;
|
|
325
|
+
const safeStorage = storage || {
|
|
326
|
+
getItem: () => null,
|
|
327
|
+
setItem: () => {
|
|
328
|
+
},
|
|
329
|
+
removeItem: () => {
|
|
330
|
+
},
|
|
331
|
+
clear: () => {
|
|
332
|
+
},
|
|
333
|
+
key: () => null,
|
|
334
|
+
get length() {
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
return new Proxy({}, {
|
|
339
|
+
get(_target, prop) {
|
|
340
|
+
if (prop === "clearAll") {
|
|
341
|
+
return () => {
|
|
342
|
+
const keys = [];
|
|
343
|
+
for (let i = 0; i < safeStorage.length; i++) {
|
|
344
|
+
const key = safeStorage.key(i);
|
|
345
|
+
if (key) keys.push(key);
|
|
346
|
+
}
|
|
347
|
+
keys.forEach((key) => {
|
|
348
|
+
if (key.startsWith(prefix)) safeStorage.removeItem(key);
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const fullKey = `${prefix}${prop}`;
|
|
353
|
+
const storedValue = safeStorage.getItem(fullKey);
|
|
354
|
+
let parsed;
|
|
355
|
+
if (storedValue === null) {
|
|
356
|
+
parsed = void 0;
|
|
357
|
+
} else {
|
|
358
|
+
const readResult = readStoredValue(storedValue, ttl, encryption);
|
|
359
|
+
if (readResult.expired || readResult.decryptionError) {
|
|
360
|
+
safeStorage.removeItem(fullKey);
|
|
361
|
+
parsed = void 0;
|
|
362
|
+
} else {
|
|
363
|
+
maybeMigrate(safeStorage, fullKey, readResult, encryption);
|
|
364
|
+
parsed = readResult.value;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (defaults && defaults[prop] !== void 0) {
|
|
368
|
+
parsed = deepMerge(defaults[prop], parsed);
|
|
369
|
+
}
|
|
370
|
+
if (parsed === void 0) return void 0;
|
|
371
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
372
|
+
return createNestedProxy(
|
|
373
|
+
safeStorage,
|
|
374
|
+
prefix,
|
|
375
|
+
prop,
|
|
376
|
+
[],
|
|
377
|
+
defaults[prop],
|
|
378
|
+
ttl,
|
|
379
|
+
encryption
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return parsed;
|
|
383
|
+
},
|
|
384
|
+
set(_target, prop, value) {
|
|
385
|
+
try {
|
|
386
|
+
safeStorage.setItem(`${prefix}${prop}`, serializeForStorage(value, ttl, encryption));
|
|
387
|
+
return true;
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error(`TypedProxy: Failed to save ${prop}`, error);
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
has(_target, prop) {
|
|
394
|
+
if (defaults && prop in defaults) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
const fullKey = `${prefix}${prop}`;
|
|
398
|
+
const storedValue = safeStorage.getItem(fullKey);
|
|
399
|
+
if (storedValue === null) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const readResult = readStoredValue(storedValue, ttl, encryption);
|
|
404
|
+
if (readResult.expired || readResult.decryptionError) {
|
|
405
|
+
safeStorage.removeItem(fullKey);
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
} catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
deleteProperty(_target, prop) {
|
|
414
|
+
const key = `${prefix}${String(prop)}`;
|
|
415
|
+
safeStorage.removeItem(key);
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
var version = "0.4.2";
|
|
421
|
+
export {
|
|
422
|
+
oto,
|
|
423
|
+
version
|
|
424
|
+
};
|
package/package.json
CHANGED