@edge-markets/connect-node 1.3.0 → 1.4.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.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { EdgeEnvironment, EdgeTokens, User, Balance, Transfer, ListTransfersParams, TransferList } from '@edge-markets/connect';
1
+ import { EdgeEnvironment, User, VerifyIdentityOptions, VerifyIdentityResult, Balance, Transfer, ListTransfersParams, TransferList, EdgeTokens } from '@edge-markets/connect';
2
2
  export { Balance, EdgeApiError, EdgeAuthenticationError, EdgeConsentRequiredError, EdgeEnvironment, EdgeError, EdgeInsufficientScopeError, EdgeNetworkError, EdgeNotFoundError, EdgeTokenExchangeError, EdgeTokens, ListTransfersParams, Transfer, TransferList, TransferListItem, TransferStatus, TransferType, User, getEnvironmentConfig, isApiError, isAuthenticationError, isConsentRequiredError, isEdgeError, isNetworkError, isProductionEnvironment } from '@edge-markets/connect';
3
3
 
4
4
  interface RequestInfo {
@@ -46,6 +46,29 @@ interface TransferOptions {
46
46
  amount: string;
47
47
  idempotencyKey: string;
48
48
  }
49
+
50
+ /**
51
+ * A lightweight, user-scoped API client created via {@link EdgeConnectServer.forUser}.
52
+ *
53
+ * Each instance holds a single access token and delegates the actual HTTP work
54
+ * to its parent {@link EdgeConnectServer}.
55
+ */
56
+ declare class EdgeUserClient {
57
+ private readonly server;
58
+ readonly accessToken: string;
59
+ constructor(server: EdgeConnectServer, accessToken: string);
60
+ getUser(): Promise<User>;
61
+ verifyIdentity(options: VerifyIdentityOptions): Promise<VerifyIdentityResult>;
62
+ getBalance(): Promise<Balance>;
63
+ initiateTransfer(options: TransferOptions): Promise<Transfer>;
64
+ verifyTransfer(transferId: string, otp: string): Promise<Transfer>;
65
+ getTransfer(transferId: string): Promise<Transfer>;
66
+ listTransfers(params?: ListTransfersParams): Promise<TransferList>;
67
+ revokeConsent(): Promise<{
68
+ revoked: boolean;
69
+ }>;
70
+ }
71
+
49
72
  declare class EdgeConnectServer {
50
73
  private readonly config;
51
74
  private readonly apiBaseUrl;
@@ -55,25 +78,23 @@ declare class EdgeConnectServer {
55
78
  static getInstance(config: EdgeConnectServerConfig): EdgeConnectServer;
56
79
  static clearInstances(): void;
57
80
  constructor(config: EdgeConnectServerConfig);
81
+ /**
82
+ * Create a user-scoped client for making authenticated API calls.
83
+ *
84
+ * The returned {@link EdgeUserClient} is lightweight — create one per request
85
+ * or per user session and discard it when the token changes.
86
+ */
87
+ forUser(accessToken: string): EdgeUserClient;
58
88
  exchangeCode(code: string, codeVerifier: string, redirectUri?: string): Promise<EdgeTokens>;
59
89
  refreshTokens(refreshToken: string): Promise<EdgeTokens>;
60
- getUser(accessToken: string): Promise<User>;
61
- getBalance(accessToken: string): Promise<Balance>;
62
- initiateTransfer(accessToken: string, options: TransferOptions): Promise<Transfer>;
63
- private validateTransferOptions;
64
- verifyTransfer(accessToken: string, transferId: string, otp: string): Promise<Transfer>;
65
- getTransfer(accessToken: string, transferId: string): Promise<Transfer>;
66
- listTransfers(accessToken: string, params?: ListTransfersParams): Promise<TransferList>;
67
- revokeConsent(accessToken: string): Promise<{
68
- revoked: boolean;
69
- }>;
90
+ /** @internal Called by {@link EdgeUserClient} — not part of the public API. */
91
+ _apiRequest<T>(method: string, path: string, accessToken: string, body?: unknown): Promise<T>;
70
92
  private getRetryDelay;
71
93
  private sleep;
72
- private apiRequest;
73
94
  private fetchWithTimeout;
74
95
  private parseTokenResponse;
75
96
  private handleTokenError;
76
97
  private handleApiErrorFromBody;
77
98
  }
78
99
 
79
- export { EdgeConnectServer, type EdgeConnectServerConfig, type RequestInfo, type ResponseInfo, type RetryConfig, type TransferOptions };
100
+ export { EdgeConnectServer, type EdgeConnectServerConfig, EdgeUserClient, type RequestInfo, type ResponseInfo, type RetryConfig, type TransferOptions };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EdgeEnvironment, EdgeTokens, User, Balance, Transfer, ListTransfersParams, TransferList } from '@edge-markets/connect';
1
+ import { EdgeEnvironment, User, VerifyIdentityOptions, VerifyIdentityResult, Balance, Transfer, ListTransfersParams, TransferList, EdgeTokens } from '@edge-markets/connect';
2
2
  export { Balance, EdgeApiError, EdgeAuthenticationError, EdgeConsentRequiredError, EdgeEnvironment, EdgeError, EdgeInsufficientScopeError, EdgeNetworkError, EdgeNotFoundError, EdgeTokenExchangeError, EdgeTokens, ListTransfersParams, Transfer, TransferList, TransferListItem, TransferStatus, TransferType, User, getEnvironmentConfig, isApiError, isAuthenticationError, isConsentRequiredError, isEdgeError, isNetworkError, isProductionEnvironment } from '@edge-markets/connect';
3
3
 
4
4
  interface RequestInfo {
@@ -46,6 +46,29 @@ interface TransferOptions {
46
46
  amount: string;
47
47
  idempotencyKey: string;
48
48
  }
49
+
50
+ /**
51
+ * A lightweight, user-scoped API client created via {@link EdgeConnectServer.forUser}.
52
+ *
53
+ * Each instance holds a single access token and delegates the actual HTTP work
54
+ * to its parent {@link EdgeConnectServer}.
55
+ */
56
+ declare class EdgeUserClient {
57
+ private readonly server;
58
+ readonly accessToken: string;
59
+ constructor(server: EdgeConnectServer, accessToken: string);
60
+ getUser(): Promise<User>;
61
+ verifyIdentity(options: VerifyIdentityOptions): Promise<VerifyIdentityResult>;
62
+ getBalance(): Promise<Balance>;
63
+ initiateTransfer(options: TransferOptions): Promise<Transfer>;
64
+ verifyTransfer(transferId: string, otp: string): Promise<Transfer>;
65
+ getTransfer(transferId: string): Promise<Transfer>;
66
+ listTransfers(params?: ListTransfersParams): Promise<TransferList>;
67
+ revokeConsent(): Promise<{
68
+ revoked: boolean;
69
+ }>;
70
+ }
71
+
49
72
  declare class EdgeConnectServer {
50
73
  private readonly config;
51
74
  private readonly apiBaseUrl;
@@ -55,25 +78,23 @@ declare class EdgeConnectServer {
55
78
  static getInstance(config: EdgeConnectServerConfig): EdgeConnectServer;
56
79
  static clearInstances(): void;
57
80
  constructor(config: EdgeConnectServerConfig);
81
+ /**
82
+ * Create a user-scoped client for making authenticated API calls.
83
+ *
84
+ * The returned {@link EdgeUserClient} is lightweight — create one per request
85
+ * or per user session and discard it when the token changes.
86
+ */
87
+ forUser(accessToken: string): EdgeUserClient;
58
88
  exchangeCode(code: string, codeVerifier: string, redirectUri?: string): Promise<EdgeTokens>;
59
89
  refreshTokens(refreshToken: string): Promise<EdgeTokens>;
60
- getUser(accessToken: string): Promise<User>;
61
- getBalance(accessToken: string): Promise<Balance>;
62
- initiateTransfer(accessToken: string, options: TransferOptions): Promise<Transfer>;
63
- private validateTransferOptions;
64
- verifyTransfer(accessToken: string, transferId: string, otp: string): Promise<Transfer>;
65
- getTransfer(accessToken: string, transferId: string): Promise<Transfer>;
66
- listTransfers(accessToken: string, params?: ListTransfersParams): Promise<TransferList>;
67
- revokeConsent(accessToken: string): Promise<{
68
- revoked: boolean;
69
- }>;
90
+ /** @internal Called by {@link EdgeUserClient} — not part of the public API. */
91
+ _apiRequest<T>(method: string, path: string, accessToken: string, body?: unknown): Promise<T>;
70
92
  private getRetryDelay;
71
93
  private sleep;
72
- private apiRequest;
73
94
  private fetchWithTimeout;
74
95
  private parseTokenResponse;
75
96
  private handleTokenError;
76
97
  private handleApiErrorFromBody;
77
98
  }
78
99
 
79
- export { EdgeConnectServer, type EdgeConnectServerConfig, type RequestInfo, type ResponseInfo, type RetryConfig, type TransferOptions };
100
+ export { EdgeConnectServer, type EdgeConnectServerConfig, EdgeUserClient, type RequestInfo, type ResponseInfo, type RetryConfig, type TransferOptions };
package/dist/index.js CHANGED
@@ -20,27 +20,125 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- EdgeApiError: () => import_connect2.EdgeApiError,
24
- EdgeAuthenticationError: () => import_connect2.EdgeAuthenticationError,
23
+ EdgeApiError: () => import_connect3.EdgeApiError,
24
+ EdgeAuthenticationError: () => import_connect3.EdgeAuthenticationError,
25
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
26
+ EdgeConsentRequiredError: () => import_connect3.EdgeConsentRequiredError,
27
+ EdgeError: () => import_connect3.EdgeError,
28
+ EdgeInsufficientScopeError: () => import_connect3.EdgeInsufficientScopeError,
29
+ EdgeNetworkError: () => import_connect3.EdgeNetworkError,
30
+ EdgeNotFoundError: () => import_connect3.EdgeNotFoundError,
31
+ EdgeTokenExchangeError: () => import_connect3.EdgeTokenExchangeError,
32
+ EdgeUserClient: () => EdgeUserClient,
33
+ getEnvironmentConfig: () => import_connect4.getEnvironmentConfig,
34
+ isApiError: () => import_connect3.isApiError,
35
+ isAuthenticationError: () => import_connect3.isAuthenticationError,
36
+ isConsentRequiredError: () => import_connect3.isConsentRequiredError,
37
+ isEdgeError: () => import_connect3.isEdgeError,
38
+ isNetworkError: () => import_connect3.isNetworkError,
39
+ isProductionEnvironment: () => import_connect4.isProductionEnvironment
39
40
  });
40
41
  module.exports = __toCommonJS(index_exports);
41
42
 
42
43
  // src/edge-connect-server.ts
44
+ var import_connect2 = require("@edge-markets/connect");
45
+
46
+ // src/validators.ts
43
47
  var import_connect = require("@edge-markets/connect");
48
+ function validateVerifyIdentityOptions(options) {
49
+ const errors = {};
50
+ const firstName = typeof options.firstName === "string" ? options.firstName.trim() : "";
51
+ if (!firstName) {
52
+ errors.firstName = ["firstName is required for identity verification"];
53
+ }
54
+ const lastName = typeof options.lastName === "string" ? options.lastName.trim() : "";
55
+ if (!lastName) {
56
+ errors.lastName = ["lastName is required for identity verification"];
57
+ }
58
+ if (!options.address || typeof options.address !== "object" || Array.isArray(options.address)) {
59
+ errors.address = ["address is required for identity verification"];
60
+ }
61
+ if (Object.keys(errors).length > 0) {
62
+ const firstMessage = Object.values(errors)[0][0];
63
+ throw new import_connect.EdgeValidationError(firstMessage, errors);
64
+ }
65
+ }
66
+ function validateTransferOptions(options) {
67
+ const errors = {};
68
+ if (!options.type || !["debit", "credit"].includes(options.type)) {
69
+ errors.type = ['Must be "debit" or "credit"'];
70
+ }
71
+ if (!options.amount) {
72
+ errors.amount = ["Amount is required"];
73
+ } else {
74
+ const amount = parseFloat(options.amount);
75
+ if (isNaN(amount)) {
76
+ errors.amount = ["Must be a valid number"];
77
+ } else if (amount <= 0) {
78
+ errors.amount = ["Must be greater than 0"];
79
+ } else if (!/^\d+(\.\d{1,2})?$/.test(options.amount)) {
80
+ errors.amount = ["Must have at most 2 decimal places"];
81
+ }
82
+ }
83
+ if (!options.idempotencyKey || options.idempotencyKey.trim() === "") {
84
+ errors.idempotencyKey = ["idempotencyKey is required"];
85
+ } else if (options.idempotencyKey.length > 255) {
86
+ errors.idempotencyKey = ["Must be 255 characters or less"];
87
+ }
88
+ if (Object.keys(errors).length > 0) {
89
+ throw new import_connect.EdgeValidationError("Invalid transfer options", errors);
90
+ }
91
+ }
92
+
93
+ // src/edge-user-client.ts
94
+ var EdgeUserClient = class {
95
+ constructor(server, accessToken) {
96
+ this.server = server;
97
+ this.accessToken = accessToken;
98
+ }
99
+ async getUser() {
100
+ return this.server._apiRequest("GET", "/user", this.accessToken);
101
+ }
102
+ async verifyIdentity(options) {
103
+ validateVerifyIdentityOptions(options);
104
+ return this.server._apiRequest("POST", "/user/verify-identity", this.accessToken, options);
105
+ }
106
+ async getBalance() {
107
+ return this.server._apiRequest("GET", "/balance", this.accessToken);
108
+ }
109
+ async initiateTransfer(options) {
110
+ validateTransferOptions(options);
111
+ const body = {
112
+ type: options.type,
113
+ amount: options.amount,
114
+ idempotencyKey: options.idempotencyKey
115
+ };
116
+ return this.server._apiRequest("POST", "/transfer", this.accessToken, body);
117
+ }
118
+ async verifyTransfer(transferId, otp) {
119
+ return this.server._apiRequest(
120
+ "POST",
121
+ `/transfer/${encodeURIComponent(transferId)}/verify`,
122
+ this.accessToken,
123
+ { otp }
124
+ );
125
+ }
126
+ async getTransfer(transferId) {
127
+ return this.server._apiRequest("GET", `/transfer/${encodeURIComponent(transferId)}`, this.accessToken);
128
+ }
129
+ async listTransfers(params) {
130
+ const queryParams = new URLSearchParams();
131
+ if (params?.limit) queryParams.set("limit", String(params.limit));
132
+ if (params?.offset) queryParams.set("offset", String(params.offset));
133
+ if (params?.status) queryParams.set("status", params.status);
134
+ const query = queryParams.toString();
135
+ const path = query ? `/transfers?${query}` : "/transfers";
136
+ return this.server._apiRequest("GET", path, this.accessToken);
137
+ }
138
+ async revokeConsent() {
139
+ return this.server._apiRequest("DELETE", "/consent", this.accessToken);
140
+ }
141
+ };
44
142
 
45
143
  // src/mle.ts
46
144
  var import_crypto = require("crypto");
@@ -117,7 +215,7 @@ function fromBase64Url(value) {
117
215
  return Buffer.from(normalized, "base64");
118
216
  }
119
217
 
120
- // src/edge-connect-server.ts
218
+ // src/types.ts
121
219
  var DEFAULT_TIMEOUT = 3e4;
122
220
  var USER_AGENT = "@edge-markets/connect-node/1.0.0";
123
221
  var DEFAULT_RETRY_CONFIG = {
@@ -126,6 +224,8 @@ var DEFAULT_RETRY_CONFIG = {
126
224
  backoff: "exponential",
127
225
  baseDelayMs: 1e3
128
226
  };
227
+
228
+ // src/edge-connect-server.ts
129
229
  var instances = /* @__PURE__ */ new Map();
130
230
  function getInstanceKey(config) {
131
231
  return `${config.clientId}:${config.environment}`;
@@ -153,7 +253,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
153
253
  throw new Error("EdgeConnectServer: environment is required");
154
254
  }
155
255
  this.config = config;
156
- const envConfig = (0, import_connect.getEnvironmentConfig)(config.environment);
256
+ const envConfig = (0, import_connect2.getEnvironmentConfig)(config.environment);
157
257
  this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
158
258
  this.oauthBaseUrl = config.oauthBaseUrl || envConfig.oauthBaseUrl;
159
259
  this.timeout = config.timeout || DEFAULT_TIMEOUT;
@@ -162,6 +262,18 @@ var EdgeConnectServer = class _EdgeConnectServer {
162
262
  ...config.retry
163
263
  };
164
264
  }
265
+ /**
266
+ * Create a user-scoped client for making authenticated API calls.
267
+ *
268
+ * The returned {@link EdgeUserClient} is lightweight — create one per request
269
+ * or per user session and discard it when the token changes.
270
+ */
271
+ forUser(accessToken) {
272
+ if (!accessToken) {
273
+ throw new import_connect2.EdgeAuthenticationError("accessToken is required when calling forUser()");
274
+ }
275
+ return new EdgeUserClient(this, accessToken);
276
+ }
165
277
  async exchangeCode(code, codeVerifier, redirectUri) {
166
278
  const tokenUrl = `${this.oauthBaseUrl}/token`;
167
279
  const body = {
@@ -188,8 +300,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
188
300
  const data = await response.json();
189
301
  return this.parseTokenResponse(data);
190
302
  } catch (error) {
191
- if (error instanceof import_connect.EdgeError) throw error;
192
- throw new import_connect.EdgeNetworkError("Failed to exchange code", error);
303
+ if (error instanceof import_connect2.EdgeError) throw error;
304
+ throw new import_connect2.EdgeNetworkError("Failed to exchange code", error);
193
305
  }
194
306
  }
195
307
  async refreshTokens(refreshToken) {
@@ -211,7 +323,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
211
323
  });
212
324
  if (!response.ok) {
213
325
  const error = await response.json().catch(() => ({}));
214
- throw new import_connect.EdgeAuthenticationError(
326
+ throw new import_connect2.EdgeAuthenticationError(
215
327
  error.message || error.error_description || "Token refresh failed. User may need to reconnect.",
216
328
  { tokenError: error }
217
329
  );
@@ -219,89 +331,12 @@ var EdgeConnectServer = class _EdgeConnectServer {
219
331
  const data = await response.json();
220
332
  return this.parseTokenResponse(data, refreshToken);
221
333
  } catch (error) {
222
- if (error instanceof import_connect.EdgeError) throw error;
223
- throw new import_connect.EdgeNetworkError("Failed to refresh tokens", error);
224
- }
225
- }
226
- async getUser(accessToken) {
227
- return this.apiRequest("GET", "/user", accessToken);
228
- }
229
- async getBalance(accessToken) {
230
- return this.apiRequest("GET", "/balance", accessToken);
231
- }
232
- async initiateTransfer(accessToken, options) {
233
- this.validateTransferOptions(options);
234
- const body = {
235
- type: options.type,
236
- amount: options.amount,
237
- idempotencyKey: options.idempotencyKey
238
- };
239
- return this.apiRequest("POST", "/transfer", accessToken, body);
240
- }
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
- }
267
- async verifyTransfer(accessToken, transferId, otp) {
268
- return this.apiRequest(
269
- "POST",
270
- `/transfer/${encodeURIComponent(transferId)}/verify`,
271
- accessToken,
272
- { otp }
273
- );
274
- }
275
- async getTransfer(accessToken, transferId) {
276
- return this.apiRequest(
277
- "GET",
278
- `/transfer/${encodeURIComponent(transferId)}`,
279
- accessToken
280
- );
281
- }
282
- async listTransfers(accessToken, params) {
283
- const queryParams = new URLSearchParams();
284
- if (params?.limit) queryParams.set("limit", String(params.limit));
285
- if (params?.offset) queryParams.set("offset", String(params.offset));
286
- if (params?.status) queryParams.set("status", params.status);
287
- const query = queryParams.toString();
288
- const path = query ? `/transfers?${query}` : "/transfers";
289
- return this.apiRequest("GET", path, accessToken);
290
- }
291
- async revokeConsent(accessToken) {
292
- return this.apiRequest("DELETE", "/consent", accessToken);
293
- }
294
- getRetryDelay(attempt) {
295
- const { backoff, baseDelayMs } = this.retryConfig;
296
- if (backoff === "exponential") {
297
- return baseDelayMs * Math.pow(2, attempt);
334
+ if (error instanceof import_connect2.EdgeError) throw error;
335
+ throw new import_connect2.EdgeNetworkError("Failed to refresh tokens", error);
298
336
  }
299
- return baseDelayMs * (attempt + 1);
300
337
  }
301
- async sleep(ms) {
302
- return new Promise((resolve) => setTimeout(resolve, ms));
303
- }
304
- async apiRequest(method, path, accessToken, body) {
338
+ /** @internal Called by {@link EdgeUserClient} — not part of the public API. */
339
+ async _apiRequest(method, path, accessToken, body) {
305
340
  const url = `${this.apiBaseUrl}${path}`;
306
341
  let lastError = null;
307
342
  const mleConfig = this.config.mle;
@@ -335,14 +370,14 @@ var EdgeConnectServer = class _EdgeConnectServer {
335
370
  if (mleEnabled && typeof rawResponseBody?.jwe === "string") {
336
371
  responseBody = decryptMle(rawResponseBody.jwe, mleConfig);
337
372
  } else if (!mleEnabled && typeof rawResponseBody?.jwe === "string") {
338
- throw new import_connect.EdgeApiError(
373
+ throw new import_connect2.EdgeApiError(
339
374
  "mle_required",
340
375
  "The API responded with message-level encryption. Enable MLE in SDK config.",
341
376
  response.status,
342
377
  rawResponseBody
343
378
  );
344
379
  } else if (mleEnabled && mleConfig?.strictResponseEncryption !== false && response.ok && typeof rawResponseBody?.jwe !== "string") {
345
- throw new import_connect.EdgeApiError(
380
+ throw new import_connect2.EdgeApiError(
346
381
  "mle_response_missing",
347
382
  "Expected encrypted response payload but received plaintext.",
348
383
  response.status,
@@ -360,8 +395,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
360
395
  }
361
396
  return responseBody;
362
397
  } catch (error) {
363
- if (error instanceof import_connect.EdgeError) {
364
- if (!(error instanceof import_connect.EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
398
+ if (error instanceof import_connect2.EdgeError) {
399
+ if (!(error instanceof import_connect2.EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
365
400
  throw error;
366
401
  }
367
402
  lastError = error;
@@ -369,11 +404,24 @@ var EdgeConnectServer = class _EdgeConnectServer {
369
404
  }
370
405
  lastError = error;
371
406
  if (attempt >= this.retryConfig.maxRetries) {
372
- throw new import_connect.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
407
+ throw new import_connect2.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
373
408
  }
374
409
  }
375
410
  }
376
- throw new import_connect.EdgeNetworkError(`API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`, lastError);
411
+ throw new import_connect2.EdgeNetworkError(
412
+ `API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`,
413
+ lastError
414
+ );
415
+ }
416
+ getRetryDelay(attempt) {
417
+ const { backoff, baseDelayMs } = this.retryConfig;
418
+ if (backoff === "exponential") {
419
+ return baseDelayMs * Math.pow(2, attempt);
420
+ }
421
+ return baseDelayMs * (attempt + 1);
422
+ }
423
+ async sleep(ms) {
424
+ return new Promise((resolve) => setTimeout(resolve, ms));
377
425
  }
378
426
  async fetchWithTimeout(url, options) {
379
427
  const controller = new AbortController();
@@ -401,45 +449,37 @@ var EdgeConnectServer = class _EdgeConnectServer {
401
449
  const errorCode = error.error;
402
450
  const errorMessage = error.message || error.error_description;
403
451
  if (errorCode === "invalid_grant" || errorMessage?.includes("Invalid or expired")) {
404
- return new import_connect.EdgeTokenExchangeError(
405
- "Authorization code is invalid, expired, or already used. Please try again.",
406
- { edgeBoostError: error }
407
- );
452
+ return new import_connect2.EdgeTokenExchangeError("Authorization code is invalid, expired, or already used. Please try again.", {
453
+ edgeBoostError: error
454
+ });
408
455
  }
409
456
  if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
410
- return new import_connect.EdgeAuthenticationError(
411
- "Invalid client credentials. Check your client ID and secret.",
412
- { edgeBoostError: error }
413
- );
457
+ return new import_connect2.EdgeAuthenticationError("Invalid client credentials. Check your client ID and secret.", {
458
+ edgeBoostError: error
459
+ });
414
460
  }
415
461
  if (errorMessage?.includes("code_verifier") || errorMessage?.includes("PKCE")) {
416
- return new import_connect.EdgeTokenExchangeError(
417
- "PKCE verification failed. Please try again.",
418
- { edgeBoostError: error }
419
- );
462
+ return new import_connect2.EdgeTokenExchangeError("PKCE verification failed. Please try again.", { edgeBoostError: error });
420
463
  }
421
- return new import_connect.EdgeTokenExchangeError(
422
- errorMessage || "Failed to exchange authorization code",
423
- { edgeBoostError: error, statusCode: status }
424
- );
464
+ return new import_connect2.EdgeTokenExchangeError(errorMessage || "Failed to exchange authorization code", {
465
+ edgeBoostError: error,
466
+ statusCode: status
467
+ });
425
468
  }
426
469
  async handleApiErrorFromBody(error, status, path) {
427
470
  if (status === 401) {
428
- return new import_connect.EdgeAuthenticationError(
429
- error.message || "Access token is invalid or expired",
430
- error
431
- );
471
+ return new import_connect2.EdgeAuthenticationError(error.message || "Access token is invalid or expired", error);
432
472
  }
433
473
  if (status === 403) {
434
474
  if (error.error === "consent_required") {
435
- return new import_connect.EdgeConsentRequiredError(
475
+ return new import_connect2.EdgeConsentRequiredError(
436
476
  this.config.clientId,
437
477
  error.consentUrl,
438
478
  error.message
439
479
  );
440
480
  }
441
481
  if (error.error === "insufficient_scope" || error.error === "insufficient_consent") {
442
- return new import_connect.EdgeInsufficientScopeError(
482
+ return new import_connect2.EdgeInsufficientScopeError(
443
483
  error.missing_scopes || error.missingScopes || [],
444
484
  error.message
445
485
  );
@@ -448,9 +488,15 @@ var EdgeConnectServer = class _EdgeConnectServer {
448
488
  if (status === 404) {
449
489
  const resourceType = path.includes("/transfer") ? "Transfer" : "Resource";
450
490
  const resourceId = path.split("/").pop() || "unknown";
451
- return new import_connect.EdgeNotFoundError(resourceType, resourceId);
491
+ return new import_connect2.EdgeNotFoundError(resourceType, resourceId);
452
492
  }
453
- return new import_connect.EdgeApiError(
493
+ if (status === 422 && error.error === "identity_verification_failed") {
494
+ return new import_connect2.EdgeIdentityVerificationError(
495
+ error.fieldErrors || {},
496
+ error.message
497
+ );
498
+ }
499
+ return new import_connect2.EdgeApiError(
454
500
  error.error || "api_error",
455
501
  error.message || error.error_description || `Request failed with status ${status}`,
456
502
  status,
@@ -460,8 +506,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
460
506
  };
461
507
 
462
508
  // src/index.ts
463
- var import_connect2 = require("@edge-markets/connect");
464
509
  var import_connect3 = require("@edge-markets/connect");
510
+ var import_connect4 = require("@edge-markets/connect");
465
511
  // Annotate the CommonJS export names for ESM import in node:
466
512
  0 && (module.exports = {
467
513
  EdgeApiError,
@@ -473,6 +519,7 @@ var import_connect3 = require("@edge-markets/connect");
473
519
  EdgeNetworkError,
474
520
  EdgeNotFoundError,
475
521
  EdgeTokenExchangeError,
522
+ EdgeUserClient,
476
523
  getEnvironmentConfig,
477
524
  isApiError,
478
525
  isAuthenticationError,
package/dist/index.mjs CHANGED
@@ -1,17 +1,114 @@
1
1
  // src/edge-connect-server.ts
2
2
  import {
3
- getEnvironmentConfig,
4
- EdgeError,
3
+ EdgeApiError,
5
4
  EdgeAuthenticationError,
6
- EdgeTokenExchangeError,
7
5
  EdgeConsentRequiredError,
6
+ EdgeError,
7
+ EdgeIdentityVerificationError,
8
8
  EdgeInsufficientScopeError,
9
- EdgeApiError,
10
- EdgeNotFoundError,
11
9
  EdgeNetworkError,
12
- EdgeValidationError
10
+ EdgeNotFoundError,
11
+ EdgeTokenExchangeError,
12
+ getEnvironmentConfig
13
13
  } from "@edge-markets/connect";
14
14
 
15
+ // src/validators.ts
16
+ import { EdgeValidationError } from "@edge-markets/connect";
17
+ function validateVerifyIdentityOptions(options) {
18
+ const errors = {};
19
+ const firstName = typeof options.firstName === "string" ? options.firstName.trim() : "";
20
+ if (!firstName) {
21
+ errors.firstName = ["firstName is required for identity verification"];
22
+ }
23
+ const lastName = typeof options.lastName === "string" ? options.lastName.trim() : "";
24
+ if (!lastName) {
25
+ errors.lastName = ["lastName is required for identity verification"];
26
+ }
27
+ if (!options.address || typeof options.address !== "object" || Array.isArray(options.address)) {
28
+ errors.address = ["address is required for identity verification"];
29
+ }
30
+ if (Object.keys(errors).length > 0) {
31
+ const firstMessage = Object.values(errors)[0][0];
32
+ throw new EdgeValidationError(firstMessage, errors);
33
+ }
34
+ }
35
+ function validateTransferOptions(options) {
36
+ const errors = {};
37
+ if (!options.type || !["debit", "credit"].includes(options.type)) {
38
+ errors.type = ['Must be "debit" or "credit"'];
39
+ }
40
+ if (!options.amount) {
41
+ errors.amount = ["Amount is required"];
42
+ } else {
43
+ const amount = parseFloat(options.amount);
44
+ if (isNaN(amount)) {
45
+ errors.amount = ["Must be a valid number"];
46
+ } else if (amount <= 0) {
47
+ errors.amount = ["Must be greater than 0"];
48
+ } else if (!/^\d+(\.\d{1,2})?$/.test(options.amount)) {
49
+ errors.amount = ["Must have at most 2 decimal places"];
50
+ }
51
+ }
52
+ if (!options.idempotencyKey || options.idempotencyKey.trim() === "") {
53
+ errors.idempotencyKey = ["idempotencyKey is required"];
54
+ } else if (options.idempotencyKey.length > 255) {
55
+ errors.idempotencyKey = ["Must be 255 characters or less"];
56
+ }
57
+ if (Object.keys(errors).length > 0) {
58
+ throw new EdgeValidationError("Invalid transfer options", errors);
59
+ }
60
+ }
61
+
62
+ // src/edge-user-client.ts
63
+ var EdgeUserClient = class {
64
+ constructor(server, accessToken) {
65
+ this.server = server;
66
+ this.accessToken = accessToken;
67
+ }
68
+ async getUser() {
69
+ return this.server._apiRequest("GET", "/user", this.accessToken);
70
+ }
71
+ async verifyIdentity(options) {
72
+ validateVerifyIdentityOptions(options);
73
+ return this.server._apiRequest("POST", "/user/verify-identity", this.accessToken, options);
74
+ }
75
+ async getBalance() {
76
+ return this.server._apiRequest("GET", "/balance", this.accessToken);
77
+ }
78
+ async initiateTransfer(options) {
79
+ validateTransferOptions(options);
80
+ const body = {
81
+ type: options.type,
82
+ amount: options.amount,
83
+ idempotencyKey: options.idempotencyKey
84
+ };
85
+ return this.server._apiRequest("POST", "/transfer", this.accessToken, body);
86
+ }
87
+ async verifyTransfer(transferId, otp) {
88
+ return this.server._apiRequest(
89
+ "POST",
90
+ `/transfer/${encodeURIComponent(transferId)}/verify`,
91
+ this.accessToken,
92
+ { otp }
93
+ );
94
+ }
95
+ async getTransfer(transferId) {
96
+ return this.server._apiRequest("GET", `/transfer/${encodeURIComponent(transferId)}`, this.accessToken);
97
+ }
98
+ async listTransfers(params) {
99
+ const queryParams = new URLSearchParams();
100
+ if (params?.limit) queryParams.set("limit", String(params.limit));
101
+ if (params?.offset) queryParams.set("offset", String(params.offset));
102
+ if (params?.status) queryParams.set("status", params.status);
103
+ const query = queryParams.toString();
104
+ const path = query ? `/transfers?${query}` : "/transfers";
105
+ return this.server._apiRequest("GET", path, this.accessToken);
106
+ }
107
+ async revokeConsent() {
108
+ return this.server._apiRequest("DELETE", "/consent", this.accessToken);
109
+ }
110
+ };
111
+
15
112
  // src/mle.ts
16
113
  import { randomUUID, randomBytes, createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from "crypto";
17
114
  function encryptMle(payload, clientId, config) {
@@ -87,7 +184,7 @@ function fromBase64Url(value) {
87
184
  return Buffer.from(normalized, "base64");
88
185
  }
89
186
 
90
- // src/edge-connect-server.ts
187
+ // src/types.ts
91
188
  var DEFAULT_TIMEOUT = 3e4;
92
189
  var USER_AGENT = "@edge-markets/connect-node/1.0.0";
93
190
  var DEFAULT_RETRY_CONFIG = {
@@ -96,6 +193,8 @@ var DEFAULT_RETRY_CONFIG = {
96
193
  backoff: "exponential",
97
194
  baseDelayMs: 1e3
98
195
  };
196
+
197
+ // src/edge-connect-server.ts
99
198
  var instances = /* @__PURE__ */ new Map();
100
199
  function getInstanceKey(config) {
101
200
  return `${config.clientId}:${config.environment}`;
@@ -132,6 +231,18 @@ var EdgeConnectServer = class _EdgeConnectServer {
132
231
  ...config.retry
133
232
  };
134
233
  }
234
+ /**
235
+ * Create a user-scoped client for making authenticated API calls.
236
+ *
237
+ * The returned {@link EdgeUserClient} is lightweight — create one per request
238
+ * or per user session and discard it when the token changes.
239
+ */
240
+ forUser(accessToken) {
241
+ if (!accessToken) {
242
+ throw new EdgeAuthenticationError("accessToken is required when calling forUser()");
243
+ }
244
+ return new EdgeUserClient(this, accessToken);
245
+ }
135
246
  async exchangeCode(code, codeVerifier, redirectUri) {
136
247
  const tokenUrl = `${this.oauthBaseUrl}/token`;
137
248
  const body = {
@@ -193,85 +304,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
193
304
  throw new EdgeNetworkError("Failed to refresh tokens", error);
194
305
  }
195
306
  }
196
- async getUser(accessToken) {
197
- return this.apiRequest("GET", "/user", accessToken);
198
- }
199
- async getBalance(accessToken) {
200
- return this.apiRequest("GET", "/balance", accessToken);
201
- }
202
- async initiateTransfer(accessToken, options) {
203
- this.validateTransferOptions(options);
204
- const body = {
205
- type: options.type,
206
- amount: options.amount,
207
- idempotencyKey: options.idempotencyKey
208
- };
209
- return this.apiRequest("POST", "/transfer", accessToken, body);
210
- }
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
- }
237
- async verifyTransfer(accessToken, transferId, otp) {
238
- return this.apiRequest(
239
- "POST",
240
- `/transfer/${encodeURIComponent(transferId)}/verify`,
241
- accessToken,
242
- { otp }
243
- );
244
- }
245
- async getTransfer(accessToken, transferId) {
246
- return this.apiRequest(
247
- "GET",
248
- `/transfer/${encodeURIComponent(transferId)}`,
249
- accessToken
250
- );
251
- }
252
- async listTransfers(accessToken, params) {
253
- const queryParams = new URLSearchParams();
254
- if (params?.limit) queryParams.set("limit", String(params.limit));
255
- if (params?.offset) queryParams.set("offset", String(params.offset));
256
- if (params?.status) queryParams.set("status", params.status);
257
- const query = queryParams.toString();
258
- const path = query ? `/transfers?${query}` : "/transfers";
259
- return this.apiRequest("GET", path, accessToken);
260
- }
261
- async revokeConsent(accessToken) {
262
- return this.apiRequest("DELETE", "/consent", accessToken);
263
- }
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
- }
274
- async apiRequest(method, path, accessToken, body) {
307
+ /** @internal Called by {@link EdgeUserClient} — not part of the public API. */
308
+ async _apiRequest(method, path, accessToken, body) {
275
309
  const url = `${this.apiBaseUrl}${path}`;
276
310
  let lastError = null;
277
311
  const mleConfig = this.config.mle;
@@ -343,7 +377,20 @@ var EdgeConnectServer = class _EdgeConnectServer {
343
377
  }
344
378
  }
345
379
  }
346
- throw new EdgeNetworkError(`API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`, lastError);
380
+ throw new EdgeNetworkError(
381
+ `API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`,
382
+ lastError
383
+ );
384
+ }
385
+ getRetryDelay(attempt) {
386
+ const { backoff, baseDelayMs } = this.retryConfig;
387
+ if (backoff === "exponential") {
388
+ return baseDelayMs * Math.pow(2, attempt);
389
+ }
390
+ return baseDelayMs * (attempt + 1);
391
+ }
392
+ async sleep(ms) {
393
+ return new Promise((resolve) => setTimeout(resolve, ms));
347
394
  }
348
395
  async fetchWithTimeout(url, options) {
349
396
  const controller = new AbortController();
@@ -371,34 +418,26 @@ var EdgeConnectServer = class _EdgeConnectServer {
371
418
  const errorCode = error.error;
372
419
  const errorMessage = error.message || error.error_description;
373
420
  if (errorCode === "invalid_grant" || errorMessage?.includes("Invalid or expired")) {
374
- return new EdgeTokenExchangeError(
375
- "Authorization code is invalid, expired, or already used. Please try again.",
376
- { edgeBoostError: error }
377
- );
421
+ return new EdgeTokenExchangeError("Authorization code is invalid, expired, or already used. Please try again.", {
422
+ edgeBoostError: error
423
+ });
378
424
  }
379
425
  if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
380
- return new EdgeAuthenticationError(
381
- "Invalid client credentials. Check your client ID and secret.",
382
- { edgeBoostError: error }
383
- );
426
+ return new EdgeAuthenticationError("Invalid client credentials. Check your client ID and secret.", {
427
+ edgeBoostError: error
428
+ });
384
429
  }
385
430
  if (errorMessage?.includes("code_verifier") || errorMessage?.includes("PKCE")) {
386
- return new EdgeTokenExchangeError(
387
- "PKCE verification failed. Please try again.",
388
- { edgeBoostError: error }
389
- );
431
+ return new EdgeTokenExchangeError("PKCE verification failed. Please try again.", { edgeBoostError: error });
390
432
  }
391
- return new EdgeTokenExchangeError(
392
- errorMessage || "Failed to exchange authorization code",
393
- { edgeBoostError: error, statusCode: status }
394
- );
433
+ return new EdgeTokenExchangeError(errorMessage || "Failed to exchange authorization code", {
434
+ edgeBoostError: error,
435
+ statusCode: status
436
+ });
395
437
  }
396
438
  async handleApiErrorFromBody(error, status, path) {
397
439
  if (status === 401) {
398
- return new EdgeAuthenticationError(
399
- error.message || "Access token is invalid or expired",
400
- error
401
- );
440
+ return new EdgeAuthenticationError(error.message || "Access token is invalid or expired", error);
402
441
  }
403
442
  if (status === 403) {
404
443
  if (error.error === "consent_required") {
@@ -420,6 +459,12 @@ var EdgeConnectServer = class _EdgeConnectServer {
420
459
  const resourceId = path.split("/").pop() || "unknown";
421
460
  return new EdgeNotFoundError(resourceType, resourceId);
422
461
  }
462
+ if (status === 422 && error.error === "identity_verification_failed") {
463
+ return new EdgeIdentityVerificationError(
464
+ error.fieldErrors || {},
465
+ error.message
466
+ );
467
+ }
423
468
  return new EdgeApiError(
424
469
  error.error || "api_error",
425
470
  error.message || error.error_description || `Request failed with status ${status}`,
@@ -431,24 +476,21 @@ var EdgeConnectServer = class _EdgeConnectServer {
431
476
 
432
477
  // src/index.ts
433
478
  import {
434
- EdgeError as EdgeError2,
479
+ EdgeApiError as EdgeApiError2,
435
480
  EdgeAuthenticationError as EdgeAuthenticationError2,
436
- EdgeTokenExchangeError as EdgeTokenExchangeError2,
437
481
  EdgeConsentRequiredError as EdgeConsentRequiredError2,
482
+ EdgeError as EdgeError2,
438
483
  EdgeInsufficientScopeError as EdgeInsufficientScopeError2,
439
- EdgeApiError as EdgeApiError2,
440
- EdgeNotFoundError as EdgeNotFoundError2,
441
484
  EdgeNetworkError as EdgeNetworkError2,
442
- isEdgeError,
485
+ EdgeNotFoundError as EdgeNotFoundError2,
486
+ EdgeTokenExchangeError as EdgeTokenExchangeError2,
487
+ isApiError,
443
488
  isAuthenticationError,
444
489
  isConsentRequiredError,
445
- isApiError,
490
+ isEdgeError,
446
491
  isNetworkError
447
492
  } from "@edge-markets/connect";
448
- import {
449
- getEnvironmentConfig as getEnvironmentConfig2,
450
- isProductionEnvironment
451
- } from "@edge-markets/connect";
493
+ import { getEnvironmentConfig as getEnvironmentConfig2, isProductionEnvironment } from "@edge-markets/connect";
452
494
  export {
453
495
  EdgeApiError2 as EdgeApiError,
454
496
  EdgeAuthenticationError2 as EdgeAuthenticationError,
@@ -459,6 +501,7 @@ export {
459
501
  EdgeNetworkError2 as EdgeNetworkError,
460
502
  EdgeNotFoundError2 as EdgeNotFoundError,
461
503
  EdgeTokenExchangeError2 as EdgeTokenExchangeError,
504
+ EdgeUserClient,
462
505
  getEnvironmentConfig2 as getEnvironmentConfig,
463
506
  isApiError,
464
507
  isAuthenticationError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edge-markets/connect-node",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Server SDK for EDGE Connect token exchange and API calls",
5
5
  "author": "Edge Markets",
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@edge-markets/connect": "^1.2.0"
24
+ "@edge-markets/connect": "^1.3.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsup": "^8.0.0",