@buoy-gg/impersonate 1.0.3-beta.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 (95) hide show
  1. package/LICENSE +58 -0
  2. package/lib/commonjs/impersonate/components/DataNukeSettings.js +715 -0
  3. package/lib/commonjs/impersonate/components/ImpersonateBanner.js +217 -0
  4. package/lib/commonjs/impersonate/components/ImpersonateHistoryList.js +173 -0
  5. package/lib/commonjs/impersonate/components/ImpersonateModal.js +304 -0
  6. package/lib/commonjs/impersonate/components/ImpersonateStatusBar.js +130 -0
  7. package/lib/commonjs/impersonate/components/UserAvatar.js +146 -0
  8. package/lib/commonjs/impersonate/components/UserCard.js +200 -0
  9. package/lib/commonjs/impersonate/components/UserSearchView.js +227 -0
  10. package/lib/commonjs/impersonate/components/index.js +85 -0
  11. package/lib/commonjs/impersonate/hooks/index.js +64 -0
  12. package/lib/commonjs/impersonate/hooks/useAutoClearAsyncStorage.js +144 -0
  13. package/lib/commonjs/impersonate/hooks/useAutoClearReactQuery.js +155 -0
  14. package/lib/commonjs/impersonate/hooks/useAutoClearRedux.js +188 -0
  15. package/lib/commonjs/impersonate/hooks/useImpersonate.js +215 -0
  16. package/lib/commonjs/impersonate/hooks/useImpersonateHistory.js +56 -0
  17. package/lib/commonjs/impersonate/index.js +49 -0
  18. package/lib/commonjs/impersonate/types/index.js +16 -0
  19. package/lib/commonjs/impersonate/types/types.js +1 -0
  20. package/lib/commonjs/impersonate/utils/impersonateListener.js +280 -0
  21. package/lib/commonjs/impersonate/utils/impersonateStore.js +607 -0
  22. package/lib/commonjs/impersonate/utils/index.js +49 -0
  23. package/lib/commonjs/index.js +118 -0
  24. package/lib/commonjs/package.json +1 -0
  25. package/lib/commonjs/preset.js +214 -0
  26. package/lib/module/impersonate/components/DataNukeSettings.js +710 -0
  27. package/lib/module/impersonate/components/ImpersonateBanner.js +211 -0
  28. package/lib/module/impersonate/components/ImpersonateHistoryList.js +168 -0
  29. package/lib/module/impersonate/components/ImpersonateModal.js +300 -0
  30. package/lib/module/impersonate/components/ImpersonateStatusBar.js +125 -0
  31. package/lib/module/impersonate/components/UserAvatar.js +140 -0
  32. package/lib/module/impersonate/components/UserCard.js +195 -0
  33. package/lib/module/impersonate/components/UserSearchView.js +222 -0
  34. package/lib/module/impersonate/components/index.js +11 -0
  35. package/lib/module/impersonate/hooks/index.js +7 -0
  36. package/lib/module/impersonate/hooks/useAutoClearAsyncStorage.js +140 -0
  37. package/lib/module/impersonate/hooks/useAutoClearReactQuery.js +151 -0
  38. package/lib/module/impersonate/hooks/useAutoClearRedux.js +183 -0
  39. package/lib/module/impersonate/hooks/useImpersonate.js +212 -0
  40. package/lib/module/impersonate/hooks/useImpersonateHistory.js +52 -0
  41. package/lib/module/impersonate/index.js +13 -0
  42. package/lib/module/impersonate/types/index.js +3 -0
  43. package/lib/module/impersonate/types/types.js +1 -0
  44. package/lib/module/impersonate/utils/impersonateListener.js +271 -0
  45. package/lib/module/impersonate/utils/impersonateStore.js +604 -0
  46. package/lib/module/impersonate/utils/index.js +4 -0
  47. package/lib/module/index.js +103 -0
  48. package/lib/module/preset.js +209 -0
  49. package/lib/typescript/impersonate/components/DataNukeSettings.d.ts +37 -0
  50. package/lib/typescript/impersonate/components/DataNukeSettings.d.ts.map +1 -0
  51. package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts +40 -0
  52. package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts.map +1 -0
  53. package/lib/typescript/impersonate/components/ImpersonateHistoryList.d.ts +24 -0
  54. package/lib/typescript/impersonate/components/ImpersonateHistoryList.d.ts.map +1 -0
  55. package/lib/typescript/impersonate/components/ImpersonateModal.d.ts +10 -0
  56. package/lib/typescript/impersonate/components/ImpersonateModal.d.ts.map +1 -0
  57. package/lib/typescript/impersonate/components/ImpersonateStatusBar.d.ts +15 -0
  58. package/lib/typescript/impersonate/components/ImpersonateStatusBar.d.ts.map +1 -0
  59. package/lib/typescript/impersonate/components/UserAvatar.d.ts +32 -0
  60. package/lib/typescript/impersonate/components/UserAvatar.d.ts.map +1 -0
  61. package/lib/typescript/impersonate/components/UserCard.d.ts +28 -0
  62. package/lib/typescript/impersonate/components/UserCard.d.ts.map +1 -0
  63. package/lib/typescript/impersonate/components/UserSearchView.d.ts +31 -0
  64. package/lib/typescript/impersonate/components/UserSearchView.d.ts.map +1 -0
  65. package/lib/typescript/impersonate/components/index.d.ts +16 -0
  66. package/lib/typescript/impersonate/components/index.d.ts.map +1 -0
  67. package/lib/typescript/impersonate/hooks/index.d.ts +11 -0
  68. package/lib/typescript/impersonate/hooks/index.d.ts.map +1 -0
  69. package/lib/typescript/impersonate/hooks/useAutoClearAsyncStorage.d.ts +48 -0
  70. package/lib/typescript/impersonate/hooks/useAutoClearAsyncStorage.d.ts.map +1 -0
  71. package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.d.ts +48 -0
  72. package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.d.ts.map +1 -0
  73. package/lib/typescript/impersonate/hooks/useAutoClearRedux.d.ts +78 -0
  74. package/lib/typescript/impersonate/hooks/useAutoClearRedux.d.ts.map +1 -0
  75. package/lib/typescript/impersonate/hooks/useImpersonate.d.ts +76 -0
  76. package/lib/typescript/impersonate/hooks/useImpersonate.d.ts.map +1 -0
  77. package/lib/typescript/impersonate/hooks/useImpersonateHistory.d.ts +43 -0
  78. package/lib/typescript/impersonate/hooks/useImpersonateHistory.d.ts.map +1 -0
  79. package/lib/typescript/impersonate/index.d.ts +5 -0
  80. package/lib/typescript/impersonate/index.d.ts.map +1 -0
  81. package/lib/typescript/impersonate/types/index.d.ts +2 -0
  82. package/lib/typescript/impersonate/types/index.d.ts.map +1 -0
  83. package/lib/typescript/impersonate/types/types.d.ts +177 -0
  84. package/lib/typescript/impersonate/types/types.d.ts.map +1 -0
  85. package/lib/typescript/impersonate/utils/impersonateListener.d.ts +115 -0
  86. package/lib/typescript/impersonate/utils/impersonateListener.d.ts.map +1 -0
  87. package/lib/typescript/impersonate/utils/impersonateStore.d.ts +151 -0
  88. package/lib/typescript/impersonate/utils/impersonateStore.d.ts.map +1 -0
  89. package/lib/typescript/impersonate/utils/index.d.ts +3 -0
  90. package/lib/typescript/impersonate/utils/index.d.ts.map +1 -0
  91. package/lib/typescript/index.d.ts +80 -0
  92. package/lib/typescript/index.d.ts.map +1 -0
  93. package/lib/typescript/preset.d.ts +71 -0
  94. package/lib/typescript/preset.d.ts.map +1 -0
  95. package/package.json +78 -0
@@ -0,0 +1,604 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Impersonate Store - State Management with Persistence
5
+ *
6
+ * Singleton store for managing impersonation state. Uses a subscription
7
+ * pattern compatible with React's useSyncExternalStore.
8
+ */
9
+
10
+ import { impersonateListener, setImpersonateConfig } from "./impersonateListener";
11
+
12
+ // =============================================================================
13
+ // CONSTANTS
14
+ // =============================================================================
15
+
16
+ const STORAGE_KEY = "@buoy/impersonate/state";
17
+ const MAX_HISTORY = 10;
18
+ const DEFAULT_DATA_NUKE_SETTINGS = {
19
+ reactQuery: true,
20
+ redux: true,
21
+ asyncStorage: false,
22
+ // Dangerous - default off
23
+ mmkv: false // Dangerous - default off
24
+ };
25
+ const DEFAULT_STATE = {
26
+ isActive: false,
27
+ isPaused: false,
28
+ currentUser: null,
29
+ headerKey: "x-impersonate-user-id",
30
+ ignorePatterns: [],
31
+ dataNukeSettings: DEFAULT_DATA_NUKE_SETTINGS,
32
+ showBanner: true,
33
+ history: []
34
+ };
35
+
36
+ // =============================================================================
37
+ // TYPES
38
+ // =============================================================================
39
+
40
+ // Guard to prevent double-execution of data nuking
41
+ let isNuking = false;
42
+
43
+ // =============================================================================
44
+ // SIMPLE STORAGE ABSTRACTION
45
+ // =============================================================================
46
+
47
+ // Use a simple localStorage-like interface for persistence
48
+ // This works on both web and native (via polyfills)
49
+ const storage = {
50
+ getItem: key => {
51
+ try {
52
+ if (typeof localStorage !== "undefined") {
53
+ return localStorage.getItem(key);
54
+ }
55
+ // React Native: use AsyncStorage if available
56
+ // For now, return null (state won't persist until async storage is loaded)
57
+ return null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ },
62
+ setItem: (key, value) => {
63
+ try {
64
+ if (typeof localStorage !== "undefined") {
65
+ localStorage.setItem(key, value);
66
+ }
67
+ // React Native: handled separately via async methods
68
+ } catch {
69
+ // Ignore storage errors
70
+ }
71
+ },
72
+ removeItem: key => {
73
+ try {
74
+ if (typeof localStorage !== "undefined") {
75
+ localStorage.removeItem(key);
76
+ }
77
+ } catch {
78
+ // Ignore
79
+ }
80
+ }
81
+ };
82
+
83
+ // =============================================================================
84
+ // STORE CLASS
85
+ // =============================================================================
86
+
87
+ /**
88
+ * Singleton store for impersonation state
89
+ *
90
+ * Features:
91
+ * - Subscription pattern for React integration
92
+ * - Persistence of settings and history
93
+ * - Data nuking on impersonation change
94
+ * - Automatic sync with impersonateListener
95
+ */
96
+ class ImpersonateStore {
97
+ state = {
98
+ ...DEFAULT_STATE
99
+ };
100
+ listeners = new Set();
101
+ nukeCallbacks = {};
102
+ isInitialized = false;
103
+ asyncStorageRef = null;
104
+ storageReadyPromise = null;
105
+ resolveStorageReady = null;
106
+ developerDefaults = null;
107
+ constructor() {
108
+ // Try to load persisted state synchronously
109
+ this.loadFromStorage();
110
+
111
+ // Check if localStorage is available (web) - if so, storage is ready
112
+ if (typeof localStorage !== "undefined") {
113
+ this.storageReadyPromise = Promise.resolve();
114
+ } else {
115
+ // React Native - wait for AsyncStorage to be set up
116
+ this.storageReadyPromise = new Promise(resolve => {
117
+ this.resolveStorageReady = resolve;
118
+ });
119
+ }
120
+ }
121
+
122
+ // ===========================================================================
123
+ // INITIALIZATION
124
+ // ===========================================================================
125
+
126
+ /**
127
+ * Load persisted state from storage (sync)
128
+ */
129
+ loadFromStorage() {
130
+ try {
131
+ const stored = storage.getItem(STORAGE_KEY);
132
+ const effectiveDefaults = this.getEffectiveDefaults();
133
+ if (stored) {
134
+ const parsed = JSON.parse(stored);
135
+ this.state = {
136
+ ...DEFAULT_STATE,
137
+ // Use effective defaults (developer > hardcoded) as fallback
138
+ headerKey: parsed.headerKey ?? effectiveDefaults.headerKey,
139
+ ignorePatterns: parsed.ignorePatterns ?? DEFAULT_STATE.ignorePatterns,
140
+ dataNukeSettings: {
141
+ ...effectiveDefaults.dataNukeSettings,
142
+ ...parsed.dataNukeSettings
143
+ },
144
+ showBanner: parsed.showBanner ?? effectiveDefaults.showBanner,
145
+ history: parsed.history ?? [],
146
+ // Restore active session for persistence across reloads
147
+ isActive: parsed.isActive ?? false,
148
+ isPaused: parsed.isPaused ?? false,
149
+ currentUser: parsed.currentUser ?? null
150
+ };
151
+
152
+ // If we restored an active session, sync with listener
153
+ if (this.state.isActive && this.state.currentUser) {
154
+ this.syncWithListener();
155
+ }
156
+ } else {
157
+ // No persisted state - use effective defaults
158
+ this.state = {
159
+ ...DEFAULT_STATE,
160
+ headerKey: effectiveDefaults.headerKey,
161
+ dataNukeSettings: effectiveDefaults.dataNukeSettings,
162
+ showBanner: effectiveDefaults.showBanner
163
+ };
164
+ }
165
+ this.isInitialized = true;
166
+ } catch {
167
+ this.isInitialized = true;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Set async storage reference for persistence (call before initializeAsync)
173
+ * This ensures all writes use AsyncStorage even before load completes
174
+ */
175
+ setAsyncStorage(asyncStorage) {
176
+ this.asyncStorageRef = asyncStorage;
177
+ // Resolve the storage ready promise so persist() can proceed
178
+ if (this.resolveStorageReady) {
179
+ this.resolveStorageReady();
180
+ this.resolveStorageReady = null;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Set developer-provided defaults
186
+ * These override hardcoded defaults but are overridden by persisted values
187
+ * Call this before the component mounts (in createImpersonateTool)
188
+ */
189
+ setDeveloperDefaults(defaults) {
190
+ this.developerDefaults = defaults;
191
+
192
+ // Apply defaults to current state if not already initialized from storage
193
+ // Priority: persisted > developer defaults > hardcoded defaults
194
+ const effectiveDefaults = this.getEffectiveDefaults();
195
+
196
+ // Only apply if we haven't loaded persisted values yet
197
+ if (!this.isInitialized) {
198
+ this.state = {
199
+ ...this.state,
200
+ headerKey: effectiveDefaults.headerKey,
201
+ dataNukeSettings: effectiveDefaults.dataNukeSettings,
202
+ showBanner: effectiveDefaults.showBanner
203
+ };
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Get effective defaults (developer defaults merged with hardcoded defaults)
209
+ */
210
+ getEffectiveDefaults() {
211
+ return {
212
+ headerKey: this.developerDefaults?.headerKey ?? DEFAULT_STATE.headerKey,
213
+ dataNukeSettings: {
214
+ ...DEFAULT_DATA_NUKE_SETTINGS,
215
+ ...this.developerDefaults?.dataNukeSettings
216
+ },
217
+ showBanner: this.developerDefaults?.showBanner ?? DEFAULT_STATE.showBanner
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Get developer defaults (for use in UI to show what the "default" values are)
223
+ */
224
+ getDeveloperDefaults() {
225
+ return this.developerDefaults;
226
+ }
227
+
228
+ /**
229
+ * Initialize with async storage (for React Native)
230
+ * Call this in useEffect to load from AsyncStorage
231
+ */
232
+ async initializeAsync(asyncStorage) {
233
+ if (!asyncStorage) return;
234
+
235
+ // Store reference for future persistence (in case not already set)
236
+ this.asyncStorageRef = asyncStorage;
237
+ try {
238
+ const stored = await asyncStorage.getItem(STORAGE_KEY);
239
+ console.log("[ImpersonateStore] initializeAsync - stored:", stored);
240
+ if (stored) {
241
+ const parsed = JSON.parse(stored);
242
+ console.log("[ImpersonateStore] initializeAsync - parsed dataNukeSettings:", parsed.dataNukeSettings);
243
+ this.state = {
244
+ ...this.state,
245
+ headerKey: parsed.headerKey ?? this.state.headerKey,
246
+ ignorePatterns: parsed.ignorePatterns ?? this.state.ignorePatterns,
247
+ dataNukeSettings: {
248
+ ...this.state.dataNukeSettings,
249
+ ...parsed.dataNukeSettings
250
+ },
251
+ showBanner: parsed.showBanner ?? this.state.showBanner,
252
+ history: parsed.history ?? this.state.history,
253
+ // Restore active session
254
+ isActive: parsed.isActive ?? this.state.isActive,
255
+ isPaused: parsed.isPaused ?? this.state.isPaused,
256
+ currentUser: parsed.currentUser ?? this.state.currentUser
257
+ };
258
+ console.log("[ImpersonateStore] initializeAsync - new dataNukeSettings:", this.state.dataNukeSettings);
259
+
260
+ // If we restored an active session, sync with listener
261
+ if (this.state.isActive && this.state.currentUser) {
262
+ this.syncWithListener();
263
+ }
264
+ this.notify();
265
+ }
266
+ } catch (e) {
267
+ console.log("[ImpersonateStore] initializeAsync error:", e);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Register callbacks for data nuking
273
+ */
274
+ registerNukeCallbacks(callbacks) {
275
+ this.nukeCallbacks = {
276
+ ...this.nukeCallbacks,
277
+ ...callbacks
278
+ };
279
+ }
280
+
281
+ // ===========================================================================
282
+ // STATE ACCESS
283
+ // ===========================================================================
284
+
285
+ /**
286
+ * Get current state (creates a copy)
287
+ */
288
+ getState() {
289
+ return {
290
+ ...this.state
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Get snapshot for useSyncExternalStore
296
+ * Returns the same reference if state hasn't changed
297
+ */
298
+ getSnapshot = () => {
299
+ return this.state;
300
+ };
301
+
302
+ /**
303
+ * Subscribe to state changes
304
+ * Returns unsubscribe function
305
+ */
306
+ subscribe = listener => {
307
+ this.listeners.add(listener);
308
+ return () => this.listeners.delete(listener);
309
+ };
310
+
311
+ // ===========================================================================
312
+ // IMPERSONATION ACTIONS
313
+ // ===========================================================================
314
+
315
+ /**
316
+ * Start impersonating a user
317
+ *
318
+ * This will:
319
+ * 1. Execute data nuking based on settings
320
+ * 2. Update state with new user
321
+ * 3. Add to history
322
+ * 4. Sync with impersonateListener
323
+ * 5. Persist settings
324
+ */
325
+ async startImpersonation(user) {
326
+ // Execute data nuking BEFORE switching
327
+ await this.executeDataNuke();
328
+
329
+ // Create history entry with timestamp
330
+ const historyEntry = {
331
+ user,
332
+ lastUsedAt: new Date().toISOString()
333
+ };
334
+
335
+ // Update history (remove duplicate, add to front)
336
+ const newHistory = [historyEntry, ...this.state.history.filter(entry => entry.user.id !== user.id)].slice(0, MAX_HISTORY);
337
+
338
+ // Update state
339
+ this.state = {
340
+ ...this.state,
341
+ isActive: true,
342
+ currentUser: user,
343
+ history: newHistory
344
+ };
345
+
346
+ // Sync with listener
347
+ this.syncWithListener();
348
+
349
+ // Persist
350
+ await this.persist();
351
+
352
+ // Notify subscribers
353
+ this.notify();
354
+ }
355
+
356
+ /**
357
+ * Stop impersonating
358
+ *
359
+ * This will:
360
+ * 1. Execute data nuking based on settings
361
+ * 2. Clear impersonation state
362
+ * 3. Sync with impersonateListener
363
+ */
364
+ async stopImpersonation() {
365
+ // Execute data nuking
366
+ await this.executeDataNuke();
367
+
368
+ // Update state
369
+ this.state = {
370
+ ...this.state,
371
+ isActive: false,
372
+ isPaused: false,
373
+ currentUser: null
374
+ };
375
+
376
+ // Sync with listener
377
+ this.syncWithListener();
378
+
379
+ // Persist
380
+ await this.persist();
381
+
382
+ // Notify
383
+ this.notify();
384
+ }
385
+
386
+ /**
387
+ * Pause impersonation (temporarily stop injecting headers)
388
+ */
389
+ async pauseImpersonation() {
390
+ if (!this.state.isActive || this.state.isPaused) return;
391
+ this.state = {
392
+ ...this.state,
393
+ isPaused: true
394
+ };
395
+
396
+ // Sync with listener (will stop injecting headers)
397
+ this.syncWithListener();
398
+
399
+ // Persist
400
+ await this.persist();
401
+
402
+ // Notify
403
+ this.notify();
404
+ }
405
+
406
+ /**
407
+ * Resume impersonation (start injecting headers again)
408
+ */
409
+ async resumeImpersonation() {
410
+ if (!this.state.isActive || !this.state.isPaused) return;
411
+ this.state = {
412
+ ...this.state,
413
+ isPaused: false
414
+ };
415
+
416
+ // Sync with listener (will resume injecting headers)
417
+ this.syncWithListener();
418
+
419
+ // Persist
420
+ await this.persist();
421
+
422
+ // Notify
423
+ this.notify();
424
+ }
425
+
426
+ /**
427
+ * Quick switch to a user from history
428
+ */
429
+ async quickSwitch(user) {
430
+ return this.startImpersonation(user);
431
+ }
432
+
433
+ // ===========================================================================
434
+ // SETTINGS
435
+ // ===========================================================================
436
+
437
+ /**
438
+ * Update settings (header key, ignore patterns, data nuke settings, show banner)
439
+ */
440
+ async updateSettings(settings) {
441
+ console.log("[ImpersonateStore] updateSettings called with:", settings);
442
+ console.log("[ImpersonateStore] Current dataNukeSettings:", this.state.dataNukeSettings);
443
+ this.state = {
444
+ ...this.state,
445
+ headerKey: settings.headerKey ?? this.state.headerKey,
446
+ ignorePatterns: settings.ignorePatterns ?? this.state.ignorePatterns,
447
+ showBanner: settings.showBanner ?? this.state.showBanner,
448
+ dataNukeSettings: settings.dataNukeSettings ? {
449
+ ...this.state.dataNukeSettings,
450
+ ...settings.dataNukeSettings
451
+ } : this.state.dataNukeSettings
452
+ };
453
+ console.log("[ImpersonateStore] New dataNukeSettings:", this.state.dataNukeSettings);
454
+
455
+ // Sync header key with listener
456
+ if (settings.headerKey || settings.ignorePatterns) {
457
+ this.syncWithListener();
458
+ }
459
+ await this.persist();
460
+ this.notify();
461
+ }
462
+
463
+ // ===========================================================================
464
+ // HISTORY
465
+ // ===========================================================================
466
+
467
+ /**
468
+ * Remove a user from history
469
+ */
470
+ async removeFromHistory(userId) {
471
+ this.state = {
472
+ ...this.state,
473
+ history: this.state.history.filter(entry => entry.user.id !== userId)
474
+ };
475
+ await this.persist();
476
+ this.notify();
477
+ }
478
+
479
+ /**
480
+ * Clear all history
481
+ */
482
+ async clearHistory() {
483
+ this.state = {
484
+ ...this.state,
485
+ history: []
486
+ };
487
+ await this.persist();
488
+ this.notify();
489
+ }
490
+
491
+ // ===========================================================================
492
+ // PRIVATE METHODS
493
+ // ===========================================================================
494
+
495
+ /**
496
+ * Execute data nuking based on current settings
497
+ */
498
+ async executeDataNuke() {
499
+ // Guard against double-execution (can happen with React Strict Mode)
500
+ if (isNuking) {
501
+ return;
502
+ }
503
+ isNuking = true;
504
+ try {
505
+ const {
506
+ dataNukeSettings
507
+ } = this.state;
508
+ const promises = [];
509
+ if (dataNukeSettings.reactQuery && this.nukeCallbacks.reactQuery) {
510
+ promises.push(Promise.resolve(this.nukeCallbacks.reactQuery()));
511
+ }
512
+ if (dataNukeSettings.redux && this.nukeCallbacks.redux) {
513
+ promises.push(Promise.resolve(this.nukeCallbacks.redux()));
514
+ }
515
+ if (dataNukeSettings.asyncStorage && this.nukeCallbacks.asyncStorage) {
516
+ promises.push(Promise.resolve(this.nukeCallbacks.asyncStorage()));
517
+ }
518
+ if (dataNukeSettings.mmkv && this.nukeCallbacks.mmkv) {
519
+ promises.push(Promise.resolve(this.nukeCallbacks.mmkv()));
520
+ }
521
+
522
+ // Wait for all, but don't fail if some error
523
+ await Promise.allSettled(promises);
524
+ } finally {
525
+ // Reset flag after a short delay to allow next legitimate call
526
+ setTimeout(() => {
527
+ isNuking = false;
528
+ }, 100);
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Sync current state with the impersonateListener
534
+ */
535
+ syncWithListener() {
536
+ // When paused, don't inject headers (pass null userId)
537
+ const shouldInject = this.state.isActive && !this.state.isPaused;
538
+ setImpersonateConfig({
539
+ headerKey: this.state.headerKey,
540
+ userId: shouldInject ? this.state.currentUser?.id ?? null : null,
541
+ ignorePatterns: this.state.ignorePatterns.map(p => new RegExp(p))
542
+ });
543
+
544
+ // Ensure listener is started
545
+ if (!impersonateListener().isListening) {
546
+ impersonateListener().startListening();
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Persist state to storage
552
+ */
553
+ async persist() {
554
+ // Wait for storage to be ready (AsyncStorage setup on React Native)
555
+ if (this.storageReadyPromise) {
556
+ await this.storageReadyPromise;
557
+ }
558
+ const toStore = {
559
+ headerKey: this.state.headerKey,
560
+ ignorePatterns: this.state.ignorePatterns,
561
+ dataNukeSettings: this.state.dataNukeSettings,
562
+ showBanner: this.state.showBanner,
563
+ history: this.state.history,
564
+ // Persist active session for continuity across reloads
565
+ isActive: this.state.isActive,
566
+ isPaused: this.state.isPaused,
567
+ currentUser: this.state.currentUser
568
+ };
569
+ const serialized = JSON.stringify(toStore);
570
+ console.log("[ImpersonateStore] Persisting dataNukeSettings:", this.state.dataNukeSettings);
571
+ try {
572
+ // Prefer AsyncStorage for React Native
573
+ if (this.asyncStorageRef) {
574
+ await this.asyncStorageRef.setItem(STORAGE_KEY, serialized);
575
+ console.log("[ImpersonateStore] Persisted to AsyncStorage");
576
+ } else {
577
+ // Fall back to localStorage (web)
578
+ storage.setItem(STORAGE_KEY, serialized);
579
+ console.log("[ImpersonateStore] Persisted to localStorage");
580
+ }
581
+ } catch (e) {
582
+ console.log("[ImpersonateStore] Persist error:", e);
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Notify all listeners of state change
588
+ */
589
+ notify() {
590
+ this.listeners.forEach(listener => {
591
+ try {
592
+ listener(this.state);
593
+ } catch {
594
+ // Ignore listener errors
595
+ }
596
+ });
597
+ }
598
+ }
599
+
600
+ // =============================================================================
601
+ // SINGLETON EXPORT
602
+ // =============================================================================
603
+
604
+ export const impersonateStore = new ImpersonateStore();
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export { impersonateListener, startImpersonateListener, stopImpersonateListener, setImpersonateConfig, isImpersonating, getImpersonatedUserId } from "./impersonateListener";
4
+ export { impersonateStore } from "./impersonateStore";
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * @buoy-gg/impersonate
5
+ *
6
+ * User impersonation tool for React Native DevTools.
7
+ * Injects custom headers into fetch/XHR requests when impersonating.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { createImpersonateTool } from '@buoy-gg/impersonate';
12
+ *
13
+ * const impersonateTool = createImpersonateTool({
14
+ * onSearchUsers: async (query) => {
15
+ * const res = await api.searchUsers(query);
16
+ * return res.users;
17
+ * },
18
+ * onClearReactQuery: () => queryClient.clear(),
19
+ * });
20
+ *
21
+ * <FloatingDevTools apps={[impersonateTool]} />
22
+ * ```
23
+ */
24
+
25
+ // =============================================================================
26
+ // FACTORY (Primary entry point)
27
+ // =============================================================================
28
+
29
+ export { createImpersonateTool } from "./preset";
30
+
31
+ // =============================================================================
32
+ // COMPONENTS (For custom UI implementations)
33
+ // =============================================================================
34
+
35
+ export { ImpersonateModal } from "./impersonate/components/ImpersonateModal";
36
+ export { ImpersonateBanner, ImpersonateBannerMinimal } from "./impersonate/components/ImpersonateBanner";
37
+ export { UserSearchView } from "./impersonate/components/UserSearchView";
38
+ export { DataNukeSettings as DataNukeSettingsComponent } from "./impersonate/components/DataNukeSettings";
39
+ export { ImpersonateHistoryList } from "./impersonate/components/ImpersonateHistoryList";
40
+ export { ImpersonateStatusBar } from "./impersonate/components/ImpersonateStatusBar";
41
+
42
+ // =============================================================================
43
+ // HOOKS (For consuming impersonation state in custom components)
44
+ // =============================================================================
45
+
46
+ export { useImpersonate } from "./impersonate/hooks/useImpersonate";
47
+ export { useImpersonateHistory } from "./impersonate/hooks/useImpersonateHistory";
48
+
49
+ // =============================================================================
50
+ // TYPES (For TypeScript users)
51
+ // =============================================================================
52
+
53
+ // =============================================================================
54
+ // LOW-LEVEL EXPORTS (For advanced use cases)
55
+ // =============================================================================
56
+
57
+ /**
58
+ * Direct access to the impersonation store singleton.
59
+ * Use this for custom integrations or debugging.
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * import { impersonateStore } from '@buoy-gg/impersonate';
64
+ *
65
+ * // Check if impersonating
66
+ * const state = impersonateStore.getState();
67
+ * if (state.isActive) {
68
+ * console.log('Impersonating:', state.currentUser?.email);
69
+ * }
70
+ *
71
+ * // Subscribe to changes
72
+ * const unsubscribe = impersonateStore.subscribe((state) => {
73
+ * console.log('Impersonation state changed:', state);
74
+ * });
75
+ * ```
76
+ */
77
+ export { impersonateStore } from "./impersonate/utils/impersonateStore";
78
+
79
+ /**
80
+ * Direct access to the impersonate listener for advanced control.
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * import {
85
+ * impersonateListener,
86
+ * startImpersonateListener,
87
+ * setImpersonateConfig,
88
+ * } from '@buoy-gg/impersonate';
89
+ *
90
+ * // Manually start the listener
91
+ * startImpersonateListener();
92
+ *
93
+ * // Configure impersonation
94
+ * setImpersonateConfig({
95
+ * headerKey: 'x-custom-impersonate-header',
96
+ * userId: 'user_123',
97
+ * });
98
+ *
99
+ * // Check status
100
+ * console.log('Listener active:', impersonateListener().isListening);
101
+ * ```
102
+ */
103
+ export { impersonateListener, startImpersonateListener, stopImpersonateListener, setImpersonateConfig, isImpersonating, getImpersonatedUserId } from "./impersonate/utils/impersonateListener";