@edge-markets/connect-node 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/dist/index.js ADDED
@@ -0,0 +1,543 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ EdgeApiError: () => import_connect2.EdgeApiError,
24
+ EdgeAuthenticationError: () => import_connect2.EdgeAuthenticationError,
25
+ EdgeConnectServer: () => EdgeConnectServer,
26
+ EdgeConsentRequiredError: () => import_connect2.EdgeConsentRequiredError,
27
+ EdgeError: () => import_connect2.EdgeError,
28
+ EdgeInsufficientScopeError: () => import_connect2.EdgeInsufficientScopeError,
29
+ EdgeNetworkError: () => import_connect2.EdgeNetworkError,
30
+ EdgeNotFoundError: () => import_connect2.EdgeNotFoundError,
31
+ EdgeTokenExchangeError: () => import_connect2.EdgeTokenExchangeError,
32
+ getEnvironmentConfig: () => import_connect3.getEnvironmentConfig,
33
+ isApiError: () => import_connect2.isApiError,
34
+ isAuthenticationError: () => import_connect2.isAuthenticationError,
35
+ isConsentRequiredError: () => import_connect2.isConsentRequiredError,
36
+ isEdgeError: () => import_connect2.isEdgeError,
37
+ isNetworkError: () => import_connect2.isNetworkError,
38
+ isProductionEnvironment: () => import_connect3.isProductionEnvironment
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/edge-connect-server.ts
43
+ var import_connect = require("@edge-markets/connect");
44
+ var DEFAULT_TIMEOUT = 3e4;
45
+ 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
+ */
53
+ constructor(config) {
54
+ if (!config.clientId) {
55
+ throw new Error("EdgeConnectServer: clientId is required");
56
+ }
57
+ if (!config.clientSecret) {
58
+ throw new Error("EdgeConnectServer: clientSecret is required");
59
+ }
60
+ if (!config.environment) {
61
+ throw new Error("EdgeConnectServer: environment is required");
62
+ }
63
+ this.config = config;
64
+ const envConfig = (0, import_connect.getEnvironmentConfig)(config.environment);
65
+ this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
66
+ this.oauthBaseUrl = envConfig.oauthBaseUrl;
67
+ this.timeout = config.timeout || DEFAULT_TIMEOUT;
68
+ }
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`;
111
+ const body = {
112
+ grant_type: "authorization_code",
113
+ code,
114
+ code_verifier: codeVerifier,
115
+ client_id: this.config.clientId,
116
+ client_secret: this.config.clientSecret
117
+ };
118
+ try {
119
+ const response = await this.fetchWithTimeout(tokenUrl, {
120
+ method: "POST",
121
+ headers: {
122
+ "Content-Type": "application/json",
123
+ "User-Agent": USER_AGENT,
124
+ // Required for ngrok tunnels in local development
125
+ "ngrok-skip-browser-warning": "true"
126
+ },
127
+ body: JSON.stringify(body)
128
+ });
129
+ if (!response.ok) {
130
+ const error = await response.json().catch(() => ({}));
131
+ throw this.handleTokenError(error, response.status);
132
+ }
133
+ const data = await response.json();
134
+ return this.parseTokenResponse(data);
135
+ } catch (error) {
136
+ if (error instanceof import_connect.EdgeError) throw error;
137
+ throw new import_connect.EdgeNetworkError("Failed to exchange code", error);
138
+ }
139
+ }
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
+ async refreshTokens(refreshToken) {
177
+ const tokenUrl = `${this.oauthBaseUrl}/oauth/token`;
178
+ const body = {
179
+ grant_type: "refresh_token",
180
+ refresh_token: refreshToken,
181
+ client_id: this.config.clientId,
182
+ client_secret: this.config.clientSecret
183
+ };
184
+ try {
185
+ const response = await this.fetchWithTimeout(tokenUrl, {
186
+ method: "POST",
187
+ headers: {
188
+ "Content-Type": "application/json",
189
+ "User-Agent": USER_AGENT,
190
+ "ngrok-skip-browser-warning": "true"
191
+ },
192
+ body: JSON.stringify(body)
193
+ });
194
+ if (!response.ok) {
195
+ const error = await response.json().catch(() => ({}));
196
+ throw new import_connect.EdgeAuthenticationError(
197
+ error.error_description || "Token refresh failed. User may need to reconnect.",
198
+ { tokenError: error }
199
+ );
200
+ }
201
+ const data = await response.json();
202
+ return this.parseTokenResponse(data, refreshToken);
203
+ } catch (error) {
204
+ if (error instanceof import_connect.EdgeError) throw error;
205
+ throw new import_connect.EdgeNetworkError("Failed to refresh tokens", error);
206
+ }
207
+ }
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
+ async getUser(accessToken) {
226
+ return this.apiRequest("GET", "/user", accessToken);
227
+ }
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
+ async getBalance(accessToken) {
243
+ return this.apiRequest("GET", "/balance", accessToken);
244
+ }
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
+ async initiateTransfer(accessToken, options) {
282
+ const body = {
283
+ type: options.type,
284
+ amount: options.amount,
285
+ idempotencyKey: options.idempotencyKey
286
+ };
287
+ return this.apiRequest("POST", "/transfer", accessToken, body);
288
+ }
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
+ */
311
+ async verifyTransfer(accessToken, transferId, otp) {
312
+ return this.apiRequest(
313
+ "POST",
314
+ `/transfer/${encodeURIComponent(transferId)}/verify`,
315
+ accessToken,
316
+ { otp }
317
+ );
318
+ }
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
+ async getTransfer(accessToken, transferId) {
335
+ return this.apiRequest(
336
+ "GET",
337
+ `/transfer/${encodeURIComponent(transferId)}`,
338
+ accessToken
339
+ );
340
+ }
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
+ async listTransfers(accessToken, params) {
363
+ const queryParams = new URLSearchParams();
364
+ if (params?.limit) queryParams.set("limit", String(params.limit));
365
+ if (params?.offset) queryParams.set("offset", String(params.offset));
366
+ if (params?.status) queryParams.set("status", params.status);
367
+ const query = queryParams.toString();
368
+ const path = query ? `/transfers?${query}` : "/transfers";
369
+ return this.apiRequest("GET", path, accessToken);
370
+ }
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
+ async revokeConsent(accessToken) {
399
+ return this.apiRequest("DELETE", "/consent", accessToken);
400
+ }
401
+ // ===========================================================================
402
+ // PRIVATE HELPERS
403
+ // ===========================================================================
404
+ /**
405
+ * Makes an authenticated API request.
406
+ */
407
+ async apiRequest(method, path, accessToken, body) {
408
+ const url = `${this.apiBaseUrl}${path}`;
409
+ try {
410
+ const response = await this.fetchWithTimeout(url, {
411
+ method,
412
+ headers: {
413
+ Authorization: `Bearer ${accessToken}`,
414
+ "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);
423
+ }
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
+ }
429
+ }
430
+ /**
431
+ * Fetch with timeout support.
432
+ */
433
+ async fetchWithTimeout(url, options) {
434
+ const controller = new AbortController();
435
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
436
+ try {
437
+ return await fetch(url, {
438
+ ...options,
439
+ signal: controller.signal
440
+ });
441
+ } finally {
442
+ clearTimeout(timeoutId);
443
+ }
444
+ }
445
+ /**
446
+ * Parses token response from Cognito.
447
+ */
448
+ parseTokenResponse(data, existingRefreshToken) {
449
+ return {
450
+ accessToken: data.access_token,
451
+ refreshToken: data.refresh_token || existingRefreshToken || "",
452
+ idToken: data.id_token,
453
+ expiresIn: data.expires_in,
454
+ expiresAt: Date.now() + data.expires_in * 1e3,
455
+ scope: data.scope || ""
456
+ };
457
+ }
458
+ /**
459
+ * Handles token exchange errors.
460
+ */
461
+ handleTokenError(error, status) {
462
+ const errorCode = error.error;
463
+ const errorDescription = error.error_description;
464
+ if (errorCode === "invalid_grant") {
465
+ return new import_connect.EdgeTokenExchangeError(
466
+ "Authorization code is invalid, expired, or already used. Please try again.",
467
+ { cognitoError: error }
468
+ );
469
+ }
470
+ if (errorCode === "invalid_client") {
471
+ return new import_connect.EdgeAuthenticationError(
472
+ "Invalid client credentials. Check your client ID and secret.",
473
+ { cognitoError: error }
474
+ );
475
+ }
476
+ return new import_connect.EdgeTokenExchangeError(
477
+ errorDescription || "Failed to exchange authorization code",
478
+ { cognitoError: error, statusCode: status }
479
+ );
480
+ }
481
+ /**
482
+ * Handles API errors.
483
+ */
484
+ async handleApiError(response, path) {
485
+ const error = await response.json().catch(() => ({}));
486
+ const status = response.status;
487
+ if (status === 401) {
488
+ return new import_connect.EdgeAuthenticationError(
489
+ error.message || "Access token is invalid or expired",
490
+ error
491
+ );
492
+ }
493
+ if (status === 403) {
494
+ if (error.error === "consent_required") {
495
+ return new import_connect.EdgeConsentRequiredError(
496
+ this.config.clientId,
497
+ error.consentUrl,
498
+ error.message
499
+ );
500
+ }
501
+ if (error.error === "insufficient_scope" || error.error === "insufficient_consent") {
502
+ return new import_connect.EdgeInsufficientScopeError(
503
+ error.missing_scopes || error.missingScopes || [],
504
+ error.message
505
+ );
506
+ }
507
+ }
508
+ if (status === 404) {
509
+ const resourceType = path.includes("/transfer") ? "Transfer" : "Resource";
510
+ const resourceId = path.split("/").pop() || "unknown";
511
+ return new import_connect.EdgeNotFoundError(resourceType, resourceId);
512
+ }
513
+ return new import_connect.EdgeApiError(
514
+ error.error || "api_error",
515
+ error.message || error.error_description || `Request failed with status ${status}`,
516
+ status,
517
+ error
518
+ );
519
+ }
520
+ };
521
+
522
+ // src/index.ts
523
+ var import_connect2 = require("@edge-markets/connect");
524
+ var import_connect3 = require("@edge-markets/connect");
525
+ // Annotate the CommonJS export names for ESM import in node:
526
+ 0 && (module.exports = {
527
+ EdgeApiError,
528
+ EdgeAuthenticationError,
529
+ EdgeConnectServer,
530
+ EdgeConsentRequiredError,
531
+ EdgeError,
532
+ EdgeInsufficientScopeError,
533
+ EdgeNetworkError,
534
+ EdgeNotFoundError,
535
+ EdgeTokenExchangeError,
536
+ getEnvironmentConfig,
537
+ isApiError,
538
+ isAuthenticationError,
539
+ isConsentRequiredError,
540
+ isEdgeError,
541
+ isNetworkError,
542
+ isProductionEnvironment
543
+ });