@edge-markets/connect-node 1.0.3 → 1.2.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 +15 -9
- package/dist/index.d.mts +32 -348
- package/dist/index.d.ts +32 -348
- package/dist/index.js +127 -292
- package/dist/index.mjs +129 -293
- package/package.json +10 -12
package/dist/index.mjs
CHANGED
|
@@ -8,17 +8,33 @@ import {
|
|
|
8
8
|
EdgeInsufficientScopeError,
|
|
9
9
|
EdgeApiError,
|
|
10
10
|
EdgeNotFoundError,
|
|
11
|
-
EdgeNetworkError
|
|
11
|
+
EdgeNetworkError,
|
|
12
|
+
EdgeValidationError
|
|
12
13
|
} from "@edge-markets/connect";
|
|
13
14
|
var DEFAULT_TIMEOUT = 3e4;
|
|
14
15
|
var USER_AGENT = "@edge-markets/connect-node/1.0.0";
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
17
|
+
maxRetries: 3,
|
|
18
|
+
retryOn: [408, 429, 500, 502, 503, 504],
|
|
19
|
+
backoff: "exponential",
|
|
20
|
+
baseDelayMs: 1e3
|
|
21
|
+
};
|
|
22
|
+
var instances = /* @__PURE__ */ new Map();
|
|
23
|
+
function getInstanceKey(config) {
|
|
24
|
+
return `${config.clientId}:${config.environment}`;
|
|
25
|
+
}
|
|
26
|
+
var EdgeConnectServer = class _EdgeConnectServer {
|
|
27
|
+
static getInstance(config) {
|
|
28
|
+
const key = getInstanceKey(config);
|
|
29
|
+
const existing = instances.get(key);
|
|
30
|
+
if (existing) return existing;
|
|
31
|
+
const instance = new _EdgeConnectServer(config);
|
|
32
|
+
instances.set(key, instance);
|
|
33
|
+
return instance;
|
|
34
|
+
}
|
|
35
|
+
static clearInstances() {
|
|
36
|
+
instances.clear();
|
|
37
|
+
}
|
|
22
38
|
constructor(config) {
|
|
23
39
|
if (!config.clientId) {
|
|
24
40
|
throw new Error("EdgeConnectServer: clientId is required");
|
|
@@ -32,66 +48,29 @@ var EdgeConnectServer = class {
|
|
|
32
48
|
this.config = config;
|
|
33
49
|
const envConfig = getEnvironmentConfig(config.environment);
|
|
34
50
|
this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
|
|
35
|
-
this.oauthBaseUrl = envConfig.oauthBaseUrl;
|
|
51
|
+
this.oauthBaseUrl = config.oauthBaseUrl || envConfig.oauthBaseUrl;
|
|
36
52
|
this.timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
53
|
+
this.retryConfig = {
|
|
54
|
+
...DEFAULT_RETRY_CONFIG,
|
|
55
|
+
...config.retry
|
|
56
|
+
};
|
|
37
57
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// ===========================================================================
|
|
41
|
-
/**
|
|
42
|
-
* Exchanges an authorization code for tokens.
|
|
43
|
-
*
|
|
44
|
-
* Call this after receiving the code from EdgeLink's `onSuccess` callback.
|
|
45
|
-
* The code is single-use and expires in ~10 minutes.
|
|
46
|
-
*
|
|
47
|
-
* @param code - Authorization code from EdgeLink
|
|
48
|
-
* @param codeVerifier - PKCE code verifier from EdgeLink
|
|
49
|
-
* @returns Access token, refresh token, and metadata
|
|
50
|
-
* @throws EdgeTokenExchangeError if exchange fails
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```typescript
|
|
54
|
-
* // In your /api/edge/exchange endpoint
|
|
55
|
-
* const { code, codeVerifier } = req.body
|
|
56
|
-
*
|
|
57
|
-
* try {
|
|
58
|
-
* const tokens = await edge.exchangeCode(code, codeVerifier)
|
|
59
|
-
*
|
|
60
|
-
* // Store tokens securely
|
|
61
|
-
* await db.edgeConnections.upsert({
|
|
62
|
-
* userId: req.user.id,
|
|
63
|
-
* accessToken: encrypt(tokens.accessToken),
|
|
64
|
-
* refreshToken: encrypt(tokens.refreshToken),
|
|
65
|
-
* expiresAt: new Date(tokens.expiresAt),
|
|
66
|
-
* })
|
|
67
|
-
*
|
|
68
|
-
* return { success: true }
|
|
69
|
-
* } catch (error) {
|
|
70
|
-
* if (error instanceof EdgeTokenExchangeError) {
|
|
71
|
-
* // Code expired or already used
|
|
72
|
-
* return { error: 'Please try connecting again' }
|
|
73
|
-
* }
|
|
74
|
-
* throw error
|
|
75
|
-
* }
|
|
76
|
-
* ```
|
|
77
|
-
*/
|
|
78
|
-
async exchangeCode(code, codeVerifier) {
|
|
79
|
-
const tokenUrl = `${this.oauthBaseUrl}/oauth/token`;
|
|
58
|
+
async exchangeCode(code, codeVerifier, redirectUri) {
|
|
59
|
+
const tokenUrl = `${this.oauthBaseUrl}/token`;
|
|
80
60
|
const body = {
|
|
81
61
|
grant_type: "authorization_code",
|
|
82
62
|
code,
|
|
83
63
|
code_verifier: codeVerifier,
|
|
84
64
|
client_id: this.config.clientId,
|
|
85
|
-
client_secret: this.config.clientSecret
|
|
65
|
+
client_secret: this.config.clientSecret,
|
|
66
|
+
redirect_uri: redirectUri || this.config.redirectUri || ""
|
|
86
67
|
};
|
|
87
68
|
try {
|
|
88
69
|
const response = await this.fetchWithTimeout(tokenUrl, {
|
|
89
70
|
method: "POST",
|
|
90
71
|
headers: {
|
|
91
72
|
"Content-Type": "application/json",
|
|
92
|
-
"User-Agent": USER_AGENT
|
|
93
|
-
// Required for ngrok tunnels in local development
|
|
94
|
-
"ngrok-skip-browser-warning": "true"
|
|
73
|
+
"User-Agent": USER_AGENT
|
|
95
74
|
},
|
|
96
75
|
body: JSON.stringify(body)
|
|
97
76
|
});
|
|
@@ -106,44 +85,8 @@ var EdgeConnectServer = class {
|
|
|
106
85
|
throw new EdgeNetworkError("Failed to exchange code", error);
|
|
107
86
|
}
|
|
108
87
|
}
|
|
109
|
-
/**
|
|
110
|
-
* Refreshes an access token using a refresh token.
|
|
111
|
-
*
|
|
112
|
-
* Call this when the access token is expired or about to expire.
|
|
113
|
-
* Check `tokens.expiresAt` to know when to refresh.
|
|
114
|
-
*
|
|
115
|
-
* @param refreshToken - Refresh token from previous exchange
|
|
116
|
-
* @returns New tokens (refresh token may or may not change)
|
|
117
|
-
* @throws EdgeAuthenticationError if refresh fails
|
|
118
|
-
*
|
|
119
|
-
* @example
|
|
120
|
-
* ```typescript
|
|
121
|
-
* // Check if token needs refresh (with 5 minute buffer)
|
|
122
|
-
* const BUFFER_MS = 5 * 60 * 1000
|
|
123
|
-
*
|
|
124
|
-
* async function getValidAccessToken(userId: string): Promise<string> {
|
|
125
|
-
* const connection = await db.edgeConnections.get(userId)
|
|
126
|
-
*
|
|
127
|
-
* if (Date.now() > connection.expiresAt.getTime() - BUFFER_MS) {
|
|
128
|
-
* // Token expired or expiring soon - refresh it
|
|
129
|
-
* const newTokens = await edge.refreshTokens(decrypt(connection.refreshToken))
|
|
130
|
-
*
|
|
131
|
-
* // Update stored tokens
|
|
132
|
-
* await db.edgeConnections.update(userId, {
|
|
133
|
-
* accessToken: encrypt(newTokens.accessToken),
|
|
134
|
-
* refreshToken: encrypt(newTokens.refreshToken),
|
|
135
|
-
* expiresAt: new Date(newTokens.expiresAt),
|
|
136
|
-
* })
|
|
137
|
-
*
|
|
138
|
-
* return newTokens.accessToken
|
|
139
|
-
* }
|
|
140
|
-
*
|
|
141
|
-
* return decrypt(connection.accessToken)
|
|
142
|
-
* }
|
|
143
|
-
* ```
|
|
144
|
-
*/
|
|
145
88
|
async refreshTokens(refreshToken) {
|
|
146
|
-
const tokenUrl = `${this.oauthBaseUrl}/
|
|
89
|
+
const tokenUrl = `${this.oauthBaseUrl}/token`;
|
|
147
90
|
const body = {
|
|
148
91
|
grant_type: "refresh_token",
|
|
149
92
|
refresh_token: refreshToken,
|
|
@@ -155,15 +98,14 @@ var EdgeConnectServer = class {
|
|
|
155
98
|
method: "POST",
|
|
156
99
|
headers: {
|
|
157
100
|
"Content-Type": "application/json",
|
|
158
|
-
"User-Agent": USER_AGENT
|
|
159
|
-
"ngrok-skip-browser-warning": "true"
|
|
101
|
+
"User-Agent": USER_AGENT
|
|
160
102
|
},
|
|
161
103
|
body: JSON.stringify(body)
|
|
162
104
|
});
|
|
163
105
|
if (!response.ok) {
|
|
164
106
|
const error = await response.json().catch(() => ({}));
|
|
165
107
|
throw new EdgeAuthenticationError(
|
|
166
|
-
error.error_description || "Token refresh failed. User may need to reconnect.",
|
|
108
|
+
error.message || error.error_description || "Token refresh failed. User may need to reconnect.",
|
|
167
109
|
{ tokenError: error }
|
|
168
110
|
);
|
|
169
111
|
}
|
|
@@ -174,80 +116,14 @@ var EdgeConnectServer = class {
|
|
|
174
116
|
throw new EdgeNetworkError("Failed to refresh tokens", error);
|
|
175
117
|
}
|
|
176
118
|
}
|
|
177
|
-
// ===========================================================================
|
|
178
|
-
// USER & BALANCE
|
|
179
|
-
// ===========================================================================
|
|
180
|
-
/**
|
|
181
|
-
* Gets the connected user's profile.
|
|
182
|
-
*
|
|
183
|
-
* Requires scope: `user.read`
|
|
184
|
-
*
|
|
185
|
-
* @param accessToken - Valid access token
|
|
186
|
-
* @returns User profile information
|
|
187
|
-
*
|
|
188
|
-
* @example
|
|
189
|
-
* ```typescript
|
|
190
|
-
* const user = await edge.getUser(accessToken)
|
|
191
|
-
* console.log(`Connected: ${user.firstName} ${user.lastName}`)
|
|
192
|
-
* ```
|
|
193
|
-
*/
|
|
194
119
|
async getUser(accessToken) {
|
|
195
120
|
return this.apiRequest("GET", "/user", accessToken);
|
|
196
121
|
}
|
|
197
|
-
/**
|
|
198
|
-
* Gets the connected user's EdgeBoost balance.
|
|
199
|
-
*
|
|
200
|
-
* Requires scope: `balance.read`
|
|
201
|
-
*
|
|
202
|
-
* @param accessToken - Valid access token
|
|
203
|
-
* @returns Balance information
|
|
204
|
-
*
|
|
205
|
-
* @example
|
|
206
|
-
* ```typescript
|
|
207
|
-
* const balance = await edge.getBalance(accessToken)
|
|
208
|
-
* console.log(`Balance: $${balance.availableBalance.toFixed(2)} ${balance.currency}`)
|
|
209
|
-
* ```
|
|
210
|
-
*/
|
|
211
122
|
async getBalance(accessToken) {
|
|
212
123
|
return this.apiRequest("GET", "/balance", accessToken);
|
|
213
124
|
}
|
|
214
|
-
// ===========================================================================
|
|
215
|
-
// TRANSFERS
|
|
216
|
-
// ===========================================================================
|
|
217
|
-
/**
|
|
218
|
-
* Initiates a fund transfer.
|
|
219
|
-
*
|
|
220
|
-
* Requires scope: `transfer.write`
|
|
221
|
-
*
|
|
222
|
-
* **Transfer Types:**
|
|
223
|
-
* - `debit`: Pull funds FROM user's EdgeBoost TO your platform
|
|
224
|
-
* - `credit`: Push funds FROM your platform TO user's EdgeBoost
|
|
225
|
-
*
|
|
226
|
-
* **Idempotency:** Using the same `idempotencyKey` returns the existing
|
|
227
|
-
* transfer instead of creating a duplicate. Use a unique key per transaction.
|
|
228
|
-
*
|
|
229
|
-
* **OTP Verification:** Transfers require OTP verification before completion.
|
|
230
|
-
* The response includes `otpMethod` indicating how the user will receive the code.
|
|
231
|
-
*
|
|
232
|
-
* @param accessToken - Valid access token
|
|
233
|
-
* @param options - Transfer options
|
|
234
|
-
* @returns Transfer with status and OTP method
|
|
235
|
-
*
|
|
236
|
-
* @example
|
|
237
|
-
* ```typescript
|
|
238
|
-
* const transfer = await edge.initiateTransfer(accessToken, {
|
|
239
|
-
* type: 'debit',
|
|
240
|
-
* amount: '100.00',
|
|
241
|
-
* idempotencyKey: `withdraw_${userId}_${Date.now()}`,
|
|
242
|
-
* })
|
|
243
|
-
*
|
|
244
|
-
* if (transfer.status === 'pending_verification') {
|
|
245
|
-
* // Show OTP input to user
|
|
246
|
-
* console.log(`Enter code sent via ${transfer.otpMethod}`)
|
|
247
|
-
* }
|
|
248
|
-
* ```
|
|
249
|
-
*/
|
|
250
125
|
async initiateTransfer(accessToken, options) {
|
|
126
|
+
this.validateTransferOptions(options);
|
|
251
127
|
const body = {
|
|
252
128
|
type: options.type,
|
|
253
129
|
amount: options.amount,
|
|
@@ -255,28 +131,32 @@ var EdgeConnectServer = class {
|
|
|
255
131
|
};
|
|
256
132
|
return this.apiRequest("POST", "/transfer", accessToken, body);
|
|
257
133
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
134
|
+
validateTransferOptions(options) {
|
|
135
|
+
const errors = {};
|
|
136
|
+
if (!options.type || !["debit", "credit"].includes(options.type)) {
|
|
137
|
+
errors.type = ['Must be "debit" or "credit"'];
|
|
138
|
+
}
|
|
139
|
+
if (!options.amount) {
|
|
140
|
+
errors.amount = ["Amount is required"];
|
|
141
|
+
} else {
|
|
142
|
+
const amount = parseFloat(options.amount);
|
|
143
|
+
if (isNaN(amount)) {
|
|
144
|
+
errors.amount = ["Must be a valid number"];
|
|
145
|
+
} else if (amount <= 0) {
|
|
146
|
+
errors.amount = ["Must be greater than 0"];
|
|
147
|
+
} else if (!/^\d+(\.\d{1,2})?$/.test(options.amount)) {
|
|
148
|
+
errors.amount = ["Must have at most 2 decimal places"];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!options.idempotencyKey || options.idempotencyKey.trim() === "") {
|
|
152
|
+
errors.idempotencyKey = ["idempotencyKey is required"];
|
|
153
|
+
} else if (options.idempotencyKey.length > 255) {
|
|
154
|
+
errors.idempotencyKey = ["Must be 255 characters or less"];
|
|
155
|
+
}
|
|
156
|
+
if (Object.keys(errors).length > 0) {
|
|
157
|
+
throw new EdgeValidationError("Invalid transfer options", errors);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
280
160
|
async verifyTransfer(accessToken, transferId, otp) {
|
|
281
161
|
return this.apiRequest(
|
|
282
162
|
"POST",
|
|
@@ -285,21 +165,6 @@ var EdgeConnectServer = class {
|
|
|
285
165
|
{ otp }
|
|
286
166
|
);
|
|
287
167
|
}
|
|
288
|
-
/**
|
|
289
|
-
* Gets the status of a transfer.
|
|
290
|
-
*
|
|
291
|
-
* Use for polling after initiating a transfer.
|
|
292
|
-
*
|
|
293
|
-
* @param accessToken - Valid access token
|
|
294
|
-
* @param transferId - Transfer ID
|
|
295
|
-
* @returns Current transfer status
|
|
296
|
-
*
|
|
297
|
-
* @example
|
|
298
|
-
* ```typescript
|
|
299
|
-
* const transfer = await edge.getTransfer(accessToken, transferId)
|
|
300
|
-
* console.log(`Status: ${transfer.status}`)
|
|
301
|
-
* ```
|
|
302
|
-
*/
|
|
303
168
|
async getTransfer(accessToken, transferId) {
|
|
304
169
|
return this.apiRequest(
|
|
305
170
|
"GET",
|
|
@@ -307,27 +172,6 @@ var EdgeConnectServer = class {
|
|
|
307
172
|
accessToken
|
|
308
173
|
);
|
|
309
174
|
}
|
|
310
|
-
/**
|
|
311
|
-
* Lists transfers for the connected user.
|
|
312
|
-
*
|
|
313
|
-
* Useful for reconciliation and showing transfer history.
|
|
314
|
-
*
|
|
315
|
-
* @param accessToken - Valid access token
|
|
316
|
-
* @param params - Pagination and filter options
|
|
317
|
-
* @returns Paginated list of transfers
|
|
318
|
-
*
|
|
319
|
-
* @example
|
|
320
|
-
* ```typescript
|
|
321
|
-
* // Get first page of completed transfers
|
|
322
|
-
* const { transfers, total } = await edge.listTransfers(accessToken, {
|
|
323
|
-
* status: 'completed',
|
|
324
|
-
* limit: 10,
|
|
325
|
-
* offset: 0,
|
|
326
|
-
* })
|
|
327
|
-
*
|
|
328
|
-
* console.log(`Showing ${transfers.length} of ${total} transfers`)
|
|
329
|
-
* ```
|
|
330
|
-
*/
|
|
331
175
|
async listTransfers(accessToken, params) {
|
|
332
176
|
const queryParams = new URLSearchParams();
|
|
333
177
|
if (params?.limit) queryParams.set("limit", String(params.limit));
|
|
@@ -337,68 +181,65 @@ var EdgeConnectServer = class {
|
|
|
337
181
|
const path = query ? `/transfers?${query}` : "/transfers";
|
|
338
182
|
return this.apiRequest("GET", path, accessToken);
|
|
339
183
|
}
|
|
340
|
-
// ===========================================================================
|
|
341
|
-
// CONSENT
|
|
342
|
-
// ===========================================================================
|
|
343
|
-
/**
|
|
344
|
-
* Revokes the user's consent (disconnects their account).
|
|
345
|
-
*
|
|
346
|
-
* After revocation:
|
|
347
|
-
* - All API calls will fail with `consent_required` error
|
|
348
|
-
* - User must go through EdgeLink again to reconnect
|
|
349
|
-
* - Stored tokens become invalid
|
|
350
|
-
*
|
|
351
|
-
* Use this for "Disconnect" or "Unlink" features in your app.
|
|
352
|
-
*
|
|
353
|
-
* @param accessToken - Valid access token
|
|
354
|
-
* @returns Confirmation of revocation
|
|
355
|
-
*
|
|
356
|
-
* @example
|
|
357
|
-
* ```typescript
|
|
358
|
-
* // Disconnect user's EdgeBoost account
|
|
359
|
-
* await edge.revokeConsent(accessToken)
|
|
360
|
-
*
|
|
361
|
-
* // Clean up stored tokens
|
|
362
|
-
* await db.edgeConnections.delete(userId)
|
|
363
|
-
*
|
|
364
|
-
* console.log('EdgeBoost disconnected')
|
|
365
|
-
* ```
|
|
366
|
-
*/
|
|
367
184
|
async revokeConsent(accessToken) {
|
|
368
185
|
return this.apiRequest("DELETE", "/consent", accessToken);
|
|
369
186
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
187
|
+
getRetryDelay(attempt) {
|
|
188
|
+
const { backoff, baseDelayMs } = this.retryConfig;
|
|
189
|
+
if (backoff === "exponential") {
|
|
190
|
+
return baseDelayMs * Math.pow(2, attempt);
|
|
191
|
+
}
|
|
192
|
+
return baseDelayMs * (attempt + 1);
|
|
193
|
+
}
|
|
194
|
+
async sleep(ms) {
|
|
195
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
196
|
+
}
|
|
376
197
|
async apiRequest(method, path, accessToken, body) {
|
|
377
198
|
const url = `${this.apiBaseUrl}${path}`;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
199
|
+
let lastError = null;
|
|
200
|
+
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
201
|
+
const startTime = Date.now();
|
|
202
|
+
if (attempt > 0) {
|
|
203
|
+
await this.sleep(this.getRetryDelay(attempt - 1));
|
|
204
|
+
}
|
|
205
|
+
this.config.onRequest?.({ method, url, body });
|
|
206
|
+
try {
|
|
207
|
+
const response = await this.fetchWithTimeout(url, {
|
|
208
|
+
method,
|
|
209
|
+
headers: {
|
|
210
|
+
Authorization: `Bearer ${accessToken}`,
|
|
211
|
+
"Content-Type": "application/json",
|
|
212
|
+
"User-Agent": USER_AGENT
|
|
213
|
+
},
|
|
214
|
+
body: body ? JSON.stringify(body) : void 0
|
|
215
|
+
});
|
|
216
|
+
const responseBody = await response.json().catch(() => ({}));
|
|
217
|
+
const durationMs = Date.now() - startTime;
|
|
218
|
+
this.config.onResponse?.({ status: response.status, body: responseBody, durationMs });
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
if (this.retryConfig.retryOn.includes(response.status) && attempt < this.retryConfig.maxRetries) {
|
|
221
|
+
lastError = await this.handleApiErrorFromBody(responseBody, response.status, path);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
throw await this.handleApiErrorFromBody(responseBody, response.status, path);
|
|
225
|
+
}
|
|
226
|
+
return responseBody;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof EdgeError) {
|
|
229
|
+
if (!(error instanceof EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
lastError = error;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
lastError = error;
|
|
236
|
+
if (attempt >= this.retryConfig.maxRetries) {
|
|
237
|
+
throw new EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
|
|
238
|
+
}
|
|
392
239
|
}
|
|
393
|
-
return response.json();
|
|
394
|
-
} catch (error) {
|
|
395
|
-
if (error instanceof EdgeError) throw error;
|
|
396
|
-
throw new EdgeNetworkError(`API request failed: ${method} ${path}`, error);
|
|
397
240
|
}
|
|
241
|
+
throw new EdgeNetworkError(`API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`, lastError);
|
|
398
242
|
}
|
|
399
|
-
/**
|
|
400
|
-
* Fetch with timeout support.
|
|
401
|
-
*/
|
|
402
243
|
async fetchWithTimeout(url, options) {
|
|
403
244
|
const controller = new AbortController();
|
|
404
245
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
@@ -411,9 +252,6 @@ var EdgeConnectServer = class {
|
|
|
411
252
|
clearTimeout(timeoutId);
|
|
412
253
|
}
|
|
413
254
|
}
|
|
414
|
-
/**
|
|
415
|
-
* Parses token response from Cognito.
|
|
416
|
-
*/
|
|
417
255
|
parseTokenResponse(data, existingRefreshToken) {
|
|
418
256
|
return {
|
|
419
257
|
accessToken: data.access_token,
|
|
@@ -424,35 +262,33 @@ var EdgeConnectServer = class {
|
|
|
424
262
|
scope: data.scope || ""
|
|
425
263
|
};
|
|
426
264
|
}
|
|
427
|
-
/**
|
|
428
|
-
* Handles token exchange errors.
|
|
429
|
-
*/
|
|
430
265
|
handleTokenError(error, status) {
|
|
431
266
|
const errorCode = error.error;
|
|
432
|
-
const
|
|
433
|
-
if (errorCode === "invalid_grant") {
|
|
267
|
+
const errorMessage = error.message || error.error_description;
|
|
268
|
+
if (errorCode === "invalid_grant" || errorMessage?.includes("Invalid or expired")) {
|
|
434
269
|
return new EdgeTokenExchangeError(
|
|
435
270
|
"Authorization code is invalid, expired, or already used. Please try again.",
|
|
436
|
-
{
|
|
271
|
+
{ edgeBoostError: error }
|
|
437
272
|
);
|
|
438
273
|
}
|
|
439
|
-
if (errorCode === "invalid_client") {
|
|
274
|
+
if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
|
|
440
275
|
return new EdgeAuthenticationError(
|
|
441
276
|
"Invalid client credentials. Check your client ID and secret.",
|
|
442
|
-
{
|
|
277
|
+
{ edgeBoostError: error }
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
if (errorMessage?.includes("code_verifier") || errorMessage?.includes("PKCE")) {
|
|
281
|
+
return new EdgeTokenExchangeError(
|
|
282
|
+
"PKCE verification failed. Please try again.",
|
|
283
|
+
{ edgeBoostError: error }
|
|
443
284
|
);
|
|
444
285
|
}
|
|
445
286
|
return new EdgeTokenExchangeError(
|
|
446
|
-
|
|
447
|
-
{
|
|
287
|
+
errorMessage || "Failed to exchange authorization code",
|
|
288
|
+
{ edgeBoostError: error, statusCode: status }
|
|
448
289
|
);
|
|
449
290
|
}
|
|
450
|
-
|
|
451
|
-
* Handles API errors.
|
|
452
|
-
*/
|
|
453
|
-
async handleApiError(response, path) {
|
|
454
|
-
const error = await response.json().catch(() => ({}));
|
|
455
|
-
const status = response.status;
|
|
291
|
+
async handleApiErrorFromBody(error, status, path) {
|
|
456
292
|
if (status === 401) {
|
|
457
293
|
return new EdgeAuthenticationError(
|
|
458
294
|
error.message || "Access token is invalid or expired",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edge-markets/connect-node",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Server SDK for EDGE Connect token exchange and API calls",
|
|
5
5
|
"author": "Edge Markets",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,15 +20,8 @@
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
|
-
"scripts": {
|
|
24
|
-
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
25
|
-
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
26
|
-
"typecheck": "tsc --noEmit",
|
|
27
|
-
"test": "vitest --passWithNoTests",
|
|
28
|
-
"test:run": "vitest run --passWithNoTests"
|
|
29
|
-
},
|
|
30
23
|
"dependencies": {
|
|
31
|
-
"@edge-markets/connect": "^1.
|
|
24
|
+
"@edge-markets/connect": "^1.2.0"
|
|
32
25
|
},
|
|
33
26
|
"devDependencies": {
|
|
34
27
|
"tsup": "^8.0.0",
|
|
@@ -55,7 +48,12 @@
|
|
|
55
48
|
},
|
|
56
49
|
"engines": {
|
|
57
50
|
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
54
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"test": "vitest --passWithNoTests",
|
|
57
|
+
"test:run": "vitest run --passWithNoTests"
|
|
58
58
|
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
}
|