@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/README.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# @edgeboost/edge-connect-link
|
|
2
|
+
|
|
3
|
+
Browser SDK for EDGE Connect popup authentication - inspired by [Plaid Link](https://plaid.com/docs/link/).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔒 **Secure by default** - PKCE OAuth flow, no client secret in browser
|
|
8
|
+
- 🚀 **Simple API** - Just `new EdgeLink()` and `link.open()`
|
|
9
|
+
- 📱 **Works everywhere** - Handles popup blockers gracefully
|
|
10
|
+
- 📊 **Event tracking** - `onSuccess`, `onExit`, `onEvent` callbacks
|
|
11
|
+
- 🎨 **Beautiful loading state** - Professional branded experience
|
|
12
|
+
- 📝 **Full TypeScript** - Complete type definitions
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @edgeboost/edge-connect-link
|
|
18
|
+
# or
|
|
19
|
+
pnpm add @edgeboost/edge-connect-link
|
|
20
|
+
# or
|
|
21
|
+
yarn add @edgeboost/edge-connect-link
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { EdgeLink } from '@edgeboost/edge-connect-link'
|
|
28
|
+
|
|
29
|
+
// 1. Create instance (do this once)
|
|
30
|
+
const link = new EdgeLink({
|
|
31
|
+
clientId: 'your-client-id',
|
|
32
|
+
environment: 'staging',
|
|
33
|
+
onSuccess: (result) => {
|
|
34
|
+
// Send to your backend for token exchange
|
|
35
|
+
fetch('/api/edge/exchange', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
code: result.code,
|
|
40
|
+
codeVerifier: result.codeVerifier,
|
|
41
|
+
}),
|
|
42
|
+
})
|
|
43
|
+
},
|
|
44
|
+
onExit: (metadata) => {
|
|
45
|
+
if (metadata.reason === 'popup_blocked') {
|
|
46
|
+
alert('Please allow popups for this site')
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// 2. Open from a click handler
|
|
52
|
+
document.getElementById('connect-btn')!.onclick = () => link.open()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## ⚠️ Important: User Gesture Requirement
|
|
56
|
+
|
|
57
|
+
**`link.open()` MUST be called directly from a user click handler!**
|
|
58
|
+
|
|
59
|
+
Browsers block popups that aren't triggered by user interaction. Any async work before calling `open()` will break this.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// ✅ Correct - direct click handler
|
|
63
|
+
button.onclick = () => link.open()
|
|
64
|
+
|
|
65
|
+
// ✅ Correct - immediate call in handler
|
|
66
|
+
button.onclick = () => {
|
|
67
|
+
trackClick() // sync ok
|
|
68
|
+
link.open()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ❌ Wrong - async gap breaks user gesture
|
|
72
|
+
button.onclick = async () => {
|
|
73
|
+
await someAsyncWork() // breaks it!
|
|
74
|
+
link.open() // BLOCKED
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ❌ Wrong - setTimeout breaks user gesture
|
|
78
|
+
button.onclick = () => {
|
|
79
|
+
setTimeout(() => link.open(), 100) // BLOCKED
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
interface EdgeLinkConfig {
|
|
87
|
+
// Required
|
|
88
|
+
clientId: string // Your OAuth client ID
|
|
89
|
+
environment: EdgeEnvironment // 'production' | 'staging' | 'sandbox'
|
|
90
|
+
onSuccess: (result) => void // Called on successful auth
|
|
91
|
+
|
|
92
|
+
// Optional
|
|
93
|
+
onExit?: (metadata) => void // Called when user exits
|
|
94
|
+
onEvent?: (event) => void // Called for analytics events
|
|
95
|
+
scopes?: EdgeScope[] // Scopes to request (default: all)
|
|
96
|
+
linkUrl?: string // Custom Link URL (dev only)
|
|
97
|
+
redirectUri?: string // Custom redirect URI
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Callbacks
|
|
102
|
+
|
|
103
|
+
### onSuccess
|
|
104
|
+
|
|
105
|
+
Called when user successfully authenticates and grants consent:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
onSuccess: (result) => {
|
|
109
|
+
// result.code - Authorization code (send to backend)
|
|
110
|
+
// result.codeVerifier - PKCE verifier (send to backend)
|
|
111
|
+
// result.state - State parameter (for validation)
|
|
112
|
+
|
|
113
|
+
// IMPORTANT: Exchange tokens on your backend, not here!
|
|
114
|
+
// The backend has your client secret, the browser doesn't.
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### onExit
|
|
119
|
+
|
|
120
|
+
Called when user exits the flow:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
onExit: (metadata) => {
|
|
124
|
+
switch (metadata.reason) {
|
|
125
|
+
case 'user_closed':
|
|
126
|
+
// User closed the popup
|
|
127
|
+
break
|
|
128
|
+
case 'popup_blocked':
|
|
129
|
+
// Popup was blocked - show instructions
|
|
130
|
+
alert('Please allow popups')
|
|
131
|
+
break
|
|
132
|
+
case 'error':
|
|
133
|
+
// An error occurred
|
|
134
|
+
console.error(metadata.error?.message)
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### onEvent
|
|
141
|
+
|
|
142
|
+
Called for analytics and debugging:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
onEvent: (event) => {
|
|
146
|
+
// event.eventName - 'OPEN' | 'CLOSE' | 'HANDOFF' | 'SUCCESS' | 'ERROR'
|
|
147
|
+
// event.timestamp - Unix timestamp
|
|
148
|
+
// event.metadata - Additional context
|
|
149
|
+
|
|
150
|
+
analytics.track('edge_link_event', event)
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Methods
|
|
155
|
+
|
|
156
|
+
| Method | Description |
|
|
157
|
+
|--------|-------------|
|
|
158
|
+
| `open(options?)` | Opens the Link popup (must be from click handler) |
|
|
159
|
+
| `close()` | Closes the popup programmatically |
|
|
160
|
+
| `destroy()` | Cleans up resources (call when done) |
|
|
161
|
+
| `isOpen()` | Returns true if popup is open |
|
|
162
|
+
|
|
163
|
+
## Scopes
|
|
164
|
+
|
|
165
|
+
Request only the permissions you need:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { EdgeLink, EDGE_SCOPES } from '@edgeboost/edge-connect-link'
|
|
169
|
+
|
|
170
|
+
const link = new EdgeLink({
|
|
171
|
+
clientId: 'your-client-id',
|
|
172
|
+
environment: 'staging',
|
|
173
|
+
scopes: [
|
|
174
|
+
EDGE_SCOPES.USER_READ, // Read profile
|
|
175
|
+
EDGE_SCOPES.BALANCE_READ, // Read balance
|
|
176
|
+
// EDGE_SCOPES.TRANSFER_WRITE - Only if you need transfers
|
|
177
|
+
],
|
|
178
|
+
onSuccess: handleSuccess,
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## React Example
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import { useEffect, useRef, useCallback } from 'react'
|
|
186
|
+
import { EdgeLink } from '@edgeboost/edge-connect-link'
|
|
187
|
+
|
|
188
|
+
function ConnectButton() {
|
|
189
|
+
const linkRef = useRef<EdgeLink | null>(null)
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
linkRef.current = new EdgeLink({
|
|
193
|
+
clientId: process.env.NEXT_PUBLIC_EDGE_CLIENT_ID!,
|
|
194
|
+
environment: 'staging',
|
|
195
|
+
onSuccess: async (result) => {
|
|
196
|
+
await fetch('/api/edge/exchange', {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
body: JSON.stringify(result),
|
|
199
|
+
})
|
|
200
|
+
// Refresh user state
|
|
201
|
+
},
|
|
202
|
+
onExit: (metadata) => {
|
|
203
|
+
if (metadata.reason === 'popup_blocked') {
|
|
204
|
+
alert('Please allow popups')
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
return () => linkRef.current?.destroy()
|
|
210
|
+
}, [])
|
|
211
|
+
|
|
212
|
+
const handleClick = useCallback(() => {
|
|
213
|
+
linkRef.current?.open()
|
|
214
|
+
}, [])
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<button onClick={handleClick}>
|
|
218
|
+
Connect EdgeBoost
|
|
219
|
+
</button>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## How It Works
|
|
225
|
+
|
|
226
|
+
1. **User clicks button** → `link.open()` is called
|
|
227
|
+
2. **Popup opens immediately** → Shows branded loading state
|
|
228
|
+
3. **PKCE generated** → Secure OAuth without client secret
|
|
229
|
+
4. **Popup navigates to Link page** → User logs in & grants consent
|
|
230
|
+
5. **Code returned via postMessage** → Secure cross-origin communication
|
|
231
|
+
6. **onSuccess called** → You send code to backend for token exchange
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
┌─────────────┐ ┌─────────────────┐ ┌──────────────┐
|
|
235
|
+
│ Your App │ │ EdgeLink Popup │ │ Your Backend │
|
|
236
|
+
└──────┬──────┘ └────────┬────────┘ └──────┬───────┘
|
|
237
|
+
│ │ │
|
|
238
|
+
│ link.open() │ │
|
|
239
|
+
├────────────────────►│ │
|
|
240
|
+
│ │ │
|
|
241
|
+
│ (user logs in, grants consent) │
|
|
242
|
+
│ │ │
|
|
243
|
+
│ postMessage(code) │ │
|
|
244
|
+
│◄────────────────────┤ │
|
|
245
|
+
│ │ │
|
|
246
|
+
│ onSuccess(result) │ │
|
|
247
|
+
├─────────────────────┼────────────────────►│
|
|
248
|
+
│ │ POST /api/exchange │
|
|
249
|
+
│ │ │
|
|
250
|
+
│ │ tokens │
|
|
251
|
+
│◄────────────────────┼─────────────────────┤
|
|
252
|
+
│ │ │
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Security
|
|
256
|
+
|
|
257
|
+
- **PKCE** - Prevents authorization code interception
|
|
258
|
+
- **State parameter** - Prevents CSRF attacks
|
|
259
|
+
- **Origin validation** - postMessage only accepted from expected origin
|
|
260
|
+
- **No client secret in browser** - Token exchange happens on your backend
|
|
261
|
+
|
|
262
|
+
## Related Packages
|
|
263
|
+
|
|
264
|
+
- `@edgeboost/edge-connect-sdk` - Core types and utilities
|
|
265
|
+
- `@edgeboost/edge-connect-server` - Server SDK for token exchange
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT
|
|
270
|
+
|
|
271
|
+
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { EdgeEnvironment, EdgeLinkSuccess, EdgeLinkExit, EdgeScope } from '@edge-markets/connect';
|
|
2
|
+
export { ALL_EDGE_SCOPES, EDGE_SCOPES, EdgeEnvironment, EdgeError, EdgeLinkExit, EdgeLinkSuccess, EdgePopupBlockedError, EdgeScope, EdgeStateMismatchError, isEdgeError } from '@edge-markets/connect';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EdgeLink - Plaid-Style Popup Authentication for EDGE Connect
|
|
6
|
+
*
|
|
7
|
+
* EdgeLink provides a simple, reliable way to authenticate users with EdgeBoost
|
|
8
|
+
* using a popup window - similar to how Plaid Link works.
|
|
9
|
+
*
|
|
10
|
+
* **Key Features:**
|
|
11
|
+
* - Callback-based API (onSuccess, onExit, onEvent)
|
|
12
|
+
* - Handles popup blockers gracefully
|
|
13
|
+
* - PKCE for secure OAuth (no client secret in browser)
|
|
14
|
+
* - Cross-origin communication via postMessage
|
|
15
|
+
* - CSRF protection via state parameter
|
|
16
|
+
*
|
|
17
|
+
* @module @edge-markets/connect-link
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { EdgeLink } from '@edge-markets/connect-link'
|
|
22
|
+
*
|
|
23
|
+
* const link = new EdgeLink({
|
|
24
|
+
* clientId: 'your-client-id',
|
|
25
|
+
* environment: 'staging',
|
|
26
|
+
* onSuccess: (result) => {
|
|
27
|
+
* // Send to your backend for token exchange
|
|
28
|
+
* fetch('/api/edge/exchange', {
|
|
29
|
+
* method: 'POST',
|
|
30
|
+
* body: JSON.stringify({
|
|
31
|
+
* code: result.code,
|
|
32
|
+
* codeVerifier: result.codeVerifier,
|
|
33
|
+
* }),
|
|
34
|
+
* })
|
|
35
|
+
* },
|
|
36
|
+
* onExit: (metadata) => {
|
|
37
|
+
* if (metadata.reason === 'popup_blocked') {
|
|
38
|
+
* alert('Please allow popups for this site')
|
|
39
|
+
* }
|
|
40
|
+
* },
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* // In a click handler
|
|
44
|
+
* document.getElementById('connect-btn')!.onclick = () => link.open()
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Configuration options for EdgeLink.
|
|
50
|
+
*
|
|
51
|
+
* Only `clientId`, `environment`, and `onSuccess` are required.
|
|
52
|
+
* Everything else has sensible defaults.
|
|
53
|
+
*/
|
|
54
|
+
interface EdgeLinkConfig {
|
|
55
|
+
/**
|
|
56
|
+
* Your OAuth client ID from the EdgeBoost partner portal.
|
|
57
|
+
* This is public and safe to include in frontend code.
|
|
58
|
+
*/
|
|
59
|
+
clientId: string;
|
|
60
|
+
/**
|
|
61
|
+
* Environment to connect to.
|
|
62
|
+
* - `'production'` - Live environment with real money
|
|
63
|
+
* - `'staging'` - Test environment for development
|
|
64
|
+
* - `'sandbox'` - Isolated mock environment (coming soon)
|
|
65
|
+
*/
|
|
66
|
+
environment: EdgeEnvironment;
|
|
67
|
+
/**
|
|
68
|
+
* Called when user successfully authenticates and grants consent.
|
|
69
|
+
*
|
|
70
|
+
* Send the `code` and `codeVerifier` to your backend for token exchange.
|
|
71
|
+
* **Never exchange tokens in the frontend** - that would expose your client secret.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* onSuccess: async (result) => {
|
|
76
|
+
* const response = await fetch('/api/edge/exchange', {
|
|
77
|
+
* method: 'POST',
|
|
78
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
* body: JSON.stringify({
|
|
80
|
+
* code: result.code,
|
|
81
|
+
* codeVerifier: result.codeVerifier,
|
|
82
|
+
* }),
|
|
83
|
+
* })
|
|
84
|
+
* if (response.ok) {
|
|
85
|
+
* showSuccess('EdgeBoost connected!')
|
|
86
|
+
* }
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
onSuccess: (result: EdgeLinkSuccess) => void;
|
|
91
|
+
/**
|
|
92
|
+
* Called when user exits the Link flow (closes popup, error, etc.).
|
|
93
|
+
*
|
|
94
|
+
* Use this to handle errors and provide user feedback.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* onExit: (metadata) => {
|
|
99
|
+
* switch (metadata.reason) {
|
|
100
|
+
* case 'user_closed':
|
|
101
|
+
* // User closed popup - maybe show "Connect later" option
|
|
102
|
+
* break
|
|
103
|
+
* case 'popup_blocked':
|
|
104
|
+
* showMessage('Please allow popups and try again')
|
|
105
|
+
* break
|
|
106
|
+
* case 'error':
|
|
107
|
+
* showError(metadata.error?.message || 'Something went wrong')
|
|
108
|
+
* break
|
|
109
|
+
* }
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
onExit?: (metadata: EdgeLinkExit) => void;
|
|
114
|
+
/**
|
|
115
|
+
* Called for various events during the Link flow.
|
|
116
|
+
* Useful for analytics and debugging.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* onEvent: (event) => {
|
|
121
|
+
* analytics.track('edge_link_event', {
|
|
122
|
+
* eventName: event.eventName,
|
|
123
|
+
* timestamp: event.timestamp,
|
|
124
|
+
* })
|
|
125
|
+
* }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
onEvent?: (event: EdgeLinkEvent) => void;
|
|
129
|
+
/**
|
|
130
|
+
* OAuth scopes to request.
|
|
131
|
+
* @default All available scopes
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* // Request only what you need
|
|
136
|
+
* scopes: ['user.read', 'balance.read']
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
scopes?: EdgeScope[];
|
|
140
|
+
/**
|
|
141
|
+
* Custom URL for the Link page.
|
|
142
|
+
* Only use for local development.
|
|
143
|
+
*
|
|
144
|
+
* @default Derived from environment config
|
|
145
|
+
*/
|
|
146
|
+
linkUrl?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Custom redirect URI after authentication.
|
|
149
|
+
* Must be registered in your OAuth client settings.
|
|
150
|
+
*
|
|
151
|
+
* @default `${window.location.origin}/oauth/edge/callback`
|
|
152
|
+
*/
|
|
153
|
+
redirectUri?: string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Event emitted during the Link flow.
|
|
157
|
+
*/
|
|
158
|
+
interface EdgeLinkEvent {
|
|
159
|
+
/** Name of the event */
|
|
160
|
+
eventName: EdgeLinkEventName;
|
|
161
|
+
/** Unix timestamp when event occurred */
|
|
162
|
+
timestamp: number;
|
|
163
|
+
/** Additional event-specific data */
|
|
164
|
+
metadata?: Record<string, unknown>;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Possible Link events.
|
|
168
|
+
*/
|
|
169
|
+
type EdgeLinkEventName = 'OPEN' | 'CLOSE' | 'HANDOFF' | 'TRANSITION' | 'ERROR' | 'SUCCESS';
|
|
170
|
+
/**
|
|
171
|
+
* Options for opening Link.
|
|
172
|
+
*/
|
|
173
|
+
interface EdgeLinkOpenOptions {
|
|
174
|
+
/**
|
|
175
|
+
* Override scopes for this open call.
|
|
176
|
+
*/
|
|
177
|
+
scopes?: EdgeScope[];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* EdgeLink - Simple popup-based authentication for EDGE Connect.
|
|
181
|
+
*
|
|
182
|
+
* Create one instance and reuse it. Call `open()` from a click handler.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* ```typescript
|
|
186
|
+
* // Create instance once
|
|
187
|
+
* const link = new EdgeLink({
|
|
188
|
+
* clientId: 'your-client-id',
|
|
189
|
+
* environment: 'staging',
|
|
190
|
+
* onSuccess: handleSuccess,
|
|
191
|
+
* onExit: handleExit,
|
|
192
|
+
* })
|
|
193
|
+
*
|
|
194
|
+
* // Open from click handler (required for popup to work)
|
|
195
|
+
* connectButton.onclick = () => link.open()
|
|
196
|
+
*
|
|
197
|
+
* // Clean up when done
|
|
198
|
+
* link.destroy()
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
declare class EdgeLink {
|
|
202
|
+
private readonly config;
|
|
203
|
+
private readonly popup;
|
|
204
|
+
private readonly expectedOrigin;
|
|
205
|
+
private pkce;
|
|
206
|
+
private state;
|
|
207
|
+
private messageHandler;
|
|
208
|
+
private isDestroyed;
|
|
209
|
+
/**
|
|
210
|
+
* Creates a new EdgeLink instance.
|
|
211
|
+
*
|
|
212
|
+
* @param config - Configuration options
|
|
213
|
+
* @throws Error if required config is missing or crypto is unavailable
|
|
214
|
+
*/
|
|
215
|
+
constructor(config: EdgeLinkConfig);
|
|
216
|
+
/**
|
|
217
|
+
* Opens the EdgeLink popup.
|
|
218
|
+
*
|
|
219
|
+
* **MUST be called directly from a user click handler!**
|
|
220
|
+
* Calling from setTimeout, Promise.then, or other async contexts will fail.
|
|
221
|
+
*
|
|
222
|
+
* @param options - Optional overrides for this open call
|
|
223
|
+
* @throws EdgePopupBlockedError if popup is blocked
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```typescript
|
|
227
|
+
* // ✅ Correct - direct click handler
|
|
228
|
+
* button.onclick = () => link.open()
|
|
229
|
+
*
|
|
230
|
+
* // ❌ Wrong - async gap
|
|
231
|
+
* button.onclick = async () => {
|
|
232
|
+
* await someAsyncWork()
|
|
233
|
+
* link.open() // Will be blocked!
|
|
234
|
+
* }
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
open(options?: EdgeLinkOpenOptions): void;
|
|
238
|
+
/**
|
|
239
|
+
* Closes the EdgeLink popup if open.
|
|
240
|
+
*
|
|
241
|
+
* Call this to programmatically close the popup without triggering onExit.
|
|
242
|
+
*/
|
|
243
|
+
close(): void;
|
|
244
|
+
/**
|
|
245
|
+
* Destroys the EdgeLink instance.
|
|
246
|
+
*
|
|
247
|
+
* Call this when unmounting your component or when done with EdgeLink.
|
|
248
|
+
* After destroy(), the instance cannot be reused.
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```typescript
|
|
252
|
+
* // React cleanup
|
|
253
|
+
* useEffect(() => {
|
|
254
|
+
* const link = new EdgeLink({ ... })
|
|
255
|
+
* return () => link.destroy()
|
|
256
|
+
* }, [])
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
destroy(): void;
|
|
260
|
+
/**
|
|
261
|
+
* Checks if the popup is currently open.
|
|
262
|
+
*/
|
|
263
|
+
isOpen(): boolean;
|
|
264
|
+
/**
|
|
265
|
+
* Initializes PKCE and navigates popup to auth URL.
|
|
266
|
+
*/
|
|
267
|
+
private initializeAuth;
|
|
268
|
+
/**
|
|
269
|
+
* Builds the URL for the EdgeLink page.
|
|
270
|
+
*
|
|
271
|
+
* The Link page handles:
|
|
272
|
+
* - User authentication (if not logged in)
|
|
273
|
+
* - Consent UI (showing requested permissions)
|
|
274
|
+
* - OAuth redirect to Cognito
|
|
275
|
+
* - Returning code via postMessage
|
|
276
|
+
*/
|
|
277
|
+
private buildLinkUrl;
|
|
278
|
+
/**
|
|
279
|
+
* Sets up the postMessage listener.
|
|
280
|
+
*/
|
|
281
|
+
private setupMessageListener;
|
|
282
|
+
/**
|
|
283
|
+
* Removes the postMessage listener.
|
|
284
|
+
*/
|
|
285
|
+
private removeMessageListener;
|
|
286
|
+
/**
|
|
287
|
+
* Handles successful authentication from popup.
|
|
288
|
+
*/
|
|
289
|
+
private handleSuccess;
|
|
290
|
+
/**
|
|
291
|
+
* Handles exit from popup.
|
|
292
|
+
*/
|
|
293
|
+
private handleExit;
|
|
294
|
+
/**
|
|
295
|
+
* Handles user closing the popup manually.
|
|
296
|
+
*/
|
|
297
|
+
private handleUserClose;
|
|
298
|
+
/**
|
|
299
|
+
* Emits an event.
|
|
300
|
+
*/
|
|
301
|
+
private emitEvent;
|
|
302
|
+
/**
|
|
303
|
+
* Cleans up stored state.
|
|
304
|
+
*/
|
|
305
|
+
private cleanup;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* PKCE (Proof Key for Code Exchange) Utilities
|
|
310
|
+
*
|
|
311
|
+
* PKCE is a security extension to OAuth 2.0 that prevents authorization code
|
|
312
|
+
* interception attacks. It's essential for public clients like browser apps
|
|
313
|
+
* where you can't securely store a client secret.
|
|
314
|
+
*
|
|
315
|
+
* How it works:
|
|
316
|
+
* 1. Generate a random `code_verifier` (high entropy secret)
|
|
317
|
+
* 2. Create `code_challenge` = base64url(sha256(code_verifier))
|
|
318
|
+
* 3. Send `code_challenge` in the authorization request
|
|
319
|
+
* 4. Send `code_verifier` in the token exchange
|
|
320
|
+
* 5. Server verifies: sha256(code_verifier) === code_challenge
|
|
321
|
+
*
|
|
322
|
+
* This ensures only the client that started the flow can complete it.
|
|
323
|
+
*
|
|
324
|
+
* @module @edge-markets/connect-link/pkce
|
|
325
|
+
*/
|
|
326
|
+
/**
|
|
327
|
+
* A PKCE code verifier and challenge pair.
|
|
328
|
+
*
|
|
329
|
+
* Keep the `verifier` secret until token exchange.
|
|
330
|
+
* Send the `challenge` in the authorization URL.
|
|
331
|
+
*/
|
|
332
|
+
interface PKCEPair {
|
|
333
|
+
/**
|
|
334
|
+
* High-entropy random string (43-128 characters).
|
|
335
|
+
* Keep this secret - only send during token exchange.
|
|
336
|
+
*/
|
|
337
|
+
verifier: string;
|
|
338
|
+
/**
|
|
339
|
+
* SHA-256 hash of verifier, base64url encoded.
|
|
340
|
+
* This is public - include in authorization URL.
|
|
341
|
+
*/
|
|
342
|
+
challenge: string;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Generates a PKCE code verifier and challenge pair.
|
|
346
|
+
*
|
|
347
|
+
* The verifier is a 128-character hex string (64 bytes of entropy).
|
|
348
|
+
* This exceeds the OAuth 2.0 PKCE spec minimum of 43 characters.
|
|
349
|
+
*
|
|
350
|
+
* @returns Promise resolving to verifier and challenge
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```typescript
|
|
354
|
+
* const pkce = await generatePKCE()
|
|
355
|
+
*
|
|
356
|
+
* // Include challenge in authorization URL
|
|
357
|
+
* const authUrl = new URL('https://auth.example.com/authorize')
|
|
358
|
+
* authUrl.searchParams.set('code_challenge', pkce.challenge)
|
|
359
|
+
* authUrl.searchParams.set('code_challenge_method', 'S256')
|
|
360
|
+
*
|
|
361
|
+
* // Later, include verifier in token exchange
|
|
362
|
+
* const tokens = await exchangeCode(code, pkce.verifier)
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
declare function generatePKCE(): Promise<PKCEPair>;
|
|
366
|
+
/**
|
|
367
|
+
* Generates a random state parameter for CSRF protection.
|
|
368
|
+
*
|
|
369
|
+
* The state parameter prevents cross-site request forgery attacks by ensuring
|
|
370
|
+
* the authorization response came from a request we initiated.
|
|
371
|
+
*
|
|
372
|
+
* @returns 64-character hex string (32 bytes of entropy)
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```typescript
|
|
376
|
+
* const state = generateState()
|
|
377
|
+
*
|
|
378
|
+
* // Store state before redirect
|
|
379
|
+
* sessionStorage.setItem('oauth_state', state)
|
|
380
|
+
*
|
|
381
|
+
* // Include in authorization URL
|
|
382
|
+
* authUrl.searchParams.set('state', state)
|
|
383
|
+
*
|
|
384
|
+
* // After redirect, verify state matches
|
|
385
|
+
* if (responseState !== sessionStorage.getItem('oauth_state')) {
|
|
386
|
+
* throw new Error('State mismatch - possible CSRF attack')
|
|
387
|
+
* }
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
declare function generateState(): string;
|
|
391
|
+
/**
|
|
392
|
+
* Validates that the Web Crypto API is available.
|
|
393
|
+
*
|
|
394
|
+
* Call this early to fail fast if running in an unsupported environment.
|
|
395
|
+
*
|
|
396
|
+
* @throws Error if Web Crypto API is not available
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* ```typescript
|
|
400
|
+
* try {
|
|
401
|
+
* assertCryptoAvailable()
|
|
402
|
+
* } catch {
|
|
403
|
+
* showMessage('Please use a modern browser with HTTPS')
|
|
404
|
+
* }
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
declare function assertCryptoAvailable(): void;
|
|
408
|
+
|
|
409
|
+
export { EdgeLink, type EdgeLinkConfig, type EdgeLinkEvent, type EdgeLinkEventName, type EdgeLinkOpenOptions, type PKCEPair, assertCryptoAvailable, generatePKCE, generateState };
|