@edge-markets/connect-node 1.8.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 +146 -24
- package/dist/index.d.mts +301 -90
- package/dist/index.d.ts +301 -90
- package/dist/index.js +786 -223
- package/dist/index.mjs +754 -207
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -26,16 +26,28 @@ import {
|
|
|
26
26
|
import crypto from "crypto";
|
|
27
27
|
var DEFAULT_MIN_AMOUNT = "1.00";
|
|
28
28
|
var DEFAULT_MAX_AMOUNT = "10000.00";
|
|
29
|
-
|
|
30
|
-
|
|
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);
|
|
31
43
|
const dollars = cents / 100n;
|
|
32
44
|
const remainder = cents % 100n;
|
|
33
45
|
return `${dollars.toString()}.${remainder.toString().padStart(2, "0")}`;
|
|
34
46
|
}
|
|
35
|
-
function validateTransferAmount(value, limits = {}) {
|
|
36
|
-
const amount = parseMoneyToCents(value, "amount");
|
|
37
|
-
const min = parseMoneyToCents(limits.min ?? DEFAULT_MIN_AMOUNT, "min");
|
|
38
|
-
const max = parseMoneyToCents(limits.max ?? DEFAULT_MAX_AMOUNT, "max");
|
|
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 });
|
|
39
51
|
const errors = {};
|
|
40
52
|
if (min <= 0n) errors.min = ["Minimum amount must be greater than 0"];
|
|
41
53
|
if (max < min) errors.max = ["Maximum amount must be greater than or equal to minimum amount"];
|
|
@@ -83,6 +95,27 @@ function assertSameTransferIntent(existing, requested) {
|
|
|
83
95
|
throw new EdgeValidationError("Transfer intent does not match existing idempotent transfer", errors);
|
|
84
96
|
}
|
|
85
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
|
+
}
|
|
86
119
|
async function startTransferVerification(client, options) {
|
|
87
120
|
const origin = typeof options.origin === "string" ? options.origin.trim() : "";
|
|
88
121
|
if (!origin) {
|
|
@@ -103,9 +136,14 @@ async function startTransferVerification(client, options) {
|
|
|
103
136
|
const verificationSession = await client.createVerificationSession(transferId, { origin });
|
|
104
137
|
return { transfer, verificationSession };
|
|
105
138
|
}
|
|
106
|
-
function parseMoneyToCents(value, field) {
|
|
139
|
+
function parseMoneyToCents(value, field, options = {}) {
|
|
107
140
|
let input;
|
|
108
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
|
+
}
|
|
109
147
|
if (!Number.isFinite(value)) {
|
|
110
148
|
throw new EdgeValidationError("Money amount must be finite", { [field]: ["Must be a finite number"] });
|
|
111
149
|
}
|
|
@@ -417,6 +455,7 @@ function validateMtlsConfig(config) {
|
|
|
417
455
|
import { EdgeAuthenticationError, EdgeValidationError as EdgeValidationError3 } from "@edge-markets/connect";
|
|
418
456
|
var EdgeUserSession = class {
|
|
419
457
|
constructor(options) {
|
|
458
|
+
this.refreshPromise = null;
|
|
420
459
|
if (!options.edge?.forUser || !options.edge?.refreshTokens) {
|
|
421
460
|
throw new EdgeValidationError3("EdgeUserSession requires an EdgeConnectServer instance", {
|
|
422
461
|
edge: ["Provide edge.forUser and edge.refreshTokens"]
|
|
@@ -445,29 +484,44 @@ var EdgeUserSession = class {
|
|
|
445
484
|
this.refreshSkewMs = options.refreshSkewMs ?? 6e4;
|
|
446
485
|
this.now = options.now ?? Date.now;
|
|
447
486
|
this.onRefresh = options.onRefresh;
|
|
487
|
+
this.onEvent = options.onEvent;
|
|
488
|
+
this.singleFlightRefresh = options.singleFlightRefresh ?? true;
|
|
448
489
|
}
|
|
449
490
|
async getValidTokens(options = {}) {
|
|
450
491
|
const tokens = await this.loadTokens();
|
|
451
492
|
if (!options.forceRefresh && tokens.expiresAt > this.now() + this.refreshSkewMs) {
|
|
452
493
|
return tokens;
|
|
453
494
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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);
|
|
458
504
|
}
|
|
459
505
|
async getClient(options = {}) {
|
|
460
506
|
const tokens = await this.getValidTokens(options);
|
|
461
507
|
return this.edge.forUser(tokens.accessToken);
|
|
462
508
|
}
|
|
463
|
-
async withClient(callback) {
|
|
464
|
-
const tokens = await this.getValidTokens();
|
|
509
|
+
async withClient(callback, options = {}) {
|
|
510
|
+
const tokens = await this.getValidTokens(options);
|
|
465
511
|
return callback(this.edge.forUser(tokens.accessToken), tokens);
|
|
466
512
|
}
|
|
467
513
|
async saveTokens(tokens, refreshed = false) {
|
|
468
514
|
validateTokens(tokens);
|
|
469
515
|
const value = this.tokenVault ? this.tokenVault.encryptTokens(tokens) : tokens;
|
|
470
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
|
+
});
|
|
471
525
|
}
|
|
472
526
|
async loadTokens() {
|
|
473
527
|
const value = await this.tokenStore.load(this.subjectId);
|
|
@@ -484,11 +538,60 @@ var EdgeUserSession = class {
|
|
|
484
538
|
}
|
|
485
539
|
const tokens = this.tokenVault.decryptTokens(value);
|
|
486
540
|
validateTokens(tokens);
|
|
541
|
+
await this.emitLoaded(tokens, true);
|
|
487
542
|
return tokens;
|
|
488
543
|
}
|
|
489
544
|
validateTokens(value);
|
|
545
|
+
await this.emitLoaded(value, false);
|
|
490
546
|
return value;
|
|
491
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
|
+
}
|
|
492
595
|
};
|
|
493
596
|
function createEdgeUserSession(options) {
|
|
494
597
|
return new EdgeUserSession(options);
|
|
@@ -540,8 +643,8 @@ var EdgeWebhookReconciler = class {
|
|
|
540
643
|
if (!options.cursorStore?.load || !options.cursorStore?.save) {
|
|
541
644
|
throw new Error("EdgeWebhookReconciler: cursorStore.load and cursorStore.save are required");
|
|
542
645
|
}
|
|
543
|
-
if (!options.processEvent) {
|
|
544
|
-
throw new Error("EdgeWebhookReconciler: processEvent is required");
|
|
646
|
+
if (!options.processEvent && !options.inbox) {
|
|
647
|
+
throw new Error("EdgeWebhookReconciler: processEvent or inbox is required");
|
|
545
648
|
}
|
|
546
649
|
if (options.maxPages !== void 0 && (!Number.isInteger(options.maxPages) || options.maxPages <= 0)) {
|
|
547
650
|
throw new Error("EdgeWebhookReconciler: maxPages must be a positive integer");
|
|
@@ -568,6 +671,8 @@ var EdgeWebhookReconciler = class {
|
|
|
568
671
|
async runInternal() {
|
|
569
672
|
let cursor = normalizeCursor(await this.options.cursorStore.load());
|
|
570
673
|
let processed = 0;
|
|
674
|
+
let accepted = 0;
|
|
675
|
+
let duplicates = 0;
|
|
571
676
|
let pages = 0;
|
|
572
677
|
let hasMore = false;
|
|
573
678
|
const maxPages = this.options.maxPages ?? DEFAULT_MAX_PAGES;
|
|
@@ -581,6 +686,7 @@ var EdgeWebhookReconciler = class {
|
|
|
581
686
|
if (result.events.length === 0) {
|
|
582
687
|
return {
|
|
583
688
|
processed,
|
|
689
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
584
690
|
pages,
|
|
585
691
|
...cursor ? { cursor } : {},
|
|
586
692
|
hasMore,
|
|
@@ -589,14 +695,23 @@ var EdgeWebhookReconciler = class {
|
|
|
589
695
|
};
|
|
590
696
|
}
|
|
591
697
|
for (const event of result.events) {
|
|
592
|
-
|
|
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
|
+
}
|
|
593
708
|
cursor = event.id;
|
|
594
709
|
await this.options.cursorStore.save(cursor);
|
|
595
|
-
processed++;
|
|
596
710
|
}
|
|
597
711
|
if (!result.hasMore) {
|
|
598
712
|
return {
|
|
599
713
|
processed,
|
|
714
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
600
715
|
pages,
|
|
601
716
|
...cursor ? { cursor } : {},
|
|
602
717
|
hasMore: false,
|
|
@@ -606,6 +721,7 @@ var EdgeWebhookReconciler = class {
|
|
|
606
721
|
}
|
|
607
722
|
return {
|
|
608
723
|
processed,
|
|
724
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
609
725
|
pages,
|
|
610
726
|
...cursor ? { cursor } : {},
|
|
611
727
|
hasMore,
|
|
@@ -1530,8 +1646,229 @@ function edgeTitle(error) {
|
|
|
1530
1646
|
return error.code.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1531
1647
|
}
|
|
1532
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
|
+
|
|
1659
|
+
// src/webhook-signing.ts
|
|
1660
|
+
import crypto2 from "crypto";
|
|
1661
|
+
function verifyWebhookSignature(header, body, secret, options = {}) {
|
|
1662
|
+
if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
|
|
1663
|
+
return false;
|
|
1664
|
+
}
|
|
1665
|
+
const requestedTolerance = options.toleranceSeconds;
|
|
1666
|
+
const toleranceSeconds = typeof requestedTolerance === "number" && Number.isFinite(requestedTolerance) && requestedTolerance >= 0 ? requestedTolerance : 300;
|
|
1667
|
+
const parts = header.split(",").map((p) => p.trim());
|
|
1668
|
+
const tPart = parts.find((p) => p.startsWith("t="));
|
|
1669
|
+
const v1Part = parts.find((p) => p.startsWith("v1="));
|
|
1670
|
+
if (!tPart || !v1Part) return false;
|
|
1671
|
+
const timestamp = parseInt(tPart.slice(2), 10);
|
|
1672
|
+
const signature = v1Part.slice(3);
|
|
1673
|
+
if (isNaN(timestamp) || !signature) return false;
|
|
1674
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1675
|
+
if (Math.abs(now - timestamp) > toleranceSeconds) return false;
|
|
1676
|
+
const signedContent = `${timestamp}.${body}`;
|
|
1677
|
+
const expectedHmac = crypto2.createHmac("sha256", secret).update(signedContent).digest("hex");
|
|
1678
|
+
try {
|
|
1679
|
+
const sigBuf = Buffer.from(signature, "hex");
|
|
1680
|
+
const expBuf = Buffer.from(expectedHmac, "hex");
|
|
1681
|
+
if (sigBuf.length !== expBuf.length) return false;
|
|
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;
|
|
1731
|
+
} catch {
|
|
1732
|
+
return false;
|
|
1733
|
+
}
|
|
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
|
+
|
|
1533
1870
|
// src/identity.ts
|
|
1534
|
-
import { EdgeValidationError as
|
|
1871
|
+
import { EdgeValidationError as EdgeValidationError8 } from "@edge-markets/connect";
|
|
1535
1872
|
var DEFAULT_IDENTITY_POLICY = {
|
|
1536
1873
|
minNameScore: 70,
|
|
1537
1874
|
minAddressScore: 65,
|
|
@@ -1559,7 +1896,7 @@ function buildVerifyIdentityPayload(profile) {
|
|
|
1559
1896
|
errors.address = ["At least one address field is required"];
|
|
1560
1897
|
}
|
|
1561
1898
|
if (Object.keys(errors).length > 0) {
|
|
1562
|
-
throw new
|
|
1899
|
+
throw new EdgeValidationError8("Partner identity profile is incomplete", errors);
|
|
1563
1900
|
}
|
|
1564
1901
|
return payload;
|
|
1565
1902
|
}
|
|
@@ -1596,7 +1933,7 @@ function validatePolicy(policy) {
|
|
|
1596
1933
|
if (!isScore(policy.minNameScore)) errors.minNameScore = ["Must be between 0 and 100"];
|
|
1597
1934
|
if (!isScore(policy.minAddressScore)) errors.minAddressScore = ["Must be between 0 and 100"];
|
|
1598
1935
|
if (Object.keys(errors).length > 0) {
|
|
1599
|
-
throw new
|
|
1936
|
+
throw new EdgeValidationError8("Invalid identity policy", errors);
|
|
1600
1937
|
}
|
|
1601
1938
|
}
|
|
1602
1939
|
function isScore(value) {
|
|
@@ -1612,9 +1949,155 @@ function firstNonEmpty(...values) {
|
|
|
1612
1949
|
return "";
|
|
1613
1950
|
}
|
|
1614
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
|
+
|
|
1615
2098
|
// src/token-vault.ts
|
|
1616
|
-
import { EdgeValidationError as
|
|
1617
|
-
import
|
|
2099
|
+
import { EdgeValidationError as EdgeValidationError10 } from "@edge-markets/connect";
|
|
2100
|
+
import crypto3 from "crypto";
|
|
1618
2101
|
var TOKEN_VAULT_VERSION = "edge_vault_v1";
|
|
1619
2102
|
var IV_BYTES = 12;
|
|
1620
2103
|
var TAG_BYTES = 16;
|
|
@@ -1626,7 +2109,7 @@ var EdgeTokenVault = class {
|
|
|
1626
2109
|
for (const [index, key] of (options.previousKeys ?? []).entries()) {
|
|
1627
2110
|
const normalized = normalizeVaultKey(key, `previousKeys[${index}]`);
|
|
1628
2111
|
if (this.keysById.has(normalized.id)) {
|
|
1629
|
-
throw new
|
|
2112
|
+
throw new EdgeValidationError10("Duplicate token vault key ID", {
|
|
1630
2113
|
keyId: [`Duplicate key ID "${normalized.id}"`]
|
|
1631
2114
|
});
|
|
1632
2115
|
}
|
|
@@ -1635,9 +2118,9 @@ var EdgeTokenVault = class {
|
|
|
1635
2118
|
}
|
|
1636
2119
|
encryptTokens(tokens) {
|
|
1637
2120
|
validateEdgeTokens(tokens);
|
|
1638
|
-
const iv =
|
|
2121
|
+
const iv = crypto3.randomBytes(IV_BYTES);
|
|
1639
2122
|
const aad = Buffer.from(`${TOKEN_VAULT_VERSION}.${this.currentKey.id}`, "utf8");
|
|
1640
|
-
const cipher =
|
|
2123
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", this.currentKey.key, iv);
|
|
1641
2124
|
cipher.setAAD(aad);
|
|
1642
2125
|
const plaintext = Buffer.from(JSON.stringify(tokens), "utf8");
|
|
1643
2126
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
@@ -1654,12 +2137,12 @@ var EdgeTokenVault = class {
|
|
|
1654
2137
|
const parsed = parseEnvelope(envelope);
|
|
1655
2138
|
const key = this.keysById.get(parsed.keyId);
|
|
1656
2139
|
if (!key) {
|
|
1657
|
-
throw new
|
|
2140
|
+
throw new EdgeValidationError10("Unknown token vault key ID", {
|
|
1658
2141
|
keyId: [`No key configured for "${parsed.keyId}"`]
|
|
1659
2142
|
});
|
|
1660
2143
|
}
|
|
1661
2144
|
try {
|
|
1662
|
-
const decipher =
|
|
2145
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", key, parsed.iv);
|
|
1663
2146
|
decipher.setAAD(Buffer.from(`${TOKEN_VAULT_VERSION}.${parsed.keyId}`, "utf8"));
|
|
1664
2147
|
decipher.setAuthTag(parsed.tag);
|
|
1665
2148
|
const plaintext = Buffer.concat([decipher.update(parsed.ciphertext), decipher.final()]).toString("utf8");
|
|
@@ -1667,8 +2150,8 @@ var EdgeTokenVault = class {
|
|
|
1667
2150
|
validateEdgeTokens(tokens);
|
|
1668
2151
|
return tokens;
|
|
1669
2152
|
} catch (error) {
|
|
1670
|
-
if (error instanceof
|
|
1671
|
-
throw new
|
|
2153
|
+
if (error instanceof EdgeValidationError10) throw error;
|
|
2154
|
+
throw new EdgeValidationError10("Failed to decrypt EDGE tokens", {
|
|
1672
2155
|
tokenEnvelope: ["Envelope is malformed, corrupted, or encrypted with a different key"]
|
|
1673
2156
|
});
|
|
1674
2157
|
}
|
|
@@ -1686,7 +2169,7 @@ function isEdgeTokenVaultEnvelope(value) {
|
|
|
1686
2169
|
function normalizeVaultKey(input, fieldName) {
|
|
1687
2170
|
const id = typeof input.id === "string" ? input.id.trim() : "";
|
|
1688
2171
|
if (!id) {
|
|
1689
|
-
throw new
|
|
2172
|
+
throw new EdgeValidationError10("Token vault key ID is required", {
|
|
1690
2173
|
[`${fieldName}.id`]: ["Required"]
|
|
1691
2174
|
});
|
|
1692
2175
|
}
|
|
@@ -1717,7 +2200,7 @@ function normalizeKeyMaterial(input, fieldName) {
|
|
|
1717
2200
|
}
|
|
1718
2201
|
const key = candidates.find((candidate) => candidate.length === KEY_BYTES);
|
|
1719
2202
|
if (!key) {
|
|
1720
|
-
throw new
|
|
2203
|
+
throw new EdgeValidationError10("Token vault key must decode to 32 bytes", {
|
|
1721
2204
|
[fieldName]: ["Provide a 32-byte key as base64, base64url, hex, Buffer, or Uint8Array"]
|
|
1722
2205
|
});
|
|
1723
2206
|
}
|
|
@@ -1748,18 +2231,18 @@ function validateEdgeTokens(tokens) {
|
|
|
1748
2231
|
}
|
|
1749
2232
|
}
|
|
1750
2233
|
if (Object.keys(errors).length > 0) {
|
|
1751
|
-
throw new
|
|
2234
|
+
throw new EdgeValidationError10("Invalid EDGE token payload", errors);
|
|
1752
2235
|
}
|
|
1753
2236
|
}
|
|
1754
2237
|
function parseEnvelope(envelope) {
|
|
1755
2238
|
if (typeof envelope !== "string") {
|
|
1756
|
-
throw new
|
|
2239
|
+
throw new EdgeValidationError10("Token envelope must be a string", {
|
|
1757
2240
|
tokenEnvelope: ["Expected string"]
|
|
1758
2241
|
});
|
|
1759
2242
|
}
|
|
1760
2243
|
const parts = envelope.split(".");
|
|
1761
2244
|
if (parts.length !== 5 || parts[0] !== TOKEN_VAULT_VERSION) {
|
|
1762
|
-
throw new
|
|
2245
|
+
throw new EdgeValidationError10("Invalid EDGE token vault envelope", {
|
|
1763
2246
|
tokenEnvelope: [`Expected ${TOKEN_VAULT_VERSION}.<keyId>.<iv>.<tag>.<ciphertext>`]
|
|
1764
2247
|
});
|
|
1765
2248
|
}
|
|
@@ -1774,7 +2257,7 @@ function parseEnvelope(envelope) {
|
|
|
1774
2257
|
if (tag.length !== TAG_BYTES) errors.tag = [`Must be ${TAG_BYTES} bytes`];
|
|
1775
2258
|
if (ciphertext.length === 0) errors.ciphertext = ["Required"];
|
|
1776
2259
|
if (Object.keys(errors).length > 0) {
|
|
1777
|
-
throw new
|
|
2260
|
+
throw new EdgeValidationError10("Invalid EDGE token vault envelope", errors);
|
|
1778
2261
|
}
|
|
1779
2262
|
return { keyId, iv, tag, ciphertext };
|
|
1780
2263
|
}
|
|
@@ -1788,13 +2271,13 @@ function fromBase64Url2(value) {
|
|
|
1788
2271
|
}
|
|
1789
2272
|
|
|
1790
2273
|
// src/webhook-guards.ts
|
|
1791
|
-
import { EdgeValidationError as
|
|
2274
|
+
import { EdgeValidationError as EdgeValidationError11 } from "@edge-markets/connect";
|
|
1792
2275
|
function isTransferWebhookEvent(event) {
|
|
1793
2276
|
return event.type.startsWith("transfer.");
|
|
1794
2277
|
}
|
|
1795
2278
|
function assertTransferEventMatchesIntent(event, intent) {
|
|
1796
2279
|
if (!isTransferWebhookEvent(event)) {
|
|
1797
|
-
throw new
|
|
2280
|
+
throw new EdgeValidationError11("Webhook event is not a transfer event", {
|
|
1798
2281
|
type: [`Received ${event.type}`]
|
|
1799
2282
|
});
|
|
1800
2283
|
}
|
|
@@ -1817,195 +2300,243 @@ function assertTransferEventMatchesIntent(event, intent) {
|
|
|
1817
2300
|
}
|
|
1818
2301
|
}
|
|
1819
2302
|
if (Object.keys(errors).length > 0) {
|
|
1820
|
-
throw new
|
|
2303
|
+
throw new EdgeValidationError11("Webhook transfer event does not match expected transfer intent", errors);
|
|
1821
2304
|
}
|
|
1822
2305
|
}
|
|
2306
|
+
var assertWebhookMatchesTransfer = assertTransferEventMatchesIntent;
|
|
1823
2307
|
|
|
1824
|
-
// src/webhook-
|
|
1825
|
-
import {
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
const body = normalizeRawBody(options.rawBody);
|
|
1865
|
-
const header = normalizeSignatureHeader(options.signatureHeader);
|
|
1866
|
-
const signatureTimestamp = extractWebhookSignatureTimestamp(header);
|
|
1867
|
-
if (!header || !signatureTimestamp) {
|
|
1868
|
-
throw new EdgeAuthenticationError3("Missing or malformed EDGE webhook signature");
|
|
1869
|
-
}
|
|
1870
|
-
if (!verifyWebhookSignature(header, body, options.secret, { toleranceSeconds: options.toleranceSeconds })) {
|
|
1871
|
-
throw new EdgeAuthenticationError3("Invalid EDGE webhook signature");
|
|
1872
|
-
}
|
|
1873
|
-
let parsed;
|
|
1874
|
-
try {
|
|
1875
|
-
parsed = JSON.parse(body);
|
|
1876
|
-
} catch {
|
|
1877
|
-
throw new EdgeValidationError9("EDGE webhook body must be valid JSON", {
|
|
1878
|
-
rawBody: ["Unable to parse JSON after signature verification"]
|
|
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
|
|
1879
2348
|
});
|
|
1880
2349
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
};
|
|
1889
|
-
}
|
|
1890
|
-
function extractWebhookSignatureTimestamp(header) {
|
|
1891
|
-
const normalizedHeader = normalizeSignatureHeader(header);
|
|
1892
|
-
if (!normalizedHeader) return void 0;
|
|
1893
|
-
const part = normalizedHeader.split(",").map((value) => value.trim()).find((value) => value.startsWith("t="));
|
|
1894
|
-
if (!part) return void 0;
|
|
1895
|
-
const timestamp = Number(part.slice(2));
|
|
1896
|
-
if (!Number.isInteger(timestamp) || timestamp <= 0) return void 0;
|
|
1897
|
-
return timestamp;
|
|
1898
|
-
}
|
|
1899
|
-
function isEdgeWebhookEvent(value) {
|
|
1900
|
-
try {
|
|
1901
|
-
validateEdgeWebhookEvent(value);
|
|
1902
|
-
return true;
|
|
1903
|
-
} catch {
|
|
1904
|
-
return false;
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
function validateEdgeWebhookEvent(value) {
|
|
1908
|
-
const errors = {};
|
|
1909
|
-
if (!isRecord(value)) {
|
|
1910
|
-
throw new EdgeValidationError9("EDGE webhook event must be an object", {
|
|
1911
|
-
event: ["Expected JSON object"]
|
|
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
|
|
1912
2357
|
});
|
|
1913
2358
|
}
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
errors.type = [`Must be one of: ${EDGE_WEBHOOK_EVENT_TYPES.join(", ")}`];
|
|
1923
|
-
}
|
|
1924
|
-
if (typeof createdAt !== "string" || !createdAt.trim() || Number.isNaN(Date.parse(createdAt))) {
|
|
1925
|
-
errors.created_at = ["Required ISO 8601 timestamp string"];
|
|
1926
|
-
}
|
|
1927
|
-
if (!isRecord(data)) {
|
|
1928
|
-
errors.data = ["Required object"];
|
|
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
|
+
});
|
|
1929
2367
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
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
|
+
}
|
|
1932
2436
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
2437
|
+
async replay(eventId) {
|
|
2438
|
+
return this.process(eventId, { force: true });
|
|
1935
2439
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
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
|
+
});
|
|
1947
2465
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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]
|
|
1964
2492
|
});
|
|
1965
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
|
+
};
|
|
1966
2510
|
}
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
}
|
|
1973
|
-
if (typeof data.type !== "string" || !TRANSFER_TYPES.has(data.type)) {
|
|
1974
|
-
errors["data.type"] = ['Must be "debit" or "credit"'];
|
|
2511
|
+
async emit(event) {
|
|
2512
|
+
try {
|
|
2513
|
+
await this.onEvent?.(event);
|
|
2514
|
+
} catch {
|
|
2515
|
+
}
|
|
1975
2516
|
}
|
|
1976
|
-
|
|
2517
|
+
};
|
|
2518
|
+
function createWebhookInbox(options) {
|
|
2519
|
+
return new EdgeWebhookInbox(options);
|
|
1977
2520
|
}
|
|
1978
|
-
function
|
|
1979
|
-
|
|
1980
|
-
errors[`data.${field}`] = ["Required string"];
|
|
1981
|
-
}
|
|
2521
|
+
function fingerprintWebhookEvent(event) {
|
|
2522
|
+
return crypto4.createHash("sha256").update(stableStringify(event)).digest("hex");
|
|
1982
2523
|
}
|
|
1983
|
-
function
|
|
1984
|
-
if (typeof
|
|
1985
|
-
|
|
2524
|
+
function normalizeEventId(eventId) {
|
|
2525
|
+
if (typeof eventId !== "string" || !eventId.trim()) {
|
|
2526
|
+
throw new EdgeValidationError12("Webhook event ID is required", {
|
|
2527
|
+
eventId: ["Required"]
|
|
2528
|
+
});
|
|
1986
2529
|
}
|
|
2530
|
+
return eventId.trim();
|
|
1987
2531
|
}
|
|
1988
|
-
function
|
|
1989
|
-
if (
|
|
1990
|
-
return
|
|
2532
|
+
function stableStringify(value) {
|
|
2533
|
+
if (Array.isArray(value)) {
|
|
2534
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
1991
2535
|
}
|
|
1992
|
-
if (
|
|
1993
|
-
return
|
|
1994
|
-
}
|
|
1995
|
-
throw new EdgeValidationError9("EDGE webhook raw body is required", {
|
|
1996
|
-
rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
|
|
1997
|
-
});
|
|
1998
|
-
}
|
|
1999
|
-
function normalizeSignatureHeader(header) {
|
|
2000
|
-
if (Array.isArray(header)) {
|
|
2001
|
-
return header.map((value) => value.trim()).filter(Boolean).join(",");
|
|
2536
|
+
if (value && typeof value === "object") {
|
|
2537
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
2002
2538
|
}
|
|
2003
|
-
|
|
2004
|
-
const trimmed = header.trim();
|
|
2005
|
-
return trimmed || void 0;
|
|
2006
|
-
}
|
|
2007
|
-
function isRecord(value) {
|
|
2008
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2539
|
+
return JSON.stringify(value);
|
|
2009
2540
|
}
|
|
2010
2541
|
|
|
2011
2542
|
// src/index.ts
|
|
@@ -2019,7 +2550,7 @@ import {
|
|
|
2019
2550
|
EdgeNetworkError as EdgeNetworkError3,
|
|
2020
2551
|
EdgeNotFoundError as EdgeNotFoundError2,
|
|
2021
2552
|
EdgeTokenExchangeError as EdgeTokenExchangeError2,
|
|
2022
|
-
EdgeValidationError as
|
|
2553
|
+
EdgeValidationError as EdgeValidationError13,
|
|
2023
2554
|
isApiError,
|
|
2024
2555
|
isAuthenticationError,
|
|
2025
2556
|
isConsentRequiredError,
|
|
@@ -2034,6 +2565,7 @@ import {
|
|
|
2034
2565
|
isProductionEnvironment
|
|
2035
2566
|
} from "@edge-markets/connect";
|
|
2036
2567
|
export {
|
|
2568
|
+
CONNECT_TRANSFER_DIRECTIONS,
|
|
2037
2569
|
EDGE_WEBHOOK_EVENT_TYPES2 as EDGE_WEBHOOK_EVENT_TYPES,
|
|
2038
2570
|
EdgeApiError2 as EdgeApiError,
|
|
2039
2571
|
EdgeAuthenticationError4 as EdgeAuthenticationError,
|
|
@@ -2048,19 +2580,29 @@ export {
|
|
|
2048
2580
|
EdgeTokenVault,
|
|
2049
2581
|
EdgeUserClient,
|
|
2050
2582
|
EdgeUserSession,
|
|
2051
|
-
|
|
2583
|
+
EdgeValidationError13 as EdgeValidationError,
|
|
2584
|
+
EdgeWebhookInbox,
|
|
2052
2585
|
EdgeWebhookReconciler,
|
|
2053
2586
|
TRANSFER_CATEGORIES,
|
|
2054
2587
|
assertSameTransferIntent,
|
|
2055
2588
|
assertTransferEventMatchesIntent,
|
|
2589
|
+
assertWebhookMatchesTransfer,
|
|
2056
2590
|
buildVerifyIdentityPayload,
|
|
2057
2591
|
createEdgeConnectServerFromEnv,
|
|
2058
2592
|
createEdgeTokenVault,
|
|
2059
2593
|
createEdgeUserSession,
|
|
2594
|
+
createSessionTokenRecord,
|
|
2060
2595
|
createTransferIdempotencyKey,
|
|
2596
|
+
createWebhookInbox,
|
|
2061
2597
|
createWebhookReconciler,
|
|
2598
|
+
encryptLegacySessionTokens,
|
|
2062
2599
|
evaluateIdentityResult,
|
|
2063
2600
|
extractWebhookSignatureTimestamp,
|
|
2601
|
+
fingerprintTransferIntent,
|
|
2602
|
+
fingerprintWebhookEvent,
|
|
2603
|
+
getConnectTransferDirection,
|
|
2604
|
+
getEdgeWebhookRawBody,
|
|
2605
|
+
getEdgeWebhookSignatureHeader,
|
|
2064
2606
|
getEnvironmentConfig3 as getEnvironmentConfig,
|
|
2065
2607
|
isApiError,
|
|
2066
2608
|
isAuthenticationError,
|
|
@@ -2072,8 +2614,13 @@ export {
|
|
|
2072
2614
|
isNetworkError,
|
|
2073
2615
|
isProductionEnvironment,
|
|
2074
2616
|
isTransferWebhookEvent,
|
|
2617
|
+
mapTransferStatusToPartnerState,
|
|
2618
|
+
moneyAmountsEqual,
|
|
2075
2619
|
normalizeMoneyAmount,
|
|
2620
|
+
normalizeSessionExpiresAt,
|
|
2076
2621
|
parseAndVerifyWebhook,
|
|
2622
|
+
parseSessionTokenRecord,
|
|
2623
|
+
parseWebhookHttpRequest,
|
|
2077
2624
|
startTransferVerification,
|
|
2078
2625
|
toEdgeHttpError,
|
|
2079
2626
|
toEdgeProblemJson,
|