@edge-markets/connect-node 1.0.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -9
- package/dist/index.d.mts +40 -348
- package/dist/index.d.ts +40 -348
- package/dist/index.js +230 -290
- package/dist/index.mjs +232 -291
- package/package.json +10 -12
package/dist/index.js
CHANGED
|
@@ -41,15 +41,107 @@ module.exports = __toCommonJS(index_exports);
|
|
|
41
41
|
|
|
42
42
|
// src/edge-connect-server.ts
|
|
43
43
|
var import_connect = require("@edge-markets/connect");
|
|
44
|
+
|
|
45
|
+
// src/mle.ts
|
|
46
|
+
var import_crypto = require("crypto");
|
|
47
|
+
function encryptMle(payload, clientId, config) {
|
|
48
|
+
const header = {
|
|
49
|
+
alg: "RSA-OAEP-256",
|
|
50
|
+
enc: "A256GCM",
|
|
51
|
+
kid: config.edgeKeyId,
|
|
52
|
+
typ: "application/json",
|
|
53
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
54
|
+
jti: (0, import_crypto.randomUUID)(),
|
|
55
|
+
clientId
|
|
56
|
+
};
|
|
57
|
+
const protectedHeaderB64 = toBase64Url(Buffer.from(JSON.stringify(header), "utf8"));
|
|
58
|
+
const aad = Buffer.from(protectedHeaderB64, "utf8");
|
|
59
|
+
const cek = (0, import_crypto.randomBytes)(32);
|
|
60
|
+
const iv = (0, import_crypto.randomBytes)(12);
|
|
61
|
+
const cipher = (0, import_crypto.createCipheriv)("aes-256-gcm", cek, iv);
|
|
62
|
+
cipher.setAAD(aad);
|
|
63
|
+
const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
|
|
64
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
65
|
+
const tag = cipher.getAuthTag();
|
|
66
|
+
const encryptedKey = (0, import_crypto.publicEncrypt)(
|
|
67
|
+
{
|
|
68
|
+
key: config.edgePublicKey,
|
|
69
|
+
padding: import_crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
70
|
+
oaepHash: "sha256"
|
|
71
|
+
},
|
|
72
|
+
cek
|
|
73
|
+
);
|
|
74
|
+
return [
|
|
75
|
+
protectedHeaderB64,
|
|
76
|
+
toBase64Url(encryptedKey),
|
|
77
|
+
toBase64Url(iv),
|
|
78
|
+
toBase64Url(ciphertext),
|
|
79
|
+
toBase64Url(tag)
|
|
80
|
+
].join(".");
|
|
81
|
+
}
|
|
82
|
+
function decryptMle(jwe, config) {
|
|
83
|
+
const parts = jwe.split(".");
|
|
84
|
+
if (parts.length !== 5) {
|
|
85
|
+
throw new Error("Invalid MLE payload format");
|
|
86
|
+
}
|
|
87
|
+
const [protectedHeaderB64, encryptedKeyB64, ivB64, ciphertextB64, tagB64] = parts;
|
|
88
|
+
const protectedHeader = JSON.parse(fromBase64Url(protectedHeaderB64).toString("utf8"));
|
|
89
|
+
if (protectedHeader.alg !== "RSA-OAEP-256" || protectedHeader.enc !== "A256GCM") {
|
|
90
|
+
throw new Error("Unsupported MLE algorithms");
|
|
91
|
+
}
|
|
92
|
+
if (protectedHeader.kid && protectedHeader.kid !== config.partnerKeyId) {
|
|
93
|
+
throw new Error(`Unexpected response key id: ${protectedHeader.kid}`);
|
|
94
|
+
}
|
|
95
|
+
const cek = (0, import_crypto.privateDecrypt)(
|
|
96
|
+
{
|
|
97
|
+
key: config.partnerPrivateKey,
|
|
98
|
+
padding: import_crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
99
|
+
oaepHash: "sha256"
|
|
100
|
+
},
|
|
101
|
+
fromBase64Url(encryptedKeyB64)
|
|
102
|
+
);
|
|
103
|
+
const decipher = (0, import_crypto.createDecipheriv)("aes-256-gcm", cek, fromBase64Url(ivB64));
|
|
104
|
+
decipher.setAAD(Buffer.from(protectedHeaderB64, "utf8"));
|
|
105
|
+
decipher.setAuthTag(fromBase64Url(tagB64));
|
|
106
|
+
const plaintext = Buffer.concat([
|
|
107
|
+
decipher.update(fromBase64Url(ciphertextB64)),
|
|
108
|
+
decipher.final()
|
|
109
|
+
]);
|
|
110
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
111
|
+
}
|
|
112
|
+
function toBase64Url(buffer) {
|
|
113
|
+
return buffer.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
114
|
+
}
|
|
115
|
+
function fromBase64Url(value) {
|
|
116
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
117
|
+
return Buffer.from(normalized, "base64");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/edge-connect-server.ts
|
|
44
121
|
var DEFAULT_TIMEOUT = 3e4;
|
|
45
122
|
var USER_AGENT = "@edge-markets/connect-node/1.0.0";
|
|
46
|
-
var
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
123
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
124
|
+
maxRetries: 3,
|
|
125
|
+
retryOn: [408, 429, 500, 502, 503, 504],
|
|
126
|
+
backoff: "exponential",
|
|
127
|
+
baseDelayMs: 1e3
|
|
128
|
+
};
|
|
129
|
+
var instances = /* @__PURE__ */ new Map();
|
|
130
|
+
function getInstanceKey(config) {
|
|
131
|
+
return `${config.clientId}:${config.environment}`;
|
|
132
|
+
}
|
|
133
|
+
var EdgeConnectServer = class _EdgeConnectServer {
|
|
134
|
+
static getInstance(config) {
|
|
135
|
+
const key = getInstanceKey(config);
|
|
136
|
+
const existing = instances.get(key);
|
|
137
|
+
if (existing) return existing;
|
|
138
|
+
const instance = new _EdgeConnectServer(config);
|
|
139
|
+
instances.set(key, instance);
|
|
140
|
+
return instance;
|
|
141
|
+
}
|
|
142
|
+
static clearInstances() {
|
|
143
|
+
instances.clear();
|
|
144
|
+
}
|
|
53
145
|
constructor(config) {
|
|
54
146
|
if (!config.clientId) {
|
|
55
147
|
throw new Error("EdgeConnectServer: clientId is required");
|
|
@@ -63,66 +155,29 @@ var EdgeConnectServer = class {
|
|
|
63
155
|
this.config = config;
|
|
64
156
|
const envConfig = (0, import_connect.getEnvironmentConfig)(config.environment);
|
|
65
157
|
this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
|
|
66
|
-
this.oauthBaseUrl = envConfig.oauthBaseUrl;
|
|
158
|
+
this.oauthBaseUrl = config.oauthBaseUrl || envConfig.oauthBaseUrl;
|
|
67
159
|
this.timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
160
|
+
this.retryConfig = {
|
|
161
|
+
...DEFAULT_RETRY_CONFIG,
|
|
162
|
+
...config.retry
|
|
163
|
+
};
|
|
68
164
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// ===========================================================================
|
|
72
|
-
/**
|
|
73
|
-
* Exchanges an authorization code for tokens.
|
|
74
|
-
*
|
|
75
|
-
* Call this after receiving the code from EdgeLink's `onSuccess` callback.
|
|
76
|
-
* The code is single-use and expires in ~10 minutes.
|
|
77
|
-
*
|
|
78
|
-
* @param code - Authorization code from EdgeLink
|
|
79
|
-
* @param codeVerifier - PKCE code verifier from EdgeLink
|
|
80
|
-
* @returns Access token, refresh token, and metadata
|
|
81
|
-
* @throws EdgeTokenExchangeError if exchange fails
|
|
82
|
-
*
|
|
83
|
-
* @example
|
|
84
|
-
* ```typescript
|
|
85
|
-
* // In your /api/edge/exchange endpoint
|
|
86
|
-
* const { code, codeVerifier } = req.body
|
|
87
|
-
*
|
|
88
|
-
* try {
|
|
89
|
-
* const tokens = await edge.exchangeCode(code, codeVerifier)
|
|
90
|
-
*
|
|
91
|
-
* // Store tokens securely
|
|
92
|
-
* await db.edgeConnections.upsert({
|
|
93
|
-
* userId: req.user.id,
|
|
94
|
-
* accessToken: encrypt(tokens.accessToken),
|
|
95
|
-
* refreshToken: encrypt(tokens.refreshToken),
|
|
96
|
-
* expiresAt: new Date(tokens.expiresAt),
|
|
97
|
-
* })
|
|
98
|
-
*
|
|
99
|
-
* return { success: true }
|
|
100
|
-
* } catch (error) {
|
|
101
|
-
* if (error instanceof EdgeTokenExchangeError) {
|
|
102
|
-
* // Code expired or already used
|
|
103
|
-
* return { error: 'Please try connecting again' }
|
|
104
|
-
* }
|
|
105
|
-
* throw error
|
|
106
|
-
* }
|
|
107
|
-
* ```
|
|
108
|
-
*/
|
|
109
|
-
async exchangeCode(code, codeVerifier) {
|
|
110
|
-
const tokenUrl = `${this.oauthBaseUrl}/oauth/token`;
|
|
165
|
+
async exchangeCode(code, codeVerifier, redirectUri) {
|
|
166
|
+
const tokenUrl = `${this.oauthBaseUrl}/token`;
|
|
111
167
|
const body = {
|
|
112
168
|
grant_type: "authorization_code",
|
|
113
169
|
code,
|
|
114
170
|
code_verifier: codeVerifier,
|
|
115
171
|
client_id: this.config.clientId,
|
|
116
|
-
client_secret: this.config.clientSecret
|
|
172
|
+
client_secret: this.config.clientSecret,
|
|
173
|
+
redirect_uri: redirectUri || this.config.redirectUri || ""
|
|
117
174
|
};
|
|
118
175
|
try {
|
|
119
176
|
const response = await this.fetchWithTimeout(tokenUrl, {
|
|
120
177
|
method: "POST",
|
|
121
178
|
headers: {
|
|
122
179
|
"Content-Type": "application/json",
|
|
123
|
-
"User-Agent": USER_AGENT
|
|
124
|
-
// Required for ngrok tunnels in local development
|
|
125
|
-
"ngrok-skip-browser-warning": "true"
|
|
180
|
+
"User-Agent": USER_AGENT
|
|
126
181
|
},
|
|
127
182
|
body: JSON.stringify(body)
|
|
128
183
|
});
|
|
@@ -137,44 +192,8 @@ var EdgeConnectServer = class {
|
|
|
137
192
|
throw new import_connect.EdgeNetworkError("Failed to exchange code", error);
|
|
138
193
|
}
|
|
139
194
|
}
|
|
140
|
-
/**
|
|
141
|
-
* Refreshes an access token using a refresh token.
|
|
142
|
-
*
|
|
143
|
-
* Call this when the access token is expired or about to expire.
|
|
144
|
-
* Check `tokens.expiresAt` to know when to refresh.
|
|
145
|
-
*
|
|
146
|
-
* @param refreshToken - Refresh token from previous exchange
|
|
147
|
-
* @returns New tokens (refresh token may or may not change)
|
|
148
|
-
* @throws EdgeAuthenticationError if refresh fails
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* ```typescript
|
|
152
|
-
* // Check if token needs refresh (with 5 minute buffer)
|
|
153
|
-
* const BUFFER_MS = 5 * 60 * 1000
|
|
154
|
-
*
|
|
155
|
-
* async function getValidAccessToken(userId: string): Promise<string> {
|
|
156
|
-
* const connection = await db.edgeConnections.get(userId)
|
|
157
|
-
*
|
|
158
|
-
* if (Date.now() > connection.expiresAt.getTime() - BUFFER_MS) {
|
|
159
|
-
* // Token expired or expiring soon - refresh it
|
|
160
|
-
* const newTokens = await edge.refreshTokens(decrypt(connection.refreshToken))
|
|
161
|
-
*
|
|
162
|
-
* // Update stored tokens
|
|
163
|
-
* await db.edgeConnections.update(userId, {
|
|
164
|
-
* accessToken: encrypt(newTokens.accessToken),
|
|
165
|
-
* refreshToken: encrypt(newTokens.refreshToken),
|
|
166
|
-
* expiresAt: new Date(newTokens.expiresAt),
|
|
167
|
-
* })
|
|
168
|
-
*
|
|
169
|
-
* return newTokens.accessToken
|
|
170
|
-
* }
|
|
171
|
-
*
|
|
172
|
-
* return decrypt(connection.accessToken)
|
|
173
|
-
* }
|
|
174
|
-
* ```
|
|
175
|
-
*/
|
|
176
195
|
async refreshTokens(refreshToken) {
|
|
177
|
-
const tokenUrl = `${this.oauthBaseUrl}/
|
|
196
|
+
const tokenUrl = `${this.oauthBaseUrl}/token`;
|
|
178
197
|
const body = {
|
|
179
198
|
grant_type: "refresh_token",
|
|
180
199
|
refresh_token: refreshToken,
|
|
@@ -186,15 +205,14 @@ var EdgeConnectServer = class {
|
|
|
186
205
|
method: "POST",
|
|
187
206
|
headers: {
|
|
188
207
|
"Content-Type": "application/json",
|
|
189
|
-
"User-Agent": USER_AGENT
|
|
190
|
-
"ngrok-skip-browser-warning": "true"
|
|
208
|
+
"User-Agent": USER_AGENT
|
|
191
209
|
},
|
|
192
210
|
body: JSON.stringify(body)
|
|
193
211
|
});
|
|
194
212
|
if (!response.ok) {
|
|
195
213
|
const error = await response.json().catch(() => ({}));
|
|
196
214
|
throw new import_connect.EdgeAuthenticationError(
|
|
197
|
-
error.error_description || "Token refresh failed. User may need to reconnect.",
|
|
215
|
+
error.message || error.error_description || "Token refresh failed. User may need to reconnect.",
|
|
198
216
|
{ tokenError: error }
|
|
199
217
|
);
|
|
200
218
|
}
|
|
@@ -205,80 +223,14 @@ var EdgeConnectServer = class {
|
|
|
205
223
|
throw new import_connect.EdgeNetworkError("Failed to refresh tokens", error);
|
|
206
224
|
}
|
|
207
225
|
}
|
|
208
|
-
// ===========================================================================
|
|
209
|
-
// USER & BALANCE
|
|
210
|
-
// ===========================================================================
|
|
211
|
-
/**
|
|
212
|
-
* Gets the connected user's profile.
|
|
213
|
-
*
|
|
214
|
-
* Requires scope: `user.read`
|
|
215
|
-
*
|
|
216
|
-
* @param accessToken - Valid access token
|
|
217
|
-
* @returns User profile information
|
|
218
|
-
*
|
|
219
|
-
* @example
|
|
220
|
-
* ```typescript
|
|
221
|
-
* const user = await edge.getUser(accessToken)
|
|
222
|
-
* console.log(`Connected: ${user.firstName} ${user.lastName}`)
|
|
223
|
-
* ```
|
|
224
|
-
*/
|
|
225
226
|
async getUser(accessToken) {
|
|
226
227
|
return this.apiRequest("GET", "/user", accessToken);
|
|
227
228
|
}
|
|
228
|
-
/**
|
|
229
|
-
* Gets the connected user's EdgeBoost balance.
|
|
230
|
-
*
|
|
231
|
-
* Requires scope: `balance.read`
|
|
232
|
-
*
|
|
233
|
-
* @param accessToken - Valid access token
|
|
234
|
-
* @returns Balance information
|
|
235
|
-
*
|
|
236
|
-
* @example
|
|
237
|
-
* ```typescript
|
|
238
|
-
* const balance = await edge.getBalance(accessToken)
|
|
239
|
-
* console.log(`Balance: $${balance.availableBalance.toFixed(2)} ${balance.currency}`)
|
|
240
|
-
* ```
|
|
241
|
-
*/
|
|
242
229
|
async getBalance(accessToken) {
|
|
243
230
|
return this.apiRequest("GET", "/balance", accessToken);
|
|
244
231
|
}
|
|
245
|
-
// ===========================================================================
|
|
246
|
-
// TRANSFERS
|
|
247
|
-
// ===========================================================================
|
|
248
|
-
/**
|
|
249
|
-
* Initiates a fund transfer.
|
|
250
|
-
*
|
|
251
|
-
* Requires scope: `transfer.write`
|
|
252
|
-
*
|
|
253
|
-
* **Transfer Types:**
|
|
254
|
-
* - `debit`: Pull funds FROM user's EdgeBoost TO your platform
|
|
255
|
-
* - `credit`: Push funds FROM your platform TO user's EdgeBoost
|
|
256
|
-
*
|
|
257
|
-
* **Idempotency:** Using the same `idempotencyKey` returns the existing
|
|
258
|
-
* transfer instead of creating a duplicate. Use a unique key per transaction.
|
|
259
|
-
*
|
|
260
|
-
* **OTP Verification:** Transfers require OTP verification before completion.
|
|
261
|
-
* The response includes `otpMethod` indicating how the user will receive the code.
|
|
262
|
-
*
|
|
263
|
-
* @param accessToken - Valid access token
|
|
264
|
-
* @param options - Transfer options
|
|
265
|
-
* @returns Transfer with status and OTP method
|
|
266
|
-
*
|
|
267
|
-
* @example
|
|
268
|
-
* ```typescript
|
|
269
|
-
* const transfer = await edge.initiateTransfer(accessToken, {
|
|
270
|
-
* type: 'debit',
|
|
271
|
-
* amount: '100.00',
|
|
272
|
-
* idempotencyKey: `withdraw_${userId}_${Date.now()}`,
|
|
273
|
-
* })
|
|
274
|
-
*
|
|
275
|
-
* if (transfer.status === 'pending_verification') {
|
|
276
|
-
* // Show OTP input to user
|
|
277
|
-
* console.log(`Enter code sent via ${transfer.otpMethod}`)
|
|
278
|
-
* }
|
|
279
|
-
* ```
|
|
280
|
-
*/
|
|
281
232
|
async initiateTransfer(accessToken, options) {
|
|
233
|
+
this.validateTransferOptions(options);
|
|
282
234
|
const body = {
|
|
283
235
|
type: options.type,
|
|
284
236
|
amount: options.amount,
|
|
@@ -286,28 +238,32 @@ var EdgeConnectServer = class {
|
|
|
286
238
|
};
|
|
287
239
|
return this.apiRequest("POST", "/transfer", accessToken, body);
|
|
288
240
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
}
|
|
311
267
|
async verifyTransfer(accessToken, transferId, otp) {
|
|
312
268
|
return this.apiRequest(
|
|
313
269
|
"POST",
|
|
@@ -316,21 +272,6 @@ var EdgeConnectServer = class {
|
|
|
316
272
|
{ otp }
|
|
317
273
|
);
|
|
318
274
|
}
|
|
319
|
-
/**
|
|
320
|
-
* Gets the status of a transfer.
|
|
321
|
-
*
|
|
322
|
-
* Use for polling after initiating a transfer.
|
|
323
|
-
*
|
|
324
|
-
* @param accessToken - Valid access token
|
|
325
|
-
* @param transferId - Transfer ID
|
|
326
|
-
* @returns Current transfer status
|
|
327
|
-
*
|
|
328
|
-
* @example
|
|
329
|
-
* ```typescript
|
|
330
|
-
* const transfer = await edge.getTransfer(accessToken, transferId)
|
|
331
|
-
* console.log(`Status: ${transfer.status}`)
|
|
332
|
-
* ```
|
|
333
|
-
*/
|
|
334
275
|
async getTransfer(accessToken, transferId) {
|
|
335
276
|
return this.apiRequest(
|
|
336
277
|
"GET",
|
|
@@ -338,27 +279,6 @@ var EdgeConnectServer = class {
|
|
|
338
279
|
accessToken
|
|
339
280
|
);
|
|
340
281
|
}
|
|
341
|
-
/**
|
|
342
|
-
* Lists transfers for the connected user.
|
|
343
|
-
*
|
|
344
|
-
* Useful for reconciliation and showing transfer history.
|
|
345
|
-
*
|
|
346
|
-
* @param accessToken - Valid access token
|
|
347
|
-
* @param params - Pagination and filter options
|
|
348
|
-
* @returns Paginated list of transfers
|
|
349
|
-
*
|
|
350
|
-
* @example
|
|
351
|
-
* ```typescript
|
|
352
|
-
* // Get first page of completed transfers
|
|
353
|
-
* const { transfers, total } = await edge.listTransfers(accessToken, {
|
|
354
|
-
* status: 'completed',
|
|
355
|
-
* limit: 10,
|
|
356
|
-
* offset: 0,
|
|
357
|
-
* })
|
|
358
|
-
*
|
|
359
|
-
* console.log(`Showing ${transfers.length} of ${total} transfers`)
|
|
360
|
-
* ```
|
|
361
|
-
*/
|
|
362
282
|
async listTransfers(accessToken, params) {
|
|
363
283
|
const queryParams = new URLSearchParams();
|
|
364
284
|
if (params?.limit) queryParams.set("limit", String(params.limit));
|
|
@@ -368,68 +288,93 @@ var EdgeConnectServer = class {
|
|
|
368
288
|
const path = query ? `/transfers?${query}` : "/transfers";
|
|
369
289
|
return this.apiRequest("GET", path, accessToken);
|
|
370
290
|
}
|
|
371
|
-
// ===========================================================================
|
|
372
|
-
// CONSENT
|
|
373
|
-
// ===========================================================================
|
|
374
|
-
/**
|
|
375
|
-
* Revokes the user's consent (disconnects their account).
|
|
376
|
-
*
|
|
377
|
-
* After revocation:
|
|
378
|
-
* - All API calls will fail with `consent_required` error
|
|
379
|
-
* - User must go through EdgeLink again to reconnect
|
|
380
|
-
* - Stored tokens become invalid
|
|
381
|
-
*
|
|
382
|
-
* Use this for "Disconnect" or "Unlink" features in your app.
|
|
383
|
-
*
|
|
384
|
-
* @param accessToken - Valid access token
|
|
385
|
-
* @returns Confirmation of revocation
|
|
386
|
-
*
|
|
387
|
-
* @example
|
|
388
|
-
* ```typescript
|
|
389
|
-
* // Disconnect user's EdgeBoost account
|
|
390
|
-
* await edge.revokeConsent(accessToken)
|
|
391
|
-
*
|
|
392
|
-
* // Clean up stored tokens
|
|
393
|
-
* await db.edgeConnections.delete(userId)
|
|
394
|
-
*
|
|
395
|
-
* console.log('EdgeBoost disconnected')
|
|
396
|
-
* ```
|
|
397
|
-
*/
|
|
398
291
|
async revokeConsent(accessToken) {
|
|
399
292
|
return this.apiRequest("DELETE", "/consent", accessToken);
|
|
400
293
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
294
|
+
getRetryDelay(attempt) {
|
|
295
|
+
const { backoff, baseDelayMs } = this.retryConfig;
|
|
296
|
+
if (backoff === "exponential") {
|
|
297
|
+
return baseDelayMs * Math.pow(2, attempt);
|
|
298
|
+
}
|
|
299
|
+
return baseDelayMs * (attempt + 1);
|
|
300
|
+
}
|
|
301
|
+
async sleep(ms) {
|
|
302
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
303
|
+
}
|
|
407
304
|
async apiRequest(method, path, accessToken, body) {
|
|
408
305
|
const url = `${this.apiBaseUrl}${path}`;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
306
|
+
let lastError = null;
|
|
307
|
+
const mleConfig = this.config.mle;
|
|
308
|
+
const mleEnabled = !!mleConfig?.enabled;
|
|
309
|
+
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
310
|
+
const startTime = Date.now();
|
|
311
|
+
if (attempt > 0) {
|
|
312
|
+
await this.sleep(this.getRetryDelay(attempt - 1));
|
|
313
|
+
}
|
|
314
|
+
this.config.onRequest?.({ method, url, body });
|
|
315
|
+
try {
|
|
316
|
+
const requestHeaders = {
|
|
413
317
|
Authorization: `Bearer ${accessToken}`,
|
|
414
318
|
"Content-Type": "application/json",
|
|
415
|
-
"User-Agent": USER_AGENT
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
319
|
+
"User-Agent": USER_AGENT
|
|
320
|
+
};
|
|
321
|
+
let requestBody = body;
|
|
322
|
+
if (mleEnabled) {
|
|
323
|
+
requestHeaders["X-Edge-MLE"] = "v1";
|
|
324
|
+
if (body !== void 0) {
|
|
325
|
+
requestBody = { jwe: encryptMle(body, this.config.clientId, mleConfig) };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const response = await this.fetchWithTimeout(url, {
|
|
329
|
+
method,
|
|
330
|
+
headers: requestHeaders,
|
|
331
|
+
body: requestBody !== void 0 ? JSON.stringify(requestBody) : void 0
|
|
332
|
+
});
|
|
333
|
+
const rawResponseBody = await response.json().catch(() => ({}));
|
|
334
|
+
let responseBody = rawResponseBody;
|
|
335
|
+
if (mleEnabled && typeof rawResponseBody?.jwe === "string") {
|
|
336
|
+
responseBody = decryptMle(rawResponseBody.jwe, mleConfig);
|
|
337
|
+
} else if (!mleEnabled && typeof rawResponseBody?.jwe === "string") {
|
|
338
|
+
throw new import_connect.EdgeApiError(
|
|
339
|
+
"mle_required",
|
|
340
|
+
"The API responded with message-level encryption. Enable MLE in SDK config.",
|
|
341
|
+
response.status,
|
|
342
|
+
rawResponseBody
|
|
343
|
+
);
|
|
344
|
+
} else if (mleEnabled && mleConfig?.strictResponseEncryption !== false && response.ok && typeof rawResponseBody?.jwe !== "string") {
|
|
345
|
+
throw new import_connect.EdgeApiError(
|
|
346
|
+
"mle_response_missing",
|
|
347
|
+
"Expected encrypted response payload but received plaintext.",
|
|
348
|
+
response.status,
|
|
349
|
+
rawResponseBody
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const durationMs = Date.now() - startTime;
|
|
353
|
+
this.config.onResponse?.({ status: response.status, body: responseBody, durationMs });
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
if (this.retryConfig.retryOn.includes(response.status) && attempt < this.retryConfig.maxRetries) {
|
|
356
|
+
lastError = await this.handleApiErrorFromBody(responseBody, response.status, path);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
throw await this.handleApiErrorFromBody(responseBody, response.status, path);
|
|
360
|
+
}
|
|
361
|
+
return responseBody;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (error instanceof import_connect.EdgeError) {
|
|
364
|
+
if (!(error instanceof import_connect.EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
lastError = error;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
lastError = error;
|
|
371
|
+
if (attempt >= this.retryConfig.maxRetries) {
|
|
372
|
+
throw new import_connect.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
|
|
373
|
+
}
|
|
423
374
|
}
|
|
424
|
-
return response.json();
|
|
425
|
-
} catch (error) {
|
|
426
|
-
if (error instanceof import_connect.EdgeError) throw error;
|
|
427
|
-
throw new import_connect.EdgeNetworkError(`API request failed: ${method} ${path}`, error);
|
|
428
375
|
}
|
|
376
|
+
throw new import_connect.EdgeNetworkError(`API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`, lastError);
|
|
429
377
|
}
|
|
430
|
-
/**
|
|
431
|
-
* Fetch with timeout support.
|
|
432
|
-
*/
|
|
433
378
|
async fetchWithTimeout(url, options) {
|
|
434
379
|
const controller = new AbortController();
|
|
435
380
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
@@ -442,9 +387,6 @@ var EdgeConnectServer = class {
|
|
|
442
387
|
clearTimeout(timeoutId);
|
|
443
388
|
}
|
|
444
389
|
}
|
|
445
|
-
/**
|
|
446
|
-
* Parses token response from Cognito.
|
|
447
|
-
*/
|
|
448
390
|
parseTokenResponse(data, existingRefreshToken) {
|
|
449
391
|
return {
|
|
450
392
|
accessToken: data.access_token,
|
|
@@ -455,35 +397,33 @@ var EdgeConnectServer = class {
|
|
|
455
397
|
scope: data.scope || ""
|
|
456
398
|
};
|
|
457
399
|
}
|
|
458
|
-
/**
|
|
459
|
-
* Handles token exchange errors.
|
|
460
|
-
*/
|
|
461
400
|
handleTokenError(error, status) {
|
|
462
401
|
const errorCode = error.error;
|
|
463
|
-
const
|
|
464
|
-
if (errorCode === "invalid_grant") {
|
|
402
|
+
const errorMessage = error.message || error.error_description;
|
|
403
|
+
if (errorCode === "invalid_grant" || errorMessage?.includes("Invalid or expired")) {
|
|
465
404
|
return new import_connect.EdgeTokenExchangeError(
|
|
466
405
|
"Authorization code is invalid, expired, or already used. Please try again.",
|
|
467
|
-
{
|
|
406
|
+
{ edgeBoostError: error }
|
|
468
407
|
);
|
|
469
408
|
}
|
|
470
|
-
if (errorCode === "invalid_client") {
|
|
409
|
+
if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
|
|
471
410
|
return new import_connect.EdgeAuthenticationError(
|
|
472
411
|
"Invalid client credentials. Check your client ID and secret.",
|
|
473
|
-
{
|
|
412
|
+
{ edgeBoostError: error }
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if (errorMessage?.includes("code_verifier") || errorMessage?.includes("PKCE")) {
|
|
416
|
+
return new import_connect.EdgeTokenExchangeError(
|
|
417
|
+
"PKCE verification failed. Please try again.",
|
|
418
|
+
{ edgeBoostError: error }
|
|
474
419
|
);
|
|
475
420
|
}
|
|
476
421
|
return new import_connect.EdgeTokenExchangeError(
|
|
477
|
-
|
|
478
|
-
{
|
|
422
|
+
errorMessage || "Failed to exchange authorization code",
|
|
423
|
+
{ edgeBoostError: error, statusCode: status }
|
|
479
424
|
);
|
|
480
425
|
}
|
|
481
|
-
|
|
482
|
-
* Handles API errors.
|
|
483
|
-
*/
|
|
484
|
-
async handleApiError(response, path) {
|
|
485
|
-
const error = await response.json().catch(() => ({}));
|
|
486
|
-
const status = response.status;
|
|
426
|
+
async handleApiErrorFromBody(error, status, path) {
|
|
487
427
|
if (status === 401) {
|
|
488
428
|
return new import_connect.EdgeAuthenticationError(
|
|
489
429
|
error.message || "Access token is invalid or expired",
|