@melio-eng/web-sdk 1.1.0-pr.93.4b31895 → 1.1.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/dist/flows/Flow.d.ts +0 -15
- package/dist/flows/Flow.js +63 -201
- package/dist/flows/InitFlow.d.ts +0 -16
- package/dist/flows/InitFlow.js +36 -198
- package/dist/flows/utils.js +0 -2
- package/dist/index.js +9 -26
- package/package.json +1 -1
- package/dist/flows/logger.d.ts +0 -39
- package/dist/flows/logger.js +0 -75
package/dist/flows/Flow.d.ts
CHANGED
|
@@ -13,25 +13,11 @@ export declare class Flow implements FlowInstance {
|
|
|
13
13
|
private eventListeners;
|
|
14
14
|
protected keepAliveInterval: number | null;
|
|
15
15
|
private messageHandler;
|
|
16
|
-
private loadWatchdog;
|
|
17
|
-
private watchdogReports;
|
|
18
|
-
private iframeLoaded;
|
|
19
|
-
private readyForInteraction;
|
|
20
16
|
constructor(containerId: string, config: BaseFlowConfig, partnerName: string, environment: Environment, branchOverride?: string | undefined);
|
|
21
17
|
/**
|
|
22
18
|
* Initialize the flow by creating and injecting the iframe
|
|
23
19
|
*/
|
|
24
20
|
initialize(): Promise<void>;
|
|
25
|
-
/**
|
|
26
|
-
* Report a flow that has neither loaded nor become interactive within the budget.
|
|
27
|
-
*
|
|
28
|
-
* Diagnostic only: this logs and does nothing else. It separates "the iframe
|
|
29
|
-
* never loaded" from "it loaded but never reported READY_FOR_INTERACTION",
|
|
30
|
-
* which is the difference between a transport problem and the embedded app
|
|
31
|
-
* stalling after its document arrived.
|
|
32
|
-
*/
|
|
33
|
-
private startLoadWatchdog;
|
|
34
|
-
private clearLoadWatchdog;
|
|
35
21
|
/**
|
|
36
22
|
* Construct the specific flow URL - can be overridden by subclasses
|
|
37
23
|
*/
|
|
@@ -41,7 +27,6 @@ export declare class Flow implements FlowInstance {
|
|
|
41
27
|
*/
|
|
42
28
|
protected createFlowUrl(): string;
|
|
43
29
|
private setupEventListeners;
|
|
44
|
-
private handleMessage;
|
|
45
30
|
/**
|
|
46
31
|
* Emit events to registered listeners
|
|
47
32
|
*/
|
package/dist/flows/Flow.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getBaseUrl } from './utils.js';
|
|
2
|
-
import { logger, redactUrl, describeError, LOAD_WATCHDOG_MS, WATCHDOG_MAX_REPORTS, } from './logger.js';
|
|
3
2
|
/**
|
|
4
3
|
* Flow class implementation for handling iframe flows and events
|
|
5
4
|
*/
|
|
@@ -15,10 +14,6 @@ export class Flow {
|
|
|
15
14
|
this.eventListeners = new Map();
|
|
16
15
|
this.keepAliveInterval = null;
|
|
17
16
|
this.messageHandler = null;
|
|
18
|
-
this.loadWatchdog = null;
|
|
19
|
-
this.watchdogReports = 0;
|
|
20
|
-
this.iframeLoaded = false;
|
|
21
|
-
this.readyForInteraction = false;
|
|
22
17
|
this.setupEventListeners();
|
|
23
18
|
}
|
|
24
19
|
/**
|
|
@@ -27,87 +22,15 @@ export class Flow {
|
|
|
27
22
|
async initialize() {
|
|
28
23
|
this.container = document.getElementById(this.containerId);
|
|
29
24
|
if (!this.container) {
|
|
30
|
-
logger.error('Flow container not found in the DOM', {
|
|
31
|
-
containerId: this.containerId,
|
|
32
|
-
partnerName: this.partnerName,
|
|
33
|
-
});
|
|
34
25
|
throw new Error(`Container with ID "${this.containerId}" not found`);
|
|
35
26
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.iframe.style.height = '1000px';
|
|
44
|
-
this.iframe.style.border = 'none';
|
|
45
|
-
this.iframe.style.display = 'block';
|
|
46
|
-
this.iframe.addEventListener('load', () => {
|
|
47
|
-
this.iframeLoaded = true;
|
|
48
|
-
logger.debug('Flow iframe fired `load`', { url: safeUrl });
|
|
49
|
-
});
|
|
50
|
-
// Fires for network-level failures. A document that loads but then stalls
|
|
51
|
-
// internally will NOT fire this — that case is covered by the watchdog.
|
|
52
|
-
this.iframe.addEventListener('error', () => {
|
|
53
|
-
this.clearLoadWatchdog();
|
|
54
|
-
logger.error('Flow iframe failed to load', { url: safeUrl });
|
|
55
|
-
});
|
|
56
|
-
this.startLoadWatchdog(safeUrl);
|
|
57
|
-
logger.debug('Injecting flow iframe', {
|
|
58
|
-
url: safeUrl,
|
|
59
|
-
containerId: this.containerId,
|
|
60
|
-
});
|
|
61
|
-
this.container.appendChild(this.iframe);
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
this.clearLoadWatchdog();
|
|
65
|
-
logger.error('Failed to inject flow iframe', {
|
|
66
|
-
url: safeUrl,
|
|
67
|
-
...describeError(error),
|
|
68
|
-
});
|
|
69
|
-
throw error;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Report a flow that has neither loaded nor become interactive within the budget.
|
|
74
|
-
*
|
|
75
|
-
* Diagnostic only: this logs and does nothing else. It separates "the iframe
|
|
76
|
-
* never loaded" from "it loaded but never reported READY_FOR_INTERACTION",
|
|
77
|
-
* which is the difference between a transport problem and the embedded app
|
|
78
|
-
* stalling after its document arrived.
|
|
79
|
-
*/
|
|
80
|
-
startLoadWatchdog(safeUrl) {
|
|
81
|
-
this.loadWatchdog = window.setInterval(() => {
|
|
82
|
-
if (this.iframeLoaded && this.readyForInteraction) {
|
|
83
|
-
this.clearLoadWatchdog();
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
this.watchdogReports += 1;
|
|
87
|
-
const context = {
|
|
88
|
-
url: safeUrl,
|
|
89
|
-
waitedMs: this.watchdogReports * LOAD_WATCHDOG_MS,
|
|
90
|
-
report: `${this.watchdogReports}/${WATCHDOG_MAX_REPORTS}`,
|
|
91
|
-
containerId: this.containerId,
|
|
92
|
-
environment: this.environment,
|
|
93
|
-
};
|
|
94
|
-
if (!this.iframeLoaded) {
|
|
95
|
-
logger.error('Flow iframe has not fired `load` — the flow document appears to be stuck', context);
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
logger.error('Flow iframe loaded but never sent READY_FOR_INTERACTION — the embedded app is stuck after its document loaded', context);
|
|
99
|
-
}
|
|
100
|
-
if (this.watchdogReports >= WATCHDOG_MAX_REPORTS) {
|
|
101
|
-
logger.error('Giving up on stuck-flow reporting — the flow never became interactive. If this is the last SDK line in the console, the page stopped executing here', context);
|
|
102
|
-
this.clearLoadWatchdog();
|
|
103
|
-
}
|
|
104
|
-
}, LOAD_WATCHDOG_MS);
|
|
105
|
-
}
|
|
106
|
-
clearLoadWatchdog() {
|
|
107
|
-
if (this.loadWatchdog !== null) {
|
|
108
|
-
clearInterval(this.loadWatchdog);
|
|
109
|
-
this.loadWatchdog = null;
|
|
110
|
-
}
|
|
27
|
+
this.iframe = document.createElement('iframe');
|
|
28
|
+
this.iframe.src = this.createFlowUrl();
|
|
29
|
+
this.iframe.style.width = '100%';
|
|
30
|
+
this.iframe.style.height = '1000px';
|
|
31
|
+
this.iframe.style.border = 'none';
|
|
32
|
+
this.iframe.style.display = 'block';
|
|
33
|
+
this.container.appendChild(this.iframe);
|
|
111
34
|
}
|
|
112
35
|
/**
|
|
113
36
|
* Construct the specific flow URL - can be overridden by subclasses
|
|
@@ -119,6 +42,11 @@ export class Flow {
|
|
|
119
42
|
* Create flow URL using partner name and environment
|
|
120
43
|
*/
|
|
121
44
|
createFlowUrl() {
|
|
45
|
+
console.log('🔧 Creating flow URL...');
|
|
46
|
+
console.log('📝 Config:', this.config);
|
|
47
|
+
console.log('🏢 Partner:', this.partnerName);
|
|
48
|
+
console.log('🌍 Environment:', this.environment);
|
|
49
|
+
console.log('🔝 Branch Override:', this.branchOverride);
|
|
122
50
|
const baseUrl = getBaseUrl(this.environment);
|
|
123
51
|
let finalUrl = this.constructFlowUrl(baseUrl);
|
|
124
52
|
// Add cdn_branch_override parameter for non-production environments
|
|
@@ -126,123 +54,70 @@ export class Flow {
|
|
|
126
54
|
const separator = finalUrl.includes('?') ? '&' : '?';
|
|
127
55
|
finalUrl += `${separator}cdn_branch_override=${this.branchOverride}`;
|
|
128
56
|
}
|
|
129
|
-
|
|
130
|
-
url: redactUrl(finalUrl),
|
|
131
|
-
partnerName: this.partnerName,
|
|
132
|
-
environment: this.environment,
|
|
133
|
-
branchOverride: this.branchOverride,
|
|
134
|
-
config: this.config,
|
|
135
|
-
});
|
|
57
|
+
console.log('🌐 Final URL:', finalUrl);
|
|
136
58
|
return finalUrl;
|
|
137
59
|
}
|
|
138
60
|
setupEventListeners() {
|
|
139
61
|
// Add post message handlers for internal events - setHeight, scroll etc
|
|
140
62
|
// Also need to implement callbacks for flow completed exit etc in the platform-app
|
|
141
63
|
this.messageHandler = (event) => {
|
|
142
|
-
|
|
143
|
-
|
|
64
|
+
if (!/melio\.com|melioservices\.com/.test(event.origin)) {
|
|
65
|
+
return;
|
|
144
66
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
67
|
+
const { type, ...data } = event.data;
|
|
68
|
+
console.log('📬 Received message from iframe:', {
|
|
69
|
+
type,
|
|
70
|
+
data,
|
|
71
|
+
origin: event.origin,
|
|
72
|
+
});
|
|
73
|
+
switch (type) {
|
|
74
|
+
case 'FLOW_COMPLETED':
|
|
75
|
+
this.emit('completed', data);
|
|
76
|
+
break;
|
|
77
|
+
case 'FLOW_EXIT':
|
|
78
|
+
this.emit('exit');
|
|
79
|
+
break;
|
|
80
|
+
case 'NAVIGATED_TO_TARGET':
|
|
81
|
+
this.emit('navigated', data);
|
|
82
|
+
break;
|
|
83
|
+
case 'ONBOARDING_FORM_COMPLETED':
|
|
84
|
+
this.emit('onboardingCompleted');
|
|
85
|
+
break;
|
|
86
|
+
case 'ONBOARDING_REQUIRED':
|
|
87
|
+
this.emit('onboardingRequired');
|
|
88
|
+
break;
|
|
89
|
+
case 'READY_FOR_INTERACTION':
|
|
90
|
+
this.emit('loaded');
|
|
91
|
+
break;
|
|
92
|
+
case 'PAYMENT_SCHEDULED':
|
|
93
|
+
this.emit('completed', { flowName: 'payment', ...data });
|
|
94
|
+
break;
|
|
95
|
+
case 'MELIO_ERROR':
|
|
96
|
+
if (data.code === 'failed_to_sync_bills') {
|
|
97
|
+
this.emit('error', { errorCode: 'billsSyncFailed' });
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case 'HEIGHT_CHANGE':
|
|
101
|
+
if (this.iframe) {
|
|
102
|
+
this.iframe.style.height = `${data.height}px`;
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
152
105
|
}
|
|
153
106
|
};
|
|
154
107
|
window.addEventListener('message', this.messageHandler);
|
|
155
108
|
}
|
|
156
|
-
handleMessage(event) {
|
|
157
|
-
if (!/melio\.com|melioservices\.com/.test(event.origin)) {
|
|
158
|
-
// Only surface near-misses. Host pages receive plenty of unrelated
|
|
159
|
-
// postMessage traffic and logging all of it would bury the signal.
|
|
160
|
-
if (/melio/i.test(event.origin)) {
|
|
161
|
-
logger.warn('Ignored message from an unrecognised Melio origin — check the origin allowlist', { origin: event.origin });
|
|
162
|
-
}
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
// Destructuring a null or primitive payload throws, and this handler runs
|
|
166
|
-
// for every message that clears the origin check.
|
|
167
|
-
if (event.data === null || typeof event.data !== 'object') {
|
|
168
|
-
logger.warn('Ignored non-object message from a Melio origin', {
|
|
169
|
-
origin: event.origin,
|
|
170
|
-
dataType: event.data === null ? 'null' : typeof event.data,
|
|
171
|
-
});
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const { type, ...data } = event.data;
|
|
175
|
-
logger.debug('Received message from flow iframe', {
|
|
176
|
-
type,
|
|
177
|
-
data,
|
|
178
|
-
origin: event.origin,
|
|
179
|
-
});
|
|
180
|
-
switch (type) {
|
|
181
|
-
case 'FLOW_COMPLETED':
|
|
182
|
-
this.emit('completed', data);
|
|
183
|
-
break;
|
|
184
|
-
case 'FLOW_EXIT':
|
|
185
|
-
this.emit('exit');
|
|
186
|
-
break;
|
|
187
|
-
case 'NAVIGATED_TO_TARGET':
|
|
188
|
-
this.emit('navigated', data);
|
|
189
|
-
break;
|
|
190
|
-
case 'ONBOARDING_FORM_COMPLETED':
|
|
191
|
-
this.emit('onboardingCompleted');
|
|
192
|
-
break;
|
|
193
|
-
case 'ONBOARDING_REQUIRED':
|
|
194
|
-
this.emit('onboardingRequired');
|
|
195
|
-
break;
|
|
196
|
-
case 'READY_FOR_INTERACTION':
|
|
197
|
-
this.readyForInteraction = true;
|
|
198
|
-
this.clearLoadWatchdog();
|
|
199
|
-
logger.debug('Flow reported READY_FOR_INTERACTION');
|
|
200
|
-
this.emit('loaded');
|
|
201
|
-
break;
|
|
202
|
-
case 'PAYMENT_SCHEDULED':
|
|
203
|
-
this.emit('completed', { flowName: 'payment', ...data });
|
|
204
|
-
break;
|
|
205
|
-
case 'MELIO_ERROR':
|
|
206
|
-
logger.error('Embedded app reported MELIO_ERROR', { ...data });
|
|
207
|
-
if (data.code === 'failed_to_sync_bills') {
|
|
208
|
-
this.emit('error', { errorCode: 'billsSyncFailed' });
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
// Only failed_to_sync_bills maps to a public ErrorData code, so any
|
|
212
|
-
// other code is not forwarded to the host at all. Log it loudly
|
|
213
|
-
// rather than dropping it silently.
|
|
214
|
-
logger.error('MELIO_ERROR code has no mapping and was not forwarded to the host', { code: data.code });
|
|
215
|
-
}
|
|
216
|
-
break;
|
|
217
|
-
case 'HEIGHT_CHANGE':
|
|
218
|
-
if (this.iframe) {
|
|
219
|
-
this.iframe.style.height = `${data.height}px`;
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
logger.warn('Received HEIGHT_CHANGE but the flow iframe no longer exists', { height: data.height });
|
|
223
|
-
}
|
|
224
|
-
break;
|
|
225
|
-
default:
|
|
226
|
-
logger.debug('Unhandled message type from flow iframe', { type });
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
109
|
emit(event, data) {
|
|
231
110
|
const listeners = this.eventListeners.get(event);
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
111
|
+
if (listeners) {
|
|
112
|
+
listeners.forEach((callback) => {
|
|
113
|
+
try {
|
|
114
|
+
callback(data);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error(`Error in ${event} event listener:`, error);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
237
120
|
}
|
|
238
|
-
listeners.forEach((callback) => {
|
|
239
|
-
try {
|
|
240
|
-
callback(data);
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
243
|
-
logger.error(`Listener for '${event}' threw`, describeError(error));
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
121
|
}
|
|
247
122
|
on(event, callback) {
|
|
248
123
|
if (!this.eventListeners.has(event)) {
|
|
@@ -264,25 +139,12 @@ export class Flow {
|
|
|
264
139
|
* Close the flow and clean up resources
|
|
265
140
|
*/
|
|
266
141
|
close() {
|
|
267
|
-
logger.debug('Closing flow', {
|
|
268
|
-
containerId: this.containerId,
|
|
269
|
-
iframeLoaded: this.iframeLoaded,
|
|
270
|
-
readyForInteraction: this.readyForInteraction,
|
|
271
|
-
});
|
|
272
|
-
this.clearLoadWatchdog();
|
|
273
142
|
if (this.keepAliveInterval) {
|
|
274
143
|
clearInterval(this.keepAliveInterval);
|
|
275
144
|
this.keepAliveInterval = null;
|
|
276
145
|
}
|
|
277
146
|
if (this.iframe && this.container) {
|
|
278
|
-
|
|
279
|
-
this.container.removeChild(this.iframe);
|
|
280
|
-
}
|
|
281
|
-
catch (error) {
|
|
282
|
-
// Throwing here would abandon the rest of teardown, leaving the message
|
|
283
|
-
// listener attached and the keep-alive running.
|
|
284
|
-
logger.error('Failed to remove the flow iframe during close — continuing teardown', describeError(error));
|
|
285
|
-
}
|
|
147
|
+
this.container.removeChild(this.iframe);
|
|
286
148
|
this.iframe = null;
|
|
287
149
|
}
|
|
288
150
|
if (this.messageHandler) {
|
package/dist/flows/InitFlow.d.ts
CHANGED
|
@@ -13,26 +13,11 @@ export declare class InitFlow implements InitFlowInstance {
|
|
|
13
13
|
private partnerName;
|
|
14
14
|
private environment;
|
|
15
15
|
private branchOverride?;
|
|
16
|
-
private loadWatchdog;
|
|
17
|
-
private watchdogReports;
|
|
18
|
-
private iframeLoaded;
|
|
19
|
-
private authResolved;
|
|
20
|
-
private keepAliveTargetLost;
|
|
21
16
|
constructor(config: InitConfig, partnerName: string, environment: Environment, branchOverride?: string);
|
|
22
17
|
/**
|
|
23
18
|
* Initialize the init flow by creating and injecting a hidden iframe
|
|
24
19
|
*/
|
|
25
20
|
initialize(): Promise<void>;
|
|
26
|
-
/**
|
|
27
|
-
* Report a flow that has neither loaded nor authenticated within the budget.
|
|
28
|
-
*
|
|
29
|
-
* Diagnostic only: this logs and does nothing else. It deliberately separates
|
|
30
|
-
* "the auth document never loaded" from "it loaded but never authenticated",
|
|
31
|
-
* because those point at different layers and the distinction is invisible
|
|
32
|
-
* from the host page otherwise.
|
|
33
|
-
*/
|
|
34
|
-
private startLoadWatchdog;
|
|
35
|
-
private clearLoadWatchdog;
|
|
36
21
|
/**
|
|
37
22
|
* Construct the specific flow URL for initialization
|
|
38
23
|
*/
|
|
@@ -45,7 +30,6 @@ export declare class InitFlow implements InitFlowInstance {
|
|
|
45
30
|
* Setup event listeners to only handle authentication events
|
|
46
31
|
*/
|
|
47
32
|
private setupEventListeners;
|
|
48
|
-
private handleMessage;
|
|
49
33
|
/**
|
|
50
34
|
* Emit events to registered listeners
|
|
51
35
|
*/
|
package/dist/flows/InitFlow.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isEmptyString, getBaseUrl } from './utils.js';
|
|
2
|
-
import { logger, redactUrl, describeError, LOAD_WATCHDOG_MS, WATCHDOG_MAX_REPORTS, KEEP_ALIVE_INTERVAL_MS, } from './logger.js';
|
|
3
2
|
/**
|
|
4
3
|
* Standalone InitFlow class for handling initialization with callbacks
|
|
5
4
|
*/
|
|
@@ -10,16 +9,7 @@ export class InitFlow {
|
|
|
10
9
|
this.eventListeners = new Map();
|
|
11
10
|
this.keepAliveInterval = null;
|
|
12
11
|
this.messageHandler = null;
|
|
13
|
-
this.loadWatchdog = null;
|
|
14
|
-
this.watchdogReports = 0;
|
|
15
|
-
this.iframeLoaded = false;
|
|
16
|
-
this.authResolved = false;
|
|
17
|
-
this.keepAliveTargetLost = false;
|
|
18
12
|
if (isEmptyString(config.authCode)) {
|
|
19
|
-
logger.error('Cannot create init flow: authorization code is missing', {
|
|
20
|
-
partnerName,
|
|
21
|
-
environment,
|
|
22
|
-
});
|
|
23
13
|
throw new Error('Authorization code is required for init flow');
|
|
24
14
|
}
|
|
25
15
|
this.authorizationCode = config.authCode;
|
|
@@ -33,92 +23,15 @@ export class InitFlow {
|
|
|
33
23
|
* Initialize the init flow by creating and injecting a hidden iframe
|
|
34
24
|
*/
|
|
35
25
|
async initialize() {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
throw new Error('document.body is not available for iframe injection');
|
|
46
|
-
}
|
|
47
|
-
this.container = document.body;
|
|
48
|
-
this.iframe = document.createElement('iframe');
|
|
49
|
-
this.iframe.src = url;
|
|
50
|
-
this.iframe.style.width = '1px';
|
|
51
|
-
this.iframe.style.height = '1px';
|
|
52
|
-
this.iframe.style.position = 'absolute';
|
|
53
|
-
this.iframe.style.left = '-9999px';
|
|
54
|
-
this.iframe.style.top = '-9999px';
|
|
55
|
-
this.iframe.addEventListener('load', () => {
|
|
56
|
-
this.iframeLoaded = true;
|
|
57
|
-
logger.debug('Auth iframe fired `load`', { url: safeUrl });
|
|
58
|
-
});
|
|
59
|
-
// Fires for network-level failures. A document that loads but then stalls
|
|
60
|
-
// internally will NOT fire this — that case is covered by the watchdog.
|
|
61
|
-
this.iframe.addEventListener('error', () => {
|
|
62
|
-
this.clearLoadWatchdog();
|
|
63
|
-
logger.error('Auth iframe failed to load', { url: safeUrl });
|
|
64
|
-
});
|
|
65
|
-
this.startLoadWatchdog(safeUrl);
|
|
66
|
-
logger.debug('Injecting hidden auth iframe', {
|
|
67
|
-
url: safeUrl,
|
|
68
|
-
partnerName: this.partnerName,
|
|
69
|
-
environment: this.environment,
|
|
70
|
-
});
|
|
71
|
-
this.container.appendChild(this.iframe);
|
|
72
|
-
}
|
|
73
|
-
catch (error) {
|
|
74
|
-
this.clearLoadWatchdog();
|
|
75
|
-
logger.error('Failed to inject auth iframe', {
|
|
76
|
-
url: safeUrl,
|
|
77
|
-
...describeError(error),
|
|
78
|
-
});
|
|
79
|
-
throw error;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Report a flow that has neither loaded nor authenticated within the budget.
|
|
84
|
-
*
|
|
85
|
-
* Diagnostic only: this logs and does nothing else. It deliberately separates
|
|
86
|
-
* "the auth document never loaded" from "it loaded but never authenticated",
|
|
87
|
-
* because those point at different layers and the distinction is invisible
|
|
88
|
-
* from the host page otherwise.
|
|
89
|
-
*/
|
|
90
|
-
startLoadWatchdog(safeUrl) {
|
|
91
|
-
this.loadWatchdog = window.setInterval(() => {
|
|
92
|
-
if (this.iframeLoaded && this.authResolved) {
|
|
93
|
-
this.clearLoadWatchdog();
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
this.watchdogReports += 1;
|
|
97
|
-
const waitedMs = this.watchdogReports * LOAD_WATCHDOG_MS;
|
|
98
|
-
const context = {
|
|
99
|
-
url: safeUrl,
|
|
100
|
-
waitedMs,
|
|
101
|
-
report: `${this.watchdogReports}/${WATCHDOG_MAX_REPORTS}`,
|
|
102
|
-
partnerName: this.partnerName,
|
|
103
|
-
environment: this.environment,
|
|
104
|
-
};
|
|
105
|
-
if (!this.iframeLoaded) {
|
|
106
|
-
logger.error('Auth iframe has not fired `load` — the auth document appears to be stuck', context);
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
logger.error('Auth iframe loaded but never reported AUTHENTICATION_SUCCESS or AUTHENTICATION_ERROR — auth is stuck inside the iframe', context);
|
|
110
|
-
}
|
|
111
|
-
if (this.watchdogReports >= WATCHDOG_MAX_REPORTS) {
|
|
112
|
-
logger.error('Giving up on stuck-flow reporting — the auth flow never resolved. If this is the last SDK line in the console, the page stopped executing here', context);
|
|
113
|
-
this.clearLoadWatchdog();
|
|
114
|
-
}
|
|
115
|
-
}, LOAD_WATCHDOG_MS);
|
|
116
|
-
}
|
|
117
|
-
clearLoadWatchdog() {
|
|
118
|
-
if (this.loadWatchdog !== null) {
|
|
119
|
-
clearInterval(this.loadWatchdog);
|
|
120
|
-
this.loadWatchdog = null;
|
|
121
|
-
}
|
|
26
|
+
this.container = document.body;
|
|
27
|
+
this.iframe = document.createElement('iframe');
|
|
28
|
+
this.iframe.src = this.createFlowUrl();
|
|
29
|
+
this.iframe.style.width = '1px';
|
|
30
|
+
this.iframe.style.height = '1px';
|
|
31
|
+
this.iframe.style.position = 'absolute';
|
|
32
|
+
this.iframe.style.left = '-9999px';
|
|
33
|
+
this.iframe.style.top = '-9999px';
|
|
34
|
+
this.container.appendChild(this.iframe);
|
|
122
35
|
}
|
|
123
36
|
/**
|
|
124
37
|
* Construct the specific flow URL for initialization
|
|
@@ -148,80 +61,42 @@ export class InitFlow {
|
|
|
148
61
|
*/
|
|
149
62
|
setupEventListeners() {
|
|
150
63
|
this.messageHandler = (event) => {
|
|
151
|
-
|
|
152
|
-
|
|
64
|
+
if (!/melio\.com|melioservices\.com/.test(event.origin)) {
|
|
65
|
+
return;
|
|
153
66
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
67
|
+
const { type, ...data } = event.data;
|
|
68
|
+
console.log('📬 Received message from iframe:', {
|
|
69
|
+
type,
|
|
70
|
+
data,
|
|
71
|
+
origin: event.origin,
|
|
72
|
+
});
|
|
73
|
+
switch (type) {
|
|
74
|
+
case 'AUTHENTICATION_SUCCESS':
|
|
75
|
+
this.emit('authenticationSucceeded');
|
|
76
|
+
break;
|
|
77
|
+
case 'AUTHENTICATION_ERROR':
|
|
78
|
+
this.emit('authenticationFailed');
|
|
79
|
+
break;
|
|
161
80
|
}
|
|
162
81
|
};
|
|
163
82
|
window.addEventListener('message', this.messageHandler);
|
|
164
83
|
}
|
|
165
|
-
handleMessage(event) {
|
|
166
|
-
if (!/melio\.com|melioservices\.com/.test(event.origin)) {
|
|
167
|
-
// Only surface near-misses. Host pages receive plenty of unrelated
|
|
168
|
-
// postMessage traffic and logging all of it would bury the signal.
|
|
169
|
-
if (/melio/i.test(event.origin)) {
|
|
170
|
-
logger.warn('Ignored message from an unrecognised Melio origin — check the origin allowlist', { origin: event.origin });
|
|
171
|
-
}
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
// Destructuring a null or primitive payload throws, and this handler runs
|
|
175
|
-
// for every message that clears the origin check.
|
|
176
|
-
if (event.data === null || typeof event.data !== 'object') {
|
|
177
|
-
logger.warn('Ignored non-object message from a Melio origin', {
|
|
178
|
-
origin: event.origin,
|
|
179
|
-
dataType: event.data === null ? 'null' : typeof event.data,
|
|
180
|
-
});
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const { type, ...data } = event.data;
|
|
184
|
-
logger.debug('Received message from auth iframe', {
|
|
185
|
-
type,
|
|
186
|
-
data,
|
|
187
|
-
origin: event.origin,
|
|
188
|
-
});
|
|
189
|
-
switch (type) {
|
|
190
|
-
case 'AUTHENTICATION_SUCCESS':
|
|
191
|
-
this.authResolved = true;
|
|
192
|
-
this.clearLoadWatchdog();
|
|
193
|
-
logger.debug('Authentication succeeded');
|
|
194
|
-
this.emit('authenticationSucceeded');
|
|
195
|
-
break;
|
|
196
|
-
case 'AUTHENTICATION_ERROR':
|
|
197
|
-
this.authResolved = true;
|
|
198
|
-
this.clearLoadWatchdog();
|
|
199
|
-
logger.error('Authentication failed', { ...data });
|
|
200
|
-
this.emit('authenticationFailed', data);
|
|
201
|
-
break;
|
|
202
|
-
default:
|
|
203
|
-
logger.debug('Unhandled message type from auth iframe', { type });
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
84
|
/**
|
|
208
85
|
* Emit events to registered listeners
|
|
209
86
|
*/
|
|
210
87
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
211
88
|
emit(event, data) {
|
|
212
89
|
const listeners = this.eventListeners.get(event);
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
90
|
+
if (listeners) {
|
|
91
|
+
listeners.forEach((callback) => {
|
|
92
|
+
try {
|
|
93
|
+
callback(data);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error(`Error in ${event} event listener:`, error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
216
99
|
}
|
|
217
|
-
listeners.forEach((callback) => {
|
|
218
|
-
try {
|
|
219
|
-
callback(data);
|
|
220
|
-
}
|
|
221
|
-
catch (error) {
|
|
222
|
-
logger.error(`Listener for '${event}' threw`, describeError(error));
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
100
|
}
|
|
226
101
|
/**
|
|
227
102
|
* Register an event listener
|
|
@@ -246,24 +121,12 @@ export class InitFlow {
|
|
|
246
121
|
* Close the flow and clean up resources
|
|
247
122
|
*/
|
|
248
123
|
close() {
|
|
249
|
-
logger.debug('Closing init flow', {
|
|
250
|
-
iframeLoaded: this.iframeLoaded,
|
|
251
|
-
authResolved: this.authResolved,
|
|
252
|
-
});
|
|
253
|
-
this.clearLoadWatchdog();
|
|
254
124
|
if (this.keepAliveInterval) {
|
|
255
125
|
clearInterval(this.keepAliveInterval);
|
|
256
126
|
this.keepAliveInterval = null;
|
|
257
127
|
}
|
|
258
128
|
if (this.iframe && this.container) {
|
|
259
|
-
|
|
260
|
-
this.container.removeChild(this.iframe);
|
|
261
|
-
}
|
|
262
|
-
catch (error) {
|
|
263
|
-
// Throwing here would abandon the rest of teardown, leaving the message
|
|
264
|
-
// listener attached and the keep-alive running.
|
|
265
|
-
logger.error('Failed to remove the auth iframe during close — continuing teardown', describeError(error));
|
|
266
|
-
}
|
|
129
|
+
this.container.removeChild(this.iframe);
|
|
267
130
|
this.iframe = null;
|
|
268
131
|
}
|
|
269
132
|
if (this.messageHandler) {
|
|
@@ -273,35 +136,10 @@ export class InitFlow {
|
|
|
273
136
|
this.eventListeners.clear();
|
|
274
137
|
}
|
|
275
138
|
setupKeepAlive() {
|
|
276
|
-
if (this.keepAliveInterval !== null) {
|
|
277
|
-
logger.warn('setupKeepAlive called while a keep-alive is already running — clearing the previous interval to avoid a leak');
|
|
278
|
-
clearInterval(this.keepAliveInterval);
|
|
279
|
-
this.keepAliveInterval = null;
|
|
280
|
-
}
|
|
281
139
|
this.keepAliveInterval = window.setInterval(() => {
|
|
282
|
-
if (
|
|
283
|
-
// Logged once: the interval keeps running, so an unguarded log here
|
|
284
|
-
// would repeat every 30s for the life of the page.
|
|
285
|
-
if (!this.keepAliveTargetLost) {
|
|
286
|
-
this.keepAliveTargetLost = true;
|
|
287
|
-
logger.error('Keep-alive ping has no target — the auth iframe or its content window is gone, every subsequent ping is a no-op', {
|
|
288
|
-
hasIframe: Boolean(this.iframe),
|
|
289
|
-
iframeLoaded: this.iframeLoaded,
|
|
290
|
-
authResolved: this.authResolved,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (this.keepAliveTargetLost) {
|
|
296
|
-
this.keepAliveTargetLost = false;
|
|
297
|
-
logger.warn('Keep-alive target recovered');
|
|
298
|
-
}
|
|
299
|
-
try {
|
|
140
|
+
if (this.iframe && this.iframe.contentWindow) {
|
|
300
141
|
this.iframe.contentWindow.postMessage({ type: 'USER_ACTIVE_PING' }, '*');
|
|
301
142
|
}
|
|
302
|
-
|
|
303
|
-
logger.error('Keep-alive ping threw', describeError(error));
|
|
304
|
-
}
|
|
305
|
-
}, KEEP_ALIVE_INTERVAL_MS);
|
|
143
|
+
}, 30000); // Send ping every 30 seconds
|
|
306
144
|
}
|
|
307
145
|
}
|
package/dist/flows/utils.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { logger } from './logger.js';
|
|
2
1
|
/**
|
|
3
2
|
* Utility function to get the base URL for a given environment
|
|
4
3
|
*/
|
|
@@ -17,7 +16,6 @@ export function getBaseUrl(environment) {
|
|
|
17
16
|
case 'localhost':
|
|
18
17
|
return 'http://localhost:3005';
|
|
19
18
|
default:
|
|
20
|
-
logger.warn('Unknown environment — falling back to the default base URL, which may not serve this partner', { environment, fallback: 'https://partnerships.melioservices.com' });
|
|
21
19
|
return 'https://partnerships.melioservices.com';
|
|
22
20
|
}
|
|
23
21
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { InitFlow, OnboardingFlow, PayFlow, JustPayFlow, SettingsFlow, PaymentsDashboardFlow, isEmptyString, } from './flows/index.js';
|
|
2
|
-
import { logger, markInitStart, describeError } from './flows/logger.js';
|
|
3
2
|
/**
|
|
4
3
|
* Main SDK implementation - now partner agnostic
|
|
5
4
|
*/
|
|
@@ -17,26 +16,20 @@ export class MelioSDK {
|
|
|
17
16
|
*/
|
|
18
17
|
init(authenticationCode, options) {
|
|
19
18
|
if (this.initFlow) {
|
|
20
|
-
|
|
19
|
+
console.log('SDK already initialized. Returning existing init flow instance.');
|
|
21
20
|
return this.initFlow;
|
|
22
21
|
}
|
|
23
|
-
markInitStart();
|
|
24
22
|
this.partnerName = options.partnerName;
|
|
25
23
|
this.environment = options.environment || 'production';
|
|
26
24
|
this.branchOverride = options.branchOverride;
|
|
27
25
|
this.authCode = authenticationCode;
|
|
28
|
-
|
|
26
|
+
console.log('starting init flow', {
|
|
29
27
|
partnerName: this.partnerName,
|
|
30
28
|
environment: this.environment,
|
|
31
|
-
keepAlive: Boolean(options.keepAlive),
|
|
32
29
|
});
|
|
33
30
|
const initFlow = new InitFlow({ authCode: authenticationCode, containerId: '' }, this.partnerName, this.environment, this.branchOverride);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// is invisible to partners debugging from the console.
|
|
37
|
-
initFlow.initialize().catch((error) => {
|
|
38
|
-
logger.error('Init flow failed to initialize', describeError(error));
|
|
39
|
-
});
|
|
31
|
+
initFlow.initialize();
|
|
32
|
+
console.log('InitFlow initialized successfully');
|
|
40
33
|
if (options.keepAlive)
|
|
41
34
|
initFlow.setupKeepAlive();
|
|
42
35
|
this.initFlow = initFlow;
|
|
@@ -50,9 +43,7 @@ export class MelioSDK {
|
|
|
50
43
|
throw new Error('SDK not initialized. Please call init() before opening onboarding flow.');
|
|
51
44
|
}
|
|
52
45
|
const flow = new OnboardingFlow(config.containerId, config, this.partnerName, this.environment, this.authCode, this.branchOverride);
|
|
53
|
-
flow.initialize()
|
|
54
|
-
logger.error(`${flow.constructor.name} failed to initialize`, describeError(error));
|
|
55
|
-
});
|
|
46
|
+
flow.initialize();
|
|
56
47
|
return flow;
|
|
57
48
|
}
|
|
58
49
|
/**
|
|
@@ -63,9 +54,7 @@ export class MelioSDK {
|
|
|
63
54
|
throw new Error('SDK not initialized. Please call init() before opening pay flow.');
|
|
64
55
|
}
|
|
65
56
|
const flow = new PayFlow(config.containerId, config, this.partnerName, this.environment, this.authCode, this.branchOverride);
|
|
66
|
-
flow.initialize()
|
|
67
|
-
logger.error(`${flow.constructor.name} failed to initialize`, describeError(error));
|
|
68
|
-
});
|
|
57
|
+
flow.initialize();
|
|
69
58
|
return flow;
|
|
70
59
|
}
|
|
71
60
|
/**
|
|
@@ -76,9 +65,7 @@ export class MelioSDK {
|
|
|
76
65
|
throw new Error('SDK not initialized. Please call init() before opening just pay flow.');
|
|
77
66
|
}
|
|
78
67
|
const flow = new JustPayFlow(config.containerId, config, this.partnerName, this.environment, this.authCode, this.branchOverride);
|
|
79
|
-
flow.initialize()
|
|
80
|
-
logger.error(`${flow.constructor.name} failed to initialize`, describeError(error));
|
|
81
|
-
});
|
|
68
|
+
flow.initialize();
|
|
82
69
|
return flow;
|
|
83
70
|
}
|
|
84
71
|
/**
|
|
@@ -89,9 +76,7 @@ export class MelioSDK {
|
|
|
89
76
|
throw new Error('SDK not initialized. Please call init() before opening settings flow.');
|
|
90
77
|
}
|
|
91
78
|
const flow = new SettingsFlow(config.containerId, config, this.partnerName, this.environment, this.authCode, this.branchOverride);
|
|
92
|
-
flow.initialize()
|
|
93
|
-
logger.error(`${flow.constructor.name} failed to initialize`, describeError(error));
|
|
94
|
-
});
|
|
79
|
+
flow.initialize();
|
|
95
80
|
return flow;
|
|
96
81
|
}
|
|
97
82
|
/**
|
|
@@ -102,9 +87,7 @@ export class MelioSDK {
|
|
|
102
87
|
throw new Error('SDK not initialized. Please call init() before opening payments dashboard flow.');
|
|
103
88
|
}
|
|
104
89
|
const flow = new PaymentsDashboardFlow(config.containerId, config, this.partnerName, this.environment, this.authCode, this.branchOverride);
|
|
105
|
-
flow.initialize()
|
|
106
|
-
logger.error(`${flow.constructor.name} failed to initialize`, describeError(error));
|
|
107
|
-
});
|
|
90
|
+
flow.initialize();
|
|
108
91
|
return flow;
|
|
109
92
|
}
|
|
110
93
|
}
|
package/package.json
CHANGED
package/dist/flows/logger.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight console logger for the SDK.
|
|
3
|
-
*
|
|
4
|
-
* Every line is prefixed with `[melio-sdk]` so partners can filter SDK output out
|
|
5
|
-
* of a busy host page, and carries `msSinceInit` — elapsed time since `init()` was
|
|
6
|
-
* called. The elapsed time is what makes a *hang* legible in a console capture: a
|
|
7
|
-
* hard failure shows up as an error either way, but a flow that simply never
|
|
8
|
-
* finishes is only visible as a gap on the clock.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* How long a flow may stay unloaded before the SDK reports it as stuck, and how
|
|
12
|
-
* often it keeps repeating that report afterwards.
|
|
13
|
-
*
|
|
14
|
-
* Diagnostic only — reaching it logs, it does not tear the flow down.
|
|
15
|
-
*
|
|
16
|
-
* The repeat matters for diagnosing a page that dies rather than recovers: a
|
|
17
|
-
* stuck flow keeps announcing itself every 15s, so the timestamp of the *last*
|
|
18
|
-
* report marks the moment the tab stopped executing. A one-shot log cannot
|
|
19
|
-
* distinguish "still hanging" from "the renderer was killed".
|
|
20
|
-
*/
|
|
21
|
-
export declare const LOAD_WATCHDOG_MS = 15000;
|
|
22
|
-
/** Stop repeating the stuck-flow report after this many rounds (~2 minutes). */
|
|
23
|
-
export declare const WATCHDOG_MAX_REPORTS = 8;
|
|
24
|
-
/** Interval between keep-alive pings to an initialized flow. */
|
|
25
|
-
export declare const KEEP_ALIVE_INTERVAL_MS = 30000;
|
|
26
|
-
/** Start the SDK clock. Called once from `init()`. */
|
|
27
|
-
export declare const markInitStart: () => void;
|
|
28
|
-
/**
|
|
29
|
-
* Strip credential-bearing query params so a flow URL can be logged safely.
|
|
30
|
-
* On a URL we cannot parse we fall back to the path, never the raw string.
|
|
31
|
-
*/
|
|
32
|
-
export declare const redactUrl: (url: string) => string;
|
|
33
|
-
/** Flatten an unknown throwable into something that survives a console capture. */
|
|
34
|
-
export declare const describeError: (error: unknown) => Record<string, unknown>;
|
|
35
|
-
export declare const logger: {
|
|
36
|
-
debug(message: string, context?: Record<string, unknown>): void;
|
|
37
|
-
warn(message: string, context?: Record<string, unknown>): void;
|
|
38
|
-
error(message: string, context?: Record<string, unknown>): void;
|
|
39
|
-
};
|
package/dist/flows/logger.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight console logger for the SDK.
|
|
3
|
-
*
|
|
4
|
-
* Every line is prefixed with `[melio-sdk]` so partners can filter SDK output out
|
|
5
|
-
* of a busy host page, and carries `msSinceInit` — elapsed time since `init()` was
|
|
6
|
-
* called. The elapsed time is what makes a *hang* legible in a console capture: a
|
|
7
|
-
* hard failure shows up as an error either way, but a flow that simply never
|
|
8
|
-
* finishes is only visible as a gap on the clock.
|
|
9
|
-
*/
|
|
10
|
-
const PREFIX = '[melio-sdk]';
|
|
11
|
-
/** Query params that may carry a credential and must never reach the console. */
|
|
12
|
-
const REDACTED_PARAMS = ['token', 'authCode', 'code'];
|
|
13
|
-
/**
|
|
14
|
-
* How long a flow may stay unloaded before the SDK reports it as stuck, and how
|
|
15
|
-
* often it keeps repeating that report afterwards.
|
|
16
|
-
*
|
|
17
|
-
* Diagnostic only — reaching it logs, it does not tear the flow down.
|
|
18
|
-
*
|
|
19
|
-
* The repeat matters for diagnosing a page that dies rather than recovers: a
|
|
20
|
-
* stuck flow keeps announcing itself every 15s, so the timestamp of the *last*
|
|
21
|
-
* report marks the moment the tab stopped executing. A one-shot log cannot
|
|
22
|
-
* distinguish "still hanging" from "the renderer was killed".
|
|
23
|
-
*/
|
|
24
|
-
export const LOAD_WATCHDOG_MS = 15000;
|
|
25
|
-
/** Stop repeating the stuck-flow report after this many rounds (~2 minutes). */
|
|
26
|
-
export const WATCHDOG_MAX_REPORTS = 8;
|
|
27
|
-
/** Interval between keep-alive pings to an initialized flow. */
|
|
28
|
-
export const KEEP_ALIVE_INTERVAL_MS = 30000;
|
|
29
|
-
let initTimestamp = null;
|
|
30
|
-
/** Start the SDK clock. Called once from `init()`. */
|
|
31
|
-
export const markInitStart = () => {
|
|
32
|
-
initTimestamp = Date.now();
|
|
33
|
-
};
|
|
34
|
-
const buildContext = (context) => ({
|
|
35
|
-
...(initTimestamp === null
|
|
36
|
-
? {}
|
|
37
|
-
: { msSinceInit: Date.now() - initTimestamp }),
|
|
38
|
-
...context,
|
|
39
|
-
});
|
|
40
|
-
/**
|
|
41
|
-
* Strip credential-bearing query params so a flow URL can be logged safely.
|
|
42
|
-
* On a URL we cannot parse we fall back to the path, never the raw string.
|
|
43
|
-
*/
|
|
44
|
-
export const redactUrl = (url) => {
|
|
45
|
-
try {
|
|
46
|
-
const parsed = new URL(url);
|
|
47
|
-
REDACTED_PARAMS.forEach((param) => {
|
|
48
|
-
if (parsed.searchParams.has(param)) {
|
|
49
|
-
parsed.searchParams.set(param, 'redacted');
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
return parsed.toString();
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return `${url.split('?')[0]}?[unparsed-query-redacted]`;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
/** Flatten an unknown throwable into something that survives a console capture. */
|
|
59
|
-
export const describeError = (error) => {
|
|
60
|
-
if (error instanceof Error) {
|
|
61
|
-
return { name: error.name, message: error.message, stack: error.stack };
|
|
62
|
-
}
|
|
63
|
-
return { thrown: String(error) };
|
|
64
|
-
};
|
|
65
|
-
export const logger = {
|
|
66
|
-
debug(message, context) {
|
|
67
|
-
console.log(`${PREFIX} ${message}`, buildContext(context));
|
|
68
|
-
},
|
|
69
|
-
warn(message, context) {
|
|
70
|
-
console.warn(`${PREFIX} ${message}`, buildContext(context));
|
|
71
|
-
},
|
|
72
|
-
error(message, context) {
|
|
73
|
-
console.error(`${PREFIX} ${message}`, buildContext(context));
|
|
74
|
-
},
|
|
75
|
-
};
|