strata-storage 2.4.3 → 2.6.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 (114) hide show
  1. package/AI-INTEGRATION-GUIDE.md +115 -261
  2. package/README.md +426 -182
  3. package/android/AGENTS.md +51 -0
  4. package/android/CLAUDE.md +89 -0
  5. package/android/build.gradle +1 -1
  6. package/android/src/main/java/com/strata/storage/FilesystemStorage.java +287 -0
  7. package/android/src/main/java/com/strata/storage/SQLiteStorage.java +260 -203
  8. package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +357 -69
  9. package/dist/README.md +426 -182
  10. package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
  11. package/dist/adapters/capacitor/FilesystemAdapter.js +2 -1
  12. package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
  13. package/dist/adapters/capacitor/PreferencesAdapter.js +2 -1
  14. package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
  15. package/dist/adapters/capacitor/SecureAdapter.js +2 -1
  16. package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
  17. package/dist/adapters/capacitor/SqliteAdapter.js +2 -1
  18. package/dist/adapters/web/CacheAdapter.d.ts.map +1 -1
  19. package/dist/adapters/web/CacheAdapter.js +11 -3
  20. package/dist/adapters/web/CookieAdapter.d.ts +37 -1
  21. package/dist/adapters/web/CookieAdapter.d.ts.map +1 -1
  22. package/dist/adapters/web/CookieAdapter.js +89 -9
  23. package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
  24. package/dist/adapters/web/IndexedDBAdapter.js +10 -2
  25. package/dist/adapters/web/LocalStorageAdapter.d.ts +31 -0
  26. package/dist/adapters/web/LocalStorageAdapter.d.ts.map +1 -1
  27. package/dist/adapters/web/LocalStorageAdapter.js +92 -19
  28. package/dist/adapters/web/MemoryAdapter.d.ts +24 -0
  29. package/dist/adapters/web/MemoryAdapter.d.ts.map +1 -1
  30. package/dist/adapters/web/MemoryAdapter.js +69 -18
  31. package/dist/adapters/web/SessionStorageAdapter.d.ts +24 -0
  32. package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
  33. package/dist/adapters/web/SessionStorageAdapter.js +71 -9
  34. package/dist/adapters/web/URLAdapter.d.ts +59 -0
  35. package/dist/adapters/web/URLAdapter.d.ts.map +1 -0
  36. package/dist/adapters/web/URLAdapter.js +234 -0
  37. package/dist/adapters/web/index.d.ts +1 -0
  38. package/dist/adapters/web/index.d.ts.map +1 -1
  39. package/dist/adapters/web/index.js +1 -0
  40. package/dist/android/AGENTS.md +51 -0
  41. package/dist/android/CLAUDE.md +89 -0
  42. package/dist/android/build.gradle +1 -1
  43. package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +287 -0
  44. package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +260 -203
  45. package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +357 -69
  46. package/dist/capacitor.d.ts.map +1 -1
  47. package/dist/capacitor.js +2 -1
  48. package/dist/core/BaseAdapter.d.ts +8 -0
  49. package/dist/core/BaseAdapter.d.ts.map +1 -1
  50. package/dist/core/BaseAdapter.js +34 -14
  51. package/dist/core/Strata.d.ts +56 -2
  52. package/dist/core/Strata.d.ts.map +1 -1
  53. package/dist/core/Strata.js +501 -53
  54. package/dist/features/encryption.d.ts.map +1 -1
  55. package/dist/features/encryption.js +3 -2
  56. package/dist/features/integrity.d.ts +16 -0
  57. package/dist/features/integrity.d.ts.map +1 -0
  58. package/dist/features/integrity.js +28 -0
  59. package/dist/features/observer.d.ts.map +1 -1
  60. package/dist/features/observer.js +2 -1
  61. package/dist/features/query.d.ts +7 -1
  62. package/dist/features/query.d.ts.map +1 -1
  63. package/dist/features/query.js +9 -2
  64. package/dist/features/sync.d.ts.map +1 -1
  65. package/dist/features/sync.js +4 -3
  66. package/dist/index.d.ts +35 -2
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +55 -30
  69. package/dist/integrations/angular/index.d.ts +158 -0
  70. package/dist/integrations/angular/index.d.ts.map +1 -0
  71. package/dist/integrations/angular/index.js +395 -0
  72. package/dist/integrations/index.d.ts +15 -0
  73. package/dist/integrations/index.d.ts.map +1 -0
  74. package/dist/integrations/index.js +18 -0
  75. package/dist/integrations/react/index.d.ts +75 -0
  76. package/dist/integrations/react/index.d.ts.map +1 -0
  77. package/dist/integrations/react/index.js +191 -0
  78. package/dist/integrations/vue/index.d.ts +103 -0
  79. package/dist/integrations/vue/index.d.ts.map +1 -0
  80. package/dist/integrations/vue/index.js +274 -0
  81. package/dist/ios/AGENTS.md +48 -0
  82. package/dist/ios/CLAUDE.md +84 -0
  83. package/dist/ios/Plugin/FilesystemStorage.swift +218 -0
  84. package/dist/ios/Plugin/KeychainStorage.swift +139 -50
  85. package/dist/ios/Plugin/SQLiteStorage.swift +279 -147
  86. package/dist/ios/Plugin/StrataStoragePlugin.m +23 -0
  87. package/dist/ios/Plugin/StrataStoragePlugin.swift +272 -65
  88. package/dist/package.json +21 -5
  89. package/dist/plugin/index.d.ts.map +1 -1
  90. package/dist/plugin/index.js +2 -1
  91. package/dist/types/index.d.ts +58 -9
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/dist/types/index.js +0 -13
  94. package/dist/utils/errors.d.ts +7 -0
  95. package/dist/utils/errors.d.ts.map +1 -1
  96. package/dist/utils/errors.js +15 -3
  97. package/dist/utils/index.d.ts +63 -5
  98. package/dist/utils/index.d.ts.map +1 -1
  99. package/dist/utils/index.js +109 -16
  100. package/dist/utils/logger.d.ts +31 -0
  101. package/dist/utils/logger.d.ts.map +1 -0
  102. package/dist/utils/logger.js +63 -0
  103. package/ios/AGENTS.md +48 -0
  104. package/ios/CLAUDE.md +84 -0
  105. package/ios/Plugin/FilesystemStorage.swift +218 -0
  106. package/ios/Plugin/KeychainStorage.swift +139 -50
  107. package/ios/Plugin/SQLiteStorage.swift +279 -147
  108. package/ios/Plugin/StrataStoragePlugin.m +23 -0
  109. package/ios/Plugin/StrataStoragePlugin.swift +272 -65
  110. package/package.json +31 -20
  111. package/scripts/build.js +16 -5
  112. package/scripts/configure.js +2 -6
  113. package/scripts/postinstall.js +2 -2
  114. package/Readme.md +0 -271
@@ -3,93 +3,159 @@ import Capacitor
3
3
 
4
4
  /**
5
5
  * Main Capacitor plugin for Strata Storage
6
- * Coordinates between different storage types on iOS
6
+ * Coordinates between different storage types on iOS.
7
+ *
8
+ * NOTE: Method registration with the Capacitor bridge lives in
9
+ * StrataStoragePlugin.m (CAP_PLUGIN / CAP_PLUGIN_METHOD). Keep the two in
10
+ * sync when adding or renaming methods.
7
11
  */
8
12
  @objc(StrataStoragePlugin)
9
13
  public class StrataStoragePlugin: CAPPlugin {
10
14
  private let userDefaultsStorage = UserDefaultsStorage()
11
15
  private let keychainStorage = KeychainStorage()
12
- private let sqliteStorage = SQLiteStorage()
13
-
16
+ private let filesystemStorage = FilesystemStorage()
17
+
18
+ /// Storage types that have a real native backend on iOS.
19
+ private let supportedStorageTypes: Set<String> = ["preferences", "secure", "sqlite", "filesystem"]
20
+
21
+ // MARK: - SQLite multi-store helpers
22
+
23
+ /// Default database name (→ file `strata_storage.db`) and table.
24
+ private static let defaultDatabase = "strata_storage"
25
+ private static let defaultTable = "storage"
26
+
27
+ /// Sanitizes a database name into a safe `.db` filename. Strips everything
28
+ /// outside `[A-Za-z0-9_]` (which also blocks path separators / traversal),
29
+ /// then appends `.db`. Empty input falls back to the default. The filename
30
+ /// CANNOT be a bound parameter, so it must be sanitized before use.
31
+ private func sqliteFileName(from database: String?) -> String {
32
+ let raw = database ?? StrataStoragePlugin.defaultDatabase
33
+ let cleaned = String(raw.unicodeScalars.filter { CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_").contains($0) })
34
+ let safe = cleaned.isEmpty ? StrataStoragePlugin.defaultDatabase : cleaned
35
+ return "\(safe).db"
36
+ }
37
+
38
+ /// Sanitizes a table name to a safe SQL identifier matching `^[A-Za-z0-9_]+$`.
39
+ /// Disallowed characters are removed; empty input falls back to the default.
40
+ /// The table name is interpolated into SQL (it cannot be bound), so this is
41
+ /// the single point that guarantees injection-safety for the identifier.
42
+ private func sqliteTableName(from table: String?) -> String {
43
+ let raw = table ?? StrataStoragePlugin.defaultTable
44
+ let cleaned = String(raw.unicodeScalars.filter { CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_").contains($0) })
45
+ return cleaned.isEmpty ? StrataStoragePlugin.defaultTable : cleaned
46
+ }
47
+
48
+ /// Returns the cached SQLite store for the call's `database` option.
49
+ private func sqliteStore(for call: CAPPluginCall) -> SQLiteStorage {
50
+ return SQLiteStorageManager.shared.store(forFile: sqliteFileName(from: call.getString("database")))
51
+ }
52
+
14
53
  /**
15
- * Check if storage is available
54
+ * Check if a specific storage type is available.
55
+ * Matches the JS contract: resolves `{ available: boolean }`.
16
56
  */
17
57
  @objc func isAvailable(_ call: CAPPluginCall) {
58
+ let storage = call.getString("storage") ?? "preferences"
18
59
  call.resolve([
19
- "available": true,
20
- "platform": "ios",
21
- "adapters": [
22
- "preferences": true,
23
- "secure": true,
24
- "sqlite": true,
25
- "filesystem": true
26
- ]
60
+ "available": supportedStorageTypes.contains(storage)
27
61
  ])
28
62
  }
29
-
63
+
30
64
  /**
31
- * Get value from storage
65
+ * Get value from storage.
66
+ * For sqlite/filesystem the resolved `value` is the full StorageValue
67
+ * wrapper object (or NSNull on a miss).
32
68
  */
33
69
  @objc func get(_ call: CAPPluginCall) {
34
70
  guard let key = call.getString("key") else {
35
71
  call.reject("Key is required")
36
72
  return
37
73
  }
38
-
74
+
39
75
  let storage = call.getString("storage") ?? "preferences"
40
-
76
+
41
77
  do {
42
78
  let value: Any?
43
-
79
+
44
80
  switch storage {
45
81
  case "secure":
46
82
  value = try keychainStorage.get(key: key)
47
83
  case "sqlite":
48
- value = try sqliteStorage.get(key: key)
84
+ value = sqliteStore(for: call).get(table: sqliteTableName(from: call.getString("table")), key: key)
85
+ case "filesystem":
86
+ value = filesystemStorage.get(key: key)
49
87
  case "preferences":
50
88
  fallthrough
51
89
  default:
52
90
  value = userDefaultsStorage.get(key: key)
53
91
  }
54
-
92
+
55
93
  call.resolve([
56
94
  "value": value ?? NSNull()
57
95
  ])
58
96
  } catch {
59
- call.reject("Failed to get value", error.localizedDescription)
97
+ call.reject("Failed to get value", nil, error)
60
98
  }
61
99
  }
62
-
100
+
63
101
  /**
64
- * Set value in storage
102
+ * Set value in storage.
103
+ * For sqlite/filesystem the `value` option is the full StorageValue
104
+ * wrapper object and is stored verbatim (JSON) for a perfect round-trip.
65
105
  */
66
106
  @objc func set(_ call: CAPPluginCall) {
67
107
  guard let key = call.getString("key") else {
68
108
  call.reject("Key is required")
69
109
  return
70
110
  }
71
-
72
- let value = call.getValue("value") ?? NSNull()
111
+
73
112
  let storage = call.getString("storage") ?? "preferences"
74
-
113
+
75
114
  do {
76
115
  switch storage {
77
116
  case "secure":
78
- try keychainStorage.set(key: key, value: value)
117
+ let value = call.getValue("value") ?? NSNull()
118
+ _ = try keychainStorage.set(key: key, value: value)
79
119
  case "sqlite":
80
- try sqliteStorage.set(key: key, value: value)
120
+ // getObject returns JSObject ([String: JSValue]); upcast each
121
+ // value to Any (the canonical Capacitor pattern) so the bridged
122
+ // Foundation values are JSONSerialization-compatible.
123
+ guard let jsObject = call.getObject("value") else {
124
+ call.reject("SQLite value must be a StorageValue object")
125
+ return
126
+ }
127
+ let ok = try sqliteStore(for: call).set(
128
+ table: sqliteTableName(from: call.getString("table")),
129
+ key: key,
130
+ wrapper: jsObject as [String: Any]
131
+ )
132
+ if !ok {
133
+ call.reject("Failed to write value to SQLite")
134
+ return
135
+ }
136
+ case "filesystem":
137
+ guard let jsObject = call.getObject("value") else {
138
+ call.reject("Filesystem value must be a StorageValue object")
139
+ return
140
+ }
141
+ let ok = try filesystemStorage.set(key: key, wrapper: jsObject as [String: Any])
142
+ if !ok {
143
+ call.reject("Failed to write value to filesystem")
144
+ return
145
+ }
81
146
  case "preferences":
82
147
  fallthrough
83
148
  default:
149
+ let value = call.getValue("value") ?? NSNull()
84
150
  userDefaultsStorage.set(key: key, value: value)
85
151
  }
86
-
152
+
87
153
  call.resolve()
88
154
  } catch {
89
- call.reject("Failed to set value", error.localizedDescription)
155
+ call.reject("Failed to set value", nil, error)
90
156
  }
91
157
  }
92
-
158
+
93
159
  /**
94
160
  * Remove value from storage
95
161
  */
@@ -98,107 +164,248 @@ public class StrataStoragePlugin: CAPPlugin {
98
164
  call.reject("Key is required")
99
165
  return
100
166
  }
101
-
167
+
102
168
  let storage = call.getString("storage") ?? "preferences"
103
-
169
+
104
170
  do {
105
171
  switch storage {
106
172
  case "secure":
107
- try keychainStorage.remove(key: key)
173
+ _ = try keychainStorage.remove(key: key)
108
174
  case "sqlite":
109
- try sqliteStorage.remove(key: key)
175
+ _ = sqliteStore(for: call).remove(table: sqliteTableName(from: call.getString("table")), key: key)
176
+ case "filesystem":
177
+ _ = filesystemStorage.remove(key: key)
110
178
  case "preferences":
111
179
  fallthrough
112
180
  default:
113
181
  userDefaultsStorage.remove(key: key)
114
182
  }
115
-
183
+
116
184
  call.resolve()
117
185
  } catch {
118
- call.reject("Failed to remove value", error.localizedDescription)
186
+ call.reject("Failed to remove value", nil, error)
119
187
  }
120
188
  }
121
-
189
+
122
190
  /**
123
- * Clear storage
191
+ * Clear storage.
192
+ * The JS contract sends an optional `pattern`. The native backends accept
193
+ * a prefix; we use `pattern` as that prefix (prefix/contains matching).
124
194
  */
125
195
  @objc func clear(_ call: CAPPluginCall) {
126
196
  let storage = call.getString("storage") ?? "preferences"
127
- let prefix = call.getString("prefix")
128
-
197
+ // Accept both `pattern` (JS contract) and legacy `prefix`.
198
+ let prefix = call.getString("pattern") ?? call.getString("prefix")
199
+
129
200
  do {
130
201
  switch storage {
131
202
  case "secure":
132
- try keychainStorage.clear(prefix: prefix)
203
+ _ = try keychainStorage.clear(prefix: prefix)
133
204
  case "sqlite":
134
- try sqliteStorage.clear(prefix: prefix)
205
+ _ = sqliteStore(for: call).clear(table: sqliteTableName(from: call.getString("table")), prefix: prefix)
206
+ case "filesystem":
207
+ _ = filesystemStorage.clear(prefix: prefix)
135
208
  case "preferences":
136
209
  fallthrough
137
210
  default:
138
211
  userDefaultsStorage.clear(prefix: prefix)
139
212
  }
140
-
213
+
141
214
  call.resolve()
142
215
  } catch {
143
- call.reject("Failed to clear storage", error.localizedDescription)
216
+ call.reject("Failed to clear storage", nil, error)
144
217
  }
145
218
  }
146
-
219
+
147
220
  /**
148
221
  * Get all keys from storage
149
222
  */
150
223
  @objc func keys(_ call: CAPPluginCall) {
151
224
  let storage = call.getString("storage") ?? "preferences"
152
225
  let pattern = call.getString("pattern")
153
-
226
+
154
227
  do {
155
228
  let keys: [String]
156
-
229
+
157
230
  switch storage {
158
231
  case "secure":
159
232
  keys = try keychainStorage.keys(pattern: pattern)
160
233
  case "sqlite":
161
- keys = try sqliteStorage.keys(pattern: pattern)
234
+ keys = sqliteStore(for: call).keys(table: sqliteTableName(from: call.getString("table")), pattern: pattern)
235
+ case "filesystem":
236
+ keys = filesystemStorage.keys(pattern: pattern)
162
237
  case "preferences":
163
238
  fallthrough
164
239
  default:
165
240
  keys = userDefaultsStorage.keys(pattern: pattern)
166
241
  }
167
-
242
+
168
243
  call.resolve([
169
244
  "keys": keys
170
245
  ])
171
246
  } catch {
172
- call.reject("Failed to get keys", error.localizedDescription)
247
+ call.reject("Failed to get keys", nil, error)
173
248
  }
174
249
  }
175
-
250
+
176
251
  /**
177
- * Get storage size information
252
+ * Get storage size information.
253
+ * When `detailed` is true, resolves `{ total, count, detailed: { keys,
254
+ * values, metadata } }`; otherwise `{ total, count }`.
178
255
  */
179
256
  @objc func size(_ call: CAPPluginCall) {
180
257
  let storage = call.getString("storage") ?? "preferences"
181
-
258
+ let detailed = call.getBool("detailed") ?? false
259
+
182
260
  do {
183
- let sizeInfo: (total: Int, count: Int)
184
-
261
+ // segments carries total/count/keys/values/metadata in bytes.
262
+ let segments: [String: Int]
263
+
185
264
  switch storage {
186
265
  case "secure":
187
- sizeInfo = try keychainStorage.size()
266
+ let info = try keychainStorage.size()
267
+ segments = ["total": info.total, "count": info.count, "keys": 0, "values": info.total, "metadata": 0]
188
268
  case "sqlite":
189
- sizeInfo = try sqliteStorage.size()
269
+ segments = try sqliteStore(for: call).size(table: sqliteTableName(from: call.getString("table")))
270
+ case "filesystem":
271
+ segments = filesystemStorage.size()
190
272
  case "preferences":
191
273
  fallthrough
192
274
  default:
193
- sizeInfo = userDefaultsStorage.size()
275
+ let info = userDefaultsStorage.size()
276
+ segments = ["total": info.total, "count": info.count, "keys": 0, "values": info.total, "metadata": 0]
194
277
  }
195
-
196
- call.resolve([
197
- "total": sizeInfo.total,
198
- "count": sizeInfo.count
199
- ])
278
+
279
+ var result: [String: Any] = [
280
+ "total": segments["total"] ?? 0,
281
+ "count": segments["count"] ?? 0
282
+ ]
283
+ if detailed {
284
+ result["detailed"] = [
285
+ "keys": segments["keys"] ?? 0,
286
+ "values": segments["values"] ?? 0,
287
+ "metadata": segments["metadata"] ?? 0
288
+ ]
289
+ }
290
+ call.resolve(result)
291
+ } catch {
292
+ call.reject("Failed to get size", nil, error)
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Query SQLite-backed storage.
298
+ * Matches the optional `query` method in the JS contract:
299
+ * resolves `{ results: [{ key }] }` (JS re-fetches each value via get).
300
+ */
301
+ @objc func query(_ call: CAPPluginCall) {
302
+ let storage = call.getString("storage") ?? "sqlite"
303
+
304
+ guard storage == "sqlite" else {
305
+ call.reject("Query is only supported for the 'sqlite' storage type")
306
+ return
307
+ }
308
+
309
+ let condition = call.getObject("condition") ?? [:]
310
+ let results = sqliteStore(for: call).query(
311
+ table: sqliteTableName(from: call.getString("table")),
312
+ condition: condition
313
+ )
314
+ call.resolve([
315
+ "results": results
316
+ ])
317
+ }
318
+
319
+ /**
320
+ * iOS-specific: read a UserDefaults value.
321
+ * Resolves `{ value: unknown }`.
322
+ */
323
+ @objc func getUserDefaults(_ call: CAPPluginCall) {
324
+ guard let key = call.getString("key") else {
325
+ call.reject("Key is required")
326
+ return
327
+ }
328
+ let suiteName = call.getString("suiteName")
329
+ let store = suiteName != nil ? UserDefaultsStorage(suiteName: suiteName) : userDefaultsStorage
330
+ call.resolve([
331
+ "value": store.get(key: key) ?? NSNull()
332
+ ])
333
+ }
334
+
335
+ /**
336
+ * iOS-specific: write a UserDefaults value.
337
+ */
338
+ @objc func setUserDefaults(_ call: CAPPluginCall) {
339
+ guard let key = call.getString("key") else {
340
+ call.reject("Key is required")
341
+ return
342
+ }
343
+ let value = call.getValue("value") ?? NSNull()
344
+ let suiteName = call.getString("suiteName")
345
+ let store = suiteName != nil ? UserDefaultsStorage(suiteName: suiteName) : userDefaultsStorage
346
+ _ = store.set(key: key, value: value)
347
+ call.resolve()
348
+ }
349
+
350
+ /**
351
+ * iOS-specific: read a Keychain value. Resolves `{ value: string | null }`.
352
+ */
353
+ @objc func getKeychain(_ call: CAPPluginCall) {
354
+ guard let key = call.getString("key") else {
355
+ call.reject("Key is required")
356
+ return
357
+ }
358
+ let service = call.getString("service")
359
+ let accessGroup = call.getString("accessGroup")
360
+ let store = (service != nil || accessGroup != nil)
361
+ ? KeychainStorage(service: service, accessGroup: accessGroup)
362
+ : keychainStorage
363
+
364
+ do {
365
+ let value = try store.get(key: key)
366
+ // The JS contract types this as `string | null`. Serialize
367
+ // non-string values to a JSON string so the bridge stays typed.
368
+ if let value = value {
369
+ if let str = value as? String {
370
+ call.resolve(["value": str])
371
+ } else if let data = try? JSONSerialization.data(withJSONObject: value, options: []),
372
+ let str = String(data: data, encoding: .utf8) {
373
+ call.resolve(["value": str])
374
+ } else {
375
+ call.resolve(["value": NSNull()])
376
+ }
377
+ } else {
378
+ call.resolve(["value": NSNull()])
379
+ }
380
+ } catch {
381
+ call.reject("Failed to read keychain item", nil, error)
382
+ }
383
+ }
384
+
385
+ /**
386
+ * iOS-specific: write a Keychain value, honoring the `accessible` option.
387
+ */
388
+ @objc func setKeychain(_ call: CAPPluginCall) {
389
+ guard let key = call.getString("key") else {
390
+ call.reject("Key is required")
391
+ return
392
+ }
393
+ guard let value = call.getString("value") else {
394
+ call.reject("Value is required and must be a string")
395
+ return
396
+ }
397
+ let service = call.getString("service")
398
+ let accessGroup = call.getString("accessGroup")
399
+ let accessible = call.getString("accessible")
400
+ let store = (service != nil || accessGroup != nil)
401
+ ? KeychainStorage(service: service, accessGroup: accessGroup)
402
+ : keychainStorage
403
+
404
+ do {
405
+ _ = try store.set(key: key, value: value, accessible: accessible)
406
+ call.resolve()
200
407
  } catch {
201
- call.reject("Failed to get size", error.localizedDescription)
408
+ call.reject("Failed to write keychain item", nil, error)
202
409
  }
203
410
  }
204
- }
411
+ }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strata-storage",
3
- "version": "2.4.3",
3
+ "version": "2.6.0",
4
4
  "description": "Zero-dependency universal storage plugin providing a unified API for all storage operations across web, Android, and iOS platforms",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -17,6 +17,18 @@
17
17
  "./firebase": {
18
18
  "types": "./firebase.d.ts",
19
19
  "default": "./firebase.js"
20
+ },
21
+ "./react": {
22
+ "types": "./integrations/react/index.d.ts",
23
+ "default": "./integrations/react/index.js"
24
+ },
25
+ "./vue": {
26
+ "types": "./integrations/vue/index.d.ts",
27
+ "default": "./integrations/vue/index.js"
28
+ },
29
+ "./angular": {
30
+ "types": "./integrations/angular/index.d.ts",
31
+ "default": "./integrations/angular/index.js"
20
32
  }
21
33
  },
22
34
  "author": "Ahsan Mahmood",
@@ -45,11 +57,18 @@
45
57
  ],
46
58
  "peerDependencies": {
47
59
  "@angular/core": ">=21.0.6",
60
+ "@angular/forms": ">=21.0.6",
48
61
  "@capacitor/core": ">=8.0.0",
49
62
  "react": ">=19.2.3",
50
63
  "vue": ">=3.5.26"
51
64
  },
52
65
  "peerDependenciesMeta": {
66
+ "@angular/core": {
67
+ "optional": true
68
+ },
69
+ "@angular/forms": {
70
+ "optional": true
71
+ },
53
72
  "@capacitor/core": {
54
73
  "optional": true
55
74
  },
@@ -58,9 +77,6 @@
58
77
  },
59
78
  "vue": {
60
79
  "optional": true
61
- },
62
- "@angular/core": {
63
- "optional": true
64
80
  }
65
81
  },
66
82
  "capacitor": {
@@ -74,6 +90,6 @@
74
90
  },
75
91
  "sideEffects": false,
76
92
  "engines": {
77
- "node": ">=18.0.0"
93
+ "node": ">=24.13.0"
78
94
  }
79
95
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,mBAAmB,EASpB,MAAM,eAAe,CAAC;AA8IvB,eAAO,MAAM,aAAa,EAAE,mBAAmD,CAAC;AAEhF,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,mBAAmB,EASpB,MAAM,eAAe,CAAC;AA+IvB,eAAO,MAAM,aAAa,EAAE,mBAAmD,CAAC;AAEhF,cAAc,eAAe,CAAC"}
@@ -4,6 +4,7 @@
4
4
  * This is now optional and only loaded when Capacitor adapters are used
5
5
  */
6
6
  import { NotSupportedError } from "../utils/errors.js";
7
+ import { logger } from "../utils/logger.js";
7
8
  // Mock implementation for when Capacitor is not available
8
9
  const mockPlugin = {
9
10
  isAvailable: async () => ({ available: false }),
@@ -56,7 +57,7 @@ class LazyStrataStoragePlugin {
56
57
  });
57
58
  }
58
59
  catch (error) {
59
- console.warn('Failed to register StrataStorage plugin:', error);
60
+ logger.warn('Failed to register StrataStorage plugin:', error);
60
61
  this.plugin = mockPlugin;
61
62
  }
62
63
  }
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Supported storage types across all platforms
7
7
  */
8
- export type StorageType = 'memory' | 'localStorage' | 'sessionStorage' | 'indexedDB' | 'cookies' | 'cache' | 'preferences' | 'sqlite' | 'filesystem' | 'secure';
8
+ export type StorageType = 'memory' | 'localStorage' | 'sessionStorage' | 'indexedDB' | 'cookies' | 'cache' | 'url' | 'preferences' | 'sqlite' | 'filesystem' | 'secure';
9
9
  /**
10
10
  * Platform types
11
11
  */
@@ -75,6 +75,20 @@ export interface StorageOptions {
75
75
  * Metadata to attach
76
76
  */
77
77
  metadata?: Record<string, unknown>;
78
+ /**
79
+ * Compute + store an integrity checksum for this write and verify it on read.
80
+ */
81
+ verify?: boolean;
82
+ /**
83
+ * Durable write — read the value back and verify it after writing, retrying
84
+ * on mismatch. Guards against silent partial writes.
85
+ */
86
+ durable?: boolean;
87
+ /**
88
+ * On a checksum mismatch during read, return null instead of throwing
89
+ * IntegrityError (after mirror read-repair has been attempted).
90
+ */
91
+ ignoreCorruption?: boolean;
78
92
  }
79
93
  /**
80
94
  * Storage value with metadata
@@ -112,6 +126,11 @@ export interface StorageValue<T = unknown> {
112
126
  * Whether value is compressed
113
127
  */
114
128
  compressed?: boolean;
129
+ /**
130
+ * Integrity checksum (FNV-1a) over the stored value, present when integrity
131
+ * is enabled. Verified on read to detect corruption.
132
+ */
133
+ checksum?: string;
115
134
  }
116
135
  /**
117
136
  * Storage change event
@@ -390,6 +409,32 @@ export interface StrataConfig {
390
409
  */
391
410
  onExpire?: (keys: string[]) => void;
392
411
  };
412
+ /**
413
+ * Data integrity — compute + verify an FNV-1a checksum on every value so
414
+ * corruption is detected on read. Off by default.
415
+ */
416
+ integrity?: boolean;
417
+ /**
418
+ * Durable writes — read back and verify every write, retrying on mismatch.
419
+ * Off by default (adds one read per write).
420
+ */
421
+ durableWrites?: boolean;
422
+ /**
423
+ * Mirror every write/remove to these backup storage types. On a primary read
424
+ * miss or corruption, the value is recovered from a mirror (read-repair).
425
+ */
426
+ mirror?: StorageType[];
427
+ /**
428
+ * Periodic automatic snapshot of all data to a durable storage type.
429
+ */
430
+ autoBackup?: {
431
+ /** Interval in milliseconds between snapshots. */
432
+ interval: number;
433
+ /** Storage type to write the snapshot to. */
434
+ storage: StorageType;
435
+ /** Key to store the snapshot under (default `__strata_backup__`). */
436
+ key?: string;
437
+ };
393
438
  /**
394
439
  * Debug configuration
395
440
  */
@@ -460,6 +505,18 @@ export interface StorageAdapter {
460
505
  * Get all keys
461
506
  */
462
507
  keys(pattern?: string | RegExp): Promise<string[]>;
508
+ /** Synchronous get — sync-capable adapters only. */
509
+ getSync?<T = unknown>(key: string): StorageValue<T> | null;
510
+ /** Synchronous set — sync-capable adapters only. */
511
+ setSync?<T = unknown>(key: string, value: StorageValue<T>): void;
512
+ /** Synchronous remove — sync-capable adapters only. */
513
+ removeSync?(key: string): void;
514
+ /** Synchronous existence check — sync-capable adapters only. */
515
+ hasSync?(key: string): boolean;
516
+ /** Synchronous keys — sync-capable adapters only. */
517
+ keysSync?(pattern?: string | RegExp): string[];
518
+ /** Synchronous clear — sync-capable adapters only. */
519
+ clearSync?(options?: ClearOptions): void;
463
520
  /**
464
521
  * Get storage size
465
522
  */
@@ -695,14 +752,6 @@ export interface StorageEvent {
695
752
  */
696
753
  timestamp: number;
697
754
  }
698
- /**
699
- * Storage error class
700
- */
701
- export declare class StorageError extends Error {
702
- code?: string | undefined;
703
- details?: unknown | undefined;
704
- constructor(message: string, code?: string | undefined, details?: unknown | undefined);
705
- }
706
755
  /**
707
756
  * Storage metadata
708
757
  */