@buoy-gg/impersonate 7.0.41 → 7.0.43
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 +12 -80
- package/lib/commonjs/impersonate/components/DataNukeSettings.js +8 -675
- package/lib/commonjs/impersonate/components/ImpersonateBanner.js +1 -221
- package/lib/commonjs/impersonate/components/ImpersonateHistoryList.js +1 -181
- package/lib/commonjs/impersonate/components/ImpersonateModal.js +1 -319
- package/lib/commonjs/impersonate/components/ImpersonateOverlay.js +1 -26
- package/lib/commonjs/impersonate/components/ImpersonateStatusBar.js +1 -130
- package/lib/commonjs/impersonate/components/UserAvatar.js +1 -146
- package/lib/commonjs/impersonate/components/UserCard.js +1 -201
- package/lib/commonjs/impersonate/components/UserSearchView.js +1 -227
- package/lib/commonjs/impersonate/hooks/asyncStoragePeer.js +1 -17
- package/lib/commonjs/impersonate/hooks/asyncStoragePeer.web.js +1 -11
- package/lib/commonjs/impersonate/hooks/useAutoClearAsyncStorage.js +1 -134
- package/lib/commonjs/impersonate/hooks/useAutoClearReactQuery.js +1 -155
- package/lib/commonjs/impersonate/hooks/useAutoClearReactQuery.web.js +1 -20
- package/lib/commonjs/impersonate/hooks/useAutoClearRedux.js +1 -188
- package/lib/commonjs/impersonate/hooks/useAutoClearRedux.web.js +1 -27
- package/lib/commonjs/impersonate/hooks/useImpersonate.js +1 -215
- package/lib/commonjs/impersonate/hooks/useImpersonateHistory.js +1 -56
- package/lib/commonjs/impersonate/types/index.js +1 -16
- package/lib/commonjs/impersonate/types/types.js +1 -1
- package/lib/commonjs/impersonate/utils/impersonateListener.js +1 -350
- package/lib/commonjs/impersonate/utils/impersonateStore.js +1 -689
- package/lib/commonjs/index.js +1 -150
- package/lib/commonjs/preset.js +1 -232
- package/lib/commonjs/sync/impersonateSyncAdapter.js +1 -170
- package/lib/module/impersonate/components/DataNukeSettings.js +8 -670
- package/lib/module/impersonate/components/ImpersonateBanner.js +1 -215
- package/lib/module/impersonate/components/ImpersonateHistoryList.js +1 -176
- package/lib/module/impersonate/components/ImpersonateModal.js +1 -314
- package/lib/module/impersonate/components/ImpersonateOverlay.js +1 -21
- package/lib/module/impersonate/components/ImpersonateStatusBar.js +1 -125
- package/lib/module/impersonate/components/UserAvatar.js +1 -140
- package/lib/module/impersonate/components/UserCard.js +1 -196
- package/lib/module/impersonate/components/UserSearchView.js +1 -222
- package/lib/module/impersonate/hooks/asyncStoragePeer.js +1 -13
- package/lib/module/impersonate/hooks/asyncStoragePeer.web.js +1 -6
- package/lib/module/impersonate/hooks/useAutoClearAsyncStorage.js +1 -130
- package/lib/module/impersonate/hooks/useAutoClearReactQuery.js +1 -151
- package/lib/module/impersonate/hooks/useAutoClearReactQuery.web.js +1 -16
- package/lib/module/impersonate/hooks/useAutoClearRedux.js +1 -183
- package/lib/module/impersonate/hooks/useAutoClearRedux.web.js +1 -22
- package/lib/module/impersonate/hooks/useImpersonate.js +1 -212
- package/lib/module/impersonate/hooks/useImpersonateHistory.js +1 -52
- package/lib/module/impersonate/types/index.js +1 -3
- package/lib/module/impersonate/types/types.js +1 -1
- package/lib/module/impersonate/utils/impersonateListener.js +1 -341
- package/lib/module/impersonate/utils/impersonateStore.js +1 -686
- package/lib/module/index.js +1 -117
- package/lib/module/preset.js +1 -216
- package/lib/module/sync/impersonateSyncAdapter.js +1 -162
- package/lib/web/index.mjs +7 -8
- package/package.json +3 -6
- package/lib/typescript/impersonate/components/DataNukeSettings.d.ts.map +0 -1
- package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts.map +0 -1
- package/lib/typescript/impersonate/components/ImpersonateHistoryList.d.ts.map +0 -1
- package/lib/typescript/impersonate/components/ImpersonateModal.d.ts.map +0 -1
- package/lib/typescript/impersonate/components/ImpersonateOverlay.d.ts.map +0 -1
- package/lib/typescript/impersonate/components/ImpersonateStatusBar.d.ts.map +0 -1
- package/lib/typescript/impersonate/components/UserAvatar.d.ts.map +0 -1
- package/lib/typescript/impersonate/components/UserCard.d.ts.map +0 -1
- package/lib/typescript/impersonate/components/UserSearchView.d.ts.map +0 -1
- package/lib/typescript/impersonate/hooks/asyncStoragePeer.d.ts.map +0 -1
- package/lib/typescript/impersonate/hooks/asyncStoragePeer.web.d.ts.map +0 -1
- package/lib/typescript/impersonate/hooks/useAutoClearAsyncStorage.d.ts.map +0 -1
- package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.d.ts.map +0 -1
- package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.web.d.ts.map +0 -1
- package/lib/typescript/impersonate/hooks/useAutoClearRedux.d.ts.map +0 -1
- package/lib/typescript/impersonate/hooks/useAutoClearRedux.web.d.ts.map +0 -1
- package/lib/typescript/impersonate/hooks/useImpersonate.d.ts.map +0 -1
- package/lib/typescript/impersonate/hooks/useImpersonateHistory.d.ts.map +0 -1
- package/lib/typescript/impersonate/types/index.d.ts.map +0 -1
- package/lib/typescript/impersonate/types/types.d.ts.map +0 -1
- package/lib/typescript/impersonate/utils/impersonateListener.d.ts.map +0 -1
- package/lib/typescript/impersonate/utils/impersonateStore.d.ts.map +0 -1
- package/lib/typescript/index.d.ts.map +0 -1
- package/lib/typescript/preset.d.ts.map +0 -1
- package/lib/typescript/sync/impersonateSyncAdapter.d.ts.map +0 -1
|
@@ -1,689 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.impersonateStore = void 0;
|
|
7
|
-
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
8
|
-
var _impersonateListener = require("./impersonateListener");
|
|
9
|
-
/**
|
|
10
|
-
* Impersonate Store - State Management with Persistence
|
|
11
|
-
*
|
|
12
|
-
* Singleton store for managing impersonation state. Uses a subscription
|
|
13
|
-
* pattern compatible with React's useSyncExternalStore.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// =============================================================================
|
|
17
|
-
// CONSTANTS
|
|
18
|
-
// =============================================================================
|
|
19
|
-
|
|
20
|
-
const STORAGE_KEY = "@buoy/impersonate/state";
|
|
21
|
-
const MAX_HISTORY = 10;
|
|
22
|
-
const DEFAULT_DATA_NUKE_SETTINGS = {
|
|
23
|
-
reactQuery: true,
|
|
24
|
-
redux: true,
|
|
25
|
-
asyncStorage: false,
|
|
26
|
-
// Dangerous - default off
|
|
27
|
-
mmkv: false // Dangerous - default off
|
|
28
|
-
};
|
|
29
|
-
const DEFAULT_STATE = {
|
|
30
|
-
isActive: false,
|
|
31
|
-
isPaused: false,
|
|
32
|
-
currentUser: null,
|
|
33
|
-
headerKey: "x-impersonate-user-id",
|
|
34
|
-
ignorePatterns: [],
|
|
35
|
-
dataNukeSettings: DEFAULT_DATA_NUKE_SETTINGS,
|
|
36
|
-
showBanner: true,
|
|
37
|
-
history: []
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// =============================================================================
|
|
41
|
-
// TYPES
|
|
42
|
-
// =============================================================================
|
|
43
|
-
|
|
44
|
-
// Guard to prevent double-execution of data nuking
|
|
45
|
-
let isNuking = false;
|
|
46
|
-
|
|
47
|
-
// =============================================================================
|
|
48
|
-
// SIMPLE STORAGE ABSTRACTION
|
|
49
|
-
// =============================================================================
|
|
50
|
-
|
|
51
|
-
// Use a simple localStorage-like interface for persistence
|
|
52
|
-
// This works on both web and native (via polyfills)
|
|
53
|
-
const storage = {
|
|
54
|
-
getItem: key => {
|
|
55
|
-
try {
|
|
56
|
-
if (typeof localStorage !== "undefined") {
|
|
57
|
-
return localStorage.getItem(key);
|
|
58
|
-
}
|
|
59
|
-
// React Native: use AsyncStorage if available
|
|
60
|
-
// For now, return null (state won't persist until async storage is loaded)
|
|
61
|
-
return null;
|
|
62
|
-
} catch {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
setItem: (key, value) => {
|
|
67
|
-
try {
|
|
68
|
-
if (typeof localStorage !== "undefined") {
|
|
69
|
-
localStorage.setItem(key, value);
|
|
70
|
-
}
|
|
71
|
-
// React Native: handled separately via async methods
|
|
72
|
-
} catch {
|
|
73
|
-
// Ignore storage errors
|
|
74
|
-
}
|
|
75
|
-
},
|
|
76
|
-
removeItem: key => {
|
|
77
|
-
try {
|
|
78
|
-
if (typeof localStorage !== "undefined") {
|
|
79
|
-
localStorage.removeItem(key);
|
|
80
|
-
}
|
|
81
|
-
} catch {
|
|
82
|
-
// Ignore
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// =============================================================================
|
|
88
|
-
// STORE CLASS
|
|
89
|
-
// =============================================================================
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Singleton store for impersonation state
|
|
93
|
-
*
|
|
94
|
-
* Features:
|
|
95
|
-
* - Subscription pattern for React integration
|
|
96
|
-
* - Persistence of settings and history
|
|
97
|
-
* - Data nuking on impersonation change
|
|
98
|
-
* - Automatic sync with impersonateListener
|
|
99
|
-
*/
|
|
100
|
-
class ImpersonateStore {
|
|
101
|
-
state = {
|
|
102
|
-
...DEFAULT_STATE
|
|
103
|
-
};
|
|
104
|
-
listeners = new Set();
|
|
105
|
-
nukeCallbacks = {};
|
|
106
|
-
isInitialized = false;
|
|
107
|
-
asyncStorageRef = null;
|
|
108
|
-
storageReadyPromise = null;
|
|
109
|
-
resolveStorageReady = null;
|
|
110
|
-
developerDefaults = null;
|
|
111
|
-
constructor() {
|
|
112
|
-
// Try to load persisted state synchronously
|
|
113
|
-
this.loadFromStorage();
|
|
114
|
-
Promise.resolve().then(() => {
|
|
115
|
-
// Only a sign-out ends the session: access that was admitted and is now
|
|
116
|
-
// locked. The license reports "locked" for a moment while it boots or
|
|
117
|
-
// reloads, before validation starts, and ending the session then wiped a
|
|
118
|
-
// restored impersonation for good. The per-request gate already keeps
|
|
119
|
-
// the header off whenever access isn't admitted.
|
|
120
|
-
let wasAdmitted = false;
|
|
121
|
-
const reconcile = () => {
|
|
122
|
-
const status = (0, _sharedUi.getAccountAccess)().status;
|
|
123
|
-
if (status === "admitted") wasAdmitted = true;else if (status === "locked" && wasAdmitted) {
|
|
124
|
-
wasAdmitted = false;
|
|
125
|
-
this.disposeAccountSession();
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
(0, _sharedUi.subscribeAccountAccess)(reconcile);
|
|
129
|
-
reconcile();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// Check if localStorage is available (web) - if so, storage is ready
|
|
133
|
-
if (typeof localStorage !== "undefined") {
|
|
134
|
-
this.storageReadyPromise = Promise.resolve();
|
|
135
|
-
} else {
|
|
136
|
-
// React Native - wait for AsyncStorage to be set up
|
|
137
|
-
this.storageReadyPromise = new Promise(resolve => {
|
|
138
|
-
this.resolveStorageReady = resolve;
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ===========================================================================
|
|
144
|
-
// INITIALIZATION
|
|
145
|
-
// ===========================================================================
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Load persisted state from storage (sync)
|
|
149
|
-
*/
|
|
150
|
-
loadFromStorage() {
|
|
151
|
-
try {
|
|
152
|
-
const stored = storage.getItem(STORAGE_KEY);
|
|
153
|
-
const effectiveDefaults = this.getEffectiveDefaults();
|
|
154
|
-
if (stored) {
|
|
155
|
-
const parsed = JSON.parse(stored);
|
|
156
|
-
this.state = {
|
|
157
|
-
...DEFAULT_STATE,
|
|
158
|
-
// Use effective defaults (developer > hardcoded) as fallback
|
|
159
|
-
headerKey: parsed.headerKey ?? effectiveDefaults.headerKey,
|
|
160
|
-
ignorePatterns: parsed.ignorePatterns ?? DEFAULT_STATE.ignorePatterns,
|
|
161
|
-
dataNukeSettings: {
|
|
162
|
-
...effectiveDefaults.dataNukeSettings,
|
|
163
|
-
...parsed.dataNukeSettings
|
|
164
|
-
},
|
|
165
|
-
showBanner: parsed.showBanner ?? effectiveDefaults.showBanner,
|
|
166
|
-
history: parsed.history ?? [],
|
|
167
|
-
// Restore active session for persistence across reloads
|
|
168
|
-
isActive: parsed.isActive ?? false,
|
|
169
|
-
isPaused: parsed.isPaused ?? false,
|
|
170
|
-
currentUser: parsed.currentUser ?? null
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// If we restored an active session, sync with listener
|
|
174
|
-
if (this.state.isActive && this.state.currentUser) {
|
|
175
|
-
this.syncWithListener();
|
|
176
|
-
}
|
|
177
|
-
} else {
|
|
178
|
-
// No persisted state - use effective defaults
|
|
179
|
-
this.state = {
|
|
180
|
-
...DEFAULT_STATE,
|
|
181
|
-
headerKey: effectiveDefaults.headerKey,
|
|
182
|
-
dataNukeSettings: effectiveDefaults.dataNukeSettings,
|
|
183
|
-
showBanner: effectiveDefaults.showBanner
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
this.isInitialized = true;
|
|
187
|
-
} catch {
|
|
188
|
-
this.isInitialized = true;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Set async storage reference for persistence (call before initializeAsync)
|
|
194
|
-
* This ensures all writes use AsyncStorage even before load completes
|
|
195
|
-
*/
|
|
196
|
-
setAsyncStorage(asyncStorage) {
|
|
197
|
-
this.asyncStorageRef = asyncStorage;
|
|
198
|
-
// Resolve the storage ready promise so persist() can proceed
|
|
199
|
-
if (this.resolveStorageReady) {
|
|
200
|
-
this.resolveStorageReady();
|
|
201
|
-
this.resolveStorageReady = null;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Set developer-provided defaults
|
|
207
|
-
* These override hardcoded defaults but are overridden by persisted values
|
|
208
|
-
* Call this before the component mounts (in createImpersonateTool)
|
|
209
|
-
*/
|
|
210
|
-
setDeveloperDefaults(defaults) {
|
|
211
|
-
this.developerDefaults = defaults;
|
|
212
|
-
|
|
213
|
-
// Apply defaults to current state if not already initialized from storage
|
|
214
|
-
// Priority: persisted > developer defaults > hardcoded defaults
|
|
215
|
-
const effectiveDefaults = this.getEffectiveDefaults();
|
|
216
|
-
|
|
217
|
-
// Only apply if we haven't loaded persisted values yet
|
|
218
|
-
if (!this.isInitialized) {
|
|
219
|
-
this.state = {
|
|
220
|
-
...this.state,
|
|
221
|
-
headerKey: effectiveDefaults.headerKey,
|
|
222
|
-
dataNukeSettings: effectiveDefaults.dataNukeSettings,
|
|
223
|
-
showBanner: effectiveDefaults.showBanner
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Get effective defaults (developer defaults merged with hardcoded defaults)
|
|
230
|
-
*/
|
|
231
|
-
getEffectiveDefaults() {
|
|
232
|
-
return {
|
|
233
|
-
headerKey: this.developerDefaults?.headerKey ?? DEFAULT_STATE.headerKey,
|
|
234
|
-
dataNukeSettings: {
|
|
235
|
-
...DEFAULT_DATA_NUKE_SETTINGS,
|
|
236
|
-
...this.developerDefaults?.dataNukeSettings
|
|
237
|
-
},
|
|
238
|
-
showBanner: this.developerDefaults?.showBanner ?? DEFAULT_STATE.showBanner
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Get developer defaults (for use in UI to show what the "default" values are)
|
|
244
|
-
*/
|
|
245
|
-
getDeveloperDefaults() {
|
|
246
|
-
return this.developerDefaults;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Initialize with async storage (for React Native)
|
|
251
|
-
* Call this in useEffect to load from AsyncStorage
|
|
252
|
-
*/
|
|
253
|
-
async initializeAsync(asyncStorage) {
|
|
254
|
-
if (!asyncStorage) return;
|
|
255
|
-
|
|
256
|
-
// Store reference for future persistence (in case not already set)
|
|
257
|
-
this.asyncStorageRef = asyncStorage;
|
|
258
|
-
try {
|
|
259
|
-
const stored = await asyncStorage.getItem(STORAGE_KEY);
|
|
260
|
-
// eslint-disable-next-line no-console
|
|
261
|
-
console.log(`[impersonate] initializeAsync read STORAGE_KEY=${STORAGE_KEY} stored=${stored ? stored.slice(0, 200) : "null"}`);
|
|
262
|
-
if (stored) {
|
|
263
|
-
const parsed = JSON.parse(stored);
|
|
264
|
-
this.state = {
|
|
265
|
-
...this.state,
|
|
266
|
-
headerKey: parsed.headerKey ?? this.state.headerKey,
|
|
267
|
-
ignorePatterns: parsed.ignorePatterns ?? this.state.ignorePatterns,
|
|
268
|
-
dataNukeSettings: {
|
|
269
|
-
...this.state.dataNukeSettings,
|
|
270
|
-
...parsed.dataNukeSettings
|
|
271
|
-
},
|
|
272
|
-
showBanner: parsed.showBanner ?? this.state.showBanner,
|
|
273
|
-
history: parsed.history ?? this.state.history,
|
|
274
|
-
// Restore active session
|
|
275
|
-
isActive: parsed.isActive ?? this.state.isActive,
|
|
276
|
-
isPaused: parsed.isPaused ?? this.state.isPaused,
|
|
277
|
-
currentUser: parsed.currentUser ?? this.state.currentUser
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
// If we restored an active session, sync with listener
|
|
281
|
-
if (this.state.isActive && this.state.currentUser) {
|
|
282
|
-
this.syncWithListener();
|
|
283
|
-
}
|
|
284
|
-
this.notify();
|
|
285
|
-
}
|
|
286
|
-
} catch {
|
|
287
|
-
// Persisted blob unreadable — fall through with in-memory defaults.
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Register callbacks for data nuking
|
|
293
|
-
*/
|
|
294
|
-
registerNukeCallbacks(callbacks) {
|
|
295
|
-
this.nukeCallbacks = {
|
|
296
|
-
...this.nukeCallbacks,
|
|
297
|
-
...callbacks
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// ===========================================================================
|
|
302
|
-
// STATE ACCESS
|
|
303
|
-
// ===========================================================================
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Get current state (creates a copy)
|
|
307
|
-
*/
|
|
308
|
-
getState() {
|
|
309
|
-
return {
|
|
310
|
-
...this.state
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Get snapshot for useSyncExternalStore
|
|
316
|
-
* Returns the same reference if state hasn't changed
|
|
317
|
-
*/
|
|
318
|
-
getSnapshot = () => {
|
|
319
|
-
return this.state;
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Subscribe to state changes
|
|
324
|
-
* Returns unsubscribe function
|
|
325
|
-
*/
|
|
326
|
-
subscribe = listener => {
|
|
327
|
-
this.listeners.add(listener);
|
|
328
|
-
return () => this.listeners.delete(listener);
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
// ===========================================================================
|
|
332
|
-
// REMOTE MIRROR MODE
|
|
333
|
-
// ===========================================================================
|
|
334
|
-
|
|
335
|
-
remoteHandler = null;
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Remote mirror mode (desktop dashboard): while a handler is set, every
|
|
339
|
-
* mutation forwards (method, params) to the synced device instead of
|
|
340
|
-
* running locally — header patching, data nuking and persistence all
|
|
341
|
-
* happen on the device, and the resulting state comes back via
|
|
342
|
-
* replaceState(). Pass null to restore local behavior.
|
|
343
|
-
*/
|
|
344
|
-
setRemoteHandler(handler) {
|
|
345
|
-
this.remoteHandler = handler;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Replace the mirrored state from a synced device snapshot. Does not
|
|
350
|
-
* persist or touch the fetch listener — the device owns both.
|
|
351
|
-
*/
|
|
352
|
-
replaceState(state) {
|
|
353
|
-
this.state = state;
|
|
354
|
-
this.notify();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// ===========================================================================
|
|
358
|
-
// IMPERSONATION ACTIONS
|
|
359
|
-
// ===========================================================================
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Start impersonating a user.
|
|
363
|
-
*
|
|
364
|
-
* Order matters: we update state + listener + UI BEFORE clearing caches,
|
|
365
|
-
* so refetches triggered by the cache clear go out with the new user's
|
|
366
|
-
* impersonation header. Doing it the other way around (nuke first) makes
|
|
367
|
-
* react-query refetch active queries with the OLD identity, producing
|
|
368
|
-
* the visual symptom of "I clicked a user and nothing changed."
|
|
369
|
-
*/
|
|
370
|
-
async startImpersonation(user) {
|
|
371
|
-
(0, _sharedUi.assertAccountAccess)();
|
|
372
|
-
if (this.remoteHandler) {
|
|
373
|
-
this.remoteHandler("startImpersonation", {
|
|
374
|
-
user
|
|
375
|
-
});
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
const historyEntry = {
|
|
379
|
-
user,
|
|
380
|
-
lastUsedAt: new Date().toISOString()
|
|
381
|
-
};
|
|
382
|
-
const newHistory = [historyEntry, ...this.state.history.filter(entry => entry.user.id !== user.id)].slice(0, MAX_HISTORY);
|
|
383
|
-
this.state = {
|
|
384
|
-
...this.state,
|
|
385
|
-
isActive: true,
|
|
386
|
-
currentUser: user,
|
|
387
|
-
history: newHistory
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
// 1) Point the fetch interceptor at the new user. Any request after this
|
|
391
|
-
// line carries the new header.
|
|
392
|
-
this.syncWithListener();
|
|
393
|
-
|
|
394
|
-
// 2) Tell React subscribers right away so the UI updates without waiting
|
|
395
|
-
// on AsyncStorage / data nuke.
|
|
396
|
-
this.notify();
|
|
397
|
-
|
|
398
|
-
// 3) Nuke caches — refetches now use the new identity.
|
|
399
|
-
await this.executeDataNuke();
|
|
400
|
-
|
|
401
|
-
// 4) Persist for next launch.
|
|
402
|
-
await this.persist();
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Stop impersonating.
|
|
407
|
-
*
|
|
408
|
-
* Same ordering rationale as startImpersonation: clear state + listener
|
|
409
|
-
* before nuking caches so subsequent refetches go out without the header.
|
|
410
|
-
*/
|
|
411
|
-
async stopImpersonation() {
|
|
412
|
-
if (this.remoteHandler) {
|
|
413
|
-
this.remoteHandler("stopImpersonation");
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
// eslint-disable-next-line no-console
|
|
417
|
-
console.log(`[impersonate] stopImpersonation: isActive ${this.state.isActive} -> false, listeners=${this.listeners.size}`);
|
|
418
|
-
this.state = {
|
|
419
|
-
...this.state,
|
|
420
|
-
isActive: false,
|
|
421
|
-
isPaused: false,
|
|
422
|
-
currentUser: null
|
|
423
|
-
};
|
|
424
|
-
this.syncWithListener();
|
|
425
|
-
this.notify();
|
|
426
|
-
await this.executeDataNuke();
|
|
427
|
-
await this.persist();
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Pause impersonation (temporarily stop injecting headers)
|
|
432
|
-
*/
|
|
433
|
-
async pauseImpersonation() {
|
|
434
|
-
if (this.remoteHandler) {
|
|
435
|
-
this.remoteHandler("pauseImpersonation");
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
if (!this.state.isActive || this.state.isPaused) return;
|
|
439
|
-
this.state = {
|
|
440
|
-
...this.state,
|
|
441
|
-
isPaused: true
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
// Sync with listener (will stop injecting headers)
|
|
445
|
-
this.syncWithListener();
|
|
446
|
-
|
|
447
|
-
// Persist
|
|
448
|
-
await this.persist();
|
|
449
|
-
|
|
450
|
-
// Notify
|
|
451
|
-
this.notify();
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Resume impersonation (start injecting headers again)
|
|
456
|
-
*/
|
|
457
|
-
async resumeImpersonation() {
|
|
458
|
-
(0, _sharedUi.assertAccountAccess)();
|
|
459
|
-
if (this.remoteHandler) {
|
|
460
|
-
this.remoteHandler("resumeImpersonation");
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
if (!this.state.isActive || !this.state.isPaused) return;
|
|
464
|
-
this.state = {
|
|
465
|
-
...this.state,
|
|
466
|
-
isPaused: false
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
// Sync with listener (will resume injecting headers)
|
|
470
|
-
this.syncWithListener();
|
|
471
|
-
|
|
472
|
-
// Persist
|
|
473
|
-
await this.persist();
|
|
474
|
-
|
|
475
|
-
// Notify
|
|
476
|
-
this.notify();
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Quick switch to a user from history
|
|
481
|
-
*/
|
|
482
|
-
async quickSwitch(user) {
|
|
483
|
-
return this.startImpersonation(user);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// ===========================================================================
|
|
487
|
-
// SETTINGS
|
|
488
|
-
// ===========================================================================
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Update settings (header key, ignore patterns, data nuke settings, show banner)
|
|
492
|
-
*/
|
|
493
|
-
async updateSettings(settings) {
|
|
494
|
-
if (this.remoteHandler) {
|
|
495
|
-
this.remoteHandler("updateSettings", {
|
|
496
|
-
settings
|
|
497
|
-
});
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
this.state = {
|
|
501
|
-
...this.state,
|
|
502
|
-
headerKey: settings.headerKey ?? this.state.headerKey,
|
|
503
|
-
ignorePatterns: settings.ignorePatterns ?? this.state.ignorePatterns,
|
|
504
|
-
showBanner: settings.showBanner ?? this.state.showBanner,
|
|
505
|
-
dataNukeSettings: settings.dataNukeSettings ? {
|
|
506
|
-
...this.state.dataNukeSettings,
|
|
507
|
-
...settings.dataNukeSettings
|
|
508
|
-
} : this.state.dataNukeSettings
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
// Sync header key with listener
|
|
512
|
-
if (settings.headerKey || settings.ignorePatterns) {
|
|
513
|
-
this.syncWithListener();
|
|
514
|
-
}
|
|
515
|
-
await this.persist();
|
|
516
|
-
this.notify();
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// ===========================================================================
|
|
520
|
-
// HISTORY
|
|
521
|
-
// ===========================================================================
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* Remove a user from history
|
|
525
|
-
*/
|
|
526
|
-
async removeFromHistory(userId) {
|
|
527
|
-
if (this.remoteHandler) {
|
|
528
|
-
this.remoteHandler("removeFromHistory", {
|
|
529
|
-
userId
|
|
530
|
-
});
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
this.state = {
|
|
534
|
-
...this.state,
|
|
535
|
-
history: this.state.history.filter(entry => entry.user.id !== userId)
|
|
536
|
-
};
|
|
537
|
-
await this.persist();
|
|
538
|
-
this.notify();
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Clear all history
|
|
543
|
-
*/
|
|
544
|
-
async clearHistory() {
|
|
545
|
-
if (this.remoteHandler) {
|
|
546
|
-
this.remoteHandler("clearHistory");
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
this.state = {
|
|
550
|
-
...this.state,
|
|
551
|
-
history: []
|
|
552
|
-
};
|
|
553
|
-
await this.persist();
|
|
554
|
-
this.notify();
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// ===========================================================================
|
|
558
|
-
// PRIVATE METHODS
|
|
559
|
-
// ===========================================================================
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Execute data nuking based on current settings
|
|
563
|
-
*/
|
|
564
|
-
async executeDataNuke() {
|
|
565
|
-
// Guard against double-execution (can happen with React Strict Mode)
|
|
566
|
-
if (isNuking) {
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
isNuking = true;
|
|
570
|
-
try {
|
|
571
|
-
const {
|
|
572
|
-
dataNukeSettings
|
|
573
|
-
} = this.state;
|
|
574
|
-
const promises = [];
|
|
575
|
-
if (dataNukeSettings.reactQuery && this.nukeCallbacks.reactQuery) {
|
|
576
|
-
promises.push(Promise.resolve(this.nukeCallbacks.reactQuery()));
|
|
577
|
-
}
|
|
578
|
-
if (dataNukeSettings.redux && this.nukeCallbacks.redux) {
|
|
579
|
-
promises.push(Promise.resolve(this.nukeCallbacks.redux()));
|
|
580
|
-
}
|
|
581
|
-
if (dataNukeSettings.asyncStorage && this.nukeCallbacks.asyncStorage) {
|
|
582
|
-
promises.push(Promise.resolve(this.nukeCallbacks.asyncStorage()));
|
|
583
|
-
}
|
|
584
|
-
if (dataNukeSettings.mmkv && this.nukeCallbacks.mmkv) {
|
|
585
|
-
promises.push(Promise.resolve(this.nukeCallbacks.mmkv()));
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Wait for all, but don't fail if some error
|
|
589
|
-
await Promise.allSettled(promises);
|
|
590
|
-
} finally {
|
|
591
|
-
// Reset flag after a short delay to allow next legitimate call
|
|
592
|
-
setTimeout(() => {
|
|
593
|
-
isNuking = false;
|
|
594
|
-
}, 100);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Sync current state with the impersonateListener
|
|
600
|
-
*/
|
|
601
|
-
/** Remove only the active header override. Sign-out must not nuke host caches. */
|
|
602
|
-
disposeAccountSession() {
|
|
603
|
-
if (this.remoteHandler || !this.state.isActive) return;
|
|
604
|
-
this.state = {
|
|
605
|
-
...this.state,
|
|
606
|
-
isActive: false,
|
|
607
|
-
isPaused: false,
|
|
608
|
-
currentUser: null
|
|
609
|
-
};
|
|
610
|
-
this.syncWithListener();
|
|
611
|
-
this.notify();
|
|
612
|
-
void this.persist();
|
|
613
|
-
}
|
|
614
|
-
syncWithListener() {
|
|
615
|
-
// When paused, don't inject headers (pass null userId). Account access is
|
|
616
|
-
// checked per request by the gate, not here: a session restored at launch
|
|
617
|
-
// syncs before the account check finishes, and reading it once here sent
|
|
618
|
-
// no header for the rest of the run, until a pause and resume re-synced.
|
|
619
|
-
const shouldInject = this.state.isActive && !this.state.isPaused;
|
|
620
|
-
(0, _impersonateListener.setImpersonateConfig)({
|
|
621
|
-
headerKey: this.state.headerKey,
|
|
622
|
-
userId: shouldInject ? this.state.currentUser?.id ?? null : null,
|
|
623
|
-
ignorePatterns: this.state.ignorePatterns.map(p => new RegExp(p)),
|
|
624
|
-
gate: _sharedUi.hasAccountAccess
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
// Ensure listener is started
|
|
628
|
-
if (!(0, _impersonateListener.impersonateListener)().isListening) {
|
|
629
|
-
(0, _impersonateListener.impersonateListener)().startListening();
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Persist state to storage
|
|
635
|
-
*/
|
|
636
|
-
async persist() {
|
|
637
|
-
// Wait for storage to be ready (AsyncStorage setup on React Native)
|
|
638
|
-
if (this.storageReadyPromise) {
|
|
639
|
-
await this.storageReadyPromise;
|
|
640
|
-
}
|
|
641
|
-
const toStore = {
|
|
642
|
-
headerKey: this.state.headerKey,
|
|
643
|
-
ignorePatterns: this.state.ignorePatterns,
|
|
644
|
-
dataNukeSettings: this.state.dataNukeSettings,
|
|
645
|
-
showBanner: this.state.showBanner,
|
|
646
|
-
history: this.state.history,
|
|
647
|
-
// Persist active session for continuity across reloads
|
|
648
|
-
isActive: this.state.isActive,
|
|
649
|
-
isPaused: this.state.isPaused,
|
|
650
|
-
currentUser: this.state.currentUser
|
|
651
|
-
};
|
|
652
|
-
const serialized = JSON.stringify(toStore);
|
|
653
|
-
try {
|
|
654
|
-
// Prefer AsyncStorage for React Native
|
|
655
|
-
if (this.asyncStorageRef) {
|
|
656
|
-
await this.asyncStorageRef.setItem(STORAGE_KEY, serialized);
|
|
657
|
-
// eslint-disable-next-line no-console
|
|
658
|
-
console.log(`[impersonate] persist -> AsyncStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
|
|
659
|
-
} else {
|
|
660
|
-
// Fall back to localStorage (web)
|
|
661
|
-
storage.setItem(STORAGE_KEY, serialized);
|
|
662
|
-
// eslint-disable-next-line no-console
|
|
663
|
-
console.log(`[impersonate] persist -> localStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
|
|
664
|
-
}
|
|
665
|
-
} catch (e) {
|
|
666
|
-
// eslint-disable-next-line no-console
|
|
667
|
-
console.warn("[impersonate] persist failed", e);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/**
|
|
672
|
-
* Notify all listeners of state change
|
|
673
|
-
*/
|
|
674
|
-
notify() {
|
|
675
|
-
this.listeners.forEach(listener => {
|
|
676
|
-
try {
|
|
677
|
-
listener(this.state);
|
|
678
|
-
} catch {
|
|
679
|
-
// Ignore listener errors
|
|
680
|
-
}
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// =============================================================================
|
|
686
|
-
// SINGLETON EXPORT
|
|
687
|
-
// =============================================================================
|
|
688
|
-
|
|
689
|
-
const impersonateStore = exports.impersonateStore = new ImpersonateStore();
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.impersonateStore=void 0;var _sharedUi=require("@buoy-gg/shared-ui");var _impersonateListener=require("./impersonateListener");const STORAGE_KEY="@buoy/impersonate/state";const MAX_HISTORY=10;const DEFAULT_DATA_NUKE_SETTINGS={reactQuery:true,redux:true,asyncStorage:false,mmkv:false};const DEFAULT_STATE={isActive:false,isPaused:false,currentUser:null,headerKey:"x-impersonate-user-id",ignorePatterns:[],dataNukeSettings:DEFAULT_DATA_NUKE_SETTINGS,showBanner:true,history:[]};let isNuking=false;const storage={getItem:i=>{try{if(typeof localStorage!=="undefined"){return localStorage.getItem(i)}return null}catch{return null}},setItem:(i,e)=>{try{if(typeof localStorage!=="undefined"){localStorage.setItem(i,e)}}catch{}},removeItem:i=>{try{if(typeof localStorage!=="undefined"){localStorage.removeItem(i)}}catch{}}};class ImpersonateStore{state={...DEFAULT_STATE};listeners=new Set;nukeCallbacks={};isInitialized=false;asyncStorageRef=null;storageReadyPromise=null;resolveStorageReady=null;developerDefaults=null;constructor(){this.loadFromStorage();Promise.resolve().then(()=>{let e=false;const t=()=>{const s=(0,_sharedUi.getAccountAccess)().status;if(s==="admitted")e=true;else if(s==="locked"&&e){e=false;this.disposeAccountSession()}};(0,_sharedUi.subscribeAccountAccess)(t);t()});if(typeof localStorage!=="undefined"){this.storageReadyPromise=Promise.resolve()}else{this.storageReadyPromise=new Promise(e=>{this.resolveStorageReady=e})}}loadFromStorage(){try{const e=storage.getItem(STORAGE_KEY);const t=this.getEffectiveDefaults();if(e){const s=JSON.parse(e);this.state={...DEFAULT_STATE,headerKey:s.headerKey??t.headerKey,ignorePatterns:s.ignorePatterns??DEFAULT_STATE.ignorePatterns,dataNukeSettings:{...t.dataNukeSettings,...s.dataNukeSettings},showBanner:s.showBanner??t.showBanner,history:s.history??[],isActive:s.isActive??false,isPaused:s.isPaused??false,currentUser:s.currentUser??null};if(this.state.isActive&&this.state.currentUser){this.syncWithListener()}}else{this.state={...DEFAULT_STATE,headerKey:t.headerKey,dataNukeSettings:t.dataNukeSettings,showBanner:t.showBanner}}this.isInitialized=true}catch{this.isInitialized=true}}setAsyncStorage(e){this.asyncStorageRef=e;if(this.resolveStorageReady){this.resolveStorageReady();this.resolveStorageReady=null}}setDeveloperDefaults(e){this.developerDefaults=e;const t=this.getEffectiveDefaults();if(!this.isInitialized){this.state={...this.state,headerKey:t.headerKey,dataNukeSettings:t.dataNukeSettings,showBanner:t.showBanner}}}getEffectiveDefaults(){return{headerKey:this.developerDefaults?.headerKey??DEFAULT_STATE.headerKey,dataNukeSettings:{...DEFAULT_DATA_NUKE_SETTINGS,...this.developerDefaults?.dataNukeSettings},showBanner:this.developerDefaults?.showBanner??DEFAULT_STATE.showBanner}}getDeveloperDefaults(){return this.developerDefaults}async initializeAsync(e){if(!e)return;this.asyncStorageRef=e;try{const t=await e.getItem(STORAGE_KEY);console.log(`[impersonate] initializeAsync read STORAGE_KEY=${STORAGE_KEY} stored=${t?t.slice(0,200):"null"}`);if(t){const s=JSON.parse(t);this.state={...this.state,headerKey:s.headerKey??this.state.headerKey,ignorePatterns:s.ignorePatterns??this.state.ignorePatterns,dataNukeSettings:{...this.state.dataNukeSettings,...s.dataNukeSettings},showBanner:s.showBanner??this.state.showBanner,history:s.history??this.state.history,isActive:s.isActive??this.state.isActive,isPaused:s.isPaused??this.state.isPaused,currentUser:s.currentUser??this.state.currentUser};if(this.state.isActive&&this.state.currentUser){this.syncWithListener()}this.notify()}}catch{}}registerNukeCallbacks(e){this.nukeCallbacks={...this.nukeCallbacks,...e}}getState(){return{...this.state}}getSnapshot=()=>{return this.state};subscribe=e=>{this.listeners.add(e);return()=>this.listeners.delete(e)};remoteHandler=null;setRemoteHandler(e){this.remoteHandler=e}replaceState(e){this.state=e;this.notify()}async startImpersonation(e){(0,_sharedUi.assertAccountAccess)();if(this.remoteHandler){this.remoteHandler("startImpersonation",{user:e});return}const t={user:e,lastUsedAt:new Date().toISOString()};const s=[t,...this.state.history.filter(a=>a.user.id!==e.id)].slice(0,MAX_HISTORY);this.state={...this.state,isActive:true,currentUser:e,history:s};this.syncWithListener();this.notify();await this.executeDataNuke();await this.persist()}async stopImpersonation(){if(this.remoteHandler){this.remoteHandler("stopImpersonation");return}console.log(`[impersonate] stopImpersonation: isActive ${this.state.isActive} -> false, listeners=${this.listeners.size}`);this.state={...this.state,isActive:false,isPaused:false,currentUser:null};this.syncWithListener();this.notify();await this.executeDataNuke();await this.persist()}async pauseImpersonation(){if(this.remoteHandler){this.remoteHandler("pauseImpersonation");return}if(!this.state.isActive||this.state.isPaused)return;this.state={...this.state,isPaused:true};this.syncWithListener();await this.persist();this.notify()}async resumeImpersonation(){(0,_sharedUi.assertAccountAccess)();if(this.remoteHandler){this.remoteHandler("resumeImpersonation");return}if(!this.state.isActive||!this.state.isPaused)return;this.state={...this.state,isPaused:false};this.syncWithListener();await this.persist();this.notify()}async quickSwitch(e){return this.startImpersonation(e)}async updateSettings(e){if(this.remoteHandler){this.remoteHandler("updateSettings",{settings:e});return}this.state={...this.state,headerKey:e.headerKey??this.state.headerKey,ignorePatterns:e.ignorePatterns??this.state.ignorePatterns,showBanner:e.showBanner??this.state.showBanner,dataNukeSettings:e.dataNukeSettings?{...this.state.dataNukeSettings,...e.dataNukeSettings}:this.state.dataNukeSettings};if(e.headerKey||e.ignorePatterns){this.syncWithListener()}await this.persist();this.notify()}async removeFromHistory(e){if(this.remoteHandler){this.remoteHandler("removeFromHistory",{userId:e});return}this.state={...this.state,history:this.state.history.filter(t=>t.user.id!==e)};await this.persist();this.notify()}async clearHistory(){if(this.remoteHandler){this.remoteHandler("clearHistory");return}this.state={...this.state,history:[]};await this.persist();this.notify()}async executeDataNuke(){if(isNuking){return}isNuking=true;try{const{dataNukeSettings:e}=this.state;const t=[];if(e.reactQuery&&this.nukeCallbacks.reactQuery){t.push(Promise.resolve(this.nukeCallbacks.reactQuery()))}if(e.redux&&this.nukeCallbacks.redux){t.push(Promise.resolve(this.nukeCallbacks.redux()))}if(e.asyncStorage&&this.nukeCallbacks.asyncStorage){t.push(Promise.resolve(this.nukeCallbacks.asyncStorage()))}if(e.mmkv&&this.nukeCallbacks.mmkv){t.push(Promise.resolve(this.nukeCallbacks.mmkv()))}await Promise.allSettled(t)}finally{setTimeout(()=>{isNuking=false},100)}}disposeAccountSession(){if(this.remoteHandler||!this.state.isActive)return;this.state={...this.state,isActive:false,isPaused:false,currentUser:null};this.syncWithListener();this.notify();void this.persist()}syncWithListener(){const e=this.state.isActive&&!this.state.isPaused;(0,_impersonateListener.setImpersonateConfig)({headerKey:this.state.headerKey,userId:e?this.state.currentUser?.id??null:null,ignorePatterns:this.state.ignorePatterns.map(t=>new RegExp(t)),gate:_sharedUi.hasAccountAccess});if(!(0,_impersonateListener.impersonateListener)().isListening){(0,_impersonateListener.impersonateListener)().startListening()}}async persist(){if(this.storageReadyPromise){await this.storageReadyPromise}const e={headerKey:this.state.headerKey,ignorePatterns:this.state.ignorePatterns,dataNukeSettings:this.state.dataNukeSettings,showBanner:this.state.showBanner,history:this.state.history,isActive:this.state.isActive,isPaused:this.state.isPaused,currentUser:this.state.currentUser};const t=JSON.stringify(e);try{if(this.asyncStorageRef){await this.asyncStorageRef.setItem(STORAGE_KEY,t);console.log(`[impersonate] persist -> AsyncStorage isActive=${e.isActive} userId=${e.currentUser?.id??"null"}`)}else{storage.setItem(STORAGE_KEY,t);console.log(`[impersonate] persist -> localStorage isActive=${e.isActive} userId=${e.currentUser?.id??"null"}`)}}catch(s){console.warn("[impersonate] persist failed",s)}}notify(){this.listeners.forEach(e=>{try{e(this.state)}catch{}})}}const impersonateStore=exports.impersonateStore=new ImpersonateStore;
|