@buoy-gg/impersonate 1.0.3-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +58 -0
  2. package/lib/commonjs/impersonate/components/DataNukeSettings.js +715 -0
  3. package/lib/commonjs/impersonate/components/ImpersonateBanner.js +217 -0
  4. package/lib/commonjs/impersonate/components/ImpersonateHistoryList.js +173 -0
  5. package/lib/commonjs/impersonate/components/ImpersonateModal.js +304 -0
  6. package/lib/commonjs/impersonate/components/ImpersonateStatusBar.js +130 -0
  7. package/lib/commonjs/impersonate/components/UserAvatar.js +146 -0
  8. package/lib/commonjs/impersonate/components/UserCard.js +200 -0
  9. package/lib/commonjs/impersonate/components/UserSearchView.js +227 -0
  10. package/lib/commonjs/impersonate/components/index.js +85 -0
  11. package/lib/commonjs/impersonate/hooks/index.js +64 -0
  12. package/lib/commonjs/impersonate/hooks/useAutoClearAsyncStorage.js +144 -0
  13. package/lib/commonjs/impersonate/hooks/useAutoClearReactQuery.js +155 -0
  14. package/lib/commonjs/impersonate/hooks/useAutoClearRedux.js +188 -0
  15. package/lib/commonjs/impersonate/hooks/useImpersonate.js +215 -0
  16. package/lib/commonjs/impersonate/hooks/useImpersonateHistory.js +56 -0
  17. package/lib/commonjs/impersonate/index.js +49 -0
  18. package/lib/commonjs/impersonate/types/index.js +16 -0
  19. package/lib/commonjs/impersonate/types/types.js +1 -0
  20. package/lib/commonjs/impersonate/utils/impersonateListener.js +280 -0
  21. package/lib/commonjs/impersonate/utils/impersonateStore.js +607 -0
  22. package/lib/commonjs/impersonate/utils/index.js +49 -0
  23. package/lib/commonjs/index.js +118 -0
  24. package/lib/commonjs/package.json +1 -0
  25. package/lib/commonjs/preset.js +214 -0
  26. package/lib/module/impersonate/components/DataNukeSettings.js +710 -0
  27. package/lib/module/impersonate/components/ImpersonateBanner.js +211 -0
  28. package/lib/module/impersonate/components/ImpersonateHistoryList.js +168 -0
  29. package/lib/module/impersonate/components/ImpersonateModal.js +300 -0
  30. package/lib/module/impersonate/components/ImpersonateStatusBar.js +125 -0
  31. package/lib/module/impersonate/components/UserAvatar.js +140 -0
  32. package/lib/module/impersonate/components/UserCard.js +195 -0
  33. package/lib/module/impersonate/components/UserSearchView.js +222 -0
  34. package/lib/module/impersonate/components/index.js +11 -0
  35. package/lib/module/impersonate/hooks/index.js +7 -0
  36. package/lib/module/impersonate/hooks/useAutoClearAsyncStorage.js +140 -0
  37. package/lib/module/impersonate/hooks/useAutoClearReactQuery.js +151 -0
  38. package/lib/module/impersonate/hooks/useAutoClearRedux.js +183 -0
  39. package/lib/module/impersonate/hooks/useImpersonate.js +212 -0
  40. package/lib/module/impersonate/hooks/useImpersonateHistory.js +52 -0
  41. package/lib/module/impersonate/index.js +13 -0
  42. package/lib/module/impersonate/types/index.js +3 -0
  43. package/lib/module/impersonate/types/types.js +1 -0
  44. package/lib/module/impersonate/utils/impersonateListener.js +271 -0
  45. package/lib/module/impersonate/utils/impersonateStore.js +604 -0
  46. package/lib/module/impersonate/utils/index.js +4 -0
  47. package/lib/module/index.js +103 -0
  48. package/lib/module/preset.js +209 -0
  49. package/lib/typescript/impersonate/components/DataNukeSettings.d.ts +37 -0
  50. package/lib/typescript/impersonate/components/DataNukeSettings.d.ts.map +1 -0
  51. package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts +40 -0
  52. package/lib/typescript/impersonate/components/ImpersonateBanner.d.ts.map +1 -0
  53. package/lib/typescript/impersonate/components/ImpersonateHistoryList.d.ts +24 -0
  54. package/lib/typescript/impersonate/components/ImpersonateHistoryList.d.ts.map +1 -0
  55. package/lib/typescript/impersonate/components/ImpersonateModal.d.ts +10 -0
  56. package/lib/typescript/impersonate/components/ImpersonateModal.d.ts.map +1 -0
  57. package/lib/typescript/impersonate/components/ImpersonateStatusBar.d.ts +15 -0
  58. package/lib/typescript/impersonate/components/ImpersonateStatusBar.d.ts.map +1 -0
  59. package/lib/typescript/impersonate/components/UserAvatar.d.ts +32 -0
  60. package/lib/typescript/impersonate/components/UserAvatar.d.ts.map +1 -0
  61. package/lib/typescript/impersonate/components/UserCard.d.ts +28 -0
  62. package/lib/typescript/impersonate/components/UserCard.d.ts.map +1 -0
  63. package/lib/typescript/impersonate/components/UserSearchView.d.ts +31 -0
  64. package/lib/typescript/impersonate/components/UserSearchView.d.ts.map +1 -0
  65. package/lib/typescript/impersonate/components/index.d.ts +16 -0
  66. package/lib/typescript/impersonate/components/index.d.ts.map +1 -0
  67. package/lib/typescript/impersonate/hooks/index.d.ts +11 -0
  68. package/lib/typescript/impersonate/hooks/index.d.ts.map +1 -0
  69. package/lib/typescript/impersonate/hooks/useAutoClearAsyncStorage.d.ts +48 -0
  70. package/lib/typescript/impersonate/hooks/useAutoClearAsyncStorage.d.ts.map +1 -0
  71. package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.d.ts +48 -0
  72. package/lib/typescript/impersonate/hooks/useAutoClearReactQuery.d.ts.map +1 -0
  73. package/lib/typescript/impersonate/hooks/useAutoClearRedux.d.ts +78 -0
  74. package/lib/typescript/impersonate/hooks/useAutoClearRedux.d.ts.map +1 -0
  75. package/lib/typescript/impersonate/hooks/useImpersonate.d.ts +76 -0
  76. package/lib/typescript/impersonate/hooks/useImpersonate.d.ts.map +1 -0
  77. package/lib/typescript/impersonate/hooks/useImpersonateHistory.d.ts +43 -0
  78. package/lib/typescript/impersonate/hooks/useImpersonateHistory.d.ts.map +1 -0
  79. package/lib/typescript/impersonate/index.d.ts +5 -0
  80. package/lib/typescript/impersonate/index.d.ts.map +1 -0
  81. package/lib/typescript/impersonate/types/index.d.ts +2 -0
  82. package/lib/typescript/impersonate/types/index.d.ts.map +1 -0
  83. package/lib/typescript/impersonate/types/types.d.ts +177 -0
  84. package/lib/typescript/impersonate/types/types.d.ts.map +1 -0
  85. package/lib/typescript/impersonate/utils/impersonateListener.d.ts +115 -0
  86. package/lib/typescript/impersonate/utils/impersonateListener.d.ts.map +1 -0
  87. package/lib/typescript/impersonate/utils/impersonateStore.d.ts +151 -0
  88. package/lib/typescript/impersonate/utils/impersonateStore.d.ts.map +1 -0
  89. package/lib/typescript/impersonate/utils/index.d.ts +3 -0
  90. package/lib/typescript/impersonate/utils/index.d.ts.map +1 -0
  91. package/lib/typescript/index.d.ts +80 -0
  92. package/lib/typescript/index.d.ts.map +1 -0
  93. package/lib/typescript/preset.d.ts +71 -0
  94. package/lib/typescript/preset.d.ts.map +1 -0
  95. package/package.json +78 -0
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useImpersonate = useImpersonate;
7
+ var _react = require("react");
8
+ var _impersonateStore = require("../utils/impersonateStore");
9
+ var _impersonateListener = require("../utils/impersonateListener");
10
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } /**
11
+ * useImpersonate Hook
12
+ *
13
+ * Main hook for managing impersonation state and actions.
14
+ * Provides all the controls needed to search users, start/stop
15
+ * impersonation, and manage settings.
16
+ */
17
+ /**
18
+ * Hook for managing impersonation
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * function ImpersonatePanel() {
23
+ * const {
24
+ * isActive,
25
+ * currentUser,
26
+ * searchUsers,
27
+ * searchResults,
28
+ * startImpersonation,
29
+ * stopImpersonation,
30
+ * } = useImpersonate({
31
+ * onSearchUsers: async (query) => api.searchUsers(query),
32
+ * onClearReactQuery: () => queryClient.clear(),
33
+ * });
34
+ *
35
+ * return (
36
+ * <View>
37
+ * {isActive ? (
38
+ * <Text>Impersonating: {currentUser?.email}</Text>
39
+ * ) : (
40
+ * <Text>Not impersonating</Text>
41
+ * )}
42
+ * </View>
43
+ * );
44
+ * }
45
+ * ```
46
+ */
47
+ function useImpersonate(options = {}) {
48
+ const [searchQuery, setSearchQuery] = (0, _react.useState)("");
49
+ const [searchResults, setSearchResults] = (0, _react.useState)([]);
50
+ const [isSearching, setIsSearching] = (0, _react.useState)(false);
51
+ const [searchError, setSearchError] = (0, _react.useState)(null);
52
+
53
+ // Subscribe to store state using useSyncExternalStore
54
+ const state = (0, _react.useSyncExternalStore)(_impersonateStore.impersonateStore.subscribe, _impersonateStore.impersonateStore.getSnapshot, _impersonateStore.impersonateStore.getSnapshot // Server snapshot (same for SSR)
55
+ );
56
+
57
+ // Register nuke callbacks when they change
58
+ (0, _react.useEffect)(() => {
59
+ _impersonateStore.impersonateStore.registerNukeCallbacks({
60
+ reactQuery: options.onClearReactQuery,
61
+ redux: options.onClearRedux,
62
+ asyncStorage: options.onClearAsyncStorage,
63
+ mmkv: options.onClearMMKV
64
+ });
65
+ }, [options.onClearReactQuery, options.onClearRedux, options.onClearAsyncStorage, options.onClearMMKV]);
66
+
67
+ // Set up AsyncStorage reference immediately for writes, then load persisted state
68
+ (0, _react.useEffect)(() => {
69
+ let isMounted = true;
70
+ const setupAsyncStorage = async () => {
71
+ try {
72
+ // Dynamically import AsyncStorage
73
+ const AsyncStorage = await Promise.resolve().then(() => _interopRequireWildcard(require("@react-native-async-storage/async-storage"))).then(m => m.default);
74
+ const storageAdapter = {
75
+ getItem: key => AsyncStorage.getItem(key),
76
+ setItem: (key, value) => AsyncStorage.setItem(key, value)
77
+ };
78
+
79
+ // Set storage reference FIRST so writes work immediately
80
+ _impersonateStore.impersonateStore.setAsyncStorage(storageAdapter);
81
+
82
+ // Then load persisted state
83
+ if (isMounted) {
84
+ await _impersonateStore.impersonateStore.initializeAsync(storageAdapter);
85
+ }
86
+ } catch {
87
+ // AsyncStorage not available - using localStorage fallback (web)
88
+ }
89
+ };
90
+ setupAsyncStorage();
91
+
92
+ // Start listener
93
+ if (!(0, _impersonateListener.impersonateListener)().isListening) {
94
+ (0, _impersonateListener.impersonateListener)().startListening();
95
+ }
96
+ return () => {
97
+ isMounted = false;
98
+ };
99
+ // Don't stop listener on unmount - headers should keep being injected
100
+ }, []);
101
+
102
+ // Search users
103
+ const searchUsers = (0, _react.useCallback)(async query => {
104
+ setSearchQuery(query);
105
+ setSearchError(null);
106
+ if (!query.trim()) {
107
+ setSearchResults([]);
108
+ return;
109
+ }
110
+ if (!options.onSearchUsers) {
111
+ setSearchError("Search not configured");
112
+ setSearchResults([]);
113
+ return;
114
+ }
115
+ setIsSearching(true);
116
+ try {
117
+ const results = await options.onSearchUsers(query);
118
+ setSearchResults(results);
119
+ } catch (error) {
120
+ setSearchError(error instanceof Error ? error.message : "Search failed");
121
+ setSearchResults([]);
122
+ } finally {
123
+ setIsSearching(false);
124
+ }
125
+ }, [options.onSearchUsers]);
126
+
127
+ // Clear search
128
+ const clearSearch = (0, _react.useCallback)(() => {
129
+ setSearchQuery("");
130
+ setSearchResults([]);
131
+ setSearchError(null);
132
+ }, []);
133
+
134
+ // Start impersonation
135
+ const startImpersonation = (0, _react.useCallback)(async user => {
136
+ await _impersonateStore.impersonateStore.startImpersonation(user);
137
+ }, []);
138
+
139
+ // Stop impersonation
140
+ const stopImpersonation = (0, _react.useCallback)(async () => {
141
+ await _impersonateStore.impersonateStore.stopImpersonation();
142
+ }, []);
143
+
144
+ // Quick switch
145
+ const quickSwitch = (0, _react.useCallback)(async user => {
146
+ await _impersonateStore.impersonateStore.quickSwitch(user);
147
+ }, []);
148
+
149
+ // Update header key
150
+ const updateHeaderKey = (0, _react.useCallback)(async key => {
151
+ await _impersonateStore.impersonateStore.updateSettings({
152
+ headerKey: key
153
+ });
154
+ }, []);
155
+
156
+ // Update ignore patterns
157
+ const updateIgnorePatterns = (0, _react.useCallback)(async patterns => {
158
+ await _impersonateStore.impersonateStore.updateSettings({
159
+ ignorePatterns: patterns
160
+ });
161
+ }, []);
162
+
163
+ // Update data nuke settings
164
+ const updateDataNukeSettings = (0, _react.useCallback)(async settings => {
165
+ await _impersonateStore.impersonateStore.updateSettings({
166
+ dataNukeSettings: settings
167
+ });
168
+ }, []);
169
+
170
+ // Update show banner
171
+ const updateShowBanner = (0, _react.useCallback)(async show => {
172
+ await _impersonateStore.impersonateStore.updateSettings({
173
+ showBanner: show
174
+ });
175
+ }, []);
176
+
177
+ // Remove from history
178
+ const removeFromHistory = (0, _react.useCallback)(async userId => {
179
+ await _impersonateStore.impersonateStore.removeFromHistory(userId);
180
+ }, []);
181
+
182
+ // Clear history
183
+ const clearHistory = (0, _react.useCallback)(async () => {
184
+ await _impersonateStore.impersonateStore.clearHistory();
185
+ }, []);
186
+ return {
187
+ // State
188
+ isActive: state.isActive,
189
+ currentUser: state.currentUser,
190
+ headerKey: state.headerKey,
191
+ ignorePatterns: state.ignorePatterns,
192
+ dataNukeSettings: state.dataNukeSettings,
193
+ showBanner: state.showBanner,
194
+ history: state.history,
195
+ // Search
196
+ searchQuery,
197
+ searchResults,
198
+ isSearching,
199
+ searchError,
200
+ searchUsers,
201
+ clearSearch,
202
+ // Actions
203
+ startImpersonation,
204
+ stopImpersonation,
205
+ quickSwitch,
206
+ // Settings
207
+ updateHeaderKey,
208
+ updateIgnorePatterns,
209
+ updateDataNukeSettings,
210
+ updateShowBanner,
211
+ // History
212
+ removeFromHistory,
213
+ clearHistory
214
+ };
215
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useImpersonateHistory = useImpersonateHistory;
7
+ var _react = require("react");
8
+ var _impersonateStore = require("../utils/impersonateStore");
9
+ /**
10
+ * useImpersonateHistory Hook
11
+ *
12
+ * Specialized hook for working with impersonation history.
13
+ * Use this when you only need history functionality.
14
+ */
15
+
16
+ /**
17
+ * Hook for managing impersonation history
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * function HistoryList() {
22
+ * const { history, currentUserId, quickSwitch } = useImpersonateHistory();
23
+ *
24
+ * return (
25
+ * <FlatList
26
+ * data={history}
27
+ * renderItem={({ item }) => (
28
+ * <TouchableOpacity onPress={() => quickSwitch(item)}>
29
+ * <Text>{item.email}</Text>
30
+ * {currentUserId === item.id && <Badge>Active</Badge>}
31
+ * </TouchableOpacity>
32
+ * )}
33
+ * />
34
+ * );
35
+ * }
36
+ * ```
37
+ */
38
+ function useImpersonateHistory() {
39
+ const state = (0, _react.useSyncExternalStore)(_impersonateStore.impersonateStore.subscribe, _impersonateStore.impersonateStore.getSnapshot, _impersonateStore.impersonateStore.getSnapshot);
40
+ const quickSwitch = (0, _react.useCallback)(async user => {
41
+ await _impersonateStore.impersonateStore.quickSwitch(user);
42
+ }, []);
43
+ const removeFromHistory = (0, _react.useCallback)(async userId => {
44
+ await _impersonateStore.impersonateStore.removeFromHistory(userId);
45
+ }, []);
46
+ const clearHistory = (0, _react.useCallback)(async () => {
47
+ await _impersonateStore.impersonateStore.clearHistory();
48
+ }, []);
49
+ return {
50
+ history: state.history,
51
+ currentUserId: state.currentUser?.id ?? null,
52
+ quickSwitch,
53
+ removeFromHistory,
54
+ clearHistory
55
+ };
56
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ var _types = require("./types");
7
+ Object.keys(_types).forEach(function (key) {
8
+ if (key === "default" || key === "__esModule") return;
9
+ if (key in exports && exports[key] === _types[key]) return;
10
+ Object.defineProperty(exports, key, {
11
+ enumerable: true,
12
+ get: function () {
13
+ return _types[key];
14
+ }
15
+ });
16
+ });
17
+ var _utils = require("./utils");
18
+ Object.keys(_utils).forEach(function (key) {
19
+ if (key === "default" || key === "__esModule") return;
20
+ if (key in exports && exports[key] === _utils[key]) return;
21
+ Object.defineProperty(exports, key, {
22
+ enumerable: true,
23
+ get: function () {
24
+ return _utils[key];
25
+ }
26
+ });
27
+ });
28
+ var _hooks = require("./hooks");
29
+ Object.keys(_hooks).forEach(function (key) {
30
+ if (key === "default" || key === "__esModule") return;
31
+ if (key in exports && exports[key] === _hooks[key]) return;
32
+ Object.defineProperty(exports, key, {
33
+ enumerable: true,
34
+ get: function () {
35
+ return _hooks[key];
36
+ }
37
+ });
38
+ });
39
+ var _components = require("./components");
40
+ Object.keys(_components).forEach(function (key) {
41
+ if (key === "default" || key === "__esModule") return;
42
+ if (key in exports && exports[key] === _components[key]) return;
43
+ Object.defineProperty(exports, key, {
44
+ enumerable: true,
45
+ get: function () {
46
+ return _components[key];
47
+ }
48
+ });
49
+ });
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ var _types = require("./types");
7
+ Object.keys(_types).forEach(function (key) {
8
+ if (key === "default" || key === "__esModule") return;
9
+ if (key in exports && exports[key] === _types[key]) return;
10
+ Object.defineProperty(exports, key, {
11
+ enumerable: true,
12
+ get: function () {
13
+ return _types[key];
14
+ }
15
+ });
16
+ });
@@ -0,0 +1 @@
1
+ "use strict";
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getImpersonatedUserId = getImpersonatedUserId;
7
+ exports.impersonateListener = impersonateListener;
8
+ exports.isImpersonating = isImpersonating;
9
+ exports.setImpersonateConfig = setImpersonateConfig;
10
+ exports.startImpersonateListener = startImpersonateListener;
11
+ exports.stopImpersonateListener = stopImpersonateListener;
12
+ /**
13
+ * Impersonate Listener - Header Injection via Fetch/XHR Monkey-Patching
14
+ *
15
+ * This module intercepts all network requests and injects the impersonation
16
+ * header when impersonation is active. It follows the same singleton pattern
17
+ * as @buoy-gg/network's networkListener.
18
+ */
19
+
20
+ // Extended XMLHttpRequest interface for tracking URL
21
+
22
+ /**
23
+ * Default URL patterns to ignore (won't inject header)
24
+ * These are development-related URLs that shouldn't have impersonation
25
+ */
26
+ const DEFAULT_IGNORE_PATTERNS = [/\/symbolicate$/, /\/logs$/, /\/debugger-proxy/, /\/reload$/, /\/launch-js-devtools/, /localhost:8081/, /100\.64\.\d+\.\d+:8081/,
27
+ // iOS simulator Metro
28
+ /10\.0\.\d+\.\d+:8081/,
29
+ // Android emulator Metro
30
+ /\.hot-update\./, /__metro/];
31
+
32
+ /**
33
+ * Impersonation header injector for network requests
34
+ *
35
+ * This class monkey-patches fetch and XMLHttpRequest to inject the
36
+ * impersonation header on all outgoing requests when impersonation is active.
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * // Get the singleton instance
41
+ * const listener = impersonateListener();
42
+ *
43
+ * // Configure impersonation
44
+ * listener.setConfig({
45
+ * headerKey: 'x-impersonate-user-id',
46
+ * userId: '12345',
47
+ * ignorePatterns: [/\/health/]
48
+ * });
49
+ *
50
+ * // Start injecting headers
51
+ * listener.startListening();
52
+ *
53
+ * // All fetch/XHR requests will now include the header
54
+ *
55
+ * // Stop injecting when done
56
+ * listener.stopListening();
57
+ * ```
58
+ */
59
+ class ImpersonateListener {
60
+ isListeningFlag = false;
61
+
62
+ // Store original methods - MUST be captured before other tools patch
63
+
64
+ constructor() {
65
+ // Initialize with current fetch - will be re-captured in startListening
66
+ this.originalFetch = globalThis.fetch.bind(globalThis);
67
+ this.originalXHROpen = XMLHttpRequest.prototype.open;
68
+ this.originalXHRSend = XMLHttpRequest.prototype.send;
69
+ this.config = {
70
+ headerKey: "x-impersonate-user-id",
71
+ userId: null,
72
+ ignorePatterns: [...DEFAULT_IGNORE_PATTERNS]
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Update impersonation config without restarting listener
78
+ *
79
+ * This is the primary way to activate/deactivate impersonation.
80
+ * Set userId to null to stop injecting headers.
81
+ */
82
+ setConfig(partial) {
83
+ this.config = {
84
+ ...this.config,
85
+ ...partial
86
+ };
87
+
88
+ // Merge ignore patterns if provided
89
+ if (partial.ignorePatterns) {
90
+ this.config.ignorePatterns = [...DEFAULT_IGNORE_PATTERNS, ...partial.ignorePatterns];
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get current configuration
96
+ */
97
+ getConfig() {
98
+ return {
99
+ ...this.config
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Check if URL should have header injected
105
+ *
106
+ * Returns false if:
107
+ * - No userId is set (not impersonating)
108
+ * - URL matches any ignore pattern
109
+ */
110
+ shouldInjectHeader(url) {
111
+ // Not impersonating
112
+ if (!this.config.userId) {
113
+ return false;
114
+ }
115
+
116
+ // Check ignore patterns
117
+ return !this.config.ignorePatterns.some(pattern => pattern.test(url));
118
+ }
119
+
120
+ /**
121
+ * Extract URL from various input types
122
+ */
123
+ extractUrl(input) {
124
+ if (typeof input === "string") {
125
+ return input;
126
+ }
127
+ if (input instanceof URL) {
128
+ return input.href;
129
+ }
130
+ // Request object
131
+ return input.url;
132
+ }
133
+
134
+ /**
135
+ * Start intercepting and injecting headers
136
+ *
137
+ * This patches globalThis.fetch and XMLHttpRequest.prototype methods
138
+ * to inject the impersonation header on all requests.
139
+ */
140
+ startListening() {
141
+ if (this.isListeningFlag) {
142
+ return;
143
+ }
144
+
145
+ // CRITICAL: Re-capture fetch/XHR NOW, not at construction time
146
+ // This ensures we patch OVER any existing interceptors (like @buoy-gg/network)
147
+ // so our modifications are visible to them
148
+ this.originalFetch = globalThis.fetch.bind(globalThis);
149
+ this.originalXHROpen = XMLHttpRequest.prototype.open;
150
+ this.originalXHRSend = XMLHttpRequest.prototype.send;
151
+ const self = this;
152
+
153
+ // =======================================================================
154
+ // PATCH FETCH
155
+ // =======================================================================
156
+ globalThis.fetch = async function (input, init) {
157
+ const url = self.extractUrl(input);
158
+ if (self.shouldInjectHeader(url)) {
159
+ // Create new headers with impersonation header added
160
+ const existingHeaders = init?.headers;
161
+ const headers = new Headers(existingHeaders);
162
+ headers.set(self.config.headerKey, self.config.userId);
163
+
164
+ // Call original fetch with modified init
165
+ return self.originalFetch(input, {
166
+ ...init,
167
+ headers
168
+ });
169
+ }
170
+
171
+ // Not injecting - call original fetch as-is
172
+ return self.originalFetch(input, init);
173
+ };
174
+
175
+ // =======================================================================
176
+ // PATCH XMLHttpRequest
177
+ // =======================================================================
178
+
179
+ // Patch open() to store the URL for later use in send()
180
+ XMLHttpRequest.prototype.open = function (method, url, async = true, username, password) {
181
+ const xhr = this;
182
+ xhr._impersonateUrl = typeof url === "string" ? url : url.href;
183
+ const urlStr = typeof url === "string" ? url : url.toString();
184
+ return self.originalXHROpen.call(this, method, urlStr, async, username, password);
185
+ };
186
+
187
+ // Patch send() to inject header before sending
188
+ XMLHttpRequest.prototype.send = function (body) {
189
+ const xhr = this;
190
+ if (xhr._impersonateUrl && self.shouldInjectHeader(xhr._impersonateUrl)) {
191
+ // Inject the impersonation header
192
+ this.setRequestHeader(self.config.headerKey, self.config.userId);
193
+ }
194
+ return self.originalXHRSend.call(this, body);
195
+ };
196
+ this.isListeningFlag = true;
197
+ }
198
+
199
+ /**
200
+ * Stop intercepting - restore original methods
201
+ *
202
+ * Note: This restores fetch/XHR to their state when ImpersonateListener
203
+ * was constructed. If other interceptors were installed after, this may
204
+ * break their functionality.
205
+ */
206
+ stopListening() {
207
+ if (!this.isListeningFlag) {
208
+ return;
209
+ }
210
+ globalThis.fetch = this.originalFetch;
211
+ XMLHttpRequest.prototype.open = this.originalXHROpen;
212
+ XMLHttpRequest.prototype.send = this.originalXHRSend;
213
+ this.isListeningFlag = false;
214
+ }
215
+
216
+ /**
217
+ * Check if listener is currently active
218
+ */
219
+ get isListening() {
220
+ return this.isListeningFlag;
221
+ }
222
+
223
+ /**
224
+ * Check if impersonation is currently active (has userId set)
225
+ */
226
+ get isImpersonating() {
227
+ return this.config.userId !== null;
228
+ }
229
+ }
230
+
231
+ // =============================================================================
232
+ // SINGLETON PATTERN
233
+ // =============================================================================
234
+
235
+ let _instance = null;
236
+
237
+ /**
238
+ * Get the singleton ImpersonateListener instance
239
+ */
240
+ function impersonateListener() {
241
+ if (!_instance) {
242
+ _instance = new ImpersonateListener();
243
+ }
244
+ return _instance;
245
+ }
246
+
247
+ /**
248
+ * Convenience function to start the listener
249
+ */
250
+ function startImpersonateListener() {
251
+ impersonateListener().startListening();
252
+ }
253
+
254
+ /**
255
+ * Convenience function to stop the listener
256
+ */
257
+ function stopImpersonateListener() {
258
+ impersonateListener().stopListening();
259
+ }
260
+
261
+ /**
262
+ * Convenience function to update config
263
+ */
264
+ function setImpersonateConfig(config) {
265
+ impersonateListener().setConfig(config);
266
+ }
267
+
268
+ /**
269
+ * Convenience function to check if impersonating
270
+ */
271
+ function isImpersonating() {
272
+ return _instance?.isImpersonating ?? false;
273
+ }
274
+
275
+ /**
276
+ * Convenience function to get impersonated user ID
277
+ */
278
+ function getImpersonatedUserId() {
279
+ return _instance?.getConfig().userId ?? null;
280
+ }