@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.
Files changed (78) hide show
  1. package/README.md +12 -80
  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 -17
  12. package/lib/commonjs/impersonate/hooks/asyncStoragePeer.web.js +1 -11
  13. package/lib/commonjs/impersonate/hooks/useAutoClearAsyncStorage.js +1 -134
  14. package/lib/commonjs/impersonate/hooks/useAutoClearReactQuery.js +1 -155
  15. package/lib/commonjs/impersonate/hooks/useAutoClearReactQuery.web.js +1 -20
  16. package/lib/commonjs/impersonate/hooks/useAutoClearRedux.js +1 -188
  17. package/lib/commonjs/impersonate/hooks/useAutoClearRedux.web.js +1 -27
  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 -350
  23. package/lib/commonjs/impersonate/utils/impersonateStore.js +1 -689
  24. package/lib/commonjs/index.js +1 -150
  25. package/lib/commonjs/preset.js +1 -232
  26. package/lib/commonjs/sync/impersonateSyncAdapter.js +1 -170
  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 -13
  37. package/lib/module/impersonate/hooks/asyncStoragePeer.web.js +1 -6
  38. package/lib/module/impersonate/hooks/useAutoClearAsyncStorage.js +1 -130
  39. package/lib/module/impersonate/hooks/useAutoClearReactQuery.js +1 -151
  40. package/lib/module/impersonate/hooks/useAutoClearReactQuery.web.js +1 -16
  41. package/lib/module/impersonate/hooks/useAutoClearRedux.js +1 -183
  42. package/lib/module/impersonate/hooks/useAutoClearRedux.web.js +1 -22
  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 -341
  48. package/lib/module/impersonate/utils/impersonateStore.js +1 -686
  49. package/lib/module/index.js +1 -117
  50. package/lib/module/preset.js +1 -216
  51. package/lib/module/sync/impersonateSyncAdapter.js +1 -162
  52. package/lib/web/index.mjs +7 -8
  53. package/package.json +3 -6
  54. package/lib/typescript/impersonate/components/DataNukeSettings.d.ts.map +0 -1
  55. package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts.map +0 -1
  56. package/lib/typescript/impersonate/components/ImpersonateHistoryList.d.ts.map +0 -1
  57. package/lib/typescript/impersonate/components/ImpersonateModal.d.ts.map +0 -1
  58. package/lib/typescript/impersonate/components/ImpersonateOverlay.d.ts.map +0 -1
  59. package/lib/typescript/impersonate/components/ImpersonateStatusBar.d.ts.map +0 -1
  60. package/lib/typescript/impersonate/components/UserAvatar.d.ts.map +0 -1
  61. package/lib/typescript/impersonate/components/UserCard.d.ts.map +0 -1
  62. package/lib/typescript/impersonate/components/UserSearchView.d.ts.map +0 -1
  63. package/lib/typescript/impersonate/hooks/asyncStoragePeer.d.ts.map +0 -1
  64. package/lib/typescript/impersonate/hooks/asyncStoragePeer.web.d.ts.map +0 -1
  65. package/lib/typescript/impersonate/hooks/useAutoClearAsyncStorage.d.ts.map +0 -1
  66. package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.d.ts.map +0 -1
  67. package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.web.d.ts.map +0 -1
  68. package/lib/typescript/impersonate/hooks/useAutoClearRedux.d.ts.map +0 -1
  69. package/lib/typescript/impersonate/hooks/useAutoClearRedux.web.d.ts.map +0 -1
  70. package/lib/typescript/impersonate/hooks/useImpersonate.d.ts.map +0 -1
  71. package/lib/typescript/impersonate/hooks/useImpersonateHistory.d.ts.map +0 -1
  72. package/lib/typescript/impersonate/types/index.d.ts.map +0 -1
  73. package/lib/typescript/impersonate/types/types.d.ts.map +0 -1
  74. package/lib/typescript/impersonate/utils/impersonateListener.d.ts.map +0 -1
  75. package/lib/typescript/impersonate/utils/impersonateStore.d.ts.map +0 -1
  76. package/lib/typescript/index.d.ts.map +0 -1
  77. package/lib/typescript/preset.d.ts.map +0 -1
  78. 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;