@edge-markets/connect-link 1.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/README.md +271 -0
- package/dist/index.d.mts +409 -0
- package/dist/index.d.ts +409 -0
- package/dist/index.js +609 -0
- package/dist/index.mjs +587 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ALL_EDGE_SCOPES: () => import_connect3.ALL_EDGE_SCOPES,
|
|
24
|
+
EDGE_SCOPES: () => import_connect3.EDGE_SCOPES,
|
|
25
|
+
EdgeError: () => import_connect2.EdgeError,
|
|
26
|
+
EdgeLink: () => EdgeLink,
|
|
27
|
+
EdgePopupBlockedError: () => import_connect2.EdgePopupBlockedError,
|
|
28
|
+
EdgeStateMismatchError: () => import_connect2.EdgeStateMismatchError,
|
|
29
|
+
assertCryptoAvailable: () => assertCryptoAvailable,
|
|
30
|
+
generatePKCE: () => generatePKCE,
|
|
31
|
+
generateState: () => generateState,
|
|
32
|
+
isEdgeError: () => import_connect2.isEdgeError
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
|
|
36
|
+
// src/edge-link.ts
|
|
37
|
+
var import_connect = require("@edge-markets/connect");
|
|
38
|
+
|
|
39
|
+
// src/pkce.ts
|
|
40
|
+
function generateRandomBytes(byteLength = 64) {
|
|
41
|
+
const array = new Uint8Array(byteLength);
|
|
42
|
+
crypto.getRandomValues(array);
|
|
43
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
44
|
+
}
|
|
45
|
+
async function sha256(plain) {
|
|
46
|
+
const encoder = new TextEncoder();
|
|
47
|
+
const data = encoder.encode(plain);
|
|
48
|
+
return crypto.subtle.digest("SHA-256", data);
|
|
49
|
+
}
|
|
50
|
+
function base64UrlEncode(buffer) {
|
|
51
|
+
const bytes = new Uint8Array(buffer);
|
|
52
|
+
let binary = "";
|
|
53
|
+
bytes.forEach((byte) => {
|
|
54
|
+
binary += String.fromCharCode(byte);
|
|
55
|
+
});
|
|
56
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
57
|
+
}
|
|
58
|
+
async function generatePKCE() {
|
|
59
|
+
const verifier = generateRandomBytes(64);
|
|
60
|
+
const hash = await sha256(verifier);
|
|
61
|
+
const challenge = base64UrlEncode(hash);
|
|
62
|
+
return { verifier, challenge };
|
|
63
|
+
}
|
|
64
|
+
function generateState() {
|
|
65
|
+
return generateRandomBytes(32);
|
|
66
|
+
}
|
|
67
|
+
function assertCryptoAvailable() {
|
|
68
|
+
if (typeof crypto === "undefined" || !crypto.subtle) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
"Web Crypto API not available. EDGE Connect requires a secure context (HTTPS) and a modern browser."
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/popup.ts
|
|
76
|
+
var DEFAULT_CONFIG = {
|
|
77
|
+
width: 450,
|
|
78
|
+
height: 700
|
|
79
|
+
};
|
|
80
|
+
var CLOSE_CHECK_INTERVAL_MS = 500;
|
|
81
|
+
var PopupManager = class {
|
|
82
|
+
constructor() {
|
|
83
|
+
this.popup = null;
|
|
84
|
+
this.closeCheckInterval = null;
|
|
85
|
+
this.callbacks = {};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Opens a popup window immediately.
|
|
89
|
+
*
|
|
90
|
+
* **MUST be called synchronously from a user click handler!**
|
|
91
|
+
* Any async operations before this call will cause popup blockers.
|
|
92
|
+
*
|
|
93
|
+
* The popup opens with a loading state and can be navigated later.
|
|
94
|
+
*
|
|
95
|
+
* @param callbacks - Optional lifecycle callbacks
|
|
96
|
+
* @returns The popup window, or null if blocked
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* // In a click handler
|
|
101
|
+
* const popup = manager.open({
|
|
102
|
+
* onUserClose: () => handleExit('user_closed'),
|
|
103
|
+
* })
|
|
104
|
+
*
|
|
105
|
+
* if (!popup) {
|
|
106
|
+
* throw new EdgePopupBlockedError()
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
open(callbacks = {}) {
|
|
111
|
+
this.callbacks = callbacks;
|
|
112
|
+
const left = Math.round((window.screen.width - DEFAULT_CONFIG.width) / 2);
|
|
113
|
+
const top = Math.round((window.screen.height - DEFAULT_CONFIG.height) / 2);
|
|
114
|
+
const features = [
|
|
115
|
+
`width=${DEFAULT_CONFIG.width}`,
|
|
116
|
+
`height=${DEFAULT_CONFIG.height}`,
|
|
117
|
+
`left=${left}`,
|
|
118
|
+
`top=${top}`,
|
|
119
|
+
"scrollbars=yes",
|
|
120
|
+
"resizable=yes",
|
|
121
|
+
"toolbar=no",
|
|
122
|
+
"menubar=no",
|
|
123
|
+
"location=no",
|
|
124
|
+
"status=no"
|
|
125
|
+
].join(",");
|
|
126
|
+
this.popup = window.open("about:blank", "edge-connect", features);
|
|
127
|
+
if (!this.popup) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
this.renderLoadingState();
|
|
131
|
+
this.startCloseMonitor();
|
|
132
|
+
return this.popup;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Navigates the popup to a URL.
|
|
136
|
+
*
|
|
137
|
+
* Call this after async initialization is complete.
|
|
138
|
+
* Safe to call anytime after open().
|
|
139
|
+
*
|
|
140
|
+
* @param url - URL to navigate to
|
|
141
|
+
*/
|
|
142
|
+
navigateTo(url) {
|
|
143
|
+
if (this.popup && !this.popup.closed) {
|
|
144
|
+
this.popup.location.href = url;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Focuses the popup window.
|
|
149
|
+
*
|
|
150
|
+
* Useful when user clicks "Connect" again while popup is already open.
|
|
151
|
+
*/
|
|
152
|
+
focus() {
|
|
153
|
+
if (this.popup && !this.popup.closed) {
|
|
154
|
+
this.popup.focus();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Closes the popup window and cleans up resources.
|
|
159
|
+
*/
|
|
160
|
+
close() {
|
|
161
|
+
this.stopCloseMonitor();
|
|
162
|
+
if (this.popup && !this.popup.closed) {
|
|
163
|
+
this.popup.close();
|
|
164
|
+
}
|
|
165
|
+
this.popup = null;
|
|
166
|
+
this.callbacks = {};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Checks if the popup is currently open.
|
|
170
|
+
*/
|
|
171
|
+
isOpen() {
|
|
172
|
+
return this.popup !== null && !this.popup.closed;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Gets the popup window reference.
|
|
176
|
+
*/
|
|
177
|
+
getWindow() {
|
|
178
|
+
return this.popup;
|
|
179
|
+
}
|
|
180
|
+
// ===========================================================================
|
|
181
|
+
// PRIVATE METHODS
|
|
182
|
+
// ===========================================================================
|
|
183
|
+
/**
|
|
184
|
+
* Renders a branded loading state in the popup.
|
|
185
|
+
*
|
|
186
|
+
* This shows immediately while we do async work (PKCE, URL building).
|
|
187
|
+
* Creates a polished, professional feel like Plaid Link.
|
|
188
|
+
*/
|
|
189
|
+
renderLoadingState() {
|
|
190
|
+
if (!this.popup) return;
|
|
191
|
+
this.popup.document.write(`
|
|
192
|
+
<!DOCTYPE html>
|
|
193
|
+
<html lang="en">
|
|
194
|
+
<head>
|
|
195
|
+
<meta charset="UTF-8">
|
|
196
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
197
|
+
<title>EDGE Connect</title>
|
|
198
|
+
<style>
|
|
199
|
+
* {
|
|
200
|
+
box-sizing: border-box;
|
|
201
|
+
margin: 0;
|
|
202
|
+
padding: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
body {
|
|
206
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
justify-content: center;
|
|
210
|
+
min-height: 100vh;
|
|
211
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
212
|
+
color: white;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.container {
|
|
216
|
+
text-align: center;
|
|
217
|
+
padding: 40px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.logo {
|
|
221
|
+
width: 64px;
|
|
222
|
+
height: 64px;
|
|
223
|
+
margin: 0 auto 24px;
|
|
224
|
+
background: linear-gradient(135deg, #00d4aa 0%, #00a080 100%);
|
|
225
|
+
border-radius: 16px;
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
font-size: 28px;
|
|
230
|
+
font-weight: bold;
|
|
231
|
+
box-shadow: 0 8px 32px rgba(0, 212, 170, 0.3);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.spinner {
|
|
235
|
+
width: 40px;
|
|
236
|
+
height: 40px;
|
|
237
|
+
border: 3px solid rgba(255, 255, 255, 0.2);
|
|
238
|
+
border-top-color: #00d4aa;
|
|
239
|
+
border-radius: 50%;
|
|
240
|
+
animation: spin 1s linear infinite;
|
|
241
|
+
margin: 0 auto 24px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@keyframes spin {
|
|
245
|
+
to { transform: rotate(360deg); }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
h1 {
|
|
249
|
+
font-size: 22px;
|
|
250
|
+
font-weight: 600;
|
|
251
|
+
margin-bottom: 8px;
|
|
252
|
+
letter-spacing: -0.5px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
p {
|
|
256
|
+
font-size: 15px;
|
|
257
|
+
color: rgba(255, 255, 255, 0.7);
|
|
258
|
+
line-height: 1.5;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.secure {
|
|
262
|
+
display: flex;
|
|
263
|
+
align-items: center;
|
|
264
|
+
justify-content: center;
|
|
265
|
+
gap: 6px;
|
|
266
|
+
margin-top: 32px;
|
|
267
|
+
font-size: 13px;
|
|
268
|
+
color: rgba(255, 255, 255, 0.5);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.secure svg {
|
|
272
|
+
width: 14px;
|
|
273
|
+
height: 14px;
|
|
274
|
+
}
|
|
275
|
+
</style>
|
|
276
|
+
</head>
|
|
277
|
+
<body>
|
|
278
|
+
<div class="container">
|
|
279
|
+
<div class="logo">E</div>
|
|
280
|
+
<div class="spinner"></div>
|
|
281
|
+
<h1>EDGE Connect</h1>
|
|
282
|
+
<p>Preparing secure connection...</p>
|
|
283
|
+
<div class="secure">
|
|
284
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
285
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
|
286
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
287
|
+
</svg>
|
|
288
|
+
<span>256-bit encryption</span>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</body>
|
|
292
|
+
</html>
|
|
293
|
+
`);
|
|
294
|
+
this.popup.document.close();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Starts monitoring for user closing the popup.
|
|
298
|
+
*/
|
|
299
|
+
startCloseMonitor() {
|
|
300
|
+
this.closeCheckInterval = setInterval(() => {
|
|
301
|
+
if (this.popup?.closed) {
|
|
302
|
+
this.stopCloseMonitor();
|
|
303
|
+
this.popup = null;
|
|
304
|
+
this.callbacks.onUserClose?.();
|
|
305
|
+
}
|
|
306
|
+
}, CLOSE_CHECK_INTERVAL_MS);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Stops the close monitor interval.
|
|
310
|
+
*/
|
|
311
|
+
stopCloseMonitor() {
|
|
312
|
+
if (this.closeCheckInterval !== null) {
|
|
313
|
+
clearInterval(this.closeCheckInterval);
|
|
314
|
+
this.closeCheckInterval = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// src/edge-link.ts
|
|
320
|
+
var EdgeLink = class {
|
|
321
|
+
/**
|
|
322
|
+
* Creates a new EdgeLink instance.
|
|
323
|
+
*
|
|
324
|
+
* @param config - Configuration options
|
|
325
|
+
* @throws Error if required config is missing or crypto is unavailable
|
|
326
|
+
*/
|
|
327
|
+
constructor(config) {
|
|
328
|
+
this.pkce = null;
|
|
329
|
+
this.state = null;
|
|
330
|
+
this.messageHandler = null;
|
|
331
|
+
this.isDestroyed = false;
|
|
332
|
+
if (!config.clientId) {
|
|
333
|
+
throw new Error("EdgeLink: clientId is required");
|
|
334
|
+
}
|
|
335
|
+
if (!config.environment) {
|
|
336
|
+
throw new Error("EdgeLink: environment is required");
|
|
337
|
+
}
|
|
338
|
+
if (!config.onSuccess) {
|
|
339
|
+
throw new Error("EdgeLink: onSuccess callback is required");
|
|
340
|
+
}
|
|
341
|
+
assertCryptoAvailable();
|
|
342
|
+
this.config = config;
|
|
343
|
+
this.popup = new PopupManager();
|
|
344
|
+
const envConfig = (0, import_connect.getEnvironmentConfig)(config.environment);
|
|
345
|
+
this.expectedOrigin = config.linkUrl ? new URL(config.linkUrl).origin : new URL(envConfig.userClientUrl).origin;
|
|
346
|
+
this.setupMessageListener();
|
|
347
|
+
}
|
|
348
|
+
// ===========================================================================
|
|
349
|
+
// PUBLIC API
|
|
350
|
+
// ===========================================================================
|
|
351
|
+
/**
|
|
352
|
+
* Opens the EdgeLink popup.
|
|
353
|
+
*
|
|
354
|
+
* **MUST be called directly from a user click handler!**
|
|
355
|
+
* Calling from setTimeout, Promise.then, or other async contexts will fail.
|
|
356
|
+
*
|
|
357
|
+
* @param options - Optional overrides for this open call
|
|
358
|
+
* @throws EdgePopupBlockedError if popup is blocked
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```typescript
|
|
362
|
+
* // ✅ Correct - direct click handler
|
|
363
|
+
* button.onclick = () => link.open()
|
|
364
|
+
*
|
|
365
|
+
* // ❌ Wrong - async gap
|
|
366
|
+
* button.onclick = async () => {
|
|
367
|
+
* await someAsyncWork()
|
|
368
|
+
* link.open() // Will be blocked!
|
|
369
|
+
* }
|
|
370
|
+
* ```
|
|
371
|
+
*/
|
|
372
|
+
open(options = {}) {
|
|
373
|
+
if (this.isDestroyed) {
|
|
374
|
+
throw new Error("EdgeLink: Cannot open - instance has been destroyed");
|
|
375
|
+
}
|
|
376
|
+
if (this.popup.isOpen()) {
|
|
377
|
+
this.popup.focus();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
this.emitEvent("OPEN");
|
|
381
|
+
const win = this.popup.open({
|
|
382
|
+
onUserClose: () => this.handleUserClose()
|
|
383
|
+
});
|
|
384
|
+
if (!win) {
|
|
385
|
+
this.emitEvent("ERROR", { reason: "popup_blocked" });
|
|
386
|
+
this.config.onExit?.({
|
|
387
|
+
reason: "popup_blocked",
|
|
388
|
+
error: {
|
|
389
|
+
code: "popup_blocked",
|
|
390
|
+
message: "Please allow popups for this site and try again."
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
throw new import_connect.EdgePopupBlockedError();
|
|
394
|
+
}
|
|
395
|
+
this.initializeAuth(options.scopes || this.config.scopes || import_connect.ALL_EDGE_SCOPES);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Closes the EdgeLink popup if open.
|
|
399
|
+
*
|
|
400
|
+
* Call this to programmatically close the popup without triggering onExit.
|
|
401
|
+
*/
|
|
402
|
+
close() {
|
|
403
|
+
this.popup.close();
|
|
404
|
+
this.cleanup();
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Destroys the EdgeLink instance.
|
|
408
|
+
*
|
|
409
|
+
* Call this when unmounting your component or when done with EdgeLink.
|
|
410
|
+
* After destroy(), the instance cannot be reused.
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```typescript
|
|
414
|
+
* // React cleanup
|
|
415
|
+
* useEffect(() => {
|
|
416
|
+
* const link = new EdgeLink({ ... })
|
|
417
|
+
* return () => link.destroy()
|
|
418
|
+
* }, [])
|
|
419
|
+
* ```
|
|
420
|
+
*/
|
|
421
|
+
destroy() {
|
|
422
|
+
this.isDestroyed = true;
|
|
423
|
+
this.close();
|
|
424
|
+
this.removeMessageListener();
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Checks if the popup is currently open.
|
|
428
|
+
*/
|
|
429
|
+
isOpen() {
|
|
430
|
+
return this.popup.isOpen();
|
|
431
|
+
}
|
|
432
|
+
// ===========================================================================
|
|
433
|
+
// PRIVATE METHODS
|
|
434
|
+
// ===========================================================================
|
|
435
|
+
/**
|
|
436
|
+
* Initializes PKCE and navigates popup to auth URL.
|
|
437
|
+
*/
|
|
438
|
+
async initializeAuth(scopes) {
|
|
439
|
+
try {
|
|
440
|
+
this.pkce = await generatePKCE();
|
|
441
|
+
this.state = generateState();
|
|
442
|
+
const url = this.buildLinkUrl(scopes);
|
|
443
|
+
this.emitEvent("HANDOFF", { url });
|
|
444
|
+
this.popup.navigateTo(url);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
this.popup.close();
|
|
447
|
+
this.emitEvent("ERROR", { error });
|
|
448
|
+
this.config.onExit?.({
|
|
449
|
+
reason: "error",
|
|
450
|
+
error: {
|
|
451
|
+
code: "init_failed",
|
|
452
|
+
message: error instanceof Error ? error.message : "Failed to initialize"
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Builds the URL for the EdgeLink page.
|
|
459
|
+
*
|
|
460
|
+
* The Link page handles:
|
|
461
|
+
* - User authentication (if not logged in)
|
|
462
|
+
* - Consent UI (showing requested permissions)
|
|
463
|
+
* - OAuth redirect to Cognito
|
|
464
|
+
* - Returning code via postMessage
|
|
465
|
+
*/
|
|
466
|
+
buildLinkUrl(scopes) {
|
|
467
|
+
const envConfig = (0, import_connect.getEnvironmentConfig)(this.config.environment);
|
|
468
|
+
const baseUrl = this.config.linkUrl || `${envConfig.userClientUrl}/oauth/link`;
|
|
469
|
+
const url = new URL(baseUrl);
|
|
470
|
+
url.searchParams.set("client_id", this.config.clientId);
|
|
471
|
+
url.searchParams.set("state", this.state);
|
|
472
|
+
url.searchParams.set("code_challenge", this.pkce.challenge);
|
|
473
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
474
|
+
const formattedScopes = (0, import_connect.formatScopesForEnvironment)(scopes, this.config.environment);
|
|
475
|
+
url.searchParams.set("scope", formattedScopes.join(" "));
|
|
476
|
+
const redirectUri = this.config.redirectUri || `${window.location.origin}/oauth/edge/callback`;
|
|
477
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
478
|
+
url.searchParams.set("origin", window.location.origin);
|
|
479
|
+
return url.toString();
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Sets up the postMessage listener.
|
|
483
|
+
*/
|
|
484
|
+
setupMessageListener() {
|
|
485
|
+
this.messageHandler = (event) => {
|
|
486
|
+
if (event.origin !== this.expectedOrigin) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const data = event.data;
|
|
490
|
+
if (!data?.type) return;
|
|
491
|
+
switch (data.type) {
|
|
492
|
+
case "EDGE_LINK_SUCCESS":
|
|
493
|
+
this.handleSuccess(data);
|
|
494
|
+
break;
|
|
495
|
+
case "EDGE_LINK_EXIT":
|
|
496
|
+
this.handleExit(data);
|
|
497
|
+
break;
|
|
498
|
+
case "EDGE_LINK_EVENT":
|
|
499
|
+
if (data.eventName) {
|
|
500
|
+
this.emitEvent(data.eventName);
|
|
501
|
+
}
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
window.addEventListener("message", this.messageHandler);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Removes the postMessage listener.
|
|
509
|
+
*/
|
|
510
|
+
removeMessageListener() {
|
|
511
|
+
if (this.messageHandler) {
|
|
512
|
+
window.removeEventListener("message", this.messageHandler);
|
|
513
|
+
this.messageHandler = null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Handles successful authentication from popup.
|
|
518
|
+
*/
|
|
519
|
+
handleSuccess(data) {
|
|
520
|
+
if (data.state !== this.state) {
|
|
521
|
+
this.emitEvent("ERROR", { reason: "state_mismatch" });
|
|
522
|
+
this.config.onExit?.({
|
|
523
|
+
reason: "error",
|
|
524
|
+
error: {
|
|
525
|
+
code: "state_mismatch",
|
|
526
|
+
message: "Security error: state parameter mismatch. Please try again."
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
this.close();
|
|
530
|
+
throw new import_connect.EdgeStateMismatchError();
|
|
531
|
+
}
|
|
532
|
+
if (!this.pkce?.verifier) {
|
|
533
|
+
this.config.onExit?.({
|
|
534
|
+
reason: "error",
|
|
535
|
+
error: {
|
|
536
|
+
code: "missing_verifier",
|
|
537
|
+
message: "Security error: code verifier not found. Please try again."
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
this.close();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const codeVerifier = this.pkce.verifier;
|
|
544
|
+
this.popup.close();
|
|
545
|
+
this.cleanup();
|
|
546
|
+
this.emitEvent("SUCCESS");
|
|
547
|
+
this.config.onSuccess({
|
|
548
|
+
code: data.code,
|
|
549
|
+
codeVerifier,
|
|
550
|
+
state: data.state
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Handles exit from popup.
|
|
555
|
+
*/
|
|
556
|
+
handleExit(data) {
|
|
557
|
+
this.popup.close();
|
|
558
|
+
this.cleanup();
|
|
559
|
+
this.emitEvent("CLOSE", { reason: data.error ? "error" : "user_action" });
|
|
560
|
+
this.config.onExit?.({
|
|
561
|
+
reason: data.error ? "error" : "user_closed",
|
|
562
|
+
error: data.error
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Handles user closing the popup manually.
|
|
567
|
+
*/
|
|
568
|
+
handleUserClose() {
|
|
569
|
+
this.cleanup();
|
|
570
|
+
this.emitEvent("CLOSE", { reason: "user_closed" });
|
|
571
|
+
this.config.onExit?.({
|
|
572
|
+
reason: "user_closed"
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Emits an event.
|
|
577
|
+
*/
|
|
578
|
+
emitEvent(eventName, metadata) {
|
|
579
|
+
this.config.onEvent?.({
|
|
580
|
+
eventName,
|
|
581
|
+
timestamp: Date.now(),
|
|
582
|
+
metadata
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Cleans up stored state.
|
|
587
|
+
*/
|
|
588
|
+
cleanup() {
|
|
589
|
+
this.pkce = null;
|
|
590
|
+
this.state = null;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// src/index.ts
|
|
595
|
+
var import_connect2 = require("@edge-markets/connect");
|
|
596
|
+
var import_connect3 = require("@edge-markets/connect");
|
|
597
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
598
|
+
0 && (module.exports = {
|
|
599
|
+
ALL_EDGE_SCOPES,
|
|
600
|
+
EDGE_SCOPES,
|
|
601
|
+
EdgeError,
|
|
602
|
+
EdgeLink,
|
|
603
|
+
EdgePopupBlockedError,
|
|
604
|
+
EdgeStateMismatchError,
|
|
605
|
+
assertCryptoAvailable,
|
|
606
|
+
generatePKCE,
|
|
607
|
+
generateState,
|
|
608
|
+
isEdgeError
|
|
609
|
+
});
|