strata-storage 2.6.1 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/AI-INTEGRATION-GUIDE.md +129 -11
  2. package/README.md +42 -64
  3. package/android/AGENTS.md +18 -3
  4. package/android/CLAUDE.md +36 -3
  5. package/android/build.gradle +5 -1
  6. package/android/src/main/java/com/strata/storage/EncryptedStorage.java +31 -18
  7. package/android/src/main/java/com/strata/storage/FilesystemStorage.java +62 -10
  8. package/android/src/main/java/com/strata/storage/SQLiteStorage.java +65 -13
  9. package/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +63 -51
  10. package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +80 -11
  11. package/dist/README.md +42 -64
  12. package/dist/adapters/capacitor/FilesystemAdapter.d.ts +2 -2
  13. package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
  14. package/dist/adapters/capacitor/FilesystemAdapter.js +6 -2
  15. package/dist/adapters/capacitor/PreferencesAdapter.d.ts +2 -2
  16. package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
  17. package/dist/adapters/capacitor/PreferencesAdapter.js +6 -2
  18. package/dist/adapters/capacitor/SecureAdapter.d.ts +3 -3
  19. package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
  20. package/dist/adapters/capacitor/SecureAdapter.js +6 -2
  21. package/dist/adapters/capacitor/SqliteAdapter.d.ts +24 -3
  22. package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
  23. package/dist/adapters/capacitor/SqliteAdapter.js +105 -23
  24. package/dist/adapters/capacitor/index.d.ts +4 -4
  25. package/dist/adapters/web/CacheAdapter.d.ts +2 -2
  26. package/dist/adapters/web/CacheAdapter.d.ts.map +1 -1
  27. package/dist/adapters/web/CacheAdapter.js +20 -5
  28. package/dist/adapters/web/CookieAdapter.d.ts +2 -2
  29. package/dist/adapters/web/CookieAdapter.d.ts.map +1 -1
  30. package/dist/adapters/web/CookieAdapter.js +27 -3
  31. package/dist/adapters/web/IndexedDBAdapter.d.ts +10 -3
  32. package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
  33. package/dist/adapters/web/IndexedDBAdapter.js +19 -4
  34. package/dist/adapters/web/LocalStorageAdapter.d.ts +2 -2
  35. package/dist/adapters/web/LocalStorageAdapter.d.ts.map +1 -1
  36. package/dist/adapters/web/LocalStorageAdapter.js +15 -1
  37. package/dist/adapters/web/MemoryAdapter.d.ts +9 -2
  38. package/dist/adapters/web/MemoryAdapter.d.ts.map +1 -1
  39. package/dist/adapters/web/MemoryAdapter.js +22 -5
  40. package/dist/adapters/web/SessionStorageAdapter.d.ts +10 -10
  41. package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
  42. package/dist/adapters/web/SessionStorageAdapter.js +17 -3
  43. package/dist/adapters/web/URLAdapter.d.ts +2 -2
  44. package/dist/adapters/web/index.d.ts +7 -7
  45. package/dist/capacitor.d.ts +6 -6
  46. package/dist/capacitor.d.ts.map +1 -1
  47. package/dist/capacitor.js +5 -4
  48. package/dist/core/AdapterRegistry.d.ts +1 -1
  49. package/dist/core/BaseAdapter.d.ts +21 -5
  50. package/dist/core/BaseAdapter.d.ts.map +1 -1
  51. package/dist/core/BaseAdapter.js +29 -1
  52. package/dist/core/Strata.d.ts +53 -7
  53. package/dist/core/Strata.d.ts.map +1 -1
  54. package/dist/core/Strata.js +581 -125
  55. package/dist/features/compression/index.d.ts +1 -1
  56. package/dist/features/compression.d.ts +28 -9
  57. package/dist/features/compression.d.ts.map +1 -1
  58. package/dist/features/compression.js +164 -77
  59. package/dist/features/encryption/index.d.ts +1 -1
  60. package/dist/features/encryption.d.ts +55 -2
  61. package/dist/features/encryption.d.ts.map +1 -1
  62. package/dist/features/encryption.js +139 -26
  63. package/dist/features/migration.d.ts +1 -1
  64. package/dist/features/migration.d.ts.map +1 -1
  65. package/dist/features/migration.js +7 -2
  66. package/dist/features/observer/index.d.ts +1 -1
  67. package/dist/features/observer.d.ts +1 -1
  68. package/dist/features/query/index.d.ts +1 -1
  69. package/dist/features/query.d.ts +2 -1
  70. package/dist/features/query.d.ts.map +1 -1
  71. package/dist/features/query.js +32 -7
  72. package/dist/features/sync/index.d.ts +1 -1
  73. package/dist/features/sync.d.ts +19 -3
  74. package/dist/features/sync.d.ts.map +1 -1
  75. package/dist/features/sync.js +75 -12
  76. package/dist/features/ttl/index.d.ts +1 -1
  77. package/dist/features/ttl.d.ts +2 -2
  78. package/dist/features/ttl.d.ts.map +1 -1
  79. package/dist/features/ttl.js +4 -2
  80. package/dist/firebase.d.ts +1 -1
  81. package/dist/firebase.d.ts.map +1 -1
  82. package/dist/firebase.js +72 -10
  83. package/dist/index.d.ts +24 -22
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +6 -1
  86. package/dist/integrations/angular/index.d.ts +2 -2
  87. package/dist/integrations/angular/index.d.ts.map +1 -1
  88. package/dist/integrations/angular/index.js +7 -8
  89. package/dist/integrations/react/index.d.ts +3 -3
  90. package/dist/integrations/react/index.d.ts.map +1 -1
  91. package/dist/integrations/react/index.js +16 -4
  92. package/dist/integrations/vue/index.d.ts +2 -2
  93. package/dist/integrations/vue/index.d.ts.map +1 -1
  94. package/dist/integrations/vue/index.js +5 -2
  95. package/dist/package.json +11 -2
  96. package/dist/plugin/definitions.d.ts +29 -3
  97. package/dist/plugin/definitions.d.ts.map +1 -1
  98. package/dist/plugin/index.d.ts +2 -2
  99. package/dist/plugin/index.d.ts.map +1 -1
  100. package/dist/plugin/index.js +3 -0
  101. package/dist/plugin/web.d.ts +11 -2
  102. package/dist/plugin/web.d.ts.map +1 -1
  103. package/dist/plugin/web.js +23 -4
  104. package/dist/types/index.d.ts +50 -20
  105. package/dist/types/index.d.ts.map +1 -1
  106. package/dist/types/index.js +1 -10
  107. package/dist/utils/index.d.ts +5 -0
  108. package/dist/utils/index.d.ts.map +1 -1
  109. package/dist/utils/index.js +7 -0
  110. package/ios/AGENTS.md +14 -3
  111. package/ios/CLAUDE.md +25 -3
  112. package/ios/Plugin/FilesystemStorage.swift +19 -0
  113. package/ios/Plugin/SQLiteStorage.swift +59 -9
  114. package/ios/Plugin/StrataStoragePlugin.m +1 -0
  115. package/ios/Plugin/StrataStoragePlugin.swift +87 -34
  116. package/ios/Plugin/UserDefaultsStorage.swift +47 -46
  117. package/package.json +27 -24
  118. package/scripts/build.js +46 -27
  119. package/scripts/cli.js +1 -1
  120. package/scripts/configure.js +2 -2
  121. package/scripts/postinstall.js +3 -2
  122. package/dist/android/AGENTS.md +0 -51
  123. package/dist/android/CLAUDE.md +0 -89
  124. package/dist/android/build.gradle +0 -58
  125. package/dist/android/proguard-rules.pro +0 -11
  126. package/dist/android/settings.gradle +0 -2
  127. package/dist/android/src/main/AndroidManifest.xml +0 -2
  128. package/dist/android/src/main/java/com/strata/storage/EncryptedStorage.java +0 -218
  129. package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +0 -287
  130. package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +0 -371
  131. package/dist/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +0 -209
  132. package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +0 -616
  133. package/dist/core/StorageStrategy.d.ts +0 -55
  134. package/dist/core/StorageStrategy.d.ts.map +0 -1
  135. package/dist/core/StorageStrategy.js +0 -200
  136. package/dist/ios/AGENTS.md +0 -48
  137. package/dist/ios/CLAUDE.md +0 -84
  138. package/dist/ios/Plugin/FilesystemStorage.swift +0 -218
  139. package/dist/ios/Plugin/KeychainStorage.swift +0 -233
  140. package/dist/ios/Plugin/SQLiteStorage.swift +0 -426
  141. package/dist/ios/Plugin/StrataStoragePlugin.m +0 -23
  142. package/dist/ios/Plugin/StrataStoragePlugin.swift +0 -411
  143. package/dist/ios/Plugin/UserDefaultsStorage.swift +0 -87
@@ -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.1",
3
+ "version": "2.7.1",
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.15",
107
- "@angular/core": "^21.2.15",
108
- "@angular/forms": "^21.2.15",
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
123
  "eslint-plugin-prettier": "^5.5.6",
118
- "jsdom": "^29.1.1",
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",
128
+ "typescript-eslint": "^8.60.1",
125
129
  "vue": "^3.5.35",
126
- "zone.js": "~0.16.0"
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,27 +72,48 @@ 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
75
83
  .replace(/from\s+["']\.\/utils["']/g, `from "./utils/index.js"`)
76
84
  .replace(/from\s+["']\.\/plugin["']/g, `from "./plugin/index.js"`);
77
85
 
78
- // Add .js extension to relative imports
86
+ // Add the correct extension to extensionless relative imports.
87
+ // Directory-aware: a barrel import like './types' must resolve to
88
+ // './types/index.js', a file import like './core/Strata' to
89
+ // './core/Strata.js'. tsc has already emitted every .js by now, so we
90
+ // resolve against the real dist files instead of blindly appending '.js'
91
+ // (the previous naive appender produced a dead './types.js' that broke
92
+ // type resolution for every TS consumer).
93
+ const fileDir = path.dirname(filePath);
94
+ const resolveRel = (importPath) => {
95
+ if (importPath.endsWith('.js') || importPath.endsWith('.json')) {
96
+ return importPath;
97
+ }
98
+ const abs = path.resolve(fileDir, importPath);
99
+ if (fs.existsSync(`${abs}.js`)) {
100
+ return `${importPath}.js`;
101
+ }
102
+ if (fs.existsSync(path.join(abs, 'index.js'))) {
103
+ return `${importPath.replace(/\/+$/, '')}/index.js`;
104
+ }
105
+ // Fall back to the legacy behavior if nothing matched on disk.
106
+ return `${importPath}.js`;
107
+ };
79
108
  content = content
80
- .replace(/from\s+["'](\.[^"']+)["']/g, (match, path) => {
81
- if (!path.endsWith('.js') && !path.endsWith('.json')) {
82
- return `from "${path}.js"`;
83
- }
84
- return match;
85
- })
86
- .replace(/import\(["'](\.[^"']+)["']\)/g, (match, path) => {
87
- if (!path.endsWith('.js') && !path.endsWith('.json')) {
88
- return `import("${path}.js")`;
89
- }
90
- return match;
91
- });
109
+ .replace(
110
+ /from\s+["'](\.[^"']+)["']/g,
111
+ (_match, p) => `from "${resolveRel(p)}"`,
112
+ )
113
+ .replace(
114
+ /import\(["'](\.[^"']+)["']\)/g,
115
+ (_match, p) => `import("${resolveRel(p)}")`,
116
+ );
92
117
 
93
118
  fs.writeFileSync(filePath, content);
94
119
  }
@@ -97,17 +122,6 @@ const fixPathAliases = (dir) => {
97
122
 
98
123
  fixPathAliases(distDir);
99
124
 
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
125
  // Copy other files
112
126
  console.log('📄 Copying additional files...');
113
127
  const filesToCopy = ['README.md', 'LICENSE'];
@@ -153,11 +167,16 @@ const distPackageJson = {
153
167
  './angular': {
154
168
  types: './integrations/angular/index.d.ts',
155
169
  default: './integrations/angular/index.js'
156
- }
170
+ },
171
+ './package.json': './package.json'
157
172
  },
158
173
  author: packageJson.author,
159
174
  license: packageJson.license,
160
175
  repository: packageJson.repository,
176
+ // Carry metadata-only fields through so the dist manifest never drifts from
177
+ // the root manifest (bugs/homepage were previously dropped).
178
+ bugs: packageJson.bugs,
179
+ homepage: packageJson.homepage,
161
180
  keywords: packageJson.keywords,
162
181
  peerDependencies: packageJson.peerDependencies,
163
182
  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');