strata-storage 2.6.1 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/AI-INTEGRATION-GUIDE.md +129 -11
  2. package/README.md +42 -64
  3. package/android/AGENTS.md +18 -3
  4. package/android/CLAUDE.md +36 -3
  5. package/android/build.gradle +5 -1
  6. package/android/src/main/java/com/strata/storage/EncryptedStorage.java +31 -18
  7. package/android/src/main/java/com/strata/storage/FilesystemStorage.java +62 -10
  8. package/android/src/main/java/com/strata/storage/SQLiteStorage.java +65 -13
  9. package/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +63 -51
  10. package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +80 -11
  11. package/dist/README.md +42 -64
  12. package/dist/adapters/capacitor/FilesystemAdapter.d.ts +2 -2
  13. package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
  14. package/dist/adapters/capacitor/FilesystemAdapter.js +6 -2
  15. package/dist/adapters/capacitor/PreferencesAdapter.d.ts +2 -2
  16. package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
  17. package/dist/adapters/capacitor/PreferencesAdapter.js +6 -2
  18. package/dist/adapters/capacitor/SecureAdapter.d.ts +3 -3
  19. package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
  20. package/dist/adapters/capacitor/SecureAdapter.js +6 -2
  21. package/dist/adapters/capacitor/SqliteAdapter.d.ts +24 -3
  22. package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
  23. package/dist/adapters/capacitor/SqliteAdapter.js +105 -23
  24. package/dist/adapters/capacitor/index.d.ts +4 -4
  25. package/dist/adapters/web/CacheAdapter.d.ts +2 -2
  26. package/dist/adapters/web/CacheAdapter.d.ts.map +1 -1
  27. package/dist/adapters/web/CacheAdapter.js +20 -5
  28. package/dist/adapters/web/CookieAdapter.d.ts +2 -2
  29. package/dist/adapters/web/CookieAdapter.d.ts.map +1 -1
  30. package/dist/adapters/web/CookieAdapter.js +27 -3
  31. package/dist/adapters/web/IndexedDBAdapter.d.ts +10 -3
  32. package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
  33. package/dist/adapters/web/IndexedDBAdapter.js +19 -4
  34. package/dist/adapters/web/LocalStorageAdapter.d.ts +2 -2
  35. package/dist/adapters/web/LocalStorageAdapter.d.ts.map +1 -1
  36. package/dist/adapters/web/LocalStorageAdapter.js +15 -1
  37. package/dist/adapters/web/MemoryAdapter.d.ts +9 -2
  38. package/dist/adapters/web/MemoryAdapter.d.ts.map +1 -1
  39. package/dist/adapters/web/MemoryAdapter.js +22 -5
  40. package/dist/adapters/web/SessionStorageAdapter.d.ts +10 -10
  41. package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
  42. package/dist/adapters/web/SessionStorageAdapter.js +17 -3
  43. package/dist/adapters/web/URLAdapter.d.ts +2 -2
  44. package/dist/adapters/web/index.d.ts +7 -7
  45. package/dist/capacitor.d.ts +6 -6
  46. package/dist/capacitor.d.ts.map +1 -1
  47. package/dist/capacitor.js +5 -4
  48. package/dist/core/AdapterRegistry.d.ts +1 -1
  49. package/dist/core/BaseAdapter.d.ts +21 -5
  50. package/dist/core/BaseAdapter.d.ts.map +1 -1
  51. package/dist/core/BaseAdapter.js +29 -1
  52. package/dist/core/Strata.d.ts +53 -7
  53. package/dist/core/Strata.d.ts.map +1 -1
  54. package/dist/core/Strata.js +581 -125
  55. package/dist/features/compression/index.d.ts +1 -1
  56. package/dist/features/compression.d.ts +28 -9
  57. package/dist/features/compression.d.ts.map +1 -1
  58. package/dist/features/compression.js +164 -77
  59. package/dist/features/encryption/index.d.ts +1 -1
  60. package/dist/features/encryption.d.ts +55 -2
  61. package/dist/features/encryption.d.ts.map +1 -1
  62. package/dist/features/encryption.js +139 -26
  63. package/dist/features/migration.d.ts +1 -1
  64. package/dist/features/migration.d.ts.map +1 -1
  65. package/dist/features/migration.js +7 -2
  66. package/dist/features/observer/index.d.ts +1 -1
  67. package/dist/features/observer.d.ts +1 -1
  68. package/dist/features/query/index.d.ts +1 -1
  69. package/dist/features/query.d.ts +2 -1
  70. package/dist/features/query.d.ts.map +1 -1
  71. package/dist/features/query.js +32 -7
  72. package/dist/features/sync/index.d.ts +1 -1
  73. package/dist/features/sync.d.ts +19 -3
  74. package/dist/features/sync.d.ts.map +1 -1
  75. package/dist/features/sync.js +75 -12
  76. package/dist/features/ttl/index.d.ts +1 -1
  77. package/dist/features/ttl.d.ts +2 -2
  78. package/dist/features/ttl.d.ts.map +1 -1
  79. package/dist/features/ttl.js +4 -2
  80. package/dist/firebase.d.ts +1 -1
  81. package/dist/firebase.d.ts.map +1 -1
  82. package/dist/firebase.js +72 -10
  83. package/dist/index.d.ts +24 -22
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +6 -1
  86. package/dist/integrations/angular/index.d.ts +2 -2
  87. package/dist/integrations/angular/index.d.ts.map +1 -1
  88. package/dist/integrations/angular/index.js +7 -8
  89. package/dist/integrations/react/index.d.ts +3 -3
  90. package/dist/integrations/react/index.d.ts.map +1 -1
  91. package/dist/integrations/react/index.js +16 -4
  92. package/dist/integrations/vue/index.d.ts +2 -2
  93. package/dist/integrations/vue/index.d.ts.map +1 -1
  94. package/dist/integrations/vue/index.js +5 -2
  95. package/dist/package.json +11 -2
  96. package/dist/plugin/definitions.d.ts +29 -3
  97. package/dist/plugin/definitions.d.ts.map +1 -1
  98. package/dist/plugin/index.d.ts +2 -2
  99. package/dist/plugin/index.d.ts.map +1 -1
  100. package/dist/plugin/index.js +3 -0
  101. package/dist/plugin/web.d.ts +11 -2
  102. package/dist/plugin/web.d.ts.map +1 -1
  103. package/dist/plugin/web.js +23 -4
  104. package/dist/types/index.d.ts +50 -20
  105. package/dist/types/index.d.ts.map +1 -1
  106. package/dist/types/index.js +1 -10
  107. package/dist/utils/index.d.ts +5 -0
  108. package/dist/utils/index.d.ts.map +1 -1
  109. package/dist/utils/index.js +7 -0
  110. package/ios/AGENTS.md +14 -3
  111. package/ios/CLAUDE.md +25 -3
  112. package/ios/Plugin/FilesystemStorage.swift +19 -0
  113. package/ios/Plugin/SQLiteStorage.swift +59 -9
  114. package/ios/Plugin/StrataStoragePlugin.m +1 -0
  115. package/ios/Plugin/StrataStoragePlugin.swift +87 -34
  116. package/ios/Plugin/UserDefaultsStorage.swift +47 -46
  117. package/package.json +27 -24
  118. package/scripts/build.js +46 -27
  119. package/scripts/cli.js +1 -1
  120. package/scripts/configure.js +2 -2
  121. package/scripts/postinstall.js +3 -2
  122. package/dist/android/AGENTS.md +0 -51
  123. package/dist/android/CLAUDE.md +0 -89
  124. package/dist/android/build.gradle +0 -58
  125. package/dist/android/proguard-rules.pro +0 -11
  126. package/dist/android/settings.gradle +0 -2
  127. package/dist/android/src/main/AndroidManifest.xml +0 -2
  128. package/dist/android/src/main/java/com/strata/storage/EncryptedStorage.java +0 -218
  129. package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +0 -287
  130. package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +0 -371
  131. package/dist/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +0 -209
  132. package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +0 -616
  133. package/dist/core/StorageStrategy.d.ts +0 -55
  134. package/dist/core/StorageStrategy.d.ts.map +0 -1
  135. package/dist/core/StorageStrategy.js +0 -200
  136. package/dist/ios/AGENTS.md +0 -48
  137. package/dist/ios/CLAUDE.md +0 -84
  138. package/dist/ios/Plugin/FilesystemStorage.swift +0 -218
  139. package/dist/ios/Plugin/KeychainStorage.swift +0 -233
  140. package/dist/ios/Plugin/SQLiteStorage.swift +0 -426
  141. package/dist/ios/Plugin/StrataStoragePlugin.m +0 -23
  142. package/dist/ios/Plugin/StrataStoragePlugin.swift +0 -411
  143. package/dist/ios/Plugin/UserDefaultsStorage.swift +0 -87
@@ -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
  }
@@ -37,22 +41,37 @@ export declare class CompressionManager {
37
41
  * Check if data is compressed
38
42
  */
39
43
  isCompressedData(data: unknown): data is CompressedData;
44
+ private static readonly FORMAT;
45
+ private static readonly LZW_MAX_CODE;
40
46
  /**
41
- * LZ-string compression implementation
47
+ * LZ compression over UTF-8 bytes.
48
+ *
49
+ * The alphabet is bytes (0–255), so the literal-vs-dictionary-code
50
+ * discriminator (`< 256`) on decompress is correct for ALL input — including
51
+ * CJK / emoji / symbols whose code points are ≥ 256. (Operating on raw UTF-16
52
+ * chars previously misclassified any such literal as a dictionary code and
53
+ * silently corrupted the stored value, or threw on read.)
42
54
  */
43
55
  private lzCompress;
44
56
  /**
45
- * LZ-string decompression implementation
57
+ * LZ decompression — inverse of {@link lzCompress}, reconstructing UTF-8 bytes
58
+ * then decoding them back to the original string.
46
59
  */
47
60
  private lzDecompress;
61
+ private static readonly B64_CHARS;
62
+ /** Base64-encode raw bytes (btoa when available, else a pure-JS fallback). */
63
+ private bytesToBase64;
64
+ /** Decode a base64 string back to raw bytes (atob when available, else pure-JS). */
65
+ private base64ToBytes;
48
66
  /**
49
- * Base64 encoding for compressed data
50
- */
51
- private encodeToBase64;
52
- /**
53
- * Base64 decoding for compressed data
67
+ * Legacy decoder for payloads written by <= 2.7.0 (algorithm 'lz'): the
68
+ * original UTF-16-char LZW over the raw string with a `decodeURIComponent`
69
+ * base64 layer. Kept read-only so existing compressed-at-rest values still
70
+ * decode after upgrading to the current byte-LZW codec. (That codec corrupted
71
+ * non-Latin1 input, so legacy payloads are Latin1 in practice.)
54
72
  */
55
- private decodeFromBase64;
73
+ private legacyLzDecompress;
74
+ private legacyDecodeFromBase64;
56
75
  /**
57
76
  * Get compression ratio
58
77
  */
@@ -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;IAkChE;;OAEG;IACG,UAAU,CAAC,CAAC,GAAG,OAAO,EAAE,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;IAmBzE;;OAEG;IACH,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,cAAc;IAcvD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAW;IAMzC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAU;IAE9C;;;;;;;;OAQG;IACH,OAAO,CAAC,UAAU;IAmClB;;;OAGG;IACH,OAAO,CAAC,YAAY;IA4CpB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CACoC;IAErE,8EAA8E;IAC9E,OAAO,CAAC,aAAa;IAyBrB,oFAAoF;IACpF,OAAO,CAAC,aAAa;IA2BrB;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;IA8B1B,OAAO,CAAC,sBAAsB;IAiB9B;;OAEG;IACH,mBAAmB,CAAC,cAAc,EAAE,cAAc,GAAG,MAAM;IAI3D;;OAEG;IACH,oBAAoB,CAAC,cAAc,EAAE,cAAc,GAAG,MAAM;CAO7D"}
@@ -34,8 +34,12 @@ export class CompressionManager {
34
34
  return data;
35
35
  }
36
36
  return {
37
+ // Tag the on-disk format so decompress() can pick the right decoder.
38
+ // Older releases wrote `algorithm: 'lz'` with a UTF-16-char codec that
39
+ // corrupted non-Latin1 data; the current byte-LZW format is tagged
40
+ // distinctly and old 'lz' payloads still read via the legacy path.
37
41
  data: compressed,
38
- algorithm: this.config.algorithm,
42
+ algorithm: CompressionManager.FORMAT,
39
43
  originalSize,
40
44
  compressedSize,
41
45
  };
@@ -52,7 +56,12 @@ export class CompressionManager {
52
56
  if (!this.isCompressedData(compressedData)) {
53
57
  return compressedData;
54
58
  }
55
- const decompressed = this.lzDecompress(compressedData.data);
59
+ // Route by stored format tag: the current byte-LZW format vs. the legacy
60
+ // UTF-16-char codec written by <= 2.7.0 (algorithm 'lz'), so existing
61
+ // compressed-at-rest values keep decoding after upgrade.
62
+ const decompressed = compressedData.algorithm === CompressionManager.FORMAT
63
+ ? this.lzDecompress(compressedData.data)
64
+ : this.legacyLzDecompress(compressedData.data);
56
65
  return JSON.parse(decompressed);
57
66
  }
58
67
  catch (error) {
@@ -70,120 +79,198 @@ export class CompressionManager {
70
79
  'originalSize' in data &&
71
80
  'compressedSize' in data);
72
81
  }
82
+ // Format tag stored in CompressedData.algorithm for the current byte-LZW
83
+ // codec. Payloads written by <= 2.7.0 are tagged 'lz' and decode via the
84
+ // legacy path; anything tagged FORMAT uses the current decoder.
85
+ static FORMAT = 'lz-u8';
86
+ // Freeze the LZW dictionary at 16 bits so every token serializes to exactly
87
+ // two bytes; both encoder and decoder stop growing the dictionary at the same
88
+ // point, staying in sync. 0xFFFF entries is ample for storage-sized values;
89
+ // beyond it, already-learned phrases still compress, just no new ones.
90
+ static LZW_MAX_CODE = 0xffff;
73
91
  /**
74
- * LZ-string compression implementation
92
+ * LZ compression over UTF-8 bytes.
93
+ *
94
+ * The alphabet is bytes (0–255), so the literal-vs-dictionary-code
95
+ * discriminator (`< 256`) on decompress is correct for ALL input — including
96
+ * CJK / emoji / symbols whose code points are ≥ 256. (Operating on raw UTF-16
97
+ * chars previously misclassified any such literal as a dictionary code and
98
+ * silently corrupted the stored value, or threw on read.)
75
99
  */
76
100
  lzCompress(uncompressed) {
77
- if (uncompressed == null)
101
+ const bytes = new TextEncoder().encode(uncompressed);
102
+ if (bytes.length === 0)
78
103
  return '';
79
- const context = new Map();
80
- const out = [];
81
- let currentChar;
82
- let phrase = uncompressed.charAt(0);
83
- let code = 256;
84
- for (let i = 1; i < uncompressed.length; i++) {
85
- currentChar = uncompressed.charAt(i);
86
- if (context.has(phrase + currentChar)) {
87
- phrase += currentChar;
104
+ const dictionary = new Map();
105
+ let nextCode = 256;
106
+ const tokens = [];
107
+ let phrase = String.fromCharCode(bytes[0]); // byte-string (each char 0–255)
108
+ for (let i = 1; i < bytes.length; i++) {
109
+ const current = String.fromCharCode(bytes[i]);
110
+ const combined = phrase + current;
111
+ if (dictionary.has(combined)) {
112
+ phrase = combined;
88
113
  }
89
114
  else {
90
- if (phrase.length > 1) {
91
- out.push(String.fromCharCode(context.get(phrase)));
92
- }
93
- else {
94
- out.push(phrase);
115
+ // `phrase` is always either a single byte (literal) or an existing
116
+ // dictionary entry, so the lookup below is always defined.
117
+ tokens.push(phrase.length === 1 ? phrase.charCodeAt(0) : dictionary.get(phrase));
118
+ if (nextCode <= CompressionManager.LZW_MAX_CODE) {
119
+ dictionary.set(combined, nextCode++);
95
120
  }
96
- context.set(phrase + currentChar, code);
97
- code++;
98
- phrase = currentChar;
121
+ phrase = current;
99
122
  }
100
123
  }
101
- if (phrase.length > 1) {
102
- out.push(String.fromCharCode(context.get(phrase)));
124
+ tokens.push(phrase.length === 1 ? phrase.charCodeAt(0) : dictionary.get(phrase));
125
+ // Serialize each token as a 16-bit big-endian value, then base64.
126
+ const out = new Uint8Array(tokens.length * 2);
127
+ for (let i = 0; i < tokens.length; i++) {
128
+ out[i * 2] = (tokens[i] >> 8) & 0xff;
129
+ out[i * 2 + 1] = tokens[i] & 0xff;
103
130
  }
104
- else {
105
- out.push(phrase);
106
- }
107
- return this.encodeToBase64(out.join(''));
131
+ return this.bytesToBase64(out);
108
132
  }
109
133
  /**
110
- * LZ-string decompression implementation
134
+ * LZ decompression — inverse of {@link lzCompress}, reconstructing UTF-8 bytes
135
+ * then decoding them back to the original string.
111
136
  */
112
137
  lzDecompress(compressed) {
113
- if (compressed == null)
138
+ if (!compressed)
114
139
  return '';
115
- if (compressed === '')
140
+ const inBytes = this.base64ToBytes(compressed);
141
+ const tokenCount = inBytes.length >> 1;
142
+ if (tokenCount === 0)
116
143
  return '';
117
- const decoded = this.decodeFromBase64(compressed);
118
144
  const dictionary = new Map();
119
- let currentChar = decoded.charAt(0);
120
- let oldPhrase = currentChar;
121
- const out = [currentChar];
122
- let code = 256;
123
- let phrase;
124
- for (let i = 1; i < decoded.length; i++) {
125
- const currentCode = decoded.charCodeAt(i);
126
- if (currentCode < 256) {
127
- phrase = decoded.charAt(i);
145
+ let nextCode = 256;
146
+ const readToken = (i) => (inBytes[i * 2] << 8) | inBytes[i * 2 + 1];
147
+ // The first token is always a literal byte (the dictionary starts empty).
148
+ let oldPhrase = String.fromCharCode(readToken(0));
149
+ const out = [oldPhrase];
150
+ for (let i = 1; i < tokenCount; i++) {
151
+ const code = readToken(i);
152
+ let phrase;
153
+ if (code < 256) {
154
+ phrase = String.fromCharCode(code);
155
+ }
156
+ else if (dictionary.has(code)) {
157
+ phrase = dictionary.get(code);
128
158
  }
129
159
  else {
130
- phrase = dictionary.get(currentCode) || oldPhrase + currentChar;
160
+ // Classic LZW edge: the code equals the entry we are about to define.
161
+ phrase = oldPhrase + oldPhrase.charAt(0);
131
162
  }
132
163
  out.push(phrase);
133
- currentChar = phrase.charAt(0);
134
- dictionary.set(code, oldPhrase + currentChar);
135
- code++;
164
+ if (nextCode <= CompressionManager.LZW_MAX_CODE) {
165
+ dictionary.set(nextCode++, oldPhrase + phrase.charAt(0));
166
+ }
136
167
  oldPhrase = phrase;
137
168
  }
138
- return out.join('');
169
+ // `out` holds byte-strings; rebuild the byte sequence and UTF-8 decode it.
170
+ const joined = out.join('');
171
+ const bytes = new Uint8Array(joined.length);
172
+ for (let i = 0; i < joined.length; i++) {
173
+ bytes[i] = joined.charCodeAt(i) & 0xff;
174
+ }
175
+ return new TextDecoder().decode(bytes);
139
176
  }
140
- /**
141
- * Base64 encoding for compressed data
142
- */
143
- encodeToBase64(input) {
177
+ static B64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
178
+ /** Base64-encode raw bytes (btoa when available, else a pure-JS fallback). */
179
+ bytesToBase64(bytes) {
144
180
  if (typeof btoa !== 'undefined') {
145
- // Browser environment
146
- return btoa(unescape(encodeURIComponent(input)));
181
+ let binary = '';
182
+ const chunk = 0x8000;
183
+ for (let i = 0; i < bytes.length; i += chunk) {
184
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
185
+ }
186
+ return btoa(binary);
147
187
  }
148
- // Pure JS implementation for Node.js or other environments
149
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
188
+ const chars = CompressionManager.B64_CHARS;
150
189
  let result = '';
151
- let i = 0;
152
- while (i < input.length) {
153
- const a = input.charCodeAt(i++);
154
- const b = i < input.length ? input.charCodeAt(i++) : 0;
155
- const c = i < input.length ? input.charCodeAt(i++) : 0;
190
+ for (let i = 0; i < bytes.length; i += 3) {
191
+ const a = bytes[i];
192
+ const b = i + 1 < bytes.length ? bytes[i + 1] : 0;
193
+ const c = i + 2 < bytes.length ? bytes[i + 2] : 0;
156
194
  const bitmap = (a << 16) | (b << 8) | c;
157
195
  result += chars.charAt((bitmap >> 18) & 63);
158
196
  result += chars.charAt((bitmap >> 12) & 63);
159
- result += i - 2 < input.length ? chars.charAt((bitmap >> 6) & 63) : '=';
160
- result += i - 1 < input.length ? chars.charAt(bitmap & 63) : '=';
197
+ result += i + 1 < bytes.length ? chars.charAt((bitmap >> 6) & 63) : '=';
198
+ result += i + 2 < bytes.length ? chars.charAt(bitmap & 63) : '=';
161
199
  }
162
200
  return result;
163
201
  }
202
+ /** Decode a base64 string back to raw bytes (atob when available, else pure-JS). */
203
+ base64ToBytes(input) {
204
+ if (typeof atob !== 'undefined') {
205
+ const binary = atob(input);
206
+ const bytes = new Uint8Array(binary.length);
207
+ for (let i = 0; i < binary.length; i++) {
208
+ bytes[i] = binary.charCodeAt(i) & 0xff;
209
+ }
210
+ return bytes;
211
+ }
212
+ const chars = CompressionManager.B64_CHARS;
213
+ const clean = input.replace(/[^A-Za-z0-9+/]/g, '');
214
+ const bytes = [];
215
+ for (let i = 0; i < clean.length; i += 4) {
216
+ const e1 = chars.indexOf(clean.charAt(i));
217
+ const e2 = chars.indexOf(clean.charAt(i + 1));
218
+ const e3 = chars.indexOf(clean.charAt(i + 2));
219
+ const e4 = chars.indexOf(clean.charAt(i + 3));
220
+ const bitmap = (e1 << 18) | (Math.max(e2, 0) << 12) | (Math.max(e3, 0) << 6) | Math.max(e4, 0);
221
+ bytes.push((bitmap >> 16) & 255);
222
+ if (e3 !== -1)
223
+ bytes.push((bitmap >> 8) & 255);
224
+ if (e4 !== -1)
225
+ bytes.push(bitmap & 255);
226
+ }
227
+ return new Uint8Array(bytes);
228
+ }
164
229
  /**
165
- * Base64 decoding for compressed data
230
+ * Legacy decoder for payloads written by <= 2.7.0 (algorithm 'lz'): the
231
+ * original UTF-16-char LZW over the raw string with a `decodeURIComponent`
232
+ * base64 layer. Kept read-only so existing compressed-at-rest values still
233
+ * decode after upgrading to the current byte-LZW codec. (That codec corrupted
234
+ * non-Latin1 input, so legacy payloads are Latin1 in practice.)
166
235
  */
167
- decodeFromBase64(input) {
168
- if (typeof atob !== 'undefined') {
169
- // Browser environment
170
- return decodeURIComponent(escape(atob(input)));
236
+ legacyLzDecompress(compressed) {
237
+ if (!compressed)
238
+ return '';
239
+ const decoded = typeof atob !== 'undefined'
240
+ ? decodeURIComponent(escape(atob(compressed)))
241
+ : this.legacyDecodeFromBase64(compressed);
242
+ const dictionary = new Map();
243
+ let currentChar = decoded.charAt(0);
244
+ let oldPhrase = currentChar;
245
+ const out = [currentChar];
246
+ let code = 256;
247
+ for (let i = 1; i < decoded.length; i++) {
248
+ const currentCode = decoded.charCodeAt(i);
249
+ const phrase = currentCode < 256
250
+ ? decoded.charAt(i)
251
+ : dictionary.get(currentCode) || oldPhrase + currentChar;
252
+ out.push(phrase);
253
+ currentChar = phrase.charAt(0);
254
+ dictionary.set(code, oldPhrase + currentChar);
255
+ code++;
256
+ oldPhrase = phrase;
171
257
  }
172
- // Pure JS implementation
173
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
258
+ return out.join('');
259
+ }
260
+ legacyDecodeFromBase64(input) {
261
+ const chars = `${CompressionManager.B64_CHARS}=`;
174
262
  let result = '';
175
- let i = 0;
176
- input = input.replace(/[^A-Za-z0-9+/]/g, '');
177
- while (i < input.length) {
178
- const encoded1 = chars.indexOf(input.charAt(i++));
179
- const encoded2 = chars.indexOf(input.charAt(i++));
180
- const encoded3 = chars.indexOf(input.charAt(i++));
181
- const encoded4 = chars.indexOf(input.charAt(i++));
182
- const bitmap = (encoded1 << 18) | (encoded2 << 12) | (encoded3 << 6) | encoded4;
263
+ const clean = input.replace(/[^A-Za-z0-9+/]/g, '');
264
+ for (let i = 0; i < clean.length; i += 4) {
265
+ const e1 = chars.indexOf(clean.charAt(i));
266
+ const e2 = chars.indexOf(clean.charAt(i + 1));
267
+ const e3 = chars.indexOf(clean.charAt(i + 2));
268
+ const e4 = chars.indexOf(clean.charAt(i + 3));
269
+ const bitmap = (e1 << 18) | (e2 << 12) | (e3 << 6) | e4;
183
270
  result += String.fromCharCode((bitmap >> 16) & 255);
184
- if (encoded3 !== 64)
271
+ if (e3 !== 64 && e3 !== -1)
185
272
  result += String.fromCharCode((bitmap >> 8) & 255);
186
- if (encoded4 !== 64)
273
+ if (e4 !== 64 && e4 !== -1)
187
274
  result += String.fromCharCode(bitmap & 255);
188
275
  }
189
276
  return result;
@@ -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"}