@edge-markets/connect-node 1.7.0 → 1.9.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 +323 -71
- package/dist/index.d.mts +539 -66
- package/dist/index.d.ts +539 -66
- package/dist/index.js +1881 -85
- package/dist/index.mjs +1826 -57
- 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,12 +19,175 @@ 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
|
+
var CONNECT_TRANSFER_DIRECTIONS = {
|
|
30
|
+
debit: "user_to_partner",
|
|
31
|
+
credit: "partner_to_user"
|
|
32
|
+
};
|
|
33
|
+
function getConnectTransferDirection(type) {
|
|
34
|
+
if (type !== "debit" && type !== "credit") {
|
|
35
|
+
throw new EdgeValidationError("Invalid Connect transfer type", {
|
|
36
|
+
type: ['Must be "debit" or "credit"']
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return CONNECT_TRANSFER_DIRECTIONS[type];
|
|
40
|
+
}
|
|
41
|
+
function normalizeMoneyAmount(value, options = {}) {
|
|
42
|
+
const cents = parseMoneyToCents(value, "amount", options);
|
|
43
|
+
const dollars = cents / 100n;
|
|
44
|
+
const remainder = cents % 100n;
|
|
45
|
+
return `${dollars.toString()}.${remainder.toString().padStart(2, "0")}`;
|
|
46
|
+
}
|
|
47
|
+
function validateTransferAmount(value, limits = {}, options = {}) {
|
|
48
|
+
const amount = parseMoneyToCents(value, "amount", options);
|
|
49
|
+
const min = parseMoneyToCents(limits.min ?? DEFAULT_MIN_AMOUNT, "min", { allowNumber: true });
|
|
50
|
+
const max = parseMoneyToCents(limits.max ?? DEFAULT_MAX_AMOUNT, "max", { allowNumber: true });
|
|
51
|
+
const errors = {};
|
|
52
|
+
if (min <= 0n) errors.min = ["Minimum amount must be greater than 0"];
|
|
53
|
+
if (max < min) errors.max = ["Maximum amount must be greater than or equal to minimum amount"];
|
|
54
|
+
if (amount < min) errors.amount = [`Must be at least ${formatCents(min)}`];
|
|
55
|
+
if (amount > max) errors.amount = [`Must be at most ${formatCents(max)}`];
|
|
56
|
+
if (Object.keys(errors).length > 0) {
|
|
57
|
+
throw new EdgeValidationError("Invalid transfer amount", errors);
|
|
58
|
+
}
|
|
59
|
+
return formatCents(amount);
|
|
60
|
+
}
|
|
61
|
+
function createTransferIdempotencyKey(options) {
|
|
62
|
+
const namespace = sanitizeKeyPart(options.namespace || "edge");
|
|
63
|
+
const partnerUserId = requireKeyPart(options.partnerUserId, "partnerUserId");
|
|
64
|
+
const externalId = options.externalId ? sanitizeKeyPart(options.externalId) : void 0;
|
|
65
|
+
const nonce = options.nonce ? sanitizeKeyPart(options.nonce) : void 0;
|
|
66
|
+
const amount = normalizeMoneyAmount(options.amount);
|
|
67
|
+
const digest = crypto.createHash("sha256").update(
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
partnerUserId,
|
|
70
|
+
type: options.type,
|
|
71
|
+
amount,
|
|
72
|
+
category: options.category ?? null,
|
|
73
|
+
externalId: externalId ?? null,
|
|
74
|
+
nonce: nonce ?? null
|
|
75
|
+
})
|
|
76
|
+
).digest("hex").slice(0, 32);
|
|
77
|
+
return [namespace, options.type, digest].join("_");
|
|
78
|
+
}
|
|
79
|
+
function assertSameTransferIntent(existing, requested) {
|
|
80
|
+
const errors = {};
|
|
81
|
+
if (existing.type !== requested.type) {
|
|
82
|
+
errors.type = [`Expected ${existing.type}, received ${requested.type}`];
|
|
83
|
+
}
|
|
84
|
+
const existingAmount = normalizeMoneyAmount(existing.amount);
|
|
85
|
+
const requestedAmount = normalizeMoneyAmount(requested.amount);
|
|
86
|
+
if (existingAmount !== requestedAmount) {
|
|
87
|
+
errors.amount = [`Expected ${existingAmount}, received ${requestedAmount}`];
|
|
88
|
+
}
|
|
89
|
+
const existingCategory = existing.category ?? void 0;
|
|
90
|
+
const requestedCategory = requested.category ?? void 0;
|
|
91
|
+
if (existingCategory !== requestedCategory) {
|
|
92
|
+
errors.category = [`Expected ${existingCategory || "unset"}, received ${requestedCategory || "unset"}`];
|
|
93
|
+
}
|
|
94
|
+
if (Object.keys(errors).length > 0) {
|
|
95
|
+
throw new EdgeValidationError("Transfer intent does not match existing idempotent transfer", errors);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function fingerprintTransferIntent(intent) {
|
|
99
|
+
return crypto.createHash("sha256").update(
|
|
100
|
+
JSON.stringify({
|
|
101
|
+
type: intent.type,
|
|
102
|
+
amount: normalizeMoneyAmount(intent.amount),
|
|
103
|
+
category: intent.category ?? null
|
|
104
|
+
})
|
|
105
|
+
).digest("hex");
|
|
106
|
+
}
|
|
107
|
+
function moneyAmountsEqual(left, right, options = {}) {
|
|
108
|
+
return normalizeMoneyAmount(left, options) === normalizeMoneyAmount(right, options);
|
|
109
|
+
}
|
|
110
|
+
function mapTransferStatusToPartnerState(status, mapping) {
|
|
111
|
+
const partnerState = mapping[status];
|
|
112
|
+
if (!partnerState) {
|
|
113
|
+
throw new EdgeValidationError("Unsupported transfer status", {
|
|
114
|
+
status: [`Unsupported status: ${status}`]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return partnerState;
|
|
118
|
+
}
|
|
119
|
+
async function startTransferVerification(client, options) {
|
|
120
|
+
const origin = typeof options.origin === "string" ? options.origin.trim() : "";
|
|
121
|
+
if (!origin) {
|
|
122
|
+
throw new EdgeValidationError("Verification origin is required", {
|
|
123
|
+
origin: ["Required"]
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const transfer = await client.initiateTransfer({
|
|
127
|
+
...options.transfer,
|
|
128
|
+
amount: normalizeMoneyAmount(options.transfer.amount)
|
|
129
|
+
});
|
|
130
|
+
const transferId = transfer.transferId;
|
|
131
|
+
if (!transferId) {
|
|
132
|
+
throw new EdgeValidationError("Transfer response missing transferId", {
|
|
133
|
+
transferId: ["Required to create a verification session"]
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const verificationSession = await client.createVerificationSession(transferId, { origin });
|
|
137
|
+
return { transfer, verificationSession };
|
|
138
|
+
}
|
|
139
|
+
function parseMoneyToCents(value, field, options = {}) {
|
|
140
|
+
let input;
|
|
141
|
+
if (typeof value === "number") {
|
|
142
|
+
if (options.allowNumber === false) {
|
|
143
|
+
throw new EdgeValidationError("Money amount must be a string in strict mode", {
|
|
144
|
+
[field]: ['Pass a decimal string like "25.00"']
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (!Number.isFinite(value)) {
|
|
148
|
+
throw new EdgeValidationError("Money amount must be finite", { [field]: ["Must be a finite number"] });
|
|
149
|
+
}
|
|
150
|
+
input = value.toFixed(2);
|
|
151
|
+
} else if (typeof value === "string") {
|
|
152
|
+
input = value.trim();
|
|
153
|
+
} else {
|
|
154
|
+
throw new EdgeValidationError("Money amount must be a string or number", { [field]: ["Invalid type"] });
|
|
155
|
+
}
|
|
156
|
+
if (!/^\d+(\.\d{0,2})?$/.test(input)) {
|
|
157
|
+
throw new EdgeValidationError("Money amount must be a positive decimal with at most 2 places", {
|
|
158
|
+
[field]: ['Use a decimal string like "25.00"']
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const [dollars, cents = ""] = input.split(".");
|
|
162
|
+
const normalizedDollars = dollars.replace(/^0+(?=\d)/, "") || "0";
|
|
163
|
+
const amount = BigInt(normalizedDollars) * 100n + BigInt(cents.padEnd(2, "0"));
|
|
164
|
+
if (amount <= 0n) {
|
|
165
|
+
throw new EdgeValidationError("Money amount must be greater than 0", { [field]: ["Must be greater than 0"] });
|
|
166
|
+
}
|
|
167
|
+
return amount;
|
|
168
|
+
}
|
|
169
|
+
function formatCents(cents) {
|
|
170
|
+
return `${(cents / 100n).toString()}.${(cents % 100n).toString().padStart(2, "0")}`;
|
|
171
|
+
}
|
|
172
|
+
function requireKeyPart(value, field) {
|
|
173
|
+
const sanitized = sanitizeKeyPart(value);
|
|
174
|
+
if (!sanitized) {
|
|
175
|
+
throw new EdgeValidationError("Idempotency key input is incomplete", {
|
|
176
|
+
[field]: ["Required"]
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return sanitized;
|
|
180
|
+
}
|
|
181
|
+
function sanitizeKeyPart(value) {
|
|
182
|
+
return value.trim().replace(/[^a-zA-Z0-9_.:-]/g, "_").slice(0, 64);
|
|
183
|
+
}
|
|
184
|
+
|
|
22
185
|
// src/validators.ts
|
|
23
|
-
import { EdgeValidationError } from "@edge-markets/connect";
|
|
186
|
+
import { EdgeValidationError as EdgeValidationError2 } from "@edge-markets/connect";
|
|
24
187
|
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;
|
|
25
188
|
function validateTransferId(transferId) {
|
|
26
189
|
if (typeof transferId !== "string" || !UUID_V4_REGEX.test(transferId)) {
|
|
27
|
-
throw new
|
|
190
|
+
throw new EdgeValidationError2("transferId must be a UUID v4", {
|
|
28
191
|
transferId: ["Must be a valid UUID v4"]
|
|
29
192
|
});
|
|
30
193
|
}
|
|
@@ -44,7 +207,7 @@ function validateVerifyIdentityOptions(options) {
|
|
|
44
207
|
}
|
|
45
208
|
if (Object.keys(errors).length > 0) {
|
|
46
209
|
const firstMessage = Object.values(errors)[0][0];
|
|
47
|
-
throw new
|
|
210
|
+
throw new EdgeValidationError2(firstMessage, errors);
|
|
48
211
|
}
|
|
49
212
|
}
|
|
50
213
|
function validateTransferOptions(options) {
|
|
@@ -73,7 +236,7 @@ function validateTransferOptions(options) {
|
|
|
73
236
|
errors.category = ["Must be one of: sportsbook, casino, dfs, sweepstakes"];
|
|
74
237
|
}
|
|
75
238
|
if (Object.keys(errors).length > 0) {
|
|
76
|
-
throw new
|
|
239
|
+
throw new EdgeValidationError2("Invalid transfer options", errors);
|
|
77
240
|
}
|
|
78
241
|
}
|
|
79
242
|
|
|
@@ -105,6 +268,9 @@ var EdgeUserClient = class {
|
|
|
105
268
|
}
|
|
106
269
|
return this.server._apiRequest("POST", "/transfer", this.accessToken, body);
|
|
107
270
|
}
|
|
271
|
+
async startTransferVerification(options) {
|
|
272
|
+
return startTransferVerification(this, options);
|
|
273
|
+
}
|
|
108
274
|
async getTransfer(transferId) {
|
|
109
275
|
validateTransferId(transferId);
|
|
110
276
|
return this.server._apiRequest("GET", `/transfer/${encodeURIComponent(transferId)}`, this.accessToken);
|
|
@@ -225,16 +391,46 @@ function fromBase64Url(value) {
|
|
|
225
391
|
}
|
|
226
392
|
|
|
227
393
|
// src/mtls.ts
|
|
394
|
+
import fs from "fs";
|
|
228
395
|
import https from "https";
|
|
396
|
+
import tls from "tls";
|
|
397
|
+
var CERT_PATTERN = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
|
|
398
|
+
function normalizeCaBundle(ca) {
|
|
399
|
+
if (!ca) return [];
|
|
400
|
+
const values = Array.isArray(ca) ? ca : [ca];
|
|
401
|
+
return values.flatMap((value) => {
|
|
402
|
+
const normalized = value.replace(/\\n/g, "\n").trim();
|
|
403
|
+
if (!normalized) return [];
|
|
404
|
+
const certificates = normalized.match(CERT_PATTERN);
|
|
405
|
+
return certificates ? certificates.map((certificate) => certificate.trim()) : [normalized];
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function getNodeExtraCaCerts() {
|
|
409
|
+
const extraCaPath = process.env.NODE_EXTRA_CA_CERTS?.trim();
|
|
410
|
+
if (!extraCaPath) return [];
|
|
411
|
+
try {
|
|
412
|
+
return normalizeCaBundle(fs.readFileSync(extraCaPath, "utf8"));
|
|
413
|
+
} catch (error) {
|
|
414
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
415
|
+
throw new Error(`EdgeConnectServer: failed to read NODE_EXTRA_CA_CERTS from "${extraCaPath}": ${message}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function buildServerCaBundle(ca) {
|
|
419
|
+
const customCa = normalizeCaBundle(ca);
|
|
420
|
+
if (customCa.length === 0) return void 0;
|
|
421
|
+
return Array.from(/* @__PURE__ */ new Set([...tls.rootCertificates, ...getNodeExtraCaCerts(), ...customCa]));
|
|
422
|
+
}
|
|
229
423
|
function createHttpsAgent(config) {
|
|
424
|
+
const ca = buildServerCaBundle(config.ca);
|
|
230
425
|
return new https.Agent({
|
|
231
426
|
cert: config.cert,
|
|
232
427
|
key: config.key,
|
|
233
428
|
rejectUnauthorized: true,
|
|
234
|
-
...
|
|
429
|
+
...ca ? { ca } : {}
|
|
235
430
|
});
|
|
236
431
|
}
|
|
237
432
|
function createUndiciDispatcher(config) {
|
|
433
|
+
const ca = buildServerCaBundle(config.ca);
|
|
238
434
|
const moduleName = "undici";
|
|
239
435
|
const { Agent } = __require(moduleName);
|
|
240
436
|
return new Agent({
|
|
@@ -242,7 +438,7 @@ function createUndiciDispatcher(config) {
|
|
|
242
438
|
cert: config.cert,
|
|
243
439
|
key: config.key,
|
|
244
440
|
rejectUnauthorized: true,
|
|
245
|
-
...
|
|
441
|
+
...ca ? { ca } : {}
|
|
246
442
|
}
|
|
247
443
|
});
|
|
248
444
|
}
|
|
@@ -255,6 +451,177 @@ function validateMtlsConfig(config) {
|
|
|
255
451
|
}
|
|
256
452
|
}
|
|
257
453
|
|
|
454
|
+
// src/session.ts
|
|
455
|
+
import { EdgeAuthenticationError, EdgeValidationError as EdgeValidationError3 } from "@edge-markets/connect";
|
|
456
|
+
var EdgeUserSession = class {
|
|
457
|
+
constructor(options) {
|
|
458
|
+
this.refreshPromise = null;
|
|
459
|
+
if (!options.edge?.forUser || !options.edge?.refreshTokens) {
|
|
460
|
+
throw new EdgeValidationError3("EdgeUserSession requires an EdgeConnectServer instance", {
|
|
461
|
+
edge: ["Provide edge.forUser and edge.refreshTokens"]
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
const subjectId = typeof options.subjectId === "string" ? options.subjectId.trim() : "";
|
|
465
|
+
if (!subjectId) {
|
|
466
|
+
throw new EdgeValidationError3("EdgeUserSession subjectId is required", {
|
|
467
|
+
subjectId: ["Required"]
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (!options.tokenStore?.load || !options.tokenStore?.save) {
|
|
471
|
+
throw new EdgeValidationError3("EdgeUserSession tokenStore is required", {
|
|
472
|
+
tokenStore: ["Provide load(subjectId) and save(subjectId, value, context)"]
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (options.refreshSkewMs !== void 0 && (!Number.isFinite(options.refreshSkewMs) || options.refreshSkewMs < 0)) {
|
|
476
|
+
throw new EdgeValidationError3("refreshSkewMs must be a non-negative number", {
|
|
477
|
+
refreshSkewMs: ["Must be greater than or equal to 0"]
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
this.edge = options.edge;
|
|
481
|
+
this.subjectId = subjectId;
|
|
482
|
+
this.tokenStore = options.tokenStore;
|
|
483
|
+
this.tokenVault = options.tokenVault;
|
|
484
|
+
this.refreshSkewMs = options.refreshSkewMs ?? 6e4;
|
|
485
|
+
this.now = options.now ?? Date.now;
|
|
486
|
+
this.onRefresh = options.onRefresh;
|
|
487
|
+
this.onEvent = options.onEvent;
|
|
488
|
+
this.singleFlightRefresh = options.singleFlightRefresh ?? true;
|
|
489
|
+
}
|
|
490
|
+
async getValidTokens(options = {}) {
|
|
491
|
+
const tokens = await this.loadTokens();
|
|
492
|
+
if (!options.forceRefresh && tokens.expiresAt > this.now() + this.refreshSkewMs) {
|
|
493
|
+
return tokens;
|
|
494
|
+
}
|
|
495
|
+
if (this.singleFlightRefresh) {
|
|
496
|
+
if (!this.refreshPromise) {
|
|
497
|
+
this.refreshPromise = this.refreshTokens(tokens).finally(() => {
|
|
498
|
+
this.refreshPromise = null;
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return this.refreshPromise;
|
|
502
|
+
}
|
|
503
|
+
return this.refreshTokens(tokens);
|
|
504
|
+
}
|
|
505
|
+
async getClient(options = {}) {
|
|
506
|
+
const tokens = await this.getValidTokens(options);
|
|
507
|
+
return this.edge.forUser(tokens.accessToken);
|
|
508
|
+
}
|
|
509
|
+
async withClient(callback, options = {}) {
|
|
510
|
+
const tokens = await this.getValidTokens(options);
|
|
511
|
+
return callback(this.edge.forUser(tokens.accessToken), tokens);
|
|
512
|
+
}
|
|
513
|
+
async saveTokens(tokens, refreshed = false) {
|
|
514
|
+
validateTokens(tokens);
|
|
515
|
+
const value = this.tokenVault ? this.tokenVault.encryptTokens(tokens) : tokens;
|
|
516
|
+
await this.tokenStore.save(this.subjectId, value, { tokens, refreshed });
|
|
517
|
+
await this.emit({
|
|
518
|
+
type: "session.saved",
|
|
519
|
+
subjectId: this.subjectId,
|
|
520
|
+
refreshed,
|
|
521
|
+
encrypted: typeof value === "string",
|
|
522
|
+
expiresAt: tokens.expiresAt,
|
|
523
|
+
scope: tokens.scope
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
async loadTokens() {
|
|
527
|
+
const value = await this.tokenStore.load(this.subjectId);
|
|
528
|
+
if (!value) {
|
|
529
|
+
throw new EdgeAuthenticationError("No EDGE tokens are stored for this subject", {
|
|
530
|
+
subjectId: this.subjectId
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (typeof value === "string") {
|
|
534
|
+
if (!this.tokenVault) {
|
|
535
|
+
throw new EdgeAuthenticationError("Encrypted EDGE token envelope loaded but no tokenVault was configured", {
|
|
536
|
+
subjectId: this.subjectId
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
const tokens = this.tokenVault.decryptTokens(value);
|
|
540
|
+
validateTokens(tokens);
|
|
541
|
+
await this.emitLoaded(tokens, true);
|
|
542
|
+
return tokens;
|
|
543
|
+
}
|
|
544
|
+
validateTokens(value);
|
|
545
|
+
await this.emitLoaded(value, false);
|
|
546
|
+
return value;
|
|
547
|
+
}
|
|
548
|
+
async refreshTokens(currentTokens) {
|
|
549
|
+
await this.emit({
|
|
550
|
+
type: "session.refresh_started",
|
|
551
|
+
subjectId: this.subjectId,
|
|
552
|
+
expiresAt: currentTokens.expiresAt,
|
|
553
|
+
scope: currentTokens.scope
|
|
554
|
+
});
|
|
555
|
+
try {
|
|
556
|
+
const refreshed = await this.edge.refreshTokens(currentTokens.refreshToken);
|
|
557
|
+
const tokens = {
|
|
558
|
+
...refreshed,
|
|
559
|
+
refreshToken: refreshed.refreshToken || currentTokens.refreshToken
|
|
560
|
+
};
|
|
561
|
+
await this.saveTokens(tokens, true);
|
|
562
|
+
await this.onRefresh?.(tokens);
|
|
563
|
+
await this.emit({
|
|
564
|
+
type: "session.refresh_succeeded",
|
|
565
|
+
subjectId: this.subjectId,
|
|
566
|
+
refreshed: true,
|
|
567
|
+
expiresAt: tokens.expiresAt,
|
|
568
|
+
scope: tokens.scope
|
|
569
|
+
});
|
|
570
|
+
return tokens;
|
|
571
|
+
} catch (error) {
|
|
572
|
+
await this.emit({
|
|
573
|
+
type: "session.refresh_failed",
|
|
574
|
+
subjectId: this.subjectId,
|
|
575
|
+
error: error instanceof Error ? error.message : "Unknown token refresh error"
|
|
576
|
+
});
|
|
577
|
+
throw error;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async emitLoaded(tokens, encrypted) {
|
|
581
|
+
await this.emit({
|
|
582
|
+
type: "session.loaded",
|
|
583
|
+
subjectId: this.subjectId,
|
|
584
|
+
encrypted,
|
|
585
|
+
expiresAt: tokens.expiresAt,
|
|
586
|
+
scope: tokens.scope
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
async emit(event) {
|
|
590
|
+
try {
|
|
591
|
+
await this.onEvent?.(event);
|
|
592
|
+
} catch {
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
function createEdgeUserSession(options) {
|
|
597
|
+
return new EdgeUserSession(options);
|
|
598
|
+
}
|
|
599
|
+
function validateTokens(tokens) {
|
|
600
|
+
const errors = {};
|
|
601
|
+
if (!tokens || typeof tokens !== "object") {
|
|
602
|
+
errors.tokens = ["Expected token object"];
|
|
603
|
+
} else {
|
|
604
|
+
if (typeof tokens.accessToken !== "string" || !tokens.accessToken.trim()) {
|
|
605
|
+
errors.accessToken = ["Required"];
|
|
606
|
+
}
|
|
607
|
+
if (typeof tokens.refreshToken !== "string" || !tokens.refreshToken.trim()) {
|
|
608
|
+
errors.refreshToken = ["Required"];
|
|
609
|
+
}
|
|
610
|
+
if (typeof tokens.expiresAt !== "number" || !Number.isFinite(tokens.expiresAt) || tokens.expiresAt <= 0) {
|
|
611
|
+
errors.expiresAt = ["Must be a positive Unix timestamp in milliseconds"];
|
|
612
|
+
}
|
|
613
|
+
if (typeof tokens.expiresIn !== "number" || !Number.isFinite(tokens.expiresIn) || tokens.expiresIn <= 0) {
|
|
614
|
+
errors.expiresIn = ["Must be a positive number"];
|
|
615
|
+
}
|
|
616
|
+
if (typeof tokens.scope !== "string") {
|
|
617
|
+
errors.scope = ["Required"];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (Object.keys(errors).length > 0) {
|
|
621
|
+
throw new EdgeValidationError3("Invalid EDGE token payload", errors);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
258
625
|
// src/types.ts
|
|
259
626
|
var DEFAULT_TIMEOUT = 3e4;
|
|
260
627
|
var USER_AGENT = "@edge-markets/connect-node/1.0.0";
|
|
@@ -265,12 +632,126 @@ var DEFAULT_RETRY_CONFIG = {
|
|
|
265
632
|
baseDelayMs: 1e3
|
|
266
633
|
};
|
|
267
634
|
|
|
635
|
+
// src/webhook-reconciler.ts
|
|
636
|
+
var DEFAULT_MAX_PAGES = 10;
|
|
637
|
+
var EdgeWebhookReconciler = class {
|
|
638
|
+
constructor(options) {
|
|
639
|
+
this.running = false;
|
|
640
|
+
if (!options.edge?.syncWebhookEvents) {
|
|
641
|
+
throw new Error("EdgeWebhookReconciler: edge.syncWebhookEvents is required");
|
|
642
|
+
}
|
|
643
|
+
if (!options.cursorStore?.load || !options.cursorStore?.save) {
|
|
644
|
+
throw new Error("EdgeWebhookReconciler: cursorStore.load and cursorStore.save are required");
|
|
645
|
+
}
|
|
646
|
+
if (!options.processEvent && !options.inbox) {
|
|
647
|
+
throw new Error("EdgeWebhookReconciler: processEvent or inbox is required");
|
|
648
|
+
}
|
|
649
|
+
if (options.maxPages !== void 0 && (!Number.isInteger(options.maxPages) || options.maxPages <= 0)) {
|
|
650
|
+
throw new Error("EdgeWebhookReconciler: maxPages must be a positive integer");
|
|
651
|
+
}
|
|
652
|
+
this.options = options;
|
|
653
|
+
}
|
|
654
|
+
async run() {
|
|
655
|
+
if (this.running) {
|
|
656
|
+
return {
|
|
657
|
+
processed: 0,
|
|
658
|
+
pages: 0,
|
|
659
|
+
hasMore: false,
|
|
660
|
+
skipped: true,
|
|
661
|
+
reason: "already_running"
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
this.running = true;
|
|
665
|
+
try {
|
|
666
|
+
return await this.runInternal();
|
|
667
|
+
} finally {
|
|
668
|
+
this.running = false;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async runInternal() {
|
|
672
|
+
let cursor = normalizeCursor(await this.options.cursorStore.load());
|
|
673
|
+
let processed = 0;
|
|
674
|
+
let accepted = 0;
|
|
675
|
+
let duplicates = 0;
|
|
676
|
+
let pages = 0;
|
|
677
|
+
let hasMore = false;
|
|
678
|
+
const maxPages = this.options.maxPages ?? DEFAULT_MAX_PAGES;
|
|
679
|
+
while (pages < maxPages) {
|
|
680
|
+
const result = await this.options.edge.syncWebhookEvents({
|
|
681
|
+
...cursor ? { afterEventId: cursor } : {},
|
|
682
|
+
...this.options.limit !== void 0 ? { limit: this.options.limit } : {}
|
|
683
|
+
});
|
|
684
|
+
pages++;
|
|
685
|
+
hasMore = result.hasMore;
|
|
686
|
+
if (result.events.length === 0) {
|
|
687
|
+
return {
|
|
688
|
+
processed,
|
|
689
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
690
|
+
pages,
|
|
691
|
+
...cursor ? { cursor } : {},
|
|
692
|
+
hasMore,
|
|
693
|
+
skipped: false,
|
|
694
|
+
...hasMore ? { reason: "empty_page_with_has_more" } : {}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
for (const event of result.events) {
|
|
698
|
+
if (this.options.inbox) {
|
|
699
|
+
const acceptResult = await this.options.inbox.acceptSyncedEvent(event);
|
|
700
|
+
accepted++;
|
|
701
|
+
if (acceptResult.duplicate) duplicates++;
|
|
702
|
+
const processResult = await this.options.inbox.process(event.id);
|
|
703
|
+
if (processResult.processed) processed++;
|
|
704
|
+
} else {
|
|
705
|
+
await this.options.processEvent?.(event);
|
|
706
|
+
processed++;
|
|
707
|
+
}
|
|
708
|
+
cursor = event.id;
|
|
709
|
+
await this.options.cursorStore.save(cursor);
|
|
710
|
+
}
|
|
711
|
+
if (!result.hasMore) {
|
|
712
|
+
return {
|
|
713
|
+
processed,
|
|
714
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
715
|
+
pages,
|
|
716
|
+
...cursor ? { cursor } : {},
|
|
717
|
+
hasMore: false,
|
|
718
|
+
skipped: false
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
processed,
|
|
724
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
725
|
+
pages,
|
|
726
|
+
...cursor ? { cursor } : {},
|
|
727
|
+
hasMore,
|
|
728
|
+
skipped: false,
|
|
729
|
+
...hasMore ? { reason: "max_pages_reached" } : {}
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
function createWebhookReconciler(options) {
|
|
734
|
+
return new EdgeWebhookReconciler(options);
|
|
735
|
+
}
|
|
736
|
+
function normalizeCursor(cursor) {
|
|
737
|
+
if (typeof cursor !== "string") return void 0;
|
|
738
|
+
const trimmed = cursor.trim();
|
|
739
|
+
return trimmed || void 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
268
742
|
// src/edge-connect-server.ts
|
|
269
743
|
var instances = /* @__PURE__ */ new Map();
|
|
270
744
|
function getInstanceKey(config) {
|
|
271
|
-
return
|
|
745
|
+
return JSON.stringify([
|
|
746
|
+
config.clientId,
|
|
747
|
+
config.environment,
|
|
748
|
+
config.apiBaseUrl || "",
|
|
749
|
+
config.oauthBaseUrl || "",
|
|
750
|
+
config.partnerApiBaseUrl || "",
|
|
751
|
+
config.partnerClientId || ""
|
|
752
|
+
]);
|
|
272
753
|
}
|
|
273
|
-
var MTLS_ENFORCED_HOSTS = /* @__PURE__ */ new Set(["connect-staging.edgeboost.io", "
|
|
754
|
+
var MTLS_ENFORCED_HOSTS = /* @__PURE__ */ new Set(["connect-staging.edgeboost.io", "connect.edgeboost.io"]);
|
|
274
755
|
function isMtlsEnforcedHost(host) {
|
|
275
756
|
if (MTLS_ENFORCED_HOSTS.has(host)) {
|
|
276
757
|
return true;
|
|
@@ -292,6 +773,29 @@ function environmentRequiresMtls(environment, apiBaseUrl) {
|
|
|
292
773
|
return false;
|
|
293
774
|
}
|
|
294
775
|
}
|
|
776
|
+
var CONNECT_API_BASE_PATH = "/connect/v1";
|
|
777
|
+
function stripTrailingSlash(value) {
|
|
778
|
+
let end = value.length;
|
|
779
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
780
|
+
end--;
|
|
781
|
+
}
|
|
782
|
+
return value.slice(0, end);
|
|
783
|
+
}
|
|
784
|
+
function derivePartnerApiBaseUrl(apiBaseUrl) {
|
|
785
|
+
const url = new URL(apiBaseUrl);
|
|
786
|
+
const normalizedPath = stripTrailingSlash(url.pathname);
|
|
787
|
+
if (normalizedPath.endsWith(CONNECT_API_BASE_PATH)) {
|
|
788
|
+
const prefix = normalizedPath.slice(0, -CONNECT_API_BASE_PATH.length);
|
|
789
|
+
url.pathname = `${prefix}/v1`;
|
|
790
|
+
} else {
|
|
791
|
+
url.pathname = normalizedPath || "/";
|
|
792
|
+
}
|
|
793
|
+
url.search = "";
|
|
794
|
+
url.hash = "";
|
|
795
|
+
return stripTrailingSlash(url.toString());
|
|
796
|
+
}
|
|
797
|
+
var SYNC_WEBHOOK_EVENTS_MIN_LIMIT = 1;
|
|
798
|
+
var SYNC_WEBHOOK_EVENTS_MAX_LIMIT = 100;
|
|
295
799
|
var EdgeConnectServer = class _EdgeConnectServer {
|
|
296
800
|
constructor(config) {
|
|
297
801
|
this.partnerTokenCache = null;
|
|
@@ -307,6 +811,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
307
811
|
this.config = config;
|
|
308
812
|
const envConfig = getEnvironmentConfig(config.environment);
|
|
309
813
|
this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
|
|
814
|
+
this.partnerApiBaseUrl = stripTrailingSlash(config.partnerApiBaseUrl || derivePartnerApiBaseUrl(this.apiBaseUrl));
|
|
310
815
|
this.oauthBaseUrl = config.oauthBaseUrl || envConfig.oauthBaseUrl;
|
|
311
816
|
this.timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
312
817
|
this.retryConfig = {
|
|
@@ -349,10 +854,13 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
349
854
|
*/
|
|
350
855
|
forUser(accessToken) {
|
|
351
856
|
if (!accessToken) {
|
|
352
|
-
throw new
|
|
857
|
+
throw new EdgeAuthenticationError2("accessToken is required when calling forUser()");
|
|
353
858
|
}
|
|
354
859
|
return new EdgeUserClient(this, accessToken);
|
|
355
860
|
}
|
|
861
|
+
createUserSession(options) {
|
|
862
|
+
return new EdgeUserSession({ ...options, edge: this });
|
|
863
|
+
}
|
|
356
864
|
async exchangeCode(code, codeVerifier, redirectUri) {
|
|
357
865
|
const tokenUrl = `${this.oauthBaseUrl}/token`;
|
|
358
866
|
const body = {
|
|
@@ -410,7 +918,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
410
918
|
);
|
|
411
919
|
if (!response.ok) {
|
|
412
920
|
const error = await response.json().catch(() => ({}));
|
|
413
|
-
throw new
|
|
921
|
+
throw new EdgeAuthenticationError2(
|
|
414
922
|
error.message || error.error_description || "Token refresh failed. User may need to reconnect.",
|
|
415
923
|
{ tokenError: error }
|
|
416
924
|
);
|
|
@@ -543,21 +1051,41 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
543
1051
|
*/
|
|
544
1052
|
async syncWebhookEvents(options = {}) {
|
|
545
1053
|
if (!this.config.partnerClientId) {
|
|
546
|
-
throw new
|
|
1054
|
+
throw new EdgeAuthenticationError2("syncWebhookEvents requires `partnerClientId` in EdgeConnectServer config");
|
|
547
1055
|
}
|
|
548
1056
|
if (!this.config.partnerClientSecret) {
|
|
549
|
-
throw new
|
|
1057
|
+
throw new EdgeAuthenticationError2("syncWebhookEvents requires `partnerClientSecret` in EdgeConnectServer config");
|
|
550
1058
|
}
|
|
551
|
-
const
|
|
1059
|
+
const normalizedOptions = this.normalizeSyncWebhookEventsOptions(options);
|
|
1060
|
+
const url = this.buildSyncUrl(normalizedOptions);
|
|
552
1061
|
let token = await this.getPartnerToken();
|
|
553
|
-
let response = await this.partnerFetch(url, token);
|
|
1062
|
+
let { response, durationMs, attempt } = await this.partnerFetch(url, token);
|
|
554
1063
|
if (response.status === 401) {
|
|
1064
|
+
await this.drainResponseBody(response);
|
|
1065
|
+
this.config.onResponse?.({
|
|
1066
|
+
status: response.status,
|
|
1067
|
+
body: { error: "unauthorized" },
|
|
1068
|
+
durationMs,
|
|
1069
|
+
method: "GET",
|
|
1070
|
+
url,
|
|
1071
|
+
endpoint: "partner_webhook_sync",
|
|
1072
|
+
attempt
|
|
1073
|
+
});
|
|
555
1074
|
this.partnerTokenCache = null;
|
|
556
1075
|
token = await this.getPartnerToken();
|
|
557
|
-
response = await this.partnerFetch(url, token);
|
|
1076
|
+
({ response, durationMs, attempt } = await this.partnerFetch(url, token));
|
|
558
1077
|
if (response.status === 401) {
|
|
559
1078
|
const body = await response.json().catch(() => ({}));
|
|
560
|
-
|
|
1079
|
+
this.config.onResponse?.({
|
|
1080
|
+
status: response.status,
|
|
1081
|
+
body: this.redactPartnerSyncResponseBody(body),
|
|
1082
|
+
durationMs,
|
|
1083
|
+
method: "GET",
|
|
1084
|
+
url,
|
|
1085
|
+
endpoint: "partner_webhook_sync",
|
|
1086
|
+
attempt
|
|
1087
|
+
});
|
|
1088
|
+
throw new EdgeAuthenticationError2(
|
|
561
1089
|
body.message || "Partner token rejected after refresh \u2014 check partnerClientId/Secret",
|
|
562
1090
|
body
|
|
563
1091
|
);
|
|
@@ -565,6 +1093,15 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
565
1093
|
}
|
|
566
1094
|
if (!response.ok) {
|
|
567
1095
|
const body = await response.json().catch(() => ({}));
|
|
1096
|
+
this.config.onResponse?.({
|
|
1097
|
+
status: response.status,
|
|
1098
|
+
body: this.redactPartnerSyncResponseBody(body),
|
|
1099
|
+
durationMs,
|
|
1100
|
+
method: "GET",
|
|
1101
|
+
url,
|
|
1102
|
+
endpoint: "partner_webhook_sync",
|
|
1103
|
+
attempt
|
|
1104
|
+
});
|
|
568
1105
|
throw new EdgeApiError(
|
|
569
1106
|
body.error || "sync_failed",
|
|
570
1107
|
body.message || `syncWebhookEvents failed with status ${response.status}`,
|
|
@@ -573,17 +1110,57 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
573
1110
|
);
|
|
574
1111
|
}
|
|
575
1112
|
const wire = await response.json().catch(() => ({}));
|
|
1113
|
+
this.config.onResponse?.({
|
|
1114
|
+
status: response.status,
|
|
1115
|
+
body: {
|
|
1116
|
+
eventCount: wire.events?.length ?? 0,
|
|
1117
|
+
hasMore: !!wire.has_more
|
|
1118
|
+
},
|
|
1119
|
+
durationMs,
|
|
1120
|
+
method: "GET",
|
|
1121
|
+
url,
|
|
1122
|
+
endpoint: "partner_webhook_sync",
|
|
1123
|
+
attempt
|
|
1124
|
+
});
|
|
576
1125
|
const events = (wire.events ?? []).map((entry) => entry.payload).filter((p) => !!p);
|
|
577
1126
|
return {
|
|
578
1127
|
events,
|
|
579
1128
|
hasMore: !!wire.has_more
|
|
580
1129
|
};
|
|
581
1130
|
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Creates a cursor-based webhook reconciler bound to this SDK instance.
|
|
1133
|
+
*
|
|
1134
|
+
* Use this from cron jobs to process events returned by
|
|
1135
|
+
* {@link EdgeConnectServer.syncWebhookEvents} without hand-writing
|
|
1136
|
+
* pagination, cursor advancement, or process-level concurrency guards.
|
|
1137
|
+
*/
|
|
1138
|
+
createWebhookReconciler(options) {
|
|
1139
|
+
return new EdgeWebhookReconciler({ ...options, edge: this });
|
|
1140
|
+
}
|
|
582
1141
|
/**
|
|
583
1142
|
* Builds the sync URL with normalized query parameters.
|
|
584
1143
|
*/
|
|
1144
|
+
normalizeSyncWebhookEventsOptions(options) {
|
|
1145
|
+
const normalized = {};
|
|
1146
|
+
if (typeof options.afterEventId === "string" && options.afterEventId.trim()) {
|
|
1147
|
+
normalized.afterEventId = options.afterEventId.trim();
|
|
1148
|
+
}
|
|
1149
|
+
if (options.limit !== void 0) {
|
|
1150
|
+
if (!Number.isInteger(options.limit) || options.limit < SYNC_WEBHOOK_EVENTS_MIN_LIMIT || options.limit > SYNC_WEBHOOK_EVENTS_MAX_LIMIT) {
|
|
1151
|
+
throw new EdgeApiError(
|
|
1152
|
+
"invalid_sync_options",
|
|
1153
|
+
`syncWebhookEvents limit must be an integer between ${SYNC_WEBHOOK_EVENTS_MIN_LIMIT} and ${SYNC_WEBHOOK_EVENTS_MAX_LIMIT}`,
|
|
1154
|
+
400,
|
|
1155
|
+
{ limit: options.limit }
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
normalized.limit = options.limit;
|
|
1159
|
+
}
|
|
1160
|
+
return normalized;
|
|
1161
|
+
}
|
|
585
1162
|
buildSyncUrl(options) {
|
|
586
|
-
const url = new URL(`${this.
|
|
1163
|
+
const url = new URL(`${this.partnerApiBaseUrl}/partner/webhooks/events/sync`);
|
|
587
1164
|
if (options.afterEventId) {
|
|
588
1165
|
url.searchParams.set("after_event_id", options.afterEventId);
|
|
589
1166
|
}
|
|
@@ -598,22 +1175,57 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
598
1175
|
* lifecycle (process-wide, client_credentials) than user tokens.
|
|
599
1176
|
*/
|
|
600
1177
|
async partnerFetch(url, token) {
|
|
601
|
-
|
|
602
|
-
|
|
1178
|
+
let lastError = null;
|
|
1179
|
+
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
1180
|
+
if (attempt > 0) {
|
|
1181
|
+
await this.sleep(this.getRetryDelay(attempt - 1));
|
|
1182
|
+
}
|
|
1183
|
+
const startTime = Date.now();
|
|
1184
|
+
this.config.onRequest?.({
|
|
1185
|
+
method: "GET",
|
|
603
1186
|
url,
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
1187
|
+
endpoint: "partner_webhook_sync",
|
|
1188
|
+
attempt
|
|
1189
|
+
});
|
|
1190
|
+
try {
|
|
1191
|
+
const response = await this.fetchWithTimeout(
|
|
1192
|
+
url,
|
|
1193
|
+
{
|
|
1194
|
+
method: "GET",
|
|
1195
|
+
headers: {
|
|
1196
|
+
Authorization: `Bearer ${token}`,
|
|
1197
|
+
"Content-Type": "application/json",
|
|
1198
|
+
"User-Agent": USER_AGENT
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
this.dispatcher
|
|
1202
|
+
);
|
|
1203
|
+
const durationMs = Date.now() - startTime;
|
|
1204
|
+
if (response.status !== 401 && this.retryConfig.retryOn.includes(response.status) && attempt < this.retryConfig.maxRetries) {
|
|
1205
|
+
await this.drainResponseBody(response);
|
|
1206
|
+
this.config.onResponse?.({
|
|
1207
|
+
status: response.status,
|
|
1208
|
+
body: { retrying: true },
|
|
1209
|
+
durationMs,
|
|
1210
|
+
method: "GET",
|
|
1211
|
+
url,
|
|
1212
|
+
endpoint: "partner_webhook_sync",
|
|
1213
|
+
attempt
|
|
1214
|
+
});
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
return { response, durationMs, attempt };
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
lastError = error;
|
|
1220
|
+
if (attempt >= this.retryConfig.maxRetries) {
|
|
1221
|
+
throw new EdgeNetworkError("syncWebhookEvents network failure", lastError);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
616
1224
|
}
|
|
1225
|
+
throw new EdgeNetworkError(
|
|
1226
|
+
`syncWebhookEvents network failure after ${this.retryConfig.maxRetries} retries`,
|
|
1227
|
+
lastError
|
|
1228
|
+
);
|
|
617
1229
|
}
|
|
618
1230
|
/**
|
|
619
1231
|
* Fetches (or returns the cached) partner token via the
|
|
@@ -629,6 +1241,17 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
629
1241
|
}
|
|
630
1242
|
const tokenUrl = `${this.oauthBaseUrl}/token`;
|
|
631
1243
|
let response;
|
|
1244
|
+
const startTime = Date.now();
|
|
1245
|
+
this.config.onRequest?.({
|
|
1246
|
+
method: "POST",
|
|
1247
|
+
url: tokenUrl,
|
|
1248
|
+
endpoint: "oauth_token",
|
|
1249
|
+
body: {
|
|
1250
|
+
grant_type: "client_credentials",
|
|
1251
|
+
client_id: this.config.partnerClientId,
|
|
1252
|
+
client_secret: "[REDACTED]"
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
632
1255
|
try {
|
|
633
1256
|
response = await this.fetchWithTimeout(
|
|
634
1257
|
tokenUrl,
|
|
@@ -649,16 +1272,24 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
649
1272
|
} catch (error) {
|
|
650
1273
|
throw new EdgeNetworkError("Partner token request network failure", error);
|
|
651
1274
|
}
|
|
1275
|
+
const durationMs = Date.now() - startTime;
|
|
1276
|
+
const data = await response.json().catch(() => ({}));
|
|
1277
|
+
this.config.onResponse?.({
|
|
1278
|
+
status: response.status,
|
|
1279
|
+
body: this.redactPartnerTokenResponseBody(data),
|
|
1280
|
+
durationMs,
|
|
1281
|
+
method: "POST",
|
|
1282
|
+
url: tokenUrl,
|
|
1283
|
+
endpoint: "oauth_token"
|
|
1284
|
+
});
|
|
652
1285
|
if (!response.ok) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
body
|
|
1286
|
+
throw new EdgeAuthenticationError2(
|
|
1287
|
+
data.message || data.error_description || `Partner token request failed with status ${response.status}`,
|
|
1288
|
+
data
|
|
657
1289
|
);
|
|
658
1290
|
}
|
|
659
|
-
const data = await response.json().catch(() => ({}));
|
|
660
1291
|
if (!data.access_token || typeof data.expires_in !== "number") {
|
|
661
|
-
throw new
|
|
1292
|
+
throw new EdgeAuthenticationError2("Partner token response missing access_token or expires_in");
|
|
662
1293
|
}
|
|
663
1294
|
this.partnerTokenCache = {
|
|
664
1295
|
token: data.access_token,
|
|
@@ -673,6 +1304,26 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
673
1304
|
_resetPartnerTokenCache() {
|
|
674
1305
|
this.partnerTokenCache = null;
|
|
675
1306
|
}
|
|
1307
|
+
redactPartnerTokenResponseBody(body) {
|
|
1308
|
+
return {
|
|
1309
|
+
...body,
|
|
1310
|
+
...typeof body.access_token === "string" ? { access_token: "[REDACTED]" } : {},
|
|
1311
|
+
...typeof body.refresh_token === "string" ? { refresh_token: "[REDACTED]" } : {},
|
|
1312
|
+
...typeof body.id_token === "string" ? { id_token: "[REDACTED]" } : {}
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
redactPartnerSyncResponseBody(body) {
|
|
1316
|
+
if (Array.isArray(body.events)) {
|
|
1317
|
+
return {
|
|
1318
|
+
...body,
|
|
1319
|
+
events: `[REDACTED ${body.events.length} events]`
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
return body;
|
|
1323
|
+
}
|
|
1324
|
+
async drainResponseBody(response) {
|
|
1325
|
+
await response.arrayBuffer().catch(() => void 0);
|
|
1326
|
+
}
|
|
676
1327
|
getRetryDelay(attempt) {
|
|
677
1328
|
const { backoff, baseDelayMs } = this.retryConfig;
|
|
678
1329
|
if (backoff === "exponential") {
|
|
@@ -718,7 +1369,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
718
1369
|
});
|
|
719
1370
|
}
|
|
720
1371
|
if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
|
|
721
|
-
return new
|
|
1372
|
+
return new EdgeAuthenticationError2("Invalid client credentials. Check your client ID and secret.", {
|
|
722
1373
|
edgeBoostError: error
|
|
723
1374
|
});
|
|
724
1375
|
}
|
|
@@ -732,7 +1383,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
732
1383
|
}
|
|
733
1384
|
async handleApiErrorFromBody(error, status, path) {
|
|
734
1385
|
if (status === 401) {
|
|
735
|
-
return new
|
|
1386
|
+
return new EdgeAuthenticationError2(error.message || "Access token is invalid or expired", error);
|
|
736
1387
|
}
|
|
737
1388
|
if (status === 403) {
|
|
738
1389
|
if (error.error === "consent_required") {
|
|
@@ -782,8 +1433,231 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
782
1433
|
}
|
|
783
1434
|
};
|
|
784
1435
|
|
|
1436
|
+
// src/env.ts
|
|
1437
|
+
import { EDGE_ENVIRONMENTS, EdgeValidationError as EdgeValidationError4, getEnvironmentConfig as getEnvironmentConfig2 } from "@edge-markets/connect";
|
|
1438
|
+
import fs2 from "fs";
|
|
1439
|
+
var ENVIRONMENT_NAMES = new Set(Object.keys(EDGE_ENVIRONMENTS));
|
|
1440
|
+
function readEnv(env, name) {
|
|
1441
|
+
const value = env[name];
|
|
1442
|
+
if (typeof value !== "string") return void 0;
|
|
1443
|
+
const trimmed = value.trim();
|
|
1444
|
+
return trimmed ? trimmed : void 0;
|
|
1445
|
+
}
|
|
1446
|
+
function readFirstEnv(env, names) {
|
|
1447
|
+
for (const name of names) {
|
|
1448
|
+
const value = readEnv(env, name);
|
|
1449
|
+
if (value) return value;
|
|
1450
|
+
}
|
|
1451
|
+
return void 0;
|
|
1452
|
+
}
|
|
1453
|
+
function normalizePem(value) {
|
|
1454
|
+
return value.replace(/\\n/g, "\n").trim();
|
|
1455
|
+
}
|
|
1456
|
+
function readPemValue(env, valueName, pathName, warnings) {
|
|
1457
|
+
const inlineValue = readEnv(env, valueName);
|
|
1458
|
+
const filePath = readEnv(env, pathName);
|
|
1459
|
+
if (inlineValue && filePath) {
|
|
1460
|
+
warnings.push(`${valueName} and ${pathName} are both set; using ${valueName}`);
|
|
1461
|
+
}
|
|
1462
|
+
if (inlineValue) {
|
|
1463
|
+
return normalizePem(inlineValue);
|
|
1464
|
+
}
|
|
1465
|
+
if (!filePath) {
|
|
1466
|
+
return void 0;
|
|
1467
|
+
}
|
|
1468
|
+
try {
|
|
1469
|
+
return normalizePem(fs2.readFileSync(filePath, "utf8"));
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1472
|
+
throw new EdgeValidationError4(`Failed to read ${pathName}`, {
|
|
1473
|
+
[pathName]: [`Unable to read PEM file "${filePath}": ${message}`]
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function readEnvironment(env) {
|
|
1478
|
+
const value = readEnv(env, "EDGE_ENVIRONMENT") || "staging";
|
|
1479
|
+
if (!ENVIRONMENT_NAMES.has(value)) {
|
|
1480
|
+
throw new EdgeValidationError4("Invalid EDGE_ENVIRONMENT", {
|
|
1481
|
+
EDGE_ENVIRONMENT: [`Must be one of: ${Array.from(ENVIRONMENT_NAMES).join(", ")}`]
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
return value;
|
|
1485
|
+
}
|
|
1486
|
+
function readTimeout(env, override) {
|
|
1487
|
+
if (override !== void 0) return override;
|
|
1488
|
+
const value = readFirstEnv(env, ["EDGE_TIMEOUT_MS", "EDGE_REQUEST_TIMEOUT_MS"]);
|
|
1489
|
+
if (!value) return void 0;
|
|
1490
|
+
const timeout = Number(value);
|
|
1491
|
+
if (!Number.isInteger(timeout) || timeout <= 0) {
|
|
1492
|
+
throw new EdgeValidationError4("Invalid EDGE_TIMEOUT_MS", {
|
|
1493
|
+
EDGE_TIMEOUT_MS: ["Must be a positive integer number of milliseconds"]
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
return timeout;
|
|
1497
|
+
}
|
|
1498
|
+
function buildMtlsConfig(env, requireMtls, warnings) {
|
|
1499
|
+
const cert = readPemValue(env, "EDGE_MTLS_CERT", "EDGE_MTLS_CERT_PATH", warnings);
|
|
1500
|
+
const key = readPemValue(env, "EDGE_MTLS_KEY", "EDGE_MTLS_KEY_PATH", warnings);
|
|
1501
|
+
const ca = readPemValue(env, "EDGE_MTLS_CA", "EDGE_MTLS_CA_PATH", warnings);
|
|
1502
|
+
if (!cert && !key && !ca) {
|
|
1503
|
+
if (requireMtls) {
|
|
1504
|
+
throw new EdgeValidationError4("mTLS is required but no EDGE_MTLS_CERT or EDGE_MTLS_KEY was provided", {
|
|
1505
|
+
EDGE_MTLS_CERT: ["Required when requireMtls is true"],
|
|
1506
|
+
EDGE_MTLS_KEY: ["Required when requireMtls is true"]
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
return void 0;
|
|
1510
|
+
}
|
|
1511
|
+
if (!cert || !key) {
|
|
1512
|
+
throw new EdgeValidationError4("Incomplete mTLS configuration", {
|
|
1513
|
+
EDGE_MTLS_CERT: cert ? [] : ["Required when mTLS is configured"],
|
|
1514
|
+
EDGE_MTLS_KEY: key ? [] : ["Required when mTLS is configured"]
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
return {
|
|
1518
|
+
enabled: true,
|
|
1519
|
+
cert,
|
|
1520
|
+
key,
|
|
1521
|
+
...ca ? { ca } : {}
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
function inferMtlsRequirement(environment, apiBaseUrl) {
|
|
1525
|
+
if (environment === "staging" || environment === "production") return true;
|
|
1526
|
+
const resolvedApiBaseUrl = apiBaseUrl || getEnvironmentConfig2(environment).apiBaseUrl;
|
|
1527
|
+
try {
|
|
1528
|
+
const host = new URL(resolvedApiBaseUrl).host;
|
|
1529
|
+
return host === "connect-staging.edgeboost.io" || host === "connect.edgeboost.io";
|
|
1530
|
+
} catch {
|
|
1531
|
+
return false;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function createEdgeConnectServerFromEnv(options = {}) {
|
|
1535
|
+
const env = options.env ?? process.env;
|
|
1536
|
+
const warnings = [];
|
|
1537
|
+
const environment = readEnvironment(env);
|
|
1538
|
+
const apiBaseUrl = readEnv(env, "EDGE_API_BASE_URL");
|
|
1539
|
+
const mtlsRequiredByTarget = inferMtlsRequirement(environment, apiBaseUrl);
|
|
1540
|
+
const requireMtls = options.requireMtls ?? false;
|
|
1541
|
+
const mtls = buildMtlsConfig(env, requireMtls, warnings);
|
|
1542
|
+
const timeout = readTimeout(env, options.timeout);
|
|
1543
|
+
const clientId = readFirstEnv(env, ["EDGE_CLIENT_ID", "EDGE_CONNECT_CLIENT_ID"]) || "";
|
|
1544
|
+
const clientSecret = readFirstEnv(env, ["EDGE_CLIENT_SECRET", "EDGE_CONNECT_CLIENT_SECRET"]) || "";
|
|
1545
|
+
const oauthBaseUrl = readEnv(env, "EDGE_OAUTH_BASE_URL");
|
|
1546
|
+
const partnerApiBaseUrl = readEnv(env, "EDGE_PARTNER_API_BASE_URL");
|
|
1547
|
+
const partnerClientId = readFirstEnv(env, ["EDGE_PARTNER_CLIENT_ID", "EDGE_DASHBOARD_CLIENT_ID"]);
|
|
1548
|
+
const partnerClientSecret = readFirstEnv(env, ["EDGE_PARTNER_CLIENT_SECRET", "EDGE_DASHBOARD_CLIENT_SECRET"]);
|
|
1549
|
+
const validationErrors = {};
|
|
1550
|
+
if (!clientId) validationErrors.EDGE_CLIENT_ID = ["Required"];
|
|
1551
|
+
if (!clientSecret) validationErrors.EDGE_CLIENT_SECRET = ["Required"];
|
|
1552
|
+
if (Object.keys(validationErrors).length > 0) {
|
|
1553
|
+
throw new EdgeValidationError4("Missing required EDGE Connect environment variables", validationErrors);
|
|
1554
|
+
}
|
|
1555
|
+
if (mtlsRequiredByTarget && !mtls) {
|
|
1556
|
+
warnings.push(
|
|
1557
|
+
`EDGE_ENVIRONMENT=${environment} uses an mTLS-enforced EDGE Connect target; configure EDGE_MTLS_CERT and EDGE_MTLS_KEY before making API calls.`
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
const config = {
|
|
1561
|
+
clientId,
|
|
1562
|
+
clientSecret,
|
|
1563
|
+
environment,
|
|
1564
|
+
...apiBaseUrl ? { apiBaseUrl } : {},
|
|
1565
|
+
...oauthBaseUrl ? { oauthBaseUrl } : {},
|
|
1566
|
+
...partnerApiBaseUrl ? { partnerApiBaseUrl } : {},
|
|
1567
|
+
...partnerClientId ? { partnerClientId } : {},
|
|
1568
|
+
...partnerClientSecret ? { partnerClientSecret } : {},
|
|
1569
|
+
...timeout ? { timeout } : {},
|
|
1570
|
+
...options.retry ? { retry: options.retry } : {},
|
|
1571
|
+
...options.onRequest ? { onRequest: options.onRequest } : {},
|
|
1572
|
+
...options.onResponse ? { onResponse: options.onResponse } : {},
|
|
1573
|
+
...options.mle ? { mle: options.mle } : {},
|
|
1574
|
+
...mtls ? { mtls } : {}
|
|
1575
|
+
};
|
|
1576
|
+
const server = new EdgeConnectServer(config);
|
|
1577
|
+
return {
|
|
1578
|
+
server,
|
|
1579
|
+
config,
|
|
1580
|
+
capabilities: {
|
|
1581
|
+
mtlsEnabled: !!config.mtls?.enabled,
|
|
1582
|
+
webhookSyncConfigured: !!config.partnerClientId && !!config.partnerClientSecret,
|
|
1583
|
+
customApiBaseUrl: !!config.apiBaseUrl,
|
|
1584
|
+
customOAuthBaseUrl: !!config.oauthBaseUrl,
|
|
1585
|
+
customPartnerApiBaseUrl: !!config.partnerApiBaseUrl
|
|
1586
|
+
},
|
|
1587
|
+
warnings
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// src/error-adapter.ts
|
|
1592
|
+
import {
|
|
1593
|
+
EdgeConsentRequiredError as EdgeConsentRequiredError2,
|
|
1594
|
+
EdgeError as EdgeError2,
|
|
1595
|
+
EdgeIdentityVerificationError as EdgeIdentityVerificationError2,
|
|
1596
|
+
EdgeNetworkError as EdgeNetworkError2,
|
|
1597
|
+
EdgeValidationError as EdgeValidationError5
|
|
1598
|
+
} from "@edge-markets/connect";
|
|
1599
|
+
function toEdgeHttpError(error) {
|
|
1600
|
+
const body = toEdgeProblemJson(error);
|
|
1601
|
+
return { status: body.status, body };
|
|
1602
|
+
}
|
|
1603
|
+
function toEdgeProblemJson(error) {
|
|
1604
|
+
if (error instanceof EdgeConsentRequiredError2) {
|
|
1605
|
+
return problem(error, "Consent required", error.statusCode ?? 403, {
|
|
1606
|
+
clientId: error.clientId,
|
|
1607
|
+
consentUrl: error.consentUrl
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
if (error instanceof EdgeValidationError5) {
|
|
1611
|
+
return problem(error, "Validation failed", error.statusCode ?? 400, {
|
|
1612
|
+
validationErrors: error.validationErrors
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
if (error instanceof EdgeIdentityVerificationError2) {
|
|
1616
|
+
return problem(error, "Identity verification failed", error.statusCode ?? 422, {
|
|
1617
|
+
fieldErrors: error.fieldErrors
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
if (error instanceof EdgeNetworkError2) {
|
|
1621
|
+
return problem(error, "EDGE network error", error.statusCode ?? 502);
|
|
1622
|
+
}
|
|
1623
|
+
if (error instanceof EdgeError2) {
|
|
1624
|
+
return problem(error, edgeTitle(error), error.statusCode ?? 500, error.details);
|
|
1625
|
+
}
|
|
1626
|
+
const message = error instanceof Error ? error.message : "Unexpected error";
|
|
1627
|
+
return {
|
|
1628
|
+
type: "https://developer.edgemarkets.io/errors/internal_error",
|
|
1629
|
+
title: "Internal error",
|
|
1630
|
+
status: 500,
|
|
1631
|
+
detail: message,
|
|
1632
|
+
code: "internal_error"
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function problem(error, title, status, details) {
|
|
1636
|
+
return {
|
|
1637
|
+
type: `https://developer.edgemarkets.io/errors/${error.code}`,
|
|
1638
|
+
title,
|
|
1639
|
+
status,
|
|
1640
|
+
detail: error.message,
|
|
1641
|
+
code: error.code,
|
|
1642
|
+
...details && Object.keys(details).length > 0 ? { details } : {}
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
function edgeTitle(error) {
|
|
1646
|
+
return error.code.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// src/http-helpers.ts
|
|
1650
|
+
import { EdgeValidationError as EdgeValidationError7 } from "@edge-markets/connect";
|
|
1651
|
+
|
|
1652
|
+
// src/webhook-parser.ts
|
|
1653
|
+
import {
|
|
1654
|
+
EDGE_WEBHOOK_EVENT_TYPES,
|
|
1655
|
+
EdgeAuthenticationError as EdgeAuthenticationError3,
|
|
1656
|
+
EdgeValidationError as EdgeValidationError6
|
|
1657
|
+
} from "@edge-markets/connect";
|
|
1658
|
+
|
|
785
1659
|
// src/webhook-signing.ts
|
|
786
|
-
import
|
|
1660
|
+
import crypto2 from "crypto";
|
|
787
1661
|
function verifyWebhookSignature(header, body, secret, options = {}) {
|
|
788
1662
|
if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
|
|
789
1663
|
return false;
|
|
@@ -800,28 +1674,883 @@ function verifyWebhookSignature(header, body, secret, options = {}) {
|
|
|
800
1674
|
const now = Math.floor(Date.now() / 1e3);
|
|
801
1675
|
if (Math.abs(now - timestamp) > toleranceSeconds) return false;
|
|
802
1676
|
const signedContent = `${timestamp}.${body}`;
|
|
803
|
-
const expectedHmac =
|
|
1677
|
+
const expectedHmac = crypto2.createHmac("sha256", secret).update(signedContent).digest("hex");
|
|
804
1678
|
try {
|
|
805
1679
|
const sigBuf = Buffer.from(signature, "hex");
|
|
806
1680
|
const expBuf = Buffer.from(expectedHmac, "hex");
|
|
807
1681
|
if (sigBuf.length !== expBuf.length) return false;
|
|
808
|
-
return
|
|
1682
|
+
return crypto2.timingSafeEqual(sigBuf, expBuf);
|
|
1683
|
+
} catch {
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/webhook-parser.ts
|
|
1689
|
+
var EVENT_TYPES = new Set(EDGE_WEBHOOK_EVENT_TYPES);
|
|
1690
|
+
var TRANSFER_TYPES = /* @__PURE__ */ new Set(["debit", "credit"]);
|
|
1691
|
+
function parseAndVerifyWebhook(options) {
|
|
1692
|
+
const body = normalizeRawBody(options.rawBody);
|
|
1693
|
+
const header = normalizeSignatureHeader(options.signatureHeader);
|
|
1694
|
+
const signatureTimestamp = extractWebhookSignatureTimestamp(header);
|
|
1695
|
+
if (!header || !signatureTimestamp) {
|
|
1696
|
+
throw new EdgeAuthenticationError3("Missing or malformed EDGE webhook signature");
|
|
1697
|
+
}
|
|
1698
|
+
if (!verifyWebhookSignature(header, body, options.secret, { toleranceSeconds: options.toleranceSeconds })) {
|
|
1699
|
+
throw new EdgeAuthenticationError3("Invalid EDGE webhook signature");
|
|
1700
|
+
}
|
|
1701
|
+
let parsed;
|
|
1702
|
+
try {
|
|
1703
|
+
parsed = JSON.parse(body);
|
|
1704
|
+
} catch {
|
|
1705
|
+
throw new EdgeValidationError6("EDGE webhook body must be valid JSON", {
|
|
1706
|
+
rawBody: ["Unable to parse JSON after signature verification"]
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
const event = validateEdgeWebhookEvent(parsed);
|
|
1710
|
+
return {
|
|
1711
|
+
event,
|
|
1712
|
+
eventId: event.id,
|
|
1713
|
+
eventType: event.type,
|
|
1714
|
+
signatureTimestamp,
|
|
1715
|
+
receivedAt: options.receivedAt ?? /* @__PURE__ */ new Date()
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
function extractWebhookSignatureTimestamp(header) {
|
|
1719
|
+
const normalizedHeader = normalizeSignatureHeader(header);
|
|
1720
|
+
if (!normalizedHeader) return void 0;
|
|
1721
|
+
const part = normalizedHeader.split(",").map((value) => value.trim()).find((value) => value.startsWith("t="));
|
|
1722
|
+
if (!part) return void 0;
|
|
1723
|
+
const timestamp = Number(part.slice(2));
|
|
1724
|
+
if (!Number.isInteger(timestamp) || timestamp <= 0) return void 0;
|
|
1725
|
+
return timestamp;
|
|
1726
|
+
}
|
|
1727
|
+
function isEdgeWebhookEvent(value) {
|
|
1728
|
+
try {
|
|
1729
|
+
validateEdgeWebhookEvent(value);
|
|
1730
|
+
return true;
|
|
809
1731
|
} catch {
|
|
810
1732
|
return false;
|
|
811
1733
|
}
|
|
812
1734
|
}
|
|
1735
|
+
function validateEdgeWebhookEvent(value) {
|
|
1736
|
+
const errors = {};
|
|
1737
|
+
if (!isRecord(value)) {
|
|
1738
|
+
throw new EdgeValidationError6("EDGE webhook event must be an object", {
|
|
1739
|
+
event: ["Expected JSON object"]
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
const id = value.id;
|
|
1743
|
+
const type = value.type;
|
|
1744
|
+
const createdAt = value.created_at;
|
|
1745
|
+
const data = value.data;
|
|
1746
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
1747
|
+
errors.id = ["Required string"];
|
|
1748
|
+
}
|
|
1749
|
+
if (typeof type !== "string" || !EVENT_TYPES.has(type)) {
|
|
1750
|
+
errors.type = [`Must be one of: ${EDGE_WEBHOOK_EVENT_TYPES.join(", ")}`];
|
|
1751
|
+
}
|
|
1752
|
+
if (typeof createdAt !== "string" || !createdAt.trim() || Number.isNaN(Date.parse(createdAt))) {
|
|
1753
|
+
errors.created_at = ["Required ISO 8601 timestamp string"];
|
|
1754
|
+
}
|
|
1755
|
+
if (!isRecord(data)) {
|
|
1756
|
+
errors.data = ["Required object"];
|
|
1757
|
+
}
|
|
1758
|
+
if (Object.keys(errors).length === 0) {
|
|
1759
|
+
validateKnownEventData(type, data, errors);
|
|
1760
|
+
}
|
|
1761
|
+
if (Object.keys(errors).length > 0) {
|
|
1762
|
+
throw new EdgeValidationError6("Invalid EDGE webhook event", errors);
|
|
1763
|
+
}
|
|
1764
|
+
return value;
|
|
1765
|
+
}
|
|
1766
|
+
function validateKnownEventData(type, data, errors) {
|
|
1767
|
+
switch (type) {
|
|
1768
|
+
case "transfer.completed":
|
|
1769
|
+
validateTransferEventData(data, "completed", errors);
|
|
1770
|
+
return;
|
|
1771
|
+
case "transfer.failed":
|
|
1772
|
+
validateTransferEventData(data, "failed", errors);
|
|
1773
|
+
if (data.reason !== void 0 && typeof data.reason !== "string") {
|
|
1774
|
+
errors["data.reason"] = ["Must be a string when provided"];
|
|
1775
|
+
}
|
|
1776
|
+
return;
|
|
1777
|
+
case "transfer.expired":
|
|
1778
|
+
validateTransferEventData(data, "expired", errors);
|
|
1779
|
+
return;
|
|
1780
|
+
case "transfer.processing":
|
|
1781
|
+
validateTransferEventData(data, "processing", errors);
|
|
1782
|
+
return;
|
|
1783
|
+
case "consent.revoked":
|
|
1784
|
+
validateRequiredString(data, "userId", errors);
|
|
1785
|
+
validateRequiredString(data, "clientId", errors);
|
|
1786
|
+
validateRequiredIsoTimestamp(data, "revokedAt", errors);
|
|
1787
|
+
return;
|
|
1788
|
+
default: {
|
|
1789
|
+
const exhaustive = type;
|
|
1790
|
+
throw new EdgeValidationError6("Unsupported EDGE webhook event type", {
|
|
1791
|
+
type: [`Unsupported event type: ${exhaustive}`]
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function validateTransferEventData(data, status, errors) {
|
|
1797
|
+
validateRequiredString(data, "transferId", errors);
|
|
1798
|
+
if (data.status !== status) {
|
|
1799
|
+
errors["data.status"] = [`Must be "${status}" for this event type`];
|
|
1800
|
+
}
|
|
1801
|
+
if (typeof data.type !== "string" || !TRANSFER_TYPES.has(data.type)) {
|
|
1802
|
+
errors["data.type"] = ['Must be "debit" or "credit"'];
|
|
1803
|
+
}
|
|
1804
|
+
validateRequiredString(data, "amount", errors);
|
|
1805
|
+
}
|
|
1806
|
+
function validateRequiredString(data, field, errors) {
|
|
1807
|
+
if (typeof data[field] !== "string" || !data[field].trim()) {
|
|
1808
|
+
errors[`data.${field}`] = ["Required string"];
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
function validateRequiredIsoTimestamp(data, field, errors) {
|
|
1812
|
+
if (typeof data[field] !== "string" || !data[field].trim() || Number.isNaN(Date.parse(data[field]))) {
|
|
1813
|
+
errors[`data.${field}`] = ["Required ISO 8601 timestamp string"];
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
function normalizeRawBody(rawBody) {
|
|
1817
|
+
if (typeof rawBody === "string") {
|
|
1818
|
+
return rawBody;
|
|
1819
|
+
}
|
|
1820
|
+
if (Buffer.isBuffer(rawBody) || rawBody instanceof Uint8Array) {
|
|
1821
|
+
return Buffer.from(rawBody).toString("utf8");
|
|
1822
|
+
}
|
|
1823
|
+
throw new EdgeValidationError6("EDGE webhook raw body is required", {
|
|
1824
|
+
rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
function normalizeSignatureHeader(header) {
|
|
1828
|
+
if (Array.isArray(header)) {
|
|
1829
|
+
return header.map((value) => value.trim()).filter(Boolean).join(",");
|
|
1830
|
+
}
|
|
1831
|
+
if (typeof header !== "string") return void 0;
|
|
1832
|
+
const trimmed = header.trim();
|
|
1833
|
+
return trimmed || void 0;
|
|
1834
|
+
}
|
|
1835
|
+
function isRecord(value) {
|
|
1836
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// src/http-helpers.ts
|
|
1840
|
+
function getEdgeWebhookRawBody(request) {
|
|
1841
|
+
if (typeof request.rawBody === "string" || Buffer.isBuffer(request.rawBody) || request.rawBody instanceof Uint8Array) {
|
|
1842
|
+
return request.rawBody;
|
|
1843
|
+
}
|
|
1844
|
+
throw new EdgeValidationError7("EDGE webhook raw body is required", {
|
|
1845
|
+
rawBody: [
|
|
1846
|
+
"Configure your framework to expose the raw UTF-8 request body before JSON parsing. Parsed JSON bodies cannot be used for HMAC verification."
|
|
1847
|
+
]
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
function getEdgeWebhookSignatureHeader(request, headerName = "x-edge-signature") {
|
|
1851
|
+
const headers = request.headers ?? {};
|
|
1852
|
+
const lowerHeaderName = headerName.toLowerCase();
|
|
1853
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1854
|
+
if (name.toLowerCase() === lowerHeaderName) {
|
|
1855
|
+
if (typeof value === "string" || Array.isArray(value)) return value;
|
|
1856
|
+
if (value === void 0 || value === null) return void 0;
|
|
1857
|
+
return String(value);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
return void 0;
|
|
1861
|
+
}
|
|
1862
|
+
function parseWebhookHttpRequest(options) {
|
|
1863
|
+
return parseAndVerifyWebhook({
|
|
1864
|
+
...options,
|
|
1865
|
+
rawBody: getEdgeWebhookRawBody(options.request),
|
|
1866
|
+
signatureHeader: getEdgeWebhookSignatureHeader(options.request, options.signatureHeaderName)
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// src/identity.ts
|
|
1871
|
+
import { EdgeValidationError as EdgeValidationError8 } from "@edge-markets/connect";
|
|
1872
|
+
var DEFAULT_IDENTITY_POLICY = {
|
|
1873
|
+
minNameScore: 70,
|
|
1874
|
+
minAddressScore: 65,
|
|
1875
|
+
requireZipMatch: true,
|
|
1876
|
+
requireApiVerified: true
|
|
1877
|
+
};
|
|
1878
|
+
function buildVerifyIdentityPayload(profile) {
|
|
1879
|
+
const firstName = firstNonEmpty(profile.firstName, profile.givenName);
|
|
1880
|
+
const lastName = firstNonEmpty(profile.lastName, profile.familyName);
|
|
1881
|
+
const address = profile.address ?? {};
|
|
1882
|
+
const payload = {
|
|
1883
|
+
firstName,
|
|
1884
|
+
lastName,
|
|
1885
|
+
address: {
|
|
1886
|
+
street: firstNonEmpty(address.street, address.line1, address.address1),
|
|
1887
|
+
city: firstNonEmpty(address.city),
|
|
1888
|
+
state: firstNonEmpty(address.state),
|
|
1889
|
+
zip: firstNonEmpty(address.zip, address.postalCode)
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
const errors = {};
|
|
1893
|
+
if (!payload.firstName) errors.firstName = ["Required"];
|
|
1894
|
+
if (!payload.lastName) errors.lastName = ["Required"];
|
|
1895
|
+
if (!Object.values(payload.address).some((value) => !!value)) {
|
|
1896
|
+
errors.address = ["At least one address field is required"];
|
|
1897
|
+
}
|
|
1898
|
+
if (Object.keys(errors).length > 0) {
|
|
1899
|
+
throw new EdgeValidationError8("Partner identity profile is incomplete", errors);
|
|
1900
|
+
}
|
|
1901
|
+
return payload;
|
|
1902
|
+
}
|
|
1903
|
+
function evaluateIdentityResult(result, policy = {}) {
|
|
1904
|
+
const resolved = { ...DEFAULT_IDENTITY_POLICY, ...policy };
|
|
1905
|
+
validatePolicy(resolved);
|
|
1906
|
+
const reasons = [];
|
|
1907
|
+
const verifiedFlag = result.verified;
|
|
1908
|
+
if (resolved.requireApiVerified && verifiedFlag !== true) {
|
|
1909
|
+
reasons.push("api_not_verified");
|
|
1910
|
+
}
|
|
1911
|
+
const scores = result.scores;
|
|
1912
|
+
if (!scores) {
|
|
1913
|
+
reasons.push("scores_missing");
|
|
1914
|
+
} else {
|
|
1915
|
+
if (typeof scores.name === "number" && scores.name < resolved.minNameScore) {
|
|
1916
|
+
reasons.push("name_score_below_threshold");
|
|
1917
|
+
}
|
|
1918
|
+
if (typeof scores.address === "number" && scores.address < resolved.minAddressScore) {
|
|
1919
|
+
reasons.push("address_score_below_threshold");
|
|
1920
|
+
}
|
|
1921
|
+
if (resolved.requireZipMatch && scores.zipMatch !== true) {
|
|
1922
|
+
reasons.push("zip_mismatch");
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
return {
|
|
1926
|
+
verified: reasons.length === 0,
|
|
1927
|
+
reasons,
|
|
1928
|
+
...scores ? { scores } : {}
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
function validatePolicy(policy) {
|
|
1932
|
+
const errors = {};
|
|
1933
|
+
if (!isScore(policy.minNameScore)) errors.minNameScore = ["Must be between 0 and 100"];
|
|
1934
|
+
if (!isScore(policy.minAddressScore)) errors.minAddressScore = ["Must be between 0 and 100"];
|
|
1935
|
+
if (Object.keys(errors).length > 0) {
|
|
1936
|
+
throw new EdgeValidationError8("Invalid identity policy", errors);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
function isScore(value) {
|
|
1940
|
+
return Number.isFinite(value) && value >= 0 && value <= 100;
|
|
1941
|
+
}
|
|
1942
|
+
function firstNonEmpty(...values) {
|
|
1943
|
+
for (const value of values) {
|
|
1944
|
+
if (typeof value === "string") {
|
|
1945
|
+
const trimmed = value.trim();
|
|
1946
|
+
if (trimmed) return trimmed;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
return "";
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// src/session-store.ts
|
|
1953
|
+
import { EdgeValidationError as EdgeValidationError9 } from "@edge-markets/connect";
|
|
1954
|
+
function createSessionTokenRecord(subjectId, tokens, tokenVault, options = {}) {
|
|
1955
|
+
const normalizedSubjectId = normalizeRequiredString(subjectId, "subjectId");
|
|
1956
|
+
validateTokens2(tokens);
|
|
1957
|
+
return {
|
|
1958
|
+
subjectId: normalizedSubjectId,
|
|
1959
|
+
encryptedTokens: tokenVault.encryptTokens(tokens),
|
|
1960
|
+
expiresAt: normalizeSessionExpiresAt(tokens.expiresAt),
|
|
1961
|
+
scopes: options.scopes ?? splitScopes(tokens.scope),
|
|
1962
|
+
...options.edgeUserId ? { edgeUserId: options.edgeUserId } : {},
|
|
1963
|
+
...options.keyId ? { keyId: options.keyId } : {},
|
|
1964
|
+
...options.metadata ? { metadata: options.metadata } : {}
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
function parseSessionTokenRecord(record) {
|
|
1968
|
+
if (!isRecord2(record)) {
|
|
1969
|
+
throw new EdgeValidationError9("EDGE session token record must be an object", {
|
|
1970
|
+
record: ["Expected object"]
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
const subjectId = normalizeRequiredString(record.subjectId, "subjectId");
|
|
1974
|
+
const encryptedTokens = normalizeRequiredString(record.encryptedTokens, "encryptedTokens");
|
|
1975
|
+
const expiresAt = normalizeSessionExpiresAt(record.expiresAt);
|
|
1976
|
+
const scopes = record.scopes === void 0 ? void 0 : normalizeScopes(record.scopes);
|
|
1977
|
+
return {
|
|
1978
|
+
subjectId,
|
|
1979
|
+
encryptedTokens,
|
|
1980
|
+
expiresAt,
|
|
1981
|
+
...scopes ? { scopes } : {},
|
|
1982
|
+
...typeof record.edgeUserId === "string" && record.edgeUserId.trim() ? { edgeUserId: record.edgeUserId.trim() } : {},
|
|
1983
|
+
...typeof record.keyId === "string" && record.keyId.trim() ? { keyId: record.keyId.trim() } : {},
|
|
1984
|
+
...isRecord2(record.metadata) ? { metadata: record.metadata } : {}
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
function encryptLegacySessionTokens(legacyRecord, tokenVault, options) {
|
|
1988
|
+
const subjectId = normalizeRequiredString(options.subjectId || legacyRecord.subjectId, "subjectId");
|
|
1989
|
+
const accessToken = normalizeRequiredString(legacyRecord.accessToken, "accessToken");
|
|
1990
|
+
const refreshToken = normalizeRequiredString(legacyRecord.refreshToken, "refreshToken");
|
|
1991
|
+
if (!options.allowPlaintext && !tokenVault.isEncrypted(accessToken) && !tokenVault.isEncrypted(refreshToken)) {
|
|
1992
|
+
throw new EdgeValidationError9("Plaintext token migration requires allowPlaintext: true", {
|
|
1993
|
+
allowPlaintext: ["Set allowPlaintext only inside an explicit migration path"]
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
const scope = normalizeScopeString(legacyRecord.scope ?? legacyRecord.scopes);
|
|
1997
|
+
const tokens = {
|
|
1998
|
+
accessToken,
|
|
1999
|
+
refreshToken,
|
|
2000
|
+
...typeof legacyRecord.idToken === "string" && legacyRecord.idToken.trim() ? { idToken: legacyRecord.idToken.trim() } : {},
|
|
2001
|
+
expiresIn: normalizeExpiresIn(legacyRecord.expiresIn),
|
|
2002
|
+
expiresAt: normalizeSessionExpiresAt(legacyRecord.expiresAt),
|
|
2003
|
+
scope
|
|
2004
|
+
};
|
|
2005
|
+
return createSessionTokenRecord(subjectId, tokens, tokenVault, {
|
|
2006
|
+
scopes: splitScopes(scope),
|
|
2007
|
+
...legacyRecord.edgeUserId ? { edgeUserId: legacyRecord.edgeUserId } : {},
|
|
2008
|
+
...options.keyId ? { keyId: options.keyId } : {},
|
|
2009
|
+
...legacyRecord.metadata ? { metadata: legacyRecord.metadata } : {}
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
function normalizeSessionExpiresAt(value) {
|
|
2013
|
+
if (value instanceof Date) {
|
|
2014
|
+
const time = value.getTime();
|
|
2015
|
+
if (!Number.isFinite(time) || time <= 0) {
|
|
2016
|
+
throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
|
|
2017
|
+
expiresAt: ["Date must be valid"]
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
return time;
|
|
2021
|
+
}
|
|
2022
|
+
if (typeof value === "string") {
|
|
2023
|
+
const trimmed = value.trim();
|
|
2024
|
+
if (!trimmed) {
|
|
2025
|
+
throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
|
|
2026
|
+
expiresAt: ["Required"]
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
if (/^\d+$/.test(trimmed)) {
|
|
2030
|
+
return normalizeSessionExpiresAt(Number(trimmed));
|
|
2031
|
+
}
|
|
2032
|
+
const time = Date.parse(trimmed);
|
|
2033
|
+
if (!Number.isNaN(time) && time > 0) return time;
|
|
2034
|
+
}
|
|
2035
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2036
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2037
|
+
throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
|
|
2038
|
+
expiresAt: ["Must be a positive integer timestamp in milliseconds"]
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
if (value < 1e10) {
|
|
2042
|
+
throw new EdgeValidationError9("Ambiguous EDGE session expiresAt", {
|
|
2043
|
+
expiresAt: ["Use Unix milliseconds, not seconds"]
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
return value;
|
|
2047
|
+
}
|
|
2048
|
+
throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
|
|
2049
|
+
expiresAt: ["Use Unix milliseconds, ISO string, or Date"]
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
function normalizeExpiresIn(value) {
|
|
2053
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
2054
|
+
if (typeof value === "string" && value.trim()) {
|
|
2055
|
+
const parsed = Number(value);
|
|
2056
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
2057
|
+
}
|
|
2058
|
+
return 600;
|
|
2059
|
+
}
|
|
2060
|
+
function normalizeScopeString(value) {
|
|
2061
|
+
if (Array.isArray(value)) return normalizeScopes(value).join(" ");
|
|
2062
|
+
if (typeof value === "string") return splitScopes(value).join(" ");
|
|
2063
|
+
return "";
|
|
2064
|
+
}
|
|
2065
|
+
function normalizeScopes(value) {
|
|
2066
|
+
if (Array.isArray(value)) {
|
|
2067
|
+
return value.map((scope) => typeof scope === "string" ? scope.trim() : "").filter(Boolean);
|
|
2068
|
+
}
|
|
2069
|
+
if (typeof value === "string") {
|
|
2070
|
+
return splitScopes(value);
|
|
2071
|
+
}
|
|
2072
|
+
throw new EdgeValidationError9("Invalid EDGE session scopes", {
|
|
2073
|
+
scopes: ["Expected array or space-delimited string"]
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
function splitScopes(value) {
|
|
2077
|
+
return value.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
|
|
2078
|
+
}
|
|
2079
|
+
function normalizeRequiredString(value, field) {
|
|
2080
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2081
|
+
throw new EdgeValidationError9("Invalid EDGE session token record", {
|
|
2082
|
+
[field]: ["Required string"]
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
return value.trim();
|
|
2086
|
+
}
|
|
2087
|
+
function validateTokens2(tokens) {
|
|
2088
|
+
normalizeRequiredString(tokens.accessToken, "accessToken");
|
|
2089
|
+
normalizeRequiredString(tokens.refreshToken, "refreshToken");
|
|
2090
|
+
normalizeSessionExpiresAt(tokens.expiresAt);
|
|
2091
|
+
normalizeExpiresIn(tokens.expiresIn);
|
|
2092
|
+
normalizeScopeString(tokens.scope);
|
|
2093
|
+
}
|
|
2094
|
+
function isRecord2(value) {
|
|
2095
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// src/token-vault.ts
|
|
2099
|
+
import { EdgeValidationError as EdgeValidationError10 } from "@edge-markets/connect";
|
|
2100
|
+
import crypto3 from "crypto";
|
|
2101
|
+
var TOKEN_VAULT_VERSION = "edge_vault_v1";
|
|
2102
|
+
var IV_BYTES = 12;
|
|
2103
|
+
var TAG_BYTES = 16;
|
|
2104
|
+
var KEY_BYTES = 32;
|
|
2105
|
+
var EdgeTokenVault = class {
|
|
2106
|
+
constructor(options) {
|
|
2107
|
+
this.currentKey = normalizeVaultKey(options.currentKey, "currentKey");
|
|
2108
|
+
this.keysById = /* @__PURE__ */ new Map([[this.currentKey.id, this.currentKey.key]]);
|
|
2109
|
+
for (const [index, key] of (options.previousKeys ?? []).entries()) {
|
|
2110
|
+
const normalized = normalizeVaultKey(key, `previousKeys[${index}]`);
|
|
2111
|
+
if (this.keysById.has(normalized.id)) {
|
|
2112
|
+
throw new EdgeValidationError10("Duplicate token vault key ID", {
|
|
2113
|
+
keyId: [`Duplicate key ID "${normalized.id}"`]
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
this.keysById.set(normalized.id, normalized.key);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
encryptTokens(tokens) {
|
|
2120
|
+
validateEdgeTokens(tokens);
|
|
2121
|
+
const iv = crypto3.randomBytes(IV_BYTES);
|
|
2122
|
+
const aad = Buffer.from(`${TOKEN_VAULT_VERSION}.${this.currentKey.id}`, "utf8");
|
|
2123
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", this.currentKey.key, iv);
|
|
2124
|
+
cipher.setAAD(aad);
|
|
2125
|
+
const plaintext = Buffer.from(JSON.stringify(tokens), "utf8");
|
|
2126
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
2127
|
+
const tag = cipher.getAuthTag();
|
|
2128
|
+
return [
|
|
2129
|
+
TOKEN_VAULT_VERSION,
|
|
2130
|
+
toBase64Url2(Buffer.from(this.currentKey.id, "utf8")),
|
|
2131
|
+
toBase64Url2(iv),
|
|
2132
|
+
toBase64Url2(tag),
|
|
2133
|
+
toBase64Url2(ciphertext)
|
|
2134
|
+
].join(".");
|
|
2135
|
+
}
|
|
2136
|
+
decryptTokens(envelope) {
|
|
2137
|
+
const parsed = parseEnvelope(envelope);
|
|
2138
|
+
const key = this.keysById.get(parsed.keyId);
|
|
2139
|
+
if (!key) {
|
|
2140
|
+
throw new EdgeValidationError10("Unknown token vault key ID", {
|
|
2141
|
+
keyId: [`No key configured for "${parsed.keyId}"`]
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
try {
|
|
2145
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", key, parsed.iv);
|
|
2146
|
+
decipher.setAAD(Buffer.from(`${TOKEN_VAULT_VERSION}.${parsed.keyId}`, "utf8"));
|
|
2147
|
+
decipher.setAuthTag(parsed.tag);
|
|
2148
|
+
const plaintext = Buffer.concat([decipher.update(parsed.ciphertext), decipher.final()]).toString("utf8");
|
|
2149
|
+
const tokens = JSON.parse(plaintext);
|
|
2150
|
+
validateEdgeTokens(tokens);
|
|
2151
|
+
return tokens;
|
|
2152
|
+
} catch (error) {
|
|
2153
|
+
if (error instanceof EdgeValidationError10) throw error;
|
|
2154
|
+
throw new EdgeValidationError10("Failed to decrypt EDGE tokens", {
|
|
2155
|
+
tokenEnvelope: ["Envelope is malformed, corrupted, or encrypted with a different key"]
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
isEncrypted(value) {
|
|
2160
|
+
return typeof value === "string" && value.startsWith(`${TOKEN_VAULT_VERSION}.`);
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
function createEdgeTokenVault(options) {
|
|
2164
|
+
return new EdgeTokenVault(options);
|
|
2165
|
+
}
|
|
2166
|
+
function isEdgeTokenVaultEnvelope(value) {
|
|
2167
|
+
return typeof value === "string" && value.startsWith(`${TOKEN_VAULT_VERSION}.`);
|
|
2168
|
+
}
|
|
2169
|
+
function normalizeVaultKey(input, fieldName) {
|
|
2170
|
+
const id = typeof input.id === "string" ? input.id.trim() : "";
|
|
2171
|
+
if (!id) {
|
|
2172
|
+
throw new EdgeValidationError10("Token vault key ID is required", {
|
|
2173
|
+
[`${fieldName}.id`]: ["Required"]
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
return {
|
|
2177
|
+
id,
|
|
2178
|
+
key: normalizeKeyMaterial(input.key, `${fieldName}.key`)
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
function normalizeKeyMaterial(input, fieldName) {
|
|
2182
|
+
const candidates = [];
|
|
2183
|
+
if (Buffer.isBuffer(input)) {
|
|
2184
|
+
candidates.push(input);
|
|
2185
|
+
} else if (input instanceof Uint8Array) {
|
|
2186
|
+
candidates.push(Buffer.from(input));
|
|
2187
|
+
} else if (typeof input === "string") {
|
|
2188
|
+
const value = input.trim();
|
|
2189
|
+
if (value.startsWith("base64:")) {
|
|
2190
|
+
candidates.push(Buffer.from(value.slice("base64:".length), "base64"));
|
|
2191
|
+
} else if (value.startsWith("base64url:")) {
|
|
2192
|
+
candidates.push(fromBase64Url2(value.slice("base64url:".length)));
|
|
2193
|
+
} else if (/^[0-9a-f]{64}$/i.test(value)) {
|
|
2194
|
+
candidates.push(Buffer.from(value, "hex"));
|
|
2195
|
+
} else {
|
|
2196
|
+
candidates.push(Buffer.from(value, "base64"));
|
|
2197
|
+
candidates.push(fromBase64Url2(value));
|
|
2198
|
+
candidates.push(Buffer.from(value, "utf8"));
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
const key = candidates.find((candidate) => candidate.length === KEY_BYTES);
|
|
2202
|
+
if (!key) {
|
|
2203
|
+
throw new EdgeValidationError10("Token vault key must decode to 32 bytes", {
|
|
2204
|
+
[fieldName]: ["Provide a 32-byte key as base64, base64url, hex, Buffer, or Uint8Array"]
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
return key;
|
|
2208
|
+
}
|
|
2209
|
+
function validateEdgeTokens(tokens) {
|
|
2210
|
+
const errors = {};
|
|
2211
|
+
if (!tokens || typeof tokens !== "object") {
|
|
2212
|
+
errors.tokens = ["Expected token object"];
|
|
2213
|
+
} else {
|
|
2214
|
+
if (typeof tokens.accessToken !== "string" || !tokens.accessToken) {
|
|
2215
|
+
errors.accessToken = ["Required"];
|
|
2216
|
+
}
|
|
2217
|
+
if (typeof tokens.refreshToken !== "string" || !tokens.refreshToken) {
|
|
2218
|
+
errors.refreshToken = ["Required"];
|
|
2219
|
+
}
|
|
2220
|
+
if (typeof tokens.expiresIn !== "number" || !Number.isFinite(tokens.expiresIn) || tokens.expiresIn <= 0) {
|
|
2221
|
+
errors.expiresIn = ["Must be a positive number"];
|
|
2222
|
+
}
|
|
2223
|
+
if (typeof tokens.expiresAt !== "number" || !Number.isFinite(tokens.expiresAt) || tokens.expiresAt <= 0) {
|
|
2224
|
+
errors.expiresAt = ["Must be a positive Unix timestamp in milliseconds"];
|
|
2225
|
+
}
|
|
2226
|
+
if (typeof tokens.scope !== "string") {
|
|
2227
|
+
errors.scope = ["Required"];
|
|
2228
|
+
}
|
|
2229
|
+
if (tokens.idToken !== void 0 && typeof tokens.idToken !== "string") {
|
|
2230
|
+
errors.idToken = ["Must be a string when provided"];
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
if (Object.keys(errors).length > 0) {
|
|
2234
|
+
throw new EdgeValidationError10("Invalid EDGE token payload", errors);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
function parseEnvelope(envelope) {
|
|
2238
|
+
if (typeof envelope !== "string") {
|
|
2239
|
+
throw new EdgeValidationError10("Token envelope must be a string", {
|
|
2240
|
+
tokenEnvelope: ["Expected string"]
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
const parts = envelope.split(".");
|
|
2244
|
+
if (parts.length !== 5 || parts[0] !== TOKEN_VAULT_VERSION) {
|
|
2245
|
+
throw new EdgeValidationError10("Invalid EDGE token vault envelope", {
|
|
2246
|
+
tokenEnvelope: [`Expected ${TOKEN_VAULT_VERSION}.<keyId>.<iv>.<tag>.<ciphertext>`]
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
const [, encodedKeyId, encodedIv, encodedTag, encodedCiphertext] = parts;
|
|
2250
|
+
const keyId = fromBase64Url2(encodedKeyId).toString("utf8");
|
|
2251
|
+
const iv = fromBase64Url2(encodedIv);
|
|
2252
|
+
const tag = fromBase64Url2(encodedTag);
|
|
2253
|
+
const ciphertext = fromBase64Url2(encodedCiphertext);
|
|
2254
|
+
const errors = {};
|
|
2255
|
+
if (!keyId) errors.keyId = ["Required"];
|
|
2256
|
+
if (iv.length !== IV_BYTES) errors.iv = [`Must be ${IV_BYTES} bytes`];
|
|
2257
|
+
if (tag.length !== TAG_BYTES) errors.tag = [`Must be ${TAG_BYTES} bytes`];
|
|
2258
|
+
if (ciphertext.length === 0) errors.ciphertext = ["Required"];
|
|
2259
|
+
if (Object.keys(errors).length > 0) {
|
|
2260
|
+
throw new EdgeValidationError10("Invalid EDGE token vault envelope", errors);
|
|
2261
|
+
}
|
|
2262
|
+
return { keyId, iv, tag, ciphertext };
|
|
2263
|
+
}
|
|
2264
|
+
function toBase64Url2(value) {
|
|
2265
|
+
return value.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
2266
|
+
}
|
|
2267
|
+
function fromBase64Url2(value) {
|
|
2268
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
2269
|
+
const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
|
|
2270
|
+
return Buffer.from(normalized + padding, "base64");
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// src/webhook-guards.ts
|
|
2274
|
+
import { EdgeValidationError as EdgeValidationError11 } from "@edge-markets/connect";
|
|
2275
|
+
function isTransferWebhookEvent(event) {
|
|
2276
|
+
return event.type.startsWith("transfer.");
|
|
2277
|
+
}
|
|
2278
|
+
function assertTransferEventMatchesIntent(event, intent) {
|
|
2279
|
+
if (!isTransferWebhookEvent(event)) {
|
|
2280
|
+
throw new EdgeValidationError11("Webhook event is not a transfer event", {
|
|
2281
|
+
type: [`Received ${event.type}`]
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
const data = event.data;
|
|
2285
|
+
const errors = {};
|
|
2286
|
+
if (data.transferId !== intent.transferId) {
|
|
2287
|
+
errors.transferId = [`Expected ${intent.transferId}, received ${data.transferId}`];
|
|
2288
|
+
}
|
|
2289
|
+
if (intent.type && data.type !== intent.type) {
|
|
2290
|
+
errors.type = [`Expected ${intent.type}, received ${data.type}`];
|
|
2291
|
+
}
|
|
2292
|
+
if (intent.status && data.status !== intent.status) {
|
|
2293
|
+
errors.status = [`Expected ${intent.status}, received ${data.status}`];
|
|
2294
|
+
}
|
|
2295
|
+
if (intent.amount !== void 0) {
|
|
2296
|
+
const expectedAmount = normalizeMoneyAmount(intent.amount);
|
|
2297
|
+
const receivedAmount = normalizeMoneyAmount(data.amount);
|
|
2298
|
+
if (receivedAmount !== expectedAmount) {
|
|
2299
|
+
errors.amount = [`Expected ${expectedAmount}, received ${receivedAmount}`];
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
if (Object.keys(errors).length > 0) {
|
|
2303
|
+
throw new EdgeValidationError11("Webhook transfer event does not match expected transfer intent", errors);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
var assertWebhookMatchesTransfer = assertTransferEventMatchesIntent;
|
|
2307
|
+
|
|
2308
|
+
// src/webhook-inbox.ts
|
|
2309
|
+
import { EdgeValidationError as EdgeValidationError12 } from "@edge-markets/connect";
|
|
2310
|
+
import crypto4 from "crypto";
|
|
2311
|
+
var DEFAULT_PROCESSING_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2312
|
+
var EdgeWebhookInbox = class {
|
|
2313
|
+
constructor(options) {
|
|
2314
|
+
if (!options.store?.insert || !options.store?.find || !options.store?.markProcessing) {
|
|
2315
|
+
throw new EdgeValidationError12("EdgeWebhookInbox store is incomplete", {
|
|
2316
|
+
store: ["Provide insert, find, markProcessing, markProcessed, and markFailed callbacks"]
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
if (!options.store.markProcessed || !options.store.markFailed) {
|
|
2320
|
+
throw new EdgeValidationError12("EdgeWebhookInbox store is incomplete", {
|
|
2321
|
+
store: ["Provide markProcessed and markFailed callbacks"]
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
if (!options.processEvent) {
|
|
2325
|
+
throw new EdgeValidationError12("EdgeWebhookInbox processEvent callback is required", {
|
|
2326
|
+
processEvent: ["Required"]
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
if (options.processingTimeoutMs !== void 0 && (!Number.isFinite(options.processingTimeoutMs) || options.processingTimeoutMs <= 0)) {
|
|
2330
|
+
throw new EdgeValidationError12("processingTimeoutMs must be positive", {
|
|
2331
|
+
processingTimeoutMs: ["Must be greater than 0"]
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
this.store = options.store;
|
|
2335
|
+
this.processEvent = options.processEvent;
|
|
2336
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2337
|
+
this.storeRawBody = options.storeRawBody ?? false;
|
|
2338
|
+
this.processingTimeoutMs = options.processingTimeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
|
|
2339
|
+
this.onEvent = options.onEvent;
|
|
2340
|
+
}
|
|
2341
|
+
async accept(parsed) {
|
|
2342
|
+
return this.acceptEvent(parsed.event, {
|
|
2343
|
+
source: "live_webhook",
|
|
2344
|
+
receivedAt: parsed.receivedAt,
|
|
2345
|
+
signatureTimestamp: parsed.signatureTimestamp,
|
|
2346
|
+
signatureHeader: void 0,
|
|
2347
|
+
rawBody: void 0
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
async acceptRaw(parsed, rawBody, signatureHeader) {
|
|
2351
|
+
return this.acceptEvent(parsed.event, {
|
|
2352
|
+
source: "live_webhook",
|
|
2353
|
+
receivedAt: parsed.receivedAt,
|
|
2354
|
+
signatureTimestamp: parsed.signatureTimestamp,
|
|
2355
|
+
signatureHeader,
|
|
2356
|
+
rawBody: this.storeRawBody ? rawBody : void 0
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
async acceptSyncedEvent(event, receivedAt = this.now()) {
|
|
2360
|
+
return this.acceptEvent(event, {
|
|
2361
|
+
source: "sync",
|
|
2362
|
+
receivedAt,
|
|
2363
|
+
signatureTimestamp: null,
|
|
2364
|
+
signatureHeader: null,
|
|
2365
|
+
rawBody: null
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
async process(eventId, options = {}) {
|
|
2369
|
+
const normalizedEventId = normalizeEventId(eventId);
|
|
2370
|
+
const existing = await this.store.find(normalizedEventId);
|
|
2371
|
+
if (!existing) {
|
|
2372
|
+
return { eventId: normalizedEventId, status: "failed", processed: false, skipped: true, reason: "not_found" };
|
|
2373
|
+
}
|
|
2374
|
+
if (existing.status === "processed" && !options.force) {
|
|
2375
|
+
return {
|
|
2376
|
+
eventId: normalizedEventId,
|
|
2377
|
+
status: existing.status,
|
|
2378
|
+
processed: false,
|
|
2379
|
+
skipped: true,
|
|
2380
|
+
reason: "already_processed",
|
|
2381
|
+
record: existing
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
const now = this.now();
|
|
2385
|
+
const processing = await this.store.markProcessing(normalizedEventId, { now });
|
|
2386
|
+
if (!processing) {
|
|
2387
|
+
return {
|
|
2388
|
+
eventId: normalizedEventId,
|
|
2389
|
+
status: existing.status,
|
|
2390
|
+
processed: false,
|
|
2391
|
+
skipped: true,
|
|
2392
|
+
reason: "locked",
|
|
2393
|
+
record: existing
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
await this.emit({
|
|
2397
|
+
type: "webhook.processing_started",
|
|
2398
|
+
eventId: normalizedEventId,
|
|
2399
|
+
eventType: processing.eventType,
|
|
2400
|
+
status: "processing",
|
|
2401
|
+
source: processing.source,
|
|
2402
|
+
attempts: processing.attempts
|
|
2403
|
+
});
|
|
2404
|
+
try {
|
|
2405
|
+
await this.processEvent(processing.event, processing);
|
|
2406
|
+
const processed = await this.store.markProcessed(normalizedEventId, { now: this.now() }) ?? processing;
|
|
2407
|
+
await this.emit({
|
|
2408
|
+
type: "webhook.processing_succeeded",
|
|
2409
|
+
eventId: normalizedEventId,
|
|
2410
|
+
eventType: processing.eventType,
|
|
2411
|
+
status: "processed",
|
|
2412
|
+
source: processing.source,
|
|
2413
|
+
attempts: processed.attempts
|
|
2414
|
+
});
|
|
2415
|
+
return {
|
|
2416
|
+
eventId: normalizedEventId,
|
|
2417
|
+
status: "processed",
|
|
2418
|
+
processed: true,
|
|
2419
|
+
skipped: false,
|
|
2420
|
+
record: processed
|
|
2421
|
+
};
|
|
2422
|
+
} catch (error) {
|
|
2423
|
+
const message = error instanceof Error ? error.message : "Unknown webhook processing error";
|
|
2424
|
+
const failed = await this.store.markFailed(normalizedEventId, { now: this.now(), error: message }) ?? processing;
|
|
2425
|
+
await this.emit({
|
|
2426
|
+
type: "webhook.processing_failed",
|
|
2427
|
+
eventId: normalizedEventId,
|
|
2428
|
+
eventType: processing.eventType,
|
|
2429
|
+
status: "failed",
|
|
2430
|
+
source: processing.source,
|
|
2431
|
+
attempts: failed.attempts,
|
|
2432
|
+
error: message
|
|
2433
|
+
});
|
|
2434
|
+
throw error;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
async replay(eventId) {
|
|
2438
|
+
return this.process(eventId, { force: true });
|
|
2439
|
+
}
|
|
2440
|
+
async recoverStaleProcessing() {
|
|
2441
|
+
if (!this.store.list) {
|
|
2442
|
+
return { recovered: 0, skipped: true, reason: "list_not_configured" };
|
|
2443
|
+
}
|
|
2444
|
+
if (!this.store.markPending) {
|
|
2445
|
+
return { recovered: 0, skipped: true, reason: "mark_pending_not_configured" };
|
|
2446
|
+
}
|
|
2447
|
+
const updatedBefore = new Date(this.now().getTime() - this.processingTimeoutMs);
|
|
2448
|
+
const records = await this.store.list({ status: "processing", updatedBefore, limit: 100 });
|
|
2449
|
+
let recovered = 0;
|
|
2450
|
+
for (const record of records) {
|
|
2451
|
+
const next = await this.store.markPending(record.eventId, {
|
|
2452
|
+
now: this.now(),
|
|
2453
|
+
reason: "stale_processing_recovery"
|
|
2454
|
+
});
|
|
2455
|
+
if (next) {
|
|
2456
|
+
recovered++;
|
|
2457
|
+
await this.emit({
|
|
2458
|
+
type: "webhook.recovered_stale",
|
|
2459
|
+
eventId: record.eventId,
|
|
2460
|
+
eventType: record.eventType,
|
|
2461
|
+
status: next.status,
|
|
2462
|
+
source: record.source,
|
|
2463
|
+
attempts: next.attempts
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return { recovered, skipped: false };
|
|
2468
|
+
}
|
|
2469
|
+
async acceptEvent(event, options) {
|
|
2470
|
+
const payloadFingerprint = fingerprintWebhookEvent(event);
|
|
2471
|
+
const result = await this.store.insert({
|
|
2472
|
+
eventId: event.id,
|
|
2473
|
+
eventType: event.type,
|
|
2474
|
+
event,
|
|
2475
|
+
payloadFingerprint,
|
|
2476
|
+
source: options.source,
|
|
2477
|
+
receivedAt: options.receivedAt,
|
|
2478
|
+
signatureTimestamp: options.signatureTimestamp ?? null,
|
|
2479
|
+
signatureHeader: options.signatureHeader ?? null,
|
|
2480
|
+
rawBody: options.rawBody ?? null
|
|
2481
|
+
});
|
|
2482
|
+
if (result.record.payloadFingerprint !== payloadFingerprint) {
|
|
2483
|
+
await this.emit({
|
|
2484
|
+
type: "webhook.payload_mismatch",
|
|
2485
|
+
eventId: event.id,
|
|
2486
|
+
eventType: event.type,
|
|
2487
|
+
status: result.record.status,
|
|
2488
|
+
source: options.source
|
|
2489
|
+
});
|
|
2490
|
+
throw new EdgeValidationError12("Duplicate webhook event payload does not match stored event", {
|
|
2491
|
+
eventId: [event.id]
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
await this.emit({
|
|
2495
|
+
type: result.inserted ? "webhook.accepted" : "webhook.duplicate",
|
|
2496
|
+
eventId: event.id,
|
|
2497
|
+
eventType: event.type,
|
|
2498
|
+
status: result.record.status,
|
|
2499
|
+
source: options.source,
|
|
2500
|
+
attempts: result.record.attempts
|
|
2501
|
+
});
|
|
2502
|
+
return {
|
|
2503
|
+
eventId: event.id,
|
|
2504
|
+
eventType: event.type,
|
|
2505
|
+
inserted: result.inserted,
|
|
2506
|
+
duplicate: !result.inserted,
|
|
2507
|
+
status: result.record.status,
|
|
2508
|
+
record: result.record
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
async emit(event) {
|
|
2512
|
+
try {
|
|
2513
|
+
await this.onEvent?.(event);
|
|
2514
|
+
} catch {
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
function createWebhookInbox(options) {
|
|
2519
|
+
return new EdgeWebhookInbox(options);
|
|
2520
|
+
}
|
|
2521
|
+
function fingerprintWebhookEvent(event) {
|
|
2522
|
+
return crypto4.createHash("sha256").update(stableStringify(event)).digest("hex");
|
|
2523
|
+
}
|
|
2524
|
+
function normalizeEventId(eventId) {
|
|
2525
|
+
if (typeof eventId !== "string" || !eventId.trim()) {
|
|
2526
|
+
throw new EdgeValidationError12("Webhook event ID is required", {
|
|
2527
|
+
eventId: ["Required"]
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
return eventId.trim();
|
|
2531
|
+
}
|
|
2532
|
+
function stableStringify(value) {
|
|
2533
|
+
if (Array.isArray(value)) {
|
|
2534
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
2535
|
+
}
|
|
2536
|
+
if (value && typeof value === "object") {
|
|
2537
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
2538
|
+
}
|
|
2539
|
+
return JSON.stringify(value);
|
|
2540
|
+
}
|
|
813
2541
|
|
|
814
2542
|
// src/index.ts
|
|
815
2543
|
import {
|
|
816
2544
|
EdgeApiError as EdgeApiError2,
|
|
817
|
-
EdgeAuthenticationError as
|
|
818
|
-
EdgeConsentRequiredError as
|
|
819
|
-
EdgeError as
|
|
820
|
-
EdgeIdentityVerificationError as
|
|
2545
|
+
EdgeAuthenticationError as EdgeAuthenticationError4,
|
|
2546
|
+
EdgeConsentRequiredError as EdgeConsentRequiredError3,
|
|
2547
|
+
EdgeError as EdgeError3,
|
|
2548
|
+
EdgeIdentityVerificationError as EdgeIdentityVerificationError3,
|
|
821
2549
|
EdgeInsufficientScopeError as EdgeInsufficientScopeError2,
|
|
822
|
-
EdgeNetworkError as
|
|
2550
|
+
EdgeNetworkError as EdgeNetworkError3,
|
|
823
2551
|
EdgeNotFoundError as EdgeNotFoundError2,
|
|
824
2552
|
EdgeTokenExchangeError as EdgeTokenExchangeError2,
|
|
2553
|
+
EdgeValidationError as EdgeValidationError13,
|
|
825
2554
|
isApiError,
|
|
826
2555
|
isAuthenticationError,
|
|
827
2556
|
isConsentRequiredError,
|
|
@@ -830,32 +2559,72 @@ import {
|
|
|
830
2559
|
isNetworkError
|
|
831
2560
|
} from "@edge-markets/connect";
|
|
832
2561
|
import {
|
|
833
|
-
EDGE_WEBHOOK_EVENT_TYPES,
|
|
2562
|
+
EDGE_WEBHOOK_EVENT_TYPES as EDGE_WEBHOOK_EVENT_TYPES2,
|
|
834
2563
|
TRANSFER_CATEGORIES,
|
|
835
|
-
getEnvironmentConfig as
|
|
2564
|
+
getEnvironmentConfig as getEnvironmentConfig3,
|
|
836
2565
|
isProductionEnvironment
|
|
837
2566
|
} from "@edge-markets/connect";
|
|
838
2567
|
export {
|
|
839
|
-
|
|
2568
|
+
CONNECT_TRANSFER_DIRECTIONS,
|
|
2569
|
+
EDGE_WEBHOOK_EVENT_TYPES2 as EDGE_WEBHOOK_EVENT_TYPES,
|
|
840
2570
|
EdgeApiError2 as EdgeApiError,
|
|
841
|
-
|
|
2571
|
+
EdgeAuthenticationError4 as EdgeAuthenticationError,
|
|
842
2572
|
EdgeConnectServer,
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
2573
|
+
EdgeConsentRequiredError3 as EdgeConsentRequiredError,
|
|
2574
|
+
EdgeError3 as EdgeError,
|
|
2575
|
+
EdgeIdentityVerificationError3 as EdgeIdentityVerificationError,
|
|
846
2576
|
EdgeInsufficientScopeError2 as EdgeInsufficientScopeError,
|
|
847
|
-
|
|
2577
|
+
EdgeNetworkError3 as EdgeNetworkError,
|
|
848
2578
|
EdgeNotFoundError2 as EdgeNotFoundError,
|
|
849
2579
|
EdgeTokenExchangeError2 as EdgeTokenExchangeError,
|
|
2580
|
+
EdgeTokenVault,
|
|
850
2581
|
EdgeUserClient,
|
|
2582
|
+
EdgeUserSession,
|
|
2583
|
+
EdgeValidationError13 as EdgeValidationError,
|
|
2584
|
+
EdgeWebhookInbox,
|
|
2585
|
+
EdgeWebhookReconciler,
|
|
851
2586
|
TRANSFER_CATEGORIES,
|
|
852
|
-
|
|
2587
|
+
assertSameTransferIntent,
|
|
2588
|
+
assertTransferEventMatchesIntent,
|
|
2589
|
+
assertWebhookMatchesTransfer,
|
|
2590
|
+
buildVerifyIdentityPayload,
|
|
2591
|
+
createEdgeConnectServerFromEnv,
|
|
2592
|
+
createEdgeTokenVault,
|
|
2593
|
+
createEdgeUserSession,
|
|
2594
|
+
createSessionTokenRecord,
|
|
2595
|
+
createTransferIdempotencyKey,
|
|
2596
|
+
createWebhookInbox,
|
|
2597
|
+
createWebhookReconciler,
|
|
2598
|
+
encryptLegacySessionTokens,
|
|
2599
|
+
evaluateIdentityResult,
|
|
2600
|
+
extractWebhookSignatureTimestamp,
|
|
2601
|
+
fingerprintTransferIntent,
|
|
2602
|
+
fingerprintWebhookEvent,
|
|
2603
|
+
getConnectTransferDirection,
|
|
2604
|
+
getEdgeWebhookRawBody,
|
|
2605
|
+
getEdgeWebhookSignatureHeader,
|
|
2606
|
+
getEnvironmentConfig3 as getEnvironmentConfig,
|
|
853
2607
|
isApiError,
|
|
854
2608
|
isAuthenticationError,
|
|
855
2609
|
isConsentRequiredError,
|
|
856
2610
|
isEdgeError,
|
|
2611
|
+
isEdgeTokenVaultEnvelope,
|
|
2612
|
+
isEdgeWebhookEvent,
|
|
857
2613
|
isIdentityVerificationError,
|
|
858
2614
|
isNetworkError,
|
|
859
2615
|
isProductionEnvironment,
|
|
2616
|
+
isTransferWebhookEvent,
|
|
2617
|
+
mapTransferStatusToPartnerState,
|
|
2618
|
+
moneyAmountsEqual,
|
|
2619
|
+
normalizeMoneyAmount,
|
|
2620
|
+
normalizeSessionExpiresAt,
|
|
2621
|
+
parseAndVerifyWebhook,
|
|
2622
|
+
parseSessionTokenRecord,
|
|
2623
|
+
parseWebhookHttpRequest,
|
|
2624
|
+
startTransferVerification,
|
|
2625
|
+
toEdgeHttpError,
|
|
2626
|
+
toEdgeProblemJson,
|
|
2627
|
+
validateEdgeWebhookEvent,
|
|
2628
|
+
validateTransferAmount,
|
|
860
2629
|
verifyWebhookSignature
|
|
861
2630
|
};
|