@edge-markets/connect-node 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Server SDK for EDGE Connect token exchange and API calls.
6
6
 
7
7
  - 🔐 **Secure token exchange** - Exchange codes for tokens with PKCE
8
8
  - 🔄 **Token refresh** - Automatic refresh token handling
9
- - 📡 **Full API client** - User, balance, transfers
9
+ - 📡 **Full API client** - User, balance, transfers via `forUser()` pattern
10
10
  - 🛡️ **Typed errors** - Specific error classes for each scenario
11
11
  - 📝 **TypeScript first** - Complete type definitions
12
12
 
@@ -23,7 +23,7 @@ yarn add @edge-markets/connect-node
23
23
  ## Quick Start
24
24
 
25
25
  ```typescript
26
- import { EdgeConnectServer } from '@edgeboost/edge-connect-server'
26
+ import { EdgeConnectServer } from '@edge-markets/connect-node'
27
27
 
28
28
  // Create instance once (reuse for all requests)
29
29
  const edge = new EdgeConnectServer({
@@ -35,9 +35,10 @@ const edge = new EdgeConnectServer({
35
35
  // Exchange code from EdgeLink for tokens
36
36
  const tokens = await edge.exchangeCode(code, codeVerifier)
37
37
 
38
- // Make API calls
39
- const user = await edge.getUser(tokens.accessToken)
40
- const balance = await edge.getBalance(tokens.accessToken)
38
+ // Create a user-scoped client and make API calls
39
+ const client = edge.forUser(tokens.accessToken)
40
+ const user = await client.getUser()
41
+ const balance = await client.getBalance()
41
42
  ```
42
43
 
43
44
  ## ⚠️ Security
@@ -56,8 +57,11 @@ interface EdgeConnectServerConfig {
56
57
 
57
58
  // Optional
58
59
  apiBaseUrl?: string // Custom API URL (dev only)
59
- authDomain?: string // Custom Cognito domain (dev only)
60
+ oauthBaseUrl?: string // Custom OAuth URL (dev only)
60
61
  timeout?: number // Request timeout (default: 30000ms)
62
+ retry?: RetryConfig // Retry configuration
63
+ onRequest?: (info) => void // Hook called before each request
64
+ onResponse?: (info) => void // Hook called after each response
61
65
 
62
66
  // Optional: Message Level Encryption (Connect endpoints only)
63
67
  mle?: {
@@ -152,15 +156,19 @@ async function getValidAccessToken(userId: string): Promise<string> {
152
156
 
153
157
  ## API Methods
154
158
 
159
+ All user-scoped API methods live on `EdgeUserClient`, created via `edge.forUser(accessToken)`:
160
+
161
+ ```typescript
162
+ const client = edge.forUser(accessToken)
163
+ ```
164
+
155
165
  ### User & Balance
156
166
 
157
167
  ```typescript
158
- // Get user profile
159
- const user = await edge.getUser(accessToken)
168
+ const user = await client.getUser()
160
169
  // Returns: { id, email, firstName, lastName, createdAt }
161
170
 
162
- // Get balance
163
- const balance = await edge.getBalance(accessToken)
171
+ const balance = await client.getBalance()
164
172
  // Returns: { userId, availableBalance, currency, asOf }
165
173
  ```
166
174
 
@@ -168,7 +176,7 @@ const balance = await edge.getBalance(accessToken)
168
176
 
169
177
  ```typescript
170
178
  // Initiate transfer (requires OTP verification)
171
- const transfer = await edge.initiateTransfer(accessToken, {
179
+ const transfer = await client.initiateTransfer({
172
180
  type: 'debit', // 'debit' = pull from user, 'credit' = push to user
173
181
  amount: '100.00',
174
182
  idempotencyKey: `txn_${userId}_${Date.now()}`,
@@ -176,14 +184,14 @@ const transfer = await edge.initiateTransfer(accessToken, {
176
184
  // Returns: { transferId, status: 'pending_verification', otpMethod }
177
185
 
178
186
  // Verify with OTP
179
- const result = await edge.verifyTransfer(accessToken, transfer.transferId, userOtp)
187
+ const result = await client.verifyTransfer(transfer.transferId, userOtp)
180
188
  // Returns: { transferId, status: 'completed' | 'failed' }
181
189
 
182
190
  // Get transfer status
183
- const status = await edge.getTransfer(accessToken, transferId)
191
+ const status = await client.getTransfer(transferId)
184
192
 
185
193
  // List transfers
186
- const { transfers, total } = await edge.listTransfers(accessToken, {
194
+ const { transfers, total } = await client.listTransfers({
187
195
  status: 'completed',
188
196
  limit: 10,
189
197
  offset: 0,
@@ -194,7 +202,7 @@ const { transfers, total } = await edge.listTransfers(accessToken, {
194
202
 
195
203
  ```typescript
196
204
  // Revoke consent (disconnect user)
197
- await edge.revokeConsent(accessToken)
205
+ await client.revokeConsent()
198
206
 
199
207
  // Clean up stored tokens
200
208
  await db.edgeConnections.delete(userId)
@@ -212,7 +220,8 @@ import {
212
220
  } from '@edge-markets/connect-node'
213
221
 
214
222
  try {
215
- const balance = await edge.getBalance(accessToken)
223
+ const client = edge.forUser(accessToken)
224
+ const balance = await client.getBalance()
216
225
  } catch (error) {
217
226
  if (error instanceof EdgeAuthenticationError) {
218
227
  // Token expired - try refresh or reconnect
@@ -273,7 +282,8 @@ export class EdgeService {
273
282
 
274
283
  async getBalance(accessToken: string) {
275
284
  try {
276
- return await this.edge.getBalance(accessToken)
285
+ const client = this.edge.forUser(accessToken)
286
+ return await client.getBalance()
277
287
  } catch (error) {
278
288
  if (error instanceof EdgeConsentRequiredError) {
279
289
  this.logger.warn('User consent required')
@@ -323,7 +333,8 @@ app.post('/api/edge/exchange', async (req, res) => {
323
333
  app.get('/api/edge/balance', async (req, res) => {
324
334
  try {
325
335
  const accessToken = await getAccessTokenForUser(req.user.id)
326
- const balance = await edge.getBalance(accessToken)
336
+ const client = edge.forUser(accessToken)
337
+ const balance = await client.getBalance()
327
338
  res.json(balance)
328
339
  } catch (error) {
329
340
  // Handle errors...
@@ -339,10 +350,3 @@ app.get('/api/edge/balance', async (req, res) => {
339
350
  ## License
340
351
 
341
352
  MIT
342
-
343
-
344
-
345
-
346
-
347
-
348
-
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { EdgeEnvironment, EdgeTokens, User, Balance, Transfer, ListTransfersParams, TransferList } from '@edge-markets/connect';
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';
1
+ import { EdgeEnvironment, User, VerifyIdentityOptions, VerifyIdentityResult, Balance, Transfer, ListTransfersParams, TransferList, CreateVerificationSessionRequest, VerificationSession, VerificationSessionStatusResponse, EdgeTokens } from '@edge-markets/connect';
2
+ export { Balance, CreateVerificationSessionRequest, EdgeApiError, EdgeAuthenticationError, EdgeConsentRequiredError, EdgeEnvironment, EdgeError, EdgeInsufficientScopeError, EdgeNetworkError, EdgeNotFoundError, EdgeTokenExchangeError, EdgeTokens, ListTransfersParams, Transfer, TransferList, TransferListItem, TransferStatus, TransferType, User, VerificationSession, VerificationSessionStatus, VerificationSessionStatusResponse, getEnvironmentConfig, isApiError, isAuthenticationError, isConsentRequiredError, isEdgeError, isNetworkError, isProductionEnvironment } from '@edge-markets/connect';
3
3
 
4
4
  interface RequestInfo {
5
5
  method: string;
@@ -46,6 +46,40 @@ 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
+ * Creates an EDGE-hosted transfer verification session.
72
+ * Returns a `verificationUrl` to open in an iframe or popup.
73
+ * The handoff token is single-use and expires in 120 seconds.
74
+ */
75
+ createVerificationSession(transferId: string, options: CreateVerificationSessionRequest): Promise<VerificationSession>;
76
+ /**
77
+ * Polls the status of a verification session.
78
+ * Use as an alternative to postMessage events.
79
+ */
80
+ getVerificationSessionStatus(transferId: string, sessionId: string): Promise<VerificationSessionStatusResponse>;
81
+ }
82
+
49
83
  declare class EdgeConnectServer {
50
84
  private readonly config;
51
85
  private readonly apiBaseUrl;
@@ -55,25 +89,23 @@ declare class EdgeConnectServer {
55
89
  static getInstance(config: EdgeConnectServerConfig): EdgeConnectServer;
56
90
  static clearInstances(): void;
57
91
  constructor(config: EdgeConnectServerConfig);
92
+ /**
93
+ * Create a user-scoped client for making authenticated API calls.
94
+ *
95
+ * The returned {@link EdgeUserClient} is lightweight — create one per request
96
+ * or per user session and discard it when the token changes.
97
+ */
98
+ forUser(accessToken: string): EdgeUserClient;
58
99
  exchangeCode(code: string, codeVerifier: string, redirectUri?: string): Promise<EdgeTokens>;
59
100
  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
- }>;
101
+ /** @internal Called by {@link EdgeUserClient} — not part of the public API. */
102
+ _apiRequest<T>(method: string, path: string, accessToken: string, body?: unknown): Promise<T>;
70
103
  private getRetryDelay;
71
104
  private sleep;
72
- private apiRequest;
73
105
  private fetchWithTimeout;
74
106
  private parseTokenResponse;
75
107
  private handleTokenError;
76
108
  private handleApiErrorFromBody;
77
109
  }
78
110
 
79
- export { EdgeConnectServer, type EdgeConnectServerConfig, type RequestInfo, type ResponseInfo, type RetryConfig, type TransferOptions };
111
+ export { EdgeConnectServer, type EdgeConnectServerConfig, EdgeUserClient, type RequestInfo, type ResponseInfo, type RetryConfig, type TransferOptions };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { EdgeEnvironment, EdgeTokens, User, Balance, Transfer, ListTransfersParams, TransferList } from '@edge-markets/connect';
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';
1
+ import { EdgeEnvironment, User, VerifyIdentityOptions, VerifyIdentityResult, Balance, Transfer, ListTransfersParams, TransferList, CreateVerificationSessionRequest, VerificationSession, VerificationSessionStatusResponse, EdgeTokens } from '@edge-markets/connect';
2
+ export { Balance, CreateVerificationSessionRequest, EdgeApiError, EdgeAuthenticationError, EdgeConsentRequiredError, EdgeEnvironment, EdgeError, EdgeInsufficientScopeError, EdgeNetworkError, EdgeNotFoundError, EdgeTokenExchangeError, EdgeTokens, ListTransfersParams, Transfer, TransferList, TransferListItem, TransferStatus, TransferType, User, VerificationSession, VerificationSessionStatus, VerificationSessionStatusResponse, getEnvironmentConfig, isApiError, isAuthenticationError, isConsentRequiredError, isEdgeError, isNetworkError, isProductionEnvironment } from '@edge-markets/connect';
3
3
 
4
4
  interface RequestInfo {
5
5
  method: string;
@@ -46,6 +46,40 @@ 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
+ * Creates an EDGE-hosted transfer verification session.
72
+ * Returns a `verificationUrl` to open in an iframe or popup.
73
+ * The handoff token is single-use and expires in 120 seconds.
74
+ */
75
+ createVerificationSession(transferId: string, options: CreateVerificationSessionRequest): Promise<VerificationSession>;
76
+ /**
77
+ * Polls the status of a verification session.
78
+ * Use as an alternative to postMessage events.
79
+ */
80
+ getVerificationSessionStatus(transferId: string, sessionId: string): Promise<VerificationSessionStatusResponse>;
81
+ }
82
+
49
83
  declare class EdgeConnectServer {
50
84
  private readonly config;
51
85
  private readonly apiBaseUrl;
@@ -55,25 +89,23 @@ declare class EdgeConnectServer {
55
89
  static getInstance(config: EdgeConnectServerConfig): EdgeConnectServer;
56
90
  static clearInstances(): void;
57
91
  constructor(config: EdgeConnectServerConfig);
92
+ /**
93
+ * Create a user-scoped client for making authenticated API calls.
94
+ *
95
+ * The returned {@link EdgeUserClient} is lightweight — create one per request
96
+ * or per user session and discard it when the token changes.
97
+ */
98
+ forUser(accessToken: string): EdgeUserClient;
58
99
  exchangeCode(code: string, codeVerifier: string, redirectUri?: string): Promise<EdgeTokens>;
59
100
  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
- }>;
101
+ /** @internal Called by {@link EdgeUserClient} — not part of the public API. */
102
+ _apiRequest<T>(method: string, path: string, accessToken: string, body?: unknown): Promise<T>;
70
103
  private getRetryDelay;
71
104
  private sleep;
72
- private apiRequest;
73
105
  private fetchWithTimeout;
74
106
  private parseTokenResponse;
75
107
  private handleTokenError;
76
108
  private handleApiErrorFromBody;
77
109
  }
78
110
 
79
- export { EdgeConnectServer, type EdgeConnectServerConfig, type RequestInfo, type ResponseInfo, type RetryConfig, type TransferOptions };
111
+ export { EdgeConnectServer, type EdgeConnectServerConfig, EdgeUserClient, type RequestInfo, type ResponseInfo, type RetryConfig, type TransferOptions };
package/dist/index.js CHANGED
@@ -20,27 +20,149 @@ 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
+ /**
142
+ * Creates an EDGE-hosted transfer verification session.
143
+ * Returns a `verificationUrl` to open in an iframe or popup.
144
+ * The handoff token is single-use and expires in 120 seconds.
145
+ */
146
+ async createVerificationSession(transferId, options) {
147
+ return this.server._apiRequest(
148
+ "POST",
149
+ `/transfer/${encodeURIComponent(transferId)}/verification-session`,
150
+ this.accessToken,
151
+ { origin: options.origin }
152
+ );
153
+ }
154
+ /**
155
+ * Polls the status of a verification session.
156
+ * Use as an alternative to postMessage events.
157
+ */
158
+ async getVerificationSessionStatus(transferId, sessionId) {
159
+ return this.server._apiRequest(
160
+ "GET",
161
+ `/transfer/${encodeURIComponent(transferId)}/verification-session/${encodeURIComponent(sessionId)}`,
162
+ this.accessToken
163
+ );
164
+ }
165
+ };
44
166
 
45
167
  // src/mle.ts
46
168
  var import_crypto = require("crypto");
@@ -117,7 +239,7 @@ function fromBase64Url(value) {
117
239
  return Buffer.from(normalized, "base64");
118
240
  }
119
241
 
120
- // src/edge-connect-server.ts
242
+ // src/types.ts
121
243
  var DEFAULT_TIMEOUT = 3e4;
122
244
  var USER_AGENT = "@edge-markets/connect-node/1.0.0";
123
245
  var DEFAULT_RETRY_CONFIG = {
@@ -126,6 +248,8 @@ var DEFAULT_RETRY_CONFIG = {
126
248
  backoff: "exponential",
127
249
  baseDelayMs: 1e3
128
250
  };
251
+
252
+ // src/edge-connect-server.ts
129
253
  var instances = /* @__PURE__ */ new Map();
130
254
  function getInstanceKey(config) {
131
255
  return `${config.clientId}:${config.environment}`;
@@ -153,7 +277,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
153
277
  throw new Error("EdgeConnectServer: environment is required");
154
278
  }
155
279
  this.config = config;
156
- const envConfig = (0, import_connect.getEnvironmentConfig)(config.environment);
280
+ const envConfig = (0, import_connect2.getEnvironmentConfig)(config.environment);
157
281
  this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
158
282
  this.oauthBaseUrl = config.oauthBaseUrl || envConfig.oauthBaseUrl;
159
283
  this.timeout = config.timeout || DEFAULT_TIMEOUT;
@@ -162,6 +286,18 @@ var EdgeConnectServer = class _EdgeConnectServer {
162
286
  ...config.retry
163
287
  };
164
288
  }
289
+ /**
290
+ * Create a user-scoped client for making authenticated API calls.
291
+ *
292
+ * The returned {@link EdgeUserClient} is lightweight — create one per request
293
+ * or per user session and discard it when the token changes.
294
+ */
295
+ forUser(accessToken) {
296
+ if (!accessToken) {
297
+ throw new import_connect2.EdgeAuthenticationError("accessToken is required when calling forUser()");
298
+ }
299
+ return new EdgeUserClient(this, accessToken);
300
+ }
165
301
  async exchangeCode(code, codeVerifier, redirectUri) {
166
302
  const tokenUrl = `${this.oauthBaseUrl}/token`;
167
303
  const body = {
@@ -188,8 +324,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
188
324
  const data = await response.json();
189
325
  return this.parseTokenResponse(data);
190
326
  } catch (error) {
191
- if (error instanceof import_connect.EdgeError) throw error;
192
- throw new import_connect.EdgeNetworkError("Failed to exchange code", error);
327
+ if (error instanceof import_connect2.EdgeError) throw error;
328
+ throw new import_connect2.EdgeNetworkError("Failed to exchange code", error);
193
329
  }
194
330
  }
195
331
  async refreshTokens(refreshToken) {
@@ -211,7 +347,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
211
347
  });
212
348
  if (!response.ok) {
213
349
  const error = await response.json().catch(() => ({}));
214
- throw new import_connect.EdgeAuthenticationError(
350
+ throw new import_connect2.EdgeAuthenticationError(
215
351
  error.message || error.error_description || "Token refresh failed. User may need to reconnect.",
216
352
  { tokenError: error }
217
353
  );
@@ -219,89 +355,12 @@ var EdgeConnectServer = class _EdgeConnectServer {
219
355
  const data = await response.json();
220
356
  return this.parseTokenResponse(data, refreshToken);
221
357
  } 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);
358
+ if (error instanceof import_connect2.EdgeError) throw error;
359
+ throw new import_connect2.EdgeNetworkError("Failed to refresh tokens", error);
298
360
  }
299
- return baseDelayMs * (attempt + 1);
300
361
  }
301
- async sleep(ms) {
302
- return new Promise((resolve) => setTimeout(resolve, ms));
303
- }
304
- async apiRequest(method, path, accessToken, body) {
362
+ /** @internal Called by {@link EdgeUserClient} — not part of the public API. */
363
+ async _apiRequest(method, path, accessToken, body) {
305
364
  const url = `${this.apiBaseUrl}${path}`;
306
365
  let lastError = null;
307
366
  const mleConfig = this.config.mle;
@@ -335,14 +394,14 @@ var EdgeConnectServer = class _EdgeConnectServer {
335
394
  if (mleEnabled && typeof rawResponseBody?.jwe === "string") {
336
395
  responseBody = decryptMle(rawResponseBody.jwe, mleConfig);
337
396
  } else if (!mleEnabled && typeof rawResponseBody?.jwe === "string") {
338
- throw new import_connect.EdgeApiError(
397
+ throw new import_connect2.EdgeApiError(
339
398
  "mle_required",
340
399
  "The API responded with message-level encryption. Enable MLE in SDK config.",
341
400
  response.status,
342
401
  rawResponseBody
343
402
  );
344
403
  } else if (mleEnabled && mleConfig?.strictResponseEncryption !== false && response.ok && typeof rawResponseBody?.jwe !== "string") {
345
- throw new import_connect.EdgeApiError(
404
+ throw new import_connect2.EdgeApiError(
346
405
  "mle_response_missing",
347
406
  "Expected encrypted response payload but received plaintext.",
348
407
  response.status,
@@ -360,8 +419,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
360
419
  }
361
420
  return responseBody;
362
421
  } catch (error) {
363
- if (error instanceof import_connect.EdgeError) {
364
- if (!(error instanceof import_connect.EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
422
+ if (error instanceof import_connect2.EdgeError) {
423
+ if (!(error instanceof import_connect2.EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
365
424
  throw error;
366
425
  }
367
426
  lastError = error;
@@ -369,11 +428,24 @@ var EdgeConnectServer = class _EdgeConnectServer {
369
428
  }
370
429
  lastError = error;
371
430
  if (attempt >= this.retryConfig.maxRetries) {
372
- throw new import_connect.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
431
+ throw new import_connect2.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
373
432
  }
374
433
  }
375
434
  }
376
- throw new import_connect.EdgeNetworkError(`API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`, lastError);
435
+ throw new import_connect2.EdgeNetworkError(
436
+ `API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`,
437
+ lastError
438
+ );
439
+ }
440
+ getRetryDelay(attempt) {
441
+ const { backoff, baseDelayMs } = this.retryConfig;
442
+ if (backoff === "exponential") {
443
+ return baseDelayMs * Math.pow(2, attempt);
444
+ }
445
+ return baseDelayMs * (attempt + 1);
446
+ }
447
+ async sleep(ms) {
448
+ return new Promise((resolve) => setTimeout(resolve, ms));
377
449
  }
378
450
  async fetchWithTimeout(url, options) {
379
451
  const controller = new AbortController();
@@ -401,56 +473,67 @@ var EdgeConnectServer = class _EdgeConnectServer {
401
473
  const errorCode = error.error;
402
474
  const errorMessage = error.message || error.error_description;
403
475
  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
- );
476
+ return new import_connect2.EdgeTokenExchangeError("Authorization code is invalid, expired, or already used. Please try again.", {
477
+ edgeBoostError: error
478
+ });
408
479
  }
409
480
  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
- );
481
+ return new import_connect2.EdgeAuthenticationError("Invalid client credentials. Check your client ID and secret.", {
482
+ edgeBoostError: error
483
+ });
414
484
  }
415
485
  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
- );
486
+ return new import_connect2.EdgeTokenExchangeError("PKCE verification failed. Please try again.", { edgeBoostError: error });
420
487
  }
421
- return new import_connect.EdgeTokenExchangeError(
422
- errorMessage || "Failed to exchange authorization code",
423
- { edgeBoostError: error, statusCode: status }
424
- );
488
+ return new import_connect2.EdgeTokenExchangeError(errorMessage || "Failed to exchange authorization code", {
489
+ edgeBoostError: error,
490
+ statusCode: status
491
+ });
425
492
  }
426
493
  async handleApiErrorFromBody(error, status, path) {
427
494
  if (status === 401) {
428
- return new import_connect.EdgeAuthenticationError(
429
- error.message || "Access token is invalid or expired",
430
- error
431
- );
495
+ return new import_connect2.EdgeAuthenticationError(error.message || "Access token is invalid or expired", error);
432
496
  }
433
497
  if (status === 403) {
434
498
  if (error.error === "consent_required") {
435
- return new import_connect.EdgeConsentRequiredError(
499
+ return new import_connect2.EdgeConsentRequiredError(
436
500
  this.config.clientId,
437
501
  error.consentUrl,
438
502
  error.message
439
503
  );
440
504
  }
441
505
  if (error.error === "insufficient_scope" || error.error === "insufficient_consent") {
442
- return new import_connect.EdgeInsufficientScopeError(
506
+ return new import_connect2.EdgeInsufficientScopeError(
443
507
  error.missing_scopes || error.missingScopes || [],
444
508
  error.message
445
509
  );
446
510
  }
447
511
  }
448
512
  if (status === 404) {
449
- const resourceType = path.includes("/transfer") ? "Transfer" : "Resource";
450
- const resourceId = path.split("/").pop() || "unknown";
451
- return new import_connect.EdgeNotFoundError(resourceType, resourceId);
513
+ let resourceType = "Resource";
514
+ let resourceId = path.split("/").pop() || "unknown";
515
+ if (path.includes("/verification-session/")) {
516
+ resourceType = "VerificationSession";
517
+ const parts = path.split("/");
518
+ const vsIdx = parts.indexOf("verification-session");
519
+ resourceId = vsIdx >= 0 && parts[vsIdx + 1] ? parts[vsIdx + 1] : "unknown";
520
+ } else if (path.includes("/verification-session")) {
521
+ resourceType = "Transfer";
522
+ const parts = path.split("/");
523
+ const txIdx = parts.indexOf("transfer");
524
+ resourceId = txIdx >= 0 && parts[txIdx + 1] ? parts[txIdx + 1] : "unknown";
525
+ } else if (path.includes("/transfer")) {
526
+ resourceType = "Transfer";
527
+ }
528
+ return new import_connect2.EdgeNotFoundError(resourceType, resourceId);
452
529
  }
453
- return new import_connect.EdgeApiError(
530
+ if (status === 422 && error.error === "identity_verification_failed") {
531
+ return new import_connect2.EdgeIdentityVerificationError(
532
+ error.fieldErrors || {},
533
+ error.message
534
+ );
535
+ }
536
+ return new import_connect2.EdgeApiError(
454
537
  error.error || "api_error",
455
538
  error.message || error.error_description || `Request failed with status ${status}`,
456
539
  status,
@@ -460,8 +543,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
460
543
  };
461
544
 
462
545
  // src/index.ts
463
- var import_connect2 = require("@edge-markets/connect");
464
546
  var import_connect3 = require("@edge-markets/connect");
547
+ var import_connect4 = require("@edge-markets/connect");
465
548
  // Annotate the CommonJS export names for ESM import in node:
466
549
  0 && (module.exports = {
467
550
  EdgeApiError,
@@ -473,6 +556,7 @@ var import_connect3 = require("@edge-markets/connect");
473
556
  EdgeNetworkError,
474
557
  EdgeNotFoundError,
475
558
  EdgeTokenExchangeError,
559
+ EdgeUserClient,
476
560
  getEnvironmentConfig,
477
561
  isApiError,
478
562
  isAuthenticationError,
package/dist/index.mjs CHANGED
@@ -1,17 +1,138 @@
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
+ * Creates an EDGE-hosted transfer verification session.
112
+ * Returns a `verificationUrl` to open in an iframe or popup.
113
+ * The handoff token is single-use and expires in 120 seconds.
114
+ */
115
+ async createVerificationSession(transferId, options) {
116
+ return this.server._apiRequest(
117
+ "POST",
118
+ `/transfer/${encodeURIComponent(transferId)}/verification-session`,
119
+ this.accessToken,
120
+ { origin: options.origin }
121
+ );
122
+ }
123
+ /**
124
+ * Polls the status of a verification session.
125
+ * Use as an alternative to postMessage events.
126
+ */
127
+ async getVerificationSessionStatus(transferId, sessionId) {
128
+ return this.server._apiRequest(
129
+ "GET",
130
+ `/transfer/${encodeURIComponent(transferId)}/verification-session/${encodeURIComponent(sessionId)}`,
131
+ this.accessToken
132
+ );
133
+ }
134
+ };
135
+
15
136
  // src/mle.ts
16
137
  import { randomUUID, randomBytes, createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from "crypto";
17
138
  function encryptMle(payload, clientId, config) {
@@ -87,7 +208,7 @@ function fromBase64Url(value) {
87
208
  return Buffer.from(normalized, "base64");
88
209
  }
89
210
 
90
- // src/edge-connect-server.ts
211
+ // src/types.ts
91
212
  var DEFAULT_TIMEOUT = 3e4;
92
213
  var USER_AGENT = "@edge-markets/connect-node/1.0.0";
93
214
  var DEFAULT_RETRY_CONFIG = {
@@ -96,6 +217,8 @@ var DEFAULT_RETRY_CONFIG = {
96
217
  backoff: "exponential",
97
218
  baseDelayMs: 1e3
98
219
  };
220
+
221
+ // src/edge-connect-server.ts
99
222
  var instances = /* @__PURE__ */ new Map();
100
223
  function getInstanceKey(config) {
101
224
  return `${config.clientId}:${config.environment}`;
@@ -132,6 +255,18 @@ var EdgeConnectServer = class _EdgeConnectServer {
132
255
  ...config.retry
133
256
  };
134
257
  }
258
+ /**
259
+ * Create a user-scoped client for making authenticated API calls.
260
+ *
261
+ * The returned {@link EdgeUserClient} is lightweight — create one per request
262
+ * or per user session and discard it when the token changes.
263
+ */
264
+ forUser(accessToken) {
265
+ if (!accessToken) {
266
+ throw new EdgeAuthenticationError("accessToken is required when calling forUser()");
267
+ }
268
+ return new EdgeUserClient(this, accessToken);
269
+ }
135
270
  async exchangeCode(code, codeVerifier, redirectUri) {
136
271
  const tokenUrl = `${this.oauthBaseUrl}/token`;
137
272
  const body = {
@@ -193,85 +328,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
193
328
  throw new EdgeNetworkError("Failed to refresh tokens", error);
194
329
  }
195
330
  }
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) {
331
+ /** @internal Called by {@link EdgeUserClient} — not part of the public API. */
332
+ async _apiRequest(method, path, accessToken, body) {
275
333
  const url = `${this.apiBaseUrl}${path}`;
276
334
  let lastError = null;
277
335
  const mleConfig = this.config.mle;
@@ -343,7 +401,20 @@ var EdgeConnectServer = class _EdgeConnectServer {
343
401
  }
344
402
  }
345
403
  }
346
- throw new EdgeNetworkError(`API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`, lastError);
404
+ throw new EdgeNetworkError(
405
+ `API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`,
406
+ lastError
407
+ );
408
+ }
409
+ getRetryDelay(attempt) {
410
+ const { backoff, baseDelayMs } = this.retryConfig;
411
+ if (backoff === "exponential") {
412
+ return baseDelayMs * Math.pow(2, attempt);
413
+ }
414
+ return baseDelayMs * (attempt + 1);
415
+ }
416
+ async sleep(ms) {
417
+ return new Promise((resolve) => setTimeout(resolve, ms));
347
418
  }
348
419
  async fetchWithTimeout(url, options) {
349
420
  const controller = new AbortController();
@@ -371,34 +442,26 @@ var EdgeConnectServer = class _EdgeConnectServer {
371
442
  const errorCode = error.error;
372
443
  const errorMessage = error.message || error.error_description;
373
444
  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
- );
445
+ return new EdgeTokenExchangeError("Authorization code is invalid, expired, or already used. Please try again.", {
446
+ edgeBoostError: error
447
+ });
378
448
  }
379
449
  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
- );
450
+ return new EdgeAuthenticationError("Invalid client credentials. Check your client ID and secret.", {
451
+ edgeBoostError: error
452
+ });
384
453
  }
385
454
  if (errorMessage?.includes("code_verifier") || errorMessage?.includes("PKCE")) {
386
- return new EdgeTokenExchangeError(
387
- "PKCE verification failed. Please try again.",
388
- { edgeBoostError: error }
389
- );
455
+ return new EdgeTokenExchangeError("PKCE verification failed. Please try again.", { edgeBoostError: error });
390
456
  }
391
- return new EdgeTokenExchangeError(
392
- errorMessage || "Failed to exchange authorization code",
393
- { edgeBoostError: error, statusCode: status }
394
- );
457
+ return new EdgeTokenExchangeError(errorMessage || "Failed to exchange authorization code", {
458
+ edgeBoostError: error,
459
+ statusCode: status
460
+ });
395
461
  }
396
462
  async handleApiErrorFromBody(error, status, path) {
397
463
  if (status === 401) {
398
- return new EdgeAuthenticationError(
399
- error.message || "Access token is invalid or expired",
400
- error
401
- );
464
+ return new EdgeAuthenticationError(error.message || "Access token is invalid or expired", error);
402
465
  }
403
466
  if (status === 403) {
404
467
  if (error.error === "consent_required") {
@@ -416,10 +479,29 @@ var EdgeConnectServer = class _EdgeConnectServer {
416
479
  }
417
480
  }
418
481
  if (status === 404) {
419
- const resourceType = path.includes("/transfer") ? "Transfer" : "Resource";
420
- const resourceId = path.split("/").pop() || "unknown";
482
+ let resourceType = "Resource";
483
+ let resourceId = path.split("/").pop() || "unknown";
484
+ if (path.includes("/verification-session/")) {
485
+ resourceType = "VerificationSession";
486
+ const parts = path.split("/");
487
+ const vsIdx = parts.indexOf("verification-session");
488
+ resourceId = vsIdx >= 0 && parts[vsIdx + 1] ? parts[vsIdx + 1] : "unknown";
489
+ } else if (path.includes("/verification-session")) {
490
+ resourceType = "Transfer";
491
+ const parts = path.split("/");
492
+ const txIdx = parts.indexOf("transfer");
493
+ resourceId = txIdx >= 0 && parts[txIdx + 1] ? parts[txIdx + 1] : "unknown";
494
+ } else if (path.includes("/transfer")) {
495
+ resourceType = "Transfer";
496
+ }
421
497
  return new EdgeNotFoundError(resourceType, resourceId);
422
498
  }
499
+ if (status === 422 && error.error === "identity_verification_failed") {
500
+ return new EdgeIdentityVerificationError(
501
+ error.fieldErrors || {},
502
+ error.message
503
+ );
504
+ }
423
505
  return new EdgeApiError(
424
506
  error.error || "api_error",
425
507
  error.message || error.error_description || `Request failed with status ${status}`,
@@ -431,24 +513,21 @@ var EdgeConnectServer = class _EdgeConnectServer {
431
513
 
432
514
  // src/index.ts
433
515
  import {
434
- EdgeError as EdgeError2,
516
+ EdgeApiError as EdgeApiError2,
435
517
  EdgeAuthenticationError as EdgeAuthenticationError2,
436
- EdgeTokenExchangeError as EdgeTokenExchangeError2,
437
518
  EdgeConsentRequiredError as EdgeConsentRequiredError2,
519
+ EdgeError as EdgeError2,
438
520
  EdgeInsufficientScopeError as EdgeInsufficientScopeError2,
439
- EdgeApiError as EdgeApiError2,
440
- EdgeNotFoundError as EdgeNotFoundError2,
441
521
  EdgeNetworkError as EdgeNetworkError2,
442
- isEdgeError,
522
+ EdgeNotFoundError as EdgeNotFoundError2,
523
+ EdgeTokenExchangeError as EdgeTokenExchangeError2,
524
+ isApiError,
443
525
  isAuthenticationError,
444
526
  isConsentRequiredError,
445
- isApiError,
527
+ isEdgeError,
446
528
  isNetworkError
447
529
  } from "@edge-markets/connect";
448
- import {
449
- getEnvironmentConfig as getEnvironmentConfig2,
450
- isProductionEnvironment
451
- } from "@edge-markets/connect";
530
+ import { getEnvironmentConfig as getEnvironmentConfig2, isProductionEnvironment } from "@edge-markets/connect";
452
531
  export {
453
532
  EdgeApiError2 as EdgeApiError,
454
533
  EdgeAuthenticationError2 as EdgeAuthenticationError,
@@ -459,6 +538,7 @@ export {
459
538
  EdgeNetworkError2 as EdgeNetworkError,
460
539
  EdgeNotFoundError2 as EdgeNotFoundError,
461
540
  EdgeTokenExchangeError2 as EdgeTokenExchangeError,
541
+ EdgeUserClient,
462
542
  getEnvironmentConfig2 as getEnvironmentConfig,
463
543
  isApiError,
464
544
  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.5.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.4.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "tsup": "^8.0.0",