@edge-markets/connect-node 1.2.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/README.md CHANGED
@@ -58,9 +58,38 @@ interface EdgeConnectServerConfig {
58
58
  apiBaseUrl?: string // Custom API URL (dev only)
59
59
  authDomain?: string // Custom Cognito domain (dev only)
60
60
  timeout?: number // Request timeout (default: 30000ms)
61
+
62
+ // Optional: Message Level Encryption (Connect endpoints only)
63
+ mle?: {
64
+ enabled: boolean
65
+ edgePublicKey: string // EDGE public encryption key (PEM)
66
+ edgeKeyId: string // EDGE key ID (kid) used for requests
67
+ partnerPrivateKey: string // Your private key (PEM) to decrypt responses
68
+ partnerKeyId: string // Your key ID (kid) expected in response headers
69
+ strictResponseEncryption?: boolean // default true
70
+ }
61
71
  }
62
72
  ```
63
73
 
74
+ ### Message Level Encryption (MLE)
75
+
76
+ ```typescript
77
+ const edge = new EdgeConnectServer({
78
+ clientId: process.env.EDGE_CLIENT_ID!,
79
+ clientSecret: process.env.EDGE_CLIENT_SECRET!,
80
+ environment: 'staging',
81
+ mle: {
82
+ enabled: true,
83
+ edgePublicKey: process.env.EDGE_MLE_EDGE_PUBLIC_KEY!,
84
+ edgeKeyId: process.env.EDGE_MLE_EDGE_KEY_ID!,
85
+ partnerPrivateKey: process.env.EDGE_MLE_PARTNER_PRIVATE_KEY!,
86
+ partnerKeyId: process.env.EDGE_MLE_PARTNER_KEY_ID!,
87
+ },
88
+ })
89
+ ```
90
+
91
+ When enabled, the SDK sends `X-Edge-MLE: v1`, encrypts request bodies, and decrypts encrypted Connect responses.
92
+
64
93
  ## Token Exchange
65
94
 
66
95
  After EdgeLink completes, exchange the code for tokens:
@@ -317,4 +346,3 @@ MIT
317
346
 
318
347
 
319
348
 
320
-
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 {
@@ -32,12 +32,43 @@ interface EdgeConnectServerConfig {
32
32
  retry?: RetryConfig;
33
33
  onRequest?: (info: RequestInfo) => void;
34
34
  onResponse?: (info: ResponseInfo) => void;
35
+ mle?: {
36
+ enabled: boolean;
37
+ edgePublicKey: string;
38
+ edgeKeyId: string;
39
+ partnerPrivateKey: string;
40
+ partnerKeyId: string;
41
+ strictResponseEncryption?: boolean;
42
+ };
35
43
  }
36
44
  interface TransferOptions {
37
45
  type: 'debit' | 'credit';
38
46
  amount: string;
39
47
  idempotencyKey: string;
40
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
+
41
72
  declare class EdgeConnectServer {
42
73
  private readonly config;
43
74
  private readonly apiBaseUrl;
@@ -47,25 +78,23 @@ declare class EdgeConnectServer {
47
78
  static getInstance(config: EdgeConnectServerConfig): EdgeConnectServer;
48
79
  static clearInstances(): void;
49
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;
50
88
  exchangeCode(code: string, codeVerifier: string, redirectUri?: string): Promise<EdgeTokens>;
51
89
  refreshTokens(refreshToken: string): Promise<EdgeTokens>;
52
- getUser(accessToken: string): Promise<User>;
53
- getBalance(accessToken: string): Promise<Balance>;
54
- initiateTransfer(accessToken: string, options: TransferOptions): Promise<Transfer>;
55
- private validateTransferOptions;
56
- verifyTransfer(accessToken: string, transferId: string, otp: string): Promise<Transfer>;
57
- getTransfer(accessToken: string, transferId: string): Promise<Transfer>;
58
- listTransfers(accessToken: string, params?: ListTransfersParams): Promise<TransferList>;
59
- revokeConsent(accessToken: string): Promise<{
60
- revoked: boolean;
61
- }>;
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>;
62
92
  private getRetryDelay;
63
93
  private sleep;
64
- private apiRequest;
65
94
  private fetchWithTimeout;
66
95
  private parseTokenResponse;
67
96
  private handleTokenError;
68
97
  private handleApiErrorFromBody;
69
98
  }
70
99
 
71
- 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 {
@@ -32,12 +32,43 @@ interface EdgeConnectServerConfig {
32
32
  retry?: RetryConfig;
33
33
  onRequest?: (info: RequestInfo) => void;
34
34
  onResponse?: (info: ResponseInfo) => void;
35
+ mle?: {
36
+ enabled: boolean;
37
+ edgePublicKey: string;
38
+ edgeKeyId: string;
39
+ partnerPrivateKey: string;
40
+ partnerKeyId: string;
41
+ strictResponseEncryption?: boolean;
42
+ };
35
43
  }
36
44
  interface TransferOptions {
37
45
  type: 'debit' | 'credit';
38
46
  amount: string;
39
47
  idempotencyKey: string;
40
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
+
41
72
  declare class EdgeConnectServer {
42
73
  private readonly config;
43
74
  private readonly apiBaseUrl;
@@ -47,25 +78,23 @@ declare class EdgeConnectServer {
47
78
  static getInstance(config: EdgeConnectServerConfig): EdgeConnectServer;
48
79
  static clearInstances(): void;
49
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;
50
88
  exchangeCode(code: string, codeVerifier: string, redirectUri?: string): Promise<EdgeTokens>;
51
89
  refreshTokens(refreshToken: string): Promise<EdgeTokens>;
52
- getUser(accessToken: string): Promise<User>;
53
- getBalance(accessToken: string): Promise<Balance>;
54
- initiateTransfer(accessToken: string, options: TransferOptions): Promise<Transfer>;
55
- private validateTransferOptions;
56
- verifyTransfer(accessToken: string, transferId: string, otp: string): Promise<Transfer>;
57
- getTransfer(accessToken: string, transferId: string): Promise<Transfer>;
58
- listTransfers(accessToken: string, params?: ListTransfersParams): Promise<TransferList>;
59
- revokeConsent(accessToken: string): Promise<{
60
- revoked: boolean;
61
- }>;
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>;
62
92
  private getRetryDelay;
63
93
  private sleep;
64
- private apiRequest;
65
94
  private fetchWithTimeout;
66
95
  private parseTokenResponse;
67
96
  private handleTokenError;
68
97
  private handleApiErrorFromBody;
69
98
  }
70
99
 
71
- 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,202 @@ 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
+
143
+ // src/mle.ts
144
+ var import_crypto = require("crypto");
145
+ function encryptMle(payload, clientId, config) {
146
+ const header = {
147
+ alg: "RSA-OAEP-256",
148
+ enc: "A256GCM",
149
+ kid: config.edgeKeyId,
150
+ typ: "application/json",
151
+ iat: Math.floor(Date.now() / 1e3),
152
+ jti: (0, import_crypto.randomUUID)(),
153
+ clientId
154
+ };
155
+ const protectedHeaderB64 = toBase64Url(Buffer.from(JSON.stringify(header), "utf8"));
156
+ const aad = Buffer.from(protectedHeaderB64, "utf8");
157
+ const cek = (0, import_crypto.randomBytes)(32);
158
+ const iv = (0, import_crypto.randomBytes)(12);
159
+ const cipher = (0, import_crypto.createCipheriv)("aes-256-gcm", cek, iv);
160
+ cipher.setAAD(aad);
161
+ const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
162
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
163
+ const tag = cipher.getAuthTag();
164
+ const encryptedKey = (0, import_crypto.publicEncrypt)(
165
+ {
166
+ key: config.edgePublicKey,
167
+ padding: import_crypto.constants.RSA_PKCS1_OAEP_PADDING,
168
+ oaepHash: "sha256"
169
+ },
170
+ cek
171
+ );
172
+ return [
173
+ protectedHeaderB64,
174
+ toBase64Url(encryptedKey),
175
+ toBase64Url(iv),
176
+ toBase64Url(ciphertext),
177
+ toBase64Url(tag)
178
+ ].join(".");
179
+ }
180
+ function decryptMle(jwe, config) {
181
+ const parts = jwe.split(".");
182
+ if (parts.length !== 5) {
183
+ throw new Error("Invalid MLE payload format");
184
+ }
185
+ const [protectedHeaderB64, encryptedKeyB64, ivB64, ciphertextB64, tagB64] = parts;
186
+ const protectedHeader = JSON.parse(fromBase64Url(protectedHeaderB64).toString("utf8"));
187
+ if (protectedHeader.alg !== "RSA-OAEP-256" || protectedHeader.enc !== "A256GCM") {
188
+ throw new Error("Unsupported MLE algorithms");
189
+ }
190
+ if (protectedHeader.kid && protectedHeader.kid !== config.partnerKeyId) {
191
+ throw new Error(`Unexpected response key id: ${protectedHeader.kid}`);
192
+ }
193
+ const cek = (0, import_crypto.privateDecrypt)(
194
+ {
195
+ key: config.partnerPrivateKey,
196
+ padding: import_crypto.constants.RSA_PKCS1_OAEP_PADDING,
197
+ oaepHash: "sha256"
198
+ },
199
+ fromBase64Url(encryptedKeyB64)
200
+ );
201
+ const decipher = (0, import_crypto.createDecipheriv)("aes-256-gcm", cek, fromBase64Url(ivB64));
202
+ decipher.setAAD(Buffer.from(protectedHeaderB64, "utf8"));
203
+ decipher.setAuthTag(fromBase64Url(tagB64));
204
+ const plaintext = Buffer.concat([
205
+ decipher.update(fromBase64Url(ciphertextB64)),
206
+ decipher.final()
207
+ ]);
208
+ return JSON.parse(plaintext.toString("utf8"));
209
+ }
210
+ function toBase64Url(buffer) {
211
+ return buffer.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
212
+ }
213
+ function fromBase64Url(value) {
214
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
215
+ return Buffer.from(normalized, "base64");
216
+ }
217
+
218
+ // src/types.ts
44
219
  var DEFAULT_TIMEOUT = 3e4;
45
220
  var USER_AGENT = "@edge-markets/connect-node/1.0.0";
46
221
  var DEFAULT_RETRY_CONFIG = {
@@ -49,6 +224,8 @@ var DEFAULT_RETRY_CONFIG = {
49
224
  backoff: "exponential",
50
225
  baseDelayMs: 1e3
51
226
  };
227
+
228
+ // src/edge-connect-server.ts
52
229
  var instances = /* @__PURE__ */ new Map();
53
230
  function getInstanceKey(config) {
54
231
  return `${config.clientId}:${config.environment}`;
@@ -76,7 +253,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
76
253
  throw new Error("EdgeConnectServer: environment is required");
77
254
  }
78
255
  this.config = config;
79
- const envConfig = (0, import_connect.getEnvironmentConfig)(config.environment);
256
+ const envConfig = (0, import_connect2.getEnvironmentConfig)(config.environment);
80
257
  this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
81
258
  this.oauthBaseUrl = config.oauthBaseUrl || envConfig.oauthBaseUrl;
82
259
  this.timeout = config.timeout || DEFAULT_TIMEOUT;
@@ -85,6 +262,18 @@ var EdgeConnectServer = class _EdgeConnectServer {
85
262
  ...config.retry
86
263
  };
87
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
+ }
88
277
  async exchangeCode(code, codeVerifier, redirectUri) {
89
278
  const tokenUrl = `${this.oauthBaseUrl}/token`;
90
279
  const body = {
@@ -111,8 +300,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
111
300
  const data = await response.json();
112
301
  return this.parseTokenResponse(data);
113
302
  } catch (error) {
114
- if (error instanceof import_connect.EdgeError) throw error;
115
- 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);
116
305
  }
117
306
  }
118
307
  async refreshTokens(refreshToken) {
@@ -134,7 +323,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
134
323
  });
135
324
  if (!response.ok) {
136
325
  const error = await response.json().catch(() => ({}));
137
- throw new import_connect.EdgeAuthenticationError(
326
+ throw new import_connect2.EdgeAuthenticationError(
138
327
  error.message || error.error_description || "Token refresh failed. User may need to reconnect.",
139
328
  { tokenError: error }
140
329
  );
@@ -142,91 +331,16 @@ var EdgeConnectServer = class _EdgeConnectServer {
142
331
  const data = await response.json();
143
332
  return this.parseTokenResponse(data, refreshToken);
144
333
  } catch (error) {
145
- if (error instanceof import_connect.EdgeError) throw error;
146
- throw new import_connect.EdgeNetworkError("Failed to refresh tokens", error);
334
+ if (error instanceof import_connect2.EdgeError) throw error;
335
+ throw new import_connect2.EdgeNetworkError("Failed to refresh tokens", error);
147
336
  }
148
337
  }
149
- async getUser(accessToken) {
150
- return this.apiRequest("GET", "/user", accessToken);
151
- }
152
- async getBalance(accessToken) {
153
- return this.apiRequest("GET", "/balance", accessToken);
154
- }
155
- async initiateTransfer(accessToken, options) {
156
- this.validateTransferOptions(options);
157
- const body = {
158
- type: options.type,
159
- amount: options.amount,
160
- idempotencyKey: options.idempotencyKey
161
- };
162
- return this.apiRequest("POST", "/transfer", accessToken, body);
163
- }
164
- validateTransferOptions(options) {
165
- const errors = {};
166
- if (!options.type || !["debit", "credit"].includes(options.type)) {
167
- errors.type = ['Must be "debit" or "credit"'];
168
- }
169
- if (!options.amount) {
170
- errors.amount = ["Amount is required"];
171
- } else {
172
- const amount = parseFloat(options.amount);
173
- if (isNaN(amount)) {
174
- errors.amount = ["Must be a valid number"];
175
- } else if (amount <= 0) {
176
- errors.amount = ["Must be greater than 0"];
177
- } else if (!/^\d+(\.\d{1,2})?$/.test(options.amount)) {
178
- errors.amount = ["Must have at most 2 decimal places"];
179
- }
180
- }
181
- if (!options.idempotencyKey || options.idempotencyKey.trim() === "") {
182
- errors.idempotencyKey = ["idempotencyKey is required"];
183
- } else if (options.idempotencyKey.length > 255) {
184
- errors.idempotencyKey = ["Must be 255 characters or less"];
185
- }
186
- if (Object.keys(errors).length > 0) {
187
- throw new import_connect.EdgeValidationError("Invalid transfer options", errors);
188
- }
189
- }
190
- async verifyTransfer(accessToken, transferId, otp) {
191
- return this.apiRequest(
192
- "POST",
193
- `/transfer/${encodeURIComponent(transferId)}/verify`,
194
- accessToken,
195
- { otp }
196
- );
197
- }
198
- async getTransfer(accessToken, transferId) {
199
- return this.apiRequest(
200
- "GET",
201
- `/transfer/${encodeURIComponent(transferId)}`,
202
- accessToken
203
- );
204
- }
205
- async listTransfers(accessToken, params) {
206
- const queryParams = new URLSearchParams();
207
- if (params?.limit) queryParams.set("limit", String(params.limit));
208
- if (params?.offset) queryParams.set("offset", String(params.offset));
209
- if (params?.status) queryParams.set("status", params.status);
210
- const query = queryParams.toString();
211
- const path = query ? `/transfers?${query}` : "/transfers";
212
- return this.apiRequest("GET", path, accessToken);
213
- }
214
- async revokeConsent(accessToken) {
215
- return this.apiRequest("DELETE", "/consent", accessToken);
216
- }
217
- getRetryDelay(attempt) {
218
- const { backoff, baseDelayMs } = this.retryConfig;
219
- if (backoff === "exponential") {
220
- return baseDelayMs * Math.pow(2, attempt);
221
- }
222
- return baseDelayMs * (attempt + 1);
223
- }
224
- async sleep(ms) {
225
- return new Promise((resolve) => setTimeout(resolve, ms));
226
- }
227
- 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) {
228
340
  const url = `${this.apiBaseUrl}${path}`;
229
341
  let lastError = null;
342
+ const mleConfig = this.config.mle;
343
+ const mleEnabled = !!mleConfig?.enabled;
230
344
  for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
231
345
  const startTime = Date.now();
232
346
  if (attempt > 0) {
@@ -234,16 +348,42 @@ var EdgeConnectServer = class _EdgeConnectServer {
234
348
  }
235
349
  this.config.onRequest?.({ method, url, body });
236
350
  try {
351
+ const requestHeaders = {
352
+ Authorization: `Bearer ${accessToken}`,
353
+ "Content-Type": "application/json",
354
+ "User-Agent": USER_AGENT
355
+ };
356
+ let requestBody = body;
357
+ if (mleEnabled) {
358
+ requestHeaders["X-Edge-MLE"] = "v1";
359
+ if (body !== void 0) {
360
+ requestBody = { jwe: encryptMle(body, this.config.clientId, mleConfig) };
361
+ }
362
+ }
237
363
  const response = await this.fetchWithTimeout(url, {
238
364
  method,
239
- headers: {
240
- Authorization: `Bearer ${accessToken}`,
241
- "Content-Type": "application/json",
242
- "User-Agent": USER_AGENT
243
- },
244
- body: body ? JSON.stringify(body) : void 0
365
+ headers: requestHeaders,
366
+ body: requestBody !== void 0 ? JSON.stringify(requestBody) : void 0
245
367
  });
246
- const responseBody = await response.json().catch(() => ({}));
368
+ const rawResponseBody = await response.json().catch(() => ({}));
369
+ let responseBody = rawResponseBody;
370
+ if (mleEnabled && typeof rawResponseBody?.jwe === "string") {
371
+ responseBody = decryptMle(rawResponseBody.jwe, mleConfig);
372
+ } else if (!mleEnabled && typeof rawResponseBody?.jwe === "string") {
373
+ throw new import_connect2.EdgeApiError(
374
+ "mle_required",
375
+ "The API responded with message-level encryption. Enable MLE in SDK config.",
376
+ response.status,
377
+ rawResponseBody
378
+ );
379
+ } else if (mleEnabled && mleConfig?.strictResponseEncryption !== false && response.ok && typeof rawResponseBody?.jwe !== "string") {
380
+ throw new import_connect2.EdgeApiError(
381
+ "mle_response_missing",
382
+ "Expected encrypted response payload but received plaintext.",
383
+ response.status,
384
+ rawResponseBody
385
+ );
386
+ }
247
387
  const durationMs = Date.now() - startTime;
248
388
  this.config.onResponse?.({ status: response.status, body: responseBody, durationMs });
249
389
  if (!response.ok) {
@@ -255,8 +395,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
255
395
  }
256
396
  return responseBody;
257
397
  } catch (error) {
258
- if (error instanceof import_connect.EdgeError) {
259
- 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) {
260
400
  throw error;
261
401
  }
262
402
  lastError = error;
@@ -264,11 +404,24 @@ var EdgeConnectServer = class _EdgeConnectServer {
264
404
  }
265
405
  lastError = error;
266
406
  if (attempt >= this.retryConfig.maxRetries) {
267
- throw new import_connect.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
407
+ throw new import_connect2.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
268
408
  }
269
409
  }
270
410
  }
271
- 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));
272
425
  }
273
426
  async fetchWithTimeout(url, options) {
274
427
  const controller = new AbortController();
@@ -296,45 +449,37 @@ var EdgeConnectServer = class _EdgeConnectServer {
296
449
  const errorCode = error.error;
297
450
  const errorMessage = error.message || error.error_description;
298
451
  if (errorCode === "invalid_grant" || errorMessage?.includes("Invalid or expired")) {
299
- return new import_connect.EdgeTokenExchangeError(
300
- "Authorization code is invalid, expired, or already used. Please try again.",
301
- { edgeBoostError: error }
302
- );
452
+ return new import_connect2.EdgeTokenExchangeError("Authorization code is invalid, expired, or already used. Please try again.", {
453
+ edgeBoostError: error
454
+ });
303
455
  }
304
456
  if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
305
- return new import_connect.EdgeAuthenticationError(
306
- "Invalid client credentials. Check your client ID and secret.",
307
- { edgeBoostError: error }
308
- );
457
+ return new import_connect2.EdgeAuthenticationError("Invalid client credentials. Check your client ID and secret.", {
458
+ edgeBoostError: error
459
+ });
309
460
  }
310
461
  if (errorMessage?.includes("code_verifier") || errorMessage?.includes("PKCE")) {
311
- return new import_connect.EdgeTokenExchangeError(
312
- "PKCE verification failed. Please try again.",
313
- { edgeBoostError: error }
314
- );
462
+ return new import_connect2.EdgeTokenExchangeError("PKCE verification failed. Please try again.", { edgeBoostError: error });
315
463
  }
316
- return new import_connect.EdgeTokenExchangeError(
317
- errorMessage || "Failed to exchange authorization code",
318
- { edgeBoostError: error, statusCode: status }
319
- );
464
+ return new import_connect2.EdgeTokenExchangeError(errorMessage || "Failed to exchange authorization code", {
465
+ edgeBoostError: error,
466
+ statusCode: status
467
+ });
320
468
  }
321
469
  async handleApiErrorFromBody(error, status, path) {
322
470
  if (status === 401) {
323
- return new import_connect.EdgeAuthenticationError(
324
- error.message || "Access token is invalid or expired",
325
- error
326
- );
471
+ return new import_connect2.EdgeAuthenticationError(error.message || "Access token is invalid or expired", error);
327
472
  }
328
473
  if (status === 403) {
329
474
  if (error.error === "consent_required") {
330
- return new import_connect.EdgeConsentRequiredError(
475
+ return new import_connect2.EdgeConsentRequiredError(
331
476
  this.config.clientId,
332
477
  error.consentUrl,
333
478
  error.message
334
479
  );
335
480
  }
336
481
  if (error.error === "insufficient_scope" || error.error === "insufficient_consent") {
337
- return new import_connect.EdgeInsufficientScopeError(
482
+ return new import_connect2.EdgeInsufficientScopeError(
338
483
  error.missing_scopes || error.missingScopes || [],
339
484
  error.message
340
485
  );
@@ -343,9 +488,15 @@ var EdgeConnectServer = class _EdgeConnectServer {
343
488
  if (status === 404) {
344
489
  const resourceType = path.includes("/transfer") ? "Transfer" : "Resource";
345
490
  const resourceId = path.split("/").pop() || "unknown";
346
- return new import_connect.EdgeNotFoundError(resourceType, resourceId);
491
+ return new import_connect2.EdgeNotFoundError(resourceType, resourceId);
347
492
  }
348
- 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(
349
500
  error.error || "api_error",
350
501
  error.message || error.error_description || `Request failed with status ${status}`,
351
502
  status,
@@ -355,8 +506,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
355
506
  };
356
507
 
357
508
  // src/index.ts
358
- var import_connect2 = require("@edge-markets/connect");
359
509
  var import_connect3 = require("@edge-markets/connect");
510
+ var import_connect4 = require("@edge-markets/connect");
360
511
  // Annotate the CommonJS export names for ESM import in node:
361
512
  0 && (module.exports = {
362
513
  EdgeApiError,
@@ -368,6 +519,7 @@ var import_connect3 = require("@edge-markets/connect");
368
519
  EdgeNetworkError,
369
520
  EdgeNotFoundError,
370
521
  EdgeTokenExchangeError,
522
+ EdgeUserClient,
371
523
  getEnvironmentConfig,
372
524
  isApiError,
373
525
  isAuthenticationError,
package/dist/index.mjs CHANGED
@@ -1,16 +1,190 @@
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
+
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
+
112
+ // src/mle.ts
113
+ import { randomUUID, randomBytes, createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from "crypto";
114
+ function encryptMle(payload, clientId, config) {
115
+ const header = {
116
+ alg: "RSA-OAEP-256",
117
+ enc: "A256GCM",
118
+ kid: config.edgeKeyId,
119
+ typ: "application/json",
120
+ iat: Math.floor(Date.now() / 1e3),
121
+ jti: randomUUID(),
122
+ clientId
123
+ };
124
+ const protectedHeaderB64 = toBase64Url(Buffer.from(JSON.stringify(header), "utf8"));
125
+ const aad = Buffer.from(protectedHeaderB64, "utf8");
126
+ const cek = randomBytes(32);
127
+ const iv = randomBytes(12);
128
+ const cipher = createCipheriv("aes-256-gcm", cek, iv);
129
+ cipher.setAAD(aad);
130
+ const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
131
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
132
+ const tag = cipher.getAuthTag();
133
+ const encryptedKey = publicEncrypt(
134
+ {
135
+ key: config.edgePublicKey,
136
+ padding: constants.RSA_PKCS1_OAEP_PADDING,
137
+ oaepHash: "sha256"
138
+ },
139
+ cek
140
+ );
141
+ return [
142
+ protectedHeaderB64,
143
+ toBase64Url(encryptedKey),
144
+ toBase64Url(iv),
145
+ toBase64Url(ciphertext),
146
+ toBase64Url(tag)
147
+ ].join(".");
148
+ }
149
+ function decryptMle(jwe, config) {
150
+ const parts = jwe.split(".");
151
+ if (parts.length !== 5) {
152
+ throw new Error("Invalid MLE payload format");
153
+ }
154
+ const [protectedHeaderB64, encryptedKeyB64, ivB64, ciphertextB64, tagB64] = parts;
155
+ const protectedHeader = JSON.parse(fromBase64Url(protectedHeaderB64).toString("utf8"));
156
+ if (protectedHeader.alg !== "RSA-OAEP-256" || protectedHeader.enc !== "A256GCM") {
157
+ throw new Error("Unsupported MLE algorithms");
158
+ }
159
+ if (protectedHeader.kid && protectedHeader.kid !== config.partnerKeyId) {
160
+ throw new Error(`Unexpected response key id: ${protectedHeader.kid}`);
161
+ }
162
+ const cek = privateDecrypt(
163
+ {
164
+ key: config.partnerPrivateKey,
165
+ padding: constants.RSA_PKCS1_OAEP_PADDING,
166
+ oaepHash: "sha256"
167
+ },
168
+ fromBase64Url(encryptedKeyB64)
169
+ );
170
+ const decipher = createDecipheriv("aes-256-gcm", cek, fromBase64Url(ivB64));
171
+ decipher.setAAD(Buffer.from(protectedHeaderB64, "utf8"));
172
+ decipher.setAuthTag(fromBase64Url(tagB64));
173
+ const plaintext = Buffer.concat([
174
+ decipher.update(fromBase64Url(ciphertextB64)),
175
+ decipher.final()
176
+ ]);
177
+ return JSON.parse(plaintext.toString("utf8"));
178
+ }
179
+ function toBase64Url(buffer) {
180
+ return buffer.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
181
+ }
182
+ function fromBase64Url(value) {
183
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
184
+ return Buffer.from(normalized, "base64");
185
+ }
186
+
187
+ // src/types.ts
14
188
  var DEFAULT_TIMEOUT = 3e4;
15
189
  var USER_AGENT = "@edge-markets/connect-node/1.0.0";
16
190
  var DEFAULT_RETRY_CONFIG = {
@@ -19,6 +193,8 @@ var DEFAULT_RETRY_CONFIG = {
19
193
  backoff: "exponential",
20
194
  baseDelayMs: 1e3
21
195
  };
196
+
197
+ // src/edge-connect-server.ts
22
198
  var instances = /* @__PURE__ */ new Map();
23
199
  function getInstanceKey(config) {
24
200
  return `${config.clientId}:${config.environment}`;
@@ -55,6 +231,18 @@ var EdgeConnectServer = class _EdgeConnectServer {
55
231
  ...config.retry
56
232
  };
57
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
+ }
58
246
  async exchangeCode(code, codeVerifier, redirectUri) {
59
247
  const tokenUrl = `${this.oauthBaseUrl}/token`;
60
248
  const body = {
@@ -116,87 +304,12 @@ var EdgeConnectServer = class _EdgeConnectServer {
116
304
  throw new EdgeNetworkError("Failed to refresh tokens", error);
117
305
  }
118
306
  }
119
- async getUser(accessToken) {
120
- return this.apiRequest("GET", "/user", accessToken);
121
- }
122
- async getBalance(accessToken) {
123
- return this.apiRequest("GET", "/balance", accessToken);
124
- }
125
- async initiateTransfer(accessToken, options) {
126
- this.validateTransferOptions(options);
127
- const body = {
128
- type: options.type,
129
- amount: options.amount,
130
- idempotencyKey: options.idempotencyKey
131
- };
132
- return this.apiRequest("POST", "/transfer", accessToken, body);
133
- }
134
- validateTransferOptions(options) {
135
- const errors = {};
136
- if (!options.type || !["debit", "credit"].includes(options.type)) {
137
- errors.type = ['Must be "debit" or "credit"'];
138
- }
139
- if (!options.amount) {
140
- errors.amount = ["Amount is required"];
141
- } else {
142
- const amount = parseFloat(options.amount);
143
- if (isNaN(amount)) {
144
- errors.amount = ["Must be a valid number"];
145
- } else if (amount <= 0) {
146
- errors.amount = ["Must be greater than 0"];
147
- } else if (!/^\d+(\.\d{1,2})?$/.test(options.amount)) {
148
- errors.amount = ["Must have at most 2 decimal places"];
149
- }
150
- }
151
- if (!options.idempotencyKey || options.idempotencyKey.trim() === "") {
152
- errors.idempotencyKey = ["idempotencyKey is required"];
153
- } else if (options.idempotencyKey.length > 255) {
154
- errors.idempotencyKey = ["Must be 255 characters or less"];
155
- }
156
- if (Object.keys(errors).length > 0) {
157
- throw new EdgeValidationError("Invalid transfer options", errors);
158
- }
159
- }
160
- async verifyTransfer(accessToken, transferId, otp) {
161
- return this.apiRequest(
162
- "POST",
163
- `/transfer/${encodeURIComponent(transferId)}/verify`,
164
- accessToken,
165
- { otp }
166
- );
167
- }
168
- async getTransfer(accessToken, transferId) {
169
- return this.apiRequest(
170
- "GET",
171
- `/transfer/${encodeURIComponent(transferId)}`,
172
- accessToken
173
- );
174
- }
175
- async listTransfers(accessToken, params) {
176
- const queryParams = new URLSearchParams();
177
- if (params?.limit) queryParams.set("limit", String(params.limit));
178
- if (params?.offset) queryParams.set("offset", String(params.offset));
179
- if (params?.status) queryParams.set("status", params.status);
180
- const query = queryParams.toString();
181
- const path = query ? `/transfers?${query}` : "/transfers";
182
- return this.apiRequest("GET", path, accessToken);
183
- }
184
- async revokeConsent(accessToken) {
185
- return this.apiRequest("DELETE", "/consent", accessToken);
186
- }
187
- getRetryDelay(attempt) {
188
- const { backoff, baseDelayMs } = this.retryConfig;
189
- if (backoff === "exponential") {
190
- return baseDelayMs * Math.pow(2, attempt);
191
- }
192
- return baseDelayMs * (attempt + 1);
193
- }
194
- async sleep(ms) {
195
- return new Promise((resolve) => setTimeout(resolve, ms));
196
- }
197
- 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) {
198
309
  const url = `${this.apiBaseUrl}${path}`;
199
310
  let lastError = null;
311
+ const mleConfig = this.config.mle;
312
+ const mleEnabled = !!mleConfig?.enabled;
200
313
  for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
201
314
  const startTime = Date.now();
202
315
  if (attempt > 0) {
@@ -204,16 +317,42 @@ var EdgeConnectServer = class _EdgeConnectServer {
204
317
  }
205
318
  this.config.onRequest?.({ method, url, body });
206
319
  try {
320
+ const requestHeaders = {
321
+ Authorization: `Bearer ${accessToken}`,
322
+ "Content-Type": "application/json",
323
+ "User-Agent": USER_AGENT
324
+ };
325
+ let requestBody = body;
326
+ if (mleEnabled) {
327
+ requestHeaders["X-Edge-MLE"] = "v1";
328
+ if (body !== void 0) {
329
+ requestBody = { jwe: encryptMle(body, this.config.clientId, mleConfig) };
330
+ }
331
+ }
207
332
  const response = await this.fetchWithTimeout(url, {
208
333
  method,
209
- headers: {
210
- Authorization: `Bearer ${accessToken}`,
211
- "Content-Type": "application/json",
212
- "User-Agent": USER_AGENT
213
- },
214
- body: body ? JSON.stringify(body) : void 0
334
+ headers: requestHeaders,
335
+ body: requestBody !== void 0 ? JSON.stringify(requestBody) : void 0
215
336
  });
216
- const responseBody = await response.json().catch(() => ({}));
337
+ const rawResponseBody = await response.json().catch(() => ({}));
338
+ let responseBody = rawResponseBody;
339
+ if (mleEnabled && typeof rawResponseBody?.jwe === "string") {
340
+ responseBody = decryptMle(rawResponseBody.jwe, mleConfig);
341
+ } else if (!mleEnabled && typeof rawResponseBody?.jwe === "string") {
342
+ throw new EdgeApiError(
343
+ "mle_required",
344
+ "The API responded with message-level encryption. Enable MLE in SDK config.",
345
+ response.status,
346
+ rawResponseBody
347
+ );
348
+ } else if (mleEnabled && mleConfig?.strictResponseEncryption !== false && response.ok && typeof rawResponseBody?.jwe !== "string") {
349
+ throw new EdgeApiError(
350
+ "mle_response_missing",
351
+ "Expected encrypted response payload but received plaintext.",
352
+ response.status,
353
+ rawResponseBody
354
+ );
355
+ }
217
356
  const durationMs = Date.now() - startTime;
218
357
  this.config.onResponse?.({ status: response.status, body: responseBody, durationMs });
219
358
  if (!response.ok) {
@@ -238,7 +377,20 @@ var EdgeConnectServer = class _EdgeConnectServer {
238
377
  }
239
378
  }
240
379
  }
241
- 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));
242
394
  }
243
395
  async fetchWithTimeout(url, options) {
244
396
  const controller = new AbortController();
@@ -266,34 +418,26 @@ var EdgeConnectServer = class _EdgeConnectServer {
266
418
  const errorCode = error.error;
267
419
  const errorMessage = error.message || error.error_description;
268
420
  if (errorCode === "invalid_grant" || errorMessage?.includes("Invalid or expired")) {
269
- return new EdgeTokenExchangeError(
270
- "Authorization code is invalid, expired, or already used. Please try again.",
271
- { edgeBoostError: error }
272
- );
421
+ return new EdgeTokenExchangeError("Authorization code is invalid, expired, or already used. Please try again.", {
422
+ edgeBoostError: error
423
+ });
273
424
  }
274
425
  if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
275
- return new EdgeAuthenticationError(
276
- "Invalid client credentials. Check your client ID and secret.",
277
- { edgeBoostError: error }
278
- );
426
+ return new EdgeAuthenticationError("Invalid client credentials. Check your client ID and secret.", {
427
+ edgeBoostError: error
428
+ });
279
429
  }
280
430
  if (errorMessage?.includes("code_verifier") || errorMessage?.includes("PKCE")) {
281
- return new EdgeTokenExchangeError(
282
- "PKCE verification failed. Please try again.",
283
- { edgeBoostError: error }
284
- );
431
+ return new EdgeTokenExchangeError("PKCE verification failed. Please try again.", { edgeBoostError: error });
285
432
  }
286
- return new EdgeTokenExchangeError(
287
- errorMessage || "Failed to exchange authorization code",
288
- { edgeBoostError: error, statusCode: status }
289
- );
433
+ return new EdgeTokenExchangeError(errorMessage || "Failed to exchange authorization code", {
434
+ edgeBoostError: error,
435
+ statusCode: status
436
+ });
290
437
  }
291
438
  async handleApiErrorFromBody(error, status, path) {
292
439
  if (status === 401) {
293
- return new EdgeAuthenticationError(
294
- error.message || "Access token is invalid or expired",
295
- error
296
- );
440
+ return new EdgeAuthenticationError(error.message || "Access token is invalid or expired", error);
297
441
  }
298
442
  if (status === 403) {
299
443
  if (error.error === "consent_required") {
@@ -315,6 +459,12 @@ var EdgeConnectServer = class _EdgeConnectServer {
315
459
  const resourceId = path.split("/").pop() || "unknown";
316
460
  return new EdgeNotFoundError(resourceType, resourceId);
317
461
  }
462
+ if (status === 422 && error.error === "identity_verification_failed") {
463
+ return new EdgeIdentityVerificationError(
464
+ error.fieldErrors || {},
465
+ error.message
466
+ );
467
+ }
318
468
  return new EdgeApiError(
319
469
  error.error || "api_error",
320
470
  error.message || error.error_description || `Request failed with status ${status}`,
@@ -326,24 +476,21 @@ var EdgeConnectServer = class _EdgeConnectServer {
326
476
 
327
477
  // src/index.ts
328
478
  import {
329
- EdgeError as EdgeError2,
479
+ EdgeApiError as EdgeApiError2,
330
480
  EdgeAuthenticationError as EdgeAuthenticationError2,
331
- EdgeTokenExchangeError as EdgeTokenExchangeError2,
332
481
  EdgeConsentRequiredError as EdgeConsentRequiredError2,
482
+ EdgeError as EdgeError2,
333
483
  EdgeInsufficientScopeError as EdgeInsufficientScopeError2,
334
- EdgeApiError as EdgeApiError2,
335
- EdgeNotFoundError as EdgeNotFoundError2,
336
484
  EdgeNetworkError as EdgeNetworkError2,
337
- isEdgeError,
485
+ EdgeNotFoundError as EdgeNotFoundError2,
486
+ EdgeTokenExchangeError as EdgeTokenExchangeError2,
487
+ isApiError,
338
488
  isAuthenticationError,
339
489
  isConsentRequiredError,
340
- isApiError,
490
+ isEdgeError,
341
491
  isNetworkError
342
492
  } from "@edge-markets/connect";
343
- import {
344
- getEnvironmentConfig as getEnvironmentConfig2,
345
- isProductionEnvironment
346
- } from "@edge-markets/connect";
493
+ import { getEnvironmentConfig as getEnvironmentConfig2, isProductionEnvironment } from "@edge-markets/connect";
347
494
  export {
348
495
  EdgeApiError2 as EdgeApiError,
349
496
  EdgeAuthenticationError2 as EdgeAuthenticationError,
@@ -354,6 +501,7 @@ export {
354
501
  EdgeNetworkError2 as EdgeNetworkError,
355
502
  EdgeNotFoundError2 as EdgeNotFoundError,
356
503
  EdgeTokenExchangeError2 as EdgeTokenExchangeError,
504
+ EdgeUserClient,
357
505
  getEnvironmentConfig2 as getEnvironmentConfig,
358
506
  isApiError,
359
507
  isAuthenticationError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edge-markets/connect-node",
3
- "version": "1.2.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",