@buoy-gg/impersonate 2.1.15 → 3.0.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.
- package/lib/commonjs/impersonate/components/ImpersonateBanner.js +4 -0
- package/lib/commonjs/impersonate/components/ImpersonateOverlay.js +26 -0
- package/lib/commonjs/impersonate/utils/impersonateStore.js +93 -48
- package/lib/commonjs/index.js +20 -0
- package/lib/commonjs/preset.js +69 -44
- package/lib/commonjs/sync/impersonateSyncAdapter.js +36 -0
- package/lib/module/impersonate/components/ImpersonateBanner.js +4 -0
- package/lib/module/impersonate/components/ImpersonateOverlay.js +21 -0
- package/lib/module/impersonate/utils/impersonateStore.js +93 -48
- package/lib/module/index.js +13 -1
- package/lib/module/preset.js +69 -45
- package/lib/module/sync/impersonateSyncAdapter.js +32 -0
- package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts.map +1 -1
- package/lib/typescript/impersonate/components/ImpersonateOverlay.d.ts +12 -0
- package/lib/typescript/impersonate/components/ImpersonateOverlay.d.ts.map +1 -0
- package/lib/typescript/impersonate/utils/impersonateStore.d.ts +23 -12
- package/lib/typescript/impersonate/utils/impersonateStore.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +8 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/preset.d.ts +12 -1
- package/lib/typescript/preset.d.ts.map +1 -1
- package/lib/typescript/sync/impersonateSyncAdapter.d.ts +35 -0
- package/lib/typescript/sync/impersonateSyncAdapter.d.ts.map +1 -0
- package/package.json +2 -2
|
@@ -52,6 +52,8 @@ function ImpersonateBanner({
|
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
54
|
const handlePauseResume = async () => {
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.log("[impersonate] banner pause/resume pressed");
|
|
55
57
|
if (state.isPaused) {
|
|
56
58
|
await _impersonateStore.impersonateStore.resumeImpersonation();
|
|
57
59
|
} else {
|
|
@@ -65,6 +67,8 @@ function ImpersonateBanner({
|
|
|
65
67
|
bottom: offset
|
|
66
68
|
};
|
|
67
69
|
const handleStop = async () => {
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.log("[impersonate] banner STOP pressed");
|
|
68
72
|
await _impersonateStore.impersonateStore.stopImpersonation();
|
|
69
73
|
};
|
|
70
74
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.ImpersonateOverlay = ImpersonateOverlay;
|
|
7
|
+
var _react = _interopRequireDefault(require("react"));
|
|
8
|
+
var _ImpersonateBanner = require("./ImpersonateBanner");
|
|
9
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
10
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
/**
|
|
12
|
+
* ImpersonateOverlay
|
|
13
|
+
*
|
|
14
|
+
* Auto-rendered by FloatingDevTools (`@buoy-gg/core`) when
|
|
15
|
+
* `@buoy-gg/impersonate` is installed — the host does NOT need to mount
|
|
16
|
+
* anything. It renders the floating impersonation banner, which self-gates
|
|
17
|
+
* on store state (only visible while an impersonation session is active and
|
|
18
|
+
* `showBanner` is enabled), so it's safe to mount unconditionally.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function ImpersonateOverlay() {
|
|
22
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ImpersonateBanner.ImpersonateBanner, {
|
|
23
|
+
position: "top",
|
|
24
|
+
offset: 50
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -239,10 +239,10 @@ class ImpersonateStore {
|
|
|
239
239
|
this.asyncStorageRef = asyncStorage;
|
|
240
240
|
try {
|
|
241
241
|
const stored = await asyncStorage.getItem(STORAGE_KEY);
|
|
242
|
-
|
|
242
|
+
// eslint-disable-next-line no-console
|
|
243
|
+
console.log(`[impersonate] initializeAsync read STORAGE_KEY=${STORAGE_KEY} stored=${stored ? stored.slice(0, 200) : "null"}`);
|
|
243
244
|
if (stored) {
|
|
244
245
|
const parsed = JSON.parse(stored);
|
|
245
|
-
console.log("[ImpersonateStore] initializeAsync - parsed dataNukeSettings:", parsed.dataNukeSettings);
|
|
246
246
|
this.state = {
|
|
247
247
|
...this.state,
|
|
248
248
|
headerKey: parsed.headerKey ?? this.state.headerKey,
|
|
@@ -258,7 +258,6 @@ class ImpersonateStore {
|
|
|
258
258
|
isPaused: parsed.isPaused ?? this.state.isPaused,
|
|
259
259
|
currentUser: parsed.currentUser ?? this.state.currentUser
|
|
260
260
|
};
|
|
261
|
-
console.log("[ImpersonateStore] initializeAsync - new dataNukeSettings:", this.state.dataNukeSettings);
|
|
262
261
|
|
|
263
262
|
// If we restored an active session, sync with listener
|
|
264
263
|
if (this.state.isActive && this.state.currentUser) {
|
|
@@ -266,8 +265,8 @@ class ImpersonateStore {
|
|
|
266
265
|
}
|
|
267
266
|
this.notify();
|
|
268
267
|
}
|
|
269
|
-
} catch
|
|
270
|
-
|
|
268
|
+
} catch {
|
|
269
|
+
// Persisted blob unreadable — fall through with in-memory defaults.
|
|
271
270
|
}
|
|
272
271
|
}
|
|
273
272
|
|
|
@@ -311,34 +310,57 @@ class ImpersonateStore {
|
|
|
311
310
|
return () => this.listeners.delete(listener);
|
|
312
311
|
};
|
|
313
312
|
|
|
313
|
+
// ===========================================================================
|
|
314
|
+
// REMOTE MIRROR MODE
|
|
315
|
+
// ===========================================================================
|
|
316
|
+
|
|
317
|
+
remoteHandler = null;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Remote mirror mode (desktop dashboard): while a handler is set, every
|
|
321
|
+
* mutation forwards (method, params) to the synced device instead of
|
|
322
|
+
* running locally — header patching, data nuking and persistence all
|
|
323
|
+
* happen on the device, and the resulting state comes back via
|
|
324
|
+
* replaceState(). Pass null to restore local behavior.
|
|
325
|
+
*/
|
|
326
|
+
setRemoteHandler(handler) {
|
|
327
|
+
this.remoteHandler = handler;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Replace the mirrored state from a synced device snapshot. Does not
|
|
332
|
+
* persist or touch the fetch listener — the device owns both.
|
|
333
|
+
*/
|
|
334
|
+
replaceState(state) {
|
|
335
|
+
this.state = state;
|
|
336
|
+
this.notify();
|
|
337
|
+
}
|
|
338
|
+
|
|
314
339
|
// ===========================================================================
|
|
315
340
|
// IMPERSONATION ACTIONS
|
|
316
341
|
// ===========================================================================
|
|
317
342
|
|
|
318
343
|
/**
|
|
319
|
-
* Start impersonating a user
|
|
344
|
+
* Start impersonating a user.
|
|
320
345
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* 5. Persist settings
|
|
346
|
+
* Order matters: we update state + listener + UI BEFORE clearing caches,
|
|
347
|
+
* so refetches triggered by the cache clear go out with the new user's
|
|
348
|
+
* impersonation header. Doing it the other way around (nuke first) makes
|
|
349
|
+
* react-query refetch active queries with the OLD identity, producing
|
|
350
|
+
* the visual symptom of "I clicked a user and nothing changed."
|
|
327
351
|
*/
|
|
328
352
|
async startImpersonation(user) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
353
|
+
if (this.remoteHandler) {
|
|
354
|
+
this.remoteHandler("startImpersonation", {
|
|
355
|
+
user
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
333
359
|
const historyEntry = {
|
|
334
360
|
user,
|
|
335
361
|
lastUsedAt: new Date().toISOString()
|
|
336
362
|
};
|
|
337
|
-
|
|
338
|
-
// Update history (remove duplicate, add to front)
|
|
339
363
|
const newHistory = [historyEntry, ...this.state.history.filter(entry => entry.user.id !== user.id)].slice(0, MAX_HISTORY);
|
|
340
|
-
|
|
341
|
-
// Update state
|
|
342
364
|
this.state = {
|
|
343
365
|
...this.state,
|
|
344
366
|
isActive: true,
|
|
@@ -346,50 +368,54 @@ class ImpersonateStore {
|
|
|
346
368
|
history: newHistory
|
|
347
369
|
};
|
|
348
370
|
|
|
349
|
-
//
|
|
371
|
+
// 1) Point the fetch interceptor at the new user. Any request after this
|
|
372
|
+
// line carries the new header.
|
|
350
373
|
this.syncWithListener();
|
|
351
374
|
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// Notify subscribers
|
|
375
|
+
// 2) Tell React subscribers right away so the UI updates without waiting
|
|
376
|
+
// on AsyncStorage / data nuke.
|
|
356
377
|
this.notify();
|
|
378
|
+
|
|
379
|
+
// 3) Nuke caches — refetches now use the new identity.
|
|
380
|
+
await this.executeDataNuke();
|
|
381
|
+
|
|
382
|
+
// 4) Persist for next launch.
|
|
383
|
+
await this.persist();
|
|
357
384
|
}
|
|
358
385
|
|
|
359
386
|
/**
|
|
360
|
-
* Stop impersonating
|
|
387
|
+
* Stop impersonating.
|
|
361
388
|
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* 2. Clear impersonation state
|
|
365
|
-
* 3. Sync with impersonateListener
|
|
389
|
+
* Same ordering rationale as startImpersonation: clear state + listener
|
|
390
|
+
* before nuking caches so subsequent refetches go out without the header.
|
|
366
391
|
*/
|
|
367
392
|
async stopImpersonation() {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
393
|
+
if (this.remoteHandler) {
|
|
394
|
+
this.remoteHandler("stopImpersonation");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
// eslint-disable-next-line no-console
|
|
398
|
+
console.log(`[impersonate] stopImpersonation: isActive ${this.state.isActive} -> false, listeners=${this.listeners.size}`);
|
|
372
399
|
this.state = {
|
|
373
400
|
...this.state,
|
|
374
401
|
isActive: false,
|
|
375
402
|
isPaused: false,
|
|
376
403
|
currentUser: null
|
|
377
404
|
};
|
|
378
|
-
|
|
379
|
-
// Sync with listener
|
|
380
405
|
this.syncWithListener();
|
|
381
|
-
|
|
382
|
-
// Persist
|
|
383
|
-
await this.persist();
|
|
384
|
-
|
|
385
|
-
// Notify
|
|
386
406
|
this.notify();
|
|
407
|
+
await this.executeDataNuke();
|
|
408
|
+
await this.persist();
|
|
387
409
|
}
|
|
388
410
|
|
|
389
411
|
/**
|
|
390
412
|
* Pause impersonation (temporarily stop injecting headers)
|
|
391
413
|
*/
|
|
392
414
|
async pauseImpersonation() {
|
|
415
|
+
if (this.remoteHandler) {
|
|
416
|
+
this.remoteHandler("pauseImpersonation");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
393
419
|
if (!this.state.isActive || this.state.isPaused) return;
|
|
394
420
|
this.state = {
|
|
395
421
|
...this.state,
|
|
@@ -410,6 +436,10 @@ class ImpersonateStore {
|
|
|
410
436
|
* Resume impersonation (start injecting headers again)
|
|
411
437
|
*/
|
|
412
438
|
async resumeImpersonation() {
|
|
439
|
+
if (this.remoteHandler) {
|
|
440
|
+
this.remoteHandler("resumeImpersonation");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
413
443
|
if (!this.state.isActive || !this.state.isPaused) return;
|
|
414
444
|
this.state = {
|
|
415
445
|
...this.state,
|
|
@@ -441,8 +471,12 @@ class ImpersonateStore {
|
|
|
441
471
|
* Update settings (header key, ignore patterns, data nuke settings, show banner)
|
|
442
472
|
*/
|
|
443
473
|
async updateSettings(settings) {
|
|
444
|
-
|
|
445
|
-
|
|
474
|
+
if (this.remoteHandler) {
|
|
475
|
+
this.remoteHandler("updateSettings", {
|
|
476
|
+
settings
|
|
477
|
+
});
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
446
480
|
this.state = {
|
|
447
481
|
...this.state,
|
|
448
482
|
headerKey: settings.headerKey ?? this.state.headerKey,
|
|
@@ -453,7 +487,6 @@ class ImpersonateStore {
|
|
|
453
487
|
...settings.dataNukeSettings
|
|
454
488
|
} : this.state.dataNukeSettings
|
|
455
489
|
};
|
|
456
|
-
console.log("[ImpersonateStore] New dataNukeSettings:", this.state.dataNukeSettings);
|
|
457
490
|
|
|
458
491
|
// Sync header key with listener
|
|
459
492
|
if (settings.headerKey || settings.ignorePatterns) {
|
|
@@ -471,6 +504,12 @@ class ImpersonateStore {
|
|
|
471
504
|
* Remove a user from history
|
|
472
505
|
*/
|
|
473
506
|
async removeFromHistory(userId) {
|
|
507
|
+
if (this.remoteHandler) {
|
|
508
|
+
this.remoteHandler("removeFromHistory", {
|
|
509
|
+
userId
|
|
510
|
+
});
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
474
513
|
this.state = {
|
|
475
514
|
...this.state,
|
|
476
515
|
history: this.state.history.filter(entry => entry.user.id !== userId)
|
|
@@ -483,6 +522,10 @@ class ImpersonateStore {
|
|
|
483
522
|
* Clear all history
|
|
484
523
|
*/
|
|
485
524
|
async clearHistory() {
|
|
525
|
+
if (this.remoteHandler) {
|
|
526
|
+
this.remoteHandler("clearHistory");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
486
529
|
this.state = {
|
|
487
530
|
...this.state,
|
|
488
531
|
history: []
|
|
@@ -570,19 +613,21 @@ class ImpersonateStore {
|
|
|
570
613
|
currentUser: this.state.currentUser
|
|
571
614
|
};
|
|
572
615
|
const serialized = JSON.stringify(toStore);
|
|
573
|
-
console.log("[ImpersonateStore] Persisting dataNukeSettings:", this.state.dataNukeSettings);
|
|
574
616
|
try {
|
|
575
617
|
// Prefer AsyncStorage for React Native
|
|
576
618
|
if (this.asyncStorageRef) {
|
|
577
619
|
await this.asyncStorageRef.setItem(STORAGE_KEY, serialized);
|
|
578
|
-
|
|
620
|
+
// eslint-disable-next-line no-console
|
|
621
|
+
console.log(`[impersonate] persist -> AsyncStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
|
|
579
622
|
} else {
|
|
580
623
|
// Fall back to localStorage (web)
|
|
581
624
|
storage.setItem(STORAGE_KEY, serialized);
|
|
582
|
-
|
|
625
|
+
// eslint-disable-next-line no-console
|
|
626
|
+
console.log(`[impersonate] persist -> localStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
|
|
583
627
|
}
|
|
584
628
|
} catch (e) {
|
|
585
|
-
|
|
629
|
+
// eslint-disable-next-line no-console
|
|
630
|
+
console.warn("[impersonate] persist failed", e);
|
|
586
631
|
}
|
|
587
632
|
}
|
|
588
633
|
|
package/lib/commonjs/index.js
CHANGED
|
@@ -27,12 +27,24 @@ Object.defineProperty(exports, "ImpersonateHistoryList", {
|
|
|
27
27
|
return _ImpersonateHistoryList.ImpersonateHistoryList;
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
|
+
Object.defineProperty(exports, "ImpersonateIcon", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function () {
|
|
33
|
+
return _preset.ImpersonateIcon;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
30
36
|
Object.defineProperty(exports, "ImpersonateModal", {
|
|
31
37
|
enumerable: true,
|
|
32
38
|
get: function () {
|
|
33
39
|
return _ImpersonateModal.ImpersonateModal;
|
|
34
40
|
}
|
|
35
41
|
});
|
|
42
|
+
Object.defineProperty(exports, "ImpersonateOverlay", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
get: function () {
|
|
45
|
+
return _ImpersonateOverlay.ImpersonateOverlay;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
36
48
|
Object.defineProperty(exports, "ImpersonateStatusBar", {
|
|
37
49
|
enumerable: true,
|
|
38
50
|
get: function () {
|
|
@@ -45,6 +57,12 @@ Object.defineProperty(exports, "UserSearchView", {
|
|
|
45
57
|
return _UserSearchView.UserSearchView;
|
|
46
58
|
}
|
|
47
59
|
});
|
|
60
|
+
Object.defineProperty(exports, "createImpersonateSyncAdapter", {
|
|
61
|
+
enumerable: true,
|
|
62
|
+
get: function () {
|
|
63
|
+
return _impersonateSyncAdapter.createImpersonateSyncAdapter;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
48
66
|
Object.defineProperty(exports, "createImpersonateTool", {
|
|
49
67
|
enumerable: true,
|
|
50
68
|
get: function () {
|
|
@@ -106,8 +124,10 @@ Object.defineProperty(exports, "useImpersonateHistory", {
|
|
|
106
124
|
}
|
|
107
125
|
});
|
|
108
126
|
var _preset = require("./preset");
|
|
127
|
+
var _impersonateSyncAdapter = require("./sync/impersonateSyncAdapter");
|
|
109
128
|
var _ImpersonateModal = require("./impersonate/components/ImpersonateModal");
|
|
110
129
|
var _ImpersonateBanner = require("./impersonate/components/ImpersonateBanner");
|
|
130
|
+
var _ImpersonateOverlay = require("./impersonate/components/ImpersonateOverlay");
|
|
111
131
|
var _UserSearchView = require("./impersonate/components/UserSearchView");
|
|
112
132
|
var _DataNukeSettings = require("./impersonate/components/DataNukeSettings");
|
|
113
133
|
var _ImpersonateHistoryList = require("./impersonate/components/ImpersonateHistoryList");
|
package/lib/commonjs/preset.js
CHANGED
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.ImpersonateIcon = ImpersonateIcon;
|
|
6
7
|
exports.createImpersonateTool = createImpersonateTool;
|
|
7
|
-
var _react =
|
|
8
|
+
var _react = _interopRequireDefault(require("react"));
|
|
8
9
|
var _reactNative = require("react-native");
|
|
9
10
|
var _ImpersonateModal = require("./impersonate/components/ImpersonateModal");
|
|
10
11
|
var _ImpersonateBanner = require("./impersonate/components/ImpersonateBanner");
|
|
11
12
|
var _impersonateStore = require("./impersonate/utils/impersonateStore");
|
|
13
|
+
var _impersonateListener = require("./impersonate/utils/impersonateListener");
|
|
12
14
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
|
-
function
|
|
15
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
16
|
/**
|
|
15
17
|
* Impersonate Tool Preset
|
|
16
18
|
*
|
|
@@ -18,9 +20,63 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
18
20
|
* No default preset is exported because configuration (onSearchUsers) is required.
|
|
19
21
|
*/
|
|
20
22
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// EAGER BOOTSTRAP
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Patch fetch/XHR and restore persisted state the moment createImpersonateTool
|
|
27
|
+
// runs (typically at module load in the host app). If we waited until the
|
|
28
|
+
// modal mounts (the original lazy design), every request fired during app
|
|
29
|
+
// boot — bridge polls, user-data fetches, etc. — goes out without the
|
|
30
|
+
// impersonation header, even when the user already had impersonation active
|
|
31
|
+
// from a previous session. React Query then caches the real-account response,
|
|
32
|
+
// so even after the listener does eventually patch, no refetch happens.
|
|
33
|
+
//
|
|
34
|
+
// Side effect: any later fetch interceptor (e.g. @buoy-gg/network) installed
|
|
35
|
+
// AFTER us will wrap our wrapper, so its captured init.headers won't show the
|
|
36
|
+
// impersonation header. The wire request still includes it.
|
|
37
|
+
|
|
38
|
+
let bootstrapStarted = false;
|
|
39
|
+
async function bootstrapImpersonate() {
|
|
40
|
+
if (bootstrapStarted) return;
|
|
41
|
+
bootstrapStarted = true;
|
|
42
|
+
|
|
43
|
+
// eslint-disable-next-line no-console
|
|
44
|
+
console.log("[impersonate] bootstrap: start");
|
|
45
|
+
|
|
46
|
+
// Patch fetch/XHR synchronously so any request between now and the async
|
|
47
|
+
// storage load goes through our wrapper. The wrapper is a no-op until
|
|
48
|
+
// userId is set (which happens once initializeAsync resolves).
|
|
49
|
+
if (!(0, _impersonateListener.impersonateListener)().isListening) {
|
|
50
|
+
(0, _impersonateListener.impersonateListener)().startListening();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// React Native: require AsyncStorage. require() is more reliable under
|
|
54
|
+
// Metro than dynamic import(); on web this throws and the store falls
|
|
55
|
+
// back to localStorage handled synchronously in its constructor.
|
|
56
|
+
try {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
58
|
+
const mod = require("@react-native-async-storage/async-storage");
|
|
59
|
+
const AsyncStorage = mod?.default ?? mod;
|
|
60
|
+
if (!AsyncStorage || typeof AsyncStorage.getItem !== "function") {
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.warn("[impersonate] bootstrap: AsyncStorage shape unexpected", Object.keys(mod ?? {}));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const adapter = {
|
|
66
|
+
getItem: key => AsyncStorage.getItem(key),
|
|
67
|
+
setItem: (key, value) => AsyncStorage.setItem(key, value)
|
|
68
|
+
};
|
|
69
|
+
_impersonateStore.impersonateStore.setAsyncStorage(adapter);
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.log("[impersonate] bootstrap: AsyncStorage adapter installed");
|
|
72
|
+
await _impersonateStore.impersonateStore.initializeAsync(adapter);
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.log("[impersonate] bootstrap: initializeAsync done");
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
console.warn("[impersonate] bootstrap: failed", e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
24
80
|
|
|
25
81
|
// =============================================================================
|
|
26
82
|
// ICON COMPONENT
|
|
@@ -53,43 +109,6 @@ function ImpersonateIcon({
|
|
|
53
109
|
// WRAPPER COMPONENT
|
|
54
110
|
// =============================================================================
|
|
55
111
|
|
|
56
|
-
/**
|
|
57
|
-
* Wrapper component that renders both the modal and the auto-banner.
|
|
58
|
-
* The banner handles its own visibility based on store state (isActive + showBanner).
|
|
59
|
-
* Clicking the banner opens the modal.
|
|
60
|
-
*/
|
|
61
|
-
function ImpersonateToolWrapper(props) {
|
|
62
|
-
const [forceOpen, setForceOpen] = (0, _react.useState)(false);
|
|
63
|
-
|
|
64
|
-
// Reset force state when parent closes the modal
|
|
65
|
-
(0, _react.useEffect)(() => {
|
|
66
|
-
if (!props.visible) {
|
|
67
|
-
setForceOpen(false);
|
|
68
|
-
}
|
|
69
|
-
}, [props.visible]);
|
|
70
|
-
const handleBannerPress = () => {
|
|
71
|
-
setForceOpen(true);
|
|
72
|
-
};
|
|
73
|
-
const handleClose = () => {
|
|
74
|
-
setForceOpen(false);
|
|
75
|
-
// Only notify parent if they had the modal open
|
|
76
|
-
if (props.visible) {
|
|
77
|
-
props.onClose();
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
81
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_ImpersonateModal.ImpersonateModal, {
|
|
82
|
-
...props,
|
|
83
|
-
visible: props.visible || forceOpen,
|
|
84
|
-
onClose: handleClose
|
|
85
|
-
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_ImpersonateBanner.ImpersonateBanner, {
|
|
86
|
-
position: "top",
|
|
87
|
-
offset: 50,
|
|
88
|
-
onPress: handleBannerPress
|
|
89
|
-
})]
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
112
|
// =============================================================================
|
|
94
113
|
// FACTORY FUNCTION
|
|
95
114
|
// =============================================================================
|
|
@@ -132,8 +151,10 @@ function ImpersonateToolWrapper(props) {
|
|
|
132
151
|
* showSettingsTab: true,
|
|
133
152
|
* });
|
|
134
153
|
*
|
|
135
|
-
* // In your app
|
|
154
|
+
* // In your app — mount the banner globally (outside FloatingDevTools)
|
|
155
|
+
* // so it survives reloads and stays visible after the modal is closed.
|
|
136
156
|
* <FloatingDevTools apps={[impersonateTool]} />
|
|
157
|
+
* <ImpersonateBanner position="top" offset={50} />
|
|
137
158
|
* ```
|
|
138
159
|
*/
|
|
139
160
|
function createImpersonateTool(config) {
|
|
@@ -155,6 +176,10 @@ function createImpersonateTool(config) {
|
|
|
155
176
|
_impersonateStore.impersonateStore.setDeveloperDefaults(defaults);
|
|
156
177
|
}
|
|
157
178
|
|
|
179
|
+
// Patch fetch and restore persisted impersonation state up-front. Fire and
|
|
180
|
+
// forget — bootstrapImpersonate guards itself against double-invocation.
|
|
181
|
+
void bootstrapImpersonate();
|
|
182
|
+
|
|
158
183
|
// Create the tool configuration object
|
|
159
184
|
const tool = {
|
|
160
185
|
id,
|
|
@@ -167,7 +192,7 @@ function createImpersonateTool(config) {
|
|
|
167
192
|
size: size,
|
|
168
193
|
color: "#F59E0B"
|
|
169
194
|
}),
|
|
170
|
-
component: props => /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
195
|
+
component: props => /*#__PURE__*/(0, _jsxRuntime.jsx)(_ImpersonateModal.ImpersonateModal, {
|
|
171
196
|
...props,
|
|
172
197
|
onSearchUsers: onSearchUsers,
|
|
173
198
|
onClearReactQuery: onClearReactQuery,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.createImpersonateSyncAdapter = createImpersonateSyncAdapter;
|
|
7
|
+
var _impersonateStore = require("../impersonate/utils/impersonateStore");
|
|
8
|
+
/**
|
|
9
|
+
* Create a sync adapter for the impersonate tool, consumed by
|
|
10
|
+
* @buoy-gg/external-sync's `useExternalSync` (structurally matches its
|
|
11
|
+
* ToolSyncAdapter interface so this package doesn't need a dependency on it).
|
|
12
|
+
*
|
|
13
|
+
* The dashboard mirrors the impersonation state and forwards every mutation
|
|
14
|
+
* back as an action — header patching, data nuking and persistence all run
|
|
15
|
+
* on the device, so impersonating from the desktop behaves exactly like
|
|
16
|
+
* tapping the modal on the phone.
|
|
17
|
+
*/
|
|
18
|
+
function createImpersonateSyncAdapter({
|
|
19
|
+
onSearchUsers
|
|
20
|
+
}) {
|
|
21
|
+
return {
|
|
22
|
+
version: 1,
|
|
23
|
+
getSnapshot: () => _impersonateStore.impersonateStore.getState(),
|
|
24
|
+
subscribe: onChange => _impersonateStore.impersonateStore.subscribe(onChange),
|
|
25
|
+
actions: {
|
|
26
|
+
searchUsers: params => onSearchUsers(params.query),
|
|
27
|
+
startImpersonation: params => _impersonateStore.impersonateStore.startImpersonation(params.user),
|
|
28
|
+
stopImpersonation: () => _impersonateStore.impersonateStore.stopImpersonation(),
|
|
29
|
+
pauseImpersonation: () => _impersonateStore.impersonateStore.pauseImpersonation(),
|
|
30
|
+
resumeImpersonation: () => _impersonateStore.impersonateStore.resumeImpersonation(),
|
|
31
|
+
updateSettings: params => _impersonateStore.impersonateStore.updateSettings(params.settings),
|
|
32
|
+
removeFromHistory: params => _impersonateStore.impersonateStore.removeFromHistory(params.userId),
|
|
33
|
+
clearHistory: () => _impersonateStore.impersonateStore.clearHistory()
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -46,6 +46,8 @@ export function ImpersonateBanner({
|
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
48
|
const handlePauseResume = async () => {
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.log("[impersonate] banner pause/resume pressed");
|
|
49
51
|
if (state.isPaused) {
|
|
50
52
|
await impersonateStore.resumeImpersonation();
|
|
51
53
|
} else {
|
|
@@ -59,6 +61,8 @@ export function ImpersonateBanner({
|
|
|
59
61
|
bottom: offset
|
|
60
62
|
};
|
|
61
63
|
const handleStop = async () => {
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.log("[impersonate] banner STOP pressed");
|
|
62
66
|
await impersonateStore.stopImpersonation();
|
|
63
67
|
};
|
|
64
68
|
return /*#__PURE__*/_jsx(View, {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ImpersonateOverlay
|
|
5
|
+
*
|
|
6
|
+
* Auto-rendered by FloatingDevTools (`@buoy-gg/core`) when
|
|
7
|
+
* `@buoy-gg/impersonate` is installed — the host does NOT need to mount
|
|
8
|
+
* anything. It renders the floating impersonation banner, which self-gates
|
|
9
|
+
* on store state (only visible while an impersonation session is active and
|
|
10
|
+
* `showBanner` is enabled), so it's safe to mount unconditionally.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from "react";
|
|
14
|
+
import { ImpersonateBanner } from "./ImpersonateBanner";
|
|
15
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
16
|
+
export function ImpersonateOverlay() {
|
|
17
|
+
return /*#__PURE__*/_jsx(ImpersonateBanner, {
|
|
18
|
+
position: "top",
|
|
19
|
+
offset: 50
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -236,10 +236,10 @@ class ImpersonateStore {
|
|
|
236
236
|
this.asyncStorageRef = asyncStorage;
|
|
237
237
|
try {
|
|
238
238
|
const stored = await asyncStorage.getItem(STORAGE_KEY);
|
|
239
|
-
|
|
239
|
+
// eslint-disable-next-line no-console
|
|
240
|
+
console.log(`[impersonate] initializeAsync read STORAGE_KEY=${STORAGE_KEY} stored=${stored ? stored.slice(0, 200) : "null"}`);
|
|
240
241
|
if (stored) {
|
|
241
242
|
const parsed = JSON.parse(stored);
|
|
242
|
-
console.log("[ImpersonateStore] initializeAsync - parsed dataNukeSettings:", parsed.dataNukeSettings);
|
|
243
243
|
this.state = {
|
|
244
244
|
...this.state,
|
|
245
245
|
headerKey: parsed.headerKey ?? this.state.headerKey,
|
|
@@ -255,7 +255,6 @@ class ImpersonateStore {
|
|
|
255
255
|
isPaused: parsed.isPaused ?? this.state.isPaused,
|
|
256
256
|
currentUser: parsed.currentUser ?? this.state.currentUser
|
|
257
257
|
};
|
|
258
|
-
console.log("[ImpersonateStore] initializeAsync - new dataNukeSettings:", this.state.dataNukeSettings);
|
|
259
258
|
|
|
260
259
|
// If we restored an active session, sync with listener
|
|
261
260
|
if (this.state.isActive && this.state.currentUser) {
|
|
@@ -263,8 +262,8 @@ class ImpersonateStore {
|
|
|
263
262
|
}
|
|
264
263
|
this.notify();
|
|
265
264
|
}
|
|
266
|
-
} catch
|
|
267
|
-
|
|
265
|
+
} catch {
|
|
266
|
+
// Persisted blob unreadable — fall through with in-memory defaults.
|
|
268
267
|
}
|
|
269
268
|
}
|
|
270
269
|
|
|
@@ -308,34 +307,57 @@ class ImpersonateStore {
|
|
|
308
307
|
return () => this.listeners.delete(listener);
|
|
309
308
|
};
|
|
310
309
|
|
|
310
|
+
// ===========================================================================
|
|
311
|
+
// REMOTE MIRROR MODE
|
|
312
|
+
// ===========================================================================
|
|
313
|
+
|
|
314
|
+
remoteHandler = null;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Remote mirror mode (desktop dashboard): while a handler is set, every
|
|
318
|
+
* mutation forwards (method, params) to the synced device instead of
|
|
319
|
+
* running locally — header patching, data nuking and persistence all
|
|
320
|
+
* happen on the device, and the resulting state comes back via
|
|
321
|
+
* replaceState(). Pass null to restore local behavior.
|
|
322
|
+
*/
|
|
323
|
+
setRemoteHandler(handler) {
|
|
324
|
+
this.remoteHandler = handler;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Replace the mirrored state from a synced device snapshot. Does not
|
|
329
|
+
* persist or touch the fetch listener — the device owns both.
|
|
330
|
+
*/
|
|
331
|
+
replaceState(state) {
|
|
332
|
+
this.state = state;
|
|
333
|
+
this.notify();
|
|
334
|
+
}
|
|
335
|
+
|
|
311
336
|
// ===========================================================================
|
|
312
337
|
// IMPERSONATION ACTIONS
|
|
313
338
|
// ===========================================================================
|
|
314
339
|
|
|
315
340
|
/**
|
|
316
|
-
* Start impersonating a user
|
|
341
|
+
* Start impersonating a user.
|
|
317
342
|
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
* 5. Persist settings
|
|
343
|
+
* Order matters: we update state + listener + UI BEFORE clearing caches,
|
|
344
|
+
* so refetches triggered by the cache clear go out with the new user's
|
|
345
|
+
* impersonation header. Doing it the other way around (nuke first) makes
|
|
346
|
+
* react-query refetch active queries with the OLD identity, producing
|
|
347
|
+
* the visual symptom of "I clicked a user and nothing changed."
|
|
324
348
|
*/
|
|
325
349
|
async startImpersonation(user) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
350
|
+
if (this.remoteHandler) {
|
|
351
|
+
this.remoteHandler("startImpersonation", {
|
|
352
|
+
user
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
330
356
|
const historyEntry = {
|
|
331
357
|
user,
|
|
332
358
|
lastUsedAt: new Date().toISOString()
|
|
333
359
|
};
|
|
334
|
-
|
|
335
|
-
// Update history (remove duplicate, add to front)
|
|
336
360
|
const newHistory = [historyEntry, ...this.state.history.filter(entry => entry.user.id !== user.id)].slice(0, MAX_HISTORY);
|
|
337
|
-
|
|
338
|
-
// Update state
|
|
339
361
|
this.state = {
|
|
340
362
|
...this.state,
|
|
341
363
|
isActive: true,
|
|
@@ -343,50 +365,54 @@ class ImpersonateStore {
|
|
|
343
365
|
history: newHistory
|
|
344
366
|
};
|
|
345
367
|
|
|
346
|
-
//
|
|
368
|
+
// 1) Point the fetch interceptor at the new user. Any request after this
|
|
369
|
+
// line carries the new header.
|
|
347
370
|
this.syncWithListener();
|
|
348
371
|
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// Notify subscribers
|
|
372
|
+
// 2) Tell React subscribers right away so the UI updates without waiting
|
|
373
|
+
// on AsyncStorage / data nuke.
|
|
353
374
|
this.notify();
|
|
375
|
+
|
|
376
|
+
// 3) Nuke caches — refetches now use the new identity.
|
|
377
|
+
await this.executeDataNuke();
|
|
378
|
+
|
|
379
|
+
// 4) Persist for next launch.
|
|
380
|
+
await this.persist();
|
|
354
381
|
}
|
|
355
382
|
|
|
356
383
|
/**
|
|
357
|
-
* Stop impersonating
|
|
384
|
+
* Stop impersonating.
|
|
358
385
|
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
* 2. Clear impersonation state
|
|
362
|
-
* 3. Sync with impersonateListener
|
|
386
|
+
* Same ordering rationale as startImpersonation: clear state + listener
|
|
387
|
+
* before nuking caches so subsequent refetches go out without the header.
|
|
363
388
|
*/
|
|
364
389
|
async stopImpersonation() {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
390
|
+
if (this.remoteHandler) {
|
|
391
|
+
this.remoteHandler("stopImpersonation");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// eslint-disable-next-line no-console
|
|
395
|
+
console.log(`[impersonate] stopImpersonation: isActive ${this.state.isActive} -> false, listeners=${this.listeners.size}`);
|
|
369
396
|
this.state = {
|
|
370
397
|
...this.state,
|
|
371
398
|
isActive: false,
|
|
372
399
|
isPaused: false,
|
|
373
400
|
currentUser: null
|
|
374
401
|
};
|
|
375
|
-
|
|
376
|
-
// Sync with listener
|
|
377
402
|
this.syncWithListener();
|
|
378
|
-
|
|
379
|
-
// Persist
|
|
380
|
-
await this.persist();
|
|
381
|
-
|
|
382
|
-
// Notify
|
|
383
403
|
this.notify();
|
|
404
|
+
await this.executeDataNuke();
|
|
405
|
+
await this.persist();
|
|
384
406
|
}
|
|
385
407
|
|
|
386
408
|
/**
|
|
387
409
|
* Pause impersonation (temporarily stop injecting headers)
|
|
388
410
|
*/
|
|
389
411
|
async pauseImpersonation() {
|
|
412
|
+
if (this.remoteHandler) {
|
|
413
|
+
this.remoteHandler("pauseImpersonation");
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
390
416
|
if (!this.state.isActive || this.state.isPaused) return;
|
|
391
417
|
this.state = {
|
|
392
418
|
...this.state,
|
|
@@ -407,6 +433,10 @@ class ImpersonateStore {
|
|
|
407
433
|
* Resume impersonation (start injecting headers again)
|
|
408
434
|
*/
|
|
409
435
|
async resumeImpersonation() {
|
|
436
|
+
if (this.remoteHandler) {
|
|
437
|
+
this.remoteHandler("resumeImpersonation");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
410
440
|
if (!this.state.isActive || !this.state.isPaused) return;
|
|
411
441
|
this.state = {
|
|
412
442
|
...this.state,
|
|
@@ -438,8 +468,12 @@ class ImpersonateStore {
|
|
|
438
468
|
* Update settings (header key, ignore patterns, data nuke settings, show banner)
|
|
439
469
|
*/
|
|
440
470
|
async updateSettings(settings) {
|
|
441
|
-
|
|
442
|
-
|
|
471
|
+
if (this.remoteHandler) {
|
|
472
|
+
this.remoteHandler("updateSettings", {
|
|
473
|
+
settings
|
|
474
|
+
});
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
443
477
|
this.state = {
|
|
444
478
|
...this.state,
|
|
445
479
|
headerKey: settings.headerKey ?? this.state.headerKey,
|
|
@@ -450,7 +484,6 @@ class ImpersonateStore {
|
|
|
450
484
|
...settings.dataNukeSettings
|
|
451
485
|
} : this.state.dataNukeSettings
|
|
452
486
|
};
|
|
453
|
-
console.log("[ImpersonateStore] New dataNukeSettings:", this.state.dataNukeSettings);
|
|
454
487
|
|
|
455
488
|
// Sync header key with listener
|
|
456
489
|
if (settings.headerKey || settings.ignorePatterns) {
|
|
@@ -468,6 +501,12 @@ class ImpersonateStore {
|
|
|
468
501
|
* Remove a user from history
|
|
469
502
|
*/
|
|
470
503
|
async removeFromHistory(userId) {
|
|
504
|
+
if (this.remoteHandler) {
|
|
505
|
+
this.remoteHandler("removeFromHistory", {
|
|
506
|
+
userId
|
|
507
|
+
});
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
471
510
|
this.state = {
|
|
472
511
|
...this.state,
|
|
473
512
|
history: this.state.history.filter(entry => entry.user.id !== userId)
|
|
@@ -480,6 +519,10 @@ class ImpersonateStore {
|
|
|
480
519
|
* Clear all history
|
|
481
520
|
*/
|
|
482
521
|
async clearHistory() {
|
|
522
|
+
if (this.remoteHandler) {
|
|
523
|
+
this.remoteHandler("clearHistory");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
483
526
|
this.state = {
|
|
484
527
|
...this.state,
|
|
485
528
|
history: []
|
|
@@ -567,19 +610,21 @@ class ImpersonateStore {
|
|
|
567
610
|
currentUser: this.state.currentUser
|
|
568
611
|
};
|
|
569
612
|
const serialized = JSON.stringify(toStore);
|
|
570
|
-
console.log("[ImpersonateStore] Persisting dataNukeSettings:", this.state.dataNukeSettings);
|
|
571
613
|
try {
|
|
572
614
|
// Prefer AsyncStorage for React Native
|
|
573
615
|
if (this.asyncStorageRef) {
|
|
574
616
|
await this.asyncStorageRef.setItem(STORAGE_KEY, serialized);
|
|
575
|
-
|
|
617
|
+
// eslint-disable-next-line no-console
|
|
618
|
+
console.log(`[impersonate] persist -> AsyncStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
|
|
576
619
|
} else {
|
|
577
620
|
// Fall back to localStorage (web)
|
|
578
621
|
storage.setItem(STORAGE_KEY, serialized);
|
|
579
|
-
|
|
622
|
+
// eslint-disable-next-line no-console
|
|
623
|
+
console.log(`[impersonate] persist -> localStorage isActive=${toStore.isActive} userId=${toStore.currentUser?.id ?? "null"}`);
|
|
580
624
|
}
|
|
581
625
|
} catch (e) {
|
|
582
|
-
|
|
626
|
+
// eslint-disable-next-line no-console
|
|
627
|
+
console.warn("[impersonate] persist failed", e);
|
|
583
628
|
}
|
|
584
629
|
}
|
|
585
630
|
|
package/lib/module/index.js
CHANGED
|
@@ -26,7 +26,13 @@
|
|
|
26
26
|
// FACTORY (Primary entry point)
|
|
27
27
|
// =============================================================================
|
|
28
28
|
|
|
29
|
-
export { createImpersonateTool } from "./preset";
|
|
29
|
+
export { createImpersonateTool, ImpersonateIcon } from "./preset";
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// EXTERNAL SYNC (Adapter for @buoy-gg/external-sync's useExternalSync)
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
export { createImpersonateSyncAdapter } from "./sync/impersonateSyncAdapter";
|
|
30
36
|
|
|
31
37
|
// =============================================================================
|
|
32
38
|
// COMPONENTS (For custom UI implementations)
|
|
@@ -34,6 +40,12 @@ export { createImpersonateTool } from "./preset";
|
|
|
34
40
|
|
|
35
41
|
export { ImpersonateModal } from "./impersonate/components/ImpersonateModal";
|
|
36
42
|
export { ImpersonateBanner, ImpersonateBannerMinimal } from "./impersonate/components/ImpersonateBanner";
|
|
43
|
+
/**
|
|
44
|
+
* Auto-rendered by FloatingDevTools when this package is installed — the
|
|
45
|
+
* host does not need to mount it. Renders the floating impersonation
|
|
46
|
+
* banner, which is only visible while an impersonation session is active.
|
|
47
|
+
*/
|
|
48
|
+
export { ImpersonateOverlay } from "./impersonate/components/ImpersonateOverlay";
|
|
37
49
|
export { UserSearchView } from "./impersonate/components/UserSearchView";
|
|
38
50
|
export { DataNukeSettings as DataNukeSettingsComponent } from "./impersonate/components/DataNukeSettings";
|
|
39
51
|
export { ImpersonateHistoryList } from "./impersonate/components/ImpersonateHistoryList";
|
package/lib/module/preset.js
CHANGED
|
@@ -7,24 +7,79 @@
|
|
|
7
7
|
* No default preset is exported because configuration (onSearchUsers) is required.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import React
|
|
10
|
+
import React from "react";
|
|
11
11
|
import { View, Text, StyleSheet } from "react-native";
|
|
12
12
|
import { ImpersonateModal } from "./impersonate/components/ImpersonateModal";
|
|
13
13
|
import { ImpersonateBanner } from "./impersonate/components/ImpersonateBanner";
|
|
14
14
|
import { impersonateStore } from "./impersonate/utils/impersonateStore";
|
|
15
|
+
import { impersonateListener } from "./impersonate/utils/impersonateListener";
|
|
16
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// EAGER BOOTSTRAP
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Patch fetch/XHR and restore persisted state the moment createImpersonateTool
|
|
21
|
+
// runs (typically at module load in the host app). If we waited until the
|
|
22
|
+
// modal mounts (the original lazy design), every request fired during app
|
|
23
|
+
// boot — bridge polls, user-data fetches, etc. — goes out without the
|
|
24
|
+
// impersonation header, even when the user already had impersonation active
|
|
25
|
+
// from a previous session. React Query then caches the real-account response,
|
|
26
|
+
// so even after the listener does eventually patch, no refetch happens.
|
|
27
|
+
//
|
|
28
|
+
// Side effect: any later fetch interceptor (e.g. @buoy-gg/network) installed
|
|
29
|
+
// AFTER us will wrap our wrapper, so its captured init.headers won't show the
|
|
30
|
+
// impersonation header. The wire request still includes it.
|
|
31
|
+
|
|
32
|
+
let bootstrapStarted = false;
|
|
33
|
+
async function bootstrapImpersonate() {
|
|
34
|
+
if (bootstrapStarted) return;
|
|
35
|
+
bootstrapStarted = true;
|
|
36
|
+
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.log("[impersonate] bootstrap: start");
|
|
39
|
+
|
|
40
|
+
// Patch fetch/XHR synchronously so any request between now and the async
|
|
41
|
+
// storage load goes through our wrapper. The wrapper is a no-op until
|
|
42
|
+
// userId is set (which happens once initializeAsync resolves).
|
|
43
|
+
if (!impersonateListener().isListening) {
|
|
44
|
+
impersonateListener().startListening();
|
|
45
|
+
}
|
|
15
46
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
47
|
+
// React Native: require AsyncStorage. require() is more reliable under
|
|
48
|
+
// Metro than dynamic import(); on web this throws and the store falls
|
|
49
|
+
// back to localStorage handled synchronously in its constructor.
|
|
50
|
+
try {
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
52
|
+
const mod = require("@react-native-async-storage/async-storage");
|
|
53
|
+
const AsyncStorage = mod?.default ?? mod;
|
|
54
|
+
if (!AsyncStorage || typeof AsyncStorage.getItem !== "function") {
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.warn("[impersonate] bootstrap: AsyncStorage shape unexpected", Object.keys(mod ?? {}));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const adapter = {
|
|
60
|
+
getItem: key => AsyncStorage.getItem(key),
|
|
61
|
+
setItem: (key, value) => AsyncStorage.setItem(key, value)
|
|
62
|
+
};
|
|
63
|
+
impersonateStore.setAsyncStorage(adapter);
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.log("[impersonate] bootstrap: AsyncStorage adapter installed");
|
|
66
|
+
await impersonateStore.initializeAsync(adapter);
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.log("[impersonate] bootstrap: initializeAsync done");
|
|
69
|
+
} catch (e) {
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.warn("[impersonate] bootstrap: failed", e);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
19
74
|
|
|
20
75
|
// =============================================================================
|
|
21
76
|
// ICON COMPONENT
|
|
22
77
|
// =============================================================================
|
|
23
|
-
|
|
78
|
+
|
|
24
79
|
/**
|
|
25
80
|
* Impersonate tool icon - a mask/person symbol
|
|
26
81
|
*/
|
|
27
|
-
function ImpersonateIcon({
|
|
82
|
+
export function ImpersonateIcon({
|
|
28
83
|
size,
|
|
29
84
|
color = "#F59E0B"
|
|
30
85
|
}) {
|
|
@@ -48,43 +103,6 @@ function ImpersonateIcon({
|
|
|
48
103
|
// WRAPPER COMPONENT
|
|
49
104
|
// =============================================================================
|
|
50
105
|
|
|
51
|
-
/**
|
|
52
|
-
* Wrapper component that renders both the modal and the auto-banner.
|
|
53
|
-
* The banner handles its own visibility based on store state (isActive + showBanner).
|
|
54
|
-
* Clicking the banner opens the modal.
|
|
55
|
-
*/
|
|
56
|
-
function ImpersonateToolWrapper(props) {
|
|
57
|
-
const [forceOpen, setForceOpen] = useState(false);
|
|
58
|
-
|
|
59
|
-
// Reset force state when parent closes the modal
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
if (!props.visible) {
|
|
62
|
-
setForceOpen(false);
|
|
63
|
-
}
|
|
64
|
-
}, [props.visible]);
|
|
65
|
-
const handleBannerPress = () => {
|
|
66
|
-
setForceOpen(true);
|
|
67
|
-
};
|
|
68
|
-
const handleClose = () => {
|
|
69
|
-
setForceOpen(false);
|
|
70
|
-
// Only notify parent if they had the modal open
|
|
71
|
-
if (props.visible) {
|
|
72
|
-
props.onClose();
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
76
|
-
children: [/*#__PURE__*/_jsx(ImpersonateModal, {
|
|
77
|
-
...props,
|
|
78
|
-
visible: props.visible || forceOpen,
|
|
79
|
-
onClose: handleClose
|
|
80
|
-
}), /*#__PURE__*/_jsx(ImpersonateBanner, {
|
|
81
|
-
position: "top",
|
|
82
|
-
offset: 50,
|
|
83
|
-
onPress: handleBannerPress
|
|
84
|
-
})]
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
106
|
// =============================================================================
|
|
89
107
|
// FACTORY FUNCTION
|
|
90
108
|
// =============================================================================
|
|
@@ -127,8 +145,10 @@ function ImpersonateToolWrapper(props) {
|
|
|
127
145
|
* showSettingsTab: true,
|
|
128
146
|
* });
|
|
129
147
|
*
|
|
130
|
-
* // In your app
|
|
148
|
+
* // In your app — mount the banner globally (outside FloatingDevTools)
|
|
149
|
+
* // so it survives reloads and stays visible after the modal is closed.
|
|
131
150
|
* <FloatingDevTools apps={[impersonateTool]} />
|
|
151
|
+
* <ImpersonateBanner position="top" offset={50} />
|
|
132
152
|
* ```
|
|
133
153
|
*/
|
|
134
154
|
export function createImpersonateTool(config) {
|
|
@@ -150,6 +170,10 @@ export function createImpersonateTool(config) {
|
|
|
150
170
|
impersonateStore.setDeveloperDefaults(defaults);
|
|
151
171
|
}
|
|
152
172
|
|
|
173
|
+
// Patch fetch and restore persisted impersonation state up-front. Fire and
|
|
174
|
+
// forget — bootstrapImpersonate guards itself against double-invocation.
|
|
175
|
+
void bootstrapImpersonate();
|
|
176
|
+
|
|
153
177
|
// Create the tool configuration object
|
|
154
178
|
const tool = {
|
|
155
179
|
id,
|
|
@@ -162,7 +186,7 @@ export function createImpersonateTool(config) {
|
|
|
162
186
|
size: size,
|
|
163
187
|
color: "#F59E0B"
|
|
164
188
|
}),
|
|
165
|
-
component: props => /*#__PURE__*/_jsx(
|
|
189
|
+
component: props => /*#__PURE__*/_jsx(ImpersonateModal, {
|
|
166
190
|
...props,
|
|
167
191
|
onSearchUsers: onSearchUsers,
|
|
168
192
|
onClearReactQuery: onClearReactQuery,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { impersonateStore } from "../impersonate/utils/impersonateStore";
|
|
4
|
+
/**
|
|
5
|
+
* Create a sync adapter for the impersonate tool, consumed by
|
|
6
|
+
* @buoy-gg/external-sync's `useExternalSync` (structurally matches its
|
|
7
|
+
* ToolSyncAdapter interface so this package doesn't need a dependency on it).
|
|
8
|
+
*
|
|
9
|
+
* The dashboard mirrors the impersonation state and forwards every mutation
|
|
10
|
+
* back as an action — header patching, data nuking and persistence all run
|
|
11
|
+
* on the device, so impersonating from the desktop behaves exactly like
|
|
12
|
+
* tapping the modal on the phone.
|
|
13
|
+
*/
|
|
14
|
+
export function createImpersonateSyncAdapter({
|
|
15
|
+
onSearchUsers
|
|
16
|
+
}) {
|
|
17
|
+
return {
|
|
18
|
+
version: 1,
|
|
19
|
+
getSnapshot: () => impersonateStore.getState(),
|
|
20
|
+
subscribe: onChange => impersonateStore.subscribe(onChange),
|
|
21
|
+
actions: {
|
|
22
|
+
searchUsers: params => onSearchUsers(params.query),
|
|
23
|
+
startImpersonation: params => impersonateStore.startImpersonation(params.user),
|
|
24
|
+
stopImpersonation: () => impersonateStore.stopImpersonation(),
|
|
25
|
+
pauseImpersonation: () => impersonateStore.pauseImpersonation(),
|
|
26
|
+
resumeImpersonation: () => impersonateStore.resumeImpersonation(),
|
|
27
|
+
updateSettings: params => impersonateStore.updateSettings(params.settings),
|
|
28
|
+
removeFromHistory: params => impersonateStore.removeFromHistory(params.userId),
|
|
29
|
+
clearHistory: () => impersonateStore.clearHistory()
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImpersonateBanner.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/ImpersonateBanner.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA+B,MAAM,OAAO,CAAC;AAUpD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEvD,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IACjE,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC5B,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,WAAW,EACX,QAAgB,EAChB,MAAW,GACZ,EAAE,mBAAmB,
|
|
1
|
+
{"version":3,"file":"ImpersonateBanner.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/ImpersonateBanner.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA+B,MAAM,OAAO,CAAC;AAUpD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEvD,MAAM,WAAW,mBAAoB,SAAQ,sBAAsB;IACjE,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC5B,2BAA2B;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,WAAW,EACX,QAAgB,EAChB,MAAW,GACZ,EAAE,mBAAmB,4BAgFrB;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,GACR,EAAE,IAAI,CAAC,sBAAsB,EAAE,SAAS,CAAC,4BAqBzC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ImpersonateOverlay
|
|
3
|
+
*
|
|
4
|
+
* Auto-rendered by FloatingDevTools (`@buoy-gg/core`) when
|
|
5
|
+
* `@buoy-gg/impersonate` is installed — the host does NOT need to mount
|
|
6
|
+
* anything. It renders the floating impersonation banner, which self-gates
|
|
7
|
+
* on store state (only visible while an impersonation session is active and
|
|
8
|
+
* `showBanner` is enabled), so it's safe to mount unconditionally.
|
|
9
|
+
*/
|
|
10
|
+
import React from "react";
|
|
11
|
+
export declare function ImpersonateOverlay(): React.JSX.Element;
|
|
12
|
+
//# sourceMappingURL=ImpersonateOverlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImpersonateOverlay.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/components/ImpersonateOverlay.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,wBAAgB,kBAAkB,sBAEjC"}
|
|
@@ -83,24 +83,35 @@ declare class ImpersonateStore {
|
|
|
83
83
|
* Returns unsubscribe function
|
|
84
84
|
*/
|
|
85
85
|
subscribe: (listener: StateListener) => (() => void);
|
|
86
|
+
private remoteHandler;
|
|
86
87
|
/**
|
|
87
|
-
*
|
|
88
|
+
* Remote mirror mode (desktop dashboard): while a handler is set, every
|
|
89
|
+
* mutation forwards (method, params) to the synced device instead of
|
|
90
|
+
* running locally — header patching, data nuking and persistence all
|
|
91
|
+
* happen on the device, and the resulting state comes back via
|
|
92
|
+
* replaceState(). Pass null to restore local behavior.
|
|
93
|
+
*/
|
|
94
|
+
setRemoteHandler(handler: ((method: string, params?: unknown) => void) | null): void;
|
|
95
|
+
/**
|
|
96
|
+
* Replace the mirrored state from a synced device snapshot. Does not
|
|
97
|
+
* persist or touch the fetch listener — the device owns both.
|
|
98
|
+
*/
|
|
99
|
+
replaceState(state: ImpersonationState): void;
|
|
100
|
+
/**
|
|
101
|
+
* Start impersonating a user.
|
|
88
102
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* 5. Persist settings
|
|
103
|
+
* Order matters: we update state + listener + UI BEFORE clearing caches,
|
|
104
|
+
* so refetches triggered by the cache clear go out with the new user's
|
|
105
|
+
* impersonation header. Doing it the other way around (nuke first) makes
|
|
106
|
+
* react-query refetch active queries with the OLD identity, producing
|
|
107
|
+
* the visual symptom of "I clicked a user and nothing changed."
|
|
95
108
|
*/
|
|
96
109
|
startImpersonation(user: User): Promise<void>;
|
|
97
110
|
/**
|
|
98
|
-
* Stop impersonating
|
|
111
|
+
* Stop impersonating.
|
|
99
112
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* 2. Clear impersonation state
|
|
103
|
-
* 3. Sync with impersonateListener
|
|
113
|
+
* Same ordering rationale as startImpersonation: clear state + listener
|
|
114
|
+
* before nuking caches so subsequent refetches go out without the header.
|
|
104
115
|
*/
|
|
105
116
|
stopImpersonation(): Promise<void>;
|
|
106
117
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impersonateStore.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/utils/impersonateStore.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,IAAI,EAEJ,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,UAAU,CAAC;AAmClB,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAEzD,UAAU,aAAa;IACrB,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,YAAY,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAiDD;;;;;;;;GAQG;AACH,cAAM,gBAAgB;IACpB,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAGP;IAChB,OAAO,CAAC,mBAAmB,CAA8B;IACzD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,iBAAiB,CAAoC;;IAqB7D;;OAEG;IACH,OAAO,CAAC,eAAe;IA2CvB;;;OAGG;IACH,eAAe,CAAC,YAAY,EAAE;QAC5B,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QACjD,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACxD,GAAG,IAAI;IASR;;;;OAIG;IACH,oBAAoB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAkBzD;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAW5B;;OAEG;IACH,oBAAoB,IAAI,mBAAmB,GAAG,IAAI;IAIlD;;;OAGG;IACG,eAAe,CAAC,YAAY,CAAC,EAAE;QACnC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QACjD,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACxD,GAAG,OAAO,CAAC,IAAI,CAAC;IA0CjB;;OAEG;IACH,qBAAqB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI;IAQrD;;OAEG;IACH,QAAQ,IAAI,kBAAkB;IAI9B;;;OAGG;IACH,WAAW,QAAO,kBAAkB,CAElC;IAEF;;;OAGG;IACH,SAAS,GAAI,UAAU,aAAa,KAAG,CAAC,MAAM,IAAI,CAAC,CAGjD;IAMF
|
|
1
|
+
{"version":3,"file":"impersonateStore.d.ts","sourceRoot":"","sources":["../../../../src/impersonate/utils/impersonateStore.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,IAAI,EAEJ,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,UAAU,CAAC;AAmClB,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAEzD,UAAU,aAAa;IACrB,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,YAAY,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAiDD;;;;;;;;GAQG;AACH,cAAM,gBAAgB;IACpB,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAGP;IAChB,OAAO,CAAC,mBAAmB,CAA8B;IACzD,OAAO,CAAC,mBAAmB,CAA6B;IACxD,OAAO,CAAC,iBAAiB,CAAoC;;IAqB7D;;OAEG;IACH,OAAO,CAAC,eAAe;IA2CvB;;;OAGG;IACH,eAAe,CAAC,YAAY,EAAE;QAC5B,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QACjD,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACxD,GAAG,IAAI;IASR;;;;OAIG;IACH,oBAAoB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAkBzD;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAW5B;;OAEG;IACH,oBAAoB,IAAI,mBAAmB,GAAG,IAAI;IAIlD;;;OAGG;IACG,eAAe,CAAC,YAAY,CAAC,EAAE;QACnC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QACjD,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACxD,GAAG,OAAO,CAAC,IAAI,CAAC;IA0CjB;;OAEG;IACH,qBAAqB,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI;IAQrD;;OAEG;IACH,QAAQ,IAAI,kBAAkB;IAI9B;;;OAGG;IACH,WAAW,QAAO,kBAAkB,CAElC;IAEF;;;OAGG;IACH,SAAS,GAAI,UAAU,aAAa,KAAG,CAAC,MAAM,IAAI,CAAC,CAGjD;IAMF,OAAO,CAAC,aAAa,CACd;IAEP;;;;;;OAMG;IACH,gBAAgB,CACd,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAI,GAC3D,IAAI;IAIP;;;OAGG;IACH,YAAY,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI;IAS7C;;;;;;;;OAQG;IACG,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCnD;;;;;OAKG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAuBxC;;OAEG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAsBzC;;OAEG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAsB1C;;OAEG;IACG,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5C;;OAEG;IACG,cAAc,CAClB,QAAQ,EAAE,OAAO,CACf,IAAI,CAAC,kBAAkB,EAAE,WAAW,GAAG,gBAAgB,GAAG,YAAY,CAAC,GAAG;QACxE,gBAAgB,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;KAC9C,CACF,GACA,OAAO,CAAC,IAAI,CAAC;IA4BhB;;OAEG;IACG,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAatD;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBnC;;OAEG;YACW,eAAe;IAqC7B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;YACW,OAAO;IA0CrB;;OAEG;IACH,OAAO,CAAC,MAAM;CASf;AAMD,eAAO,MAAM,gBAAgB,kBAAyB,CAAC"}
|
|
@@ -19,9 +19,16 @@
|
|
|
19
19
|
* <FloatingDevTools apps={[impersonateTool]} />
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
|
-
export { createImpersonateTool } from "./preset";
|
|
22
|
+
export { createImpersonateTool, ImpersonateIcon } from "./preset";
|
|
23
|
+
export { createImpersonateSyncAdapter } from "./sync/impersonateSyncAdapter";
|
|
23
24
|
export { ImpersonateModal } from "./impersonate/components/ImpersonateModal";
|
|
24
25
|
export { ImpersonateBanner, ImpersonateBannerMinimal, } from "./impersonate/components/ImpersonateBanner";
|
|
26
|
+
/**
|
|
27
|
+
* Auto-rendered by FloatingDevTools when this package is installed — the
|
|
28
|
+
* host does not need to mount it. Renders the floating impersonation
|
|
29
|
+
* banner, which is only visible while an impersonation session is active.
|
|
30
|
+
*/
|
|
31
|
+
export { ImpersonateOverlay } from "./impersonate/components/ImpersonateOverlay";
|
|
25
32
|
export { UserSearchView } from "./impersonate/components/UserSearchView";
|
|
26
33
|
export { DataNukeSettings as DataNukeSettingsComponent } from "./impersonate/components/DataNukeSettings";
|
|
27
34
|
export { ImpersonateHistoryList } from "./impersonate/components/ImpersonateHistoryList";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAMlE,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAM7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAC7E,OAAO,EACL,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,4CAA4C,CAAC;AACpD;;;;GAIG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,gBAAgB,IAAI,yBAAyB,EAAE,MAAM,2CAA2C,CAAC;AAC1G,OAAO,EAAE,sBAAsB,EAAE,MAAM,iDAAiD,CAAC;AACzF,OAAO,EAAE,oBAAoB,EAAE,MAAM,+CAA+C,CAAC;AAMrF,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACpE,YAAY,EACV,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,2CAA2C,CAAC;AAClF,YAAY,EAAE,2BAA2B,EAAE,MAAM,2CAA2C,CAAC;AAM7F,YAAY,EACV,IAAI,EACJ,gBAAgB,IAAI,oBAAoB,EACxC,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACrB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAM7B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,EACvB,oBAAoB,EACpB,eAAe,EACf,qBAAqB,GACtB,MAAM,yCAAyC,CAAC"}
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { ImpersonateBanner } from "./impersonate/components/ImpersonateBanner";
|
|
9
9
|
import type { ImpersonateToolConfig } from "./impersonate/types";
|
|
10
|
+
interface IconProps {
|
|
11
|
+
size: number;
|
|
12
|
+
color?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Impersonate tool icon - a mask/person symbol
|
|
16
|
+
*/
|
|
17
|
+
export declare function ImpersonateIcon({ size, color }: IconProps): React.JSX.Element;
|
|
10
18
|
/**
|
|
11
19
|
* Create an impersonate tool with custom configuration
|
|
12
20
|
*
|
|
@@ -45,8 +53,10 @@ import type { ImpersonateToolConfig } from "./impersonate/types";
|
|
|
45
53
|
* showSettingsTab: true,
|
|
46
54
|
* });
|
|
47
55
|
*
|
|
48
|
-
* // In your app
|
|
56
|
+
* // In your app — mount the banner globally (outside FloatingDevTools)
|
|
57
|
+
* // so it survives reloads and stays visible after the modal is closed.
|
|
49
58
|
* <FloatingDevTools apps={[impersonateTool]} />
|
|
59
|
+
* <ImpersonateBanner position="top" offset={50} />
|
|
50
60
|
* ```
|
|
51
61
|
*/
|
|
52
62
|
export declare function createImpersonateTool(config: ImpersonateToolConfig): {
|
|
@@ -68,4 +78,5 @@ export declare function createImpersonateTool(config: ImpersonateToolConfig): {
|
|
|
68
78
|
};
|
|
69
79
|
Banner: typeof ImpersonateBanner;
|
|
70
80
|
};
|
|
81
|
+
export {};
|
|
71
82
|
//# sourceMappingURL=preset.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../../src/preset.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../../src/preset.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAG/E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAyEjE,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,KAAiB,EAAE,EAAE,SAAS,qBAmBrE;AAWD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB;;;;;qBA6B9C;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;uBAGd;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;QAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE;;;;;EAmB3H"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { User, ImpersonationState } from "../impersonate/types";
|
|
2
|
+
interface CreateImpersonateSyncAdapterOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Same user-search callback you pass to the ImpersonateModal — the
|
|
5
|
+
* dashboard's search box proxies here.
|
|
6
|
+
*/
|
|
7
|
+
onSearchUsers: (query: string) => Promise<User[]>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Create a sync adapter for the impersonate tool, consumed by
|
|
11
|
+
* @buoy-gg/external-sync's `useExternalSync` (structurally matches its
|
|
12
|
+
* ToolSyncAdapter interface so this package doesn't need a dependency on it).
|
|
13
|
+
*
|
|
14
|
+
* The dashboard mirrors the impersonation state and forwards every mutation
|
|
15
|
+
* back as an action — header patching, data nuking and persistence all run
|
|
16
|
+
* on the device, so impersonating from the desktop behaves exactly like
|
|
17
|
+
* tapping the modal on the phone.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createImpersonateSyncAdapter({ onSearchUsers, }: CreateImpersonateSyncAdapterOptions): {
|
|
20
|
+
version: number;
|
|
21
|
+
getSnapshot: () => ImpersonationState;
|
|
22
|
+
subscribe: (onChange: () => void) => () => void;
|
|
23
|
+
actions: {
|
|
24
|
+
searchUsers: (params: unknown) => Promise<User[]>;
|
|
25
|
+
startImpersonation: (params: unknown) => Promise<void>;
|
|
26
|
+
stopImpersonation: () => Promise<void>;
|
|
27
|
+
pauseImpersonation: () => Promise<void>;
|
|
28
|
+
resumeImpersonation: () => Promise<void>;
|
|
29
|
+
updateSettings: (params: unknown) => Promise<void>;
|
|
30
|
+
removeFromHistory: (params: unknown) => Promise<void>;
|
|
31
|
+
clearHistory: () => Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=impersonateSyncAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"impersonateSyncAdapter.d.ts","sourceRoot":"","sources":["../../../src/sync/impersonateSyncAdapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAErE,UAAU,mCAAmC;IAC3C;;;OAGG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;CACnD;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,aAAa,GACd,EAAE,mCAAmC;;uBAGjB,kBAAkB;0BACb,MAAM,IAAI;;8BAER,OAAO;qCAEA,OAAO;;;;iCAKX,OAAO;oCAKJ,OAAO;;;EAOxC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/impersonate",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "User impersonation tool for Buoy DevTools - inject custom headers for admin testing",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"sideEffects": false,
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@buoy-gg/shared-ui": "
|
|
29
|
+
"@buoy-gg/shared-ui": "3.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": "*",
|