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.
@@ -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 };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oto-storage",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "A lightweight, type-safe wrapper for localStorage and sessionStorage using the Proxy API.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",