@edge-markets/connect-node 1.0.3 → 1.3.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/index.js CHANGED
@@ -41,15 +41,107 @@ module.exports = __toCommonJS(index_exports);
41
41
 
42
42
  // src/edge-connect-server.ts
43
43
  var import_connect = require("@edge-markets/connect");
44
+
45
+ // src/mle.ts
46
+ var import_crypto = require("crypto");
47
+ function encryptMle(payload, clientId, config) {
48
+ const header = {
49
+ alg: "RSA-OAEP-256",
50
+ enc: "A256GCM",
51
+ kid: config.edgeKeyId,
52
+ typ: "application/json",
53
+ iat: Math.floor(Date.now() / 1e3),
54
+ jti: (0, import_crypto.randomUUID)(),
55
+ clientId
56
+ };
57
+ const protectedHeaderB64 = toBase64Url(Buffer.from(JSON.stringify(header), "utf8"));
58
+ const aad = Buffer.from(protectedHeaderB64, "utf8");
59
+ const cek = (0, import_crypto.randomBytes)(32);
60
+ const iv = (0, import_crypto.randomBytes)(12);
61
+ const cipher = (0, import_crypto.createCipheriv)("aes-256-gcm", cek, iv);
62
+ cipher.setAAD(aad);
63
+ const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
64
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
65
+ const tag = cipher.getAuthTag();
66
+ const encryptedKey = (0, import_crypto.publicEncrypt)(
67
+ {
68
+ key: config.edgePublicKey,
69
+ padding: import_crypto.constants.RSA_PKCS1_OAEP_PADDING,
70
+ oaepHash: "sha256"
71
+ },
72
+ cek
73
+ );
74
+ return [
75
+ protectedHeaderB64,
76
+ toBase64Url(encryptedKey),
77
+ toBase64Url(iv),
78
+ toBase64Url(ciphertext),
79
+ toBase64Url(tag)
80
+ ].join(".");
81
+ }
82
+ function decryptMle(jwe, config) {
83
+ const parts = jwe.split(".");
84
+ if (parts.length !== 5) {
85
+ throw new Error("Invalid MLE payload format");
86
+ }
87
+ const [protectedHeaderB64, encryptedKeyB64, ivB64, ciphertextB64, tagB64] = parts;
88
+ const protectedHeader = JSON.parse(fromBase64Url(protectedHeaderB64).toString("utf8"));
89
+ if (protectedHeader.alg !== "RSA-OAEP-256" || protectedHeader.enc !== "A256GCM") {
90
+ throw new Error("Unsupported MLE algorithms");
91
+ }
92
+ if (protectedHeader.kid && protectedHeader.kid !== config.partnerKeyId) {
93
+ throw new Error(`Unexpected response key id: ${protectedHeader.kid}`);
94
+ }
95
+ const cek = (0, import_crypto.privateDecrypt)(
96
+ {
97
+ key: config.partnerPrivateKey,
98
+ padding: import_crypto.constants.RSA_PKCS1_OAEP_PADDING,
99
+ oaepHash: "sha256"
100
+ },
101
+ fromBase64Url(encryptedKeyB64)
102
+ );
103
+ const decipher = (0, import_crypto.createDecipheriv)("aes-256-gcm", cek, fromBase64Url(ivB64));
104
+ decipher.setAAD(Buffer.from(protectedHeaderB64, "utf8"));
105
+ decipher.setAuthTag(fromBase64Url(tagB64));
106
+ const plaintext = Buffer.concat([
107
+ decipher.update(fromBase64Url(ciphertextB64)),
108
+ decipher.final()
109
+ ]);
110
+ return JSON.parse(plaintext.toString("utf8"));
111
+ }
112
+ function toBase64Url(buffer) {
113
+ return buffer.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
114
+ }
115
+ function fromBase64Url(value) {
116
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
117
+ return Buffer.from(normalized, "base64");
118
+ }
119
+
120
+ // src/edge-connect-server.ts
44
121
  var DEFAULT_TIMEOUT = 3e4;
45
122
  var USER_AGENT = "@edge-markets/connect-node/1.0.0";
46
- var EdgeConnectServer = class {
47
- /**
48
- * Creates a new EdgeConnectServer instance.
49
- *
50
- * @param config - Server configuration
51
- * @throws Error if required config is missing
52
- */
123
+ var DEFAULT_RETRY_CONFIG = {
124
+ maxRetries: 3,
125
+ retryOn: [408, 429, 500, 502, 503, 504],
126
+ backoff: "exponential",
127
+ baseDelayMs: 1e3
128
+ };
129
+ var instances = /* @__PURE__ */ new Map();
130
+ function getInstanceKey(config) {
131
+ return `${config.clientId}:${config.environment}`;
132
+ }
133
+ var EdgeConnectServer = class _EdgeConnectServer {
134
+ static getInstance(config) {
135
+ const key = getInstanceKey(config);
136
+ const existing = instances.get(key);
137
+ if (existing) return existing;
138
+ const instance = new _EdgeConnectServer(config);
139
+ instances.set(key, instance);
140
+ return instance;
141
+ }
142
+ static clearInstances() {
143
+ instances.clear();
144
+ }
53
145
  constructor(config) {
54
146
  if (!config.clientId) {
55
147
  throw new Error("EdgeConnectServer: clientId is required");
@@ -63,66 +155,29 @@ var EdgeConnectServer = class {
63
155
  this.config = config;
64
156
  const envConfig = (0, import_connect.getEnvironmentConfig)(config.environment);
65
157
  this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
66
- this.oauthBaseUrl = envConfig.oauthBaseUrl;
158
+ this.oauthBaseUrl = config.oauthBaseUrl || envConfig.oauthBaseUrl;
67
159
  this.timeout = config.timeout || DEFAULT_TIMEOUT;
160
+ this.retryConfig = {
161
+ ...DEFAULT_RETRY_CONFIG,
162
+ ...config.retry
163
+ };
68
164
  }
69
- // ===========================================================================
70
- // TOKEN OPERATIONS
71
- // ===========================================================================
72
- /**
73
- * Exchanges an authorization code for tokens.
74
- *
75
- * Call this after receiving the code from EdgeLink's `onSuccess` callback.
76
- * The code is single-use and expires in ~10 minutes.
77
- *
78
- * @param code - Authorization code from EdgeLink
79
- * @param codeVerifier - PKCE code verifier from EdgeLink
80
- * @returns Access token, refresh token, and metadata
81
- * @throws EdgeTokenExchangeError if exchange fails
82
- *
83
- * @example
84
- * ```typescript
85
- * // In your /api/edge/exchange endpoint
86
- * const { code, codeVerifier } = req.body
87
- *
88
- * try {
89
- * const tokens = await edge.exchangeCode(code, codeVerifier)
90
- *
91
- * // Store tokens securely
92
- * await db.edgeConnections.upsert({
93
- * userId: req.user.id,
94
- * accessToken: encrypt(tokens.accessToken),
95
- * refreshToken: encrypt(tokens.refreshToken),
96
- * expiresAt: new Date(tokens.expiresAt),
97
- * })
98
- *
99
- * return { success: true }
100
- * } catch (error) {
101
- * if (error instanceof EdgeTokenExchangeError) {
102
- * // Code expired or already used
103
- * return { error: 'Please try connecting again' }
104
- * }
105
- * throw error
106
- * }
107
- * ```
108
- */
109
- async exchangeCode(code, codeVerifier) {
110
- const tokenUrl = `${this.oauthBaseUrl}/oauth/token`;
165
+ async exchangeCode(code, codeVerifier, redirectUri) {
166
+ const tokenUrl = `${this.oauthBaseUrl}/token`;
111
167
  const body = {
112
168
  grant_type: "authorization_code",
113
169
  code,
114
170
  code_verifier: codeVerifier,
115
171
  client_id: this.config.clientId,
116
- client_secret: this.config.clientSecret
172
+ client_secret: this.config.clientSecret,
173
+ redirect_uri: redirectUri || this.config.redirectUri || ""
117
174
  };
118
175
  try {
119
176
  const response = await this.fetchWithTimeout(tokenUrl, {
120
177
  method: "POST",
121
178
  headers: {
122
179
  "Content-Type": "application/json",
123
- "User-Agent": USER_AGENT,
124
- // Required for ngrok tunnels in local development
125
- "ngrok-skip-browser-warning": "true"
180
+ "User-Agent": USER_AGENT
126
181
  },
127
182
  body: JSON.stringify(body)
128
183
  });
@@ -137,44 +192,8 @@ var EdgeConnectServer = class {
137
192
  throw new import_connect.EdgeNetworkError("Failed to exchange code", error);
138
193
  }
139
194
  }
140
- /**
141
- * Refreshes an access token using a refresh token.
142
- *
143
- * Call this when the access token is expired or about to expire.
144
- * Check `tokens.expiresAt` to know when to refresh.
145
- *
146
- * @param refreshToken - Refresh token from previous exchange
147
- * @returns New tokens (refresh token may or may not change)
148
- * @throws EdgeAuthenticationError if refresh fails
149
- *
150
- * @example
151
- * ```typescript
152
- * // Check if token needs refresh (with 5 minute buffer)
153
- * const BUFFER_MS = 5 * 60 * 1000
154
- *
155
- * async function getValidAccessToken(userId: string): Promise<string> {
156
- * const connection = await db.edgeConnections.get(userId)
157
- *
158
- * if (Date.now() > connection.expiresAt.getTime() - BUFFER_MS) {
159
- * // Token expired or expiring soon - refresh it
160
- * const newTokens = await edge.refreshTokens(decrypt(connection.refreshToken))
161
- *
162
- * // Update stored tokens
163
- * await db.edgeConnections.update(userId, {
164
- * accessToken: encrypt(newTokens.accessToken),
165
- * refreshToken: encrypt(newTokens.refreshToken),
166
- * expiresAt: new Date(newTokens.expiresAt),
167
- * })
168
- *
169
- * return newTokens.accessToken
170
- * }
171
- *
172
- * return decrypt(connection.accessToken)
173
- * }
174
- * ```
175
- */
176
195
  async refreshTokens(refreshToken) {
177
- const tokenUrl = `${this.oauthBaseUrl}/oauth/token`;
196
+ const tokenUrl = `${this.oauthBaseUrl}/token`;
178
197
  const body = {
179
198
  grant_type: "refresh_token",
180
199
  refresh_token: refreshToken,
@@ -186,15 +205,14 @@ var EdgeConnectServer = class {
186
205
  method: "POST",
187
206
  headers: {
188
207
  "Content-Type": "application/json",
189
- "User-Agent": USER_AGENT,
190
- "ngrok-skip-browser-warning": "true"
208
+ "User-Agent": USER_AGENT
191
209
  },
192
210
  body: JSON.stringify(body)
193
211
  });
194
212
  if (!response.ok) {
195
213
  const error = await response.json().catch(() => ({}));
196
214
  throw new import_connect.EdgeAuthenticationError(
197
- error.error_description || "Token refresh failed. User may need to reconnect.",
215
+ error.message || error.error_description || "Token refresh failed. User may need to reconnect.",
198
216
  { tokenError: error }
199
217
  );
200
218
  }
@@ -205,80 +223,14 @@ var EdgeConnectServer = class {
205
223
  throw new import_connect.EdgeNetworkError("Failed to refresh tokens", error);
206
224
  }
207
225
  }
208
- // ===========================================================================
209
- // USER & BALANCE
210
- // ===========================================================================
211
- /**
212
- * Gets the connected user's profile.
213
- *
214
- * Requires scope: `user.read`
215
- *
216
- * @param accessToken - Valid access token
217
- * @returns User profile information
218
- *
219
- * @example
220
- * ```typescript
221
- * const user = await edge.getUser(accessToken)
222
- * console.log(`Connected: ${user.firstName} ${user.lastName}`)
223
- * ```
224
- */
225
226
  async getUser(accessToken) {
226
227
  return this.apiRequest("GET", "/user", accessToken);
227
228
  }
228
- /**
229
- * Gets the connected user's EdgeBoost balance.
230
- *
231
- * Requires scope: `balance.read`
232
- *
233
- * @param accessToken - Valid access token
234
- * @returns Balance information
235
- *
236
- * @example
237
- * ```typescript
238
- * const balance = await edge.getBalance(accessToken)
239
- * console.log(`Balance: $${balance.availableBalance.toFixed(2)} ${balance.currency}`)
240
- * ```
241
- */
242
229
  async getBalance(accessToken) {
243
230
  return this.apiRequest("GET", "/balance", accessToken);
244
231
  }
245
- // ===========================================================================
246
- // TRANSFERS
247
- // ===========================================================================
248
- /**
249
- * Initiates a fund transfer.
250
- *
251
- * Requires scope: `transfer.write`
252
- *
253
- * **Transfer Types:**
254
- * - `debit`: Pull funds FROM user's EdgeBoost TO your platform
255
- * - `credit`: Push funds FROM your platform TO user's EdgeBoost
256
- *
257
- * **Idempotency:** Using the same `idempotencyKey` returns the existing
258
- * transfer instead of creating a duplicate. Use a unique key per transaction.
259
- *
260
- * **OTP Verification:** Transfers require OTP verification before completion.
261
- * The response includes `otpMethod` indicating how the user will receive the code.
262
- *
263
- * @param accessToken - Valid access token
264
- * @param options - Transfer options
265
- * @returns Transfer with status and OTP method
266
- *
267
- * @example
268
- * ```typescript
269
- * const transfer = await edge.initiateTransfer(accessToken, {
270
- * type: 'debit',
271
- * amount: '100.00',
272
- * idempotencyKey: `withdraw_${userId}_${Date.now()}`,
273
- * })
274
- *
275
- * if (transfer.status === 'pending_verification') {
276
- * // Show OTP input to user
277
- * console.log(`Enter code sent via ${transfer.otpMethod}`)
278
- * }
279
- * ```
280
- */
281
232
  async initiateTransfer(accessToken, options) {
233
+ this.validateTransferOptions(options);
282
234
  const body = {
283
235
  type: options.type,
284
236
  amount: options.amount,
@@ -286,28 +238,32 @@ var EdgeConnectServer = class {
286
238
  };
287
239
  return this.apiRequest("POST", "/transfer", accessToken, body);
288
240
  }
289
- /**
290
- * Verifies a pending transfer with OTP.
291
- *
292
- * Call this after the user enters the OTP code they received.
293
- * The OTP is valid for ~5 minutes.
294
- *
295
- * @param accessToken - Valid access token
296
- * @param transferId - Transfer ID from initiateTransfer
297
- * @param otp - 6-digit OTP code from user
298
- * @returns Updated transfer (status will be 'completed' or 'failed')
299
- *
300
- * @example
301
- * ```typescript
302
- * const result = await edge.verifyTransfer(accessToken, transferId, userOtp)
303
- *
304
- * if (result.status === 'completed') {
305
- * console.log('Transfer successful!')
306
- * } else if (result.status === 'failed') {
307
- * console.log('Transfer failed - possibly wrong OTP')
308
- * }
309
- * ```
310
- */
241
+ validateTransferOptions(options) {
242
+ const errors = {};
243
+ if (!options.type || !["debit", "credit"].includes(options.type)) {
244
+ errors.type = ['Must be "debit" or "credit"'];
245
+ }
246
+ if (!options.amount) {
247
+ errors.amount = ["Amount is required"];
248
+ } else {
249
+ const amount = parseFloat(options.amount);
250
+ if (isNaN(amount)) {
251
+ errors.amount = ["Must be a valid number"];
252
+ } else if (amount <= 0) {
253
+ errors.amount = ["Must be greater than 0"];
254
+ } else if (!/^\d+(\.\d{1,2})?$/.test(options.amount)) {
255
+ errors.amount = ["Must have at most 2 decimal places"];
256
+ }
257
+ }
258
+ if (!options.idempotencyKey || options.idempotencyKey.trim() === "") {
259
+ errors.idempotencyKey = ["idempotencyKey is required"];
260
+ } else if (options.idempotencyKey.length > 255) {
261
+ errors.idempotencyKey = ["Must be 255 characters or less"];
262
+ }
263
+ if (Object.keys(errors).length > 0) {
264
+ throw new import_connect.EdgeValidationError("Invalid transfer options", errors);
265
+ }
266
+ }
311
267
  async verifyTransfer(accessToken, transferId, otp) {
312
268
  return this.apiRequest(
313
269
  "POST",
@@ -316,21 +272,6 @@ var EdgeConnectServer = class {
316
272
  { otp }
317
273
  );
318
274
  }
319
- /**
320
- * Gets the status of a transfer.
321
- *
322
- * Use for polling after initiating a transfer.
323
- *
324
- * @param accessToken - Valid access token
325
- * @param transferId - Transfer ID
326
- * @returns Current transfer status
327
- *
328
- * @example
329
- * ```typescript
330
- * const transfer = await edge.getTransfer(accessToken, transferId)
331
- * console.log(`Status: ${transfer.status}`)
332
- * ```
333
- */
334
275
  async getTransfer(accessToken, transferId) {
335
276
  return this.apiRequest(
336
277
  "GET",
@@ -338,27 +279,6 @@ var EdgeConnectServer = class {
338
279
  accessToken
339
280
  );
340
281
  }
341
- /**
342
- * Lists transfers for the connected user.
343
- *
344
- * Useful for reconciliation and showing transfer history.
345
- *
346
- * @param accessToken - Valid access token
347
- * @param params - Pagination and filter options
348
- * @returns Paginated list of transfers
349
- *
350
- * @example
351
- * ```typescript
352
- * // Get first page of completed transfers
353
- * const { transfers, total } = await edge.listTransfers(accessToken, {
354
- * status: 'completed',
355
- * limit: 10,
356
- * offset: 0,
357
- * })
358
- *
359
- * console.log(`Showing ${transfers.length} of ${total} transfers`)
360
- * ```
361
- */
362
282
  async listTransfers(accessToken, params) {
363
283
  const queryParams = new URLSearchParams();
364
284
  if (params?.limit) queryParams.set("limit", String(params.limit));
@@ -368,68 +288,93 @@ var EdgeConnectServer = class {
368
288
  const path = query ? `/transfers?${query}` : "/transfers";
369
289
  return this.apiRequest("GET", path, accessToken);
370
290
  }
371
- // ===========================================================================
372
- // CONSENT
373
- // ===========================================================================
374
- /**
375
- * Revokes the user's consent (disconnects their account).
376
- *
377
- * After revocation:
378
- * - All API calls will fail with `consent_required` error
379
- * - User must go through EdgeLink again to reconnect
380
- * - Stored tokens become invalid
381
- *
382
- * Use this for "Disconnect" or "Unlink" features in your app.
383
- *
384
- * @param accessToken - Valid access token
385
- * @returns Confirmation of revocation
386
- *
387
- * @example
388
- * ```typescript
389
- * // Disconnect user's EdgeBoost account
390
- * await edge.revokeConsent(accessToken)
391
- *
392
- * // Clean up stored tokens
393
- * await db.edgeConnections.delete(userId)
394
- *
395
- * console.log('EdgeBoost disconnected')
396
- * ```
397
- */
398
291
  async revokeConsent(accessToken) {
399
292
  return this.apiRequest("DELETE", "/consent", accessToken);
400
293
  }
401
- // ===========================================================================
402
- // PRIVATE HELPERS
403
- // ===========================================================================
404
- /**
405
- * Makes an authenticated API request.
406
- */
294
+ getRetryDelay(attempt) {
295
+ const { backoff, baseDelayMs } = this.retryConfig;
296
+ if (backoff === "exponential") {
297
+ return baseDelayMs * Math.pow(2, attempt);
298
+ }
299
+ return baseDelayMs * (attempt + 1);
300
+ }
301
+ async sleep(ms) {
302
+ return new Promise((resolve) => setTimeout(resolve, ms));
303
+ }
407
304
  async apiRequest(method, path, accessToken, body) {
408
305
  const url = `${this.apiBaseUrl}${path}`;
409
- try {
410
- const response = await this.fetchWithTimeout(url, {
411
- method,
412
- headers: {
306
+ let lastError = null;
307
+ const mleConfig = this.config.mle;
308
+ const mleEnabled = !!mleConfig?.enabled;
309
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
310
+ const startTime = Date.now();
311
+ if (attempt > 0) {
312
+ await this.sleep(this.getRetryDelay(attempt - 1));
313
+ }
314
+ this.config.onRequest?.({ method, url, body });
315
+ try {
316
+ const requestHeaders = {
413
317
  Authorization: `Bearer ${accessToken}`,
414
318
  "Content-Type": "application/json",
415
- "User-Agent": USER_AGENT,
416
- // For ngrok during development
417
- "ngrok-skip-browser-warning": "true"
418
- },
419
- body: body ? JSON.stringify(body) : void 0
420
- });
421
- if (!response.ok) {
422
- throw await this.handleApiError(response, path);
319
+ "User-Agent": USER_AGENT
320
+ };
321
+ let requestBody = body;
322
+ if (mleEnabled) {
323
+ requestHeaders["X-Edge-MLE"] = "v1";
324
+ if (body !== void 0) {
325
+ requestBody = { jwe: encryptMle(body, this.config.clientId, mleConfig) };
326
+ }
327
+ }
328
+ const response = await this.fetchWithTimeout(url, {
329
+ method,
330
+ headers: requestHeaders,
331
+ body: requestBody !== void 0 ? JSON.stringify(requestBody) : void 0
332
+ });
333
+ const rawResponseBody = await response.json().catch(() => ({}));
334
+ let responseBody = rawResponseBody;
335
+ if (mleEnabled && typeof rawResponseBody?.jwe === "string") {
336
+ responseBody = decryptMle(rawResponseBody.jwe, mleConfig);
337
+ } else if (!mleEnabled && typeof rawResponseBody?.jwe === "string") {
338
+ throw new import_connect.EdgeApiError(
339
+ "mle_required",
340
+ "The API responded with message-level encryption. Enable MLE in SDK config.",
341
+ response.status,
342
+ rawResponseBody
343
+ );
344
+ } else if (mleEnabled && mleConfig?.strictResponseEncryption !== false && response.ok && typeof rawResponseBody?.jwe !== "string") {
345
+ throw new import_connect.EdgeApiError(
346
+ "mle_response_missing",
347
+ "Expected encrypted response payload but received plaintext.",
348
+ response.status,
349
+ rawResponseBody
350
+ );
351
+ }
352
+ const durationMs = Date.now() - startTime;
353
+ this.config.onResponse?.({ status: response.status, body: responseBody, durationMs });
354
+ if (!response.ok) {
355
+ if (this.retryConfig.retryOn.includes(response.status) && attempt < this.retryConfig.maxRetries) {
356
+ lastError = await this.handleApiErrorFromBody(responseBody, response.status, path);
357
+ continue;
358
+ }
359
+ throw await this.handleApiErrorFromBody(responseBody, response.status, path);
360
+ }
361
+ return responseBody;
362
+ } catch (error) {
363
+ if (error instanceof import_connect.EdgeError) {
364
+ if (!(error instanceof import_connect.EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
365
+ throw error;
366
+ }
367
+ lastError = error;
368
+ continue;
369
+ }
370
+ lastError = error;
371
+ if (attempt >= this.retryConfig.maxRetries) {
372
+ throw new import_connect.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
373
+ }
423
374
  }
424
- return response.json();
425
- } catch (error) {
426
- if (error instanceof import_connect.EdgeError) throw error;
427
- throw new import_connect.EdgeNetworkError(`API request failed: ${method} ${path}`, error);
428
375
  }
376
+ throw new import_connect.EdgeNetworkError(`API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`, lastError);
429
377
  }
430
- /**
431
- * Fetch with timeout support.
432
- */
433
378
  async fetchWithTimeout(url, options) {
434
379
  const controller = new AbortController();
435
380
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
@@ -442,9 +387,6 @@ var EdgeConnectServer = class {
442
387
  clearTimeout(timeoutId);
443
388
  }
444
389
  }
445
- /**
446
- * Parses token response from Cognito.
447
- */
448
390
  parseTokenResponse(data, existingRefreshToken) {
449
391
  return {
450
392
  accessToken: data.access_token,
@@ -455,35 +397,33 @@ var EdgeConnectServer = class {
455
397
  scope: data.scope || ""
456
398
  };
457
399
  }
458
- /**
459
- * Handles token exchange errors.
460
- */
461
400
  handleTokenError(error, status) {
462
401
  const errorCode = error.error;
463
- const errorDescription = error.error_description;
464
- if (errorCode === "invalid_grant") {
402
+ const errorMessage = error.message || error.error_description;
403
+ if (errorCode === "invalid_grant" || errorMessage?.includes("Invalid or expired")) {
465
404
  return new import_connect.EdgeTokenExchangeError(
466
405
  "Authorization code is invalid, expired, or already used. Please try again.",
467
- { cognitoError: error }
406
+ { edgeBoostError: error }
468
407
  );
469
408
  }
470
- if (errorCode === "invalid_client") {
409
+ if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
471
410
  return new import_connect.EdgeAuthenticationError(
472
411
  "Invalid client credentials. Check your client ID and secret.",
473
- { cognitoError: error }
412
+ { edgeBoostError: error }
413
+ );
414
+ }
415
+ if (errorMessage?.includes("code_verifier") || errorMessage?.includes("PKCE")) {
416
+ return new import_connect.EdgeTokenExchangeError(
417
+ "PKCE verification failed. Please try again.",
418
+ { edgeBoostError: error }
474
419
  );
475
420
  }
476
421
  return new import_connect.EdgeTokenExchangeError(
477
- errorDescription || "Failed to exchange authorization code",
478
- { cognitoError: error, statusCode: status }
422
+ errorMessage || "Failed to exchange authorization code",
423
+ { edgeBoostError: error, statusCode: status }
479
424
  );
480
425
  }
481
- /**
482
- * Handles API errors.
483
- */
484
- async handleApiError(response, path) {
485
- const error = await response.json().catch(() => ({}));
486
- const status = response.status;
426
+ async handleApiErrorFromBody(error, status, path) {
487
427
  if (status === 401) {
488
428
  return new import_connect.EdgeAuthenticationError(
489
429
  error.message || "Access token is invalid or expired",