strata-storage 2.5.0 → 2.6.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.
@@ -8,10 +8,11 @@ import com.getcapacitor.PluginMethod;
8
8
  import com.getcapacitor.annotation.CapacitorPlugin;
9
9
  import com.strata.storage.SharedPreferencesStorage;
10
10
  import com.strata.storage.EncryptedStorage;
11
+ import com.strata.storage.FilesystemStorage;
11
12
  import com.strata.storage.SQLiteStorage;
12
13
  import android.util.Log;
13
14
  import org.json.JSONArray;
14
- import org.json.JSONException;
15
+ import java.util.HashMap;
15
16
  import java.util.List;
16
17
  import java.util.Map;
17
18
 
@@ -19,15 +20,24 @@ import java.util.Map;
19
20
  * Main Capacitor plugin for Strata Storage
20
21
  * Coordinates between different storage types on Android.
21
22
  *
22
- * Storage types with a real native backend: preferences, secure, sqlite.
23
- * `filesystem` is NOT implemented natively and is rejected explicitly rather
24
- * than silently falling through to SharedPreferences.
23
+ * Storage types with a real native backend: preferences, secure, sqlite,
24
+ * filesystem.
25
+ *
26
+ * SQLite supports multi-store: the `database` option selects the DB file and
27
+ * `table` selects the table within it. SQLite helper instances are cached per
28
+ * database name so the connection is not reopened per call.
25
29
  */
26
30
  @CapacitorPlugin(name = "StrataStorage")
27
31
  public class StrataStoragePlugin extends Plugin {
32
+ private static final String DEFAULT_DATABASE = "strata_storage";
33
+ private static final String DEFAULT_TABLE = "storage";
34
+
28
35
  private SharedPreferencesStorage sharedPrefsStorage;
29
36
  private EncryptedStorage encryptedStorage;
30
- private SQLiteStorage sqliteStorage;
37
+ private FilesystemStorage filesystemStorage;
38
+
39
+ /** SQLite helper cache keyed by resolved database name (without ".db"). */
40
+ private final Map<String, SQLiteStorage> sqliteStores = new HashMap<>();
31
41
 
32
42
  @Override
33
43
  public void load() {
@@ -44,18 +54,43 @@ public class StrataStoragePlugin extends Plugin {
44
54
  Log.e("StrataStorage", "Failed to initialize encrypted storage", e);
45
55
  }
46
56
  try {
47
- sqliteStorage = new SQLiteStorage(getContext());
57
+ filesystemStorage = new FilesystemStorage(getContext());
48
58
  } catch (Exception e) {
49
- Log.e("StrataStorage", "Failed to initialize SQLite storage", e);
59
+ Log.e("StrataStorage", "Failed to initialize filesystem storage", e);
50
60
  }
51
61
  }
52
62
 
53
- private void rejectUnsupportedFilesystem(PluginCall call) {
54
- call.reject(
55
- "Filesystem storage is not implemented in the native Android plugin. " +
56
- "Use the 'preferences', 'secure', or 'sqlite' storage types, or a " +
57
- "web/Capacitor Filesystem-backed adapter."
58
- );
63
+ /**
64
+ * Sanitize a database name to a safe file stem ({@code ^[A-Za-z0-9_]+$})
65
+ * and return the resolved stem. The ".db" suffix is appended when opening.
66
+ */
67
+ private static String sanitizeDatabase(String database) {
68
+ if (database == null || database.isEmpty()) {
69
+ return DEFAULT_DATABASE;
70
+ }
71
+ String cleaned = database.replaceAll("[^A-Za-z0-9_]", "");
72
+ return cleaned.isEmpty() ? DEFAULT_DATABASE : cleaned;
73
+ }
74
+
75
+ /**
76
+ * Return (creating + caching on first use) the SQLite helper for the given
77
+ * database name. The cache is keyed by the sanitized DB stem so the
78
+ * connection is reused across bridge calls. May return {@code null} if the
79
+ * helper cannot be constructed.
80
+ */
81
+ private synchronized SQLiteStorage getSqliteStore(String database) {
82
+ String dbStem = sanitizeDatabase(database);
83
+ SQLiteStorage store = sqliteStores.get(dbStem);
84
+ if (store == null) {
85
+ try {
86
+ store = new SQLiteStorage(getContext(), dbStem + ".db");
87
+ sqliteStores.put(dbStem, store);
88
+ } catch (Exception e) {
89
+ Log.e("StrataStorage", "Failed to open SQLite database " + dbStem, e);
90
+ return null;
91
+ }
92
+ }
93
+ return store;
59
94
  }
60
95
 
61
96
  /**
@@ -71,11 +106,10 @@ public class StrataStoragePlugin extends Plugin {
71
106
  available = encryptedStorage != null;
72
107
  break;
73
108
  case "sqlite":
74
- available = sqliteStorage != null;
109
+ available = getSqliteStore(call.getString("database", DEFAULT_DATABASE)) != null;
75
110
  break;
76
111
  case "filesystem":
77
- // No native filesystem backend.
78
- available = false;
112
+ available = filesystemStorage != null;
79
113
  break;
80
114
  case "preferences":
81
115
  default:
@@ -89,7 +123,10 @@ public class StrataStoragePlugin extends Plugin {
89
123
  }
90
124
 
91
125
  /**
92
- * Get value from storage
126
+ * Get value from storage.
127
+ *
128
+ * For sqlite/filesystem the resolved `value` is the full wrapper object
129
+ * (parsed back from JSON). A miss resolves `value` = null.
93
130
  */
94
131
  @PluginMethod
95
132
  public void get(PluginCall call) {
@@ -102,46 +139,70 @@ public class StrataStoragePlugin extends Plugin {
102
139
  String storage = call.getString("storage", "preferences");
103
140
 
104
141
  try {
105
- Object value = null;
106
-
107
142
  switch (storage) {
108
- case "secure":
143
+ case "secure": {
109
144
  if (encryptedStorage == null) {
110
145
  call.reject("Encrypted storage not available");
111
146
  return;
112
147
  }
113
- value = encryptedStorage.get(key);
114
- break;
115
- case "sqlite":
116
- if (sqliteStorage == null) {
148
+ resolveValue(call, encryptedStorage.get(key));
149
+ return;
150
+ }
151
+ case "sqlite": {
152
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
153
+ if (store == null) {
117
154
  call.reject("SQLite storage not available");
118
155
  return;
119
156
  }
120
- value = sqliteStorage.get(key);
121
- break;
122
- case "filesystem":
123
- rejectUnsupportedFilesystem(call);
157
+ String table = call.getString("table", DEFAULT_TABLE);
158
+ resolveValue(call, store.get(table, key));
124
159
  return;
160
+ }
161
+ case "filesystem": {
162
+ if (filesystemStorage == null) {
163
+ call.reject("Filesystem storage not available");
164
+ return;
165
+ }
166
+ resolveValue(call, filesystemStorage.get(key));
167
+ return;
168
+ }
125
169
  case "preferences":
126
- default:
170
+ default: {
127
171
  if (sharedPrefsStorage == null) {
128
172
  call.reject("Preferences storage not available");
129
173
  return;
130
174
  }
131
- value = sharedPrefsStorage.get(key);
132
- break;
175
+ resolveValue(call, sharedPrefsStorage.get(key));
176
+ return;
177
+ }
133
178
  }
134
-
135
- JSObject result = new JSObject();
136
- result.put("value", value);
137
- call.resolve(result);
138
179
  } catch (Exception e) {
139
180
  call.reject("Failed to get value", e);
140
181
  }
141
182
  }
142
183
 
143
184
  /**
144
- * Set value in storage
185
+ * Resolve a get() call, mapping a Java {@code null} to a JS {@code null}
186
+ * value so the TS adapters treat it as a miss.
187
+ */
188
+ private void resolveValue(PluginCall call, Object value) {
189
+ JSObject result = new JSObject();
190
+ if (value == null) {
191
+ result.put("value", JSObject.NULL);
192
+ } else {
193
+ result.put("value", value);
194
+ }
195
+ call.resolve(result);
196
+ }
197
+
198
+ /**
199
+ * Set value in storage.
200
+ *
201
+ * For sqlite/filesystem the `value` is the FULL wrapper object
202
+ * ({ value, created, updated, expires?, tags?, metadata? }); it is read via
203
+ * call.getObject("value") and stored verbatim (with TTL/query columns
204
+ * extracted for sqlite). For preferences/secure the raw value is forwarded
205
+ * unchanged.
145
206
  */
146
207
  @PluginMethod
147
208
  public void set(PluginCall call) {
@@ -151,7 +212,6 @@ public class StrataStoragePlugin extends Plugin {
151
212
  return;
152
213
  }
153
214
 
154
- Object value = call.getData().opt("value");
155
215
  String storage = call.getString("storage", "preferences");
156
216
 
157
217
  try {
@@ -162,25 +222,42 @@ public class StrataStoragePlugin extends Plugin {
162
222
  call.reject("Encrypted storage not available");
163
223
  return;
164
224
  }
165
- ok = encryptedStorage.set(key, value);
225
+ ok = encryptedStorage.set(key, call.getData().opt("value"));
166
226
  break;
167
- case "sqlite":
168
- if (sqliteStorage == null) {
227
+ case "sqlite": {
228
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
229
+ if (store == null) {
169
230
  call.reject("SQLite storage not available");
170
231
  return;
171
232
  }
172
- ok = sqliteStorage.set(key, value);
233
+ JSObject wrapper = call.getObject("value");
234
+ if (wrapper == null) {
235
+ call.reject("A wrapper object 'value' is required for sqlite storage");
236
+ return;
237
+ }
238
+ ok = store.set(call.getString("table", DEFAULT_TABLE), key, wrapper);
173
239
  break;
174
- case "filesystem":
175
- rejectUnsupportedFilesystem(call);
176
- return;
240
+ }
241
+ case "filesystem": {
242
+ if (filesystemStorage == null) {
243
+ call.reject("Filesystem storage not available");
244
+ return;
245
+ }
246
+ JSObject wrapper = call.getObject("value");
247
+ if (wrapper == null) {
248
+ call.reject("A wrapper object 'value' is required for filesystem storage");
249
+ return;
250
+ }
251
+ ok = filesystemStorage.set(key, wrapper);
252
+ break;
253
+ }
177
254
  case "preferences":
178
255
  default:
179
256
  if (sharedPrefsStorage == null) {
180
257
  call.reject("Preferences storage not available");
181
258
  return;
182
259
  }
183
- ok = sharedPrefsStorage.set(key, value);
260
+ ok = sharedPrefsStorage.set(key, call.getData().opt("value"));
184
261
  break;
185
262
  }
186
263
 
@@ -216,16 +293,22 @@ public class StrataStoragePlugin extends Plugin {
216
293
  }
217
294
  encryptedStorage.remove(key);
218
295
  break;
219
- case "sqlite":
220
- if (sqliteStorage == null) {
296
+ case "sqlite": {
297
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
298
+ if (store == null) {
221
299
  call.reject("SQLite storage not available");
222
300
  return;
223
301
  }
224
- sqliteStorage.remove(key);
302
+ store.remove(call.getString("table", DEFAULT_TABLE), key);
225
303
  break;
304
+ }
226
305
  case "filesystem":
227
- rejectUnsupportedFilesystem(call);
228
- return;
306
+ if (filesystemStorage == null) {
307
+ call.reject("Filesystem storage not available");
308
+ return;
309
+ }
310
+ filesystemStorage.remove(key);
311
+ break;
229
312
  case "preferences":
230
313
  default:
231
314
  if (sharedPrefsStorage == null) {
@@ -263,16 +346,22 @@ public class StrataStoragePlugin extends Plugin {
263
346
  }
264
347
  encryptedStorage.clear(prefix);
265
348
  break;
266
- case "sqlite":
267
- if (sqliteStorage == null) {
349
+ case "sqlite": {
350
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
351
+ if (store == null) {
268
352
  call.reject("SQLite storage not available");
269
353
  return;
270
354
  }
271
- sqliteStorage.clear(prefix);
355
+ store.clear(call.getString("table", DEFAULT_TABLE), prefix);
272
356
  break;
357
+ }
273
358
  case "filesystem":
274
- rejectUnsupportedFilesystem(call);
275
- return;
359
+ if (filesystemStorage == null) {
360
+ call.reject("Filesystem storage not available");
361
+ return;
362
+ }
363
+ filesystemStorage.clear(prefix);
364
+ break;
276
365
  case "preferences":
277
366
  default:
278
367
  if (sharedPrefsStorage == null) {
@@ -298,7 +387,7 @@ public class StrataStoragePlugin extends Plugin {
298
387
  String pattern = call.getString("pattern");
299
388
 
300
389
  try {
301
- List<String> keys = null;
390
+ List<String> keys;
302
391
 
303
392
  switch (storage) {
304
393
  case "secure":
@@ -308,16 +397,22 @@ public class StrataStoragePlugin extends Plugin {
308
397
  }
309
398
  keys = encryptedStorage.keys(pattern);
310
399
  break;
311
- case "sqlite":
312
- if (sqliteStorage == null) {
400
+ case "sqlite": {
401
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
402
+ if (store == null) {
313
403
  call.reject("SQLite storage not available");
314
404
  return;
315
405
  }
316
- keys = sqliteStorage.keys(pattern);
406
+ keys = store.keys(call.getString("table", DEFAULT_TABLE), pattern);
317
407
  break;
408
+ }
318
409
  case "filesystem":
319
- rejectUnsupportedFilesystem(call);
320
- return;
410
+ if (filesystemStorage == null) {
411
+ call.reject("Filesystem storage not available");
412
+ return;
413
+ }
414
+ keys = filesystemStorage.keys(pattern);
415
+ break;
321
416
  case "preferences":
322
417
  default:
323
418
  if (sharedPrefsStorage == null) {
@@ -337,17 +432,21 @@ public class StrataStoragePlugin extends Plugin {
337
432
  }
338
433
 
339
434
  /**
340
- * Get storage size information
435
+ * Get storage size information.
436
+ *
437
+ * When `detailed` is true the result also carries a byte breakdown
438
+ * { detailed: { keys, values, metadata } }.
341
439
  */
342
440
  @PluginMethod
343
441
  public void size(PluginCall call) {
344
442
  String storage = call.getString("storage", "preferences");
443
+ boolean detailed = Boolean.TRUE.equals(call.getBoolean("detailed", false));
345
444
 
346
445
  try {
347
446
  JSObject result = new JSObject();
348
447
 
349
448
  switch (storage) {
350
- case "secure":
449
+ case "secure": {
351
450
  if (encryptedStorage == null) {
352
451
  call.reject("Encrypted storage not available");
353
452
  return;
@@ -356,20 +455,29 @@ public class StrataStoragePlugin extends Plugin {
356
455
  result.put("total", encryptedSizeInfo.total);
357
456
  result.put("count", encryptedSizeInfo.count);
358
457
  break;
359
- case "sqlite":
360
- if (sqliteStorage == null) {
458
+ }
459
+ case "sqlite": {
460
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
461
+ if (store == null) {
361
462
  call.reject("SQLite storage not available");
362
463
  return;
363
464
  }
364
- SQLiteStorage.SizeInfo sqliteSizeInfo = sqliteStorage.size();
365
- result.put("total", sqliteSizeInfo.total);
366
- result.put("count", sqliteSizeInfo.count);
465
+ SQLiteStorage.SizeInfo sqliteSizeInfo =
466
+ store.size(call.getString("table", DEFAULT_TABLE), detailed);
467
+ putSizeInfo(result, sqliteSizeInfo);
367
468
  break;
368
- case "filesystem":
369
- rejectUnsupportedFilesystem(call);
370
- return;
469
+ }
470
+ case "filesystem": {
471
+ if (filesystemStorage == null) {
472
+ call.reject("Filesystem storage not available");
473
+ return;
474
+ }
475
+ SQLiteStorage.SizeInfo fsSizeInfo = filesystemStorage.size(detailed);
476
+ putSizeInfo(result, fsSizeInfo);
477
+ break;
478
+ }
371
479
  case "preferences":
372
- default:
480
+ default: {
373
481
  if (sharedPrefsStorage == null) {
374
482
  call.reject("Preferences storage not available");
375
483
  return;
@@ -378,6 +486,7 @@ public class StrataStoragePlugin extends Plugin {
378
486
  result.put("total", prefsSizeInfo.total);
379
487
  result.put("count", prefsSizeInfo.count);
380
488
  break;
489
+ }
381
490
  }
382
491
 
383
492
  call.resolve(result);
@@ -386,12 +495,27 @@ public class StrataStoragePlugin extends Plugin {
386
495
  }
387
496
  }
388
497
 
498
+ /**
499
+ * Populate a size result, including the detailed byte breakdown when the
500
+ * {@link SQLiteStorage.SizeInfo} carries one.
501
+ */
502
+ private void putSizeInfo(JSObject result, SQLiteStorage.SizeInfo info) {
503
+ result.put("total", info.total);
504
+ result.put("count", info.count);
505
+ if (info.detailed) {
506
+ JSObject breakdown = new JSObject();
507
+ breakdown.put("keys", info.keysBytes);
508
+ breakdown.put("values", info.valuesBytes);
509
+ breakdown.put("metadata", info.metadataBytes);
510
+ result.put("detailed", breakdown);
511
+ }
512
+ }
513
+
389
514
  /**
390
515
  * Query SQLite-backed storage.
391
516
  * Matches the optional `query` method in the JS contract:
392
- * resolves { results: [{ key, value }] }. The JS SqliteAdapter applies the
393
- * real condition by re-fetching/filtering, so this enumerates candidate
394
- * rows.
517
+ * resolves { results: [{ key }] }. The JS SqliteAdapter applies the real
518
+ * condition by re-fetching/filtering, so this enumerates candidate keys.
395
519
  */
396
520
  @PluginMethod
397
521
  public void query(PluginCall call) {
@@ -400,18 +524,18 @@ public class StrataStoragePlugin extends Plugin {
400
524
  call.reject("Query is only supported for the 'sqlite' storage type");
401
525
  return;
402
526
  }
403
- if (sqliteStorage == null) {
527
+ SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
528
+ if (store == null) {
404
529
  call.reject("SQLite storage not available");
405
530
  return;
406
531
  }
407
532
 
408
533
  try {
409
- List<Map<String, Object>> rows = sqliteStorage.query();
534
+ List<Map<String, Object>> rows = store.query(call.getString("table", DEFAULT_TABLE));
410
535
  JSArray results = new JSArray();
411
536
  for (Map<String, Object> row : rows) {
412
537
  JSObject obj = new JSObject();
413
538
  obj.put("key", row.get("key"));
414
- obj.put("value", row.get("value"));
415
539
  results.put(obj);
416
540
  }
417
541
  JSObject result = new JSObject();
package/dist/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
10
10
  [![Platform](https://img.shields.io/badge/platform-Web%20%7C%20iOS%20%7C%20Android-lightgrey.svg)](https://github.com/aoneahsan/strata-storage)
11
11
 
12
- - **Version:** `2.5.0`
12
+ - **Version:** `2.6.1`
13
13
  - **License:** Apache-2.0
14
14
  - **Node.js:** `>= 24.13.0`
15
15
  - **Module format:** ESM only
@@ -390,15 +390,22 @@ const active = await storage.query({
390
390
 
391
391
  ### iOS and Android (via Capacitor)
392
392
 
393
- Register native adapters yourself when running under Capacitor:
393
+ Register the native adapters you need when running under Capacitor. All four are zero-runtime-dependency: SQLite is hand-rolled (no plugin dependency) and filesystem uses the platform's native `FileManager` / `java.io.File`.
394
394
 
395
395
  ```typescript
396
396
  import { defineStorage } from 'strata-storage';
397
- import { PreferencesAdapter, SecureStorageAdapter } from 'strata-storage/capacitor';
397
+ import {
398
+ PreferencesAdapter,
399
+ SecureStorageAdapter,
400
+ SqliteAdapter,
401
+ FilesystemAdapter,
402
+ } from 'strata-storage/capacitor';
398
403
 
399
404
  const storage = defineStorage();
400
- storage.registerAdapter(new PreferencesAdapter()); // UserDefaults / SharedPreferences
405
+ storage.registerAdapter(new PreferencesAdapter()); // UserDefaults / SharedPreferences
401
406
  storage.registerAdapter(new SecureStorageAdapter()); // Keychain / EncryptedSharedPreferences
407
+ storage.registerAdapter(new SqliteAdapter()); // native SQLite
408
+ storage.registerAdapter(new FilesystemAdapter()); // native files
402
409
 
403
410
  await storage.set('secret', token, { storage: 'secure' });
404
411
  ```
@@ -407,10 +414,25 @@ await storage.set('secret', token, { storage: 'secure' });
407
414
  |---------|-------------|-----------------|
408
415
  | `preferences` | UserDefaults | SharedPreferences |
409
416
  | `secure` | Keychain | EncryptedSharedPreferences |
410
- | `sqlite` | SQLite | SQLite |
411
- | `filesystem` | FileManager | Filesystem |
417
+ | `sqlite` | SQLite (multi-store) | SQLite (multi-store) |
418
+ | `filesystem` | FileManager | java.io.File |
412
419
 
413
- > **Honest note:** the native iOS/Android adapters depend on your downstream Capacitor project setup and platform configuration. Behavior on a real device should be verified on-device — native storage cannot be exercised by the web/Node test suite.
420
+ **SQLite multi-store** (2.6.0+): each `SqliteAdapter` instance binds to a `(database, table)` pair, so distinct logical stores map to distinct physical SQLite files / tables and cannot collide.
421
+
422
+ ```typescript
423
+ import { SqliteAdapter } from 'strata-storage/capacitor';
424
+
425
+ const analytics = defineStorage();
426
+ analytics.registerAdapter(new SqliteAdapter({ database: 'analytics', table: 'events' }));
427
+
428
+ const audit = defineStorage();
429
+ audit.registerAdapter(new SqliteAdapter({ database: 'audit', table: 'rows' }));
430
+ // → separate physical .db files; writes to `analytics` can never bleed into `audit`.
431
+ ```
432
+
433
+ `await storage.size(true)` aggregates `{ total, count, byStorage, ... }`; native SQLite and filesystem additionally report a per-column byte breakdown (keys / values / metadata) when called on those adapters directly.
434
+
435
+ > **Honest note:** the native iOS/Android adapters depend on your downstream Capacitor project setup and platform configuration, and native behavior cannot be exercised by the web/Node test suite. Follow [`docs/guides/platforms/device-verification.md`](./docs/guides/platforms/device-verification.md) to verify on a real iOS and Android device after integrating.
414
436
 
415
437
  ### Firebase (optional cloud sync)
416
438
 
@@ -436,8 +458,8 @@ await storage.set('data', value, { storage: 'firestore' });
436
458
  | `url` | Web | ✅ | async only | Shareable UI state, length-limited |
437
459
  | `preferences` | Mobile | ❌ | async only | UserDefaults / SharedPreferences |
438
460
  | `secure` | Mobile | ❌ | async only | Keychain / EncryptedSharedPreferences |
439
- | `sqlite` | Mobile | ❌ | async only | Native SQLite |
440
- | `filesystem` | Mobile | ❌ | async only | Native files |
461
+ | `sqlite` | Mobile | ❌ | async only | Native SQLite — multi-store via `(database, table)` (2.6.0+) |
462
+ | `filesystem` | Mobile | ❌ | async only | Native files — file-per-key with atomic writes (2.6.0+) |
441
463
 
442
464
  "async only" means encryption and compression require the `await storage.set(...)` path — the synchronous API cannot encrypt or compress.
443
465
 
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md — android/
2
2
 
3
- Last Updated: 2026-04-03
3
+ Last Updated: 2026-05-27
4
4
 
5
5
  > Agent instructions for Android native development.
6
6
 
@@ -8,27 +8,44 @@ Last Updated: 2026-04-03
8
8
 
9
9
  | File | Purpose |
10
10
  |------|---------|
11
- | `StrataStoragePlugin.java` | Capacitor plugin entry (com.stratastorage) |
12
- | `SharedPreferencesStorage.java` | General key-value storage (com.strata.storage) |
13
- | `EncryptedStorage.java` | Secure storage (com.strata.storage) |
14
- | `SQLiteStorage.java` | Database storage (com.strata.storage) |
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`.
15
24
 
16
25
  ## Agent Rules
17
26
 
18
27
  ### Security (IRON-SOLID)
19
28
  - Sensitive data MUST use `EncryptedStorage`
20
29
  - NEVER store secrets in `SharedPreferencesStorage`
30
+ - `androidx.security:security-crypto` version MUST be `1.1.0` (stable) — not alpha
21
31
 
22
32
  ### SQL Safety (IRON-SOLID)
23
33
  - ALWAYS use parameterized queries
24
34
  - NEVER concatenate user input into SQL strings
25
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
+
26
40
  ### Plugin Architecture
27
41
  - Methods exposed through `StrataStoragePlugin.java`
28
42
  - Each storage backend is a separate Java class
29
43
  - Two package paths: `com.stratastorage` (plugin) and `com.strata.storage` (impl)
30
44
 
45
+ ### Build
46
+ - `compileSdkVersion 34` required for `androidx.security 1.1.0`
47
+ - Verify Gradle build succeeds after any change
48
+
31
49
  ### Before Modifying
32
50
  - Understand Capacitor plugin protocol for Android
33
- - Test on Android emulator after changes
34
- - Verify Gradle build succeeds
51
+ - Test on Android emulator after changes; follow device-verification guide