@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.mjs CHANGED
@@ -8,17 +8,110 @@ import {
8
8
  EdgeInsufficientScopeError,
9
9
  EdgeApiError,
10
10
  EdgeNotFoundError,
11
- EdgeNetworkError
11
+ EdgeNetworkError,
12
+ EdgeValidationError
12
13
  } from "@edge-markets/connect";
14
+
15
+ // src/mle.ts
16
+ import { randomUUID, randomBytes, createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from "crypto";
17
+ function encryptMle(payload, clientId, config) {
18
+ const header = {
19
+ alg: "RSA-OAEP-256",
20
+ enc: "A256GCM",
21
+ kid: config.edgeKeyId,
22
+ typ: "application/json",
23
+ iat: Math.floor(Date.now() / 1e3),
24
+ jti: randomUUID(),
25
+ clientId
26
+ };
27
+ const protectedHeaderB64 = toBase64Url(Buffer.from(JSON.stringify(header), "utf8"));
28
+ const aad = Buffer.from(protectedHeaderB64, "utf8");
29
+ const cek = randomBytes(32);
30
+ const iv = randomBytes(12);
31
+ const cipher = createCipheriv("aes-256-gcm", cek, iv);
32
+ cipher.setAAD(aad);
33
+ const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
34
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
35
+ const tag = cipher.getAuthTag();
36
+ const encryptedKey = publicEncrypt(
37
+ {
38
+ key: config.edgePublicKey,
39
+ padding: constants.RSA_PKCS1_OAEP_PADDING,
40
+ oaepHash: "sha256"
41
+ },
42
+ cek
43
+ );
44
+ return [
45
+ protectedHeaderB64,
46
+ toBase64Url(encryptedKey),
47
+ toBase64Url(iv),
48
+ toBase64Url(ciphertext),
49
+ toBase64Url(tag)
50
+ ].join(".");
51
+ }
52
+ function decryptMle(jwe, config) {
53
+ const parts = jwe.split(".");
54
+ if (parts.length !== 5) {
55
+ throw new Error("Invalid MLE payload format");
56
+ }
57
+ const [protectedHeaderB64, encryptedKeyB64, ivB64, ciphertextB64, tagB64] = parts;
58
+ const protectedHeader = JSON.parse(fromBase64Url(protectedHeaderB64).toString("utf8"));
59
+ if (protectedHeader.alg !== "RSA-OAEP-256" || protectedHeader.enc !== "A256GCM") {
60
+ throw new Error("Unsupported MLE algorithms");
61
+ }
62
+ if (protectedHeader.kid && protectedHeader.kid !== config.partnerKeyId) {
63
+ throw new Error(`Unexpected response key id: ${protectedHeader.kid}`);
64
+ }
65
+ const cek = privateDecrypt(
66
+ {
67
+ key: config.partnerPrivateKey,
68
+ padding: constants.RSA_PKCS1_OAEP_PADDING,
69
+ oaepHash: "sha256"
70
+ },
71
+ fromBase64Url(encryptedKeyB64)
72
+ );
73
+ const decipher = createDecipheriv("aes-256-gcm", cek, fromBase64Url(ivB64));
74
+ decipher.setAAD(Buffer.from(protectedHeaderB64, "utf8"));
75
+ decipher.setAuthTag(fromBase64Url(tagB64));
76
+ const plaintext = Buffer.concat([
77
+ decipher.update(fromBase64Url(ciphertextB64)),
78
+ decipher.final()
79
+ ]);
80
+ return JSON.parse(plaintext.toString("utf8"));
81
+ }
82
+ function toBase64Url(buffer) {
83
+ return buffer.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
84
+ }
85
+ function fromBase64Url(value) {
86
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
87
+ return Buffer.from(normalized, "base64");
88
+ }
89
+
90
+ // src/edge-connect-server.ts
13
91
  var DEFAULT_TIMEOUT = 3e4;
14
92
  var USER_AGENT = "@edge-markets/connect-node/1.0.0";
15
- var EdgeConnectServer = class {
16
- /**
17
- * Creates a new EdgeConnectServer instance.
18
- *
19
- * @param config - Server configuration
20
- * @throws Error if required config is missing
21
- */
93
+ var DEFAULT_RETRY_CONFIG = {
94
+ maxRetries: 3,
95
+ retryOn: [408, 429, 500, 502, 503, 504],
96
+ backoff: "exponential",
97
+ baseDelayMs: 1e3
98
+ };
99
+ var instances = /* @__PURE__ */ new Map();
100
+ function getInstanceKey(config) {
101
+ return `${config.clientId}:${config.environment}`;
102
+ }
103
+ var EdgeConnectServer = class _EdgeConnectServer {
104
+ static getInstance(config) {
105
+ const key = getInstanceKey(config);
106
+ const existing = instances.get(key);
107
+ if (existing) return existing;
108
+ const instance = new _EdgeConnectServer(config);
109
+ instances.set(key, instance);
110
+ return instance;
111
+ }
112
+ static clearInstances() {
113
+ instances.clear();
114
+ }
22
115
  constructor(config) {
23
116
  if (!config.clientId) {
24
117
  throw new Error("EdgeConnectServer: clientId is required");
@@ -32,66 +125,29 @@ var EdgeConnectServer = class {
32
125
  this.config = config;
33
126
  const envConfig = getEnvironmentConfig(config.environment);
34
127
  this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
35
- this.oauthBaseUrl = envConfig.oauthBaseUrl;
128
+ this.oauthBaseUrl = config.oauthBaseUrl || envConfig.oauthBaseUrl;
36
129
  this.timeout = config.timeout || DEFAULT_TIMEOUT;
130
+ this.retryConfig = {
131
+ ...DEFAULT_RETRY_CONFIG,
132
+ ...config.retry
133
+ };
37
134
  }
38
- // ===========================================================================
39
- // TOKEN OPERATIONS
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`;
135
+ async exchangeCode(code, codeVerifier, redirectUri) {
136
+ const tokenUrl = `${this.oauthBaseUrl}/token`;
80
137
  const body = {
81
138
  grant_type: "authorization_code",
82
139
  code,
83
140
  code_verifier: codeVerifier,
84
141
  client_id: this.config.clientId,
85
- client_secret: this.config.clientSecret
142
+ client_secret: this.config.clientSecret,
143
+ redirect_uri: redirectUri || this.config.redirectUri || ""
86
144
  };
87
145
  try {
88
146
  const response = await this.fetchWithTimeout(tokenUrl, {
89
147
  method: "POST",
90
148
  headers: {
91
149
  "Content-Type": "application/json",
92
- "User-Agent": USER_AGENT,
93
- // Required for ngrok tunnels in local development
94
- "ngrok-skip-browser-warning": "true"
150
+ "User-Agent": USER_AGENT
95
151
  },
96
152
  body: JSON.stringify(body)
97
153
  });
@@ -106,44 +162,8 @@ var EdgeConnectServer = class {
106
162
  throw new EdgeNetworkError("Failed to exchange code", error);
107
163
  }
108
164
  }
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
165
  async refreshTokens(refreshToken) {
146
- const tokenUrl = `${this.oauthBaseUrl}/oauth/token`;
166
+ const tokenUrl = `${this.oauthBaseUrl}/token`;
147
167
  const body = {
148
168
  grant_type: "refresh_token",
149
169
  refresh_token: refreshToken,
@@ -155,15 +175,14 @@ var EdgeConnectServer = class {
155
175
  method: "POST",
156
176
  headers: {
157
177
  "Content-Type": "application/json",
158
- "User-Agent": USER_AGENT,
159
- "ngrok-skip-browser-warning": "true"
178
+ "User-Agent": USER_AGENT
160
179
  },
161
180
  body: JSON.stringify(body)
162
181
  });
163
182
  if (!response.ok) {
164
183
  const error = await response.json().catch(() => ({}));
165
184
  throw new EdgeAuthenticationError(
166
- error.error_description || "Token refresh failed. User may need to reconnect.",
185
+ error.message || error.error_description || "Token refresh failed. User may need to reconnect.",
167
186
  { tokenError: error }
168
187
  );
169
188
  }
@@ -174,80 +193,14 @@ var EdgeConnectServer = class {
174
193
  throw new EdgeNetworkError("Failed to refresh tokens", error);
175
194
  }
176
195
  }
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
196
  async getUser(accessToken) {
195
197
  return this.apiRequest("GET", "/user", accessToken);
196
198
  }
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
199
  async getBalance(accessToken) {
212
200
  return this.apiRequest("GET", "/balance", accessToken);
213
201
  }
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
202
  async initiateTransfer(accessToken, options) {
203
+ this.validateTransferOptions(options);
251
204
  const body = {
252
205
  type: options.type,
253
206
  amount: options.amount,
@@ -255,28 +208,32 @@ var EdgeConnectServer = class {
255
208
  };
256
209
  return this.apiRequest("POST", "/transfer", accessToken, body);
257
210
  }
258
- /**
259
- * Verifies a pending transfer with OTP.
260
- *
261
- * Call this after the user enters the OTP code they received.
262
- * The OTP is valid for ~5 minutes.
263
- *
264
- * @param accessToken - Valid access token
265
- * @param transferId - Transfer ID from initiateTransfer
266
- * @param otp - 6-digit OTP code from user
267
- * @returns Updated transfer (status will be 'completed' or 'failed')
268
- *
269
- * @example
270
- * ```typescript
271
- * const result = await edge.verifyTransfer(accessToken, transferId, userOtp)
272
- *
273
- * if (result.status === 'completed') {
274
- * console.log('Transfer successful!')
275
- * } else if (result.status === 'failed') {
276
- * console.log('Transfer failed - possibly wrong OTP')
277
- * }
278
- * ```
279
- */
211
+ validateTransferOptions(options) {
212
+ const errors = {};
213
+ if (!options.type || !["debit", "credit"].includes(options.type)) {
214
+ errors.type = ['Must be "debit" or "credit"'];
215
+ }
216
+ if (!options.amount) {
217
+ errors.amount = ["Amount is required"];
218
+ } else {
219
+ const amount = parseFloat(options.amount);
220
+ if (isNaN(amount)) {
221
+ errors.amount = ["Must be a valid number"];
222
+ } else if (amount <= 0) {
223
+ errors.amount = ["Must be greater than 0"];
224
+ } else if (!/^\d+(\.\d{1,2})?$/.test(options.amount)) {
225
+ errors.amount = ["Must have at most 2 decimal places"];
226
+ }
227
+ }
228
+ if (!options.idempotencyKey || options.idempotencyKey.trim() === "") {
229
+ errors.idempotencyKey = ["idempotencyKey is required"];
230
+ } else if (options.idempotencyKey.length > 255) {
231
+ errors.idempotencyKey = ["Must be 255 characters or less"];
232
+ }
233
+ if (Object.keys(errors).length > 0) {
234
+ throw new EdgeValidationError("Invalid transfer options", errors);
235
+ }
236
+ }
280
237
  async verifyTransfer(accessToken, transferId, otp) {
281
238
  return this.apiRequest(
282
239
  "POST",
@@ -285,21 +242,6 @@ var EdgeConnectServer = class {
285
242
  { otp }
286
243
  );
287
244
  }
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
245
  async getTransfer(accessToken, transferId) {
304
246
  return this.apiRequest(
305
247
  "GET",
@@ -307,27 +249,6 @@ var EdgeConnectServer = class {
307
249
  accessToken
308
250
  );
309
251
  }
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
252
  async listTransfers(accessToken, params) {
332
253
  const queryParams = new URLSearchParams();
333
254
  if (params?.limit) queryParams.set("limit", String(params.limit));
@@ -337,68 +258,93 @@ var EdgeConnectServer = class {
337
258
  const path = query ? `/transfers?${query}` : "/transfers";
338
259
  return this.apiRequest("GET", path, accessToken);
339
260
  }
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
261
  async revokeConsent(accessToken) {
368
262
  return this.apiRequest("DELETE", "/consent", accessToken);
369
263
  }
370
- // ===========================================================================
371
- // PRIVATE HELPERS
372
- // ===========================================================================
373
- /**
374
- * Makes an authenticated API request.
375
- */
264
+ getRetryDelay(attempt) {
265
+ const { backoff, baseDelayMs } = this.retryConfig;
266
+ if (backoff === "exponential") {
267
+ return baseDelayMs * Math.pow(2, attempt);
268
+ }
269
+ return baseDelayMs * (attempt + 1);
270
+ }
271
+ async sleep(ms) {
272
+ return new Promise((resolve) => setTimeout(resolve, ms));
273
+ }
376
274
  async apiRequest(method, path, accessToken, body) {
377
275
  const url = `${this.apiBaseUrl}${path}`;
378
- try {
379
- const response = await this.fetchWithTimeout(url, {
380
- method,
381
- headers: {
276
+ let lastError = null;
277
+ const mleConfig = this.config.mle;
278
+ const mleEnabled = !!mleConfig?.enabled;
279
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
280
+ const startTime = Date.now();
281
+ if (attempt > 0) {
282
+ await this.sleep(this.getRetryDelay(attempt - 1));
283
+ }
284
+ this.config.onRequest?.({ method, url, body });
285
+ try {
286
+ const requestHeaders = {
382
287
  Authorization: `Bearer ${accessToken}`,
383
288
  "Content-Type": "application/json",
384
- "User-Agent": USER_AGENT,
385
- // For ngrok during development
386
- "ngrok-skip-browser-warning": "true"
387
- },
388
- body: body ? JSON.stringify(body) : void 0
389
- });
390
- if (!response.ok) {
391
- throw await this.handleApiError(response, path);
289
+ "User-Agent": USER_AGENT
290
+ };
291
+ let requestBody = body;
292
+ if (mleEnabled) {
293
+ requestHeaders["X-Edge-MLE"] = "v1";
294
+ if (body !== void 0) {
295
+ requestBody = { jwe: encryptMle(body, this.config.clientId, mleConfig) };
296
+ }
297
+ }
298
+ const response = await this.fetchWithTimeout(url, {
299
+ method,
300
+ headers: requestHeaders,
301
+ body: requestBody !== void 0 ? JSON.stringify(requestBody) : void 0
302
+ });
303
+ const rawResponseBody = await response.json().catch(() => ({}));
304
+ let responseBody = rawResponseBody;
305
+ if (mleEnabled && typeof rawResponseBody?.jwe === "string") {
306
+ responseBody = decryptMle(rawResponseBody.jwe, mleConfig);
307
+ } else if (!mleEnabled && typeof rawResponseBody?.jwe === "string") {
308
+ throw new EdgeApiError(
309
+ "mle_required",
310
+ "The API responded with message-level encryption. Enable MLE in SDK config.",
311
+ response.status,
312
+ rawResponseBody
313
+ );
314
+ } else if (mleEnabled && mleConfig?.strictResponseEncryption !== false && response.ok && typeof rawResponseBody?.jwe !== "string") {
315
+ throw new EdgeApiError(
316
+ "mle_response_missing",
317
+ "Expected encrypted response payload but received plaintext.",
318
+ response.status,
319
+ rawResponseBody
320
+ );
321
+ }
322
+ const durationMs = Date.now() - startTime;
323
+ this.config.onResponse?.({ status: response.status, body: responseBody, durationMs });
324
+ if (!response.ok) {
325
+ if (this.retryConfig.retryOn.includes(response.status) && attempt < this.retryConfig.maxRetries) {
326
+ lastError = await this.handleApiErrorFromBody(responseBody, response.status, path);
327
+ continue;
328
+ }
329
+ throw await this.handleApiErrorFromBody(responseBody, response.status, path);
330
+ }
331
+ return responseBody;
332
+ } catch (error) {
333
+ if (error instanceof EdgeError) {
334
+ if (!(error instanceof EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
335
+ throw error;
336
+ }
337
+ lastError = error;
338
+ continue;
339
+ }
340
+ lastError = error;
341
+ if (attempt >= this.retryConfig.maxRetries) {
342
+ throw new EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
343
+ }
392
344
  }
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
345
  }
346
+ throw new EdgeNetworkError(`API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`, lastError);
398
347
  }
399
- /**
400
- * Fetch with timeout support.
401
- */
402
348
  async fetchWithTimeout(url, options) {
403
349
  const controller = new AbortController();
404
350
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
@@ -411,9 +357,6 @@ var EdgeConnectServer = class {
411
357
  clearTimeout(timeoutId);
412
358
  }
413
359
  }
414
- /**
415
- * Parses token response from Cognito.
416
- */
417
360
  parseTokenResponse(data, existingRefreshToken) {
418
361
  return {
419
362
  accessToken: data.access_token,
@@ -424,35 +367,33 @@ var EdgeConnectServer = class {
424
367
  scope: data.scope || ""
425
368
  };
426
369
  }
427
- /**
428
- * Handles token exchange errors.
429
- */
430
370
  handleTokenError(error, status) {
431
371
  const errorCode = error.error;
432
- const errorDescription = error.error_description;
433
- if (errorCode === "invalid_grant") {
372
+ const errorMessage = error.message || error.error_description;
373
+ if (errorCode === "invalid_grant" || errorMessage?.includes("Invalid or expired")) {
434
374
  return new EdgeTokenExchangeError(
435
375
  "Authorization code is invalid, expired, or already used. Please try again.",
436
- { cognitoError: error }
376
+ { edgeBoostError: error }
437
377
  );
438
378
  }
439
- if (errorCode === "invalid_client") {
379
+ if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
440
380
  return new EdgeAuthenticationError(
441
381
  "Invalid client credentials. Check your client ID and secret.",
442
- { cognitoError: error }
382
+ { edgeBoostError: error }
383
+ );
384
+ }
385
+ if (errorMessage?.includes("code_verifier") || errorMessage?.includes("PKCE")) {
386
+ return new EdgeTokenExchangeError(
387
+ "PKCE verification failed. Please try again.",
388
+ { edgeBoostError: error }
443
389
  );
444
390
  }
445
391
  return new EdgeTokenExchangeError(
446
- errorDescription || "Failed to exchange authorization code",
447
- { cognitoError: error, statusCode: status }
392
+ errorMessage || "Failed to exchange authorization code",
393
+ { edgeBoostError: error, statusCode: status }
448
394
  );
449
395
  }
450
- /**
451
- * Handles API errors.
452
- */
453
- async handleApiError(response, path) {
454
- const error = await response.json().catch(() => ({}));
455
- const status = response.status;
396
+ async handleApiErrorFromBody(error, status, path) {
456
397
  if (status === 401) {
457
398
  return new EdgeAuthenticationError(
458
399
  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",
3
+ "version": "1.3.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.0.0"
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
+ }