strata-storage 2.6.1 → 2.7.0
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 +88 -10
- 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/CookieAdapter.d.ts +2 -2
- 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 +14 -3
- package/dist/adapters/web/LocalStorageAdapter.d.ts +2 -2
- package/dist/adapters/web/MemoryAdapter.d.ts +2 -2
- 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 +3 -2
- 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 +41 -7
- package/dist/core/Strata.d.ts.map +1 -1
- package/dist/core/Strata.js +476 -118
- package/dist/features/compression/index.d.ts +1 -1
- package/dist/features/compression.d.ts +5 -1
- package/dist/features/compression.d.ts.map +1 -1
- 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/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 +1 -1
- package/dist/features/query.d.ts.map +1 -1
- package/dist/features/query.js +3 -1
- 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 +57 -9
- package/dist/features/ttl/index.d.ts +1 -1
- package/dist/features/ttl.d.ts +2 -2
- package/dist/firebase.d.ts +1 -1
- package/dist/firebase.d.ts.map +1 -1
- package/dist/firebase.js +34 -9
- 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 +16 -14
- 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
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from "../compression.js";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* Compression configuration
|
|
7
7
|
*/
|
|
8
8
|
export interface CompressionConfig {
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Compression algorithm. Only the bundled zero-dependency LZ codec is
|
|
11
|
+
* implemented (`'lz'`); the field is kept for forward compatibility.
|
|
12
|
+
*/
|
|
13
|
+
algorithm?: 'lz';
|
|
10
14
|
threshold?: number;
|
|
11
15
|
level?: number;
|
|
12
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compression.d.ts","sourceRoot":"","sources":["../../src/features/compression.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,IAAI,
|
|
1
|
+
{"version":3,"file":"compression.d.ts","sourceRoot":"","sources":["../../src/features/compression.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAA8B;gBAEhC,MAAM,GAAE,iBAAsB;IAQ1C;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,GAAG,OAAO,CAAC;IA8BhE;;OAEG;IACG,UAAU,CAAC,CAAC,GAAG,OAAO,EAAE,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAazE;;OAEG;IACH,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,cAAc;IAWvD;;OAEG;IACH,OAAO,CAAC,UAAU;IAmClB;;OAEG;IACH,OAAO,CAAC,YAAY;IA+BpB;;OAEG;IACH,OAAO,CAAC,cAAc;IA2BtB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA6BxB;;OAEG;IACH,mBAAmB,CAAC,cAAc,EAAE,cAAc,GAAG,MAAM;IAI3D;;OAEG;IACH,oBAAoB,CAAC,cAAc,EAAE,cAAc,GAAG,MAAM;CAO7D"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from
|
|
1
|
+
export * from "../encryption.js";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -10,6 +10,7 @@ export interface EncryptionConfig {
|
|
|
10
10
|
keyLength?: 128 | 192 | 256;
|
|
11
11
|
iterations?: number;
|
|
12
12
|
saltLength?: number;
|
|
13
|
+
keyDerivation?: 'PBKDF2';
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Encrypted data structure
|
|
@@ -20,16 +21,37 @@ export interface EncryptedData {
|
|
|
20
21
|
iv: string;
|
|
21
22
|
algorithm: string;
|
|
22
23
|
iterations: number;
|
|
24
|
+
/**
|
|
25
|
+
* Base64 HMAC-SHA256 tag over (iv || ciphertext), present only for AES-CBC.
|
|
26
|
+
*
|
|
27
|
+
* AES-CBC provides confidentiality but NOT authentication, so without a MAC a
|
|
28
|
+
* CBC ciphertext is malleable and vulnerable to padding-oracle / bit-flip
|
|
29
|
+
* attacks. We therefore use Encrypt-then-MAC: a separate HMAC key is derived
|
|
30
|
+
* from the same password (domain-separated salt) and the tag is verified
|
|
31
|
+
* before decryption. AES-GCM is authenticated by construction and does not
|
|
32
|
+
* carry this field.
|
|
33
|
+
*/
|
|
34
|
+
mac?: string;
|
|
23
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Default PBKDF2 iteration count. Raised to 600,000 to match current OWASP
|
|
38
|
+
* guidance for PBKDF2-HMAC-SHA256. The per-record `iterations` value is stored
|
|
39
|
+
* in {@link EncryptedData} and used on decrypt, so raising this default never
|
|
40
|
+
* breaks data written with an older, lower count.
|
|
41
|
+
*/
|
|
42
|
+
export declare const DEFAULT_PBKDF2_ITERATIONS = 600000;
|
|
24
43
|
/**
|
|
25
44
|
* Encryption manager using Web Crypto API
|
|
26
45
|
*/
|
|
27
46
|
export declare class EncryptionManager {
|
|
28
47
|
private config;
|
|
29
48
|
private keyCache;
|
|
49
|
+
private macKeyCache;
|
|
30
50
|
constructor(config?: EncryptionConfig);
|
|
31
51
|
/**
|
|
32
|
-
* Check if encryption is available
|
|
52
|
+
* Check if encryption is available. Uses the Web Crypto API via `globalThis`
|
|
53
|
+
* so it works in browsers, Web Workers, and modern Node.js / SSR runtimes
|
|
54
|
+
* (Node 20+ exposes `globalThis.crypto.subtle`) — not just the browser.
|
|
33
55
|
*/
|
|
34
56
|
isAvailable(): boolean;
|
|
35
57
|
/**
|
|
@@ -44,6 +66,32 @@ export declare class EncryptionManager {
|
|
|
44
66
|
* Derive encryption key from password
|
|
45
67
|
*/
|
|
46
68
|
private deriveKey;
|
|
69
|
+
/**
|
|
70
|
+
* Derive a separate HMAC-SHA256 key for Encrypt-then-MAC on AES-CBC.
|
|
71
|
+
*
|
|
72
|
+
* The HMAC key must be cryptographically independent of the AES key, so the
|
|
73
|
+
* PBKDF2 salt is domain-separated with a trailing marker byte. The MAC key is
|
|
74
|
+
* non-extractable and limited to sign/verify.
|
|
75
|
+
*/
|
|
76
|
+
private deriveMacKey;
|
|
77
|
+
/**
|
|
78
|
+
* Import a password string as PBKDF2 key material.
|
|
79
|
+
*/
|
|
80
|
+
private importPasswordKeyMaterial;
|
|
81
|
+
/**
|
|
82
|
+
* Build an opaque cache key by hashing the password together with the public
|
|
83
|
+
* derivation parameters. Returns a base64 SHA-256 digest so the raw password
|
|
84
|
+
* is never used as (or recoverable from) the Map key.
|
|
85
|
+
*/
|
|
86
|
+
private hashCacheKey;
|
|
87
|
+
/**
|
|
88
|
+
* Copy a (possibly offset) typed array into a standalone ArrayBuffer.
|
|
89
|
+
*/
|
|
90
|
+
private toArrayBuffer;
|
|
91
|
+
/**
|
|
92
|
+
* Concatenate two buffers into one ArrayBuffer.
|
|
93
|
+
*/
|
|
94
|
+
private concatBuffers;
|
|
47
95
|
/**
|
|
48
96
|
* Convert ArrayBuffer to base64
|
|
49
97
|
*/
|
|
@@ -57,7 +105,12 @@ export declare class EncryptionManager {
|
|
|
57
105
|
*/
|
|
58
106
|
clearCache(): void;
|
|
59
107
|
/**
|
|
60
|
-
* Generate a secure random password
|
|
108
|
+
* Generate a secure random password.
|
|
109
|
+
*
|
|
110
|
+
* Uses rejection sampling so every character in the alphabet is equally
|
|
111
|
+
* likely. Naive `randomByte % alphabetLength` is biased whenever 256 is not a
|
|
112
|
+
* multiple of the alphabet length (here 89), making some characters more
|
|
113
|
+
* probable and slightly reducing entropy.
|
|
61
114
|
*/
|
|
62
115
|
generatePassword(length?: number): string;
|
|
63
116
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../../src/features/encryption.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../../src/features/encryption.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,QAAQ,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,SAAS,CAAC;AAEhD;;GAEG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,WAAW,CAAqC;gBAE5C,MAAM,GAAE,gBAAqB;IAczC;;;;OAIG;IACH,WAAW,IAAI,OAAO;IAKtB;;OAEG;IACG,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAsDtE;;OAEG;IACG,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IA6DtF;;OAEG;YACW,SAAS;IAuCvB;;;;;;OAMG;YACW,YAAY;IAyC1B;;OAEG;YACW,yBAAyB;IASvC;;;;OAIG;YACW,YAAY;IAM1B;;OAEG;IACH,OAAO,CAAC,aAAa;IAIrB;;OAEG;IACH,OAAO,CAAC,aAAa;IAOrB;;OAEG;IACH,OAAO,CAAC,cAAc;IAStB;;OAEG;IACH,OAAO,CAAC,cAAc;IAStB;;OAEG;IACH,UAAU,IAAI,IAAI;IAKlB;;;;;;;OAOG;IACH,gBAAgB,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM;IAsB7C;;OAEG;IACG,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAU1C"}
|
|
@@ -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,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
|
@@ -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;
|
|
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;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;IA2B5E;;OAEG;IACH,OAAO,CAAC,cAAc;IAetB;;OAEG;IACH,OAAO,CAAC,iBAAiB;CAc1B;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,WAAW,CAE/C"}
|
package/dist/features/query.js
CHANGED
|
@@ -121,7 +121,9 @@ export class QueryEngine {
|
|
|
121
121
|
if (current === null || current === undefined) {
|
|
122
122
|
return undefined;
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
// Match own properties only — `in` would traverse the prototype chain and
|
|
125
|
+
// could resolve a path segment to an inherited member (e.g. `constructor`).
|
|
126
|
+
if (typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, part)) {
|
|
125
127
|
current = current[part];
|
|
126
128
|
}
|
|
127
129
|
else {
|
|
@@ -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;CA2C5C;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
|
|
@@ -215,7 +251,19 @@ export class SyncManager extends EventEmitter {
|
|
|
215
251
|
// Return the last value (most recent)
|
|
216
252
|
return values[values.length - 1];
|
|
217
253
|
case 'merge':
|
|
218
|
-
|
|
254
|
+
if (values.every(isStorageValue)) {
|
|
255
|
+
const wrappers = values;
|
|
256
|
+
const latest = wrappers[wrappers.length - 1];
|
|
257
|
+
const payloads = wrappers.map((entry) => entry.value);
|
|
258
|
+
if (payloads.every((payload) => typeof payload === 'object' && payload !== null && !Array.isArray(payload))) {
|
|
259
|
+
return {
|
|
260
|
+
...latest,
|
|
261
|
+
value: Object.assign({}, ...payloads),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return latest;
|
|
265
|
+
}
|
|
266
|
+
// Simple merge for plain user objects
|
|
219
267
|
if (values.every((v) => typeof v === 'object' && v !== null && !Array.isArray(v))) {
|
|
220
268
|
return Object.assign({}, ...values);
|
|
221
269
|
}
|
|
@@ -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
|
package/dist/firebase.d.ts
CHANGED
package/dist/firebase.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"firebase.d.ts","sourceRoot":"","sources":["../src/firebase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAI5C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;
|
|
1
|
+
{"version":3,"file":"firebase.d.ts","sourceRoot":"","sources":["../src/firebase.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAI5C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA+BD;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC,CAyOf;AAED;;GAEG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC,CAQ5D"}
|