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.
- package/AI-INTEGRATION-GUIDE.md +129 -11
- package/README.md +42 -64
- package/android/AGENTS.md +18 -3
- package/android/CLAUDE.md +36 -3
- package/android/build.gradle +5 -1
- package/android/src/main/java/com/strata/storage/EncryptedStorage.java +31 -18
- package/android/src/main/java/com/strata/storage/FilesystemStorage.java +62 -10
- package/android/src/main/java/com/strata/storage/SQLiteStorage.java +65 -13
- package/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +63 -51
- package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +80 -11
- package/dist/README.md +42 -64
- package/dist/adapters/capacitor/FilesystemAdapter.d.ts +2 -2
- package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/FilesystemAdapter.js +6 -2
- package/dist/adapters/capacitor/PreferencesAdapter.d.ts +2 -2
- package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/PreferencesAdapter.js +6 -2
- package/dist/adapters/capacitor/SecureAdapter.d.ts +3 -3
- package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/SecureAdapter.js +6 -2
- package/dist/adapters/capacitor/SqliteAdapter.d.ts +24 -3
- package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/SqliteAdapter.js +105 -23
- package/dist/adapters/capacitor/index.d.ts +4 -4
- package/dist/adapters/web/CacheAdapter.d.ts +2 -2
- package/dist/adapters/web/CacheAdapter.d.ts.map +1 -1
- package/dist/adapters/web/CacheAdapter.js +20 -5
- package/dist/adapters/web/CookieAdapter.d.ts +2 -2
- package/dist/adapters/web/CookieAdapter.d.ts.map +1 -1
- package/dist/adapters/web/CookieAdapter.js +27 -3
- package/dist/adapters/web/IndexedDBAdapter.d.ts +10 -3
- package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
- package/dist/adapters/web/IndexedDBAdapter.js +19 -4
- package/dist/adapters/web/LocalStorageAdapter.d.ts +2 -2
- package/dist/adapters/web/LocalStorageAdapter.d.ts.map +1 -1
- package/dist/adapters/web/LocalStorageAdapter.js +15 -1
- package/dist/adapters/web/MemoryAdapter.d.ts +9 -2
- package/dist/adapters/web/MemoryAdapter.d.ts.map +1 -1
- package/dist/adapters/web/MemoryAdapter.js +22 -5
- package/dist/adapters/web/SessionStorageAdapter.d.ts +10 -10
- package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
- package/dist/adapters/web/SessionStorageAdapter.js +17 -3
- package/dist/adapters/web/URLAdapter.d.ts +2 -2
- package/dist/adapters/web/index.d.ts +7 -7
- package/dist/capacitor.d.ts +6 -6
- package/dist/capacitor.d.ts.map +1 -1
- package/dist/capacitor.js +5 -4
- package/dist/core/AdapterRegistry.d.ts +1 -1
- package/dist/core/BaseAdapter.d.ts +21 -5
- package/dist/core/BaseAdapter.d.ts.map +1 -1
- package/dist/core/BaseAdapter.js +29 -1
- package/dist/core/Strata.d.ts +53 -7
- package/dist/core/Strata.d.ts.map +1 -1
- package/dist/core/Strata.js +581 -125
- package/dist/features/compression/index.d.ts +1 -1
- package/dist/features/compression.d.ts +28 -9
- package/dist/features/compression.d.ts.map +1 -1
- package/dist/features/compression.js +164 -77
- package/dist/features/encryption/index.d.ts +1 -1
- package/dist/features/encryption.d.ts +55 -2
- package/dist/features/encryption.d.ts.map +1 -1
- package/dist/features/encryption.js +139 -26
- package/dist/features/migration.d.ts +1 -1
- package/dist/features/migration.d.ts.map +1 -1
- package/dist/features/migration.js +7 -2
- package/dist/features/observer/index.d.ts +1 -1
- package/dist/features/observer.d.ts +1 -1
- package/dist/features/query/index.d.ts +1 -1
- package/dist/features/query.d.ts +2 -1
- package/dist/features/query.d.ts.map +1 -1
- package/dist/features/query.js +32 -7
- package/dist/features/sync/index.d.ts +1 -1
- package/dist/features/sync.d.ts +19 -3
- package/dist/features/sync.d.ts.map +1 -1
- package/dist/features/sync.js +75 -12
- package/dist/features/ttl/index.d.ts +1 -1
- package/dist/features/ttl.d.ts +2 -2
- package/dist/features/ttl.d.ts.map +1 -1
- package/dist/features/ttl.js +4 -2
- package/dist/firebase.d.ts +1 -1
- package/dist/firebase.d.ts.map +1 -1
- package/dist/firebase.js +72 -10
- package/dist/index.d.ts +24 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/integrations/angular/index.d.ts +2 -2
- package/dist/integrations/angular/index.d.ts.map +1 -1
- package/dist/integrations/angular/index.js +7 -8
- package/dist/integrations/react/index.d.ts +3 -3
- package/dist/integrations/react/index.d.ts.map +1 -1
- package/dist/integrations/react/index.js +16 -4
- package/dist/integrations/vue/index.d.ts +2 -2
- package/dist/integrations/vue/index.d.ts.map +1 -1
- package/dist/integrations/vue/index.js +5 -2
- package/dist/package.json +11 -2
- package/dist/plugin/definitions.d.ts +29 -3
- package/dist/plugin/definitions.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +3 -0
- package/dist/plugin/web.d.ts +11 -2
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +23 -4
- package/dist/types/index.d.ts +50 -20
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -10
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +7 -0
- package/ios/AGENTS.md +14 -3
- package/ios/CLAUDE.md +25 -3
- package/ios/Plugin/FilesystemStorage.swift +19 -0
- package/ios/Plugin/SQLiteStorage.swift +59 -9
- package/ios/Plugin/StrataStoragePlugin.m +1 -0
- package/ios/Plugin/StrataStoragePlugin.swift +87 -34
- package/ios/Plugin/UserDefaultsStorage.swift +47 -46
- package/package.json +27 -24
- package/scripts/build.js +46 -27
- package/scripts/cli.js +1 -1
- package/scripts/configure.js +2 -2
- package/scripts/postinstall.js +3 -2
- package/dist/android/AGENTS.md +0 -51
- package/dist/android/CLAUDE.md +0 -89
- package/dist/android/build.gradle +0 -58
- package/dist/android/proguard-rules.pro +0 -11
- package/dist/android/settings.gradle +0 -2
- package/dist/android/src/main/AndroidManifest.xml +0 -2
- package/dist/android/src/main/java/com/strata/storage/EncryptedStorage.java +0 -218
- package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +0 -287
- package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +0 -371
- package/dist/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +0 -209
- package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +0 -616
- package/dist/core/StorageStrategy.d.ts +0 -55
- package/dist/core/StorageStrategy.d.ts.map +0 -1
- package/dist/core/StorageStrategy.js +0 -200
- package/dist/ios/AGENTS.md +0 -48
- package/dist/ios/CLAUDE.md +0 -84
- package/dist/ios/Plugin/FilesystemStorage.swift +0 -218
- package/dist/ios/Plugin/KeychainStorage.swift +0 -233
- package/dist/ios/Plugin/SQLiteStorage.swift +0 -426
- package/dist/ios/Plugin/StrataStoragePlugin.m +0 -23
- package/dist/ios/Plugin/StrataStoragePlugin.swift +0 -411
- 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 ||
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
return {
|
|
64
|
+
const result = {
|
|
51
65
|
data: this.bufferToBase64(encryptedBuffer),
|
|
52
|
-
salt: this.bufferToBase64(
|
|
53
|
-
iv: this.bufferToBase64(
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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 +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;
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
1
|
+
export * from "../observer.js";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from "../query.js";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/features/query.d.ts
CHANGED
|
@@ -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
|
|
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;
|
|
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"}
|
package/dist/features/query.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (!(
|
|
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
|
|
1
|
+
export * from "../sync.js";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/features/sync.d.ts
CHANGED
|
@@ -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
|
|
6
|
-
import type { StorageType, SubscriptionCallback, UnsubscribeFunction } from
|
|
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
|
-
*
|
|
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,
|
|
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"}
|
package/dist/features/sync.js
CHANGED
|
@@ -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
|
-
//
|
|
156
|
+
// The browser only fires cross-tab `storage` events for localStorage.
|
|
137
157
|
if (event.storageArea !== window.localStorage)
|
|
138
158
|
return;
|
|
139
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
216
|
-
|
|
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
|
-
|
|
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
|
|
1
|
+
export * from "../ttl.js";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/features/ttl.d.ts
CHANGED
|
@@ -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
|
|
6
|
-
import { EventEmitter } from
|
|
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;
|
|
1
|
+
{"version":3,"file":"ttl.d.ts","sourceRoot":"","sources":["../../src/features/ttl.ts"],"names":[],"mappings":"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"}
|
package/dist/features/ttl.js
CHANGED
|
@@ -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) {
|