@buoy-gg/storage 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +47 -3
  2. package/lib/commonjs/index.js +13 -0
  3. package/lib/commonjs/storage/components/GameUIStorageBrowser.js +21 -18
  4. package/lib/commonjs/storage/components/SelectionActionBar.js +8 -0
  5. package/lib/commonjs/storage/components/StorageEventCard.js +3 -2
  6. package/lib/commonjs/storage/components/StorageFilterCards.js +1 -2
  7. package/lib/commonjs/storage/components/StorageKeyRow.js +8 -3
  8. package/lib/commonjs/storage/hooks/useMMKVInstances.js +9 -20
  9. package/lib/commonjs/storage/hooks/useMMKVKeys.js +12 -21
  10. package/lib/commonjs/storage/hooks/useSecureStoreKeys.js +104 -0
  11. package/lib/commonjs/storage/hooks/useStorageEvents.js +25 -11
  12. package/lib/commonjs/storage/sync/storageSyncAdapter.js +82 -1
  13. package/lib/commonjs/storage/utils/SecureStoreRegistry.js +149 -0
  14. package/lib/module/index.js +5 -0
  15. package/lib/module/storage/components/GameUIStorageBrowser.js +20 -18
  16. package/lib/module/storage/components/SelectionActionBar.js +8 -0
  17. package/lib/module/storage/components/StorageEventCard.js +4 -3
  18. package/lib/module/storage/components/StorageFilterCards.js +1 -2
  19. package/lib/module/storage/components/StorageKeyRow.js +7 -2
  20. package/lib/module/storage/hooks/useMMKVInstances.js +6 -18
  21. package/lib/module/storage/hooks/useMMKVKeys.js +7 -18
  22. package/lib/module/storage/hooks/useSecureStoreKeys.js +101 -0
  23. package/lib/module/storage/hooks/useStorageEvents.js +25 -11
  24. package/lib/module/storage/sync/storageSyncAdapter.js +83 -1
  25. package/lib/module/storage/utils/SecureStoreRegistry.js +143 -0
  26. package/lib/typescript/index.d.ts +2 -0
  27. package/lib/typescript/index.d.ts.map +1 -1
  28. package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
  29. package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
  30. package/lib/typescript/storage/components/StorageEventCard.d.ts.map +1 -1
  31. package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +1 -1
  32. package/lib/typescript/storage/components/StorageKeyRow.d.ts +1 -1
  33. package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
  34. package/lib/typescript/storage/hooks/useMMKVInstances.d.ts.map +1 -1
  35. package/lib/typescript/storage/hooks/useMMKVKeys.d.ts.map +1 -1
  36. package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts +18 -0
  37. package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts.map +1 -0
  38. package/lib/typescript/storage/hooks/useStorageEvents.d.ts.map +1 -1
  39. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +24 -0
  40. package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -1
  41. package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts +115 -0
  42. package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts.map +1 -0
  43. package/package.json +5 -5
package/README.md CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@buoy%2Fstorage)](https://www.npmjs.com/package/@buoy/storage)
4
4
 
5
- Storage browser and monitoring tool for React Native development. Supports both AsyncStorage and MMKV.
5
+ Storage browser and monitoring tool for React Native development. Supports AsyncStorage, MMKV, and Expo SecureStore.
6
6
 
7
7
  ## Features
8
8
 
9
- - **Storage Browser**: Browse all AsyncStorage and MMKV keys and values
9
+ - **Storage Browser**: Browse all AsyncStorage, MMKV, and SecureStore keys and values
10
10
  - **MMKV Support**: Monitor and debug MMKV instances (including encrypted storage)
11
- - **Multiple Storage Types**: Support for AsyncStorage, MMKV, and multiple MMKV instances
11
+ - **SecureStore Support**: Browse and delete Expo SecureStore values (registered keys — see below)
12
+ - **Multiple Storage Types**: Support for AsyncStorage, MMKV (multiple instances), and SecureStore
12
13
  - **Live Event Monitoring**: Track all storage operations in real-time
13
14
  - **Edit & Delete**: Modify or remove storage items directly
14
15
  - **Add New Items**: Create new storage entries
@@ -16,6 +17,8 @@ Storage browser and monitoring tool for React Native development. Supports both
16
17
  - **Type Checking**: Validate storage value types
17
18
  - **Value Validation**: Check if values match expected patterns
18
19
  - **Search & Filter**: Search through keys and filter by status
20
+ - **Pin & Hide**: Pin keys you're watching to the top (persisted); hide noisy keys into the filter store with one tap
21
+ - **Inline Value Previews**: Short values and color-coded boolean badges visible without expanding cards
19
22
  - **Copy Functionality**: Easily copy keys and values
20
23
  - **Beautiful UI**: Modern, game-themed interface matching other React Buoy tools
21
24
 
@@ -327,6 +330,47 @@ storage.set('user.name', 'John Doe');
327
330
  secureStorage.set('auth.token', 'secret-token');
328
331
  ```
329
332
 
333
+ ## Expo SecureStore Support
334
+
335
+ The Storage Explorer can browse and delete Expo SecureStore values (iOS Keychain / Android KeyStore).
336
+
337
+ ### Setup Requirements
338
+
339
+ SecureStore has **no key-enumeration API** — there is no way to ask the Keychain "what's stored?". So, like MMKV instances, you register the keys you want visible. You pass the `expo-secure-store` module in, so this package has zero dependency on it (bare RN apps without Expo are unaffected).
340
+
341
+ ```typescript
342
+ import * as SecureStore from 'expo-secure-store';
343
+ import { registerSecureStoreKeys } from '@buoy/storage';
344
+
345
+ registerSecureStoreKeys(SecureStore, [
346
+ // Bare key names
347
+ 'auth.accessToken',
348
+ 'auth.refreshToken',
349
+
350
+ // Keys written with options must register the SAME options,
351
+ // or the Keychain returns null on read
352
+ { key: 'session.deviceKey', keychainService: 'com.myapp.auth' },
353
+
354
+ // Biometric-protected keys are listed but NEVER auto-read,
355
+ // so DevTools won't trigger surprise Face ID / fingerprint prompts
356
+ { key: 'secure.pin', requireAuthentication: true },
357
+ ]);
358
+ ```
359
+
360
+ ### `registerSecureStoreKeys(module, keys)`
361
+
362
+ | Param | Type | Description |
363
+ |---|---|---|
364
+ | `module` | `SecureStoreLike` | The `expo-secure-store` module (`import * as SecureStore`) |
365
+ | `keys` | `(string \| { key, keychainService?, requireAuthentication?, description? })[]` | Keys to expose, with the options they were written with |
366
+
367
+ Registered keys that have no stored value show up as **Missing** — handy for spotting initialization bugs. Use `unregisterSecureStoreKey(key)` to remove one from the tool (the stored value is untouched).
368
+
369
+ ### Platform notes
370
+
371
+ - iOS Keychain values **persist across app uninstalls** (same bundle ID); Android values don't.
372
+ - Values are read with the exact options each key was registered with — a `keychainService` mismatch reads as missing.
373
+
330
374
  ## API Reference
331
375
 
332
376
  ### Presets
@@ -147,6 +147,12 @@ Object.defineProperty(exports, "registerMMKVInstance", {
147
147
  return _MMKVInstanceRegistry.registerMMKVInstance;
148
148
  }
149
149
  });
150
+ Object.defineProperty(exports, "registerSecureStoreKeys", {
151
+ enumerable: true,
152
+ get: function () {
153
+ return _SecureStoreRegistry.registerSecureStoreKeys;
154
+ }
155
+ });
150
156
  Object.defineProperty(exports, "storageEventStore", {
151
157
  enumerable: true,
152
158
  get: function () {
@@ -177,6 +183,12 @@ Object.defineProperty(exports, "unregisterMMKVInstance", {
177
183
  return _MMKVInstanceRegistry.unregisterMMKVInstance;
178
184
  }
179
185
  });
186
+ Object.defineProperty(exports, "unregisterSecureStoreKey", {
187
+ enumerable: true,
188
+ get: function () {
189
+ return _SecureStoreRegistry.unregisterSecureStoreKey;
190
+ }
191
+ });
180
192
  Object.defineProperty(exports, "useAsyncStorageKeys", {
181
193
  enumerable: true,
182
194
  get: function () {
@@ -240,5 +252,6 @@ var _mmkvAvailability = require("./storage/utils/mmkvAvailability");
240
252
  var _mmkvTypeDetection = require("./storage/utils/mmkvTypeDetection");
241
253
  var _storageTimeTravelUtils = require("./storage/utils/storageTimeTravelUtils");
242
254
  var _MMKVInstanceRegistry = require("./storage/utils/MMKVInstanceRegistry");
255
+ var _SecureStoreRegistry = require("./storage/utils/SecureStoreRegistry");
243
256
  var _storageSyncAdapter = require("./storage/sync/storageSyncAdapter");
244
257
  var _storageEventStore = require("./storage/stores/storageEventStore");
@@ -13,18 +13,15 @@ var _StorageFilterCards = require("./StorageFilterCards");
13
13
  var _useAsyncStorageKeys = require("../hooks/useAsyncStorageKeys");
14
14
  var _AsyncStorageListener = require("../utils/AsyncStorageListener");
15
15
  var _useMMKVKeys = require("../hooks/useMMKVKeys");
16
+ var _useSecureStoreKeys = require("../hooks/useSecureStoreKeys");
16
17
  var _useMMKVInstances = require("../hooks/useMMKVInstances");
17
- var _mmkvAvailability = require("../utils/mmkvAvailability");
18
18
  var _StorageActionButtons = require("./StorageActionButtons");
19
19
  var _SelectionActionBar = require("./SelectionActionBar");
20
+ var _MMKVListener = require("../utils/MMKVListener");
20
21
  var _license = require("@buoy-gg/license");
21
22
  var _jsxRuntime = require("react/jsx-runtime");
22
- // Conditionally import MMKV listener
23
- let addMMKVListener;
24
- if ((0, _mmkvAvailability.isMMKVAvailable)()) {
25
- const listener = require("../utils/MMKVListener");
26
- addMMKVListener = listener.addMMKVListener;
27
- }
23
+ // Pure JS (no react-native-mmkv dependency) — works with any registered
24
+ // instance, including remote-backed wrappers on the desktop dashboard.
28
25
 
29
26
  // Import shared Game UI components
30
27
 
@@ -123,20 +120,29 @@ function GameUIStorageBrowser({
123
120
  return allMMKVKeys.filter(k => k.instanceId === selectedMMKVInstance);
124
121
  }, [allMMKVKeys, selectedMMKVInstance]);
125
122
 
126
- // Merge storage keys from AsyncStorage and MMKV
123
+ // Use SecureStore hook — only surfaces keys declared via registerSecureStoreKeys()
124
+ const {
125
+ storageKeys: secureStorageKeys,
126
+ isLoading: isLoadingSecure,
127
+ error: secureError,
128
+ refresh: refreshSecure
129
+ } = (0, _useSecureStoreKeys.useSecureStoreKeys)();
130
+
131
+ // Merge storage keys from AsyncStorage, MMKV, and SecureStore
127
132
  const allStorageKeys = (0, _react.useMemo)(() => {
128
- return [...asyncStorageKeys, ...mmkvStorageKeys];
129
- }, [asyncStorageKeys, mmkvStorageKeys]);
133
+ return [...asyncStorageKeys, ...mmkvStorageKeys, ...secureStorageKeys];
134
+ }, [asyncStorageKeys, mmkvStorageKeys, secureStorageKeys]);
130
135
 
131
136
  // Determine loading and error states
132
- const isLoading = isLoadingAsync || isLoadingMMKV;
133
- const error = asyncError || mmkvError;
137
+ const isLoading = isLoadingAsync || isLoadingMMKV || isLoadingSecure;
138
+ const error = asyncError || mmkvError || secureError;
134
139
 
135
140
  // Combined refresh function
136
141
  const refresh = (0, _react.useCallback)(() => {
137
142
  refreshAsync();
138
143
  refreshMMKV();
139
- }, [refreshAsync, refreshMMKV]);
144
+ refreshSecure();
145
+ }, [refreshAsync, refreshMMKV, refreshSecure]);
140
146
 
141
147
  // Generate unique identifier for a storage key
142
148
  const getKeyIdentifier = (0, _react.useCallback)(storageKey => {
@@ -249,13 +255,10 @@ function GameUIStorageBrowser({
249
255
  };
250
256
  }, [refreshAsync]);
251
257
 
252
- // Auto-refresh on MMKV storage events (only if MMKV is available)
258
+ // Auto-refresh on MMKV storage events
253
259
  (0, _react.useEffect)(() => {
254
- if (!(0, _mmkvAvailability.isMMKVAvailable)() || !addMMKVListener) {
255
- return; // Skip if MMKV not available
256
- }
257
260
  let timeoutId;
258
- const unsubscribe = addMMKVListener(event => {
261
+ const unsubscribe = (0, _MMKVListener.addMMKVListener)(event => {
259
262
  // Only refresh if event is for the selected instance
260
263
  if (event.instanceId === selectedMMKVInstance) {
261
264
  clearTimeout(timeoutId);
@@ -9,6 +9,7 @@ var _reactNative = require("react-native");
9
9
  var _sharedUi = require("@buoy-gg/shared-ui");
10
10
  var _license = require("@buoy-gg/license");
11
11
  var _asyncStorageCompat = require("../utils/asyncStorageCompat");
12
+ var _SecureStoreRegistry = require("../utils/SecureStoreRegistry");
12
13
  var _jsxRuntime = require("react/jsx-runtime");
13
14
  function SelectionActionBar({
14
15
  selectedKeys,
@@ -71,12 +72,19 @@ function SelectionActionBar({
71
72
  // Group keys by storage type
72
73
  const asyncKeys = selectedKeys.filter(k => k.storageType === "async");
73
74
  const mmkvKeys = selectedKeys.filter(k => k.storageType === "mmkv");
75
+ const secureKeys = selectedKeys.filter(k => k.storageType === "secure");
74
76
 
75
77
  // Delete AsyncStorage keys
76
78
  if (asyncKeys.length > 0) {
77
79
  await (0, _asyncStorageCompat.removeMany)(asyncKeys.map(k => k.key));
78
80
  }
79
81
 
82
+ // Delete SecureStore keys (via the registry, which knows each
83
+ // key's keychainService options)
84
+ for (const k of secureKeys) {
85
+ await _SecureStoreRegistry.secureStoreRegistry.deleteValue(k.key);
86
+ }
87
+
80
88
  // Delete MMKV keys
81
89
  if (mmkvKeys.length > 0) {
82
90
  // Group by instance
@@ -87,7 +87,6 @@ const StorageEventCard = exports.StorageEventCard = /*#__PURE__*/(0, _react.memo
87
87
  const actionColor = getActionColor(data.lastAction);
88
88
  const actionLabel = (0, _storageActionHelpers.translateStorageAction)(data.lastAction);
89
89
  const totalOps = data.totalOperations ?? 1;
90
- const relativeTime = (0, _sharedUi.useRelativeTime)(data.lastEventTimestamp);
91
90
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CompactRow
92
91
  // Status section (left) - shows "Storage" with action-colored dot
93
92
  , {
@@ -104,7 +103,9 @@ const StorageEventCard = exports.StorageEventCard = /*#__PURE__*/(0, _react.memo
104
103
  showChevron: showChevron
105
104
  // Bottom right - timestamp
106
105
  ,
107
- bottomRightText: relativeTime
106
+ bottomRightText: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.RelativeTime, {
107
+ timestamp: data.lastEventTimestamp
108
+ })
108
109
  // Interaction
109
110
  ,
110
111
  isSelected: isSelected,
@@ -10,7 +10,6 @@ var _mmkvAvailability = require("../utils/mmkvAvailability");
10
10
  var _jsxRuntime = require("react/jsx-runtime");
11
11
  // Feature flags - set to true when implemented
12
12
  const IS_MMKV_IMPLEMENTED = true; // ✅ MMKV support is now implemented
13
- const IS_SECURE_STORE_IMPLEMENTED = false;
14
13
 
15
14
  // Runtime availability checks
16
15
  const IS_MMKV_AVAILABLE = (0, _mmkvAvailability.isMMKVAvailable)();
@@ -183,7 +182,7 @@ function StorageFilterCards({
183
182
  } : {
184
183
  color: _sharedUi.macOSColors.text.muted
185
184
  }],
186
- children: "Coming Soon"
185
+ children: displayStats.secureCount
187
186
  })]
188
187
  })]
189
188
  })]
@@ -3,7 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.StorageKeyRow = StorageKeyRow;
6
+ exports.StorageKeyRow = void 0;
7
+ var _react = require("react");
7
8
  var _reactNative = require("react-native");
8
9
  var _sharedUi = require("@buoy-gg/shared-ui");
9
10
  var _storageQueryUtils = require("../utils/storageQueryUtils");
@@ -73,7 +74,11 @@ const formatValue = value => {
73
74
  const str = typeof value === "string" ? value : JSON.stringify(value);
74
75
  return str;
75
76
  };
76
- function StorageKeyRow({
77
+
78
+ // Memo justified by profiling (tool-bench 2026-07-06): the browser tab
79
+ // renders every key row on each parent render (ScrollView, no
80
+ // virtualization) — memo keeps unchanged keys from re-rendering.
81
+ const StorageKeyRow = exports.StorageKeyRow = /*#__PURE__*/(0, _react.memo)(function StorageKeyRow({
77
82
  storageKey,
78
83
  isExpanded,
79
84
  onPress,
@@ -315,7 +320,7 @@ function StorageKeyRow({
315
320
  })]
316
321
  })]
317
322
  });
318
- }
323
+ });
319
324
  const getStorageTypeColor = storageType => {
320
325
  switch (storageType) {
321
326
  case "mmkv":
@@ -7,7 +7,7 @@ exports.useMMKVInstance = useMMKVInstance;
7
7
  exports.useMMKVInstanceExists = useMMKVInstanceExists;
8
8
  exports.useMMKVInstances = useMMKVInstances;
9
9
  var _react = require("react");
10
- var _mmkvAvailability = require("../utils/mmkvAvailability");
10
+ var _MMKVInstanceRegistry = require("../utils/MMKVInstanceRegistry");
11
11
  /**
12
12
  * useMMKVInstances Hook
13
13
  *
@@ -22,12 +22,9 @@ var _mmkvAvailability = require("../utils/mmkvAvailability");
22
22
  * @see MMKVInstanceRegistry.ts for instance registration
23
23
  */
24
24
 
25
- // Conditionally import registry
26
- let mmkvInstanceRegistry;
27
- if ((0, _mmkvAvailability.isMMKVAvailable)()) {
28
- const registry = require('../utils/MMKVInstanceRegistry');
29
- mmkvInstanceRegistry = registry.mmkvInstanceRegistry;
30
- }
25
+ // The registry is pure JS (no react-native-mmkv dependency) — import it
26
+ // unconditionally so registered instances show up even without the native
27
+ // module (e.g. remote-backed wrappers on the desktop dashboard).
31
28
 
32
29
  /**
33
30
  * Metadata about an MMKV instance with additional runtime information
@@ -72,13 +69,7 @@ function useMMKVInstances(autoRefresh = false) {
72
69
  const fetchInstances = () => {
73
70
  setIsLoading(true);
74
71
  try {
75
- // Guard: Check if MMKV is available
76
- if (!(0, _mmkvAvailability.isMMKVAvailable)() || !mmkvInstanceRegistry) {
77
- setInstances([]);
78
- setIsLoading(false);
79
- return; // Silently return empty when MMKV not available
80
- }
81
- const allInstances = mmkvInstanceRegistry.getAll();
72
+ const allInstances = _MMKVInstanceRegistry.mmkvInstanceRegistry.getAll();
82
73
 
83
74
  // Enhance with metadata
84
75
  const instancesWithMetadata = allInstances.map(instanceInfo => {
@@ -165,12 +156,11 @@ function useMMKVInstances(autoRefresh = false) {
165
156
  function useMMKVInstance(instanceId) {
166
157
  const [instance, setInstance] = (0, _react.useState)(null);
167
158
  (0, _react.useEffect)(() => {
168
- // Guard: Check if MMKV is available
169
- if (!(0, _mmkvAvailability.isMMKVAvailable)() || !mmkvInstanceRegistry) {
159
+ if (!_MMKVInstanceRegistry.mmkvInstanceRegistry) {
170
160
  setInstance(null);
171
161
  return;
172
162
  }
173
- const instanceInfo = mmkvInstanceRegistry.get(instanceId);
163
+ const instanceInfo = _MMKVInstanceRegistry.mmkvInstanceRegistry.get(instanceId);
174
164
  if (!instanceInfo) {
175
165
  setInstance(null);
176
166
  return;
@@ -210,12 +200,11 @@ function useMMKVInstance(instanceId) {
210
200
  function useMMKVInstanceExists(instanceId) {
211
201
  const [exists, setExists] = (0, _react.useState)(false);
212
202
  (0, _react.useEffect)(() => {
213
- // Guard: Check if MMKV is available
214
- if (!(0, _mmkvAvailability.isMMKVAvailable)() || !mmkvInstanceRegistry) {
203
+ if (!_MMKVInstanceRegistry.mmkvInstanceRegistry) {
215
204
  setExists(false);
216
205
  return;
217
206
  }
218
- setExists(mmkvInstanceRegistry.has(instanceId));
207
+ setExists(_MMKVInstanceRegistry.mmkvInstanceRegistry.has(instanceId));
219
208
  }, [instanceId]);
220
209
  return exists;
221
210
  }
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.useMMKVKeys = useMMKVKeys;
7
7
  exports.useMultiMMKVKeys = useMultiMMKVKeys;
8
8
  var _react = require("react");
9
- var _mmkvAvailability = require("../utils/mmkvAvailability");
9
+ var _mmkvTypeDetection = require("../utils/mmkvTypeDetection");
10
10
  /**
11
11
  * useMMKVKeys Hook
12
12
  *
@@ -20,17 +20,13 @@ var _mmkvAvailability = require("../utils/mmkvAvailability");
20
20
  * - No multiGet - must fetch keys individually
21
21
  */
22
22
 
23
+ // Pure-JS utilities (no react-native-mmkv dependency) — safe to import
24
+ // unconditionally. They operate on whatever instance objects are registered,
25
+ // including remote-backed wrappers on the desktop dashboard.
26
+
23
27
  // Conditionally import MMKV types
24
28
  // Use 'any' to avoid hard dependency on react-native-mmkv
25
29
 
26
- // Conditionally import utilities only if MMKV is available
27
- let detectMMKVType;
28
- let isTypeMatch;
29
- if ((0, _mmkvAvailability.isMMKVAvailable)()) {
30
- const typeDetection = require('../utils/mmkvTypeDetection');
31
- detectMMKVType = typeDetection.detectMMKVType;
32
- isTypeMatch = typeDetection.isTypeMatch;
33
- }
34
30
  /**
35
31
  * Hook to fetch and monitor keys from an MMKV instance
36
32
  *
@@ -89,14 +85,9 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
89
85
  setIsLoading(true);
90
86
  setError(null);
91
87
  try {
92
- // Guard: Check if MMKV is available
93
- if (!(0, _mmkvAvailability.isMMKVAvailable)()) {
94
- setStorageKeys([]);
95
- setIsLoading(false);
96
- return; // Silently return empty when MMKV not available
97
- }
98
-
99
- // Guard: Check if instance is provided
88
+ // Guard: Check if instance is provided. (No isMMKVAvailable() gate —
89
+ // registered instances may be remote-backed wrappers with no native
90
+ // module, e.g. on the desktop dashboard.)
100
91
  if (!instance) {
101
92
  setStorageKeys([]);
102
93
  setIsLoading(false);
@@ -137,7 +128,7 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
137
128
  const {
138
129
  value,
139
130
  type
140
- } = detectMMKVType(instance, key);
131
+ } = (0, _mmkvTypeDetection.detectMMKVType)(instance, key);
141
132
 
142
133
  // Check if this is a required key
143
134
  const requiredConfig = requiredStorageKeys.find(req => {
@@ -155,7 +146,7 @@ function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
155
146
  status = String(value) === String(requiredConfig.expectedValue) ? 'required_present' : 'required_wrong_value';
156
147
  } else if (typeof requiredConfig === 'object' && 'expectedType' in requiredConfig) {
157
148
  // Type validation
158
- status = isTypeMatch(type, requiredConfig.expectedType) ? 'required_present' : 'required_wrong_type';
149
+ status = (0, _mmkvTypeDetection.isTypeMatch)(type, requiredConfig.expectedType) ? 'required_present' : 'required_wrong_type';
159
150
  } else {
160
151
  // Key exists and is required (no specific validation)
161
152
  status = 'required_present';
@@ -291,7 +282,7 @@ function useMultiMMKVKeys(instances, requiredStorageKeys = []) {
291
282
  const {
292
283
  value,
293
284
  type
294
- } = detectMMKVType(instance, key);
285
+ } = (0, _mmkvTypeDetection.detectMMKVType)(instance, key);
295
286
  const requiredConfig = requiredStorageKeys.find(req => {
296
287
  if (typeof req === 'string') return req === key;
297
288
  return req.key === key;
@@ -303,7 +294,7 @@ function useMultiMMKVKeys(instances, requiredStorageKeys = []) {
303
294
  } else if (typeof requiredConfig === 'object' && 'expectedValue' in requiredConfig) {
304
295
  status = String(value) === String(requiredConfig.expectedValue) ? 'required_present' : 'required_wrong_value';
305
296
  } else if (typeof requiredConfig === 'object' && 'expectedType' in requiredConfig) {
306
- status = isTypeMatch(type, requiredConfig.expectedType) ? 'required_present' : 'required_wrong_type';
297
+ status = (0, _mmkvTypeDetection.isTypeMatch)(type, requiredConfig.expectedType) ? 'required_present' : 'required_wrong_type';
307
298
  } else {
308
299
  status = 'required_present';
309
300
  }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useSecureStoreKeys = useSecureStoreKeys;
7
+ var _react = require("react");
8
+ var _SecureStoreRegistry = require("../utils/SecureStoreRegistry");
9
+ /**
10
+ * Placeholder value shown for `requireAuthentication` keys. Reading those
11
+ * triggers a biometric (Face ID / fingerprint) prompt, so the tool never
12
+ * auto-reads them — it just shows they exist.
13
+ */
14
+ const BIOMETRIC_PLACEHOLDER = "•••••• (biometric protected)";
15
+ /**
16
+ * Read all registered SecureStore keys into StorageKeyInfo[] for the browser.
17
+ *
18
+ * SecureStore has no enumeration API, so this only surfaces keys declared via
19
+ * `registerSecureStoreKeys()` (see SecureStoreRegistry). Values are read with
20
+ * the options each key was registered with (keychainService must match), and
21
+ * biometric-protected keys are listed without being read.
22
+ */
23
+ function useSecureStoreKeys() {
24
+ const [storageKeys, setStorageKeys] = (0, _react.useState)([]);
25
+ const [isLoading, setIsLoading] = (0, _react.useState)(true);
26
+ const [error, setError] = (0, _react.useState)(null);
27
+ const fetchSecureStoreData = (0, _react.useCallback)(async () => {
28
+ const module = _SecureStoreRegistry.secureStoreRegistry.getModule();
29
+ const entries = _SecureStoreRegistry.secureStoreRegistry.getAll();
30
+ if (!module || entries.length === 0) {
31
+ setStorageKeys([]);
32
+ setIsLoading(false);
33
+ return;
34
+ }
35
+ setIsLoading(true);
36
+ setError(null);
37
+ try {
38
+ const keys = [];
39
+ for (const entry of entries) {
40
+ if (entry.options.requireAuthentication) {
41
+ // Never auto-read: reading would fire a biometric prompt.
42
+ keys.push({
43
+ key: entry.key,
44
+ value: BIOMETRIC_PLACEHOLDER,
45
+ storageType: "secure",
46
+ status: "optional_present",
47
+ category: "optional",
48
+ description: entry.description ?? "Protected by biometric authentication — value not read by DevTools",
49
+ ...(entry.options.keychainService && {
50
+ instanceId: entry.options.keychainService
51
+ })
52
+ });
53
+ continue;
54
+ }
55
+ let rawValue = null;
56
+ try {
57
+ rawValue = await module.getItemAsync(entry.key, entry.options);
58
+ } catch {
59
+ // Key unreadable (e.g. keychain state changed) — treat as missing.
60
+ }
61
+
62
+ // Parse JSON values for the DataViewer, same as the AsyncStorage hook.
63
+ let parsedValue = rawValue;
64
+ if (rawValue) {
65
+ try {
66
+ parsedValue = JSON.parse(rawValue);
67
+ } catch {
68
+ parsedValue = rawValue;
69
+ }
70
+ }
71
+ keys.push({
72
+ key: entry.key,
73
+ value: rawValue === null ? undefined : parsedValue,
74
+ storageType: "secure",
75
+ status: rawValue === null ? "required_missing" : "optional_present",
76
+ category: rawValue === null ? "required" : "optional",
77
+ ...(entry.description && {
78
+ description: entry.description
79
+ }),
80
+ ...(entry.options.keychainService && {
81
+ instanceId: entry.options.keychainService
82
+ })
83
+ });
84
+ }
85
+ setStorageKeys(keys);
86
+ } catch (err) {
87
+ setError(err instanceof Error ? err : new Error("Failed to read SecureStore"));
88
+ setStorageKeys([]);
89
+ } finally {
90
+ setIsLoading(false);
91
+ }
92
+ }, []);
93
+
94
+ // Initial fetch (registration happens at app startup, before the tool opens)
95
+ (0, _react.useEffect)(() => {
96
+ fetchSecureStoreData();
97
+ }, [fetchSecureStoreData]);
98
+ return {
99
+ storageKeys,
100
+ isLoading,
101
+ error,
102
+ refresh: fetchSecureStoreData
103
+ };
104
+ }
@@ -51,24 +51,38 @@ function useStorageEvents(options = {}) {
51
51
  setIsCapturing(false);
52
52
  return;
53
53
  }
54
- const unsubscribe = _storageEventStore.storageEventStore.subscribeToEvents(allEvents => {
55
- // Filter by storage type if specified
56
- let filtered = allEvents;
57
- if (storageType !== "all") {
58
- filtered = allEvents.filter(e => e.storageType === storageType);
59
- }
60
54
 
61
- // Limit events
62
- if (filtered.length > maxEvents) {
63
- filtered = filtered.slice(0, maxEvents);
64
- }
65
- setEvents(filtered);
55
+ // Frame-coalesced (same pattern as network's useNetworkEventList):
56
+ // bursts of storage writes land as ONE commit per frame instead of
57
+ // one per emission — the dominant live-churn cost measured on the
58
+ // network tool (tool-bench 2026-07-06).
59
+ let rafId = null;
60
+ let pending = null;
61
+ const unsubscribe = _storageEventStore.storageEventStore.subscribeToEvents(allEvents => {
62
+ pending = allEvents;
63
+ if (rafId != null) return;
64
+ rafId = requestAnimationFrame(() => {
65
+ rafId = null;
66
+ if (!pending) return;
67
+ // Filter by storage type if specified
68
+ let filtered = pending;
69
+ pending = null;
70
+ if (storageType !== "all") {
71
+ filtered = filtered.filter(e => e.storageType === storageType);
72
+ }
73
+ // Limit events
74
+ if (filtered.length > maxEvents) {
75
+ filtered = filtered.slice(0, maxEvents);
76
+ }
77
+ setEvents(filtered);
78
+ });
66
79
  });
67
80
 
68
81
  // Mark as capturing after successful subscription
69
82
  setIsCapturing(true);
70
83
  return () => {
71
84
  unsubscribe();
85
+ if (rafId != null) cancelAnimationFrame(rafId);
72
86
  // Note: Don't set isCapturing=false here because we might be
73
87
  // re-subscribing due to dependency change, not actually stopping
74
88
  };