@edge-markets/connect-node 1.6.1 → 1.8.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 +319 -11
- package/dist/index.d.mts +442 -4
- package/dist/index.d.ts +442 -4
- package/dist/index.js +1509 -62
- package/dist/index.mjs +1473 -34
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
8
|
// src/edge-connect-server.ts
|
|
9
9
|
import {
|
|
10
10
|
EdgeApiError,
|
|
11
|
-
EdgeAuthenticationError,
|
|
11
|
+
EdgeAuthenticationError as EdgeAuthenticationError2,
|
|
12
12
|
EdgeConsentRequiredError,
|
|
13
13
|
EdgeError,
|
|
14
14
|
EdgeIdentityVerificationError,
|
|
@@ -19,8 +19,141 @@ import {
|
|
|
19
19
|
getEnvironmentConfig
|
|
20
20
|
} from "@edge-markets/connect";
|
|
21
21
|
|
|
22
|
+
// src/transfer-helpers.ts
|
|
23
|
+
import {
|
|
24
|
+
EdgeValidationError
|
|
25
|
+
} from "@edge-markets/connect";
|
|
26
|
+
import crypto from "crypto";
|
|
27
|
+
var DEFAULT_MIN_AMOUNT = "1.00";
|
|
28
|
+
var DEFAULT_MAX_AMOUNT = "10000.00";
|
|
29
|
+
function normalizeMoneyAmount(value) {
|
|
30
|
+
const cents = parseMoneyToCents(value, "amount");
|
|
31
|
+
const dollars = cents / 100n;
|
|
32
|
+
const remainder = cents % 100n;
|
|
33
|
+
return `${dollars.toString()}.${remainder.toString().padStart(2, "0")}`;
|
|
34
|
+
}
|
|
35
|
+
function validateTransferAmount(value, limits = {}) {
|
|
36
|
+
const amount = parseMoneyToCents(value, "amount");
|
|
37
|
+
const min = parseMoneyToCents(limits.min ?? DEFAULT_MIN_AMOUNT, "min");
|
|
38
|
+
const max = parseMoneyToCents(limits.max ?? DEFAULT_MAX_AMOUNT, "max");
|
|
39
|
+
const errors = {};
|
|
40
|
+
if (min <= 0n) errors.min = ["Minimum amount must be greater than 0"];
|
|
41
|
+
if (max < min) errors.max = ["Maximum amount must be greater than or equal to minimum amount"];
|
|
42
|
+
if (amount < min) errors.amount = [`Must be at least ${formatCents(min)}`];
|
|
43
|
+
if (amount > max) errors.amount = [`Must be at most ${formatCents(max)}`];
|
|
44
|
+
if (Object.keys(errors).length > 0) {
|
|
45
|
+
throw new EdgeValidationError("Invalid transfer amount", errors);
|
|
46
|
+
}
|
|
47
|
+
return formatCents(amount);
|
|
48
|
+
}
|
|
49
|
+
function createTransferIdempotencyKey(options) {
|
|
50
|
+
const namespace = sanitizeKeyPart(options.namespace || "edge");
|
|
51
|
+
const partnerUserId = requireKeyPart(options.partnerUserId, "partnerUserId");
|
|
52
|
+
const externalId = options.externalId ? sanitizeKeyPart(options.externalId) : void 0;
|
|
53
|
+
const nonce = options.nonce ? sanitizeKeyPart(options.nonce) : void 0;
|
|
54
|
+
const amount = normalizeMoneyAmount(options.amount);
|
|
55
|
+
const digest = crypto.createHash("sha256").update(
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
partnerUserId,
|
|
58
|
+
type: options.type,
|
|
59
|
+
amount,
|
|
60
|
+
category: options.category ?? null,
|
|
61
|
+
externalId: externalId ?? null,
|
|
62
|
+
nonce: nonce ?? null
|
|
63
|
+
})
|
|
64
|
+
).digest("hex").slice(0, 32);
|
|
65
|
+
return [namespace, options.type, digest].join("_");
|
|
66
|
+
}
|
|
67
|
+
function assertSameTransferIntent(existing, requested) {
|
|
68
|
+
const errors = {};
|
|
69
|
+
if (existing.type !== requested.type) {
|
|
70
|
+
errors.type = [`Expected ${existing.type}, received ${requested.type}`];
|
|
71
|
+
}
|
|
72
|
+
const existingAmount = normalizeMoneyAmount(existing.amount);
|
|
73
|
+
const requestedAmount = normalizeMoneyAmount(requested.amount);
|
|
74
|
+
if (existingAmount !== requestedAmount) {
|
|
75
|
+
errors.amount = [`Expected ${existingAmount}, received ${requestedAmount}`];
|
|
76
|
+
}
|
|
77
|
+
const existingCategory = existing.category ?? void 0;
|
|
78
|
+
const requestedCategory = requested.category ?? void 0;
|
|
79
|
+
if (existingCategory !== requestedCategory) {
|
|
80
|
+
errors.category = [`Expected ${existingCategory || "unset"}, received ${requestedCategory || "unset"}`];
|
|
81
|
+
}
|
|
82
|
+
if (Object.keys(errors).length > 0) {
|
|
83
|
+
throw new EdgeValidationError("Transfer intent does not match existing idempotent transfer", errors);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function startTransferVerification(client, options) {
|
|
87
|
+
const origin = typeof options.origin === "string" ? options.origin.trim() : "";
|
|
88
|
+
if (!origin) {
|
|
89
|
+
throw new EdgeValidationError("Verification origin is required", {
|
|
90
|
+
origin: ["Required"]
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const transfer = await client.initiateTransfer({
|
|
94
|
+
...options.transfer,
|
|
95
|
+
amount: normalizeMoneyAmount(options.transfer.amount)
|
|
96
|
+
});
|
|
97
|
+
const transferId = transfer.transferId;
|
|
98
|
+
if (!transferId) {
|
|
99
|
+
throw new EdgeValidationError("Transfer response missing transferId", {
|
|
100
|
+
transferId: ["Required to create a verification session"]
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const verificationSession = await client.createVerificationSession(transferId, { origin });
|
|
104
|
+
return { transfer, verificationSession };
|
|
105
|
+
}
|
|
106
|
+
function parseMoneyToCents(value, field) {
|
|
107
|
+
let input;
|
|
108
|
+
if (typeof value === "number") {
|
|
109
|
+
if (!Number.isFinite(value)) {
|
|
110
|
+
throw new EdgeValidationError("Money amount must be finite", { [field]: ["Must be a finite number"] });
|
|
111
|
+
}
|
|
112
|
+
input = value.toFixed(2);
|
|
113
|
+
} else if (typeof value === "string") {
|
|
114
|
+
input = value.trim();
|
|
115
|
+
} else {
|
|
116
|
+
throw new EdgeValidationError("Money amount must be a string or number", { [field]: ["Invalid type"] });
|
|
117
|
+
}
|
|
118
|
+
if (!/^\d+(\.\d{0,2})?$/.test(input)) {
|
|
119
|
+
throw new EdgeValidationError("Money amount must be a positive decimal with at most 2 places", {
|
|
120
|
+
[field]: ['Use a decimal string like "25.00"']
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const [dollars, cents = ""] = input.split(".");
|
|
124
|
+
const normalizedDollars = dollars.replace(/^0+(?=\d)/, "") || "0";
|
|
125
|
+
const amount = BigInt(normalizedDollars) * 100n + BigInt(cents.padEnd(2, "0"));
|
|
126
|
+
if (amount <= 0n) {
|
|
127
|
+
throw new EdgeValidationError("Money amount must be greater than 0", { [field]: ["Must be greater than 0"] });
|
|
128
|
+
}
|
|
129
|
+
return amount;
|
|
130
|
+
}
|
|
131
|
+
function formatCents(cents) {
|
|
132
|
+
return `${(cents / 100n).toString()}.${(cents % 100n).toString().padStart(2, "0")}`;
|
|
133
|
+
}
|
|
134
|
+
function requireKeyPart(value, field) {
|
|
135
|
+
const sanitized = sanitizeKeyPart(value);
|
|
136
|
+
if (!sanitized) {
|
|
137
|
+
throw new EdgeValidationError("Idempotency key input is incomplete", {
|
|
138
|
+
[field]: ["Required"]
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return sanitized;
|
|
142
|
+
}
|
|
143
|
+
function sanitizeKeyPart(value) {
|
|
144
|
+
return value.trim().replace(/[^a-zA-Z0-9_.:-]/g, "_").slice(0, 64);
|
|
145
|
+
}
|
|
146
|
+
|
|
22
147
|
// src/validators.ts
|
|
23
|
-
import { EdgeValidationError } from "@edge-markets/connect";
|
|
148
|
+
import { EdgeValidationError as EdgeValidationError2 } from "@edge-markets/connect";
|
|
149
|
+
var UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
150
|
+
function validateTransferId(transferId) {
|
|
151
|
+
if (typeof transferId !== "string" || !UUID_V4_REGEX.test(transferId)) {
|
|
152
|
+
throw new EdgeValidationError2("transferId must be a UUID v4", {
|
|
153
|
+
transferId: ["Must be a valid UUID v4"]
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
24
157
|
function validateVerifyIdentityOptions(options) {
|
|
25
158
|
const errors = {};
|
|
26
159
|
const firstName = typeof options.firstName === "string" ? options.firstName.trim() : "";
|
|
@@ -36,7 +169,7 @@ function validateVerifyIdentityOptions(options) {
|
|
|
36
169
|
}
|
|
37
170
|
if (Object.keys(errors).length > 0) {
|
|
38
171
|
const firstMessage = Object.values(errors)[0][0];
|
|
39
|
-
throw new
|
|
172
|
+
throw new EdgeValidationError2(firstMessage, errors);
|
|
40
173
|
}
|
|
41
174
|
}
|
|
42
175
|
function validateTransferOptions(options) {
|
|
@@ -65,7 +198,7 @@ function validateTransferOptions(options) {
|
|
|
65
198
|
errors.category = ["Must be one of: sportsbook, casino, dfs, sweepstakes"];
|
|
66
199
|
}
|
|
67
200
|
if (Object.keys(errors).length > 0) {
|
|
68
|
-
throw new
|
|
201
|
+
throw new EdgeValidationError2("Invalid transfer options", errors);
|
|
69
202
|
}
|
|
70
203
|
}
|
|
71
204
|
|
|
@@ -97,7 +230,11 @@ var EdgeUserClient = class {
|
|
|
97
230
|
}
|
|
98
231
|
return this.server._apiRequest("POST", "/transfer", this.accessToken, body);
|
|
99
232
|
}
|
|
233
|
+
async startTransferVerification(options) {
|
|
234
|
+
return startTransferVerification(this, options);
|
|
235
|
+
}
|
|
100
236
|
async getTransfer(transferId) {
|
|
237
|
+
validateTransferId(transferId);
|
|
101
238
|
return this.server._apiRequest("GET", `/transfer/${encodeURIComponent(transferId)}`, this.accessToken);
|
|
102
239
|
}
|
|
103
240
|
async listTransfers(params) {
|
|
@@ -118,6 +255,7 @@ var EdgeUserClient = class {
|
|
|
118
255
|
* The handoff token is single-use and expires in 120 seconds.
|
|
119
256
|
*/
|
|
120
257
|
async createVerificationSession(transferId, options) {
|
|
258
|
+
validateTransferId(transferId);
|
|
121
259
|
return this.server._apiRequest(
|
|
122
260
|
"POST",
|
|
123
261
|
`/transfer/${encodeURIComponent(transferId)}/verification-session`,
|
|
@@ -130,6 +268,7 @@ var EdgeUserClient = class {
|
|
|
130
268
|
* Use as an alternative to postMessage events.
|
|
131
269
|
*/
|
|
132
270
|
async getVerificationSessionStatus(transferId, sessionId) {
|
|
271
|
+
validateTransferId(transferId);
|
|
133
272
|
return this.server._apiRequest(
|
|
134
273
|
"GET",
|
|
135
274
|
`/transfer/${encodeURIComponent(transferId)}/verification-session/${encodeURIComponent(sessionId)}`,
|
|
@@ -214,16 +353,46 @@ function fromBase64Url(value) {
|
|
|
214
353
|
}
|
|
215
354
|
|
|
216
355
|
// src/mtls.ts
|
|
356
|
+
import fs from "fs";
|
|
217
357
|
import https from "https";
|
|
358
|
+
import tls from "tls";
|
|
359
|
+
var CERT_PATTERN = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
|
|
360
|
+
function normalizeCaBundle(ca) {
|
|
361
|
+
if (!ca) return [];
|
|
362
|
+
const values = Array.isArray(ca) ? ca : [ca];
|
|
363
|
+
return values.flatMap((value) => {
|
|
364
|
+
const normalized = value.replace(/\\n/g, "\n").trim();
|
|
365
|
+
if (!normalized) return [];
|
|
366
|
+
const certificates = normalized.match(CERT_PATTERN);
|
|
367
|
+
return certificates ? certificates.map((certificate) => certificate.trim()) : [normalized];
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function getNodeExtraCaCerts() {
|
|
371
|
+
const extraCaPath = process.env.NODE_EXTRA_CA_CERTS?.trim();
|
|
372
|
+
if (!extraCaPath) return [];
|
|
373
|
+
try {
|
|
374
|
+
return normalizeCaBundle(fs.readFileSync(extraCaPath, "utf8"));
|
|
375
|
+
} catch (error) {
|
|
376
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
377
|
+
throw new Error(`EdgeConnectServer: failed to read NODE_EXTRA_CA_CERTS from "${extraCaPath}": ${message}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function buildServerCaBundle(ca) {
|
|
381
|
+
const customCa = normalizeCaBundle(ca);
|
|
382
|
+
if (customCa.length === 0) return void 0;
|
|
383
|
+
return Array.from(/* @__PURE__ */ new Set([...tls.rootCertificates, ...getNodeExtraCaCerts(), ...customCa]));
|
|
384
|
+
}
|
|
218
385
|
function createHttpsAgent(config) {
|
|
386
|
+
const ca = buildServerCaBundle(config.ca);
|
|
219
387
|
return new https.Agent({
|
|
220
388
|
cert: config.cert,
|
|
221
389
|
key: config.key,
|
|
222
390
|
rejectUnauthorized: true,
|
|
223
|
-
...
|
|
391
|
+
...ca ? { ca } : {}
|
|
224
392
|
});
|
|
225
393
|
}
|
|
226
394
|
function createUndiciDispatcher(config) {
|
|
395
|
+
const ca = buildServerCaBundle(config.ca);
|
|
227
396
|
const moduleName = "undici";
|
|
228
397
|
const { Agent } = __require(moduleName);
|
|
229
398
|
return new Agent({
|
|
@@ -231,7 +400,7 @@ function createUndiciDispatcher(config) {
|
|
|
231
400
|
cert: config.cert,
|
|
232
401
|
key: config.key,
|
|
233
402
|
rejectUnauthorized: true,
|
|
234
|
-
...
|
|
403
|
+
...ca ? { ca } : {}
|
|
235
404
|
}
|
|
236
405
|
});
|
|
237
406
|
}
|
|
@@ -244,6 +413,112 @@ function validateMtlsConfig(config) {
|
|
|
244
413
|
}
|
|
245
414
|
}
|
|
246
415
|
|
|
416
|
+
// src/session.ts
|
|
417
|
+
import { EdgeAuthenticationError, EdgeValidationError as EdgeValidationError3 } from "@edge-markets/connect";
|
|
418
|
+
var EdgeUserSession = class {
|
|
419
|
+
constructor(options) {
|
|
420
|
+
if (!options.edge?.forUser || !options.edge?.refreshTokens) {
|
|
421
|
+
throw new EdgeValidationError3("EdgeUserSession requires an EdgeConnectServer instance", {
|
|
422
|
+
edge: ["Provide edge.forUser and edge.refreshTokens"]
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
const subjectId = typeof options.subjectId === "string" ? options.subjectId.trim() : "";
|
|
426
|
+
if (!subjectId) {
|
|
427
|
+
throw new EdgeValidationError3("EdgeUserSession subjectId is required", {
|
|
428
|
+
subjectId: ["Required"]
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
if (!options.tokenStore?.load || !options.tokenStore?.save) {
|
|
432
|
+
throw new EdgeValidationError3("EdgeUserSession tokenStore is required", {
|
|
433
|
+
tokenStore: ["Provide load(subjectId) and save(subjectId, value, context)"]
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (options.refreshSkewMs !== void 0 && (!Number.isFinite(options.refreshSkewMs) || options.refreshSkewMs < 0)) {
|
|
437
|
+
throw new EdgeValidationError3("refreshSkewMs must be a non-negative number", {
|
|
438
|
+
refreshSkewMs: ["Must be greater than or equal to 0"]
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
this.edge = options.edge;
|
|
442
|
+
this.subjectId = subjectId;
|
|
443
|
+
this.tokenStore = options.tokenStore;
|
|
444
|
+
this.tokenVault = options.tokenVault;
|
|
445
|
+
this.refreshSkewMs = options.refreshSkewMs ?? 6e4;
|
|
446
|
+
this.now = options.now ?? Date.now;
|
|
447
|
+
this.onRefresh = options.onRefresh;
|
|
448
|
+
}
|
|
449
|
+
async getValidTokens(options = {}) {
|
|
450
|
+
const tokens = await this.loadTokens();
|
|
451
|
+
if (!options.forceRefresh && tokens.expiresAt > this.now() + this.refreshSkewMs) {
|
|
452
|
+
return tokens;
|
|
453
|
+
}
|
|
454
|
+
const refreshed = await this.edge.refreshTokens(tokens.refreshToken);
|
|
455
|
+
await this.saveTokens(refreshed, true);
|
|
456
|
+
await this.onRefresh?.(refreshed);
|
|
457
|
+
return refreshed;
|
|
458
|
+
}
|
|
459
|
+
async getClient(options = {}) {
|
|
460
|
+
const tokens = await this.getValidTokens(options);
|
|
461
|
+
return this.edge.forUser(tokens.accessToken);
|
|
462
|
+
}
|
|
463
|
+
async withClient(callback) {
|
|
464
|
+
const tokens = await this.getValidTokens();
|
|
465
|
+
return callback(this.edge.forUser(tokens.accessToken), tokens);
|
|
466
|
+
}
|
|
467
|
+
async saveTokens(tokens, refreshed = false) {
|
|
468
|
+
validateTokens(tokens);
|
|
469
|
+
const value = this.tokenVault ? this.tokenVault.encryptTokens(tokens) : tokens;
|
|
470
|
+
await this.tokenStore.save(this.subjectId, value, { tokens, refreshed });
|
|
471
|
+
}
|
|
472
|
+
async loadTokens() {
|
|
473
|
+
const value = await this.tokenStore.load(this.subjectId);
|
|
474
|
+
if (!value) {
|
|
475
|
+
throw new EdgeAuthenticationError("No EDGE tokens are stored for this subject", {
|
|
476
|
+
subjectId: this.subjectId
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
if (typeof value === "string") {
|
|
480
|
+
if (!this.tokenVault) {
|
|
481
|
+
throw new EdgeAuthenticationError("Encrypted EDGE token envelope loaded but no tokenVault was configured", {
|
|
482
|
+
subjectId: this.subjectId
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
const tokens = this.tokenVault.decryptTokens(value);
|
|
486
|
+
validateTokens(tokens);
|
|
487
|
+
return tokens;
|
|
488
|
+
}
|
|
489
|
+
validateTokens(value);
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
function createEdgeUserSession(options) {
|
|
494
|
+
return new EdgeUserSession(options);
|
|
495
|
+
}
|
|
496
|
+
function validateTokens(tokens) {
|
|
497
|
+
const errors = {};
|
|
498
|
+
if (!tokens || typeof tokens !== "object") {
|
|
499
|
+
errors.tokens = ["Expected token object"];
|
|
500
|
+
} else {
|
|
501
|
+
if (typeof tokens.accessToken !== "string" || !tokens.accessToken.trim()) {
|
|
502
|
+
errors.accessToken = ["Required"];
|
|
503
|
+
}
|
|
504
|
+
if (typeof tokens.refreshToken !== "string" || !tokens.refreshToken.trim()) {
|
|
505
|
+
errors.refreshToken = ["Required"];
|
|
506
|
+
}
|
|
507
|
+
if (typeof tokens.expiresAt !== "number" || !Number.isFinite(tokens.expiresAt) || tokens.expiresAt <= 0) {
|
|
508
|
+
errors.expiresAt = ["Must be a positive Unix timestamp in milliseconds"];
|
|
509
|
+
}
|
|
510
|
+
if (typeof tokens.expiresIn !== "number" || !Number.isFinite(tokens.expiresIn) || tokens.expiresIn <= 0) {
|
|
511
|
+
errors.expiresIn = ["Must be a positive number"];
|
|
512
|
+
}
|
|
513
|
+
if (typeof tokens.scope !== "string") {
|
|
514
|
+
errors.scope = ["Required"];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (Object.keys(errors).length > 0) {
|
|
518
|
+
throw new EdgeValidationError3("Invalid EDGE token payload", errors);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
247
522
|
// src/types.ts
|
|
248
523
|
var DEFAULT_TIMEOUT = 3e4;
|
|
249
524
|
var USER_AGENT = "@edge-markets/connect-node/1.0.0";
|
|
@@ -254,12 +529,113 @@ var DEFAULT_RETRY_CONFIG = {
|
|
|
254
529
|
baseDelayMs: 1e3
|
|
255
530
|
};
|
|
256
531
|
|
|
532
|
+
// src/webhook-reconciler.ts
|
|
533
|
+
var DEFAULT_MAX_PAGES = 10;
|
|
534
|
+
var EdgeWebhookReconciler = class {
|
|
535
|
+
constructor(options) {
|
|
536
|
+
this.running = false;
|
|
537
|
+
if (!options.edge?.syncWebhookEvents) {
|
|
538
|
+
throw new Error("EdgeWebhookReconciler: edge.syncWebhookEvents is required");
|
|
539
|
+
}
|
|
540
|
+
if (!options.cursorStore?.load || !options.cursorStore?.save) {
|
|
541
|
+
throw new Error("EdgeWebhookReconciler: cursorStore.load and cursorStore.save are required");
|
|
542
|
+
}
|
|
543
|
+
if (!options.processEvent) {
|
|
544
|
+
throw new Error("EdgeWebhookReconciler: processEvent is required");
|
|
545
|
+
}
|
|
546
|
+
if (options.maxPages !== void 0 && (!Number.isInteger(options.maxPages) || options.maxPages <= 0)) {
|
|
547
|
+
throw new Error("EdgeWebhookReconciler: maxPages must be a positive integer");
|
|
548
|
+
}
|
|
549
|
+
this.options = options;
|
|
550
|
+
}
|
|
551
|
+
async run() {
|
|
552
|
+
if (this.running) {
|
|
553
|
+
return {
|
|
554
|
+
processed: 0,
|
|
555
|
+
pages: 0,
|
|
556
|
+
hasMore: false,
|
|
557
|
+
skipped: true,
|
|
558
|
+
reason: "already_running"
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
this.running = true;
|
|
562
|
+
try {
|
|
563
|
+
return await this.runInternal();
|
|
564
|
+
} finally {
|
|
565
|
+
this.running = false;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
async runInternal() {
|
|
569
|
+
let cursor = normalizeCursor(await this.options.cursorStore.load());
|
|
570
|
+
let processed = 0;
|
|
571
|
+
let pages = 0;
|
|
572
|
+
let hasMore = false;
|
|
573
|
+
const maxPages = this.options.maxPages ?? DEFAULT_MAX_PAGES;
|
|
574
|
+
while (pages < maxPages) {
|
|
575
|
+
const result = await this.options.edge.syncWebhookEvents({
|
|
576
|
+
...cursor ? { afterEventId: cursor } : {},
|
|
577
|
+
...this.options.limit !== void 0 ? { limit: this.options.limit } : {}
|
|
578
|
+
});
|
|
579
|
+
pages++;
|
|
580
|
+
hasMore = result.hasMore;
|
|
581
|
+
if (result.events.length === 0) {
|
|
582
|
+
return {
|
|
583
|
+
processed,
|
|
584
|
+
pages,
|
|
585
|
+
...cursor ? { cursor } : {},
|
|
586
|
+
hasMore,
|
|
587
|
+
skipped: false,
|
|
588
|
+
...hasMore ? { reason: "empty_page_with_has_more" } : {}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
for (const event of result.events) {
|
|
592
|
+
await this.options.processEvent(event);
|
|
593
|
+
cursor = event.id;
|
|
594
|
+
await this.options.cursorStore.save(cursor);
|
|
595
|
+
processed++;
|
|
596
|
+
}
|
|
597
|
+
if (!result.hasMore) {
|
|
598
|
+
return {
|
|
599
|
+
processed,
|
|
600
|
+
pages,
|
|
601
|
+
...cursor ? { cursor } : {},
|
|
602
|
+
hasMore: false,
|
|
603
|
+
skipped: false
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
processed,
|
|
609
|
+
pages,
|
|
610
|
+
...cursor ? { cursor } : {},
|
|
611
|
+
hasMore,
|
|
612
|
+
skipped: false,
|
|
613
|
+
...hasMore ? { reason: "max_pages_reached" } : {}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
function createWebhookReconciler(options) {
|
|
618
|
+
return new EdgeWebhookReconciler(options);
|
|
619
|
+
}
|
|
620
|
+
function normalizeCursor(cursor) {
|
|
621
|
+
if (typeof cursor !== "string") return void 0;
|
|
622
|
+
const trimmed = cursor.trim();
|
|
623
|
+
return trimmed || void 0;
|
|
624
|
+
}
|
|
625
|
+
|
|
257
626
|
// src/edge-connect-server.ts
|
|
258
627
|
var instances = /* @__PURE__ */ new Map();
|
|
259
628
|
function getInstanceKey(config) {
|
|
260
|
-
return
|
|
629
|
+
return JSON.stringify([
|
|
630
|
+
config.clientId,
|
|
631
|
+
config.environment,
|
|
632
|
+
config.apiBaseUrl || "",
|
|
633
|
+
config.oauthBaseUrl || "",
|
|
634
|
+
config.partnerApiBaseUrl || "",
|
|
635
|
+
config.partnerClientId || ""
|
|
636
|
+
]);
|
|
261
637
|
}
|
|
262
|
-
var MTLS_ENFORCED_HOSTS = /* @__PURE__ */ new Set(["connect-staging.edgeboost.io", "
|
|
638
|
+
var MTLS_ENFORCED_HOSTS = /* @__PURE__ */ new Set(["connect-staging.edgeboost.io", "connect.edgeboost.io"]);
|
|
263
639
|
function isMtlsEnforcedHost(host) {
|
|
264
640
|
if (MTLS_ENFORCED_HOSTS.has(host)) {
|
|
265
641
|
return true;
|
|
@@ -281,19 +657,32 @@ function environmentRequiresMtls(environment, apiBaseUrl) {
|
|
|
281
657
|
return false;
|
|
282
658
|
}
|
|
283
659
|
}
|
|
284
|
-
var
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const instance = new _EdgeConnectServer(config);
|
|
290
|
-
instances.set(key, instance);
|
|
291
|
-
return instance;
|
|
660
|
+
var CONNECT_API_BASE_PATH = "/connect/v1";
|
|
661
|
+
function stripTrailingSlash(value) {
|
|
662
|
+
let end = value.length;
|
|
663
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
664
|
+
end--;
|
|
292
665
|
}
|
|
293
|
-
|
|
294
|
-
|
|
666
|
+
return value.slice(0, end);
|
|
667
|
+
}
|
|
668
|
+
function derivePartnerApiBaseUrl(apiBaseUrl) {
|
|
669
|
+
const url = new URL(apiBaseUrl);
|
|
670
|
+
const normalizedPath = stripTrailingSlash(url.pathname);
|
|
671
|
+
if (normalizedPath.endsWith(CONNECT_API_BASE_PATH)) {
|
|
672
|
+
const prefix = normalizedPath.slice(0, -CONNECT_API_BASE_PATH.length);
|
|
673
|
+
url.pathname = `${prefix}/v1`;
|
|
674
|
+
} else {
|
|
675
|
+
url.pathname = normalizedPath || "/";
|
|
295
676
|
}
|
|
677
|
+
url.search = "";
|
|
678
|
+
url.hash = "";
|
|
679
|
+
return stripTrailingSlash(url.toString());
|
|
680
|
+
}
|
|
681
|
+
var SYNC_WEBHOOK_EVENTS_MIN_LIMIT = 1;
|
|
682
|
+
var SYNC_WEBHOOK_EVENTS_MAX_LIMIT = 100;
|
|
683
|
+
var EdgeConnectServer = class _EdgeConnectServer {
|
|
296
684
|
constructor(config) {
|
|
685
|
+
this.partnerTokenCache = null;
|
|
297
686
|
if (!config.clientId) {
|
|
298
687
|
throw new Error("EdgeConnectServer: clientId is required");
|
|
299
688
|
}
|
|
@@ -306,6 +695,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
306
695
|
this.config = config;
|
|
307
696
|
const envConfig = getEnvironmentConfig(config.environment);
|
|
308
697
|
this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
|
|
698
|
+
this.partnerApiBaseUrl = stripTrailingSlash(config.partnerApiBaseUrl || derivePartnerApiBaseUrl(this.apiBaseUrl));
|
|
309
699
|
this.oauthBaseUrl = config.oauthBaseUrl || envConfig.oauthBaseUrl;
|
|
310
700
|
this.timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
311
701
|
this.retryConfig = {
|
|
@@ -322,6 +712,17 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
322
712
|
);
|
|
323
713
|
}
|
|
324
714
|
}
|
|
715
|
+
static getInstance(config) {
|
|
716
|
+
const key = getInstanceKey(config);
|
|
717
|
+
const existing = instances.get(key);
|
|
718
|
+
if (existing) return existing;
|
|
719
|
+
const instance = new _EdgeConnectServer(config);
|
|
720
|
+
instances.set(key, instance);
|
|
721
|
+
return instance;
|
|
722
|
+
}
|
|
723
|
+
static clearInstances() {
|
|
724
|
+
instances.clear();
|
|
725
|
+
}
|
|
325
726
|
/**
|
|
326
727
|
* Returns the `node:https.Agent` configured with mTLS client certificates.
|
|
327
728
|
* Use this with axios, got, or other HTTP clients. Returns `undefined` if mTLS is not configured.
|
|
@@ -337,10 +738,13 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
337
738
|
*/
|
|
338
739
|
forUser(accessToken) {
|
|
339
740
|
if (!accessToken) {
|
|
340
|
-
throw new
|
|
741
|
+
throw new EdgeAuthenticationError2("accessToken is required when calling forUser()");
|
|
341
742
|
}
|
|
342
743
|
return new EdgeUserClient(this, accessToken);
|
|
343
744
|
}
|
|
745
|
+
createUserSession(options) {
|
|
746
|
+
return new EdgeUserSession({ ...options, edge: this });
|
|
747
|
+
}
|
|
344
748
|
async exchangeCode(code, codeVerifier, redirectUri) {
|
|
345
749
|
const tokenUrl = `${this.oauthBaseUrl}/token`;
|
|
346
750
|
const body = {
|
|
@@ -398,7 +802,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
398
802
|
);
|
|
399
803
|
if (!response.ok) {
|
|
400
804
|
const error = await response.json().catch(() => ({}));
|
|
401
|
-
throw new
|
|
805
|
+
throw new EdgeAuthenticationError2(
|
|
402
806
|
error.message || error.error_description || "Token refresh failed. User may need to reconnect.",
|
|
403
807
|
{ tokenError: error }
|
|
404
808
|
);
|
|
@@ -492,6 +896,318 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
492
896
|
lastError
|
|
493
897
|
);
|
|
494
898
|
}
|
|
899
|
+
/**
|
|
900
|
+
* Reconcile webhook events from the partner sync endpoint.
|
|
901
|
+
*
|
|
902
|
+
* Use this from a scheduled cron (every ~5 minutes is typical) to catch
|
|
903
|
+
* events your primary webhook receiver missed — partner downtime, retry
|
|
904
|
+
* exhaustion, dead-letter queue. This is the third reliability layer
|
|
905
|
+
* ("Leg 3") on top of HTTP delivery + DLQ.
|
|
906
|
+
*
|
|
907
|
+
* **Authentication.** This method uses the OAuth `client_credentials`
|
|
908
|
+
* grant against {@link EdgeConnectServerConfig.partnerClientId} and
|
|
909
|
+
* {@link EdgeConnectServerConfig.partnerClientSecret}. The token is
|
|
910
|
+
* cached internally and refreshed automatically.
|
|
911
|
+
*
|
|
912
|
+
* **Pagination.** Pass `afterEventId` with the last `id` you processed.
|
|
913
|
+
* Loop while `hasMore` is true. The server enforces a 30-day lookback;
|
|
914
|
+
* cursors older than 30 days return events from 30 days ago, not an error.
|
|
915
|
+
*
|
|
916
|
+
* @example
|
|
917
|
+
* ```typescript
|
|
918
|
+
* let cursor: string | undefined = await loadCursorFromDb()
|
|
919
|
+
*
|
|
920
|
+
* while (true) {
|
|
921
|
+
* const { events, hasMore } = await edge.syncWebhookEvents({ afterEventId: cursor })
|
|
922
|
+
* for (const event of events) {
|
|
923
|
+
* await processEvent(event)
|
|
924
|
+
* cursor = event.id
|
|
925
|
+
* }
|
|
926
|
+
* if (events.length > 0) await saveCursorToDb(cursor!)
|
|
927
|
+
* if (!hasMore) break
|
|
928
|
+
* }
|
|
929
|
+
* ```
|
|
930
|
+
*
|
|
931
|
+
* @throws {EdgeAuthenticationError} If partner credentials are missing
|
|
932
|
+
* or the partner token cannot be obtained / refreshed
|
|
933
|
+
* @throws {EdgeNetworkError} On network failure
|
|
934
|
+
* @throws {EdgeApiError} For other non-2xx responses after retries
|
|
935
|
+
*/
|
|
936
|
+
async syncWebhookEvents(options = {}) {
|
|
937
|
+
if (!this.config.partnerClientId) {
|
|
938
|
+
throw new EdgeAuthenticationError2("syncWebhookEvents requires `partnerClientId` in EdgeConnectServer config");
|
|
939
|
+
}
|
|
940
|
+
if (!this.config.partnerClientSecret) {
|
|
941
|
+
throw new EdgeAuthenticationError2("syncWebhookEvents requires `partnerClientSecret` in EdgeConnectServer config");
|
|
942
|
+
}
|
|
943
|
+
const normalizedOptions = this.normalizeSyncWebhookEventsOptions(options);
|
|
944
|
+
const url = this.buildSyncUrl(normalizedOptions);
|
|
945
|
+
let token = await this.getPartnerToken();
|
|
946
|
+
let { response, durationMs, attempt } = await this.partnerFetch(url, token);
|
|
947
|
+
if (response.status === 401) {
|
|
948
|
+
await this.drainResponseBody(response);
|
|
949
|
+
this.config.onResponse?.({
|
|
950
|
+
status: response.status,
|
|
951
|
+
body: { error: "unauthorized" },
|
|
952
|
+
durationMs,
|
|
953
|
+
method: "GET",
|
|
954
|
+
url,
|
|
955
|
+
endpoint: "partner_webhook_sync",
|
|
956
|
+
attempt
|
|
957
|
+
});
|
|
958
|
+
this.partnerTokenCache = null;
|
|
959
|
+
token = await this.getPartnerToken();
|
|
960
|
+
({ response, durationMs, attempt } = await this.partnerFetch(url, token));
|
|
961
|
+
if (response.status === 401) {
|
|
962
|
+
const body = await response.json().catch(() => ({}));
|
|
963
|
+
this.config.onResponse?.({
|
|
964
|
+
status: response.status,
|
|
965
|
+
body: this.redactPartnerSyncResponseBody(body),
|
|
966
|
+
durationMs,
|
|
967
|
+
method: "GET",
|
|
968
|
+
url,
|
|
969
|
+
endpoint: "partner_webhook_sync",
|
|
970
|
+
attempt
|
|
971
|
+
});
|
|
972
|
+
throw new EdgeAuthenticationError2(
|
|
973
|
+
body.message || "Partner token rejected after refresh \u2014 check partnerClientId/Secret",
|
|
974
|
+
body
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (!response.ok) {
|
|
979
|
+
const body = await response.json().catch(() => ({}));
|
|
980
|
+
this.config.onResponse?.({
|
|
981
|
+
status: response.status,
|
|
982
|
+
body: this.redactPartnerSyncResponseBody(body),
|
|
983
|
+
durationMs,
|
|
984
|
+
method: "GET",
|
|
985
|
+
url,
|
|
986
|
+
endpoint: "partner_webhook_sync",
|
|
987
|
+
attempt
|
|
988
|
+
});
|
|
989
|
+
throw new EdgeApiError(
|
|
990
|
+
body.error || "sync_failed",
|
|
991
|
+
body.message || `syncWebhookEvents failed with status ${response.status}`,
|
|
992
|
+
response.status,
|
|
993
|
+
body
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
const wire = await response.json().catch(() => ({}));
|
|
997
|
+
this.config.onResponse?.({
|
|
998
|
+
status: response.status,
|
|
999
|
+
body: {
|
|
1000
|
+
eventCount: wire.events?.length ?? 0,
|
|
1001
|
+
hasMore: !!wire.has_more
|
|
1002
|
+
},
|
|
1003
|
+
durationMs,
|
|
1004
|
+
method: "GET",
|
|
1005
|
+
url,
|
|
1006
|
+
endpoint: "partner_webhook_sync",
|
|
1007
|
+
attempt
|
|
1008
|
+
});
|
|
1009
|
+
const events = (wire.events ?? []).map((entry) => entry.payload).filter((p) => !!p);
|
|
1010
|
+
return {
|
|
1011
|
+
events,
|
|
1012
|
+
hasMore: !!wire.has_more
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Creates a cursor-based webhook reconciler bound to this SDK instance.
|
|
1017
|
+
*
|
|
1018
|
+
* Use this from cron jobs to process events returned by
|
|
1019
|
+
* {@link EdgeConnectServer.syncWebhookEvents} without hand-writing
|
|
1020
|
+
* pagination, cursor advancement, or process-level concurrency guards.
|
|
1021
|
+
*/
|
|
1022
|
+
createWebhookReconciler(options) {
|
|
1023
|
+
return new EdgeWebhookReconciler({ ...options, edge: this });
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Builds the sync URL with normalized query parameters.
|
|
1027
|
+
*/
|
|
1028
|
+
normalizeSyncWebhookEventsOptions(options) {
|
|
1029
|
+
const normalized = {};
|
|
1030
|
+
if (typeof options.afterEventId === "string" && options.afterEventId.trim()) {
|
|
1031
|
+
normalized.afterEventId = options.afterEventId.trim();
|
|
1032
|
+
}
|
|
1033
|
+
if (options.limit !== void 0) {
|
|
1034
|
+
if (!Number.isInteger(options.limit) || options.limit < SYNC_WEBHOOK_EVENTS_MIN_LIMIT || options.limit > SYNC_WEBHOOK_EVENTS_MAX_LIMIT) {
|
|
1035
|
+
throw new EdgeApiError(
|
|
1036
|
+
"invalid_sync_options",
|
|
1037
|
+
`syncWebhookEvents limit must be an integer between ${SYNC_WEBHOOK_EVENTS_MIN_LIMIT} and ${SYNC_WEBHOOK_EVENTS_MAX_LIMIT}`,
|
|
1038
|
+
400,
|
|
1039
|
+
{ limit: options.limit }
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
normalized.limit = options.limit;
|
|
1043
|
+
}
|
|
1044
|
+
return normalized;
|
|
1045
|
+
}
|
|
1046
|
+
buildSyncUrl(options) {
|
|
1047
|
+
const url = new URL(`${this.partnerApiBaseUrl}/partner/webhooks/events/sync`);
|
|
1048
|
+
if (options.afterEventId) {
|
|
1049
|
+
url.searchParams.set("after_event_id", options.afterEventId);
|
|
1050
|
+
}
|
|
1051
|
+
if (options.limit !== void 0) {
|
|
1052
|
+
url.searchParams.set("limit", String(options.limit));
|
|
1053
|
+
}
|
|
1054
|
+
return url.toString();
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Issues a GET to the sync endpoint with the partner Bearer token. Kept
|
|
1058
|
+
* separate from `_apiRequest` because partner tokens have a different
|
|
1059
|
+
* lifecycle (process-wide, client_credentials) than user tokens.
|
|
1060
|
+
*/
|
|
1061
|
+
async partnerFetch(url, token) {
|
|
1062
|
+
let lastError = null;
|
|
1063
|
+
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
1064
|
+
if (attempt > 0) {
|
|
1065
|
+
await this.sleep(this.getRetryDelay(attempt - 1));
|
|
1066
|
+
}
|
|
1067
|
+
const startTime = Date.now();
|
|
1068
|
+
this.config.onRequest?.({
|
|
1069
|
+
method: "GET",
|
|
1070
|
+
url,
|
|
1071
|
+
endpoint: "partner_webhook_sync",
|
|
1072
|
+
attempt
|
|
1073
|
+
});
|
|
1074
|
+
try {
|
|
1075
|
+
const response = await this.fetchWithTimeout(
|
|
1076
|
+
url,
|
|
1077
|
+
{
|
|
1078
|
+
method: "GET",
|
|
1079
|
+
headers: {
|
|
1080
|
+
Authorization: `Bearer ${token}`,
|
|
1081
|
+
"Content-Type": "application/json",
|
|
1082
|
+
"User-Agent": USER_AGENT
|
|
1083
|
+
}
|
|
1084
|
+
},
|
|
1085
|
+
this.dispatcher
|
|
1086
|
+
);
|
|
1087
|
+
const durationMs = Date.now() - startTime;
|
|
1088
|
+
if (response.status !== 401 && this.retryConfig.retryOn.includes(response.status) && attempt < this.retryConfig.maxRetries) {
|
|
1089
|
+
await this.drainResponseBody(response);
|
|
1090
|
+
this.config.onResponse?.({
|
|
1091
|
+
status: response.status,
|
|
1092
|
+
body: { retrying: true },
|
|
1093
|
+
durationMs,
|
|
1094
|
+
method: "GET",
|
|
1095
|
+
url,
|
|
1096
|
+
endpoint: "partner_webhook_sync",
|
|
1097
|
+
attempt
|
|
1098
|
+
});
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
return { response, durationMs, attempt };
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
lastError = error;
|
|
1104
|
+
if (attempt >= this.retryConfig.maxRetries) {
|
|
1105
|
+
throw new EdgeNetworkError("syncWebhookEvents network failure", lastError);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
throw new EdgeNetworkError(
|
|
1110
|
+
`syncWebhookEvents network failure after ${this.retryConfig.maxRetries} retries`,
|
|
1111
|
+
lastError
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Fetches (or returns the cached) partner token via the
|
|
1116
|
+
* `client_credentials` grant. Caches with a 60s safety margin so the
|
|
1117
|
+
* token is renewed *before* the server-side expiry, never racing it.
|
|
1118
|
+
*
|
|
1119
|
+
* Side-effects: writes `this.partnerTokenCache` on success.
|
|
1120
|
+
*/
|
|
1121
|
+
async getPartnerToken() {
|
|
1122
|
+
const cached = this.partnerTokenCache;
|
|
1123
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
1124
|
+
return cached.token;
|
|
1125
|
+
}
|
|
1126
|
+
const tokenUrl = `${this.oauthBaseUrl}/token`;
|
|
1127
|
+
let response;
|
|
1128
|
+
const startTime = Date.now();
|
|
1129
|
+
this.config.onRequest?.({
|
|
1130
|
+
method: "POST",
|
|
1131
|
+
url: tokenUrl,
|
|
1132
|
+
endpoint: "oauth_token",
|
|
1133
|
+
body: {
|
|
1134
|
+
grant_type: "client_credentials",
|
|
1135
|
+
client_id: this.config.partnerClientId,
|
|
1136
|
+
client_secret: "[REDACTED]"
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
try {
|
|
1140
|
+
response = await this.fetchWithTimeout(
|
|
1141
|
+
tokenUrl,
|
|
1142
|
+
{
|
|
1143
|
+
method: "POST",
|
|
1144
|
+
headers: {
|
|
1145
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1146
|
+
"User-Agent": USER_AGENT
|
|
1147
|
+
},
|
|
1148
|
+
body: new URLSearchParams({
|
|
1149
|
+
grant_type: "client_credentials",
|
|
1150
|
+
client_id: this.config.partnerClientId,
|
|
1151
|
+
client_secret: this.config.partnerClientSecret
|
|
1152
|
+
}).toString()
|
|
1153
|
+
},
|
|
1154
|
+
this.dispatcher
|
|
1155
|
+
);
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
throw new EdgeNetworkError("Partner token request network failure", error);
|
|
1158
|
+
}
|
|
1159
|
+
const durationMs = Date.now() - startTime;
|
|
1160
|
+
const data = await response.json().catch(() => ({}));
|
|
1161
|
+
this.config.onResponse?.({
|
|
1162
|
+
status: response.status,
|
|
1163
|
+
body: this.redactPartnerTokenResponseBody(data),
|
|
1164
|
+
durationMs,
|
|
1165
|
+
method: "POST",
|
|
1166
|
+
url: tokenUrl,
|
|
1167
|
+
endpoint: "oauth_token"
|
|
1168
|
+
});
|
|
1169
|
+
if (!response.ok) {
|
|
1170
|
+
throw new EdgeAuthenticationError2(
|
|
1171
|
+
data.message || data.error_description || `Partner token request failed with status ${response.status}`,
|
|
1172
|
+
data
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
if (!data.access_token || typeof data.expires_in !== "number") {
|
|
1176
|
+
throw new EdgeAuthenticationError2("Partner token response missing access_token or expires_in");
|
|
1177
|
+
}
|
|
1178
|
+
this.partnerTokenCache = {
|
|
1179
|
+
token: data.access_token,
|
|
1180
|
+
expiresAt: Date.now() + (data.expires_in - 60) * 1e3
|
|
1181
|
+
};
|
|
1182
|
+
return data.access_token;
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* @internal Test-only: clears the cached partner token so the next call
|
|
1186
|
+
* to `syncWebhookEvents` re-fetches it. Not part of the public API.
|
|
1187
|
+
*/
|
|
1188
|
+
_resetPartnerTokenCache() {
|
|
1189
|
+
this.partnerTokenCache = null;
|
|
1190
|
+
}
|
|
1191
|
+
redactPartnerTokenResponseBody(body) {
|
|
1192
|
+
return {
|
|
1193
|
+
...body,
|
|
1194
|
+
...typeof body.access_token === "string" ? { access_token: "[REDACTED]" } : {},
|
|
1195
|
+
...typeof body.refresh_token === "string" ? { refresh_token: "[REDACTED]" } : {},
|
|
1196
|
+
...typeof body.id_token === "string" ? { id_token: "[REDACTED]" } : {}
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
redactPartnerSyncResponseBody(body) {
|
|
1200
|
+
if (Array.isArray(body.events)) {
|
|
1201
|
+
return {
|
|
1202
|
+
...body,
|
|
1203
|
+
events: `[REDACTED ${body.events.length} events]`
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
return body;
|
|
1207
|
+
}
|
|
1208
|
+
async drainResponseBody(response) {
|
|
1209
|
+
await response.arrayBuffer().catch(() => void 0);
|
|
1210
|
+
}
|
|
495
1211
|
getRetryDelay(attempt) {
|
|
496
1212
|
const { backoff, baseDelayMs } = this.retryConfig;
|
|
497
1213
|
if (backoff === "exponential") {
|
|
@@ -537,7 +1253,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
537
1253
|
});
|
|
538
1254
|
}
|
|
539
1255
|
if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
|
|
540
|
-
return new
|
|
1256
|
+
return new EdgeAuthenticationError2("Invalid client credentials. Check your client ID and secret.", {
|
|
541
1257
|
edgeBoostError: error
|
|
542
1258
|
});
|
|
543
1259
|
}
|
|
@@ -551,7 +1267,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
551
1267
|
}
|
|
552
1268
|
async handleApiErrorFromBody(error, status, path) {
|
|
553
1269
|
if (status === 401) {
|
|
554
|
-
return new
|
|
1270
|
+
return new EdgeAuthenticationError2(error.message || "Access token is invalid or expired", error);
|
|
555
1271
|
}
|
|
556
1272
|
if (status === 403) {
|
|
557
1273
|
if (error.error === "consent_required") {
|
|
@@ -601,17 +1317,709 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
601
1317
|
}
|
|
602
1318
|
};
|
|
603
1319
|
|
|
604
|
-
// src/
|
|
1320
|
+
// src/env.ts
|
|
1321
|
+
import { EDGE_ENVIRONMENTS, EdgeValidationError as EdgeValidationError4, getEnvironmentConfig as getEnvironmentConfig2 } from "@edge-markets/connect";
|
|
1322
|
+
import fs2 from "fs";
|
|
1323
|
+
var ENVIRONMENT_NAMES = new Set(Object.keys(EDGE_ENVIRONMENTS));
|
|
1324
|
+
function readEnv(env, name) {
|
|
1325
|
+
const value = env[name];
|
|
1326
|
+
if (typeof value !== "string") return void 0;
|
|
1327
|
+
const trimmed = value.trim();
|
|
1328
|
+
return trimmed ? trimmed : void 0;
|
|
1329
|
+
}
|
|
1330
|
+
function readFirstEnv(env, names) {
|
|
1331
|
+
for (const name of names) {
|
|
1332
|
+
const value = readEnv(env, name);
|
|
1333
|
+
if (value) return value;
|
|
1334
|
+
}
|
|
1335
|
+
return void 0;
|
|
1336
|
+
}
|
|
1337
|
+
function normalizePem(value) {
|
|
1338
|
+
return value.replace(/\\n/g, "\n").trim();
|
|
1339
|
+
}
|
|
1340
|
+
function readPemValue(env, valueName, pathName, warnings) {
|
|
1341
|
+
const inlineValue = readEnv(env, valueName);
|
|
1342
|
+
const filePath = readEnv(env, pathName);
|
|
1343
|
+
if (inlineValue && filePath) {
|
|
1344
|
+
warnings.push(`${valueName} and ${pathName} are both set; using ${valueName}`);
|
|
1345
|
+
}
|
|
1346
|
+
if (inlineValue) {
|
|
1347
|
+
return normalizePem(inlineValue);
|
|
1348
|
+
}
|
|
1349
|
+
if (!filePath) {
|
|
1350
|
+
return void 0;
|
|
1351
|
+
}
|
|
1352
|
+
try {
|
|
1353
|
+
return normalizePem(fs2.readFileSync(filePath, "utf8"));
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1356
|
+
throw new EdgeValidationError4(`Failed to read ${pathName}`, {
|
|
1357
|
+
[pathName]: [`Unable to read PEM file "${filePath}": ${message}`]
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
function readEnvironment(env) {
|
|
1362
|
+
const value = readEnv(env, "EDGE_ENVIRONMENT") || "staging";
|
|
1363
|
+
if (!ENVIRONMENT_NAMES.has(value)) {
|
|
1364
|
+
throw new EdgeValidationError4("Invalid EDGE_ENVIRONMENT", {
|
|
1365
|
+
EDGE_ENVIRONMENT: [`Must be one of: ${Array.from(ENVIRONMENT_NAMES).join(", ")}`]
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
return value;
|
|
1369
|
+
}
|
|
1370
|
+
function readTimeout(env, override) {
|
|
1371
|
+
if (override !== void 0) return override;
|
|
1372
|
+
const value = readFirstEnv(env, ["EDGE_TIMEOUT_MS", "EDGE_REQUEST_TIMEOUT_MS"]);
|
|
1373
|
+
if (!value) return void 0;
|
|
1374
|
+
const timeout = Number(value);
|
|
1375
|
+
if (!Number.isInteger(timeout) || timeout <= 0) {
|
|
1376
|
+
throw new EdgeValidationError4("Invalid EDGE_TIMEOUT_MS", {
|
|
1377
|
+
EDGE_TIMEOUT_MS: ["Must be a positive integer number of milliseconds"]
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
return timeout;
|
|
1381
|
+
}
|
|
1382
|
+
function buildMtlsConfig(env, requireMtls, warnings) {
|
|
1383
|
+
const cert = readPemValue(env, "EDGE_MTLS_CERT", "EDGE_MTLS_CERT_PATH", warnings);
|
|
1384
|
+
const key = readPemValue(env, "EDGE_MTLS_KEY", "EDGE_MTLS_KEY_PATH", warnings);
|
|
1385
|
+
const ca = readPemValue(env, "EDGE_MTLS_CA", "EDGE_MTLS_CA_PATH", warnings);
|
|
1386
|
+
if (!cert && !key && !ca) {
|
|
1387
|
+
if (requireMtls) {
|
|
1388
|
+
throw new EdgeValidationError4("mTLS is required but no EDGE_MTLS_CERT or EDGE_MTLS_KEY was provided", {
|
|
1389
|
+
EDGE_MTLS_CERT: ["Required when requireMtls is true"],
|
|
1390
|
+
EDGE_MTLS_KEY: ["Required when requireMtls is true"]
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
return void 0;
|
|
1394
|
+
}
|
|
1395
|
+
if (!cert || !key) {
|
|
1396
|
+
throw new EdgeValidationError4("Incomplete mTLS configuration", {
|
|
1397
|
+
EDGE_MTLS_CERT: cert ? [] : ["Required when mTLS is configured"],
|
|
1398
|
+
EDGE_MTLS_KEY: key ? [] : ["Required when mTLS is configured"]
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
return {
|
|
1402
|
+
enabled: true,
|
|
1403
|
+
cert,
|
|
1404
|
+
key,
|
|
1405
|
+
...ca ? { ca } : {}
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function inferMtlsRequirement(environment, apiBaseUrl) {
|
|
1409
|
+
if (environment === "staging" || environment === "production") return true;
|
|
1410
|
+
const resolvedApiBaseUrl = apiBaseUrl || getEnvironmentConfig2(environment).apiBaseUrl;
|
|
1411
|
+
try {
|
|
1412
|
+
const host = new URL(resolvedApiBaseUrl).host;
|
|
1413
|
+
return host === "connect-staging.edgeboost.io" || host === "connect.edgeboost.io";
|
|
1414
|
+
} catch {
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function createEdgeConnectServerFromEnv(options = {}) {
|
|
1419
|
+
const env = options.env ?? process.env;
|
|
1420
|
+
const warnings = [];
|
|
1421
|
+
const environment = readEnvironment(env);
|
|
1422
|
+
const apiBaseUrl = readEnv(env, "EDGE_API_BASE_URL");
|
|
1423
|
+
const mtlsRequiredByTarget = inferMtlsRequirement(environment, apiBaseUrl);
|
|
1424
|
+
const requireMtls = options.requireMtls ?? false;
|
|
1425
|
+
const mtls = buildMtlsConfig(env, requireMtls, warnings);
|
|
1426
|
+
const timeout = readTimeout(env, options.timeout);
|
|
1427
|
+
const clientId = readFirstEnv(env, ["EDGE_CLIENT_ID", "EDGE_CONNECT_CLIENT_ID"]) || "";
|
|
1428
|
+
const clientSecret = readFirstEnv(env, ["EDGE_CLIENT_SECRET", "EDGE_CONNECT_CLIENT_SECRET"]) || "";
|
|
1429
|
+
const oauthBaseUrl = readEnv(env, "EDGE_OAUTH_BASE_URL");
|
|
1430
|
+
const partnerApiBaseUrl = readEnv(env, "EDGE_PARTNER_API_BASE_URL");
|
|
1431
|
+
const partnerClientId = readFirstEnv(env, ["EDGE_PARTNER_CLIENT_ID", "EDGE_DASHBOARD_CLIENT_ID"]);
|
|
1432
|
+
const partnerClientSecret = readFirstEnv(env, ["EDGE_PARTNER_CLIENT_SECRET", "EDGE_DASHBOARD_CLIENT_SECRET"]);
|
|
1433
|
+
const validationErrors = {};
|
|
1434
|
+
if (!clientId) validationErrors.EDGE_CLIENT_ID = ["Required"];
|
|
1435
|
+
if (!clientSecret) validationErrors.EDGE_CLIENT_SECRET = ["Required"];
|
|
1436
|
+
if (Object.keys(validationErrors).length > 0) {
|
|
1437
|
+
throw new EdgeValidationError4("Missing required EDGE Connect environment variables", validationErrors);
|
|
1438
|
+
}
|
|
1439
|
+
if (mtlsRequiredByTarget && !mtls) {
|
|
1440
|
+
warnings.push(
|
|
1441
|
+
`EDGE_ENVIRONMENT=${environment} uses an mTLS-enforced EDGE Connect target; configure EDGE_MTLS_CERT and EDGE_MTLS_KEY before making API calls.`
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
const config = {
|
|
1445
|
+
clientId,
|
|
1446
|
+
clientSecret,
|
|
1447
|
+
environment,
|
|
1448
|
+
...apiBaseUrl ? { apiBaseUrl } : {},
|
|
1449
|
+
...oauthBaseUrl ? { oauthBaseUrl } : {},
|
|
1450
|
+
...partnerApiBaseUrl ? { partnerApiBaseUrl } : {},
|
|
1451
|
+
...partnerClientId ? { partnerClientId } : {},
|
|
1452
|
+
...partnerClientSecret ? { partnerClientSecret } : {},
|
|
1453
|
+
...timeout ? { timeout } : {},
|
|
1454
|
+
...options.retry ? { retry: options.retry } : {},
|
|
1455
|
+
...options.onRequest ? { onRequest: options.onRequest } : {},
|
|
1456
|
+
...options.onResponse ? { onResponse: options.onResponse } : {},
|
|
1457
|
+
...options.mle ? { mle: options.mle } : {},
|
|
1458
|
+
...mtls ? { mtls } : {}
|
|
1459
|
+
};
|
|
1460
|
+
const server = new EdgeConnectServer(config);
|
|
1461
|
+
return {
|
|
1462
|
+
server,
|
|
1463
|
+
config,
|
|
1464
|
+
capabilities: {
|
|
1465
|
+
mtlsEnabled: !!config.mtls?.enabled,
|
|
1466
|
+
webhookSyncConfigured: !!config.partnerClientId && !!config.partnerClientSecret,
|
|
1467
|
+
customApiBaseUrl: !!config.apiBaseUrl,
|
|
1468
|
+
customOAuthBaseUrl: !!config.oauthBaseUrl,
|
|
1469
|
+
customPartnerApiBaseUrl: !!config.partnerApiBaseUrl
|
|
1470
|
+
},
|
|
1471
|
+
warnings
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// src/error-adapter.ts
|
|
605
1476
|
import {
|
|
606
|
-
EdgeApiError as EdgeApiError2,
|
|
607
|
-
EdgeAuthenticationError as EdgeAuthenticationError2,
|
|
608
1477
|
EdgeConsentRequiredError as EdgeConsentRequiredError2,
|
|
609
1478
|
EdgeError as EdgeError2,
|
|
610
1479
|
EdgeIdentityVerificationError as EdgeIdentityVerificationError2,
|
|
611
|
-
EdgeInsufficientScopeError as EdgeInsufficientScopeError2,
|
|
612
1480
|
EdgeNetworkError as EdgeNetworkError2,
|
|
1481
|
+
EdgeValidationError as EdgeValidationError5
|
|
1482
|
+
} from "@edge-markets/connect";
|
|
1483
|
+
function toEdgeHttpError(error) {
|
|
1484
|
+
const body = toEdgeProblemJson(error);
|
|
1485
|
+
return { status: body.status, body };
|
|
1486
|
+
}
|
|
1487
|
+
function toEdgeProblemJson(error) {
|
|
1488
|
+
if (error instanceof EdgeConsentRequiredError2) {
|
|
1489
|
+
return problem(error, "Consent required", error.statusCode ?? 403, {
|
|
1490
|
+
clientId: error.clientId,
|
|
1491
|
+
consentUrl: error.consentUrl
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if (error instanceof EdgeValidationError5) {
|
|
1495
|
+
return problem(error, "Validation failed", error.statusCode ?? 400, {
|
|
1496
|
+
validationErrors: error.validationErrors
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
if (error instanceof EdgeIdentityVerificationError2) {
|
|
1500
|
+
return problem(error, "Identity verification failed", error.statusCode ?? 422, {
|
|
1501
|
+
fieldErrors: error.fieldErrors
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
if (error instanceof EdgeNetworkError2) {
|
|
1505
|
+
return problem(error, "EDGE network error", error.statusCode ?? 502);
|
|
1506
|
+
}
|
|
1507
|
+
if (error instanceof EdgeError2) {
|
|
1508
|
+
return problem(error, edgeTitle(error), error.statusCode ?? 500, error.details);
|
|
1509
|
+
}
|
|
1510
|
+
const message = error instanceof Error ? error.message : "Unexpected error";
|
|
1511
|
+
return {
|
|
1512
|
+
type: "https://developer.edgemarkets.io/errors/internal_error",
|
|
1513
|
+
title: "Internal error",
|
|
1514
|
+
status: 500,
|
|
1515
|
+
detail: message,
|
|
1516
|
+
code: "internal_error"
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
function problem(error, title, status, details) {
|
|
1520
|
+
return {
|
|
1521
|
+
type: `https://developer.edgemarkets.io/errors/${error.code}`,
|
|
1522
|
+
title,
|
|
1523
|
+
status,
|
|
1524
|
+
detail: error.message,
|
|
1525
|
+
code: error.code,
|
|
1526
|
+
...details && Object.keys(details).length > 0 ? { details } : {}
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function edgeTitle(error) {
|
|
1530
|
+
return error.code.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/identity.ts
|
|
1534
|
+
import { EdgeValidationError as EdgeValidationError6 } from "@edge-markets/connect";
|
|
1535
|
+
var DEFAULT_IDENTITY_POLICY = {
|
|
1536
|
+
minNameScore: 70,
|
|
1537
|
+
minAddressScore: 65,
|
|
1538
|
+
requireZipMatch: true,
|
|
1539
|
+
requireApiVerified: true
|
|
1540
|
+
};
|
|
1541
|
+
function buildVerifyIdentityPayload(profile) {
|
|
1542
|
+
const firstName = firstNonEmpty(profile.firstName, profile.givenName);
|
|
1543
|
+
const lastName = firstNonEmpty(profile.lastName, profile.familyName);
|
|
1544
|
+
const address = profile.address ?? {};
|
|
1545
|
+
const payload = {
|
|
1546
|
+
firstName,
|
|
1547
|
+
lastName,
|
|
1548
|
+
address: {
|
|
1549
|
+
street: firstNonEmpty(address.street, address.line1, address.address1),
|
|
1550
|
+
city: firstNonEmpty(address.city),
|
|
1551
|
+
state: firstNonEmpty(address.state),
|
|
1552
|
+
zip: firstNonEmpty(address.zip, address.postalCode)
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
const errors = {};
|
|
1556
|
+
if (!payload.firstName) errors.firstName = ["Required"];
|
|
1557
|
+
if (!payload.lastName) errors.lastName = ["Required"];
|
|
1558
|
+
if (!Object.values(payload.address).some((value) => !!value)) {
|
|
1559
|
+
errors.address = ["At least one address field is required"];
|
|
1560
|
+
}
|
|
1561
|
+
if (Object.keys(errors).length > 0) {
|
|
1562
|
+
throw new EdgeValidationError6("Partner identity profile is incomplete", errors);
|
|
1563
|
+
}
|
|
1564
|
+
return payload;
|
|
1565
|
+
}
|
|
1566
|
+
function evaluateIdentityResult(result, policy = {}) {
|
|
1567
|
+
const resolved = { ...DEFAULT_IDENTITY_POLICY, ...policy };
|
|
1568
|
+
validatePolicy(resolved);
|
|
1569
|
+
const reasons = [];
|
|
1570
|
+
const verifiedFlag = result.verified;
|
|
1571
|
+
if (resolved.requireApiVerified && verifiedFlag !== true) {
|
|
1572
|
+
reasons.push("api_not_verified");
|
|
1573
|
+
}
|
|
1574
|
+
const scores = result.scores;
|
|
1575
|
+
if (!scores) {
|
|
1576
|
+
reasons.push("scores_missing");
|
|
1577
|
+
} else {
|
|
1578
|
+
if (typeof scores.name === "number" && scores.name < resolved.minNameScore) {
|
|
1579
|
+
reasons.push("name_score_below_threshold");
|
|
1580
|
+
}
|
|
1581
|
+
if (typeof scores.address === "number" && scores.address < resolved.minAddressScore) {
|
|
1582
|
+
reasons.push("address_score_below_threshold");
|
|
1583
|
+
}
|
|
1584
|
+
if (resolved.requireZipMatch && scores.zipMatch !== true) {
|
|
1585
|
+
reasons.push("zip_mismatch");
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return {
|
|
1589
|
+
verified: reasons.length === 0,
|
|
1590
|
+
reasons,
|
|
1591
|
+
...scores ? { scores } : {}
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
function validatePolicy(policy) {
|
|
1595
|
+
const errors = {};
|
|
1596
|
+
if (!isScore(policy.minNameScore)) errors.minNameScore = ["Must be between 0 and 100"];
|
|
1597
|
+
if (!isScore(policy.minAddressScore)) errors.minAddressScore = ["Must be between 0 and 100"];
|
|
1598
|
+
if (Object.keys(errors).length > 0) {
|
|
1599
|
+
throw new EdgeValidationError6("Invalid identity policy", errors);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
function isScore(value) {
|
|
1603
|
+
return Number.isFinite(value) && value >= 0 && value <= 100;
|
|
1604
|
+
}
|
|
1605
|
+
function firstNonEmpty(...values) {
|
|
1606
|
+
for (const value of values) {
|
|
1607
|
+
if (typeof value === "string") {
|
|
1608
|
+
const trimmed = value.trim();
|
|
1609
|
+
if (trimmed) return trimmed;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return "";
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// src/token-vault.ts
|
|
1616
|
+
import { EdgeValidationError as EdgeValidationError7 } from "@edge-markets/connect";
|
|
1617
|
+
import crypto2 from "crypto";
|
|
1618
|
+
var TOKEN_VAULT_VERSION = "edge_vault_v1";
|
|
1619
|
+
var IV_BYTES = 12;
|
|
1620
|
+
var TAG_BYTES = 16;
|
|
1621
|
+
var KEY_BYTES = 32;
|
|
1622
|
+
var EdgeTokenVault = class {
|
|
1623
|
+
constructor(options) {
|
|
1624
|
+
this.currentKey = normalizeVaultKey(options.currentKey, "currentKey");
|
|
1625
|
+
this.keysById = /* @__PURE__ */ new Map([[this.currentKey.id, this.currentKey.key]]);
|
|
1626
|
+
for (const [index, key] of (options.previousKeys ?? []).entries()) {
|
|
1627
|
+
const normalized = normalizeVaultKey(key, `previousKeys[${index}]`);
|
|
1628
|
+
if (this.keysById.has(normalized.id)) {
|
|
1629
|
+
throw new EdgeValidationError7("Duplicate token vault key ID", {
|
|
1630
|
+
keyId: [`Duplicate key ID "${normalized.id}"`]
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
this.keysById.set(normalized.id, normalized.key);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
encryptTokens(tokens) {
|
|
1637
|
+
validateEdgeTokens(tokens);
|
|
1638
|
+
const iv = crypto2.randomBytes(IV_BYTES);
|
|
1639
|
+
const aad = Buffer.from(`${TOKEN_VAULT_VERSION}.${this.currentKey.id}`, "utf8");
|
|
1640
|
+
const cipher = crypto2.createCipheriv("aes-256-gcm", this.currentKey.key, iv);
|
|
1641
|
+
cipher.setAAD(aad);
|
|
1642
|
+
const plaintext = Buffer.from(JSON.stringify(tokens), "utf8");
|
|
1643
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1644
|
+
const tag = cipher.getAuthTag();
|
|
1645
|
+
return [
|
|
1646
|
+
TOKEN_VAULT_VERSION,
|
|
1647
|
+
toBase64Url2(Buffer.from(this.currentKey.id, "utf8")),
|
|
1648
|
+
toBase64Url2(iv),
|
|
1649
|
+
toBase64Url2(tag),
|
|
1650
|
+
toBase64Url2(ciphertext)
|
|
1651
|
+
].join(".");
|
|
1652
|
+
}
|
|
1653
|
+
decryptTokens(envelope) {
|
|
1654
|
+
const parsed = parseEnvelope(envelope);
|
|
1655
|
+
const key = this.keysById.get(parsed.keyId);
|
|
1656
|
+
if (!key) {
|
|
1657
|
+
throw new EdgeValidationError7("Unknown token vault key ID", {
|
|
1658
|
+
keyId: [`No key configured for "${parsed.keyId}"`]
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
try {
|
|
1662
|
+
const decipher = crypto2.createDecipheriv("aes-256-gcm", key, parsed.iv);
|
|
1663
|
+
decipher.setAAD(Buffer.from(`${TOKEN_VAULT_VERSION}.${parsed.keyId}`, "utf8"));
|
|
1664
|
+
decipher.setAuthTag(parsed.tag);
|
|
1665
|
+
const plaintext = Buffer.concat([decipher.update(parsed.ciphertext), decipher.final()]).toString("utf8");
|
|
1666
|
+
const tokens = JSON.parse(plaintext);
|
|
1667
|
+
validateEdgeTokens(tokens);
|
|
1668
|
+
return tokens;
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
if (error instanceof EdgeValidationError7) throw error;
|
|
1671
|
+
throw new EdgeValidationError7("Failed to decrypt EDGE tokens", {
|
|
1672
|
+
tokenEnvelope: ["Envelope is malformed, corrupted, or encrypted with a different key"]
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
isEncrypted(value) {
|
|
1677
|
+
return typeof value === "string" && value.startsWith(`${TOKEN_VAULT_VERSION}.`);
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
function createEdgeTokenVault(options) {
|
|
1681
|
+
return new EdgeTokenVault(options);
|
|
1682
|
+
}
|
|
1683
|
+
function isEdgeTokenVaultEnvelope(value) {
|
|
1684
|
+
return typeof value === "string" && value.startsWith(`${TOKEN_VAULT_VERSION}.`);
|
|
1685
|
+
}
|
|
1686
|
+
function normalizeVaultKey(input, fieldName) {
|
|
1687
|
+
const id = typeof input.id === "string" ? input.id.trim() : "";
|
|
1688
|
+
if (!id) {
|
|
1689
|
+
throw new EdgeValidationError7("Token vault key ID is required", {
|
|
1690
|
+
[`${fieldName}.id`]: ["Required"]
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
return {
|
|
1694
|
+
id,
|
|
1695
|
+
key: normalizeKeyMaterial(input.key, `${fieldName}.key`)
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
function normalizeKeyMaterial(input, fieldName) {
|
|
1699
|
+
const candidates = [];
|
|
1700
|
+
if (Buffer.isBuffer(input)) {
|
|
1701
|
+
candidates.push(input);
|
|
1702
|
+
} else if (input instanceof Uint8Array) {
|
|
1703
|
+
candidates.push(Buffer.from(input));
|
|
1704
|
+
} else if (typeof input === "string") {
|
|
1705
|
+
const value = input.trim();
|
|
1706
|
+
if (value.startsWith("base64:")) {
|
|
1707
|
+
candidates.push(Buffer.from(value.slice("base64:".length), "base64"));
|
|
1708
|
+
} else if (value.startsWith("base64url:")) {
|
|
1709
|
+
candidates.push(fromBase64Url2(value.slice("base64url:".length)));
|
|
1710
|
+
} else if (/^[0-9a-f]{64}$/i.test(value)) {
|
|
1711
|
+
candidates.push(Buffer.from(value, "hex"));
|
|
1712
|
+
} else {
|
|
1713
|
+
candidates.push(Buffer.from(value, "base64"));
|
|
1714
|
+
candidates.push(fromBase64Url2(value));
|
|
1715
|
+
candidates.push(Buffer.from(value, "utf8"));
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
const key = candidates.find((candidate) => candidate.length === KEY_BYTES);
|
|
1719
|
+
if (!key) {
|
|
1720
|
+
throw new EdgeValidationError7("Token vault key must decode to 32 bytes", {
|
|
1721
|
+
[fieldName]: ["Provide a 32-byte key as base64, base64url, hex, Buffer, or Uint8Array"]
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
return key;
|
|
1725
|
+
}
|
|
1726
|
+
function validateEdgeTokens(tokens) {
|
|
1727
|
+
const errors = {};
|
|
1728
|
+
if (!tokens || typeof tokens !== "object") {
|
|
1729
|
+
errors.tokens = ["Expected token object"];
|
|
1730
|
+
} else {
|
|
1731
|
+
if (typeof tokens.accessToken !== "string" || !tokens.accessToken) {
|
|
1732
|
+
errors.accessToken = ["Required"];
|
|
1733
|
+
}
|
|
1734
|
+
if (typeof tokens.refreshToken !== "string" || !tokens.refreshToken) {
|
|
1735
|
+
errors.refreshToken = ["Required"];
|
|
1736
|
+
}
|
|
1737
|
+
if (typeof tokens.expiresIn !== "number" || !Number.isFinite(tokens.expiresIn) || tokens.expiresIn <= 0) {
|
|
1738
|
+
errors.expiresIn = ["Must be a positive number"];
|
|
1739
|
+
}
|
|
1740
|
+
if (typeof tokens.expiresAt !== "number" || !Number.isFinite(tokens.expiresAt) || tokens.expiresAt <= 0) {
|
|
1741
|
+
errors.expiresAt = ["Must be a positive Unix timestamp in milliseconds"];
|
|
1742
|
+
}
|
|
1743
|
+
if (typeof tokens.scope !== "string") {
|
|
1744
|
+
errors.scope = ["Required"];
|
|
1745
|
+
}
|
|
1746
|
+
if (tokens.idToken !== void 0 && typeof tokens.idToken !== "string") {
|
|
1747
|
+
errors.idToken = ["Must be a string when provided"];
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (Object.keys(errors).length > 0) {
|
|
1751
|
+
throw new EdgeValidationError7("Invalid EDGE token payload", errors);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
function parseEnvelope(envelope) {
|
|
1755
|
+
if (typeof envelope !== "string") {
|
|
1756
|
+
throw new EdgeValidationError7("Token envelope must be a string", {
|
|
1757
|
+
tokenEnvelope: ["Expected string"]
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
const parts = envelope.split(".");
|
|
1761
|
+
if (parts.length !== 5 || parts[0] !== TOKEN_VAULT_VERSION) {
|
|
1762
|
+
throw new EdgeValidationError7("Invalid EDGE token vault envelope", {
|
|
1763
|
+
tokenEnvelope: [`Expected ${TOKEN_VAULT_VERSION}.<keyId>.<iv>.<tag>.<ciphertext>`]
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
const [, encodedKeyId, encodedIv, encodedTag, encodedCiphertext] = parts;
|
|
1767
|
+
const keyId = fromBase64Url2(encodedKeyId).toString("utf8");
|
|
1768
|
+
const iv = fromBase64Url2(encodedIv);
|
|
1769
|
+
const tag = fromBase64Url2(encodedTag);
|
|
1770
|
+
const ciphertext = fromBase64Url2(encodedCiphertext);
|
|
1771
|
+
const errors = {};
|
|
1772
|
+
if (!keyId) errors.keyId = ["Required"];
|
|
1773
|
+
if (iv.length !== IV_BYTES) errors.iv = [`Must be ${IV_BYTES} bytes`];
|
|
1774
|
+
if (tag.length !== TAG_BYTES) errors.tag = [`Must be ${TAG_BYTES} bytes`];
|
|
1775
|
+
if (ciphertext.length === 0) errors.ciphertext = ["Required"];
|
|
1776
|
+
if (Object.keys(errors).length > 0) {
|
|
1777
|
+
throw new EdgeValidationError7("Invalid EDGE token vault envelope", errors);
|
|
1778
|
+
}
|
|
1779
|
+
return { keyId, iv, tag, ciphertext };
|
|
1780
|
+
}
|
|
1781
|
+
function toBase64Url2(value) {
|
|
1782
|
+
return value.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
1783
|
+
}
|
|
1784
|
+
function fromBase64Url2(value) {
|
|
1785
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
1786
|
+
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
1787
|
+
return Buffer.from(normalized + padding, "base64");
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// src/webhook-guards.ts
|
|
1791
|
+
import { EdgeValidationError as EdgeValidationError8 } from "@edge-markets/connect";
|
|
1792
|
+
function isTransferWebhookEvent(event) {
|
|
1793
|
+
return event.type.startsWith("transfer.");
|
|
1794
|
+
}
|
|
1795
|
+
function assertTransferEventMatchesIntent(event, intent) {
|
|
1796
|
+
if (!isTransferWebhookEvent(event)) {
|
|
1797
|
+
throw new EdgeValidationError8("Webhook event is not a transfer event", {
|
|
1798
|
+
type: [`Received ${event.type}`]
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
const data = event.data;
|
|
1802
|
+
const errors = {};
|
|
1803
|
+
if (data.transferId !== intent.transferId) {
|
|
1804
|
+
errors.transferId = [`Expected ${intent.transferId}, received ${data.transferId}`];
|
|
1805
|
+
}
|
|
1806
|
+
if (intent.type && data.type !== intent.type) {
|
|
1807
|
+
errors.type = [`Expected ${intent.type}, received ${data.type}`];
|
|
1808
|
+
}
|
|
1809
|
+
if (intent.status && data.status !== intent.status) {
|
|
1810
|
+
errors.status = [`Expected ${intent.status}, received ${data.status}`];
|
|
1811
|
+
}
|
|
1812
|
+
if (intent.amount !== void 0) {
|
|
1813
|
+
const expectedAmount = normalizeMoneyAmount(intent.amount);
|
|
1814
|
+
const receivedAmount = normalizeMoneyAmount(data.amount);
|
|
1815
|
+
if (receivedAmount !== expectedAmount) {
|
|
1816
|
+
errors.amount = [`Expected ${expectedAmount}, received ${receivedAmount}`];
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
if (Object.keys(errors).length > 0) {
|
|
1820
|
+
throw new EdgeValidationError8("Webhook transfer event does not match expected transfer intent", errors);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// src/webhook-parser.ts
|
|
1825
|
+
import {
|
|
1826
|
+
EDGE_WEBHOOK_EVENT_TYPES,
|
|
1827
|
+
EdgeAuthenticationError as EdgeAuthenticationError3,
|
|
1828
|
+
EdgeValidationError as EdgeValidationError9
|
|
1829
|
+
} from "@edge-markets/connect";
|
|
1830
|
+
|
|
1831
|
+
// src/webhook-signing.ts
|
|
1832
|
+
import crypto3 from "crypto";
|
|
1833
|
+
function verifyWebhookSignature(header, body, secret, options = {}) {
|
|
1834
|
+
if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
|
|
1835
|
+
return false;
|
|
1836
|
+
}
|
|
1837
|
+
const requestedTolerance = options.toleranceSeconds;
|
|
1838
|
+
const toleranceSeconds = typeof requestedTolerance === "number" && Number.isFinite(requestedTolerance) && requestedTolerance >= 0 ? requestedTolerance : 300;
|
|
1839
|
+
const parts = header.split(",").map((p) => p.trim());
|
|
1840
|
+
const tPart = parts.find((p) => p.startsWith("t="));
|
|
1841
|
+
const v1Part = parts.find((p) => p.startsWith("v1="));
|
|
1842
|
+
if (!tPart || !v1Part) return false;
|
|
1843
|
+
const timestamp = parseInt(tPart.slice(2), 10);
|
|
1844
|
+
const signature = v1Part.slice(3);
|
|
1845
|
+
if (isNaN(timestamp) || !signature) return false;
|
|
1846
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1847
|
+
if (Math.abs(now - timestamp) > toleranceSeconds) return false;
|
|
1848
|
+
const signedContent = `${timestamp}.${body}`;
|
|
1849
|
+
const expectedHmac = crypto3.createHmac("sha256", secret).update(signedContent).digest("hex");
|
|
1850
|
+
try {
|
|
1851
|
+
const sigBuf = Buffer.from(signature, "hex");
|
|
1852
|
+
const expBuf = Buffer.from(expectedHmac, "hex");
|
|
1853
|
+
if (sigBuf.length !== expBuf.length) return false;
|
|
1854
|
+
return crypto3.timingSafeEqual(sigBuf, expBuf);
|
|
1855
|
+
} catch {
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
// src/webhook-parser.ts
|
|
1861
|
+
var EVENT_TYPES = new Set(EDGE_WEBHOOK_EVENT_TYPES);
|
|
1862
|
+
var TRANSFER_TYPES = /* @__PURE__ */ new Set(["debit", "credit"]);
|
|
1863
|
+
function parseAndVerifyWebhook(options) {
|
|
1864
|
+
const body = normalizeRawBody(options.rawBody);
|
|
1865
|
+
const header = normalizeSignatureHeader(options.signatureHeader);
|
|
1866
|
+
const signatureTimestamp = extractWebhookSignatureTimestamp(header);
|
|
1867
|
+
if (!header || !signatureTimestamp) {
|
|
1868
|
+
throw new EdgeAuthenticationError3("Missing or malformed EDGE webhook signature");
|
|
1869
|
+
}
|
|
1870
|
+
if (!verifyWebhookSignature(header, body, options.secret, { toleranceSeconds: options.toleranceSeconds })) {
|
|
1871
|
+
throw new EdgeAuthenticationError3("Invalid EDGE webhook signature");
|
|
1872
|
+
}
|
|
1873
|
+
let parsed;
|
|
1874
|
+
try {
|
|
1875
|
+
parsed = JSON.parse(body);
|
|
1876
|
+
} catch {
|
|
1877
|
+
throw new EdgeValidationError9("EDGE webhook body must be valid JSON", {
|
|
1878
|
+
rawBody: ["Unable to parse JSON after signature verification"]
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
const event = validateEdgeWebhookEvent(parsed);
|
|
1882
|
+
return {
|
|
1883
|
+
event,
|
|
1884
|
+
eventId: event.id,
|
|
1885
|
+
eventType: event.type,
|
|
1886
|
+
signatureTimestamp,
|
|
1887
|
+
receivedAt: options.receivedAt ?? /* @__PURE__ */ new Date()
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
function extractWebhookSignatureTimestamp(header) {
|
|
1891
|
+
const normalizedHeader = normalizeSignatureHeader(header);
|
|
1892
|
+
if (!normalizedHeader) return void 0;
|
|
1893
|
+
const part = normalizedHeader.split(",").map((value) => value.trim()).find((value) => value.startsWith("t="));
|
|
1894
|
+
if (!part) return void 0;
|
|
1895
|
+
const timestamp = Number(part.slice(2));
|
|
1896
|
+
if (!Number.isInteger(timestamp) || timestamp <= 0) return void 0;
|
|
1897
|
+
return timestamp;
|
|
1898
|
+
}
|
|
1899
|
+
function isEdgeWebhookEvent(value) {
|
|
1900
|
+
try {
|
|
1901
|
+
validateEdgeWebhookEvent(value);
|
|
1902
|
+
return true;
|
|
1903
|
+
} catch {
|
|
1904
|
+
return false;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function validateEdgeWebhookEvent(value) {
|
|
1908
|
+
const errors = {};
|
|
1909
|
+
if (!isRecord(value)) {
|
|
1910
|
+
throw new EdgeValidationError9("EDGE webhook event must be an object", {
|
|
1911
|
+
event: ["Expected JSON object"]
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
const id = value.id;
|
|
1915
|
+
const type = value.type;
|
|
1916
|
+
const createdAt = value.created_at;
|
|
1917
|
+
const data = value.data;
|
|
1918
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
1919
|
+
errors.id = ["Required string"];
|
|
1920
|
+
}
|
|
1921
|
+
if (typeof type !== "string" || !EVENT_TYPES.has(type)) {
|
|
1922
|
+
errors.type = [`Must be one of: ${EDGE_WEBHOOK_EVENT_TYPES.join(", ")}`];
|
|
1923
|
+
}
|
|
1924
|
+
if (typeof createdAt !== "string" || !createdAt.trim() || Number.isNaN(Date.parse(createdAt))) {
|
|
1925
|
+
errors.created_at = ["Required ISO 8601 timestamp string"];
|
|
1926
|
+
}
|
|
1927
|
+
if (!isRecord(data)) {
|
|
1928
|
+
errors.data = ["Required object"];
|
|
1929
|
+
}
|
|
1930
|
+
if (Object.keys(errors).length === 0) {
|
|
1931
|
+
validateKnownEventData(type, data, errors);
|
|
1932
|
+
}
|
|
1933
|
+
if (Object.keys(errors).length > 0) {
|
|
1934
|
+
throw new EdgeValidationError9("Invalid EDGE webhook event", errors);
|
|
1935
|
+
}
|
|
1936
|
+
return value;
|
|
1937
|
+
}
|
|
1938
|
+
function validateKnownEventData(type, data, errors) {
|
|
1939
|
+
switch (type) {
|
|
1940
|
+
case "transfer.completed":
|
|
1941
|
+
validateTransferEventData(data, "completed", errors);
|
|
1942
|
+
return;
|
|
1943
|
+
case "transfer.failed":
|
|
1944
|
+
validateTransferEventData(data, "failed", errors);
|
|
1945
|
+
if (data.reason !== void 0 && typeof data.reason !== "string") {
|
|
1946
|
+
errors["data.reason"] = ["Must be a string when provided"];
|
|
1947
|
+
}
|
|
1948
|
+
return;
|
|
1949
|
+
case "transfer.expired":
|
|
1950
|
+
validateTransferEventData(data, "expired", errors);
|
|
1951
|
+
return;
|
|
1952
|
+
case "transfer.processing":
|
|
1953
|
+
validateTransferEventData(data, "processing", errors);
|
|
1954
|
+
return;
|
|
1955
|
+
case "consent.revoked":
|
|
1956
|
+
validateRequiredString(data, "userId", errors);
|
|
1957
|
+
validateRequiredString(data, "clientId", errors);
|
|
1958
|
+
validateRequiredIsoTimestamp(data, "revokedAt", errors);
|
|
1959
|
+
return;
|
|
1960
|
+
default: {
|
|
1961
|
+
const exhaustive = type;
|
|
1962
|
+
throw new EdgeValidationError9("Unsupported EDGE webhook event type", {
|
|
1963
|
+
type: [`Unsupported event type: ${exhaustive}`]
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
function validateTransferEventData(data, status, errors) {
|
|
1969
|
+
validateRequiredString(data, "transferId", errors);
|
|
1970
|
+
if (data.status !== status) {
|
|
1971
|
+
errors["data.status"] = [`Must be "${status}" for this event type`];
|
|
1972
|
+
}
|
|
1973
|
+
if (typeof data.type !== "string" || !TRANSFER_TYPES.has(data.type)) {
|
|
1974
|
+
errors["data.type"] = ['Must be "debit" or "credit"'];
|
|
1975
|
+
}
|
|
1976
|
+
validateRequiredString(data, "amount", errors);
|
|
1977
|
+
}
|
|
1978
|
+
function validateRequiredString(data, field, errors) {
|
|
1979
|
+
if (typeof data[field] !== "string" || !data[field].trim()) {
|
|
1980
|
+
errors[`data.${field}`] = ["Required string"];
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
function validateRequiredIsoTimestamp(data, field, errors) {
|
|
1984
|
+
if (typeof data[field] !== "string" || !data[field].trim() || Number.isNaN(Date.parse(data[field]))) {
|
|
1985
|
+
errors[`data.${field}`] = ["Required ISO 8601 timestamp string"];
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
function normalizeRawBody(rawBody) {
|
|
1989
|
+
if (typeof rawBody === "string") {
|
|
1990
|
+
return rawBody;
|
|
1991
|
+
}
|
|
1992
|
+
if (Buffer.isBuffer(rawBody) || rawBody instanceof Uint8Array) {
|
|
1993
|
+
return Buffer.from(rawBody).toString("utf8");
|
|
1994
|
+
}
|
|
1995
|
+
throw new EdgeValidationError9("EDGE webhook raw body is required", {
|
|
1996
|
+
rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
function normalizeSignatureHeader(header) {
|
|
2000
|
+
if (Array.isArray(header)) {
|
|
2001
|
+
return header.map((value) => value.trim()).filter(Boolean).join(",");
|
|
2002
|
+
}
|
|
2003
|
+
if (typeof header !== "string") return void 0;
|
|
2004
|
+
const trimmed = header.trim();
|
|
2005
|
+
return trimmed || void 0;
|
|
2006
|
+
}
|
|
2007
|
+
function isRecord(value) {
|
|
2008
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// src/index.ts
|
|
2012
|
+
import {
|
|
2013
|
+
EdgeApiError as EdgeApiError2,
|
|
2014
|
+
EdgeAuthenticationError as EdgeAuthenticationError4,
|
|
2015
|
+
EdgeConsentRequiredError as EdgeConsentRequiredError3,
|
|
2016
|
+
EdgeError as EdgeError3,
|
|
2017
|
+
EdgeIdentityVerificationError as EdgeIdentityVerificationError3,
|
|
2018
|
+
EdgeInsufficientScopeError as EdgeInsufficientScopeError2,
|
|
2019
|
+
EdgeNetworkError as EdgeNetworkError3,
|
|
613
2020
|
EdgeNotFoundError as EdgeNotFoundError2,
|
|
614
2021
|
EdgeTokenExchangeError as EdgeTokenExchangeError2,
|
|
2022
|
+
EdgeValidationError as EdgeValidationError10,
|
|
615
2023
|
isApiError,
|
|
616
2024
|
isAuthenticationError,
|
|
617
2025
|
isConsentRequiredError,
|
|
@@ -619,26 +2027,57 @@ import {
|
|
|
619
2027
|
isIdentityVerificationError,
|
|
620
2028
|
isNetworkError
|
|
621
2029
|
} from "@edge-markets/connect";
|
|
622
|
-
import {
|
|
2030
|
+
import {
|
|
2031
|
+
EDGE_WEBHOOK_EVENT_TYPES as EDGE_WEBHOOK_EVENT_TYPES2,
|
|
2032
|
+
TRANSFER_CATEGORIES,
|
|
2033
|
+
getEnvironmentConfig as getEnvironmentConfig3,
|
|
2034
|
+
isProductionEnvironment
|
|
2035
|
+
} from "@edge-markets/connect";
|
|
623
2036
|
export {
|
|
2037
|
+
EDGE_WEBHOOK_EVENT_TYPES2 as EDGE_WEBHOOK_EVENT_TYPES,
|
|
624
2038
|
EdgeApiError2 as EdgeApiError,
|
|
625
|
-
|
|
2039
|
+
EdgeAuthenticationError4 as EdgeAuthenticationError,
|
|
626
2040
|
EdgeConnectServer,
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
2041
|
+
EdgeConsentRequiredError3 as EdgeConsentRequiredError,
|
|
2042
|
+
EdgeError3 as EdgeError,
|
|
2043
|
+
EdgeIdentityVerificationError3 as EdgeIdentityVerificationError,
|
|
630
2044
|
EdgeInsufficientScopeError2 as EdgeInsufficientScopeError,
|
|
631
|
-
|
|
2045
|
+
EdgeNetworkError3 as EdgeNetworkError,
|
|
632
2046
|
EdgeNotFoundError2 as EdgeNotFoundError,
|
|
633
2047
|
EdgeTokenExchangeError2 as EdgeTokenExchangeError,
|
|
2048
|
+
EdgeTokenVault,
|
|
634
2049
|
EdgeUserClient,
|
|
2050
|
+
EdgeUserSession,
|
|
2051
|
+
EdgeValidationError10 as EdgeValidationError,
|
|
2052
|
+
EdgeWebhookReconciler,
|
|
635
2053
|
TRANSFER_CATEGORIES,
|
|
636
|
-
|
|
2054
|
+
assertSameTransferIntent,
|
|
2055
|
+
assertTransferEventMatchesIntent,
|
|
2056
|
+
buildVerifyIdentityPayload,
|
|
2057
|
+
createEdgeConnectServerFromEnv,
|
|
2058
|
+
createEdgeTokenVault,
|
|
2059
|
+
createEdgeUserSession,
|
|
2060
|
+
createTransferIdempotencyKey,
|
|
2061
|
+
createWebhookReconciler,
|
|
2062
|
+
evaluateIdentityResult,
|
|
2063
|
+
extractWebhookSignatureTimestamp,
|
|
2064
|
+
getEnvironmentConfig3 as getEnvironmentConfig,
|
|
637
2065
|
isApiError,
|
|
638
2066
|
isAuthenticationError,
|
|
639
2067
|
isConsentRequiredError,
|
|
640
2068
|
isEdgeError,
|
|
2069
|
+
isEdgeTokenVaultEnvelope,
|
|
2070
|
+
isEdgeWebhookEvent,
|
|
641
2071
|
isIdentityVerificationError,
|
|
642
2072
|
isNetworkError,
|
|
643
|
-
isProductionEnvironment
|
|
2073
|
+
isProductionEnvironment,
|
|
2074
|
+
isTransferWebhookEvent,
|
|
2075
|
+
normalizeMoneyAmount,
|
|
2076
|
+
parseAndVerifyWebhook,
|
|
2077
|
+
startTransferVerification,
|
|
2078
|
+
toEdgeHttpError,
|
|
2079
|
+
toEdgeProblemJson,
|
|
2080
|
+
validateEdgeWebhookEvent,
|
|
2081
|
+
validateTransferAmount,
|
|
2082
|
+
verifyWebhookSignature
|
|
644
2083
|
};
|