@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.js
CHANGED
|
@@ -30,46 +30,62 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
CONNECT_TRANSFER_DIRECTIONS: () => CONNECT_TRANSFER_DIRECTIONS,
|
|
34
|
+
EDGE_WEBHOOK_EVENT_TYPES: () => import_connect15.EDGE_WEBHOOK_EVENT_TYPES,
|
|
35
|
+
EdgeApiError: () => import_connect14.EdgeApiError,
|
|
36
|
+
EdgeAuthenticationError: () => import_connect14.EdgeAuthenticationError,
|
|
36
37
|
EdgeConnectServer: () => EdgeConnectServer,
|
|
37
|
-
EdgeConsentRequiredError: () =>
|
|
38
|
-
EdgeError: () =>
|
|
39
|
-
EdgeIdentityVerificationError: () =>
|
|
40
|
-
EdgeInsufficientScopeError: () =>
|
|
41
|
-
EdgeNetworkError: () =>
|
|
42
|
-
EdgeNotFoundError: () =>
|
|
43
|
-
EdgeTokenExchangeError: () =>
|
|
38
|
+
EdgeConsentRequiredError: () => import_connect14.EdgeConsentRequiredError,
|
|
39
|
+
EdgeError: () => import_connect14.EdgeError,
|
|
40
|
+
EdgeIdentityVerificationError: () => import_connect14.EdgeIdentityVerificationError,
|
|
41
|
+
EdgeInsufficientScopeError: () => import_connect14.EdgeInsufficientScopeError,
|
|
42
|
+
EdgeNetworkError: () => import_connect14.EdgeNetworkError,
|
|
43
|
+
EdgeNotFoundError: () => import_connect14.EdgeNotFoundError,
|
|
44
|
+
EdgeTokenExchangeError: () => import_connect14.EdgeTokenExchangeError,
|
|
44
45
|
EdgeTokenVault: () => EdgeTokenVault,
|
|
45
46
|
EdgeUserClient: () => EdgeUserClient,
|
|
46
47
|
EdgeUserSession: () => EdgeUserSession,
|
|
47
|
-
EdgeValidationError: () =>
|
|
48
|
+
EdgeValidationError: () => import_connect14.EdgeValidationError,
|
|
49
|
+
EdgeWebhookInbox: () => EdgeWebhookInbox,
|
|
48
50
|
EdgeWebhookReconciler: () => EdgeWebhookReconciler,
|
|
49
|
-
TRANSFER_CATEGORIES: () =>
|
|
51
|
+
TRANSFER_CATEGORIES: () => import_connect15.TRANSFER_CATEGORIES,
|
|
50
52
|
assertSameTransferIntent: () => assertSameTransferIntent,
|
|
51
53
|
assertTransferEventMatchesIntent: () => assertTransferEventMatchesIntent,
|
|
54
|
+
assertWebhookMatchesTransfer: () => assertWebhookMatchesTransfer,
|
|
52
55
|
buildVerifyIdentityPayload: () => buildVerifyIdentityPayload,
|
|
53
56
|
createEdgeConnectServerFromEnv: () => createEdgeConnectServerFromEnv,
|
|
54
57
|
createEdgeTokenVault: () => createEdgeTokenVault,
|
|
55
58
|
createEdgeUserSession: () => createEdgeUserSession,
|
|
59
|
+
createSessionTokenRecord: () => createSessionTokenRecord,
|
|
56
60
|
createTransferIdempotencyKey: () => createTransferIdempotencyKey,
|
|
61
|
+
createWebhookInbox: () => createWebhookInbox,
|
|
57
62
|
createWebhookReconciler: () => createWebhookReconciler,
|
|
63
|
+
encryptLegacySessionTokens: () => encryptLegacySessionTokens,
|
|
58
64
|
evaluateIdentityResult: () => evaluateIdentityResult,
|
|
59
65
|
extractWebhookSignatureTimestamp: () => extractWebhookSignatureTimestamp,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
fingerprintTransferIntent: () => fingerprintTransferIntent,
|
|
67
|
+
fingerprintWebhookEvent: () => fingerprintWebhookEvent,
|
|
68
|
+
getConnectTransferDirection: () => getConnectTransferDirection,
|
|
69
|
+
getEdgeWebhookRawBody: () => getEdgeWebhookRawBody,
|
|
70
|
+
getEdgeWebhookSignatureHeader: () => getEdgeWebhookSignatureHeader,
|
|
71
|
+
getEnvironmentConfig: () => import_connect15.getEnvironmentConfig,
|
|
72
|
+
isApiError: () => import_connect14.isApiError,
|
|
73
|
+
isAuthenticationError: () => import_connect14.isAuthenticationError,
|
|
74
|
+
isConsentRequiredError: () => import_connect14.isConsentRequiredError,
|
|
75
|
+
isEdgeError: () => import_connect14.isEdgeError,
|
|
65
76
|
isEdgeTokenVaultEnvelope: () => isEdgeTokenVaultEnvelope,
|
|
66
77
|
isEdgeWebhookEvent: () => isEdgeWebhookEvent,
|
|
67
|
-
isIdentityVerificationError: () =>
|
|
68
|
-
isNetworkError: () =>
|
|
69
|
-
isProductionEnvironment: () =>
|
|
78
|
+
isIdentityVerificationError: () => import_connect14.isIdentityVerificationError,
|
|
79
|
+
isNetworkError: () => import_connect14.isNetworkError,
|
|
80
|
+
isProductionEnvironment: () => import_connect15.isProductionEnvironment,
|
|
70
81
|
isTransferWebhookEvent: () => isTransferWebhookEvent,
|
|
82
|
+
mapTransferStatusToPartnerState: () => mapTransferStatusToPartnerState,
|
|
83
|
+
moneyAmountsEqual: () => moneyAmountsEqual,
|
|
71
84
|
normalizeMoneyAmount: () => normalizeMoneyAmount,
|
|
85
|
+
normalizeSessionExpiresAt: () => normalizeSessionExpiresAt,
|
|
72
86
|
parseAndVerifyWebhook: () => parseAndVerifyWebhook,
|
|
87
|
+
parseSessionTokenRecord: () => parseSessionTokenRecord,
|
|
88
|
+
parseWebhookHttpRequest: () => parseWebhookHttpRequest,
|
|
73
89
|
startTransferVerification: () => startTransferVerification,
|
|
74
90
|
toEdgeHttpError: () => toEdgeHttpError,
|
|
75
91
|
toEdgeProblemJson: () => toEdgeProblemJson,
|
|
@@ -87,16 +103,28 @@ var import_connect = require("@edge-markets/connect");
|
|
|
87
103
|
var import_node_crypto = __toESM(require("crypto"));
|
|
88
104
|
var DEFAULT_MIN_AMOUNT = "1.00";
|
|
89
105
|
var DEFAULT_MAX_AMOUNT = "10000.00";
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
var CONNECT_TRANSFER_DIRECTIONS = {
|
|
107
|
+
debit: "user_to_partner",
|
|
108
|
+
credit: "partner_to_user"
|
|
109
|
+
};
|
|
110
|
+
function getConnectTransferDirection(type) {
|
|
111
|
+
if (type !== "debit" && type !== "credit") {
|
|
112
|
+
throw new import_connect.EdgeValidationError("Invalid Connect transfer type", {
|
|
113
|
+
type: ['Must be "debit" or "credit"']
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return CONNECT_TRANSFER_DIRECTIONS[type];
|
|
117
|
+
}
|
|
118
|
+
function normalizeMoneyAmount(value, options = {}) {
|
|
119
|
+
const cents = parseMoneyToCents(value, "amount", options);
|
|
92
120
|
const dollars = cents / 100n;
|
|
93
121
|
const remainder = cents % 100n;
|
|
94
122
|
return `${dollars.toString()}.${remainder.toString().padStart(2, "0")}`;
|
|
95
123
|
}
|
|
96
|
-
function validateTransferAmount(value, limits = {}) {
|
|
97
|
-
const amount = parseMoneyToCents(value, "amount");
|
|
98
|
-
const min = parseMoneyToCents(limits.min ?? DEFAULT_MIN_AMOUNT, "min");
|
|
99
|
-
const max = parseMoneyToCents(limits.max ?? DEFAULT_MAX_AMOUNT, "max");
|
|
124
|
+
function validateTransferAmount(value, limits = {}, options = {}) {
|
|
125
|
+
const amount = parseMoneyToCents(value, "amount", options);
|
|
126
|
+
const min = parseMoneyToCents(limits.min ?? DEFAULT_MIN_AMOUNT, "min", { allowNumber: true });
|
|
127
|
+
const max = parseMoneyToCents(limits.max ?? DEFAULT_MAX_AMOUNT, "max", { allowNumber: true });
|
|
100
128
|
const errors = {};
|
|
101
129
|
if (min <= 0n) errors.min = ["Minimum amount must be greater than 0"];
|
|
102
130
|
if (max < min) errors.max = ["Maximum amount must be greater than or equal to minimum amount"];
|
|
@@ -144,6 +172,27 @@ function assertSameTransferIntent(existing, requested) {
|
|
|
144
172
|
throw new import_connect.EdgeValidationError("Transfer intent does not match existing idempotent transfer", errors);
|
|
145
173
|
}
|
|
146
174
|
}
|
|
175
|
+
function fingerprintTransferIntent(intent) {
|
|
176
|
+
return import_node_crypto.default.createHash("sha256").update(
|
|
177
|
+
JSON.stringify({
|
|
178
|
+
type: intent.type,
|
|
179
|
+
amount: normalizeMoneyAmount(intent.amount),
|
|
180
|
+
category: intent.category ?? null
|
|
181
|
+
})
|
|
182
|
+
).digest("hex");
|
|
183
|
+
}
|
|
184
|
+
function moneyAmountsEqual(left, right, options = {}) {
|
|
185
|
+
return normalizeMoneyAmount(left, options) === normalizeMoneyAmount(right, options);
|
|
186
|
+
}
|
|
187
|
+
function mapTransferStatusToPartnerState(status, mapping) {
|
|
188
|
+
const partnerState = mapping[status];
|
|
189
|
+
if (!partnerState) {
|
|
190
|
+
throw new import_connect.EdgeValidationError("Unsupported transfer status", {
|
|
191
|
+
status: [`Unsupported status: ${status}`]
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return partnerState;
|
|
195
|
+
}
|
|
147
196
|
async function startTransferVerification(client, options) {
|
|
148
197
|
const origin = typeof options.origin === "string" ? options.origin.trim() : "";
|
|
149
198
|
if (!origin) {
|
|
@@ -164,9 +213,14 @@ async function startTransferVerification(client, options) {
|
|
|
164
213
|
const verificationSession = await client.createVerificationSession(transferId, { origin });
|
|
165
214
|
return { transfer, verificationSession };
|
|
166
215
|
}
|
|
167
|
-
function parseMoneyToCents(value, field) {
|
|
216
|
+
function parseMoneyToCents(value, field, options = {}) {
|
|
168
217
|
let input;
|
|
169
218
|
if (typeof value === "number") {
|
|
219
|
+
if (options.allowNumber === false) {
|
|
220
|
+
throw new import_connect.EdgeValidationError("Money amount must be a string in strict mode", {
|
|
221
|
+
[field]: ['Pass a decimal string like "25.00"']
|
|
222
|
+
});
|
|
223
|
+
}
|
|
170
224
|
if (!Number.isFinite(value)) {
|
|
171
225
|
throw new import_connect.EdgeValidationError("Money amount must be finite", { [field]: ["Must be a finite number"] });
|
|
172
226
|
}
|
|
@@ -478,6 +532,7 @@ function validateMtlsConfig(config) {
|
|
|
478
532
|
var import_connect3 = require("@edge-markets/connect");
|
|
479
533
|
var EdgeUserSession = class {
|
|
480
534
|
constructor(options) {
|
|
535
|
+
this.refreshPromise = null;
|
|
481
536
|
if (!options.edge?.forUser || !options.edge?.refreshTokens) {
|
|
482
537
|
throw new import_connect3.EdgeValidationError("EdgeUserSession requires an EdgeConnectServer instance", {
|
|
483
538
|
edge: ["Provide edge.forUser and edge.refreshTokens"]
|
|
@@ -506,29 +561,44 @@ var EdgeUserSession = class {
|
|
|
506
561
|
this.refreshSkewMs = options.refreshSkewMs ?? 6e4;
|
|
507
562
|
this.now = options.now ?? Date.now;
|
|
508
563
|
this.onRefresh = options.onRefresh;
|
|
564
|
+
this.onEvent = options.onEvent;
|
|
565
|
+
this.singleFlightRefresh = options.singleFlightRefresh ?? true;
|
|
509
566
|
}
|
|
510
567
|
async getValidTokens(options = {}) {
|
|
511
568
|
const tokens = await this.loadTokens();
|
|
512
569
|
if (!options.forceRefresh && tokens.expiresAt > this.now() + this.refreshSkewMs) {
|
|
513
570
|
return tokens;
|
|
514
571
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
572
|
+
if (this.singleFlightRefresh) {
|
|
573
|
+
if (!this.refreshPromise) {
|
|
574
|
+
this.refreshPromise = this.refreshTokens(tokens).finally(() => {
|
|
575
|
+
this.refreshPromise = null;
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
return this.refreshPromise;
|
|
579
|
+
}
|
|
580
|
+
return this.refreshTokens(tokens);
|
|
519
581
|
}
|
|
520
582
|
async getClient(options = {}) {
|
|
521
583
|
const tokens = await this.getValidTokens(options);
|
|
522
584
|
return this.edge.forUser(tokens.accessToken);
|
|
523
585
|
}
|
|
524
|
-
async withClient(callback) {
|
|
525
|
-
const tokens = await this.getValidTokens();
|
|
586
|
+
async withClient(callback, options = {}) {
|
|
587
|
+
const tokens = await this.getValidTokens(options);
|
|
526
588
|
return callback(this.edge.forUser(tokens.accessToken), tokens);
|
|
527
589
|
}
|
|
528
590
|
async saveTokens(tokens, refreshed = false) {
|
|
529
591
|
validateTokens(tokens);
|
|
530
592
|
const value = this.tokenVault ? this.tokenVault.encryptTokens(tokens) : tokens;
|
|
531
593
|
await this.tokenStore.save(this.subjectId, value, { tokens, refreshed });
|
|
594
|
+
await this.emit({
|
|
595
|
+
type: "session.saved",
|
|
596
|
+
subjectId: this.subjectId,
|
|
597
|
+
refreshed,
|
|
598
|
+
encrypted: typeof value === "string",
|
|
599
|
+
expiresAt: tokens.expiresAt,
|
|
600
|
+
scope: tokens.scope
|
|
601
|
+
});
|
|
532
602
|
}
|
|
533
603
|
async loadTokens() {
|
|
534
604
|
const value = await this.tokenStore.load(this.subjectId);
|
|
@@ -545,11 +615,60 @@ var EdgeUserSession = class {
|
|
|
545
615
|
}
|
|
546
616
|
const tokens = this.tokenVault.decryptTokens(value);
|
|
547
617
|
validateTokens(tokens);
|
|
618
|
+
await this.emitLoaded(tokens, true);
|
|
548
619
|
return tokens;
|
|
549
620
|
}
|
|
550
621
|
validateTokens(value);
|
|
622
|
+
await this.emitLoaded(value, false);
|
|
551
623
|
return value;
|
|
552
624
|
}
|
|
625
|
+
async refreshTokens(currentTokens) {
|
|
626
|
+
await this.emit({
|
|
627
|
+
type: "session.refresh_started",
|
|
628
|
+
subjectId: this.subjectId,
|
|
629
|
+
expiresAt: currentTokens.expiresAt,
|
|
630
|
+
scope: currentTokens.scope
|
|
631
|
+
});
|
|
632
|
+
try {
|
|
633
|
+
const refreshed = await this.edge.refreshTokens(currentTokens.refreshToken);
|
|
634
|
+
const tokens = {
|
|
635
|
+
...refreshed,
|
|
636
|
+
refreshToken: refreshed.refreshToken || currentTokens.refreshToken
|
|
637
|
+
};
|
|
638
|
+
await this.saveTokens(tokens, true);
|
|
639
|
+
await this.onRefresh?.(tokens);
|
|
640
|
+
await this.emit({
|
|
641
|
+
type: "session.refresh_succeeded",
|
|
642
|
+
subjectId: this.subjectId,
|
|
643
|
+
refreshed: true,
|
|
644
|
+
expiresAt: tokens.expiresAt,
|
|
645
|
+
scope: tokens.scope
|
|
646
|
+
});
|
|
647
|
+
return tokens;
|
|
648
|
+
} catch (error) {
|
|
649
|
+
await this.emit({
|
|
650
|
+
type: "session.refresh_failed",
|
|
651
|
+
subjectId: this.subjectId,
|
|
652
|
+
error: error instanceof Error ? error.message : "Unknown token refresh error"
|
|
653
|
+
});
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async emitLoaded(tokens, encrypted) {
|
|
658
|
+
await this.emit({
|
|
659
|
+
type: "session.loaded",
|
|
660
|
+
subjectId: this.subjectId,
|
|
661
|
+
encrypted,
|
|
662
|
+
expiresAt: tokens.expiresAt,
|
|
663
|
+
scope: tokens.scope
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
async emit(event) {
|
|
667
|
+
try {
|
|
668
|
+
await this.onEvent?.(event);
|
|
669
|
+
} catch {
|
|
670
|
+
}
|
|
671
|
+
}
|
|
553
672
|
};
|
|
554
673
|
function createEdgeUserSession(options) {
|
|
555
674
|
return new EdgeUserSession(options);
|
|
@@ -601,8 +720,8 @@ var EdgeWebhookReconciler = class {
|
|
|
601
720
|
if (!options.cursorStore?.load || !options.cursorStore?.save) {
|
|
602
721
|
throw new Error("EdgeWebhookReconciler: cursorStore.load and cursorStore.save are required");
|
|
603
722
|
}
|
|
604
|
-
if (!options.processEvent) {
|
|
605
|
-
throw new Error("EdgeWebhookReconciler: processEvent is required");
|
|
723
|
+
if (!options.processEvent && !options.inbox) {
|
|
724
|
+
throw new Error("EdgeWebhookReconciler: processEvent or inbox is required");
|
|
606
725
|
}
|
|
607
726
|
if (options.maxPages !== void 0 && (!Number.isInteger(options.maxPages) || options.maxPages <= 0)) {
|
|
608
727
|
throw new Error("EdgeWebhookReconciler: maxPages must be a positive integer");
|
|
@@ -629,6 +748,8 @@ var EdgeWebhookReconciler = class {
|
|
|
629
748
|
async runInternal() {
|
|
630
749
|
let cursor = normalizeCursor(await this.options.cursorStore.load());
|
|
631
750
|
let processed = 0;
|
|
751
|
+
let accepted = 0;
|
|
752
|
+
let duplicates = 0;
|
|
632
753
|
let pages = 0;
|
|
633
754
|
let hasMore = false;
|
|
634
755
|
const maxPages = this.options.maxPages ?? DEFAULT_MAX_PAGES;
|
|
@@ -642,6 +763,7 @@ var EdgeWebhookReconciler = class {
|
|
|
642
763
|
if (result.events.length === 0) {
|
|
643
764
|
return {
|
|
644
765
|
processed,
|
|
766
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
645
767
|
pages,
|
|
646
768
|
...cursor ? { cursor } : {},
|
|
647
769
|
hasMore,
|
|
@@ -650,14 +772,23 @@ var EdgeWebhookReconciler = class {
|
|
|
650
772
|
};
|
|
651
773
|
}
|
|
652
774
|
for (const event of result.events) {
|
|
653
|
-
|
|
775
|
+
if (this.options.inbox) {
|
|
776
|
+
const acceptResult = await this.options.inbox.acceptSyncedEvent(event);
|
|
777
|
+
accepted++;
|
|
778
|
+
if (acceptResult.duplicate) duplicates++;
|
|
779
|
+
const processResult = await this.options.inbox.process(event.id);
|
|
780
|
+
if (processResult.processed) processed++;
|
|
781
|
+
} else {
|
|
782
|
+
await this.options.processEvent?.(event);
|
|
783
|
+
processed++;
|
|
784
|
+
}
|
|
654
785
|
cursor = event.id;
|
|
655
786
|
await this.options.cursorStore.save(cursor);
|
|
656
|
-
processed++;
|
|
657
787
|
}
|
|
658
788
|
if (!result.hasMore) {
|
|
659
789
|
return {
|
|
660
790
|
processed,
|
|
791
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
661
792
|
pages,
|
|
662
793
|
...cursor ? { cursor } : {},
|
|
663
794
|
hasMore: false,
|
|
@@ -667,6 +798,7 @@ var EdgeWebhookReconciler = class {
|
|
|
667
798
|
}
|
|
668
799
|
return {
|
|
669
800
|
processed,
|
|
801
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
670
802
|
pages,
|
|
671
803
|
...cursor ? { cursor } : {},
|
|
672
804
|
hasMore,
|
|
@@ -1585,8 +1717,225 @@ function edgeTitle(error) {
|
|
|
1585
1717
|
return error.code.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1586
1718
|
}
|
|
1587
1719
|
|
|
1588
|
-
// src/
|
|
1720
|
+
// src/http-helpers.ts
|
|
1721
|
+
var import_connect8 = require("@edge-markets/connect");
|
|
1722
|
+
|
|
1723
|
+
// src/webhook-parser.ts
|
|
1589
1724
|
var import_connect7 = require("@edge-markets/connect");
|
|
1725
|
+
|
|
1726
|
+
// src/webhook-signing.ts
|
|
1727
|
+
var import_node_crypto2 = __toESM(require("crypto"));
|
|
1728
|
+
function verifyWebhookSignature(header, body, secret, options = {}) {
|
|
1729
|
+
if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
|
|
1730
|
+
return false;
|
|
1731
|
+
}
|
|
1732
|
+
const requestedTolerance = options.toleranceSeconds;
|
|
1733
|
+
const toleranceSeconds = typeof requestedTolerance === "number" && Number.isFinite(requestedTolerance) && requestedTolerance >= 0 ? requestedTolerance : 300;
|
|
1734
|
+
const parts = header.split(",").map((p) => p.trim());
|
|
1735
|
+
const tPart = parts.find((p) => p.startsWith("t="));
|
|
1736
|
+
const v1Part = parts.find((p) => p.startsWith("v1="));
|
|
1737
|
+
if (!tPart || !v1Part) return false;
|
|
1738
|
+
const timestamp = parseInt(tPart.slice(2), 10);
|
|
1739
|
+
const signature = v1Part.slice(3);
|
|
1740
|
+
if (isNaN(timestamp) || !signature) return false;
|
|
1741
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1742
|
+
if (Math.abs(now - timestamp) > toleranceSeconds) return false;
|
|
1743
|
+
const signedContent = `${timestamp}.${body}`;
|
|
1744
|
+
const expectedHmac = import_node_crypto2.default.createHmac("sha256", secret).update(signedContent).digest("hex");
|
|
1745
|
+
try {
|
|
1746
|
+
const sigBuf = Buffer.from(signature, "hex");
|
|
1747
|
+
const expBuf = Buffer.from(expectedHmac, "hex");
|
|
1748
|
+
if (sigBuf.length !== expBuf.length) return false;
|
|
1749
|
+
return import_node_crypto2.default.timingSafeEqual(sigBuf, expBuf);
|
|
1750
|
+
} catch {
|
|
1751
|
+
return false;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/webhook-parser.ts
|
|
1756
|
+
var EVENT_TYPES = new Set(import_connect7.EDGE_WEBHOOK_EVENT_TYPES);
|
|
1757
|
+
var TRANSFER_TYPES = /* @__PURE__ */ new Set(["debit", "credit"]);
|
|
1758
|
+
function parseAndVerifyWebhook(options) {
|
|
1759
|
+
const body = normalizeRawBody(options.rawBody);
|
|
1760
|
+
const header = normalizeSignatureHeader(options.signatureHeader);
|
|
1761
|
+
const signatureTimestamp = extractWebhookSignatureTimestamp(header);
|
|
1762
|
+
if (!header || !signatureTimestamp) {
|
|
1763
|
+
throw new import_connect7.EdgeAuthenticationError("Missing or malformed EDGE webhook signature");
|
|
1764
|
+
}
|
|
1765
|
+
if (!verifyWebhookSignature(header, body, options.secret, { toleranceSeconds: options.toleranceSeconds })) {
|
|
1766
|
+
throw new import_connect7.EdgeAuthenticationError("Invalid EDGE webhook signature");
|
|
1767
|
+
}
|
|
1768
|
+
let parsed;
|
|
1769
|
+
try {
|
|
1770
|
+
parsed = JSON.parse(body);
|
|
1771
|
+
} catch {
|
|
1772
|
+
throw new import_connect7.EdgeValidationError("EDGE webhook body must be valid JSON", {
|
|
1773
|
+
rawBody: ["Unable to parse JSON after signature verification"]
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
const event = validateEdgeWebhookEvent(parsed);
|
|
1777
|
+
return {
|
|
1778
|
+
event,
|
|
1779
|
+
eventId: event.id,
|
|
1780
|
+
eventType: event.type,
|
|
1781
|
+
signatureTimestamp,
|
|
1782
|
+
receivedAt: options.receivedAt ?? /* @__PURE__ */ new Date()
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
function extractWebhookSignatureTimestamp(header) {
|
|
1786
|
+
const normalizedHeader = normalizeSignatureHeader(header);
|
|
1787
|
+
if (!normalizedHeader) return void 0;
|
|
1788
|
+
const part = normalizedHeader.split(",").map((value) => value.trim()).find((value) => value.startsWith("t="));
|
|
1789
|
+
if (!part) return void 0;
|
|
1790
|
+
const timestamp = Number(part.slice(2));
|
|
1791
|
+
if (!Number.isInteger(timestamp) || timestamp <= 0) return void 0;
|
|
1792
|
+
return timestamp;
|
|
1793
|
+
}
|
|
1794
|
+
function isEdgeWebhookEvent(value) {
|
|
1795
|
+
try {
|
|
1796
|
+
validateEdgeWebhookEvent(value);
|
|
1797
|
+
return true;
|
|
1798
|
+
} catch {
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
function validateEdgeWebhookEvent(value) {
|
|
1803
|
+
const errors = {};
|
|
1804
|
+
if (!isRecord(value)) {
|
|
1805
|
+
throw new import_connect7.EdgeValidationError("EDGE webhook event must be an object", {
|
|
1806
|
+
event: ["Expected JSON object"]
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
const id = value.id;
|
|
1810
|
+
const type = value.type;
|
|
1811
|
+
const createdAt = value.created_at;
|
|
1812
|
+
const data = value.data;
|
|
1813
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
1814
|
+
errors.id = ["Required string"];
|
|
1815
|
+
}
|
|
1816
|
+
if (typeof type !== "string" || !EVENT_TYPES.has(type)) {
|
|
1817
|
+
errors.type = [`Must be one of: ${import_connect7.EDGE_WEBHOOK_EVENT_TYPES.join(", ")}`];
|
|
1818
|
+
}
|
|
1819
|
+
if (typeof createdAt !== "string" || !createdAt.trim() || Number.isNaN(Date.parse(createdAt))) {
|
|
1820
|
+
errors.created_at = ["Required ISO 8601 timestamp string"];
|
|
1821
|
+
}
|
|
1822
|
+
if (!isRecord(data)) {
|
|
1823
|
+
errors.data = ["Required object"];
|
|
1824
|
+
}
|
|
1825
|
+
if (Object.keys(errors).length === 0) {
|
|
1826
|
+
validateKnownEventData(type, data, errors);
|
|
1827
|
+
}
|
|
1828
|
+
if (Object.keys(errors).length > 0) {
|
|
1829
|
+
throw new import_connect7.EdgeValidationError("Invalid EDGE webhook event", errors);
|
|
1830
|
+
}
|
|
1831
|
+
return value;
|
|
1832
|
+
}
|
|
1833
|
+
function validateKnownEventData(type, data, errors) {
|
|
1834
|
+
switch (type) {
|
|
1835
|
+
case "transfer.completed":
|
|
1836
|
+
validateTransferEventData(data, "completed", errors);
|
|
1837
|
+
return;
|
|
1838
|
+
case "transfer.failed":
|
|
1839
|
+
validateTransferEventData(data, "failed", errors);
|
|
1840
|
+
if (data.reason !== void 0 && typeof data.reason !== "string") {
|
|
1841
|
+
errors["data.reason"] = ["Must be a string when provided"];
|
|
1842
|
+
}
|
|
1843
|
+
return;
|
|
1844
|
+
case "transfer.expired":
|
|
1845
|
+
validateTransferEventData(data, "expired", errors);
|
|
1846
|
+
return;
|
|
1847
|
+
case "transfer.processing":
|
|
1848
|
+
validateTransferEventData(data, "processing", errors);
|
|
1849
|
+
return;
|
|
1850
|
+
case "consent.revoked":
|
|
1851
|
+
validateRequiredString(data, "userId", errors);
|
|
1852
|
+
validateRequiredString(data, "clientId", errors);
|
|
1853
|
+
validateRequiredIsoTimestamp(data, "revokedAt", errors);
|
|
1854
|
+
return;
|
|
1855
|
+
default: {
|
|
1856
|
+
const exhaustive = type;
|
|
1857
|
+
throw new import_connect7.EdgeValidationError("Unsupported EDGE webhook event type", {
|
|
1858
|
+
type: [`Unsupported event type: ${exhaustive}`]
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
function validateTransferEventData(data, status, errors) {
|
|
1864
|
+
validateRequiredString(data, "transferId", errors);
|
|
1865
|
+
if (data.status !== status) {
|
|
1866
|
+
errors["data.status"] = [`Must be "${status}" for this event type`];
|
|
1867
|
+
}
|
|
1868
|
+
if (typeof data.type !== "string" || !TRANSFER_TYPES.has(data.type)) {
|
|
1869
|
+
errors["data.type"] = ['Must be "debit" or "credit"'];
|
|
1870
|
+
}
|
|
1871
|
+
validateRequiredString(data, "amount", errors);
|
|
1872
|
+
}
|
|
1873
|
+
function validateRequiredString(data, field, errors) {
|
|
1874
|
+
if (typeof data[field] !== "string" || !data[field].trim()) {
|
|
1875
|
+
errors[`data.${field}`] = ["Required string"];
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
function validateRequiredIsoTimestamp(data, field, errors) {
|
|
1879
|
+
if (typeof data[field] !== "string" || !data[field].trim() || Number.isNaN(Date.parse(data[field]))) {
|
|
1880
|
+
errors[`data.${field}`] = ["Required ISO 8601 timestamp string"];
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
function normalizeRawBody(rawBody) {
|
|
1884
|
+
if (typeof rawBody === "string") {
|
|
1885
|
+
return rawBody;
|
|
1886
|
+
}
|
|
1887
|
+
if (Buffer.isBuffer(rawBody) || rawBody instanceof Uint8Array) {
|
|
1888
|
+
return Buffer.from(rawBody).toString("utf8");
|
|
1889
|
+
}
|
|
1890
|
+
throw new import_connect7.EdgeValidationError("EDGE webhook raw body is required", {
|
|
1891
|
+
rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
function normalizeSignatureHeader(header) {
|
|
1895
|
+
if (Array.isArray(header)) {
|
|
1896
|
+
return header.map((value) => value.trim()).filter(Boolean).join(",");
|
|
1897
|
+
}
|
|
1898
|
+
if (typeof header !== "string") return void 0;
|
|
1899
|
+
const trimmed = header.trim();
|
|
1900
|
+
return trimmed || void 0;
|
|
1901
|
+
}
|
|
1902
|
+
function isRecord(value) {
|
|
1903
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// src/http-helpers.ts
|
|
1907
|
+
function getEdgeWebhookRawBody(request) {
|
|
1908
|
+
if (typeof request.rawBody === "string" || Buffer.isBuffer(request.rawBody) || request.rawBody instanceof Uint8Array) {
|
|
1909
|
+
return request.rawBody;
|
|
1910
|
+
}
|
|
1911
|
+
throw new import_connect8.EdgeValidationError("EDGE webhook raw body is required", {
|
|
1912
|
+
rawBody: [
|
|
1913
|
+
"Configure your framework to expose the raw UTF-8 request body before JSON parsing. Parsed JSON bodies cannot be used for HMAC verification."
|
|
1914
|
+
]
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
function getEdgeWebhookSignatureHeader(request, headerName = "x-edge-signature") {
|
|
1918
|
+
const headers = request.headers ?? {};
|
|
1919
|
+
const lowerHeaderName = headerName.toLowerCase();
|
|
1920
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1921
|
+
if (name.toLowerCase() === lowerHeaderName) {
|
|
1922
|
+
if (typeof value === "string" || Array.isArray(value)) return value;
|
|
1923
|
+
if (value === void 0 || value === null) return void 0;
|
|
1924
|
+
return String(value);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
return void 0;
|
|
1928
|
+
}
|
|
1929
|
+
function parseWebhookHttpRequest(options) {
|
|
1930
|
+
return parseAndVerifyWebhook({
|
|
1931
|
+
...options,
|
|
1932
|
+
rawBody: getEdgeWebhookRawBody(options.request),
|
|
1933
|
+
signatureHeader: getEdgeWebhookSignatureHeader(options.request, options.signatureHeaderName)
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// src/identity.ts
|
|
1938
|
+
var import_connect9 = require("@edge-markets/connect");
|
|
1590
1939
|
var DEFAULT_IDENTITY_POLICY = {
|
|
1591
1940
|
minNameScore: 70,
|
|
1592
1941
|
minAddressScore: 65,
|
|
@@ -1614,7 +1963,7 @@ function buildVerifyIdentityPayload(profile) {
|
|
|
1614
1963
|
errors.address = ["At least one address field is required"];
|
|
1615
1964
|
}
|
|
1616
1965
|
if (Object.keys(errors).length > 0) {
|
|
1617
|
-
throw new
|
|
1966
|
+
throw new import_connect9.EdgeValidationError("Partner identity profile is incomplete", errors);
|
|
1618
1967
|
}
|
|
1619
1968
|
return payload;
|
|
1620
1969
|
}
|
|
@@ -1651,7 +2000,7 @@ function validatePolicy(policy) {
|
|
|
1651
2000
|
if (!isScore(policy.minNameScore)) errors.minNameScore = ["Must be between 0 and 100"];
|
|
1652
2001
|
if (!isScore(policy.minAddressScore)) errors.minAddressScore = ["Must be between 0 and 100"];
|
|
1653
2002
|
if (Object.keys(errors).length > 0) {
|
|
1654
|
-
throw new
|
|
2003
|
+
throw new import_connect9.EdgeValidationError("Invalid identity policy", errors);
|
|
1655
2004
|
}
|
|
1656
2005
|
}
|
|
1657
2006
|
function isScore(value) {
|
|
@@ -1667,9 +2016,155 @@ function firstNonEmpty(...values) {
|
|
|
1667
2016
|
return "";
|
|
1668
2017
|
}
|
|
1669
2018
|
|
|
2019
|
+
// src/session-store.ts
|
|
2020
|
+
var import_connect10 = require("@edge-markets/connect");
|
|
2021
|
+
function createSessionTokenRecord(subjectId, tokens, tokenVault, options = {}) {
|
|
2022
|
+
const normalizedSubjectId = normalizeRequiredString(subjectId, "subjectId");
|
|
2023
|
+
validateTokens2(tokens);
|
|
2024
|
+
return {
|
|
2025
|
+
subjectId: normalizedSubjectId,
|
|
2026
|
+
encryptedTokens: tokenVault.encryptTokens(tokens),
|
|
2027
|
+
expiresAt: normalizeSessionExpiresAt(tokens.expiresAt),
|
|
2028
|
+
scopes: options.scopes ?? splitScopes(tokens.scope),
|
|
2029
|
+
...options.edgeUserId ? { edgeUserId: options.edgeUserId } : {},
|
|
2030
|
+
...options.keyId ? { keyId: options.keyId } : {},
|
|
2031
|
+
...options.metadata ? { metadata: options.metadata } : {}
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
function parseSessionTokenRecord(record) {
|
|
2035
|
+
if (!isRecord2(record)) {
|
|
2036
|
+
throw new import_connect10.EdgeValidationError("EDGE session token record must be an object", {
|
|
2037
|
+
record: ["Expected object"]
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
const subjectId = normalizeRequiredString(record.subjectId, "subjectId");
|
|
2041
|
+
const encryptedTokens = normalizeRequiredString(record.encryptedTokens, "encryptedTokens");
|
|
2042
|
+
const expiresAt = normalizeSessionExpiresAt(record.expiresAt);
|
|
2043
|
+
const scopes = record.scopes === void 0 ? void 0 : normalizeScopes(record.scopes);
|
|
2044
|
+
return {
|
|
2045
|
+
subjectId,
|
|
2046
|
+
encryptedTokens,
|
|
2047
|
+
expiresAt,
|
|
2048
|
+
...scopes ? { scopes } : {},
|
|
2049
|
+
...typeof record.edgeUserId === "string" && record.edgeUserId.trim() ? { edgeUserId: record.edgeUserId.trim() } : {},
|
|
2050
|
+
...typeof record.keyId === "string" && record.keyId.trim() ? { keyId: record.keyId.trim() } : {},
|
|
2051
|
+
...isRecord2(record.metadata) ? { metadata: record.metadata } : {}
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
function encryptLegacySessionTokens(legacyRecord, tokenVault, options) {
|
|
2055
|
+
const subjectId = normalizeRequiredString(options.subjectId || legacyRecord.subjectId, "subjectId");
|
|
2056
|
+
const accessToken = normalizeRequiredString(legacyRecord.accessToken, "accessToken");
|
|
2057
|
+
const refreshToken = normalizeRequiredString(legacyRecord.refreshToken, "refreshToken");
|
|
2058
|
+
if (!options.allowPlaintext && !tokenVault.isEncrypted(accessToken) && !tokenVault.isEncrypted(refreshToken)) {
|
|
2059
|
+
throw new import_connect10.EdgeValidationError("Plaintext token migration requires allowPlaintext: true", {
|
|
2060
|
+
allowPlaintext: ["Set allowPlaintext only inside an explicit migration path"]
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
const scope = normalizeScopeString(legacyRecord.scope ?? legacyRecord.scopes);
|
|
2064
|
+
const tokens = {
|
|
2065
|
+
accessToken,
|
|
2066
|
+
refreshToken,
|
|
2067
|
+
...typeof legacyRecord.idToken === "string" && legacyRecord.idToken.trim() ? { idToken: legacyRecord.idToken.trim() } : {},
|
|
2068
|
+
expiresIn: normalizeExpiresIn(legacyRecord.expiresIn),
|
|
2069
|
+
expiresAt: normalizeSessionExpiresAt(legacyRecord.expiresAt),
|
|
2070
|
+
scope
|
|
2071
|
+
};
|
|
2072
|
+
return createSessionTokenRecord(subjectId, tokens, tokenVault, {
|
|
2073
|
+
scopes: splitScopes(scope),
|
|
2074
|
+
...legacyRecord.edgeUserId ? { edgeUserId: legacyRecord.edgeUserId } : {},
|
|
2075
|
+
...options.keyId ? { keyId: options.keyId } : {},
|
|
2076
|
+
...legacyRecord.metadata ? { metadata: legacyRecord.metadata } : {}
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
function normalizeSessionExpiresAt(value) {
|
|
2080
|
+
if (value instanceof Date) {
|
|
2081
|
+
const time = value.getTime();
|
|
2082
|
+
if (!Number.isFinite(time) || time <= 0) {
|
|
2083
|
+
throw new import_connect10.EdgeValidationError("Invalid EDGE session expiresAt", {
|
|
2084
|
+
expiresAt: ["Date must be valid"]
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
return time;
|
|
2088
|
+
}
|
|
2089
|
+
if (typeof value === "string") {
|
|
2090
|
+
const trimmed = value.trim();
|
|
2091
|
+
if (!trimmed) {
|
|
2092
|
+
throw new import_connect10.EdgeValidationError("Invalid EDGE session expiresAt", {
|
|
2093
|
+
expiresAt: ["Required"]
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
if (/^\d+$/.test(trimmed)) {
|
|
2097
|
+
return normalizeSessionExpiresAt(Number(trimmed));
|
|
2098
|
+
}
|
|
2099
|
+
const time = Date.parse(trimmed);
|
|
2100
|
+
if (!Number.isNaN(time) && time > 0) return time;
|
|
2101
|
+
}
|
|
2102
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2103
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2104
|
+
throw new import_connect10.EdgeValidationError("Invalid EDGE session expiresAt", {
|
|
2105
|
+
expiresAt: ["Must be a positive integer timestamp in milliseconds"]
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
if (value < 1e10) {
|
|
2109
|
+
throw new import_connect10.EdgeValidationError("Ambiguous EDGE session expiresAt", {
|
|
2110
|
+
expiresAt: ["Use Unix milliseconds, not seconds"]
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
return value;
|
|
2114
|
+
}
|
|
2115
|
+
throw new import_connect10.EdgeValidationError("Invalid EDGE session expiresAt", {
|
|
2116
|
+
expiresAt: ["Use Unix milliseconds, ISO string, or Date"]
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
function normalizeExpiresIn(value) {
|
|
2120
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
2121
|
+
if (typeof value === "string" && value.trim()) {
|
|
2122
|
+
const parsed = Number(value);
|
|
2123
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
2124
|
+
}
|
|
2125
|
+
return 600;
|
|
2126
|
+
}
|
|
2127
|
+
function normalizeScopeString(value) {
|
|
2128
|
+
if (Array.isArray(value)) return normalizeScopes(value).join(" ");
|
|
2129
|
+
if (typeof value === "string") return splitScopes(value).join(" ");
|
|
2130
|
+
return "";
|
|
2131
|
+
}
|
|
2132
|
+
function normalizeScopes(value) {
|
|
2133
|
+
if (Array.isArray(value)) {
|
|
2134
|
+
return value.map((scope) => typeof scope === "string" ? scope.trim() : "").filter(Boolean);
|
|
2135
|
+
}
|
|
2136
|
+
if (typeof value === "string") {
|
|
2137
|
+
return splitScopes(value);
|
|
2138
|
+
}
|
|
2139
|
+
throw new import_connect10.EdgeValidationError("Invalid EDGE session scopes", {
|
|
2140
|
+
scopes: ["Expected array or space-delimited string"]
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
function splitScopes(value) {
|
|
2144
|
+
return value.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
|
|
2145
|
+
}
|
|
2146
|
+
function normalizeRequiredString(value, field) {
|
|
2147
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2148
|
+
throw new import_connect10.EdgeValidationError("Invalid EDGE session token record", {
|
|
2149
|
+
[field]: ["Required string"]
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
return value.trim();
|
|
2153
|
+
}
|
|
2154
|
+
function validateTokens2(tokens) {
|
|
2155
|
+
normalizeRequiredString(tokens.accessToken, "accessToken");
|
|
2156
|
+
normalizeRequiredString(tokens.refreshToken, "refreshToken");
|
|
2157
|
+
normalizeSessionExpiresAt(tokens.expiresAt);
|
|
2158
|
+
normalizeExpiresIn(tokens.expiresIn);
|
|
2159
|
+
normalizeScopeString(tokens.scope);
|
|
2160
|
+
}
|
|
2161
|
+
function isRecord2(value) {
|
|
2162
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
1670
2165
|
// src/token-vault.ts
|
|
1671
|
-
var
|
|
1672
|
-
var
|
|
2166
|
+
var import_connect11 = require("@edge-markets/connect");
|
|
2167
|
+
var import_node_crypto3 = __toESM(require("crypto"));
|
|
1673
2168
|
var TOKEN_VAULT_VERSION = "edge_vault_v1";
|
|
1674
2169
|
var IV_BYTES = 12;
|
|
1675
2170
|
var TAG_BYTES = 16;
|
|
@@ -1681,7 +2176,7 @@ var EdgeTokenVault = class {
|
|
|
1681
2176
|
for (const [index, key] of (options.previousKeys ?? []).entries()) {
|
|
1682
2177
|
const normalized = normalizeVaultKey(key, `previousKeys[${index}]`);
|
|
1683
2178
|
if (this.keysById.has(normalized.id)) {
|
|
1684
|
-
throw new
|
|
2179
|
+
throw new import_connect11.EdgeValidationError("Duplicate token vault key ID", {
|
|
1685
2180
|
keyId: [`Duplicate key ID "${normalized.id}"`]
|
|
1686
2181
|
});
|
|
1687
2182
|
}
|
|
@@ -1690,9 +2185,9 @@ var EdgeTokenVault = class {
|
|
|
1690
2185
|
}
|
|
1691
2186
|
encryptTokens(tokens) {
|
|
1692
2187
|
validateEdgeTokens(tokens);
|
|
1693
|
-
const iv =
|
|
2188
|
+
const iv = import_node_crypto3.default.randomBytes(IV_BYTES);
|
|
1694
2189
|
const aad = Buffer.from(`${TOKEN_VAULT_VERSION}.${this.currentKey.id}`, "utf8");
|
|
1695
|
-
const cipher =
|
|
2190
|
+
const cipher = import_node_crypto3.default.createCipheriv("aes-256-gcm", this.currentKey.key, iv);
|
|
1696
2191
|
cipher.setAAD(aad);
|
|
1697
2192
|
const plaintext = Buffer.from(JSON.stringify(tokens), "utf8");
|
|
1698
2193
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
@@ -1709,12 +2204,12 @@ var EdgeTokenVault = class {
|
|
|
1709
2204
|
const parsed = parseEnvelope(envelope);
|
|
1710
2205
|
const key = this.keysById.get(parsed.keyId);
|
|
1711
2206
|
if (!key) {
|
|
1712
|
-
throw new
|
|
2207
|
+
throw new import_connect11.EdgeValidationError("Unknown token vault key ID", {
|
|
1713
2208
|
keyId: [`No key configured for "${parsed.keyId}"`]
|
|
1714
2209
|
});
|
|
1715
2210
|
}
|
|
1716
2211
|
try {
|
|
1717
|
-
const decipher =
|
|
2212
|
+
const decipher = import_node_crypto3.default.createDecipheriv("aes-256-gcm", key, parsed.iv);
|
|
1718
2213
|
decipher.setAAD(Buffer.from(`${TOKEN_VAULT_VERSION}.${parsed.keyId}`, "utf8"));
|
|
1719
2214
|
decipher.setAuthTag(parsed.tag);
|
|
1720
2215
|
const plaintext = Buffer.concat([decipher.update(parsed.ciphertext), decipher.final()]).toString("utf8");
|
|
@@ -1722,8 +2217,8 @@ var EdgeTokenVault = class {
|
|
|
1722
2217
|
validateEdgeTokens(tokens);
|
|
1723
2218
|
return tokens;
|
|
1724
2219
|
} catch (error) {
|
|
1725
|
-
if (error instanceof
|
|
1726
|
-
throw new
|
|
2220
|
+
if (error instanceof import_connect11.EdgeValidationError) throw error;
|
|
2221
|
+
throw new import_connect11.EdgeValidationError("Failed to decrypt EDGE tokens", {
|
|
1727
2222
|
tokenEnvelope: ["Envelope is malformed, corrupted, or encrypted with a different key"]
|
|
1728
2223
|
});
|
|
1729
2224
|
}
|
|
@@ -1741,7 +2236,7 @@ function isEdgeTokenVaultEnvelope(value) {
|
|
|
1741
2236
|
function normalizeVaultKey(input, fieldName) {
|
|
1742
2237
|
const id = typeof input.id === "string" ? input.id.trim() : "";
|
|
1743
2238
|
if (!id) {
|
|
1744
|
-
throw new
|
|
2239
|
+
throw new import_connect11.EdgeValidationError("Token vault key ID is required", {
|
|
1745
2240
|
[`${fieldName}.id`]: ["Required"]
|
|
1746
2241
|
});
|
|
1747
2242
|
}
|
|
@@ -1772,7 +2267,7 @@ function normalizeKeyMaterial(input, fieldName) {
|
|
|
1772
2267
|
}
|
|
1773
2268
|
const key = candidates.find((candidate) => candidate.length === KEY_BYTES);
|
|
1774
2269
|
if (!key) {
|
|
1775
|
-
throw new
|
|
2270
|
+
throw new import_connect11.EdgeValidationError("Token vault key must decode to 32 bytes", {
|
|
1776
2271
|
[fieldName]: ["Provide a 32-byte key as base64, base64url, hex, Buffer, or Uint8Array"]
|
|
1777
2272
|
});
|
|
1778
2273
|
}
|
|
@@ -1803,18 +2298,18 @@ function validateEdgeTokens(tokens) {
|
|
|
1803
2298
|
}
|
|
1804
2299
|
}
|
|
1805
2300
|
if (Object.keys(errors).length > 0) {
|
|
1806
|
-
throw new
|
|
2301
|
+
throw new import_connect11.EdgeValidationError("Invalid EDGE token payload", errors);
|
|
1807
2302
|
}
|
|
1808
2303
|
}
|
|
1809
2304
|
function parseEnvelope(envelope) {
|
|
1810
2305
|
if (typeof envelope !== "string") {
|
|
1811
|
-
throw new
|
|
2306
|
+
throw new import_connect11.EdgeValidationError("Token envelope must be a string", {
|
|
1812
2307
|
tokenEnvelope: ["Expected string"]
|
|
1813
2308
|
});
|
|
1814
2309
|
}
|
|
1815
2310
|
const parts = envelope.split(".");
|
|
1816
2311
|
if (parts.length !== 5 || parts[0] !== TOKEN_VAULT_VERSION) {
|
|
1817
|
-
throw new
|
|
2312
|
+
throw new import_connect11.EdgeValidationError("Invalid EDGE token vault envelope", {
|
|
1818
2313
|
tokenEnvelope: [`Expected ${TOKEN_VAULT_VERSION}.<keyId>.<iv>.<tag>.<ciphertext>`]
|
|
1819
2314
|
});
|
|
1820
2315
|
}
|
|
@@ -1829,7 +2324,7 @@ function parseEnvelope(envelope) {
|
|
|
1829
2324
|
if (tag.length !== TAG_BYTES) errors.tag = [`Must be ${TAG_BYTES} bytes`];
|
|
1830
2325
|
if (ciphertext.length === 0) errors.ciphertext = ["Required"];
|
|
1831
2326
|
if (Object.keys(errors).length > 0) {
|
|
1832
|
-
throw new
|
|
2327
|
+
throw new import_connect11.EdgeValidationError("Invalid EDGE token vault envelope", errors);
|
|
1833
2328
|
}
|
|
1834
2329
|
return { keyId, iv, tag, ciphertext };
|
|
1835
2330
|
}
|
|
@@ -1843,13 +2338,13 @@ function fromBase64Url2(value) {
|
|
|
1843
2338
|
}
|
|
1844
2339
|
|
|
1845
2340
|
// src/webhook-guards.ts
|
|
1846
|
-
var
|
|
2341
|
+
var import_connect12 = require("@edge-markets/connect");
|
|
1847
2342
|
function isTransferWebhookEvent(event) {
|
|
1848
2343
|
return event.type.startsWith("transfer.");
|
|
1849
2344
|
}
|
|
1850
2345
|
function assertTransferEventMatchesIntent(event, intent) {
|
|
1851
2346
|
if (!isTransferWebhookEvent(event)) {
|
|
1852
|
-
throw new
|
|
2347
|
+
throw new import_connect12.EdgeValidationError("Webhook event is not a transfer event", {
|
|
1853
2348
|
type: [`Received ${event.type}`]
|
|
1854
2349
|
});
|
|
1855
2350
|
}
|
|
@@ -1872,198 +2367,251 @@ function assertTransferEventMatchesIntent(event, intent) {
|
|
|
1872
2367
|
}
|
|
1873
2368
|
}
|
|
1874
2369
|
if (Object.keys(errors).length > 0) {
|
|
1875
|
-
throw new
|
|
2370
|
+
throw new import_connect12.EdgeValidationError("Webhook transfer event does not match expected transfer intent", errors);
|
|
1876
2371
|
}
|
|
1877
2372
|
}
|
|
2373
|
+
var assertWebhookMatchesTransfer = assertTransferEventMatchesIntent;
|
|
1878
2374
|
|
|
1879
|
-
// src/webhook-
|
|
1880
|
-
var
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
var
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
throw new import_connect10.EdgeAuthenticationError("Missing or malformed EDGE webhook signature");
|
|
1920
|
-
}
|
|
1921
|
-
if (!verifyWebhookSignature(header, body, options.secret, { toleranceSeconds: options.toleranceSeconds })) {
|
|
1922
|
-
throw new import_connect10.EdgeAuthenticationError("Invalid EDGE webhook signature");
|
|
1923
|
-
}
|
|
1924
|
-
let parsed;
|
|
1925
|
-
try {
|
|
1926
|
-
parsed = JSON.parse(body);
|
|
1927
|
-
} catch {
|
|
1928
|
-
throw new import_connect10.EdgeValidationError("EDGE webhook body must be valid JSON", {
|
|
1929
|
-
rawBody: ["Unable to parse JSON after signature verification"]
|
|
2375
|
+
// src/webhook-inbox.ts
|
|
2376
|
+
var import_connect13 = require("@edge-markets/connect");
|
|
2377
|
+
var import_node_crypto4 = __toESM(require("crypto"));
|
|
2378
|
+
var DEFAULT_PROCESSING_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2379
|
+
var EdgeWebhookInbox = class {
|
|
2380
|
+
constructor(options) {
|
|
2381
|
+
if (!options.store?.insert || !options.store?.find || !options.store?.markProcessing) {
|
|
2382
|
+
throw new import_connect13.EdgeValidationError("EdgeWebhookInbox store is incomplete", {
|
|
2383
|
+
store: ["Provide insert, find, markProcessing, markProcessed, and markFailed callbacks"]
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
if (!options.store.markProcessed || !options.store.markFailed) {
|
|
2387
|
+
throw new import_connect13.EdgeValidationError("EdgeWebhookInbox store is incomplete", {
|
|
2388
|
+
store: ["Provide markProcessed and markFailed callbacks"]
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
if (!options.processEvent) {
|
|
2392
|
+
throw new import_connect13.EdgeValidationError("EdgeWebhookInbox processEvent callback is required", {
|
|
2393
|
+
processEvent: ["Required"]
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
if (options.processingTimeoutMs !== void 0 && (!Number.isFinite(options.processingTimeoutMs) || options.processingTimeoutMs <= 0)) {
|
|
2397
|
+
throw new import_connect13.EdgeValidationError("processingTimeoutMs must be positive", {
|
|
2398
|
+
processingTimeoutMs: ["Must be greater than 0"]
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
this.store = options.store;
|
|
2402
|
+
this.processEvent = options.processEvent;
|
|
2403
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2404
|
+
this.storeRawBody = options.storeRawBody ?? false;
|
|
2405
|
+
this.processingTimeoutMs = options.processingTimeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
|
|
2406
|
+
this.onEvent = options.onEvent;
|
|
2407
|
+
}
|
|
2408
|
+
async accept(parsed) {
|
|
2409
|
+
return this.acceptEvent(parsed.event, {
|
|
2410
|
+
source: "live_webhook",
|
|
2411
|
+
receivedAt: parsed.receivedAt,
|
|
2412
|
+
signatureTimestamp: parsed.signatureTimestamp,
|
|
2413
|
+
signatureHeader: void 0,
|
|
2414
|
+
rawBody: void 0
|
|
1930
2415
|
});
|
|
1931
2416
|
}
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
};
|
|
1940
|
-
}
|
|
1941
|
-
function extractWebhookSignatureTimestamp(header) {
|
|
1942
|
-
const normalizedHeader = normalizeSignatureHeader(header);
|
|
1943
|
-
if (!normalizedHeader) return void 0;
|
|
1944
|
-
const part = normalizedHeader.split(",").map((value) => value.trim()).find((value) => value.startsWith("t="));
|
|
1945
|
-
if (!part) return void 0;
|
|
1946
|
-
const timestamp = Number(part.slice(2));
|
|
1947
|
-
if (!Number.isInteger(timestamp) || timestamp <= 0) return void 0;
|
|
1948
|
-
return timestamp;
|
|
1949
|
-
}
|
|
1950
|
-
function isEdgeWebhookEvent(value) {
|
|
1951
|
-
try {
|
|
1952
|
-
validateEdgeWebhookEvent(value);
|
|
1953
|
-
return true;
|
|
1954
|
-
} catch {
|
|
1955
|
-
return false;
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
function validateEdgeWebhookEvent(value) {
|
|
1959
|
-
const errors = {};
|
|
1960
|
-
if (!isRecord(value)) {
|
|
1961
|
-
throw new import_connect10.EdgeValidationError("EDGE webhook event must be an object", {
|
|
1962
|
-
event: ["Expected JSON object"]
|
|
2417
|
+
async acceptRaw(parsed, rawBody, signatureHeader) {
|
|
2418
|
+
return this.acceptEvent(parsed.event, {
|
|
2419
|
+
source: "live_webhook",
|
|
2420
|
+
receivedAt: parsed.receivedAt,
|
|
2421
|
+
signatureTimestamp: parsed.signatureTimestamp,
|
|
2422
|
+
signatureHeader,
|
|
2423
|
+
rawBody: this.storeRawBody ? rawBody : void 0
|
|
1963
2424
|
});
|
|
1964
2425
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
errors.type = [`Must be one of: ${import_connect10.EDGE_WEBHOOK_EVENT_TYPES.join(", ")}`];
|
|
1974
|
-
}
|
|
1975
|
-
if (typeof createdAt !== "string" || !createdAt.trim() || Number.isNaN(Date.parse(createdAt))) {
|
|
1976
|
-
errors.created_at = ["Required ISO 8601 timestamp string"];
|
|
1977
|
-
}
|
|
1978
|
-
if (!isRecord(data)) {
|
|
1979
|
-
errors.data = ["Required object"];
|
|
2426
|
+
async acceptSyncedEvent(event, receivedAt = this.now()) {
|
|
2427
|
+
return this.acceptEvent(event, {
|
|
2428
|
+
source: "sync",
|
|
2429
|
+
receivedAt,
|
|
2430
|
+
signatureTimestamp: null,
|
|
2431
|
+
signatureHeader: null,
|
|
2432
|
+
rawBody: null
|
|
2433
|
+
});
|
|
1980
2434
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
2435
|
+
async process(eventId, options = {}) {
|
|
2436
|
+
const normalizedEventId = normalizeEventId(eventId);
|
|
2437
|
+
const existing = await this.store.find(normalizedEventId);
|
|
2438
|
+
if (!existing) {
|
|
2439
|
+
return { eventId: normalizedEventId, status: "failed", processed: false, skipped: true, reason: "not_found" };
|
|
2440
|
+
}
|
|
2441
|
+
if (existing.status === "processed" && !options.force) {
|
|
2442
|
+
return {
|
|
2443
|
+
eventId: normalizedEventId,
|
|
2444
|
+
status: existing.status,
|
|
2445
|
+
processed: false,
|
|
2446
|
+
skipped: true,
|
|
2447
|
+
reason: "already_processed",
|
|
2448
|
+
record: existing
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
const now = this.now();
|
|
2452
|
+
const processing = await this.store.markProcessing(normalizedEventId, { now });
|
|
2453
|
+
if (!processing) {
|
|
2454
|
+
return {
|
|
2455
|
+
eventId: normalizedEventId,
|
|
2456
|
+
status: existing.status,
|
|
2457
|
+
processed: false,
|
|
2458
|
+
skipped: true,
|
|
2459
|
+
reason: "locked",
|
|
2460
|
+
record: existing
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
await this.emit({
|
|
2464
|
+
type: "webhook.processing_started",
|
|
2465
|
+
eventId: normalizedEventId,
|
|
2466
|
+
eventType: processing.eventType,
|
|
2467
|
+
status: "processing",
|
|
2468
|
+
source: processing.source,
|
|
2469
|
+
attempts: processing.attempts
|
|
2470
|
+
});
|
|
2471
|
+
try {
|
|
2472
|
+
await this.processEvent(processing.event, processing);
|
|
2473
|
+
const processed = await this.store.markProcessed(normalizedEventId, { now: this.now() }) ?? processing;
|
|
2474
|
+
await this.emit({
|
|
2475
|
+
type: "webhook.processing_succeeded",
|
|
2476
|
+
eventId: normalizedEventId,
|
|
2477
|
+
eventType: processing.eventType,
|
|
2478
|
+
status: "processed",
|
|
2479
|
+
source: processing.source,
|
|
2480
|
+
attempts: processed.attempts
|
|
2481
|
+
});
|
|
2482
|
+
return {
|
|
2483
|
+
eventId: normalizedEventId,
|
|
2484
|
+
status: "processed",
|
|
2485
|
+
processed: true,
|
|
2486
|
+
skipped: false,
|
|
2487
|
+
record: processed
|
|
2488
|
+
};
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
const message = error instanceof Error ? error.message : "Unknown webhook processing error";
|
|
2491
|
+
const failed = await this.store.markFailed(normalizedEventId, { now: this.now(), error: message }) ?? processing;
|
|
2492
|
+
await this.emit({
|
|
2493
|
+
type: "webhook.processing_failed",
|
|
2494
|
+
eventId: normalizedEventId,
|
|
2495
|
+
eventType: processing.eventType,
|
|
2496
|
+
status: "failed",
|
|
2497
|
+
source: processing.source,
|
|
2498
|
+
attempts: failed.attempts,
|
|
2499
|
+
error: message
|
|
2500
|
+
});
|
|
2501
|
+
throw error;
|
|
2502
|
+
}
|
|
1983
2503
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
2504
|
+
async replay(eventId) {
|
|
2505
|
+
return this.process(eventId, { force: true });
|
|
1986
2506
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2507
|
+
async recoverStaleProcessing() {
|
|
2508
|
+
if (!this.store.list) {
|
|
2509
|
+
return { recovered: 0, skipped: true, reason: "list_not_configured" };
|
|
2510
|
+
}
|
|
2511
|
+
if (!this.store.markPending) {
|
|
2512
|
+
return { recovered: 0, skipped: true, reason: "mark_pending_not_configured" };
|
|
2513
|
+
}
|
|
2514
|
+
const updatedBefore = new Date(this.now().getTime() - this.processingTimeoutMs);
|
|
2515
|
+
const records = await this.store.list({ status: "processing", updatedBefore, limit: 100 });
|
|
2516
|
+
let recovered = 0;
|
|
2517
|
+
for (const record of records) {
|
|
2518
|
+
const next = await this.store.markPending(record.eventId, {
|
|
2519
|
+
now: this.now(),
|
|
2520
|
+
reason: "stale_processing_recovery"
|
|
2521
|
+
});
|
|
2522
|
+
if (next) {
|
|
2523
|
+
recovered++;
|
|
2524
|
+
await this.emit({
|
|
2525
|
+
type: "webhook.recovered_stale",
|
|
2526
|
+
eventId: record.eventId,
|
|
2527
|
+
eventType: record.eventType,
|
|
2528
|
+
status: next.status,
|
|
2529
|
+
source: record.source,
|
|
2530
|
+
attempts: next.attempts
|
|
2531
|
+
});
|
|
1998
2532
|
}
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2533
|
+
}
|
|
2534
|
+
return { recovered, skipped: false };
|
|
2535
|
+
}
|
|
2536
|
+
async acceptEvent(event, options) {
|
|
2537
|
+
const payloadFingerprint = fingerprintWebhookEvent(event);
|
|
2538
|
+
const result = await this.store.insert({
|
|
2539
|
+
eventId: event.id,
|
|
2540
|
+
eventType: event.type,
|
|
2541
|
+
event,
|
|
2542
|
+
payloadFingerprint,
|
|
2543
|
+
source: options.source,
|
|
2544
|
+
receivedAt: options.receivedAt,
|
|
2545
|
+
signatureTimestamp: options.signatureTimestamp ?? null,
|
|
2546
|
+
signatureHeader: options.signatureHeader ?? null,
|
|
2547
|
+
rawBody: options.rawBody ?? null
|
|
2548
|
+
});
|
|
2549
|
+
if (result.record.payloadFingerprint !== payloadFingerprint) {
|
|
2550
|
+
await this.emit({
|
|
2551
|
+
type: "webhook.payload_mismatch",
|
|
2552
|
+
eventId: event.id,
|
|
2553
|
+
eventType: event.type,
|
|
2554
|
+
status: result.record.status,
|
|
2555
|
+
source: options.source
|
|
2556
|
+
});
|
|
2557
|
+
throw new import_connect13.EdgeValidationError("Duplicate webhook event payload does not match stored event", {
|
|
2558
|
+
eventId: [event.id]
|
|
2015
2559
|
});
|
|
2016
2560
|
}
|
|
2561
|
+
await this.emit({
|
|
2562
|
+
type: result.inserted ? "webhook.accepted" : "webhook.duplicate",
|
|
2563
|
+
eventId: event.id,
|
|
2564
|
+
eventType: event.type,
|
|
2565
|
+
status: result.record.status,
|
|
2566
|
+
source: options.source,
|
|
2567
|
+
attempts: result.record.attempts
|
|
2568
|
+
});
|
|
2569
|
+
return {
|
|
2570
|
+
eventId: event.id,
|
|
2571
|
+
eventType: event.type,
|
|
2572
|
+
inserted: result.inserted,
|
|
2573
|
+
duplicate: !result.inserted,
|
|
2574
|
+
status: result.record.status,
|
|
2575
|
+
record: result.record
|
|
2576
|
+
};
|
|
2017
2577
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
}
|
|
2024
|
-
if (typeof data.type !== "string" || !TRANSFER_TYPES.has(data.type)) {
|
|
2025
|
-
errors["data.type"] = ['Must be "debit" or "credit"'];
|
|
2578
|
+
async emit(event) {
|
|
2579
|
+
try {
|
|
2580
|
+
await this.onEvent?.(event);
|
|
2581
|
+
} catch {
|
|
2582
|
+
}
|
|
2026
2583
|
}
|
|
2027
|
-
|
|
2584
|
+
};
|
|
2585
|
+
function createWebhookInbox(options) {
|
|
2586
|
+
return new EdgeWebhookInbox(options);
|
|
2028
2587
|
}
|
|
2029
|
-
function
|
|
2030
|
-
|
|
2031
|
-
errors[`data.${field}`] = ["Required string"];
|
|
2032
|
-
}
|
|
2588
|
+
function fingerprintWebhookEvent(event) {
|
|
2589
|
+
return import_node_crypto4.default.createHash("sha256").update(stableStringify(event)).digest("hex");
|
|
2033
2590
|
}
|
|
2034
|
-
function
|
|
2035
|
-
if (typeof
|
|
2036
|
-
|
|
2591
|
+
function normalizeEventId(eventId) {
|
|
2592
|
+
if (typeof eventId !== "string" || !eventId.trim()) {
|
|
2593
|
+
throw new import_connect13.EdgeValidationError("Webhook event ID is required", {
|
|
2594
|
+
eventId: ["Required"]
|
|
2595
|
+
});
|
|
2037
2596
|
}
|
|
2597
|
+
return eventId.trim();
|
|
2038
2598
|
}
|
|
2039
|
-
function
|
|
2040
|
-
if (
|
|
2041
|
-
return
|
|
2599
|
+
function stableStringify(value) {
|
|
2600
|
+
if (Array.isArray(value)) {
|
|
2601
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
2042
2602
|
}
|
|
2043
|
-
if (
|
|
2044
|
-
return
|
|
2603
|
+
if (value && typeof value === "object") {
|
|
2604
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
2045
2605
|
}
|
|
2046
|
-
|
|
2047
|
-
rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
|
|
2048
|
-
});
|
|
2049
|
-
}
|
|
2050
|
-
function normalizeSignatureHeader(header) {
|
|
2051
|
-
if (Array.isArray(header)) {
|
|
2052
|
-
return header.map((value) => value.trim()).filter(Boolean).join(",");
|
|
2053
|
-
}
|
|
2054
|
-
if (typeof header !== "string") return void 0;
|
|
2055
|
-
const trimmed = header.trim();
|
|
2056
|
-
return trimmed || void 0;
|
|
2057
|
-
}
|
|
2058
|
-
function isRecord(value) {
|
|
2059
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2606
|
+
return JSON.stringify(value);
|
|
2060
2607
|
}
|
|
2061
2608
|
|
|
2062
2609
|
// src/index.ts
|
|
2063
|
-
var
|
|
2064
|
-
var
|
|
2610
|
+
var import_connect14 = require("@edge-markets/connect");
|
|
2611
|
+
var import_connect15 = require("@edge-markets/connect");
|
|
2065
2612
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2066
2613
|
0 && (module.exports = {
|
|
2614
|
+
CONNECT_TRANSFER_DIRECTIONS,
|
|
2067
2615
|
EDGE_WEBHOOK_EVENT_TYPES,
|
|
2068
2616
|
EdgeApiError,
|
|
2069
2617
|
EdgeAuthenticationError,
|
|
@@ -2079,18 +2627,28 @@ var import_connect12 = require("@edge-markets/connect");
|
|
|
2079
2627
|
EdgeUserClient,
|
|
2080
2628
|
EdgeUserSession,
|
|
2081
2629
|
EdgeValidationError,
|
|
2630
|
+
EdgeWebhookInbox,
|
|
2082
2631
|
EdgeWebhookReconciler,
|
|
2083
2632
|
TRANSFER_CATEGORIES,
|
|
2084
2633
|
assertSameTransferIntent,
|
|
2085
2634
|
assertTransferEventMatchesIntent,
|
|
2635
|
+
assertWebhookMatchesTransfer,
|
|
2086
2636
|
buildVerifyIdentityPayload,
|
|
2087
2637
|
createEdgeConnectServerFromEnv,
|
|
2088
2638
|
createEdgeTokenVault,
|
|
2089
2639
|
createEdgeUserSession,
|
|
2640
|
+
createSessionTokenRecord,
|
|
2090
2641
|
createTransferIdempotencyKey,
|
|
2642
|
+
createWebhookInbox,
|
|
2091
2643
|
createWebhookReconciler,
|
|
2644
|
+
encryptLegacySessionTokens,
|
|
2092
2645
|
evaluateIdentityResult,
|
|
2093
2646
|
extractWebhookSignatureTimestamp,
|
|
2647
|
+
fingerprintTransferIntent,
|
|
2648
|
+
fingerprintWebhookEvent,
|
|
2649
|
+
getConnectTransferDirection,
|
|
2650
|
+
getEdgeWebhookRawBody,
|
|
2651
|
+
getEdgeWebhookSignatureHeader,
|
|
2094
2652
|
getEnvironmentConfig,
|
|
2095
2653
|
isApiError,
|
|
2096
2654
|
isAuthenticationError,
|
|
@@ -2102,8 +2660,13 @@ var import_connect12 = require("@edge-markets/connect");
|
|
|
2102
2660
|
isNetworkError,
|
|
2103
2661
|
isProductionEnvironment,
|
|
2104
2662
|
isTransferWebhookEvent,
|
|
2663
|
+
mapTransferStatusToPartnerState,
|
|
2664
|
+
moneyAmountsEqual,
|
|
2105
2665
|
normalizeMoneyAmount,
|
|
2666
|
+
normalizeSessionExpiresAt,
|
|
2106
2667
|
parseAndVerifyWebhook,
|
|
2668
|
+
parseSessionTokenRecord,
|
|
2669
|
+
parseWebhookHttpRequest,
|
|
2107
2670
|
startTransferVerification,
|
|
2108
2671
|
toEdgeHttpError,
|
|
2109
2672
|
toEdgeProblemJson,
|