@buoy-gg/storage 5.0.0 → 6.0.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.
- package/README.md +45 -572
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/storage/components/GameUIStorageBrowser.js +21 -18
- package/lib/commonjs/storage/components/SelectionActionBar.js +8 -0
- package/lib/commonjs/storage/components/StorageEventCard.js +3 -2
- package/lib/commonjs/storage/components/StorageFilterCards.js +1 -2
- package/lib/commonjs/storage/components/StorageKeyRow.js +8 -3
- package/lib/commonjs/storage/hooks/useMMKVInstances.js +9 -20
- package/lib/commonjs/storage/hooks/useMMKVKeys.js +12 -21
- package/lib/commonjs/storage/hooks/useSecureStoreKeys.js +104 -0
- package/lib/commonjs/storage/hooks/useStorageEvents.js +25 -11
- package/lib/commonjs/storage/sync/storageSyncAdapter.js +82 -1
- package/lib/commonjs/storage/utils/SecureStoreRegistry.js +149 -0
- package/lib/module/index.js +5 -0
- package/lib/module/storage/components/GameUIStorageBrowser.js +20 -18
- package/lib/module/storage/components/SelectionActionBar.js +8 -0
- package/lib/module/storage/components/StorageEventCard.js +4 -3
- package/lib/module/storage/components/StorageFilterCards.js +1 -2
- package/lib/module/storage/components/StorageKeyRow.js +7 -2
- package/lib/module/storage/hooks/useMMKVInstances.js +6 -18
- package/lib/module/storage/hooks/useMMKVKeys.js +7 -18
- package/lib/module/storage/hooks/useSecureStoreKeys.js +101 -0
- package/lib/module/storage/hooks/useStorageEvents.js +25 -11
- package/lib/module/storage/sync/storageSyncAdapter.js +83 -1
- package/lib/module/storage/utils/SecureStoreRegistry.js +143 -0
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/storage/components/GameUIStorageBrowser.d.ts.map +1 -1
- package/lib/typescript/storage/components/SelectionActionBar.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageEventCard.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageFilterCards.d.ts.map +1 -1
- package/lib/typescript/storage/components/StorageKeyRow.d.ts +1 -1
- package/lib/typescript/storage/components/StorageKeyRow.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useMMKVInstances.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useMMKVKeys.d.ts.map +1 -1
- package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts +18 -0
- package/lib/typescript/storage/hooks/useSecureStoreKeys.d.ts.map +1 -0
- package/lib/typescript/storage/hooks/useStorageEvents.d.ts.map +1 -1
- package/lib/typescript/storage/sync/storageSyncAdapter.d.ts +24 -0
- package/lib/typescript/storage/sync/storageSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts +115 -0
- package/lib/typescript/storage/utils/SecureStoreRegistry.d.ts.map +1 -0
- package/package.json +6 -6
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.registerSecureStoreKeys = registerSecureStoreKeys;
|
|
7
|
+
exports.secureStoreRegistry = void 0;
|
|
8
|
+
exports.unregisterSecureStoreKey = unregisterSecureStoreKey;
|
|
9
|
+
/**
|
|
10
|
+
* Expo SecureStore Registry
|
|
11
|
+
*
|
|
12
|
+
* SecureStore (iOS Keychain / Android KeyStore) has NO key-enumeration API —
|
|
13
|
+
* unlike AsyncStorage (`getAllKeys`) and MMKV (`getAllKeys()`), there is no way
|
|
14
|
+
* to ask "what's stored?". Apps must therefore register the keys they want
|
|
15
|
+
* visible in the Storage tool.
|
|
16
|
+
*
|
|
17
|
+
* The SecureStore module itself is passed in at registration (never imported
|
|
18
|
+
* here) — the same Metro-safe pattern as `registerMMKVInstance`: this package
|
|
19
|
+
* has zero dependency on `expo-secure-store`, so bare RN apps without it
|
|
20
|
+
* bundle fine.
|
|
21
|
+
*
|
|
22
|
+
* @module SecureStoreRegistry
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Subset of expo-secure-store's SecureStoreOptions that the tool tracks.
|
|
27
|
+
* `keychainService` MUST match what the value was written with, or reads
|
|
28
|
+
* return null. `requireAuthentication` keys trigger a biometric prompt on
|
|
29
|
+
* read, so the tool never auto-reads them.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Structural type for the expo-secure-store module. Matches
|
|
36
|
+
* `import * as SecureStore from "expo-secure-store"` without depending on it.
|
|
37
|
+
*
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A key to expose in the Storage tool: either a bare key name, or a key with
|
|
43
|
+
* the SecureStore options it was written with (+ optional description shown
|
|
44
|
+
* in the expanded card).
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Normalized info for a registered SecureStore key.
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
class SecureStoreRegistry {
|
|
56
|
+
module = null;
|
|
57
|
+
keys = new Map();
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register the SecureStore module and the keys to expose. Callable multiple
|
|
61
|
+
* times — keys accumulate, the latest module wins.
|
|
62
|
+
*/
|
|
63
|
+
register(module, descriptors) {
|
|
64
|
+
this.module = module;
|
|
65
|
+
for (const d of descriptors) {
|
|
66
|
+
const entry = typeof d === "string" ? {
|
|
67
|
+
key: d,
|
|
68
|
+
options: {}
|
|
69
|
+
} : {
|
|
70
|
+
key: d.key,
|
|
71
|
+
description: d.description,
|
|
72
|
+
options: {
|
|
73
|
+
keychainService: d.keychainService,
|
|
74
|
+
requireAuthentication: d.requireAuthentication,
|
|
75
|
+
authenticationPrompt: d.authenticationPrompt
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
this.keys.set(entry.key, entry);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Remove a key from the registry (does not delete the stored value). */
|
|
83
|
+
unregister(key) {
|
|
84
|
+
this.keys.delete(key);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The registered SecureStore module, or null if none registered yet. */
|
|
88
|
+
getModule() {
|
|
89
|
+
return this.module;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** All registered key entries. */
|
|
93
|
+
getAll() {
|
|
94
|
+
return Array.from(this.keys.values());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Number of registered keys. */
|
|
98
|
+
count() {
|
|
99
|
+
return this.keys.size;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Delete a stored value via the registered module, using the options the
|
|
104
|
+
* key was registered with (required for keychainService-scoped keys).
|
|
105
|
+
*/
|
|
106
|
+
async deleteValue(key) {
|
|
107
|
+
const entry = this.keys.get(key);
|
|
108
|
+
if (!this.module || !entry) return;
|
|
109
|
+
await this.module.deleteItemAsync(key, entry.options);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Singleton registry instance (advanced usage — most apps only need
|
|
115
|
+
* {@link registerSecureStoreKeys}).
|
|
116
|
+
*/
|
|
117
|
+
const secureStoreRegistry = exports.secureStoreRegistry = new SecureStoreRegistry();
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Register Expo SecureStore keys with React Buoy DevTools.
|
|
121
|
+
*
|
|
122
|
+
* **⚠️ REQUIRED for SecureStore visibility**: SecureStore has no
|
|
123
|
+
* key-enumeration API, so the tool can only show keys you declare here.
|
|
124
|
+
* Pass the module itself so this package never has to import
|
|
125
|
+
* `expo-secure-store`.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* import * as SecureStore from "expo-secure-store";
|
|
130
|
+
* import { registerSecureStoreKeys } from "@buoy-gg/storage";
|
|
131
|
+
*
|
|
132
|
+
* registerSecureStoreKeys(SecureStore, [
|
|
133
|
+
* "auth.token",
|
|
134
|
+
* { key: "session", keychainService: "com.myapp.auth" },
|
|
135
|
+
* // Never auto-read; shown as biometric-protected:
|
|
136
|
+
* { key: "pin", requireAuthentication: true },
|
|
137
|
+
* ]);
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
function registerSecureStoreKeys(module, keys) {
|
|
141
|
+
secureStoreRegistry.register(module, keys);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Remove a key from the DevTools registry (the stored value is untouched).
|
|
146
|
+
*/
|
|
147
|
+
function unregisterSecureStoreKey(key) {
|
|
148
|
+
secureStoreRegistry.unregister(key);
|
|
149
|
+
}
|
package/lib/module/index.js
CHANGED
|
@@ -55,6 +55,11 @@ export { undoOperation, jumpToState, canUndo } from "./storage/utils/storageTime
|
|
|
55
55
|
// =============================================================================
|
|
56
56
|
export { registerMMKVInstance, unregisterMMKVInstance } from "./storage/utils/MMKVInstanceRegistry";
|
|
57
57
|
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// SECURE STORE REGISTRATION (For users to register their expo-secure-store keys)
|
|
60
|
+
// SecureStore has no key-enumeration API, so keys must be declared to appear.
|
|
61
|
+
// =============================================================================
|
|
62
|
+
export { registerSecureStoreKeys, unregisterSecureStoreKey } from "./storage/utils/SecureStoreRegistry";
|
|
58
63
|
// =============================================================================
|
|
59
64
|
// EXTERNAL SYNC (Adapter for @buoy-gg/external-sync's useExternalSync)
|
|
60
65
|
// =============================================================================
|
|
@@ -10,17 +10,13 @@ import { StorageFilterCards } from "./StorageFilterCards";
|
|
|
10
10
|
import { useAsyncStorageKeys } from "../hooks/useAsyncStorageKeys";
|
|
11
11
|
import { addListener } from "../utils/AsyncStorageListener";
|
|
12
12
|
import { useMultiMMKVKeys } from "../hooks/useMMKVKeys";
|
|
13
|
+
import { useSecureStoreKeys } from "../hooks/useSecureStoreKeys";
|
|
13
14
|
import { useMMKVInstances } from "../hooks/useMMKVInstances";
|
|
14
|
-
import { isMMKVAvailable } from "../utils/mmkvAvailability";
|
|
15
15
|
import { StorageActionButtons } from "./StorageActionButtons";
|
|
16
16
|
import { SelectionActionBar } from "./SelectionActionBar";
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
if (isMMKVAvailable()) {
|
|
21
|
-
const listener = require("../utils/MMKVListener");
|
|
22
|
-
addMMKVListener = listener.addMMKVListener;
|
|
23
|
-
}
|
|
17
|
+
// Pure JS (no react-native-mmkv dependency) — works with any registered
|
|
18
|
+
// instance, including remote-backed wrappers on the desktop dashboard.
|
|
19
|
+
import { addMMKVListener } from "../utils/MMKVListener";
|
|
24
20
|
|
|
25
21
|
// Import shared Game UI components
|
|
26
22
|
import { gameUIColors, macOSColors, HardDrive } from "@buoy-gg/shared-ui";
|
|
@@ -122,20 +118,29 @@ export function GameUIStorageBrowser({
|
|
|
122
118
|
return allMMKVKeys.filter(k => k.instanceId === selectedMMKVInstance);
|
|
123
119
|
}, [allMMKVKeys, selectedMMKVInstance]);
|
|
124
120
|
|
|
125
|
-
//
|
|
121
|
+
// Use SecureStore hook — only surfaces keys declared via registerSecureStoreKeys()
|
|
122
|
+
const {
|
|
123
|
+
storageKeys: secureStorageKeys,
|
|
124
|
+
isLoading: isLoadingSecure,
|
|
125
|
+
error: secureError,
|
|
126
|
+
refresh: refreshSecure
|
|
127
|
+
} = useSecureStoreKeys();
|
|
128
|
+
|
|
129
|
+
// Merge storage keys from AsyncStorage, MMKV, and SecureStore
|
|
126
130
|
const allStorageKeys = useMemo(() => {
|
|
127
|
-
return [...asyncStorageKeys, ...mmkvStorageKeys];
|
|
128
|
-
}, [asyncStorageKeys, mmkvStorageKeys]);
|
|
131
|
+
return [...asyncStorageKeys, ...mmkvStorageKeys, ...secureStorageKeys];
|
|
132
|
+
}, [asyncStorageKeys, mmkvStorageKeys, secureStorageKeys]);
|
|
129
133
|
|
|
130
134
|
// Determine loading and error states
|
|
131
|
-
const isLoading = isLoadingAsync || isLoadingMMKV;
|
|
132
|
-
const error = asyncError || mmkvError;
|
|
135
|
+
const isLoading = isLoadingAsync || isLoadingMMKV || isLoadingSecure;
|
|
136
|
+
const error = asyncError || mmkvError || secureError;
|
|
133
137
|
|
|
134
138
|
// Combined refresh function
|
|
135
139
|
const refresh = useCallback(() => {
|
|
136
140
|
refreshAsync();
|
|
137
141
|
refreshMMKV();
|
|
138
|
-
|
|
142
|
+
refreshSecure();
|
|
143
|
+
}, [refreshAsync, refreshMMKV, refreshSecure]);
|
|
139
144
|
|
|
140
145
|
// Generate unique identifier for a storage key
|
|
141
146
|
const getKeyIdentifier = useCallback(storageKey => {
|
|
@@ -248,11 +253,8 @@ export function GameUIStorageBrowser({
|
|
|
248
253
|
};
|
|
249
254
|
}, [refreshAsync]);
|
|
250
255
|
|
|
251
|
-
// Auto-refresh on MMKV storage events
|
|
256
|
+
// Auto-refresh on MMKV storage events
|
|
252
257
|
useEffect(() => {
|
|
253
|
-
if (!isMMKVAvailable() || !addMMKVListener) {
|
|
254
|
-
return; // Skip if MMKV not available
|
|
255
|
-
}
|
|
256
258
|
let timeoutId;
|
|
257
259
|
const unsubscribe = addMMKVListener(event => {
|
|
258
260
|
// Only refresh if event is for the selected instance
|
|
@@ -6,6 +6,7 @@ import { Trash2, CopyButton, CheckSquare, EyeOff, ProUpgradeModal } from "@buoy-
|
|
|
6
6
|
import { macOSColors } from "@buoy-gg/shared-ui";
|
|
7
7
|
import { useIsPro } from "@buoy-gg/license";
|
|
8
8
|
import { removeMany } from "../utils/asyncStorageCompat";
|
|
9
|
+
import { secureStoreRegistry } from "../utils/SecureStoreRegistry";
|
|
9
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
11
|
export function SelectionActionBar({
|
|
11
12
|
selectedKeys,
|
|
@@ -68,12 +69,19 @@ export function SelectionActionBar({
|
|
|
68
69
|
// Group keys by storage type
|
|
69
70
|
const asyncKeys = selectedKeys.filter(k => k.storageType === "async");
|
|
70
71
|
const mmkvKeys = selectedKeys.filter(k => k.storageType === "mmkv");
|
|
72
|
+
const secureKeys = selectedKeys.filter(k => k.storageType === "secure");
|
|
71
73
|
|
|
72
74
|
// Delete AsyncStorage keys
|
|
73
75
|
if (asyncKeys.length > 0) {
|
|
74
76
|
await removeMany(asyncKeys.map(k => k.key));
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
// Delete SecureStore keys (via the registry, which knows each
|
|
80
|
+
// key's keychainService options)
|
|
81
|
+
for (const k of secureKeys) {
|
|
82
|
+
await secureStoreRegistry.deleteValue(k.key);
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
// Delete MMKV keys
|
|
78
86
|
if (mmkvKeys.length > 0) {
|
|
79
87
|
// Group by instance
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { memo } from "react";
|
|
12
|
-
import {
|
|
12
|
+
import { RelativeTime, macOSColors, parseValue, CompactRow } from "@buoy-gg/shared-ui";
|
|
13
13
|
import { translateStorageAction } from "../utils/storageActionHelpers";
|
|
14
14
|
|
|
15
15
|
/** Value type for storage events */
|
|
@@ -82,7 +82,6 @@ export const StorageEventCard = /*#__PURE__*/memo(function StorageEventCard({
|
|
|
82
82
|
const actionColor = getActionColor(data.lastAction);
|
|
83
83
|
const actionLabel = translateStorageAction(data.lastAction);
|
|
84
84
|
const totalOps = data.totalOperations ?? 1;
|
|
85
|
-
const relativeTime = useRelativeTime(data.lastEventTimestamp);
|
|
86
85
|
return /*#__PURE__*/_jsx(CompactRow
|
|
87
86
|
// Status section (left) - shows "Storage" with action-colored dot
|
|
88
87
|
, {
|
|
@@ -99,7 +98,9 @@ export const StorageEventCard = /*#__PURE__*/memo(function StorageEventCard({
|
|
|
99
98
|
showChevron: showChevron
|
|
100
99
|
// Bottom right - timestamp
|
|
101
100
|
,
|
|
102
|
-
bottomRightText:
|
|
101
|
+
bottomRightText: /*#__PURE__*/_jsx(RelativeTime, {
|
|
102
|
+
timestamp: data.lastEventTimestamp
|
|
103
|
+
})
|
|
103
104
|
// Interaction
|
|
104
105
|
,
|
|
105
106
|
isSelected: isSelected,
|
|
@@ -6,7 +6,6 @@ import { isMMKVAvailable } from "../utils/mmkvAvailability";
|
|
|
6
6
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
7
7
|
// Feature flags - set to true when implemented
|
|
8
8
|
const IS_MMKV_IMPLEMENTED = true; // ✅ MMKV support is now implemented
|
|
9
|
-
const IS_SECURE_STORE_IMPLEMENTED = false;
|
|
10
9
|
|
|
11
10
|
// Runtime availability checks
|
|
12
11
|
const IS_MMKV_AVAILABLE = isMMKVAvailable();
|
|
@@ -179,7 +178,7 @@ export function StorageFilterCards({
|
|
|
179
178
|
} : {
|
|
180
179
|
color: macOSColors.text.muted
|
|
181
180
|
}],
|
|
182
|
-
children:
|
|
181
|
+
children: displayStats.secureCount
|
|
183
182
|
})]
|
|
184
183
|
})]
|
|
185
184
|
})]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { memo } from "react";
|
|
3
4
|
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
|
4
5
|
import { macOSColors, CompactRow, TypeBadge, HardDrive, Square, CheckSquare, EyeOff, Pin, ExpandedInfoRow, PillBadge } from "@buoy-gg/shared-ui";
|
|
5
6
|
import { getStorageTypeLabel } from "../utils/storageQueryUtils";
|
|
@@ -70,7 +71,11 @@ const formatValue = value => {
|
|
|
70
71
|
const str = typeof value === "string" ? value : JSON.stringify(value);
|
|
71
72
|
return str;
|
|
72
73
|
};
|
|
73
|
-
|
|
74
|
+
|
|
75
|
+
// Memo justified by profiling (tool-bench 2026-07-06): the browser tab
|
|
76
|
+
// renders every key row on each parent render (ScrollView, no
|
|
77
|
+
// virtualization) — memo keeps unchanged keys from re-rendering.
|
|
78
|
+
export const StorageKeyRow = /*#__PURE__*/memo(function StorageKeyRow({
|
|
74
79
|
storageKey,
|
|
75
80
|
isExpanded,
|
|
76
81
|
onPress,
|
|
@@ -312,7 +317,7 @@ export function StorageKeyRow({
|
|
|
312
317
|
})]
|
|
313
318
|
})]
|
|
314
319
|
});
|
|
315
|
-
}
|
|
320
|
+
});
|
|
316
321
|
const getStorageTypeColor = storageType => {
|
|
317
322
|
switch (storageType) {
|
|
318
323
|
case "mmkv":
|
|
@@ -15,14 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { useState, useEffect } from 'react';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
if (isMMKVAvailable()) {
|
|
23
|
-
const registry = require('../utils/MMKVInstanceRegistry');
|
|
24
|
-
mmkvInstanceRegistry = registry.mmkvInstanceRegistry;
|
|
25
|
-
}
|
|
18
|
+
// The registry is pure JS (no react-native-mmkv dependency) — import it
|
|
19
|
+
// unconditionally so registered instances show up even without the native
|
|
20
|
+
// module (e.g. remote-backed wrappers on the desktop dashboard).
|
|
21
|
+
import { mmkvInstanceRegistry } from '../utils/MMKVInstanceRegistry';
|
|
26
22
|
|
|
27
23
|
/**
|
|
28
24
|
* Metadata about an MMKV instance with additional runtime information
|
|
@@ -67,12 +63,6 @@ export function useMMKVInstances(autoRefresh = false) {
|
|
|
67
63
|
const fetchInstances = () => {
|
|
68
64
|
setIsLoading(true);
|
|
69
65
|
try {
|
|
70
|
-
// Guard: Check if MMKV is available
|
|
71
|
-
if (!isMMKVAvailable() || !mmkvInstanceRegistry) {
|
|
72
|
-
setInstances([]);
|
|
73
|
-
setIsLoading(false);
|
|
74
|
-
return; // Silently return empty when MMKV not available
|
|
75
|
-
}
|
|
76
66
|
const allInstances = mmkvInstanceRegistry.getAll();
|
|
77
67
|
|
|
78
68
|
// Enhance with metadata
|
|
@@ -160,8 +150,7 @@ export function useMMKVInstances(autoRefresh = false) {
|
|
|
160
150
|
export function useMMKVInstance(instanceId) {
|
|
161
151
|
const [instance, setInstance] = useState(null);
|
|
162
152
|
useEffect(() => {
|
|
163
|
-
|
|
164
|
-
if (!isMMKVAvailable() || !mmkvInstanceRegistry) {
|
|
153
|
+
if (!mmkvInstanceRegistry) {
|
|
165
154
|
setInstance(null);
|
|
166
155
|
return;
|
|
167
156
|
}
|
|
@@ -205,8 +194,7 @@ export function useMMKVInstance(instanceId) {
|
|
|
205
194
|
export function useMMKVInstanceExists(instanceId) {
|
|
206
195
|
const [exists, setExists] = useState(false);
|
|
207
196
|
useEffect(() => {
|
|
208
|
-
|
|
209
|
-
if (!isMMKVAvailable() || !mmkvInstanceRegistry) {
|
|
197
|
+
if (!mmkvInstanceRegistry) {
|
|
210
198
|
setExists(false);
|
|
211
199
|
return;
|
|
212
200
|
}
|
|
@@ -14,20 +14,14 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
17
|
-
|
|
17
|
+
// Pure-JS utilities (no react-native-mmkv dependency) — safe to import
|
|
18
|
+
// unconditionally. They operate on whatever instance objects are registered,
|
|
19
|
+
// including remote-backed wrappers on the desktop dashboard.
|
|
20
|
+
import { detectMMKVType, isTypeMatch } from '../utils/mmkvTypeDetection';
|
|
18
21
|
|
|
19
22
|
// Conditionally import MMKV types
|
|
20
|
-
|
|
21
23
|
// Use 'any' to avoid hard dependency on react-native-mmkv
|
|
22
24
|
|
|
23
|
-
// Conditionally import utilities only if MMKV is available
|
|
24
|
-
let detectMMKVType;
|
|
25
|
-
let isTypeMatch;
|
|
26
|
-
if (isMMKVAvailable()) {
|
|
27
|
-
const typeDetection = require('../utils/mmkvTypeDetection');
|
|
28
|
-
detectMMKVType = typeDetection.detectMMKVType;
|
|
29
|
-
isTypeMatch = typeDetection.isTypeMatch;
|
|
30
|
-
}
|
|
31
25
|
/**
|
|
32
26
|
* Hook to fetch and monitor keys from an MMKV instance
|
|
33
27
|
*
|
|
@@ -86,14 +80,9 @@ export function useMMKVKeys(instance, instanceId, requiredStorageKeys = []) {
|
|
|
86
80
|
setIsLoading(true);
|
|
87
81
|
setError(null);
|
|
88
82
|
try {
|
|
89
|
-
// Guard: Check if
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
setIsLoading(false);
|
|
93
|
-
return; // Silently return empty when MMKV not available
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Guard: Check if instance is provided
|
|
83
|
+
// Guard: Check if instance is provided. (No isMMKVAvailable() gate —
|
|
84
|
+
// registered instances may be remote-backed wrappers with no native
|
|
85
|
+
// module, e.g. on the desktop dashboard.)
|
|
97
86
|
if (!instance) {
|
|
98
87
|
setStorageKeys([]);
|
|
99
88
|
setIsLoading(false);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { secureStoreRegistry } from "../utils/SecureStoreRegistry";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Placeholder value shown for `requireAuthentication` keys. Reading those
|
|
8
|
+
* triggers a biometric (Face ID / fingerprint) prompt, so the tool never
|
|
9
|
+
* auto-reads them — it just shows they exist.
|
|
10
|
+
*/
|
|
11
|
+
const BIOMETRIC_PLACEHOLDER = "•••••• (biometric protected)";
|
|
12
|
+
/**
|
|
13
|
+
* Read all registered SecureStore keys into StorageKeyInfo[] for the browser.
|
|
14
|
+
*
|
|
15
|
+
* SecureStore has no enumeration API, so this only surfaces keys declared via
|
|
16
|
+
* `registerSecureStoreKeys()` (see SecureStoreRegistry). Values are read with
|
|
17
|
+
* the options each key was registered with (keychainService must match), and
|
|
18
|
+
* biometric-protected keys are listed without being read.
|
|
19
|
+
*/
|
|
20
|
+
export function useSecureStoreKeys() {
|
|
21
|
+
const [storageKeys, setStorageKeys] = useState([]);
|
|
22
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
23
|
+
const [error, setError] = useState(null);
|
|
24
|
+
const fetchSecureStoreData = useCallback(async () => {
|
|
25
|
+
const module = secureStoreRegistry.getModule();
|
|
26
|
+
const entries = secureStoreRegistry.getAll();
|
|
27
|
+
if (!module || entries.length === 0) {
|
|
28
|
+
setStorageKeys([]);
|
|
29
|
+
setIsLoading(false);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
setIsLoading(true);
|
|
33
|
+
setError(null);
|
|
34
|
+
try {
|
|
35
|
+
const keys = [];
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (entry.options.requireAuthentication) {
|
|
38
|
+
// Never auto-read: reading would fire a biometric prompt.
|
|
39
|
+
keys.push({
|
|
40
|
+
key: entry.key,
|
|
41
|
+
value: BIOMETRIC_PLACEHOLDER,
|
|
42
|
+
storageType: "secure",
|
|
43
|
+
status: "optional_present",
|
|
44
|
+
category: "optional",
|
|
45
|
+
description: entry.description ?? "Protected by biometric authentication — value not read by DevTools",
|
|
46
|
+
...(entry.options.keychainService && {
|
|
47
|
+
instanceId: entry.options.keychainService
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let rawValue = null;
|
|
53
|
+
try {
|
|
54
|
+
rawValue = await module.getItemAsync(entry.key, entry.options);
|
|
55
|
+
} catch {
|
|
56
|
+
// Key unreadable (e.g. keychain state changed) — treat as missing.
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Parse JSON values for the DataViewer, same as the AsyncStorage hook.
|
|
60
|
+
let parsedValue = rawValue;
|
|
61
|
+
if (rawValue) {
|
|
62
|
+
try {
|
|
63
|
+
parsedValue = JSON.parse(rawValue);
|
|
64
|
+
} catch {
|
|
65
|
+
parsedValue = rawValue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
keys.push({
|
|
69
|
+
key: entry.key,
|
|
70
|
+
value: rawValue === null ? undefined : parsedValue,
|
|
71
|
+
storageType: "secure",
|
|
72
|
+
status: rawValue === null ? "required_missing" : "optional_present",
|
|
73
|
+
category: rawValue === null ? "required" : "optional",
|
|
74
|
+
...(entry.description && {
|
|
75
|
+
description: entry.description
|
|
76
|
+
}),
|
|
77
|
+
...(entry.options.keychainService && {
|
|
78
|
+
instanceId: entry.options.keychainService
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
setStorageKeys(keys);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
setError(err instanceof Error ? err : new Error("Failed to read SecureStore"));
|
|
85
|
+
setStorageKeys([]);
|
|
86
|
+
} finally {
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
}
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
// Initial fetch (registration happens at app startup, before the tool opens)
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
fetchSecureStoreData();
|
|
94
|
+
}, [fetchSecureStoreData]);
|
|
95
|
+
return {
|
|
96
|
+
storageKeys,
|
|
97
|
+
isLoading,
|
|
98
|
+
error,
|
|
99
|
+
refresh: fetchSecureStoreData
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -48,24 +48,38 @@ export function useStorageEvents(options = {}) {
|
|
|
48
48
|
setIsCapturing(false);
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
|
-
const unsubscribe = storageEventStore.subscribeToEvents(allEvents => {
|
|
52
|
-
// Filter by storage type if specified
|
|
53
|
-
let filtered = allEvents;
|
|
54
|
-
if (storageType !== "all") {
|
|
55
|
-
filtered = allEvents.filter(e => e.storageType === storageType);
|
|
56
|
-
}
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
52
|
+
// Frame-coalesced (same pattern as network's useNetworkEventList):
|
|
53
|
+
// bursts of storage writes land as ONE commit per frame instead of
|
|
54
|
+
// one per emission — the dominant live-churn cost measured on the
|
|
55
|
+
// network tool (tool-bench 2026-07-06).
|
|
56
|
+
let rafId = null;
|
|
57
|
+
let pending = null;
|
|
58
|
+
const unsubscribe = storageEventStore.subscribeToEvents(allEvents => {
|
|
59
|
+
pending = allEvents;
|
|
60
|
+
if (rafId != null) return;
|
|
61
|
+
rafId = requestAnimationFrame(() => {
|
|
62
|
+
rafId = null;
|
|
63
|
+
if (!pending) return;
|
|
64
|
+
// Filter by storage type if specified
|
|
65
|
+
let filtered = pending;
|
|
66
|
+
pending = null;
|
|
67
|
+
if (storageType !== "all") {
|
|
68
|
+
filtered = filtered.filter(e => e.storageType === storageType);
|
|
69
|
+
}
|
|
70
|
+
// Limit events
|
|
71
|
+
if (filtered.length > maxEvents) {
|
|
72
|
+
filtered = filtered.slice(0, maxEvents);
|
|
73
|
+
}
|
|
74
|
+
setEvents(filtered);
|
|
75
|
+
});
|
|
63
76
|
});
|
|
64
77
|
|
|
65
78
|
// Mark as capturing after successful subscription
|
|
66
79
|
setIsCapturing(true);
|
|
67
80
|
return () => {
|
|
68
81
|
unsubscribe();
|
|
82
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
69
83
|
// Note: Don't set isCapturing=false here because we might be
|
|
70
84
|
// re-subscribing due to dependency change, not actually stopping
|
|
71
85
|
};
|
|
@@ -5,6 +5,14 @@ import { isDevToolsStorageKey } from "@buoy-gg/shared-ui";
|
|
|
5
5
|
import { readMany, writeMany, removeMany } from "../utils/asyncStorageCompat";
|
|
6
6
|
import { storageEventStore } from "../stores/storageEventStore";
|
|
7
7
|
import { clearAllAppStorage } from "../utils/clearAllStorage";
|
|
8
|
+
import { mmkvInstanceRegistry } from "../utils/MMKVInstanceRegistry";
|
|
9
|
+
import { detectMMKVType } from "../utils/mmkvTypeDetection";
|
|
10
|
+
import { secureStoreRegistry } from "../utils/SecureStoreRegistry";
|
|
11
|
+
|
|
12
|
+
/** One MMKV entry in a `mmkv.snapshot` result. */
|
|
13
|
+
|
|
14
|
+
/** One MMKV instance in a `mmkv.snapshot` result. */
|
|
15
|
+
|
|
8
16
|
/**
|
|
9
17
|
* Sync adapter for the storage tool, consumed by @buoy-gg/external-sync's
|
|
10
18
|
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
@@ -57,6 +65,80 @@ export const storageSyncAdapter = {
|
|
|
57
65
|
"async.removeItem": params => AsyncStorage.removeItem(params.key),
|
|
58
66
|
"async.multiRemove": params => removeMany(params.keys),
|
|
59
67
|
"async.multiSet": params => writeMany(params.pairs),
|
|
60
|
-
"async.clear": () => AsyncStorage.clear()
|
|
68
|
+
"async.clear": () => AsyncStorage.clear(),
|
|
69
|
+
// ── Remote MMKV proxy (desktop browse mode + MCP) ──
|
|
70
|
+
// MMKV has no remote presence off-device; these read/write the registered
|
|
71
|
+
// instances so sync clients can browse them like AsyncStorage.
|
|
72
|
+
"mmkv.snapshot": () => mmkvInstanceRegistry.getAll().map(({
|
|
73
|
+
id,
|
|
74
|
+
instance,
|
|
75
|
+
encrypted,
|
|
76
|
+
readOnly
|
|
77
|
+
}) => {
|
|
78
|
+
let entries = [];
|
|
79
|
+
try {
|
|
80
|
+
entries = instance.getAllKeys().filter(key => !isDevToolsStorageKey(key)).map(key => {
|
|
81
|
+
const {
|
|
82
|
+
value,
|
|
83
|
+
type: valueType
|
|
84
|
+
} = detectMMKVType(instance, key);
|
|
85
|
+
return {
|
|
86
|
+
key,
|
|
87
|
+
value,
|
|
88
|
+
valueType
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
} catch {
|
|
92
|
+
// Instance unreadable — return it with no entries rather than failing
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
encrypted,
|
|
97
|
+
readOnly,
|
|
98
|
+
entries
|
|
99
|
+
};
|
|
100
|
+
}),
|
|
101
|
+
"mmkv.set": params => {
|
|
102
|
+
const {
|
|
103
|
+
instanceId,
|
|
104
|
+
key,
|
|
105
|
+
value
|
|
106
|
+
} = params;
|
|
107
|
+
mmkvInstanceRegistry.get(instanceId)?.instance.set(key, value);
|
|
108
|
+
},
|
|
109
|
+
"mmkv.remove": params => {
|
|
110
|
+
const {
|
|
111
|
+
instanceId,
|
|
112
|
+
key
|
|
113
|
+
} = params;
|
|
114
|
+
const instance = mmkvInstanceRegistry.get(instanceId)?.instance;
|
|
115
|
+
// v4 renamed delete() to remove() — support both
|
|
116
|
+
if (instance?.remove) instance.remove(key);else instance?.delete?.(key);
|
|
117
|
+
},
|
|
118
|
+
// ── Remote SecureStore proxy ──
|
|
119
|
+
// Only registered keys are known (SecureStore has no enumeration API).
|
|
120
|
+
// Biometric-protected keys are never read — reads would fire an auth prompt.
|
|
121
|
+
"secure.keys": () => secureStoreRegistry.getAll().map(({
|
|
122
|
+
key,
|
|
123
|
+
options,
|
|
124
|
+
description
|
|
125
|
+
}) => ({
|
|
126
|
+
key,
|
|
127
|
+
description,
|
|
128
|
+
keychainService: options.keychainService,
|
|
129
|
+
requireAuthentication: options.requireAuthentication ?? false
|
|
130
|
+
})),
|
|
131
|
+
"secure.get": params => {
|
|
132
|
+
const {
|
|
133
|
+
key
|
|
134
|
+
} = params;
|
|
135
|
+
const entry = secureStoreRegistry.getAll().find(e => e.key === key);
|
|
136
|
+
const module = secureStoreRegistry.getModule();
|
|
137
|
+
if (!entry || !module || entry.options.requireAuthentication) {
|
|
138
|
+
return Promise.resolve(null);
|
|
139
|
+
}
|
|
140
|
+
return module.getItemAsync(key, entry.options);
|
|
141
|
+
},
|
|
142
|
+
"secure.delete": params => secureStoreRegistry.deleteValue(params.key)
|
|
61
143
|
}
|
|
62
144
|
};
|