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.
Files changed (130) hide show
  1. package/AI-INTEGRATION-GUIDE.md +88 -10
  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/CookieAdapter.d.ts +2 -2
  27. package/dist/adapters/web/IndexedDBAdapter.d.ts +10 -3
  28. package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
  29. package/dist/adapters/web/IndexedDBAdapter.js +14 -3
  30. package/dist/adapters/web/LocalStorageAdapter.d.ts +2 -2
  31. package/dist/adapters/web/MemoryAdapter.d.ts +2 -2
  32. package/dist/adapters/web/SessionStorageAdapter.d.ts +10 -10
  33. package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
  34. package/dist/adapters/web/SessionStorageAdapter.js +3 -2
  35. package/dist/adapters/web/URLAdapter.d.ts +2 -2
  36. package/dist/adapters/web/index.d.ts +7 -7
  37. package/dist/capacitor.d.ts +6 -6
  38. package/dist/capacitor.d.ts.map +1 -1
  39. package/dist/capacitor.js +5 -4
  40. package/dist/core/AdapterRegistry.d.ts +1 -1
  41. package/dist/core/BaseAdapter.d.ts +21 -5
  42. package/dist/core/BaseAdapter.d.ts.map +1 -1
  43. package/dist/core/BaseAdapter.js +29 -1
  44. package/dist/core/Strata.d.ts +41 -7
  45. package/dist/core/Strata.d.ts.map +1 -1
  46. package/dist/core/Strata.js +476 -118
  47. package/dist/features/compression/index.d.ts +1 -1
  48. package/dist/features/compression.d.ts +5 -1
  49. package/dist/features/compression.d.ts.map +1 -1
  50. package/dist/features/encryption/index.d.ts +1 -1
  51. package/dist/features/encryption.d.ts +55 -2
  52. package/dist/features/encryption.d.ts.map +1 -1
  53. package/dist/features/encryption.js +139 -26
  54. package/dist/features/migration.d.ts +1 -1
  55. package/dist/features/observer/index.d.ts +1 -1
  56. package/dist/features/observer.d.ts +1 -1
  57. package/dist/features/query/index.d.ts +1 -1
  58. package/dist/features/query.d.ts +1 -1
  59. package/dist/features/query.d.ts.map +1 -1
  60. package/dist/features/query.js +3 -1
  61. package/dist/features/sync/index.d.ts +1 -1
  62. package/dist/features/sync.d.ts +19 -3
  63. package/dist/features/sync.d.ts.map +1 -1
  64. package/dist/features/sync.js +57 -9
  65. package/dist/features/ttl/index.d.ts +1 -1
  66. package/dist/features/ttl.d.ts +2 -2
  67. package/dist/firebase.d.ts +1 -1
  68. package/dist/firebase.d.ts.map +1 -1
  69. package/dist/firebase.js +34 -9
  70. package/dist/index.d.ts +24 -22
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +6 -1
  73. package/dist/integrations/angular/index.d.ts +2 -2
  74. package/dist/integrations/angular/index.d.ts.map +1 -1
  75. package/dist/integrations/angular/index.js +7 -8
  76. package/dist/integrations/react/index.d.ts +3 -3
  77. package/dist/integrations/react/index.d.ts.map +1 -1
  78. package/dist/integrations/react/index.js +16 -4
  79. package/dist/integrations/vue/index.d.ts +2 -2
  80. package/dist/integrations/vue/index.d.ts.map +1 -1
  81. package/dist/integrations/vue/index.js +5 -2
  82. package/dist/package.json +11 -2
  83. package/dist/plugin/definitions.d.ts +29 -3
  84. package/dist/plugin/definitions.d.ts.map +1 -1
  85. package/dist/plugin/index.d.ts +2 -2
  86. package/dist/plugin/index.d.ts.map +1 -1
  87. package/dist/plugin/index.js +3 -0
  88. package/dist/plugin/web.d.ts +11 -2
  89. package/dist/plugin/web.d.ts.map +1 -1
  90. package/dist/plugin/web.js +23 -4
  91. package/dist/types/index.d.ts +50 -20
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/dist/types/index.js +1 -10
  94. package/dist/utils/index.d.ts +5 -0
  95. package/dist/utils/index.d.ts.map +1 -1
  96. package/dist/utils/index.js +7 -0
  97. package/ios/AGENTS.md +14 -3
  98. package/ios/CLAUDE.md +25 -3
  99. package/ios/Plugin/FilesystemStorage.swift +19 -0
  100. package/ios/Plugin/SQLiteStorage.swift +59 -9
  101. package/ios/Plugin/StrataStoragePlugin.m +1 -0
  102. package/ios/Plugin/StrataStoragePlugin.swift +87 -34
  103. package/ios/Plugin/UserDefaultsStorage.swift +47 -46
  104. package/package.json +27 -24
  105. package/scripts/build.js +16 -14
  106. package/scripts/cli.js +1 -1
  107. package/scripts/configure.js +2 -2
  108. package/scripts/postinstall.js +3 -2
  109. package/dist/android/AGENTS.md +0 -51
  110. package/dist/android/CLAUDE.md +0 -89
  111. package/dist/android/build.gradle +0 -58
  112. package/dist/android/proguard-rules.pro +0 -11
  113. package/dist/android/settings.gradle +0 -2
  114. package/dist/android/src/main/AndroidManifest.xml +0 -2
  115. package/dist/android/src/main/java/com/strata/storage/EncryptedStorage.java +0 -218
  116. package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +0 -287
  117. package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +0 -371
  118. package/dist/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +0 -209
  119. package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +0 -616
  120. package/dist/core/StorageStrategy.d.ts +0 -55
  121. package/dist/core/StorageStrategy.d.ts.map +0 -1
  122. package/dist/core/StorageStrategy.js +0 -200
  123. package/dist/ios/AGENTS.md +0 -48
  124. package/dist/ios/CLAUDE.md +0 -84
  125. package/dist/ios/Plugin/FilesystemStorage.swift +0 -218
  126. package/dist/ios/Plugin/KeychainStorage.swift +0 -233
  127. package/dist/ios/Plugin/SQLiteStorage.swift +0 -426
  128. package/dist/ios/Plugin/StrataStoragePlugin.m +0 -23
  129. package/dist/ios/Plugin/StrataStoragePlugin.swift +0 -411
  130. package/dist/ios/Plugin/UserDefaultsStorage.swift +0 -87
@@ -1,2 +1,2 @@
1
- export * from '../compression';
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
- algorithm?: 'lz' | 'gzip';
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,GAAG,MAAM,CAAC;IAC1B,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
+ {"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 '../encryption';
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;CACrB;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;CACpB;AAED;;GAEG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,QAAQ,CAAqC;gBAEzC,MAAM,GAAE,gBAAqB;IASzC;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACG,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA8CtE;;OAEG;IACG,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAiCtF;;OAEG;YACW,SAAS;IA8CvB;;OAEG;IACH,OAAO,CAAC,cAAc;IAStB;;OAEG;IACH,OAAO,CAAC,cAAc;IAStB;;OAEG;IACH,UAAU,IAAI,IAAI;IAIlB;;OAEG;IACH,gBAAgB,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM;IAc7C;;OAEG;IACG,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAU1C"}
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 || 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,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
  */
@@ -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;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"}
@@ -121,7 +121,9 @@ export class QueryEngine {
121
121
  if (current === null || current === undefined) {
122
122
  return undefined;
123
123
  }
124
- if (typeof current === 'object' && part in current) {
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 '../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;CA2C5C;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
@@ -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
- // Simple merge for objects
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 '../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,4 +1,4 @@
1
- import type { Strata } from './core/Strata';
1
+ import type { Strata } from "./core/Strata.js";
2
2
  /**
3
3
  * Firebase sync configuration
4
4
  */
@@ -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;AAED;;;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"}
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"}