@buoy-gg/impersonate 7.0.40 → 7.0.42

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