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.
- package/AI-INTEGRATION-GUIDE.md +97 -10
- package/README.md +70 -70
- package/android/AGENTS.md +18 -3
- package/android/CLAUDE.md +36 -3
- package/android/build.gradle +5 -1
- package/android/src/main/java/com/strata/storage/EncryptedStorage.java +31 -18
- package/android/src/main/java/com/strata/storage/FilesystemStorage.java +62 -10
- package/android/src/main/java/com/strata/storage/SQLiteStorage.java +65 -13
- package/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +63 -51
- package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +80 -11
- package/dist/README.md +70 -70
- package/dist/adapters/capacitor/FilesystemAdapter.d.ts +2 -2
- package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/FilesystemAdapter.js +6 -2
- package/dist/adapters/capacitor/PreferencesAdapter.d.ts +2 -2
- package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/PreferencesAdapter.js +6 -2
- package/dist/adapters/capacitor/SecureAdapter.d.ts +3 -3
- package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/SecureAdapter.js +6 -2
- package/dist/adapters/capacitor/SqliteAdapter.d.ts +24 -3
- package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/SqliteAdapter.js +105 -23
- package/dist/adapters/capacitor/index.d.ts +4 -4
- package/dist/adapters/web/CacheAdapter.d.ts +2 -2
- package/dist/adapters/web/CookieAdapter.d.ts +2 -2
- package/dist/adapters/web/IndexedDBAdapter.d.ts +10 -3
- package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
- package/dist/adapters/web/IndexedDBAdapter.js +14 -3
- package/dist/adapters/web/LocalStorageAdapter.d.ts +2 -2
- package/dist/adapters/web/MemoryAdapter.d.ts +2 -2
- package/dist/adapters/web/SessionStorageAdapter.d.ts +10 -10
- package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
- package/dist/adapters/web/SessionStorageAdapter.js +3 -2
- package/dist/adapters/web/URLAdapter.d.ts +2 -2
- package/dist/adapters/web/index.d.ts +7 -7
- package/dist/capacitor.d.ts +6 -6
- package/dist/capacitor.d.ts.map +1 -1
- package/dist/capacitor.js +5 -4
- package/dist/core/AdapterRegistry.d.ts +1 -1
- package/dist/core/BaseAdapter.d.ts +21 -5
- package/dist/core/BaseAdapter.d.ts.map +1 -1
- package/dist/core/BaseAdapter.js +29 -1
- package/dist/core/Strata.d.ts +41 -7
- package/dist/core/Strata.d.ts.map +1 -1
- package/dist/core/Strata.js +476 -118
- package/dist/features/compression/index.d.ts +1 -1
- package/dist/features/compression.d.ts +5 -1
- package/dist/features/compression.d.ts.map +1 -1
- package/dist/features/encryption/index.d.ts +1 -1
- package/dist/features/encryption.d.ts +55 -2
- package/dist/features/encryption.d.ts.map +1 -1
- package/dist/features/encryption.js +139 -26
- package/dist/features/migration.d.ts +1 -1
- package/dist/features/observer/index.d.ts +1 -1
- package/dist/features/observer.d.ts +1 -1
- package/dist/features/query/index.d.ts +1 -1
- package/dist/features/query.d.ts +1 -1
- package/dist/features/query.d.ts.map +1 -1
- package/dist/features/query.js +3 -1
- package/dist/features/sync/index.d.ts +1 -1
- package/dist/features/sync.d.ts +19 -3
- package/dist/features/sync.d.ts.map +1 -1
- package/dist/features/sync.js +57 -9
- package/dist/features/ttl/index.d.ts +1 -1
- package/dist/features/ttl.d.ts +2 -2
- package/dist/firebase.d.ts +1 -1
- package/dist/firebase.d.ts.map +1 -1
- package/dist/firebase.js +34 -9
- package/dist/index.d.ts +24 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/integrations/angular/index.d.ts +2 -2
- package/dist/integrations/angular/index.d.ts.map +1 -1
- package/dist/integrations/angular/index.js +7 -8
- package/dist/integrations/react/index.d.ts +3 -3
- package/dist/integrations/react/index.d.ts.map +1 -1
- package/dist/integrations/react/index.js +16 -4
- package/dist/integrations/vue/index.d.ts +2 -2
- package/dist/integrations/vue/index.d.ts.map +1 -1
- package/dist/integrations/vue/index.js +5 -2
- package/dist/package.json +11 -2
- package/dist/plugin/definitions.d.ts +29 -3
- package/dist/plugin/definitions.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +3 -0
- package/dist/plugin/web.d.ts +11 -2
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +23 -4
- package/dist/types/index.d.ts +50 -20
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -10
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +7 -0
- package/ios/AGENTS.md +14 -3
- package/ios/CLAUDE.md +25 -3
- package/ios/Plugin/FilesystemStorage.swift +19 -0
- package/ios/Plugin/SQLiteStorage.swift +59 -9
- package/ios/Plugin/StrataStoragePlugin.m +1 -0
- package/ios/Plugin/StrataStoragePlugin.swift +87 -34
- package/ios/Plugin/UserDefaultsStorage.swift +47 -46
- package/package.json +29 -26
- package/scripts/build.js +16 -14
- package/scripts/cli.js +1 -1
- package/scripts/configure.js +2 -2
- package/scripts/postinstall.js +3 -2
- package/dist/android/AGENTS.md +0 -51
- package/dist/android/CLAUDE.md +0 -89
- package/dist/android/build.gradle +0 -58
- package/dist/android/proguard-rules.pro +0 -11
- package/dist/android/settings.gradle +0 -2
- package/dist/android/src/main/AndroidManifest.xml +0 -2
- package/dist/android/src/main/java/com/strata/storage/EncryptedStorage.java +0 -218
- package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +0 -287
- package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +0 -371
- package/dist/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +0 -209
- package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +0 -616
- package/dist/core/StorageStrategy.d.ts +0 -55
- package/dist/core/StorageStrategy.d.ts.map +0 -1
- package/dist/core/StorageStrategy.js +0 -200
- package/dist/ios/AGENTS.md +0 -48
- package/dist/ios/CLAUDE.md +0 -84
- package/dist/ios/Plugin/FilesystemStorage.swift +0 -218
- package/dist/ios/Plugin/KeychainStorage.swift +0 -233
- package/dist/ios/Plugin/SQLiteStorage.swift +0 -426
- package/dist/ios/Plugin/StrataStoragePlugin.m +0 -23
- package/dist/ios/Plugin/StrataStoragePlugin.swift +0 -411
- 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
|
-
///
|
|
28
|
-
///
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
///
|
|
39
|
-
///
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
|
128
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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 =
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
61
|
+
return logical
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
for (
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
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}'
|
|
49
|
+
"lint": "eslint 'src/**/*.{ts,tsx}'",
|
|
50
|
+
"lint:fix": "eslint 'src/**/*.{ts,tsx}' --fix",
|
|
47
51
|
"typecheck": "tsc --noEmit",
|
|
48
|
-
"
|
|
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://
|
|
81
|
+
"url": "https://stratastorage.aoneahsan.com/contact"
|
|
79
82
|
},
|
|
80
|
-
"homepage": "https://
|
|
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": "^
|
|
107
|
-
"@angular/core": "^
|
|
108
|
-
"@angular/forms": "^
|
|
109
|
-
"@capacitor/core": "^8.
|
|
110
|
-
"@types/node": "^25.9.
|
|
111
|
-
"@types/react": "^19.2.
|
|
112
|
-
"@typescript-eslint/eslint-plugin": "^8.60.
|
|
113
|
-
"@typescript-eslint/parser": "^8.60.
|
|
114
|
-
"
|
|
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.
|
|
118
|
-
"jsdom": "^29.1.1",
|
|
123
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
119
124
|
"prettier": "^3.8.3",
|
|
120
|
-
"react": "^19.2.
|
|
125
|
+
"react": "^19.2.7",
|
|
121
126
|
"rxjs": "^7.8.2",
|
|
122
127
|
"typescript": "^6.0.3",
|
|
123
|
-
"typescript-eslint": "^8.60.
|
|
124
|
-
"
|
|
125
|
-
"
|
|
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
package/scripts/configure.js
CHANGED
|
@@ -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://
|
|
436
|
+
console.log('\nDocumentation: https://stratastorage-docs.aoneahsan.com');
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
// Run configuration
|
package/scripts/postinstall.js
CHANGED
|
@@ -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://
|
|
44
|
-
console.log('
|
|
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');
|
package/dist/android/AGENTS.md
DELETED
|
@@ -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
|