strata-storage 2.6.1 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/AI-INTEGRATION-GUIDE.md +129 -11
  2. package/README.md +42 -64
  3. package/android/AGENTS.md +18 -3
  4. package/android/CLAUDE.md +36 -3
  5. package/android/build.gradle +5 -1
  6. package/android/src/main/java/com/strata/storage/EncryptedStorage.java +31 -18
  7. package/android/src/main/java/com/strata/storage/FilesystemStorage.java +62 -10
  8. package/android/src/main/java/com/strata/storage/SQLiteStorage.java +65 -13
  9. package/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +63 -51
  10. package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +80 -11
  11. package/dist/README.md +42 -64
  12. package/dist/adapters/capacitor/FilesystemAdapter.d.ts +2 -2
  13. package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
  14. package/dist/adapters/capacitor/FilesystemAdapter.js +6 -2
  15. package/dist/adapters/capacitor/PreferencesAdapter.d.ts +2 -2
  16. package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
  17. package/dist/adapters/capacitor/PreferencesAdapter.js +6 -2
  18. package/dist/adapters/capacitor/SecureAdapter.d.ts +3 -3
  19. package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
  20. package/dist/adapters/capacitor/SecureAdapter.js +6 -2
  21. package/dist/adapters/capacitor/SqliteAdapter.d.ts +24 -3
  22. package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
  23. package/dist/adapters/capacitor/SqliteAdapter.js +105 -23
  24. package/dist/adapters/capacitor/index.d.ts +4 -4
  25. package/dist/adapters/web/CacheAdapter.d.ts +2 -2
  26. package/dist/adapters/web/CacheAdapter.d.ts.map +1 -1
  27. package/dist/adapters/web/CacheAdapter.js +20 -5
  28. package/dist/adapters/web/CookieAdapter.d.ts +2 -2
  29. package/dist/adapters/web/CookieAdapter.d.ts.map +1 -1
  30. package/dist/adapters/web/CookieAdapter.js +27 -3
  31. package/dist/adapters/web/IndexedDBAdapter.d.ts +10 -3
  32. package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
  33. package/dist/adapters/web/IndexedDBAdapter.js +19 -4
  34. package/dist/adapters/web/LocalStorageAdapter.d.ts +2 -2
  35. package/dist/adapters/web/LocalStorageAdapter.d.ts.map +1 -1
  36. package/dist/adapters/web/LocalStorageAdapter.js +15 -1
  37. package/dist/adapters/web/MemoryAdapter.d.ts +9 -2
  38. package/dist/adapters/web/MemoryAdapter.d.ts.map +1 -1
  39. package/dist/adapters/web/MemoryAdapter.js +22 -5
  40. package/dist/adapters/web/SessionStorageAdapter.d.ts +10 -10
  41. package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
  42. package/dist/adapters/web/SessionStorageAdapter.js +17 -3
  43. package/dist/adapters/web/URLAdapter.d.ts +2 -2
  44. package/dist/adapters/web/index.d.ts +7 -7
  45. package/dist/capacitor.d.ts +6 -6
  46. package/dist/capacitor.d.ts.map +1 -1
  47. package/dist/capacitor.js +5 -4
  48. package/dist/core/AdapterRegistry.d.ts +1 -1
  49. package/dist/core/BaseAdapter.d.ts +21 -5
  50. package/dist/core/BaseAdapter.d.ts.map +1 -1
  51. package/dist/core/BaseAdapter.js +29 -1
  52. package/dist/core/Strata.d.ts +53 -7
  53. package/dist/core/Strata.d.ts.map +1 -1
  54. package/dist/core/Strata.js +581 -125
  55. package/dist/features/compression/index.d.ts +1 -1
  56. package/dist/features/compression.d.ts +28 -9
  57. package/dist/features/compression.d.ts.map +1 -1
  58. package/dist/features/compression.js +164 -77
  59. package/dist/features/encryption/index.d.ts +1 -1
  60. package/dist/features/encryption.d.ts +55 -2
  61. package/dist/features/encryption.d.ts.map +1 -1
  62. package/dist/features/encryption.js +139 -26
  63. package/dist/features/migration.d.ts +1 -1
  64. package/dist/features/migration.d.ts.map +1 -1
  65. package/dist/features/migration.js +7 -2
  66. package/dist/features/observer/index.d.ts +1 -1
  67. package/dist/features/observer.d.ts +1 -1
  68. package/dist/features/query/index.d.ts +1 -1
  69. package/dist/features/query.d.ts +2 -1
  70. package/dist/features/query.d.ts.map +1 -1
  71. package/dist/features/query.js +32 -7
  72. package/dist/features/sync/index.d.ts +1 -1
  73. package/dist/features/sync.d.ts +19 -3
  74. package/dist/features/sync.d.ts.map +1 -1
  75. package/dist/features/sync.js +75 -12
  76. package/dist/features/ttl/index.d.ts +1 -1
  77. package/dist/features/ttl.d.ts +2 -2
  78. package/dist/features/ttl.d.ts.map +1 -1
  79. package/dist/features/ttl.js +4 -2
  80. package/dist/firebase.d.ts +1 -1
  81. package/dist/firebase.d.ts.map +1 -1
  82. package/dist/firebase.js +72 -10
  83. package/dist/index.d.ts +24 -22
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +6 -1
  86. package/dist/integrations/angular/index.d.ts +2 -2
  87. package/dist/integrations/angular/index.d.ts.map +1 -1
  88. package/dist/integrations/angular/index.js +7 -8
  89. package/dist/integrations/react/index.d.ts +3 -3
  90. package/dist/integrations/react/index.d.ts.map +1 -1
  91. package/dist/integrations/react/index.js +16 -4
  92. package/dist/integrations/vue/index.d.ts +2 -2
  93. package/dist/integrations/vue/index.d.ts.map +1 -1
  94. package/dist/integrations/vue/index.js +5 -2
  95. package/dist/package.json +11 -2
  96. package/dist/plugin/definitions.d.ts +29 -3
  97. package/dist/plugin/definitions.d.ts.map +1 -1
  98. package/dist/plugin/index.d.ts +2 -2
  99. package/dist/plugin/index.d.ts.map +1 -1
  100. package/dist/plugin/index.js +3 -0
  101. package/dist/plugin/web.d.ts +11 -2
  102. package/dist/plugin/web.d.ts.map +1 -1
  103. package/dist/plugin/web.js +23 -4
  104. package/dist/types/index.d.ts +50 -20
  105. package/dist/types/index.d.ts.map +1 -1
  106. package/dist/types/index.js +1 -10
  107. package/dist/utils/index.d.ts +5 -0
  108. package/dist/utils/index.d.ts.map +1 -1
  109. package/dist/utils/index.js +7 -0
  110. package/ios/AGENTS.md +14 -3
  111. package/ios/CLAUDE.md +25 -3
  112. package/ios/Plugin/FilesystemStorage.swift +19 -0
  113. package/ios/Plugin/SQLiteStorage.swift +59 -9
  114. package/ios/Plugin/StrataStoragePlugin.m +1 -0
  115. package/ios/Plugin/StrataStoragePlugin.swift +87 -34
  116. package/ios/Plugin/UserDefaultsStorage.swift +47 -46
  117. package/package.json +27 -24
  118. package/scripts/build.js +46 -27
  119. package/scripts/cli.js +1 -1
  120. package/scripts/configure.js +2 -2
  121. package/scripts/postinstall.js +3 -2
  122. package/dist/android/AGENTS.md +0 -51
  123. package/dist/android/CLAUDE.md +0 -89
  124. package/dist/android/build.gradle +0 -58
  125. package/dist/android/proguard-rules.pro +0 -11
  126. package/dist/android/settings.gradle +0 -2
  127. package/dist/android/src/main/AndroidManifest.xml +0 -2
  128. package/dist/android/src/main/java/com/strata/storage/EncryptedStorage.java +0 -218
  129. package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +0 -287
  130. package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +0 -371
  131. package/dist/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +0 -209
  132. package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +0 -616
  133. package/dist/core/StorageStrategy.d.ts +0 -55
  134. package/dist/core/StorageStrategy.d.ts.map +0 -1
  135. package/dist/core/StorageStrategy.js +0 -200
  136. package/dist/ios/AGENTS.md +0 -48
  137. package/dist/ios/CLAUDE.md +0 -84
  138. package/dist/ios/Plugin/FilesystemStorage.swift +0 -218
  139. package/dist/ios/Plugin/KeychainStorage.swift +0 -233
  140. package/dist/ios/Plugin/SQLiteStorage.swift +0 -426
  141. package/dist/ios/Plugin/StrataStoragePlugin.m +0 -23
  142. package/dist/ios/Plugin/StrataStoragePlugin.swift +0 -411
  143. package/dist/ios/Plugin/UserDefaultsStorage.swift +0 -87
@@ -3,25 +3,40 @@
3
3
  * Zero-dependency encryption/decryption for storage values
4
4
  */
5
5
  import { EncryptionError } from "../utils/errors.js";
6
+ /**
7
+ * Default PBKDF2 iteration count. Raised to 600,000 to match current OWASP
8
+ * guidance for PBKDF2-HMAC-SHA256. The per-record `iterations` value is stored
9
+ * in {@link EncryptedData} and used on decrypt, so raising this default never
10
+ * breaks data written with an older, lower count.
11
+ */
12
+ export const DEFAULT_PBKDF2_ITERATIONS = 600000;
6
13
  /**
7
14
  * Encryption manager using Web Crypto API
8
15
  */
9
16
  export class EncryptionManager {
10
17
  config;
11
18
  keyCache = new Map();
19
+ macKeyCache = new Map();
12
20
  constructor(config = {}) {
21
+ if (config.keyDerivation && config.keyDerivation !== 'PBKDF2') {
22
+ throw new EncryptionError('Unsupported key derivation function: only PBKDF2 is available');
23
+ }
13
24
  this.config = {
14
25
  algorithm: config.algorithm || 'AES-GCM',
15
26
  keyLength: config.keyLength || 256,
16
- iterations: config.iterations || 100000,
27
+ iterations: config.iterations || DEFAULT_PBKDF2_ITERATIONS,
17
28
  saltLength: config.saltLength || 16,
29
+ keyDerivation: 'PBKDF2',
18
30
  };
19
31
  }
20
32
  /**
21
- * Check if encryption is available
33
+ * Check if encryption is available. Uses the Web Crypto API via `globalThis`
34
+ * so it works in browsers, Web Workers, and modern Node.js / SSR runtimes
35
+ * (Node 20+ exposes `globalThis.crypto.subtle`) — not just the browser.
22
36
  */
23
37
  isAvailable() {
24
- return typeof window !== 'undefined' && window.crypto && window.crypto.subtle !== undefined;
38
+ const cryptoObj = globalThis.crypto;
39
+ return !!cryptoObj && typeof cryptoObj.subtle !== 'undefined';
25
40
  }
26
41
  /**
27
42
  * Encrypt data
@@ -46,14 +61,20 @@ export class EncryptionManager {
46
61
  name: this.config.algorithm,
47
62
  iv: iv,
48
63
  }, key, dataBuffer);
49
- // Convert to base64 for storage
50
- return {
64
+ const result = {
51
65
  data: this.bufferToBase64(encryptedBuffer),
52
- salt: this.bufferToBase64(salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)),
53
- iv: this.bufferToBase64(iv.buffer.slice(iv.byteOffset, iv.byteOffset + iv.byteLength)),
66
+ salt: this.bufferToBase64(this.toArrayBuffer(salt)),
67
+ iv: this.bufferToBase64(this.toArrayBuffer(iv)),
54
68
  algorithm: this.config.algorithm,
55
69
  iterations: this.config.iterations,
56
70
  };
71
+ // Encrypt-then-MAC for the unauthenticated CBC mode.
72
+ if (this.config.algorithm === 'AES-CBC') {
73
+ const macKey = await this.deriveMacKey(password, salt, this.config.iterations);
74
+ const mac = await crypto.subtle.sign('HMAC', macKey, this.concatBuffers(this.toArrayBuffer(iv), encryptedBuffer));
75
+ result.mac = this.bufferToBase64(mac);
76
+ }
77
+ return result;
57
78
  }
58
79
  catch (error) {
59
80
  throw new EncryptionError(`Encryption failed: ${error}`);
@@ -71,6 +92,21 @@ export class EncryptionManager {
71
92
  const dataBuffer = this.base64ToBuffer(encryptedData.data);
72
93
  const salt = this.base64ToBuffer(encryptedData.salt);
73
94
  const iv = this.base64ToBuffer(encryptedData.iv);
95
+ // For AES-CBC, verify the HMAC tag BEFORE attempting decryption
96
+ // (Encrypt-then-MAC). This authenticates the ciphertext and prevents
97
+ // padding-oracle / tampering attacks against the unauthenticated mode.
98
+ if (encryptedData.algorithm === 'AES-CBC') {
99
+ if (!encryptedData.mac) {
100
+ throw new EncryptionError('AES-CBC value is missing its authentication tag. It was written by a version ' +
101
+ 'that did not authenticate CBC and cannot be safely decrypted — re-encrypt the ' +
102
+ 'data (preferably with the default AES-GCM).');
103
+ }
104
+ const macKey = await this.deriveMacKey(password, new Uint8Array(salt), encryptedData.iterations);
105
+ const valid = await crypto.subtle.verify('HMAC', macKey, this.base64ToBuffer(encryptedData.mac), this.concatBuffers(iv, dataBuffer));
106
+ if (!valid) {
107
+ throw new EncryptionError('Authentication failed: data has been tampered with');
108
+ }
109
+ }
74
110
  // Derive key from password
75
111
  const key = await this.deriveKey(password, new Uint8Array(salt), encryptedData.iterations);
76
112
  // Decrypt data
@@ -84,6 +120,8 @@ export class EncryptionManager {
84
120
  return JSON.parse(decryptedStr);
85
121
  }
86
122
  catch (error) {
123
+ if (error instanceof EncryptionError)
124
+ throw error;
87
125
  throw new EncryptionError(`Decryption failed: ${error}`);
88
126
  }
89
127
  }
@@ -91,19 +129,14 @@ export class EncryptionManager {
91
129
  * Derive encryption key from password
92
130
  */
93
131
  async deriveKey(password, salt, iterations = this.config.iterations) {
94
- // Check cache
95
- const saltBuffer = salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength);
96
- const cacheKey = `${password}-${this.bufferToBase64(saltBuffer)}-${iterations}`;
97
- if (this.keyCache.has(cacheKey)) {
98
- return this.keyCache.get(cacheKey);
99
- }
100
- // Import password as key material
101
- const encoder = new TextEncoder();
102
- const passwordBuffer = encoder.encode(password);
103
- const keyMaterial = await crypto.subtle.importKey('raw', passwordBuffer, 'PBKDF2', false, [
104
- 'deriveBits',
105
- 'deriveKey',
106
- ]);
132
+ const saltBuffer = this.toArrayBuffer(salt);
133
+ // Cache key is a hash of the composite, never the raw password — so the
134
+ // plaintext secret is not retained as a Map key for the process lifetime.
135
+ const cacheKey = await this.hashCacheKey(`aes:${this.bufferToBase64(saltBuffer)}:${iterations}`, password);
136
+ const cached = this.keyCache.get(cacheKey);
137
+ if (cached)
138
+ return cached;
139
+ const keyMaterial = await this.importPasswordKeyMaterial(password);
107
140
  // Derive key using PBKDF2
108
141
  const key = await crypto.subtle.deriveKey({
109
142
  name: 'PBKDF2',
@@ -114,10 +147,76 @@ export class EncryptionManager {
114
147
  name: this.config.algorithm,
115
148
  length: this.config.keyLength,
116
149
  }, false, ['encrypt', 'decrypt']);
117
- // Cache the key
118
150
  this.keyCache.set(cacheKey, key);
119
151
  return key;
120
152
  }
153
+ /**
154
+ * Derive a separate HMAC-SHA256 key for Encrypt-then-MAC on AES-CBC.
155
+ *
156
+ * The HMAC key must be cryptographically independent of the AES key, so the
157
+ * PBKDF2 salt is domain-separated with a trailing marker byte. The MAC key is
158
+ * non-extractable and limited to sign/verify.
159
+ */
160
+ async deriveMacKey(password, salt, iterations = this.config.iterations) {
161
+ // Domain separation: a distinct salt yields a key unrelated to the AES key.
162
+ const macSalt = new Uint8Array(salt.length + 1);
163
+ macSalt.set(salt, 0);
164
+ macSalt[salt.length] = 0x01; // marker distinguishing the MAC key derivation
165
+ const macSaltBuffer = this.toArrayBuffer(macSalt);
166
+ const cacheKey = await this.hashCacheKey(`hmac:${this.bufferToBase64(macSaltBuffer)}:${iterations}`, password);
167
+ const cached = this.macKeyCache.get(cacheKey);
168
+ if (cached)
169
+ return cached;
170
+ const keyMaterial = await this.importPasswordKeyMaterial(password);
171
+ const macKey = await crypto.subtle.deriveKey({
172
+ name: 'PBKDF2',
173
+ salt: macSaltBuffer,
174
+ iterations: iterations,
175
+ hash: 'SHA-256',
176
+ }, keyMaterial, {
177
+ name: 'HMAC',
178
+ hash: 'SHA-256',
179
+ length: 256,
180
+ }, false, ['sign', 'verify']);
181
+ this.macKeyCache.set(cacheKey, macKey);
182
+ return macKey;
183
+ }
184
+ /**
185
+ * Import a password string as PBKDF2 key material.
186
+ */
187
+ async importPasswordKeyMaterial(password) {
188
+ const encoder = new TextEncoder();
189
+ const passwordBuffer = encoder.encode(password);
190
+ return crypto.subtle.importKey('raw', passwordBuffer, 'PBKDF2', false, [
191
+ 'deriveBits',
192
+ 'deriveKey',
193
+ ]);
194
+ }
195
+ /**
196
+ * Build an opaque cache key by hashing the password together with the public
197
+ * derivation parameters. Returns a base64 SHA-256 digest so the raw password
198
+ * is never used as (or recoverable from) the Map key.
199
+ */
200
+ async hashCacheKey(scope, password) {
201
+ const encoder = new TextEncoder();
202
+ const digest = await crypto.subtle.digest('SHA-256', encoder.encode(`${scope}:${password}`));
203
+ return this.bufferToBase64(digest);
204
+ }
205
+ /**
206
+ * Copy a (possibly offset) typed array into a standalone ArrayBuffer.
207
+ */
208
+ toArrayBuffer(view) {
209
+ return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
210
+ }
211
+ /**
212
+ * Concatenate two buffers into one ArrayBuffer.
213
+ */
214
+ concatBuffers(a, b) {
215
+ const out = new Uint8Array(a.byteLength + b.byteLength);
216
+ out.set(new Uint8Array(a), 0);
217
+ out.set(new Uint8Array(b), a.byteLength);
218
+ return out.buffer;
219
+ }
121
220
  /**
122
221
  * Convert ArrayBuffer to base64
123
222
  */
@@ -145,17 +244,31 @@ export class EncryptionManager {
145
244
  */
146
245
  clearCache() {
147
246
  this.keyCache.clear();
247
+ this.macKeyCache.clear();
148
248
  }
149
249
  /**
150
- * Generate a secure random password
250
+ * Generate a secure random password.
251
+ *
252
+ * Uses rejection sampling so every character in the alphabet is equally
253
+ * likely. Naive `randomByte % alphabetLength` is biased whenever 256 is not a
254
+ * multiple of the alphabet length (here 89), making some characters more
255
+ * probable and slightly reducing entropy.
151
256
  */
152
257
  generatePassword(length = 32) {
153
258
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
154
- const array = new Uint8Array(length);
155
- crypto.getRandomValues(array);
259
+ // Largest multiple of chars.length that fits in a byte; bytes at or above
260
+ // this threshold are rejected to remove modulo bias.
261
+ const limit = Math.floor(256 / chars.length) * chars.length;
156
262
  let password = '';
157
- for (let i = 0; i < length; i++) {
158
- password += chars[array[i] % chars.length];
263
+ const buffer = new Uint8Array(length);
264
+ while (password.length < length) {
265
+ crypto.getRandomValues(buffer);
266
+ for (let i = 0; i < buffer.length && password.length < length; i++) {
267
+ const byte = buffer[i];
268
+ if (byte < limit) {
269
+ password += chars[byte % chars.length];
270
+ }
271
+ }
159
272
  }
160
273
  return password;
161
274
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Migration utilities for storage upgrades
3
3
  */
4
- import type { StorageAdapter } from '@/types';
4
+ import type { StorageAdapter } from "../types/index.js";
5
5
  export interface Migration {
6
6
  version: number;
7
7
  up: (adapter: StorageAdapter) => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../../src/features/migration.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,UAAU,CAAmB;IAErC,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAK9B,OAAO,CAAC,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YA0B9D,iBAAiB;YAKjB,UAAU;CAOzB"}
1
+ {"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../../src/features/migration.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,UAAU,CAAmB;IAErC,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAK9B,OAAO,CAAC,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAgC9D,iBAAiB;YAKjB,UAAU;CAOzB"}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Migration utilities for storage upgrades
3
3
  */
4
+ import { MigrationError } from "../utils/errors.js";
4
5
  export class MigrationManager {
5
6
  migrations = [];
6
7
  register(migration) {
@@ -22,9 +23,13 @@ export class MigrationManager {
22
23
  }
23
24
  else {
24
25
  for (const migration of migrationsToRun.reverse()) {
25
- if (migration.down) {
26
- await migration.down(adapter);
26
+ // An irreversible migration must NOT silently lower the stored version
27
+ // (that would claim a rollback that never reverted the data). Fail
28
+ // before touching anything so the version stays consistent with data.
29
+ if (!migration.down) {
30
+ throw new MigrationError(`Cannot roll back migration ${migration.version}: it has no down() and is irreversible.`);
27
31
  }
32
+ await migration.down(adapter);
28
33
  await this.setVersion(adapter, migration.version - 1);
29
34
  }
30
35
  }
@@ -1,2 +1,2 @@
1
- export * from '../observer';
1
+ export * from "../observer.js";
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Storage Observer - Watch for storage changes
3
3
  */
4
- import type { ObserverCallback, StorageEvent } from '@/types';
4
+ import type { ObserverCallback, StorageEvent } from "../types/index.js";
5
5
  export declare class StorageObserver {
6
6
  private observers;
7
7
  /**
@@ -1,2 +1,2 @@
1
- export * from '../query';
1
+ export * from "../query.js";
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -2,7 +2,7 @@
2
2
  * Query Engine Feature
3
3
  * Zero-dependency implementation of MongoDB-like query operators
4
4
  */
5
- import type { QueryCondition } from '@/types';
5
+ import type { QueryCondition } from "../types/index.js";
6
6
  /**
7
7
  * Query engine for advanced data filtering
8
8
  */
@@ -68,6 +68,7 @@ export declare class QueryEngine {
68
68
  /**
69
69
  * Set nested value in object using dot notation
70
70
  */
71
+ private static readonly FORBIDDEN_KEYS;
71
72
  private setNestedValue;
72
73
  /**
73
74
  * Delete nested value from object using dot notation
@@ -1 +1 @@
1
- {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/features/query.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,SAAS,CAAC;AAG9D;;GAEG;AACH,qBAAa,WAAW;IACtB;;OAEG;IACH,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,cAAc,GAAG,OAAO;IAwB3D;;OAEG;IACH,OAAO,CAAC,YAAY;IAIpB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IASxB;;OAEG;IACH,OAAO,CAAC,eAAe;IA2DvB;;OAEG;IACH,OAAO,CAAC,aAAa;IAkBrB;;OAEG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACH,OAAO,CAAC,MAAM;IA+Bd;;OAEG;IACH,OAAO,CAAC,OAAO;IAyBf;;;;;;;;OAQG;IACH,OAAO,CAAC,YAAY;IAOpB;;OAEG;IACH,OAAO,CAAC,OAAO;IASf;;OAEG;IACH,OAAO,CAAC,WAAW;IAcnB;;OAEG;IACH,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE;IAoBxD;;OAEG;IACH,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IA2B5E;;OAEG;IACH,OAAO,CAAC,cAAc;IAetB;;OAEG;IACH,OAAO,CAAC,iBAAiB;CAc1B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,CAE/C"}
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/features/query.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,SAAS,CAAC;AAG9D;;GAEG;AACH,qBAAa,WAAW;IACtB;;OAEG;IACH,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,cAAc,GAAG,OAAO;IAwB3D;;OAEG;IACH,OAAO,CAAC,YAAY;IAIpB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IASxB;;OAEG;IACH,OAAO,CAAC,eAAe;IAkEvB;;OAEG;IACH,OAAO,CAAC,aAAa;IAkBrB;;OAEG;IACH,OAAO,CAAC,cAAc;IAqBtB;;OAEG;IACH,OAAO,CAAC,MAAM;IA+Bd;;OAEG;IACH,OAAO,CAAC,OAAO;IAyBf;;;;;;;;OAQG;IACH,OAAO,CAAC,YAAY;IAOpB;;OAEG;IACH,OAAO,CAAC,OAAO;IASf;;OAEG;IACH,OAAO,CAAC,WAAW;IAcnB;;OAEG;IACH,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE;IAoBxD;;OAEG;IACH,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IA6B5E;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAsD;IAE5F,OAAO,CAAC,cAAc;IAyBtB;;OAEG;IACH,OAAO,CAAC,iBAAiB;CAkB1B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,CAE/C"}
@@ -73,7 +73,15 @@ export class QueryEngine {
73
73
  // Otherwise check if value equals any operand value
74
74
  return operand.some((v) => this.equals(value, v));
75
75
  case '$nin':
76
- return Array.isArray(operand) && !operand.some((v) => this.equals(value, v));
76
+ if (!Array.isArray(operand))
77
+ return false;
78
+ // Mirror $in: for an array field, "not in" means none of its items
79
+ // match any operand value (the previous code compared the whole array
80
+ // to each scalar operand, giving wrong results for array fields).
81
+ if (Array.isArray(value)) {
82
+ return !operand.some((v) => value.some((item) => this.equals(item, v)));
83
+ }
84
+ return !operand.some((v) => this.equals(value, v));
77
85
  case '$regex':
78
86
  return this.matchesRegex(value, operand);
79
87
  case '$exists':
@@ -121,7 +129,9 @@ export class QueryEngine {
121
129
  if (current === null || current === undefined) {
122
130
  return undefined;
123
131
  }
124
- if (typeof current === 'object' && part in current) {
132
+ // Match own properties only `in` would traverse the prototype chain and
133
+ // could resolve a path segment to an inherited member (e.g. `constructor`).
134
+ if (typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, part)) {
125
135
  current = current[part];
126
136
  }
127
137
  else {
@@ -270,8 +280,10 @@ export class QueryEngine {
270
280
  }
271
281
  }
272
282
  else {
273
- // Exclusion mode - include all except specified fields
274
- result.value = { ...item };
283
+ // Exclusion mode - include all except specified fields. Spread the item's
284
+ // own fields directly onto result (NOT under a `value` wrapper, which left
285
+ // the excluded keys unreachable and returned the whole item unchanged).
286
+ Object.assign(result, item);
275
287
  for (const [key, exclude] of Object.entries(projection)) {
276
288
  if (exclude === 0 || exclude === false) {
277
289
  this.deleteNestedValue(result, key);
@@ -283,17 +295,28 @@ export class QueryEngine {
283
295
  /**
284
296
  * Set nested value in object using dot notation
285
297
  */
298
+ static FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
286
299
  setNestedValue(obj, path, value) {
287
300
  const parts = path.split('.');
288
301
  let current = obj;
289
302
  for (let i = 0; i < parts.length - 1; i++) {
290
303
  const part = parts[i];
291
- if (!(part in current) || typeof current[part] !== 'object') {
304
+ // Refuse prototype-polluting paths and use own-property checks only — `in`
305
+ // walks the prototype chain, so a '__proto__.x' path could reach
306
+ // Object.prototype.
307
+ if (QueryEngine.FORBIDDEN_KEYS.has(part))
308
+ return;
309
+ if (!Object.prototype.hasOwnProperty.call(current, part) ||
310
+ typeof current[part] !== 'object' ||
311
+ current[part] === null) {
292
312
  current[part] = {};
293
313
  }
294
314
  current = current[part];
295
315
  }
296
- current[parts[parts.length - 1]] = value;
316
+ const last = parts[parts.length - 1];
317
+ if (QueryEngine.FORBIDDEN_KEYS.has(last))
318
+ return;
319
+ current[last] = value;
297
320
  }
298
321
  /**
299
322
  * Delete nested value from object using dot notation
@@ -303,7 +326,9 @@ export class QueryEngine {
303
326
  let current = obj;
304
327
  for (let i = 0; i < parts.length - 1; i++) {
305
328
  const part = parts[i];
306
- if (!(part in current) || typeof current[part] !== 'object') {
329
+ if (!Object.prototype.hasOwnProperty.call(current, part) ||
330
+ typeof current[part] !== 'object' ||
331
+ current[part] === null) {
307
332
  return;
308
333
  }
309
334
  current = current[part];
@@ -1,2 +1,2 @@
1
- export * from '../sync';
1
+ export * from "../sync.js";
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -2,8 +2,8 @@
2
2
  * Cross-tab Synchronization Feature
3
3
  * Zero-dependency implementation using BroadcastChannel and storage events
4
4
  */
5
- import { EventEmitter } from '@/utils';
6
- import type { StorageType, SubscriptionCallback, UnsubscribeFunction } from '@/types';
5
+ import { EventEmitter } from "../utils/index.js";
6
+ import type { StorageType, SubscriptionCallback, UnsubscribeFunction } from "../types/index.js";
7
7
  /**
8
8
  * Sync configuration
9
9
  */
@@ -61,6 +61,14 @@ export declare class SyncManager extends EventEmitter {
61
61
  private setupBroadcastChannel;
62
62
  /**
63
63
  * Set up storage event listeners for fallback sync
64
+ *
65
+ * NOTE: the `storage` event is the cross-tab fallback for environments without
66
+ * BroadcastChannel, and the browser only fires it for `localStorage` changes
67
+ * made in OTHER tabs. `sessionStorage` is tab-scoped — it never propagates
68
+ * cross-tab and never raises a cross-tab `storage` event — so this fallback is
69
+ * inherently localStorage-only (see `SessionStorageAdapter.capabilities.crossTab
70
+ * === false`). BroadcastChannel remains the real cross-tab mechanism when
71
+ * available.
64
72
  */
65
73
  private setupStorageEvents;
66
74
  /**
@@ -72,7 +80,15 @@ export declare class SyncManager extends EventEmitter {
72
80
  */
73
81
  private emitChange;
74
82
  /**
75
- * Generate unique origin ID
83
+ * Narrow an untrusted BroadcastChannel payload to a well-formed SyncMessage.
84
+ */
85
+ private isValidSyncMessage;
86
+ /**
87
+ * Generate unique origin ID.
88
+ *
89
+ * Prefers `crypto.randomUUID()` so two tabs cannot collide on the same origin
90
+ * (a collision would defeat the self-message filter and cause an echo loop).
91
+ * Falls back to timestamp + random only where Web Crypto is unavailable.
76
92
  */
77
93
  private generateOrigin;
78
94
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/features/sync.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAEV,WAAW,EACX,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,kBAAkB,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,CAAC;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;IAC3C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAC,CAAmB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAuE;IACxF,OAAO,CAAC,cAAc,CAAyD;gBAEnE,MAAM,GAAE,UAAe;IAYnC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAYjC;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,IAAI;IAuBrD;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,mBAAmB;IAY9D;;OAEG;IACH,KAAK,IAAI,IAAI;IAuBb;;OAEG;IACH,OAAO,CAAC,2BAA2B;IAInC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA6B7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA+C1B;;OAEG;IACH,OAAO,CAAC,WAAW;IAcnB;;OAEG;IACH,OAAO,CAAC,UAAU;IAIlB;;OAEG;IACH,OAAO,CAAC,cAAc;IAItB;;OAEG;IACH,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;CAyB5C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,WAAW,CAElE"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/features/sync.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAEV,WAAW,EAEX,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,SAAS,CAAC;AAYjB;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,kBAAkB,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,CAAC;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,YAAY;IAC3C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAC,CAAmB;IACnC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAuE;IACxF,OAAO,CAAC,cAAc,CAAyD;gBAEnE,MAAM,GAAE,UAAe;IAYnC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAYjC;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,IAAI;IAuBrD;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,oBAAoB,GAAG,mBAAmB;IAY9D;;OAEG;IACH,KAAK,IAAI,IAAI;IAuBb;;OAEG;IACH,OAAO,CAAC,2BAA2B;IAInC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAkC7B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,kBAAkB;IA2C1B;;OAEG;IACH,OAAO,CAAC,WAAW;IAcnB;;OAEG;IACH,OAAO,CAAC,UAAU;IAIlB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAQtB;;OAEG;IACH,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;CAyD5C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,WAAW,CAElE"}
@@ -4,6 +4,13 @@
4
4
  */
5
5
  import { EventEmitter } from "../utils/index.js";
6
6
  import { logger } from "../utils/logger.js";
7
+ function isStorageValue(value) {
8
+ return (typeof value === 'object' &&
9
+ value !== null &&
10
+ 'value' in value &&
11
+ 'created' in value &&
12
+ 'updated' in value);
13
+ }
7
14
  /**
8
15
  * Cross-tab synchronization manager
9
16
  */
@@ -105,6 +112,11 @@ export class SyncManager extends EventEmitter {
105
112
  this.channel = new BroadcastChannel(this.config.channelName);
106
113
  this.channel.onmessage = (event) => {
107
114
  const message = event.data;
115
+ // Validate inbound shape before trusting it. Same-origin scripts can
116
+ // post arbitrary messages onto this channel, so reject anything that
117
+ // isn't a well-formed SyncMessage instead of emitting a malformed change.
118
+ if (!this.isValidSyncMessage(message))
119
+ return;
108
120
  // Ignore messages from self
109
121
  if (message.origin === this.origin)
110
122
  return;
@@ -128,19 +140,23 @@ export class SyncManager extends EventEmitter {
128
140
  }
129
141
  /**
130
142
  * Set up storage event listeners for fallback sync
143
+ *
144
+ * NOTE: the `storage` event is the cross-tab fallback for environments without
145
+ * BroadcastChannel, and the browser only fires it for `localStorage` changes
146
+ * made in OTHER tabs. `sessionStorage` is tab-scoped — it never propagates
147
+ * cross-tab and never raises a cross-tab `storage` event — so this fallback is
148
+ * inherently localStorage-only (see `SessionStorageAdapter.capabilities.crossTab
149
+ * === false`). BroadcastChannel remains the real cross-tab mechanism when
150
+ * available.
131
151
  */
132
152
  setupStorageEvents() {
133
153
  if (typeof window === 'undefined')
134
154
  return;
135
155
  const listener = (event) => {
136
- // Only process events from other windows
156
+ // The browser only fires cross-tab `storage` events for localStorage.
137
157
  if (event.storageArea !== window.localStorage)
138
158
  return;
139
- // Parse the storage type from key prefix
140
- let storageType = 'localStorage';
141
- if (event.key?.startsWith('strata:session:')) {
142
- storageType = 'sessionStorage';
143
- }
159
+ const storageType = 'localStorage';
144
160
  // Check if this storage type is enabled for sync
145
161
  if (!this.config.storages.includes(storageType))
146
162
  return;
@@ -194,10 +210,30 @@ export class SyncManager extends EventEmitter {
194
210
  this.emit('change', change);
195
211
  }
196
212
  /**
197
- * Generate unique origin ID
213
+ * Narrow an untrusted BroadcastChannel payload to a well-formed SyncMessage.
214
+ */
215
+ isValidSyncMessage(message) {
216
+ if (typeof message !== 'object' || message === null)
217
+ return false;
218
+ const m = message;
219
+ return ((m.type === 'set' || m.type === 'remove' || m.type === 'clear') &&
220
+ typeof m.storage === 'string' &&
221
+ typeof m.timestamp === 'number' &&
222
+ typeof m.origin === 'string');
223
+ }
224
+ /**
225
+ * Generate unique origin ID.
226
+ *
227
+ * Prefers `crypto.randomUUID()` so two tabs cannot collide on the same origin
228
+ * (a collision would defeat the self-message filter and cause an echo loop).
229
+ * Falls back to timestamp + random only where Web Crypto is unavailable.
198
230
  */
199
231
  generateOrigin() {
200
- return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
232
+ const cryptoObj = globalThis.crypto;
233
+ if (cryptoObj && typeof cryptoObj.randomUUID === 'function') {
234
+ return cryptoObj.randomUUID();
235
+ }
236
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
201
237
  }
202
238
  /**
203
239
  * Resolve conflicts between values
@@ -211,11 +247,38 @@ export class SyncManager extends EventEmitter {
211
247
  return this.config.conflictResolution(values);
212
248
  }
213
249
  switch (this.config.conflictResolution) {
214
- case 'latest':
215
- // Return the last value (most recent)
216
- return values[values.length - 1];
250
+ case 'latest': {
251
+ // Most-recent-wins by the StorageValue `updated` timestamp so two tabs
252
+ // with concurrent same-key writes converge on the genuinely newest
253
+ // value, instead of each applying the other's (last-received-wins, which
254
+ // diverges and lets a delayed older message clobber a newer local
255
+ // write). Falls back to positional-last when values carry no timestamp.
256
+ let newest = values[values.length - 1];
257
+ let newestTime = isStorageValue(newest) ? newest.updated : undefined;
258
+ for (const candidate of values) {
259
+ if (!isStorageValue(candidate))
260
+ continue;
261
+ if (typeof newestTime !== 'number' || candidate.updated >= newestTime) {
262
+ newest = candidate;
263
+ newestTime = candidate.updated;
264
+ }
265
+ }
266
+ return newest;
267
+ }
217
268
  case 'merge':
218
- // Simple merge for objects
269
+ if (values.every(isStorageValue)) {
270
+ const wrappers = values;
271
+ const latest = wrappers[wrappers.length - 1];
272
+ const payloads = wrappers.map((entry) => entry.value);
273
+ if (payloads.every((payload) => typeof payload === 'object' && payload !== null && !Array.isArray(payload))) {
274
+ return {
275
+ ...latest,
276
+ value: Object.assign({}, ...payloads),
277
+ };
278
+ }
279
+ return latest;
280
+ }
281
+ // Simple merge for plain user objects
219
282
  if (values.every((v) => typeof v === 'object' && v !== null && !Array.isArray(v))) {
220
283
  return Object.assign({}, ...values);
221
284
  }
@@ -1,2 +1,2 @@
1
- export * from '../ttl';
1
+ export * from "../ttl.js";
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -2,8 +2,8 @@
2
2
  * TTL (Time To Live) Manager
3
3
  * Handles automatic expiration of storage items
4
4
  */
5
- import type { StorageValue } from '@/types';
6
- import { EventEmitter } from '@/utils';
5
+ import type { StorageValue } from "../types/index.js";
6
+ import { EventEmitter } from "../utils/index.js";
7
7
  export interface TTLConfig {
8
8
  /**
9
9
  * Default TTL in milliseconds
@@ -1 +1 @@
1
- {"version":3,"file":"ttl.d.ts","sourceRoot":"","sources":["../../src/features/ttl.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,WAAW,SAAS;IACxB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;OAEG;IACH,QAAQ,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAEzB;;OAEG;IACH,WAAW,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,UAAW,SAAQ,YAAY;IAC1C,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,eAAe,CAAC,CAAiC;IACzD,OAAO,CAAC,gBAAgB,CAA+C;gBAE3D,MAAM,GAAE,SAAc;IAWlC;;OAEG;IACH,mBAAmB,CAAC,OAAO,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS;IA2B7D;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO;IAKvC;;OAEG;IACH,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,YAAY;IAezE;;OAEG;IACH,gBAAgB,CACd,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,EAChC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,EACtD,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GACzC,IAAI;IAcP;;OAEG;IACH,eAAe,IAAI,IAAI;IAOvB;;OAEG;IACG,OAAO,CACX,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,EAChC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,EACtD,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GACzC,OAAO,CAAC,WAAW,EAAE,CAAC;IA0CzB;;OAEG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAI3D;;OAEG;IACH,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIxC;;OAEG;IACG,WAAW,CACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,EAChC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,GACrD,OAAO,CAAC,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAmBrD;;OAEG;IACH,SAAS,CAAC,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY;IAgB/D;;OAEG;IACH,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY;IAQ1C;;OAEG;IACH,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI;IAMjD;;OAEG;IACH,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAiBvC;;OAEG;IACH,KAAK,IAAI,IAAI;CAKd;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,UAAU,CAE/D"}
1
+ {"version":3,"file":"ttl.d.ts","sourceRoot":"","sources":["../../src/features/ttl.ts"],"names":[],"mappings":"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"}
@@ -32,10 +32,12 @@ export class TTLManager extends EventEmitter {
32
32
  const time = options.expireAt instanceof Date ? options.expireAt.getTime() : options.expireAt;
33
33
  return time > Date.now() ? time : undefined;
34
34
  }
35
- // Expire after a certain date
35
+ // Expire after a certain date — documented as an alias of expireAt, so it
36
+ // gets the same past-guard (a past time yields no expiry rather than an
37
+ // entry that is dead on arrival).
36
38
  if (options.expireAfter) {
37
39
  const afterTime = options.expireAfter instanceof Date ? options.expireAfter.getTime() : options.expireAfter;
38
- return afterTime;
40
+ return afterTime > Date.now() ? afterTime : undefined;
39
41
  }
40
42
  // TTL from now
41
43
  if (options.ttl) {