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/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": "dist/index.js",
@@ -13,9 +13,7 @@
13
13
  "StrataStorage.podspec",
14
14
  "AI-INTEGRATION-GUIDE.md"
15
15
  ],
16
- "bin": {
17
- "strata-storage": "./scripts/cli.js"
18
- },
16
+ "bin": "./scripts/cli.js",
19
17
  "exports": {
20
18
  ".": {
21
19
  "types": "./dist/index.d.ts",
@@ -82,11 +80,18 @@
82
80
  "homepage": "https://github.com/aoneahsan/strata-storage#readme",
83
81
  "peerDependencies": {
84
82
  "@angular/core": ">=21.0.6",
83
+ "@angular/forms": ">=21.0.6",
85
84
  "@capacitor/core": ">=8.0.0",
86
85
  "react": ">=19.2.3",
87
86
  "vue": ">=3.5.26"
88
87
  },
89
88
  "peerDependenciesMeta": {
89
+ "@angular/core": {
90
+ "optional": true
91
+ },
92
+ "@angular/forms": {
93
+ "optional": true
94
+ },
90
95
  "@capacitor/core": {
91
96
  "optional": true
92
97
  },
@@ -95,28 +100,33 @@
95
100
  },
96
101
  "vue": {
97
102
  "optional": true
98
- },
99
- "@angular/core": {
100
- "optional": true
101
103
  }
102
104
  },
103
105
  "devDependencies": {
104
- "@types/node": "^25.0.9",
105
- "@types/react": "^19.2.9",
106
- "@typescript-eslint/eslint-plugin": "^8.53.1",
107
- "@typescript-eslint/parser": "^8.53.1",
108
- "@vitest/coverage-v8": "^4.0.17",
109
- "eslint": "^9.39.2",
106
+ "@angular/common": "^21.2.14",
107
+ "@angular/core": "^21.2.14",
108
+ "@angular/forms": "^21.2.14",
109
+ "@capacitor/core": "^8.3.4",
110
+ "@types/node": "^25.9.1",
111
+ "@types/react": "^19.2.15",
112
+ "@typescript-eslint/eslint-plugin": "^8.60.0",
113
+ "@typescript-eslint/parser": "^8.60.0",
114
+ "@vitest/coverage-v8": "^4.1.7",
115
+ "eslint": "^10.4.0",
110
116
  "eslint-config-prettier": "^10.1.8",
111
117
  "eslint-plugin-prettier": "^5.5.5",
112
- "jsdom": "^27.4.0",
113
- "prettier": "^3.8.0",
114
- "typescript": "^5.9.3",
115
- "typescript-eslint": "^8.53.1",
116
- "vitest": "^4.0.17"
118
+ "jsdom": "^29.1.1",
119
+ "prettier": "^3.8.3",
120
+ "react": "^19.2.6",
121
+ "rxjs": "^7.8.2",
122
+ "typescript": "^6.0.3",
123
+ "typescript-eslint": "^8.60.0",
124
+ "vitest": "^4.1.7",
125
+ "vue": "^3.5.34",
126
+ "zone.js": "~0.16.0"
117
127
  },
118
128
  "engines": {
119
- "node": ">=18.0.0"
129
+ "node": ">=24.13.0"
120
130
  },
121
131
  "capacitor": {
122
132
  "ios": {
@@ -126,5 +136,6 @@
126
136
  "src": "android",
127
137
  "androidModule": "strata-storage"
128
138
  }
129
- }
139
+ },
140
+ "packageManager": "yarn@4.14.1"
130
141
  }
package/scripts/build.js CHANGED
@@ -26,7 +26,7 @@ fs.mkdirSync(distDir, { recursive: true });
26
26
  // Compile TypeScript to ESM
27
27
  console.log('📦 Building ES Modules...');
28
28
  try {
29
- execSync('npx tsc', { stdio: 'inherit', cwd: rootDir });
29
+ execSync('yarn exec tsc', { stdio: 'inherit', cwd: rootDir });
30
30
  } catch (error) {
31
31
  console.error('❌ Build failed');
32
32
  process.exit(1);
@@ -141,6 +141,18 @@ const distPackageJson = {
141
141
  './firebase': {
142
142
  types: './firebase.d.ts',
143
143
  default: './firebase.js'
144
+ },
145
+ './react': {
146
+ types: './integrations/react/index.d.ts',
147
+ default: './integrations/react/index.js'
148
+ },
149
+ './vue': {
150
+ types: './integrations/vue/index.d.ts',
151
+ default: './integrations/vue/index.js'
152
+ },
153
+ './angular': {
154
+ types: './integrations/angular/index.d.ts',
155
+ default: './integrations/angular/index.js'
144
156
  }
145
157
  },
146
158
  author: packageJson.author,
@@ -151,9 +163,8 @@ const distPackageJson = {
151
163
  peerDependenciesMeta: packageJson.peerDependenciesMeta,
152
164
  capacitor: packageJson.capacitor,
153
165
  sideEffects: false,
154
- engines: {
155
- node: '>=18.0.0'
156
- }
166
+ // Mirror the root engines so the published manifest never drifts from it.
167
+ engines: packageJson.engines || { node: '>=18.0.0' }
157
168
  };
158
169
 
159
170
  fs.writeFileSync(
@@ -162,4 +173,4 @@ fs.writeFileSync(
162
173
  );
163
174
 
164
175
  console.log('✅ Build completed successfully!');
165
- console.log(`📂 Output: ${distDir}`);
176
+ console.log(`📂 Output: ${distDir}`);
@@ -399,11 +399,7 @@ async function installDependencies(config) {
399
399
  }
400
400
 
401
401
  // Install command
402
- const command = config.packageManager === 'yarn'
403
- ? `yarn add ${deps.join(' ')}`
404
- : config.packageManager === 'pnpm'
405
- ? `pnpm add ${deps.join(' ')}`
406
- : `npm install ${deps.join(' ')}`;
402
+ const command = `yarn add ${deps.join(' ')}`;
407
403
 
408
404
  try {
409
405
  log.info(`Running: ${command}`);
@@ -445,4 +441,4 @@ configure().catch((error) => {
445
441
  log.error('Configuration failed:');
446
442
  console.error(error);
447
443
  process.exit(1);
448
- });
444
+ });
@@ -30,7 +30,7 @@ if (isCapacitorProject) {
30
30
  console.log('📱 Capacitor Support Available (Optional):');
31
31
  console.log(' import { registerCapacitorAdapters } from "strata-storage/capacitor";');
32
32
  console.log(' await registerCapacitorAdapters(storage);');
33
- console.log(' Run "npx cap sync" to sync native code\n');
33
+ console.log(' Run "yarn cap sync" to sync native code\n');
34
34
  }
35
35
 
36
36
  console.log('✨ Features:');
@@ -41,4 +41,4 @@ console.log(' • Built-in encryption & compression');
41
41
  console.log(' • Cross-tab synchronization\n');
42
42
 
43
43
  console.log('📖 Documentation: https://github.com/aoneahsan/strata-storage');
44
- console.log('⭐ Star us on GitHub: https://github.com/aoneahsan/strata-storage\n');
44
+ console.log('⭐ Star us on GitHub: https://github.com/aoneahsan/strata-storage\n');