strata-storage 2.4.3 → 2.5.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 (108) hide show
  1. package/AI-INTEGRATION-GUIDE.md +115 -261
  2. package/README.md +426 -182
  3. package/android/AGENTS.md +34 -0
  4. package/android/CLAUDE.md +51 -0
  5. package/android/src/main/java/com/strata/storage/SQLiteStorage.java +35 -0
  6. package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +191 -27
  7. package/dist/README.md +426 -182
  8. package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
  9. package/dist/adapters/capacitor/FilesystemAdapter.js +2 -1
  10. package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
  11. package/dist/adapters/capacitor/PreferencesAdapter.js +2 -1
  12. package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
  13. package/dist/adapters/capacitor/SecureAdapter.js +2 -1
  14. package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
  15. package/dist/adapters/capacitor/SqliteAdapter.js +2 -1
  16. package/dist/adapters/web/CacheAdapter.d.ts.map +1 -1
  17. package/dist/adapters/web/CacheAdapter.js +11 -3
  18. package/dist/adapters/web/CookieAdapter.d.ts +37 -1
  19. package/dist/adapters/web/CookieAdapter.d.ts.map +1 -1
  20. package/dist/adapters/web/CookieAdapter.js +89 -9
  21. package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
  22. package/dist/adapters/web/IndexedDBAdapter.js +10 -2
  23. package/dist/adapters/web/LocalStorageAdapter.d.ts +31 -0
  24. package/dist/adapters/web/LocalStorageAdapter.d.ts.map +1 -1
  25. package/dist/adapters/web/LocalStorageAdapter.js +92 -19
  26. package/dist/adapters/web/MemoryAdapter.d.ts +24 -0
  27. package/dist/adapters/web/MemoryAdapter.d.ts.map +1 -1
  28. package/dist/adapters/web/MemoryAdapter.js +69 -18
  29. package/dist/adapters/web/SessionStorageAdapter.d.ts +24 -0
  30. package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
  31. package/dist/adapters/web/SessionStorageAdapter.js +71 -9
  32. package/dist/adapters/web/URLAdapter.d.ts +59 -0
  33. package/dist/adapters/web/URLAdapter.d.ts.map +1 -0
  34. package/dist/adapters/web/URLAdapter.js +234 -0
  35. package/dist/adapters/web/index.d.ts +1 -0
  36. package/dist/adapters/web/index.d.ts.map +1 -1
  37. package/dist/adapters/web/index.js +1 -0
  38. package/dist/android/AGENTS.md +34 -0
  39. package/dist/android/CLAUDE.md +51 -0
  40. package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +35 -0
  41. package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +191 -27
  42. package/dist/capacitor.d.ts.map +1 -1
  43. package/dist/capacitor.js +2 -1
  44. package/dist/core/BaseAdapter.d.ts +8 -0
  45. package/dist/core/BaseAdapter.d.ts.map +1 -1
  46. package/dist/core/BaseAdapter.js +34 -14
  47. package/dist/core/Strata.d.ts +56 -2
  48. package/dist/core/Strata.d.ts.map +1 -1
  49. package/dist/core/Strata.js +501 -53
  50. package/dist/features/encryption.d.ts.map +1 -1
  51. package/dist/features/encryption.js +3 -2
  52. package/dist/features/integrity.d.ts +16 -0
  53. package/dist/features/integrity.d.ts.map +1 -0
  54. package/dist/features/integrity.js +28 -0
  55. package/dist/features/observer.d.ts.map +1 -1
  56. package/dist/features/observer.js +2 -1
  57. package/dist/features/query.d.ts +7 -1
  58. package/dist/features/query.d.ts.map +1 -1
  59. package/dist/features/query.js +9 -2
  60. package/dist/features/sync.d.ts.map +1 -1
  61. package/dist/features/sync.js +4 -3
  62. package/dist/index.d.ts +35 -2
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +55 -30
  65. package/dist/integrations/angular/index.d.ts +158 -0
  66. package/dist/integrations/angular/index.d.ts.map +1 -0
  67. package/dist/integrations/angular/index.js +395 -0
  68. package/dist/integrations/index.d.ts +15 -0
  69. package/dist/integrations/index.d.ts.map +1 -0
  70. package/dist/integrations/index.js +18 -0
  71. package/dist/integrations/react/index.d.ts +75 -0
  72. package/dist/integrations/react/index.d.ts.map +1 -0
  73. package/dist/integrations/react/index.js +191 -0
  74. package/dist/integrations/vue/index.d.ts +103 -0
  75. package/dist/integrations/vue/index.d.ts.map +1 -0
  76. package/dist/integrations/vue/index.js +274 -0
  77. package/dist/ios/AGENTS.md +33 -0
  78. package/dist/ios/CLAUDE.md +49 -0
  79. package/dist/ios/Plugin/KeychainStorage.swift +139 -50
  80. package/dist/ios/Plugin/SQLiteStorage.swift +40 -0
  81. package/dist/ios/Plugin/StrataStoragePlugin.m +23 -0
  82. package/dist/ios/Plugin/StrataStoragePlugin.swift +201 -52
  83. package/dist/package.json +21 -5
  84. package/dist/plugin/index.d.ts.map +1 -1
  85. package/dist/plugin/index.js +2 -1
  86. package/dist/types/index.d.ts +58 -9
  87. package/dist/types/index.d.ts.map +1 -1
  88. package/dist/types/index.js +0 -13
  89. package/dist/utils/errors.d.ts +7 -0
  90. package/dist/utils/errors.d.ts.map +1 -1
  91. package/dist/utils/errors.js +15 -3
  92. package/dist/utils/index.d.ts +63 -5
  93. package/dist/utils/index.d.ts.map +1 -1
  94. package/dist/utils/index.js +109 -16
  95. package/dist/utils/logger.d.ts +31 -0
  96. package/dist/utils/logger.d.ts.map +1 -0
  97. package/dist/utils/logger.js +63 -0
  98. package/ios/AGENTS.md +33 -0
  99. package/ios/CLAUDE.md +49 -0
  100. package/ios/Plugin/KeychainStorage.swift +139 -50
  101. package/ios/Plugin/SQLiteStorage.swift +40 -0
  102. package/ios/Plugin/StrataStoragePlugin.m +23 -0
  103. package/ios/Plugin/StrataStoragePlugin.swift +201 -52
  104. package/package.json +31 -20
  105. package/scripts/build.js +16 -5
  106. package/scripts/configure.js +2 -6
  107. package/scripts/postinstall.js +2 -2
  108. package/Readme.md +0 -271
@@ -1,19 +1,72 @@
1
1
  import Foundation
2
2
  import Security
3
3
 
4
+ /**
5
+ * Keychain-backed secure storage.
6
+ *
7
+ * Security notes:
8
+ * - Items default to `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`,
9
+ * which keeps data available to background tasks after the first unlock
10
+ * while preventing it from leaving the device via encrypted backups.
11
+ * `kSecAttrAccessibleAlways` / `kSecAttrAccessibleAlwaysThisDeviceOnly`
12
+ * are deprecated and insecure and are intentionally NOT used.
13
+ * - Callers may pass a stricter accessibility class from JS via the
14
+ * `accessible` option (see KeychainAccessible in definitions.ts).
15
+ * - Secret values are never logged.
16
+ */
4
17
  @objc public class KeychainStorage: NSObject {
5
18
  private let service: String
6
19
  private let accessGroup: String?
7
-
20
+ private let defaultAccessible: CFString
21
+
8
22
  @objc public init(service: String? = nil, accessGroup: String? = nil) {
9
23
  self.service = service ?? Bundle.main.bundleIdentifier ?? "StrataStorage"
10
24
  self.accessGroup = accessGroup
25
+ // After-first-unlock + this-device-only is a safe, widely-recommended
26
+ // default for app secrets. It is more permissive than whenUnlocked
27
+ // (so background access works) but still excluded from backups.
28
+ self.defaultAccessible = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
11
29
  super.init()
12
30
  }
13
-
14
- @objc public func set(key: String, value: Any) throws -> Bool {
31
+
32
+ /// Maps a JS `KeychainAccessible` string to the matching Security framework
33
+ /// constant. Unknown / nil values fall back to the secure default.
34
+ private func accessibleConstant(for raw: String?) -> CFString {
35
+ switch raw {
36
+ case "whenUnlocked":
37
+ return kSecAttrAccessibleWhenUnlocked
38
+ case "afterFirstUnlock":
39
+ return kSecAttrAccessibleAfterFirstUnlock
40
+ case "whenUnlockedThisDeviceOnly":
41
+ return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
42
+ case "afterFirstUnlockThisDeviceOnly":
43
+ return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
44
+ case "whenPasscodeSetThisDeviceOnly":
45
+ return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
46
+ default:
47
+ return defaultAccessible
48
+ }
49
+ }
50
+
51
+ /// Builds an NSError that surfaces the OSStatus to the JS bridge without
52
+ /// leaking any stored secret value.
53
+ private func keychainError(_ status: OSStatus, operation: String) -> NSError {
54
+ let message: String
55
+ if #available(iOS 11.3, *), let str = SecCopyErrorMessageString(status, nil) as String? {
56
+ message = str
57
+ } else {
58
+ message = "OSStatus \(status)"
59
+ }
60
+ return NSError(
61
+ domain: "StrataStorage.KeychainStorage",
62
+ code: Int(status),
63
+ userInfo: [NSLocalizedDescriptionKey: "Keychain \(operation) failed: \(message)"]
64
+ )
65
+ }
66
+
67
+ @objc public func set(key: String, value: Any, accessible: String? = nil) throws -> Bool {
15
68
  let data: Data
16
-
69
+
17
70
  if let dataValue = value as? Data {
18
71
  data = dataValue
19
72
  } else if let stringValue = value as? String {
@@ -23,28 +76,43 @@ import Security
23
76
  let jsonData = try JSONSerialization.data(withJSONObject: value, options: [])
24
77
  data = jsonData
25
78
  }
26
-
27
- let query = createQuery(key: key)
28
- SecItemDelete(query as CFDictionary)
29
-
30
- var newItem = query
79
+
80
+ // Remove any existing item first so the accessibility class is applied
81
+ // cleanly (SecItemUpdate cannot change accessibility reliably).
82
+ let deleteQuery = createQuery(key: key)
83
+ SecItemDelete(deleteQuery as CFDictionary)
84
+
85
+ var newItem = createQuery(key: key, accessible: accessibleConstant(for: accessible))
31
86
  newItem[kSecValueData as String] = data
32
-
87
+
33
88
  let status = SecItemAdd(newItem as CFDictionary, nil)
34
- return status == errSecSuccess
89
+ guard status == errSecSuccess else {
90
+ throw keychainError(status, operation: "set")
91
+ }
92
+ return true
93
+ }
94
+
95
+ // Convenience overload kept for existing Objective-C callers.
96
+ @objc public func set(key: String, value: Any) throws -> Bool {
97
+ return try set(key: key, value: value, accessible: nil)
35
98
  }
36
-
99
+
37
100
  @objc public func get(key: String) throws -> Any? {
38
101
  var query = createQuery(key: key)
39
102
  query[kSecReturnData as String] = true
40
103
  query[kSecMatchLimit as String] = kSecMatchLimitOne
41
-
104
+
42
105
  var result: AnyObject?
43
106
  let status = SecItemCopyMatching(query as CFDictionary, &result)
44
-
45
- guard status == errSecSuccess else { return nil }
107
+
108
+ if status == errSecItemNotFound {
109
+ return nil
110
+ }
111
+ guard status == errSecSuccess else {
112
+ throw keychainError(status, operation: "get")
113
+ }
46
114
  guard let data = result as? Data else { return nil }
47
-
115
+
48
116
  // Try to parse as JSON first, fallback to string
49
117
  if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) {
50
118
  return jsonObject
@@ -52,35 +120,41 @@ import Security
52
120
  return String(data: data, encoding: .utf8)
53
121
  }
54
122
  }
55
-
123
+
56
124
  @objc public func remove(key: String) throws -> Bool {
57
125
  let query = createQuery(key: key)
58
126
  let status = SecItemDelete(query as CFDictionary)
59
- return status == errSecSuccess
127
+ guard status == errSecSuccess || status == errSecItemNotFound else {
128
+ throw keychainError(status, operation: "remove")
129
+ }
130
+ return true
60
131
  }
61
-
132
+
62
133
  @objc public func clear(prefix: String? = nil) throws -> Bool {
63
134
  if let prefix = prefix {
64
135
  // Clear only keys with the given prefix
65
136
  let keysToRemove = try keys(pattern: prefix)
66
- var allSuccess = true
67
137
  for key in keysToRemove {
68
- if !(try remove(key: key)) {
69
- allSuccess = false
70
- }
138
+ _ = try remove(key: key)
71
139
  }
72
- return allSuccess
140
+ return true
73
141
  } else {
74
- // Clear all keys
75
- let query: [String: Any] = [
142
+ // Clear all keys for this service (and access group, if any).
143
+ var query: [String: Any] = [
76
144
  kSecClass as String: kSecClassGenericPassword,
77
145
  kSecAttrService as String: service
78
146
  ]
147
+ if let accessGroup = accessGroup {
148
+ query[kSecAttrAccessGroup as String] = accessGroup
149
+ }
79
150
  let status = SecItemDelete(query as CFDictionary)
80
- return status == errSecSuccess || status == errSecItemNotFound
151
+ guard status == errSecSuccess || status == errSecItemNotFound else {
152
+ throw keychainError(status, operation: "clear")
153
+ }
154
+ return true
81
155
  }
82
156
  }
83
-
157
+
84
158
  @objc public func keys(pattern: String? = nil) throws -> [String] {
85
159
  var query: [String: Any] = [
86
160
  kSecClass as String: kSecClassGenericPassword,
@@ -88,57 +162,72 @@ import Security
88
162
  kSecReturnAttributes as String: true,
89
163
  kSecMatchLimit as String: kSecMatchLimitAll
90
164
  ]
91
-
165
+
92
166
  if let accessGroup = accessGroup {
93
167
  query[kSecAttrAccessGroup as String] = accessGroup
94
168
  }
95
-
169
+
96
170
  var result: AnyObject?
97
171
  let status = SecItemCopyMatching(query as CFDictionary, &result)
98
-
172
+
173
+ if status == errSecItemNotFound {
174
+ return []
175
+ }
99
176
  guard status == errSecSuccess,
100
- let items = result as? [[String: Any]] else { return [] }
101
-
177
+ let items = result as? [[String: Any]] else {
178
+ if status == errSecSuccess { return [] }
179
+ throw keychainError(status, operation: "keys")
180
+ }
181
+
102
182
  let allKeys = items.compactMap { $0[kSecAttrAccount as String] as? String }
103
-
183
+
104
184
  guard let pattern = pattern else {
105
185
  return allKeys
106
186
  }
107
-
108
- // Filter keys by pattern (simple prefix matching)
187
+
188
+ // Filter keys by pattern (simple prefix / contains matching)
109
189
  return allKeys.filter { key in
110
190
  key.hasPrefix(pattern) || key.contains(pattern)
111
191
  }
112
192
  }
113
-
114
- private func createQuery(key: String) -> [String: Any] {
193
+
194
+ private func createQuery(key: String, accessible: CFString? = nil) -> [String: Any] {
115
195
  var query: [String: Any] = [
116
196
  kSecClass as String: kSecClassGenericPassword,
117
197
  kSecAttrService as String: service,
118
- kSecAttrAccount as String: key,
119
- kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
198
+ kSecAttrAccount as String: key
120
199
  ]
121
-
200
+
201
+ // Accessibility only needs to be set when adding/writing an item.
202
+ // Including it on read/delete queries can over-constrain matching.
203
+ if let accessible = accessible {
204
+ query[kSecAttrAccessible as String] = accessible
205
+ }
206
+
122
207
  if let accessGroup = accessGroup {
123
208
  query[kSecAttrAccessGroup as String] = accessGroup
124
209
  }
125
-
210
+
126
211
  return query
127
212
  }
128
-
213
+
129
214
  @objc public func size() throws -> (total: Int, count: Int) {
130
215
  let allKeys = try keys()
131
216
  var totalSize = 0
132
-
217
+
133
218
  for key in allKeys {
134
- if let data = try get(key: key) as? Data {
135
- totalSize += data.count
136
- } else if let string = try get(key: key) as? String {
137
- totalSize += string.data(using: .utf8)?.count ?? 0
219
+ if let value = try get(key: key) {
220
+ if let data = value as? Data {
221
+ totalSize += data.count
222
+ } else if let string = value as? String {
223
+ totalSize += string.data(using: .utf8)?.count ?? 0
224
+ } else if let jsonData = try? JSONSerialization.data(withJSONObject: value, options: []) {
225
+ totalSize += jsonData.count
226
+ }
138
227
  }
139
228
  totalSize += key.data(using: .utf8)?.count ?? 0
140
229
  }
141
-
230
+
142
231
  return (total: totalSize, count: allKeys.count)
143
232
  }
144
- }
233
+ }
@@ -291,4 +291,44 @@ import os.log
291
291
  sqlite3_finalize(statement)
292
292
  return (total: totalSize, count: count)
293
293
  }
294
+
295
+ /**
296
+ * Returns every row as `{ key, value }` where `value` is the decoded
297
+ * stored payload. The JS SqliteAdapter applies the real query `condition`
298
+ * by re-fetching and filtering each key, so this native side only needs to
299
+ * enumerate candidate rows. The `condition` argument is accepted for
300
+ * forward-compatibility but is not yet pushed down into SQL.
301
+ */
302
+ @objc public func query(condition: [String: Any]) -> [[String: Any]] {
303
+ guard db != nil else {
304
+ os_log("Database not initialized", log: logger, type: .error)
305
+ return []
306
+ }
307
+
308
+ let querySQL = "SELECT key, value FROM \(tableName)"
309
+ var statement: OpaquePointer?
310
+ var rows: [[String: Any]] = []
311
+
312
+ if sqlite3_prepare_v2(db, querySQL, -1, &statement, nil) == SQLITE_OK {
313
+ while sqlite3_step(statement) == SQLITE_ROW {
314
+ guard let keyCString = sqlite3_column_text(statement, 0) else { continue }
315
+ let key = String(cString: keyCString)
316
+
317
+ var decoded: Any = NSNull()
318
+ if let blob = sqlite3_column_blob(statement, 1) {
319
+ let valueData = Data(bytes: blob, count: Int(sqlite3_column_bytes(statement, 1)))
320
+ if let jsonObject = try? JSONSerialization.jsonObject(with: valueData, options: []) {
321
+ decoded = jsonObject
322
+ } else if let str = String(data: valueData, encoding: .utf8) {
323
+ decoded = str
324
+ }
325
+ }
326
+
327
+ rows.append(["key": key, "value": decoded])
328
+ }
329
+ }
330
+
331
+ sqlite3_finalize(statement)
332
+ return rows
333
+ }
294
334
  }
@@ -0,0 +1,23 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <Capacitor/Capacitor.h>
3
+
4
+ // Registers the StrataStorage plugin and its methods with the Capacitor
5
+ // bridge. Without this macro block the @objc Swift methods are NOT exposed
6
+ // to the JavaScript layer and every call would fail with "method not
7
+ // implemented". The method names and the parameter list below MUST match
8
+ // the @objc func names in StrataStoragePlugin.swift and the JS plugin
9
+ // contract in src/plugin/definitions.ts.
10
+ CAP_PLUGIN(StrataStoragePlugin, "StrataStorage",
11
+ CAP_PLUGIN_METHOD(isAvailable, CAPPluginReturnPromise);
12
+ CAP_PLUGIN_METHOD(get, CAPPluginReturnPromise);
13
+ CAP_PLUGIN_METHOD(set, CAPPluginReturnPromise);
14
+ CAP_PLUGIN_METHOD(remove, CAPPluginReturnPromise);
15
+ CAP_PLUGIN_METHOD(clear, CAPPluginReturnPromise);
16
+ CAP_PLUGIN_METHOD(keys, CAPPluginReturnPromise);
17
+ CAP_PLUGIN_METHOD(size, CAPPluginReturnPromise);
18
+ CAP_PLUGIN_METHOD(query, CAPPluginReturnPromise);
19
+ CAP_PLUGIN_METHOD(getUserDefaults, CAPPluginReturnPromise);
20
+ CAP_PLUGIN_METHOD(setUserDefaults, CAPPluginReturnPromise);
21
+ CAP_PLUGIN_METHOD(getKeychain, CAPPluginReturnPromise);
22
+ CAP_PLUGIN_METHOD(setKeychain, CAPPluginReturnPromise);
23
+ )