strata-storage 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/AI-INTEGRATION-GUIDE.md +97 -10
  2. package/README.md +70 -70
  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 +70 -70
  12. package/dist/adapters/capacitor/FilesystemAdapter.d.ts +2 -2
  13. package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
  14. package/dist/adapters/capacitor/FilesystemAdapter.js +6 -2
  15. package/dist/adapters/capacitor/PreferencesAdapter.d.ts +2 -2
  16. package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
  17. package/dist/adapters/capacitor/PreferencesAdapter.js +6 -2
  18. package/dist/adapters/capacitor/SecureAdapter.d.ts +3 -3
  19. package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
  20. package/dist/adapters/capacitor/SecureAdapter.js +6 -2
  21. package/dist/adapters/capacitor/SqliteAdapter.d.ts +24 -3
  22. package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
  23. package/dist/adapters/capacitor/SqliteAdapter.js +105 -23
  24. package/dist/adapters/capacitor/index.d.ts +4 -4
  25. package/dist/adapters/web/CacheAdapter.d.ts +2 -2
  26. package/dist/adapters/web/CookieAdapter.d.ts +2 -2
  27. package/dist/adapters/web/IndexedDBAdapter.d.ts +10 -3
  28. package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
  29. package/dist/adapters/web/IndexedDBAdapter.js +14 -3
  30. package/dist/adapters/web/LocalStorageAdapter.d.ts +2 -2
  31. package/dist/adapters/web/MemoryAdapter.d.ts +2 -2
  32. package/dist/adapters/web/SessionStorageAdapter.d.ts +10 -10
  33. package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
  34. package/dist/adapters/web/SessionStorageAdapter.js +3 -2
  35. package/dist/adapters/web/URLAdapter.d.ts +2 -2
  36. package/dist/adapters/web/index.d.ts +7 -7
  37. package/dist/capacitor.d.ts +6 -6
  38. package/dist/capacitor.d.ts.map +1 -1
  39. package/dist/capacitor.js +5 -4
  40. package/dist/core/AdapterRegistry.d.ts +1 -1
  41. package/dist/core/BaseAdapter.d.ts +21 -5
  42. package/dist/core/BaseAdapter.d.ts.map +1 -1
  43. package/dist/core/BaseAdapter.js +29 -1
  44. package/dist/core/Strata.d.ts +41 -7
  45. package/dist/core/Strata.d.ts.map +1 -1
  46. package/dist/core/Strata.js +476 -118
  47. package/dist/features/compression/index.d.ts +1 -1
  48. package/dist/features/compression.d.ts +5 -1
  49. package/dist/features/compression.d.ts.map +1 -1
  50. package/dist/features/encryption/index.d.ts +1 -1
  51. package/dist/features/encryption.d.ts +55 -2
  52. package/dist/features/encryption.d.ts.map +1 -1
  53. package/dist/features/encryption.js +139 -26
  54. package/dist/features/migration.d.ts +1 -1
  55. package/dist/features/observer/index.d.ts +1 -1
  56. package/dist/features/observer.d.ts +1 -1
  57. package/dist/features/query/index.d.ts +1 -1
  58. package/dist/features/query.d.ts +1 -1
  59. package/dist/features/query.d.ts.map +1 -1
  60. package/dist/features/query.js +3 -1
  61. package/dist/features/sync/index.d.ts +1 -1
  62. package/dist/features/sync.d.ts +19 -3
  63. package/dist/features/sync.d.ts.map +1 -1
  64. package/dist/features/sync.js +57 -9
  65. package/dist/features/ttl/index.d.ts +1 -1
  66. package/dist/features/ttl.d.ts +2 -2
  67. package/dist/firebase.d.ts +1 -1
  68. package/dist/firebase.d.ts.map +1 -1
  69. package/dist/firebase.js +34 -9
  70. package/dist/index.d.ts +24 -22
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +6 -1
  73. package/dist/integrations/angular/index.d.ts +2 -2
  74. package/dist/integrations/angular/index.d.ts.map +1 -1
  75. package/dist/integrations/angular/index.js +7 -8
  76. package/dist/integrations/react/index.d.ts +3 -3
  77. package/dist/integrations/react/index.d.ts.map +1 -1
  78. package/dist/integrations/react/index.js +16 -4
  79. package/dist/integrations/vue/index.d.ts +2 -2
  80. package/dist/integrations/vue/index.d.ts.map +1 -1
  81. package/dist/integrations/vue/index.js +5 -2
  82. package/dist/package.json +11 -2
  83. package/dist/plugin/definitions.d.ts +29 -3
  84. package/dist/plugin/definitions.d.ts.map +1 -1
  85. package/dist/plugin/index.d.ts +2 -2
  86. package/dist/plugin/index.d.ts.map +1 -1
  87. package/dist/plugin/index.js +3 -0
  88. package/dist/plugin/web.d.ts +11 -2
  89. package/dist/plugin/web.d.ts.map +1 -1
  90. package/dist/plugin/web.js +23 -4
  91. package/dist/types/index.d.ts +50 -20
  92. package/dist/types/index.d.ts.map +1 -1
  93. package/dist/types/index.js +1 -10
  94. package/dist/utils/index.d.ts +5 -0
  95. package/dist/utils/index.d.ts.map +1 -1
  96. package/dist/utils/index.js +7 -0
  97. package/ios/AGENTS.md +14 -3
  98. package/ios/CLAUDE.md +25 -3
  99. package/ios/Plugin/FilesystemStorage.swift +19 -0
  100. package/ios/Plugin/SQLiteStorage.swift +59 -9
  101. package/ios/Plugin/StrataStoragePlugin.m +1 -0
  102. package/ios/Plugin/StrataStoragePlugin.swift +87 -34
  103. package/ios/Plugin/UserDefaultsStorage.swift +47 -46
  104. package/package.json +29 -26
  105. package/scripts/build.js +16 -14
  106. package/scripts/cli.js +1 -1
  107. package/scripts/configure.js +2 -2
  108. package/scripts/postinstall.js +3 -2
  109. package/dist/android/AGENTS.md +0 -51
  110. package/dist/android/CLAUDE.md +0 -89
  111. package/dist/android/build.gradle +0 -58
  112. package/dist/android/proguard-rules.pro +0 -11
  113. package/dist/android/settings.gradle +0 -2
  114. package/dist/android/src/main/AndroidManifest.xml +0 -2
  115. package/dist/android/src/main/java/com/strata/storage/EncryptedStorage.java +0 -218
  116. package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +0 -287
  117. package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +0 -371
  118. package/dist/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +0 -209
  119. package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +0 -616
  120. package/dist/core/StorageStrategy.d.ts +0 -55
  121. package/dist/core/StorageStrategy.d.ts.map +0 -1
  122. package/dist/core/StorageStrategy.js +0 -200
  123. package/dist/ios/AGENTS.md +0 -48
  124. package/dist/ios/CLAUDE.md +0 -84
  125. package/dist/ios/Plugin/FilesystemStorage.swift +0 -218
  126. package/dist/ios/Plugin/KeychainStorage.swift +0 -233
  127. package/dist/ios/Plugin/SQLiteStorage.swift +0 -426
  128. package/dist/ios/Plugin/StrataStoragePlugin.m +0 -23
  129. package/dist/ios/Plugin/StrataStoragePlugin.swift +0 -411
  130. package/dist/ios/Plugin/UserDefaultsStorage.swift +0 -87
@@ -24,30 +24,44 @@ public class StrataStoragePlugin: CAPPlugin {
24
24
  private static let defaultDatabase = "strata_storage"
25
25
  private static let defaultTable = "storage"
26
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"
27
+ /// Allowed characters for a SQLite identifier body (the first character must
28
+ /// additionally not be a digit).
29
+ private static let sqliteIdentifierChars =
30
+ CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_")
31
+
32
+ /// Validates a `database`/`table` identifier against `^[A-Za-z_][A-Za-z0-9_]*$`.
33
+ /// These two cannot be bound as parameters (they are interpolated into SQL /
34
+ /// the `.db` filename), so they are the single injection surface; every
35
+ /// other value is bound. Non-conforming identifiers are REJECTED rather than
36
+ /// silently stripped — the TS adapter applies the same allow-list and rejects
37
+ /// first, so this is defense-in-depth (and also blocks path traversal).
38
+ private func isValidSqliteIdentifier(_ s: String) -> Bool {
39
+ guard let first = s.unicodeScalars.first else { return false }
40
+ if CharacterSet(charactersIn: "0123456789").contains(first) { return false }
41
+ return s.unicodeScalars.allSatisfy { StrataStoragePlugin.sqliteIdentifierChars.contains($0) }
36
42
  }
37
43
 
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
44
+ /// Returns the validated table identifier for the call, or nil after
45
+ /// rejecting `call` with a clear error.
46
+ private func validatedTable(_ call: CAPPluginCall) -> String? {
47
+ let table = call.getString("table") ?? StrataStoragePlugin.defaultTable
48
+ guard isValidSqliteIdentifier(table) else {
49
+ call.reject("Invalid SQLite table name: \(table)")
50
+ return nil
51
+ }
52
+ return table
46
53
  }
47
54
 
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")))
55
+ /// Returns the cached SQLite store for the call's validated `database`, or
56
+ /// nil after rejecting `call`. The filename cannot be a bound parameter, so
57
+ /// the name is validated before `.db` is appended.
58
+ private func validatedSqliteStore(for call: CAPPluginCall) -> SQLiteStorage? {
59
+ let database = call.getString("database") ?? StrataStoragePlugin.defaultDatabase
60
+ guard isValidSqliteIdentifier(database) else {
61
+ call.reject("Invalid SQLite database name: \(database)")
62
+ return nil
63
+ }
64
+ return SQLiteStorageManager.shared.store(forFile: "\(database).db")
51
65
  }
52
66
 
53
67
  /**
@@ -56,8 +70,21 @@ public class StrataStoragePlugin: CAPPlugin {
56
70
  */
57
71
  @objc func isAvailable(_ call: CAPPluginCall) {
58
72
  let storage = call.getString("storage") ?? "preferences"
73
+ let available: Bool
74
+ switch storage {
75
+ case "sqlite":
76
+ let database = call.getString("database") ?? StrataStoragePlugin.defaultDatabase
77
+ available = isValidSqliteIdentifier(database)
78
+ && SQLiteStorageManager.shared.store(forFile: "\(database).db").isOpen
79
+ case "filesystem":
80
+ available = filesystemStorage.isAvailable()
81
+ case "preferences", "secure":
82
+ available = true
83
+ default:
84
+ available = false
85
+ }
59
86
  call.resolve([
60
- "available": supportedStorageTypes.contains(storage)
87
+ "available": available
61
88
  ])
62
89
  }
63
90
 
@@ -81,7 +108,8 @@ public class StrataStoragePlugin: CAPPlugin {
81
108
  case "secure":
82
109
  value = try keychainStorage.get(key: key)
83
110
  case "sqlite":
84
- value = sqliteStore(for: call).get(table: sqliteTableName(from: call.getString("table")), key: key)
111
+ guard let store = validatedSqliteStore(for: call), let table = validatedTable(call) else { return }
112
+ value = store.get(table: table, key: key)
85
113
  case "filesystem":
86
114
  value = filesystemStorage.get(key: key)
87
115
  case "preferences":
@@ -117,6 +145,7 @@ public class StrataStoragePlugin: CAPPlugin {
117
145
  let value = call.getValue("value") ?? NSNull()
118
146
  _ = try keychainStorage.set(key: key, value: value)
119
147
  case "sqlite":
148
+ guard let store = validatedSqliteStore(for: call), let table = validatedTable(call) else { return }
120
149
  // getObject returns JSObject ([String: JSValue]); upcast each
121
150
  // value to Any (the canonical Capacitor pattern) so the bridged
122
151
  // Foundation values are JSONSerialization-compatible.
@@ -124,8 +153,8 @@ public class StrataStoragePlugin: CAPPlugin {
124
153
  call.reject("SQLite value must be a StorageValue object")
125
154
  return
126
155
  }
127
- let ok = try sqliteStore(for: call).set(
128
- table: sqliteTableName(from: call.getString("table")),
156
+ let ok = try store.set(
157
+ table: table,
129
158
  key: key,
130
159
  wrapper: jsObject as [String: Any]
131
160
  )
@@ -172,7 +201,8 @@ public class StrataStoragePlugin: CAPPlugin {
172
201
  case "secure":
173
202
  _ = try keychainStorage.remove(key: key)
174
203
  case "sqlite":
175
- _ = sqliteStore(for: call).remove(table: sqliteTableName(from: call.getString("table")), key: key)
204
+ guard let store = validatedSqliteStore(for: call), let table = validatedTable(call) else { return }
205
+ _ = store.remove(table: table, key: key)
176
206
  case "filesystem":
177
207
  _ = filesystemStorage.remove(key: key)
178
208
  case "preferences":
@@ -202,7 +232,8 @@ public class StrataStoragePlugin: CAPPlugin {
202
232
  case "secure":
203
233
  _ = try keychainStorage.clear(prefix: prefix)
204
234
  case "sqlite":
205
- _ = sqliteStore(for: call).clear(table: sqliteTableName(from: call.getString("table")), prefix: prefix)
235
+ guard let store = validatedSqliteStore(for: call), let table = validatedTable(call) else { return }
236
+ _ = store.clear(table: table, prefix: prefix)
206
237
  case "filesystem":
207
238
  _ = filesystemStorage.clear(prefix: prefix)
208
239
  case "preferences":
@@ -231,7 +262,8 @@ public class StrataStoragePlugin: CAPPlugin {
231
262
  case "secure":
232
263
  keys = try keychainStorage.keys(pattern: pattern)
233
264
  case "sqlite":
234
- keys = sqliteStore(for: call).keys(table: sqliteTableName(from: call.getString("table")), pattern: pattern)
265
+ guard let store = validatedSqliteStore(for: call), let table = validatedTable(call) else { return }
266
+ keys = store.keys(table: table, pattern: pattern)
235
267
  case "filesystem":
236
268
  keys = filesystemStorage.keys(pattern: pattern)
237
269
  case "preferences":
@@ -266,7 +298,8 @@ public class StrataStoragePlugin: CAPPlugin {
266
298
  let info = try keychainStorage.size()
267
299
  segments = ["total": info.total, "count": info.count, "keys": 0, "values": info.total, "metadata": 0]
268
300
  case "sqlite":
269
- segments = try sqliteStore(for: call).size(table: sqliteTableName(from: call.getString("table")))
301
+ guard let store = validatedSqliteStore(for: call), let table = validatedTable(call) else { return }
302
+ segments = try store.size(table: table)
270
303
  case "filesystem":
271
304
  segments = filesystemStorage.size()
272
305
  case "preferences":
@@ -295,8 +328,10 @@ public class StrataStoragePlugin: CAPPlugin {
295
328
 
296
329
  /**
297
330
  * 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).
331
+ * Matches the optional `query` method in the JS contract: resolves
332
+ * `{ results: [{ key, value }] }` where `value` is the full StorageValue
333
+ * wrapper for each non-expired row, so the JS adapter applies the condition
334
+ * in one round-trip without re-fetching each key.
300
335
  */
301
336
  @objc func query(_ call: CAPPluginCall) {
302
337
  let storage = call.getString("storage") ?? "sqlite"
@@ -306,16 +341,34 @@ public class StrataStoragePlugin: CAPPlugin {
306
341
  return
307
342
  }
308
343
 
344
+ guard let store = validatedSqliteStore(for: call), let table = validatedTable(call) else { return }
309
345
  let condition = call.getObject("condition") ?? [:]
310
- let results = sqliteStore(for: call).query(
311
- table: sqliteTableName(from: call.getString("table")),
312
- condition: condition
313
- )
346
+ let results = store.query(table: table, condition: condition)
314
347
  call.resolve([
315
348
  "results": results
316
349
  ])
317
350
  }
318
351
 
352
+ /**
353
+ * Delete every expired row for a SQLite store and resolve `{ removed }`.
354
+ * The JS adapter calls this on its TTL cleanup tick, since `keys`/`query`
355
+ * now exclude expired rows in SQL (no lazy per-key deletion on read).
356
+ */
357
+ @objc func cleanupExpired(_ call: CAPPluginCall) {
358
+ let storage = call.getString("storage") ?? "sqlite"
359
+
360
+ guard storage == "sqlite" else {
361
+ call.reject("cleanupExpired is only supported for the 'sqlite' storage type")
362
+ return
363
+ }
364
+
365
+ guard let store = validatedSqliteStore(for: call), let table = validatedTable(call) else { return }
366
+ let removed = store.cleanupExpired(table: table)
367
+ call.resolve([
368
+ "removed": removed
369
+ ])
370
+ }
371
+
319
372
  /**
320
373
  * iOS-specific: read a UserDefaults value.
321
374
  * Resolves `{ value: unknown }`.
@@ -3,7 +3,13 @@ import Foundation
3
3
  @objc public class UserDefaultsStorage: NSObject {
4
4
  private let suiteName: String?
5
5
  private var userDefaults: UserDefaults
6
-
6
+ /// All Strata keys are stored under this reserved prefix so that `keys()`,
7
+ /// `size()`, and `clear()` only ever touch Strata-owned entries — never the
8
+ /// rest of the app's `UserDefaults`. In particular `clear()` removes only
9
+ /// prefixed keys and NEVER calls `removePersistentDomain`, which would wipe
10
+ /// unrelated app/SDK settings in the shared domain.
11
+ private let keyPrefix = "strata_storage::"
12
+
7
13
  @objc public init(suiteName: String? = nil) {
8
14
  self.suiteName = suiteName
9
15
  if let suiteName = suiteName, let customDefaults = UserDefaults(suiteName: suiteName) {
@@ -13,75 +19,70 @@ import Foundation
13
19
  }
14
20
  super.init()
15
21
  }
16
-
22
+
23
+ private func physicalKey(_ key: String) -> String {
24
+ return keyPrefix + key
25
+ }
26
+
27
+ /// The physical keys in this domain that are owned by Strata.
28
+ private func ownedPhysicalKeys() -> [String] {
29
+ return userDefaults.dictionaryRepresentation().keys.filter { $0.hasPrefix(keyPrefix) }
30
+ }
31
+
17
32
  @objc public func set(key: String, value: Any) -> Bool {
18
- userDefaults.set(value, forKey: key)
33
+ userDefaults.set(value, forKey: physicalKey(key))
19
34
  return userDefaults.synchronize()
20
35
  }
21
-
36
+
22
37
  @objc public func get(key: String) -> Any? {
23
- return userDefaults.object(forKey: key)
38
+ return userDefaults.object(forKey: physicalKey(key))
24
39
  }
25
-
40
+
26
41
  @objc public func remove(key: String) -> Bool {
27
- userDefaults.removeObject(forKey: key)
42
+ userDefaults.removeObject(forKey: physicalKey(key))
28
43
  return userDefaults.synchronize()
29
44
  }
30
-
45
+
31
46
  @objc public func clear(prefix: String? = nil) -> Bool {
32
- if let prefix = prefix {
33
- // Clear only keys with the given prefix
34
- let keysToRemove = keys(pattern: prefix)
35
- for key in keysToRemove {
36
- userDefaults.removeObject(forKey: key)
37
- }
38
- } else {
39
- // Clear all keys
40
- if let suiteName = suiteName {
41
- UserDefaults(suiteName: suiteName)?.removePersistentDomain(forName: suiteName)
42
- } else {
43
- guard let domain = Bundle.main.bundleIdentifier else {
44
- // Fallback: manually clear all keys
45
- let keys = Array(userDefaults.dictionaryRepresentation().keys)
46
- for key in keys {
47
- userDefaults.removeObject(forKey: key)
48
- }
49
- return userDefaults.synchronize()
50
- }
51
- userDefaults.removePersistentDomain(forName: domain)
52
- }
47
+ // Only ever remove Strata-owned keys (optionally narrowed by `prefix`);
48
+ // never wipe the whole UserDefaults domain.
49
+ let targetPrefix = keyPrefix + (prefix ?? "")
50
+ for physical in userDefaults.dictionaryRepresentation().keys where physical.hasPrefix(targetPrefix) {
51
+ userDefaults.removeObject(forKey: physical)
53
52
  }
54
53
  return userDefaults.synchronize()
55
54
  }
56
-
55
+
57
56
  @objc public func keys(pattern: String? = nil) -> [String] {
58
- let allKeys = Array(userDefaults.dictionaryRepresentation().keys)
59
-
57
+ // Present logical (un-prefixed) keys to callers.
58
+ let logical = ownedPhysicalKeys().map { String($0.dropFirst(keyPrefix.count)) }
59
+
60
60
  guard let pattern = pattern else {
61
- return allKeys
61
+ return logical
62
62
  }
63
-
64
- // Filter keys by pattern (simple prefix matching)
65
- return allKeys.filter { key in
63
+
64
+ return logical.filter { key in
66
65
  key.hasPrefix(pattern) || key.contains(pattern)
67
66
  }
68
67
  }
69
-
68
+
70
69
  @objc public func has(key: String) -> Bool {
71
- return userDefaults.object(forKey: key) != nil
70
+ return userDefaults.object(forKey: physicalKey(key)) != nil
72
71
  }
73
-
72
+
74
73
  @objc public func size() -> (total: Int, count: Int) {
75
74
  let all = userDefaults.dictionaryRepresentation()
76
75
  var totalSize = 0
77
- let count = all.count
78
-
79
- for (key, value) in all {
80
- // Estimate size (key + value in bytes)
81
- totalSize += key.data(using: .utf8)?.count ?? 0
76
+ var count = 0
77
+
78
+ for (physical, value) in all where physical.hasPrefix(keyPrefix) {
79
+ count += 1
80
+ let logical = String(physical.dropFirst(keyPrefix.count))
81
+ // Estimate size (logical key + value in bytes)
82
+ totalSize += logical.data(using: .utf8)?.count ?? 0
82
83
  totalSize += "\(value)".data(using: .utf8)?.count ?? 0
83
84
  }
84
-
85
+
85
86
  return (total: totalSize, count: count)
86
87
  }
87
- }
88
+ }
package/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "name": "strata-storage",
3
- "version": "2.6.0",
3
+ "version": "2.7.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
+ "packageManager": "yarn@4.17.0",
7
+ "sideEffects": false,
6
8
  "main": "dist/index.js",
7
9
  "types": "dist/index.d.ts",
8
10
  "files": [
@@ -38,15 +40,16 @@
38
40
  "./angular": {
39
41
  "types": "./dist/integrations/angular/index.d.ts",
40
42
  "default": "./dist/integrations/angular/index.js"
41
- }
43
+ },
44
+ "./package.json": "./package.json"
42
45
  },
43
46
  "scripts": {
44
47
  "build": "node scripts/build.js",
45
48
  "clean": "rm -rf dist/",
46
- "lint": "eslint 'src/**/*.{ts,tsx}' --fix",
49
+ "lint": "eslint 'src/**/*.{ts,tsx}'",
50
+ "lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix",
47
51
  "typecheck": "tsc --noEmit",
48
- "test": "vitest",
49
- "test:coverage": "vitest --coverage",
52
+ "prepack": "yarn build && yarn lint && yarn typecheck",
50
53
  "prepublishOnly": "yarn build && yarn lint && yarn typecheck",
51
54
  "postinstall": "node scripts/postinstall.js || true"
52
55
  },
@@ -75,13 +78,14 @@
75
78
  "url": "https://github.com/aoneahsan/strata-storage.git"
76
79
  },
77
80
  "bugs": {
78
- "url": "https://github.com/aoneahsan/strata-storage/issues"
81
+ "url": "https://stratastorage.aoneahsan.com/contact"
79
82
  },
80
- "homepage": "https://github.com/aoneahsan/strata-storage#readme",
83
+ "homepage": "https://stratastorage.aoneahsan.com",
81
84
  "peerDependencies": {
82
85
  "@angular/core": ">=21.0.6",
83
86
  "@angular/forms": ">=21.0.6",
84
87
  "@capacitor/core": ">=8.0.0",
88
+ "firebase": ">=10.0.0",
85
89
  "react": ">=19.2.3",
86
90
  "vue": ">=3.5.26"
87
91
  },
@@ -95,6 +99,9 @@
95
99
  "@capacitor/core": {
96
100
  "optional": true
97
101
  },
102
+ "firebase": {
103
+ "optional": true
104
+ },
98
105
  "react": {
99
106
  "optional": true
100
107
  },
@@ -103,27 +110,24 @@
103
110
  }
104
111
  },
105
112
  "devDependencies": {
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",
113
+ "@angular/common": "^22.0.0",
114
+ "@angular/core": "^22.0.0",
115
+ "@angular/forms": "^22.0.0",
116
+ "@capacitor/core": "^8.4.0",
117
+ "@types/node": "^25.9.2",
118
+ "@types/react": "^19.2.17",
119
+ "@typescript-eslint/eslint-plugin": "^8.60.1",
120
+ "@typescript-eslint/parser": "^8.60.1",
121
+ "eslint": "^10.4.1",
116
122
  "eslint-config-prettier": "^10.1.8",
117
- "eslint-plugin-prettier": "^5.5.5",
118
- "jsdom": "^29.1.1",
123
+ "eslint-plugin-prettier": "^5.5.6",
119
124
  "prettier": "^3.8.3",
120
- "react": "^19.2.6",
125
+ "react": "^19.2.7",
121
126
  "rxjs": "^7.8.2",
122
127
  "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"
128
+ "typescript-eslint": "^8.60.1",
129
+ "vue": "^3.5.35",
130
+ "zone.js": "~0.16.2"
127
131
  },
128
132
  "engines": {
129
133
  "node": ">=24.13.0"
@@ -136,6 +140,5 @@
136
140
  "src": "android",
137
141
  "androidModule": "strata-storage"
138
142
  }
139
- },
140
- "packageManager": "yarn@4.14.1"
143
+ }
141
144
  }
package/scripts/build.js CHANGED
@@ -43,7 +43,7 @@ const fixPathAliases = (dir) => {
43
43
 
44
44
  if (stat.isDirectory()) {
45
45
  fixPathAliases(filePath);
46
- } else if (file.endsWith('.js')) {
46
+ } else if (file.endsWith('.js') || file.endsWith('.d.ts')) {
47
47
  let content = fs.readFileSync(filePath, 'utf8');
48
48
 
49
49
  // Calculate depth from dist root
@@ -61,6 +61,10 @@ const fixPathAliases = (dir) => {
61
61
  .replace(/from\s+["']@\/types\//g, `from "${pathToRoot}types/`)
62
62
  .replace(/from\s+["']@\/utils["']/g, `from "${pathToRoot}utils/index.js"`)
63
63
  .replace(/from\s+["']@\/plugin["']/g, `from "${pathToRoot}plugin/index.js"`)
64
+ .replace(/from\s+["']@\/types["']/g, `from "${pathToRoot}types/index.js"`)
65
+ .replace(/from\s+["']@\/index["']/g, `from "${pathToRoot}index.js"`)
66
+ .replace(/from\s+["']@\/config["']/g, `from "${pathToRoot}config/index.js"`)
67
+ .replace(/from\s+["']@\/integrations["']/g, `from "${pathToRoot}integrations/index.js"`)
64
68
  .replace(/import\(["']@\/core\//g, `import("${pathToRoot}core/`)
65
69
  .replace(/import\(["']@\/adapters\//g, `import("${pathToRoot}adapters/`)
66
70
  .replace(/import\(["']@\/features\//g, `import("${pathToRoot}features/`)
@@ -68,7 +72,11 @@ const fixPathAliases = (dir) => {
68
72
  .replace(/import\(["']@\/plugin\//g, `import("${pathToRoot}plugin/`)
69
73
  .replace(/import\(["']@\/types\//g, `import("${pathToRoot}types/`)
70
74
  .replace(/import\(["']@\/utils["']\)/g, `import("${pathToRoot}utils/index.js")`)
71
- .replace(/import\(["']@\/plugin["']\)/g, `import("${pathToRoot}plugin/index.js")`);
75
+ .replace(/import\(["']@\/plugin["']\)/g, `import("${pathToRoot}plugin/index.js")`)
76
+ .replace(/import\(["']@\/types["']\)/g, `import("${pathToRoot}types/index.js")`)
77
+ .replace(/import\(["']@\/index["']\)/g, `import("${pathToRoot}index.js")`)
78
+ .replace(/import\(["']@\/config["']\)/g, `import("${pathToRoot}config/index.js")`)
79
+ .replace(/import\(["']@\/integrations["']\)/g, `import("${pathToRoot}integrations/index.js")`);
72
80
 
73
81
  // Also fix any remaining short imports that should point to index.js
74
82
  content = content
@@ -97,17 +105,6 @@ const fixPathAliases = (dir) => {
97
105
 
98
106
  fixPathAliases(distDir);
99
107
 
100
- // Copy native directories if they exist
101
- console.log('📱 Copying native code...');
102
- const nativeDirs = ['android', 'ios'];
103
- nativeDirs.forEach(dir => {
104
- const srcPath = path.join(rootDir, dir);
105
- const destPath = path.join(distDir, dir);
106
- if (fs.existsSync(srcPath)) {
107
- fs.cpSync(srcPath, destPath, { recursive: true });
108
- }
109
- });
110
-
111
108
  // Copy other files
112
109
  console.log('📄 Copying additional files...');
113
110
  const filesToCopy = ['README.md', 'LICENSE'];
@@ -153,11 +150,16 @@ const distPackageJson = {
153
150
  './angular': {
154
151
  types: './integrations/angular/index.d.ts',
155
152
  default: './integrations/angular/index.js'
156
- }
153
+ },
154
+ './package.json': './package.json'
157
155
  },
158
156
  author: packageJson.author,
159
157
  license: packageJson.license,
160
158
  repository: packageJson.repository,
159
+ // Carry metadata-only fields through so the dist manifest never drifts from
160
+ // the root manifest (bugs/homepage were previously dropped).
161
+ bugs: packageJson.bugs,
162
+ homepage: packageJson.homepage,
161
163
  keywords: packageJson.keywords,
162
164
  peerDependencies: packageJson.peerDependencies,
163
165
  peerDependenciesMeta: packageJson.peerDependenciesMeta,
package/scripts/cli.js CHANGED
@@ -39,7 +39,7 @@ Examples:
39
39
  npx strata-storage configure
40
40
  npx strata-storage init
41
41
 
42
- Documentation: https://github.com/aoneahsan/strata-storage
42
+ Documentation: https://stratastorage-docs.aoneahsan.com (AI agents: /ai)
43
43
  `);
44
44
  process.exit(0);
45
45
  }
@@ -9,6 +9,7 @@ import fs from 'fs';
9
9
  import path from 'path';
10
10
  import { execSync } from 'child_process';
11
11
  import { fileURLToPath } from 'url';
12
+ import readline from 'node:readline';
12
13
 
13
14
  const __filename = fileURLToPath(import.meta.url);
14
15
  const __dirname = path.dirname(__filename);
@@ -110,7 +111,6 @@ function detectProject() {
110
111
 
111
112
  // Get configuration through prompts
112
113
  async function getConfiguration(projectInfo) {
113
- const readline = require('readline');
114
114
  const rl = readline.createInterface({
115
115
  input: process.stdin,
116
116
  output: process.stdout
@@ -433,7 +433,7 @@ function showCompletion(config) {
433
433
  }
434
434
 
435
435
  console.log('\nSee strata.example.* for a complete example.');
436
- console.log('\nDocumentation: https://github.com/aoneahsan/strata-storage');
436
+ console.log('\nDocumentation: https://stratastorage-docs.aoneahsan.com');
437
437
  }
438
438
 
439
439
  // Run configuration
@@ -40,5 +40,6 @@ console.log(' • Optional Capacitor integration');
40
40
  console.log(' • Built-in encryption & compression');
41
41
  console.log(' • Cross-tab synchronization\n');
42
42
 
43
- console.log('📖 Documentation: https://github.com/aoneahsan/strata-storage');
44
- console.log(' Star us on GitHub: https://github.com/aoneahsan/strata-storage\n');
43
+ console.log('📖 Documentation: https://stratastorage-docs.aoneahsan.com');
44
+ console.log('🤖 For AI agents: https://stratastorage-docs.aoneahsan.com/ai');
45
+ console.log('🌐 Website: https://stratastorage.aoneahsan.com\n');
@@ -1,51 +0,0 @@
1
- # AGENTS.md — android/
2
-
3
- Last Updated: 2026-05-27
4
-
5
- > Agent instructions for Android native development.
6
-
7
- ## Files
8
-
9
- | File | Purpose |
10
- |------|---------|
11
- | `StrataStoragePlugin.java` | Capacitor plugin entry (`com.stratastorage`) |
12
- | `SharedPreferencesStorage.java` | General key-value storage (`com.strata.storage`) |
13
- | `EncryptedStorage.java` | Secure storage via EncryptedSharedPreferences (`com.strata.storage`); requires `androidx.security:security-crypto 1.1.0` (stable, upgraded in v2.6.0) |
14
- | `SQLiteStorage.java` | SQLite database storage — multi-store (v2.6.0): `database`/`table` options honoured, full `StorageValue` wrapper round-trip, `size(detailed)` supported (`com.strata.storage`) |
15
- | `FilesystemStorage.java` | File-per-key native storage under `getFilesDir()/strata_storage/` (new in v2.6.0); atomic writes via staging rename; `isAvailable()` returns `true`; no external permissions needed (`com.strata.storage`) |
16
-
17
- ## v2.6.0 Notes
18
-
19
- - **`androidx.security 1.1.0`:** upgraded from `1.1.0-alpha06`. Requires `compileSdkVersion 34`. If Gradle sync fails, bump `compileSdkVersion` and `targetSdkVersion` to 34 in `android/app/build.gradle`.
20
- - **SQLite multi-store:** `call.getString("database")` and `call.getString("table")` are now read and used. Each `(database, table)` pair → distinct `.db` file. Previously ignored.
21
- - **SQLite value shape:** native `get` returns full `StorageValue` wrapper. Corrupt rows → treated as miss (no throw).
22
- - **FilesystemStorage.java:** new class. Writes are atomic (staging temp → `renameTo`). `keys()` excludes `.staging/` artifacts. `size(detailed)` → `{ keys, values, metadata }`.
23
- - **Pending on-device verification** — see `docs/guides/platforms/device-verification.md`.
24
-
25
- ## Agent Rules
26
-
27
- ### Security (IRON-SOLID)
28
- - Sensitive data MUST use `EncryptedStorage`
29
- - NEVER store secrets in `SharedPreferencesStorage`
30
- - `androidx.security:security-crypto` version MUST be `1.1.0` (stable) — not alpha
31
-
32
- ### SQL Safety (IRON-SOLID)
33
- - ALWAYS use parameterized queries
34
- - NEVER concatenate user input into SQL strings
35
-
36
- ### Filesystem Safety
37
- - NEVER read from `strata_storage/.staging/` — staging files are transient
38
- - Key sanitisation must be consistent between `set`, `get`, `remove`, and `keys`
39
-
40
- ### Plugin Architecture
41
- - Methods exposed through `StrataStoragePlugin.java`
42
- - Each storage backend is a separate Java class
43
- - Two package paths: `com.stratastorage` (plugin) and `com.strata.storage` (impl)
44
-
45
- ### Build
46
- - `compileSdkVersion 34` required for `androidx.security 1.1.0`
47
- - Verify Gradle build succeeds after any change
48
-
49
- ### Before Modifying
50
- - Understand Capacitor plugin protocol for Android
51
- - Test on Android emulator after changes; follow device-verification guide