@edge-markets/connect-node 1.8.0 → 1.10.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 +149 -25
- package/dist/index.d.mts +306 -96
- package/dist/index.d.ts +306 -96
- package/dist/index.js +901 -287
- package/dist/index.mjs +814 -214
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -19,23 +19,39 @@ import {
|
|
|
19
19
|
getEnvironmentConfig
|
|
20
20
|
} from "@edge-markets/connect";
|
|
21
21
|
|
|
22
|
+
// src/edge-user-client.ts
|
|
23
|
+
import { EdgeFeatureUnavailableError as EdgeFeatureUnavailableError2 } from "@edge-markets/connect";
|
|
24
|
+
|
|
22
25
|
// src/transfer-helpers.ts
|
|
23
26
|
import {
|
|
27
|
+
EdgeFeatureUnavailableError,
|
|
24
28
|
EdgeValidationError
|
|
25
29
|
} from "@edge-markets/connect";
|
|
26
30
|
import crypto from "crypto";
|
|
27
31
|
var DEFAULT_MIN_AMOUNT = "1.00";
|
|
28
32
|
var DEFAULT_MAX_AMOUNT = "10000.00";
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
var CONNECT_TRANSFER_DIRECTIONS = {
|
|
34
|
+
debit: "user_to_partner",
|
|
35
|
+
credit: "partner_to_user"
|
|
36
|
+
};
|
|
37
|
+
function getConnectTransferDirection(type) {
|
|
38
|
+
if (type !== "debit" && type !== "credit") {
|
|
39
|
+
throw new EdgeValidationError("Invalid Connect transfer type", {
|
|
40
|
+
type: ['Must be "debit" or "credit"']
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return CONNECT_TRANSFER_DIRECTIONS[type];
|
|
44
|
+
}
|
|
45
|
+
function normalizeMoneyAmount(value, options = {}) {
|
|
46
|
+
const cents = parseMoneyToCents(value, "amount", options);
|
|
31
47
|
const dollars = cents / 100n;
|
|
32
48
|
const remainder = cents % 100n;
|
|
33
49
|
return `${dollars.toString()}.${remainder.toString().padStart(2, "0")}`;
|
|
34
50
|
}
|
|
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");
|
|
51
|
+
function validateTransferAmount(value, limits = {}, options = {}) {
|
|
52
|
+
const amount = parseMoneyToCents(value, "amount", options);
|
|
53
|
+
const min = parseMoneyToCents(limits.min ?? DEFAULT_MIN_AMOUNT, "min", { allowNumber: true });
|
|
54
|
+
const max = parseMoneyToCents(limits.max ?? DEFAULT_MAX_AMOUNT, "max", { allowNumber: true });
|
|
39
55
|
const errors = {};
|
|
40
56
|
if (min <= 0n) errors.min = ["Minimum amount must be greater than 0"];
|
|
41
57
|
if (max < min) errors.max = ["Maximum amount must be greater than or equal to minimum amount"];
|
|
@@ -83,7 +99,29 @@ function assertSameTransferIntent(existing, requested) {
|
|
|
83
99
|
throw new EdgeValidationError("Transfer intent does not match existing idempotent transfer", errors);
|
|
84
100
|
}
|
|
85
101
|
}
|
|
102
|
+
function fingerprintTransferIntent(intent) {
|
|
103
|
+
return crypto.createHash("sha256").update(
|
|
104
|
+
JSON.stringify({
|
|
105
|
+
type: intent.type,
|
|
106
|
+
amount: normalizeMoneyAmount(intent.amount),
|
|
107
|
+
category: intent.category ?? null
|
|
108
|
+
})
|
|
109
|
+
).digest("hex");
|
|
110
|
+
}
|
|
111
|
+
function moneyAmountsEqual(left, right, options = {}) {
|
|
112
|
+
return normalizeMoneyAmount(left, options) === normalizeMoneyAmount(right, options);
|
|
113
|
+
}
|
|
114
|
+
function mapTransferStatusToPartnerState(status, mapping) {
|
|
115
|
+
const partnerState = mapping[status];
|
|
116
|
+
if (!partnerState) {
|
|
117
|
+
throw new EdgeValidationError("Unsupported transfer status", {
|
|
118
|
+
status: [`Unsupported status: ${status}`]
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return partnerState;
|
|
122
|
+
}
|
|
86
123
|
async function startTransferVerification(client, options) {
|
|
124
|
+
assertTransferFeatureAvailable();
|
|
87
125
|
const origin = typeof options.origin === "string" ? options.origin.trim() : "";
|
|
88
126
|
if (!origin) {
|
|
89
127
|
throw new EdgeValidationError("Verification origin is required", {
|
|
@@ -103,9 +141,22 @@ async function startTransferVerification(client, options) {
|
|
|
103
141
|
const verificationSession = await client.createVerificationSession(transferId, { origin });
|
|
104
142
|
return { transfer, verificationSession };
|
|
105
143
|
}
|
|
106
|
-
function
|
|
144
|
+
function assertTransferFeatureAvailable() {
|
|
145
|
+
if (!isTransferFeatureAvailable()) {
|
|
146
|
+
throw new EdgeFeatureUnavailableError();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function isTransferFeatureAvailable() {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
function parseMoneyToCents(value, field, options = {}) {
|
|
107
153
|
let input;
|
|
108
154
|
if (typeof value === "number") {
|
|
155
|
+
if (options.allowNumber === false) {
|
|
156
|
+
throw new EdgeValidationError("Money amount must be a string in strict mode", {
|
|
157
|
+
[field]: ['Pass a decimal string like "25.00"']
|
|
158
|
+
});
|
|
159
|
+
}
|
|
109
160
|
if (!Number.isFinite(value)) {
|
|
110
161
|
throw new EdgeValidationError("Money amount must be finite", { [field]: ["Must be a finite number"] });
|
|
111
162
|
}
|
|
@@ -219,6 +270,7 @@ var EdgeUserClient = class {
|
|
|
219
270
|
return this.server._apiRequest("GET", "/balance", this.accessToken);
|
|
220
271
|
}
|
|
221
272
|
async initiateTransfer(options) {
|
|
273
|
+
assertTransferFeatureAvailable2();
|
|
222
274
|
validateTransferOptions(options);
|
|
223
275
|
const body = {
|
|
224
276
|
type: options.type,
|
|
@@ -231,13 +283,16 @@ var EdgeUserClient = class {
|
|
|
231
283
|
return this.server._apiRequest("POST", "/transfer", this.accessToken, body);
|
|
232
284
|
}
|
|
233
285
|
async startTransferVerification(options) {
|
|
286
|
+
assertTransferFeatureAvailable2();
|
|
234
287
|
return startTransferVerification(this, options);
|
|
235
288
|
}
|
|
236
289
|
async getTransfer(transferId) {
|
|
290
|
+
assertTransferFeatureAvailable2();
|
|
237
291
|
validateTransferId(transferId);
|
|
238
292
|
return this.server._apiRequest("GET", `/transfer/${encodeURIComponent(transferId)}`, this.accessToken);
|
|
239
293
|
}
|
|
240
294
|
async listTransfers(params) {
|
|
295
|
+
assertTransferFeatureAvailable2();
|
|
241
296
|
const queryParams = new URLSearchParams();
|
|
242
297
|
if (params?.limit) queryParams.set("limit", String(params.limit));
|
|
243
298
|
if (params?.offset) queryParams.set("offset", String(params.offset));
|
|
@@ -250,11 +305,11 @@ var EdgeUserClient = class {
|
|
|
250
305
|
return this.server._apiRequest("DELETE", "/consent", this.accessToken);
|
|
251
306
|
}
|
|
252
307
|
/**
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* The handoff token is single-use and expires in 120 seconds.
|
|
308
|
+
* Reserved for a future transfer release.
|
|
309
|
+
* Currently throws EdgeFeatureUnavailableError before validation or network I/O.
|
|
256
310
|
*/
|
|
257
311
|
async createVerificationSession(transferId, options) {
|
|
312
|
+
assertTransferFeatureAvailable2();
|
|
258
313
|
validateTransferId(transferId);
|
|
259
314
|
return this.server._apiRequest(
|
|
260
315
|
"POST",
|
|
@@ -264,10 +319,11 @@ var EdgeUserClient = class {
|
|
|
264
319
|
);
|
|
265
320
|
}
|
|
266
321
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
322
|
+
* Reserved for a future transfer release.
|
|
323
|
+
* Currently throws EdgeFeatureUnavailableError before validation or network I/O.
|
|
269
324
|
*/
|
|
270
325
|
async getVerificationSessionStatus(transferId, sessionId) {
|
|
326
|
+
assertTransferFeatureAvailable2();
|
|
271
327
|
validateTransferId(transferId);
|
|
272
328
|
return this.server._apiRequest(
|
|
273
329
|
"GET",
|
|
@@ -276,6 +332,14 @@ var EdgeUserClient = class {
|
|
|
276
332
|
);
|
|
277
333
|
}
|
|
278
334
|
};
|
|
335
|
+
function assertTransferFeatureAvailable2() {
|
|
336
|
+
if (!isTransferFeatureAvailable2()) {
|
|
337
|
+
throw new EdgeFeatureUnavailableError2();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function isTransferFeatureAvailable2() {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
279
343
|
|
|
280
344
|
// src/mle.ts
|
|
281
345
|
import { randomUUID, randomBytes, createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from "crypto";
|
|
@@ -417,6 +481,7 @@ function validateMtlsConfig(config) {
|
|
|
417
481
|
import { EdgeAuthenticationError, EdgeValidationError as EdgeValidationError3 } from "@edge-markets/connect";
|
|
418
482
|
var EdgeUserSession = class {
|
|
419
483
|
constructor(options) {
|
|
484
|
+
this.refreshPromise = null;
|
|
420
485
|
if (!options.edge?.forUser || !options.edge?.refreshTokens) {
|
|
421
486
|
throw new EdgeValidationError3("EdgeUserSession requires an EdgeConnectServer instance", {
|
|
422
487
|
edge: ["Provide edge.forUser and edge.refreshTokens"]
|
|
@@ -445,29 +510,44 @@ var EdgeUserSession = class {
|
|
|
445
510
|
this.refreshSkewMs = options.refreshSkewMs ?? 6e4;
|
|
446
511
|
this.now = options.now ?? Date.now;
|
|
447
512
|
this.onRefresh = options.onRefresh;
|
|
513
|
+
this.onEvent = options.onEvent;
|
|
514
|
+
this.singleFlightRefresh = options.singleFlightRefresh ?? true;
|
|
448
515
|
}
|
|
449
516
|
async getValidTokens(options = {}) {
|
|
450
517
|
const tokens = await this.loadTokens();
|
|
451
518
|
if (!options.forceRefresh && tokens.expiresAt > this.now() + this.refreshSkewMs) {
|
|
452
519
|
return tokens;
|
|
453
520
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
521
|
+
if (this.singleFlightRefresh) {
|
|
522
|
+
if (!this.refreshPromise) {
|
|
523
|
+
this.refreshPromise = this.refreshTokens(tokens).finally(() => {
|
|
524
|
+
this.refreshPromise = null;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
return this.refreshPromise;
|
|
528
|
+
}
|
|
529
|
+
return this.refreshTokens(tokens);
|
|
458
530
|
}
|
|
459
531
|
async getClient(options = {}) {
|
|
460
532
|
const tokens = await this.getValidTokens(options);
|
|
461
533
|
return this.edge.forUser(tokens.accessToken);
|
|
462
534
|
}
|
|
463
|
-
async withClient(callback) {
|
|
464
|
-
const tokens = await this.getValidTokens();
|
|
535
|
+
async withClient(callback, options = {}) {
|
|
536
|
+
const tokens = await this.getValidTokens(options);
|
|
465
537
|
return callback(this.edge.forUser(tokens.accessToken), tokens);
|
|
466
538
|
}
|
|
467
539
|
async saveTokens(tokens, refreshed = false) {
|
|
468
540
|
validateTokens(tokens);
|
|
469
541
|
const value = this.tokenVault ? this.tokenVault.encryptTokens(tokens) : tokens;
|
|
470
542
|
await this.tokenStore.save(this.subjectId, value, { tokens, refreshed });
|
|
543
|
+
await this.emit({
|
|
544
|
+
type: "session.saved",
|
|
545
|
+
subjectId: this.subjectId,
|
|
546
|
+
refreshed,
|
|
547
|
+
encrypted: typeof value === "string",
|
|
548
|
+
expiresAt: tokens.expiresAt,
|
|
549
|
+
scope: tokens.scope
|
|
550
|
+
});
|
|
471
551
|
}
|
|
472
552
|
async loadTokens() {
|
|
473
553
|
const value = await this.tokenStore.load(this.subjectId);
|
|
@@ -484,11 +564,60 @@ var EdgeUserSession = class {
|
|
|
484
564
|
}
|
|
485
565
|
const tokens = this.tokenVault.decryptTokens(value);
|
|
486
566
|
validateTokens(tokens);
|
|
567
|
+
await this.emitLoaded(tokens, true);
|
|
487
568
|
return tokens;
|
|
488
569
|
}
|
|
489
570
|
validateTokens(value);
|
|
571
|
+
await this.emitLoaded(value, false);
|
|
490
572
|
return value;
|
|
491
573
|
}
|
|
574
|
+
async refreshTokens(currentTokens) {
|
|
575
|
+
await this.emit({
|
|
576
|
+
type: "session.refresh_started",
|
|
577
|
+
subjectId: this.subjectId,
|
|
578
|
+
expiresAt: currentTokens.expiresAt,
|
|
579
|
+
scope: currentTokens.scope
|
|
580
|
+
});
|
|
581
|
+
try {
|
|
582
|
+
const refreshed = await this.edge.refreshTokens(currentTokens.refreshToken);
|
|
583
|
+
const tokens = {
|
|
584
|
+
...refreshed,
|
|
585
|
+
refreshToken: refreshed.refreshToken || currentTokens.refreshToken
|
|
586
|
+
};
|
|
587
|
+
await this.saveTokens(tokens, true);
|
|
588
|
+
await this.onRefresh?.(tokens);
|
|
589
|
+
await this.emit({
|
|
590
|
+
type: "session.refresh_succeeded",
|
|
591
|
+
subjectId: this.subjectId,
|
|
592
|
+
refreshed: true,
|
|
593
|
+
expiresAt: tokens.expiresAt,
|
|
594
|
+
scope: tokens.scope
|
|
595
|
+
});
|
|
596
|
+
return tokens;
|
|
597
|
+
} catch (error) {
|
|
598
|
+
await this.emit({
|
|
599
|
+
type: "session.refresh_failed",
|
|
600
|
+
subjectId: this.subjectId,
|
|
601
|
+
error: error instanceof Error ? error.message : "Unknown token refresh error"
|
|
602
|
+
});
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async emitLoaded(tokens, encrypted) {
|
|
607
|
+
await this.emit({
|
|
608
|
+
type: "session.loaded",
|
|
609
|
+
subjectId: this.subjectId,
|
|
610
|
+
encrypted,
|
|
611
|
+
expiresAt: tokens.expiresAt,
|
|
612
|
+
scope: tokens.scope
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async emit(event) {
|
|
616
|
+
try {
|
|
617
|
+
await this.onEvent?.(event);
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
}
|
|
492
621
|
};
|
|
493
622
|
function createEdgeUserSession(options) {
|
|
494
623
|
return new EdgeUserSession(options);
|
|
@@ -540,8 +669,8 @@ var EdgeWebhookReconciler = class {
|
|
|
540
669
|
if (!options.cursorStore?.load || !options.cursorStore?.save) {
|
|
541
670
|
throw new Error("EdgeWebhookReconciler: cursorStore.load and cursorStore.save are required");
|
|
542
671
|
}
|
|
543
|
-
if (!options.processEvent) {
|
|
544
|
-
throw new Error("EdgeWebhookReconciler: processEvent is required");
|
|
672
|
+
if (!options.processEvent && !options.inbox) {
|
|
673
|
+
throw new Error("EdgeWebhookReconciler: processEvent or inbox is required");
|
|
545
674
|
}
|
|
546
675
|
if (options.maxPages !== void 0 && (!Number.isInteger(options.maxPages) || options.maxPages <= 0)) {
|
|
547
676
|
throw new Error("EdgeWebhookReconciler: maxPages must be a positive integer");
|
|
@@ -568,6 +697,8 @@ var EdgeWebhookReconciler = class {
|
|
|
568
697
|
async runInternal() {
|
|
569
698
|
let cursor = normalizeCursor(await this.options.cursorStore.load());
|
|
570
699
|
let processed = 0;
|
|
700
|
+
let accepted = 0;
|
|
701
|
+
let duplicates = 0;
|
|
571
702
|
let pages = 0;
|
|
572
703
|
let hasMore = false;
|
|
573
704
|
const maxPages = this.options.maxPages ?? DEFAULT_MAX_PAGES;
|
|
@@ -581,6 +712,7 @@ var EdgeWebhookReconciler = class {
|
|
|
581
712
|
if (result.events.length === 0) {
|
|
582
713
|
return {
|
|
583
714
|
processed,
|
|
715
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
584
716
|
pages,
|
|
585
717
|
...cursor ? { cursor } : {},
|
|
586
718
|
hasMore,
|
|
@@ -589,14 +721,23 @@ var EdgeWebhookReconciler = class {
|
|
|
589
721
|
};
|
|
590
722
|
}
|
|
591
723
|
for (const event of result.events) {
|
|
592
|
-
|
|
724
|
+
if (this.options.inbox) {
|
|
725
|
+
const acceptResult = await this.options.inbox.acceptSyncedEvent(event);
|
|
726
|
+
accepted++;
|
|
727
|
+
if (acceptResult.duplicate) duplicates++;
|
|
728
|
+
const processResult = await this.options.inbox.process(event.id);
|
|
729
|
+
if (processResult.processed) processed++;
|
|
730
|
+
} else {
|
|
731
|
+
await this.options.processEvent?.(event);
|
|
732
|
+
processed++;
|
|
733
|
+
}
|
|
593
734
|
cursor = event.id;
|
|
594
735
|
await this.options.cursorStore.save(cursor);
|
|
595
|
-
processed++;
|
|
596
736
|
}
|
|
597
737
|
if (!result.hasMore) {
|
|
598
738
|
return {
|
|
599
739
|
processed,
|
|
740
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
600
741
|
pages,
|
|
601
742
|
...cursor ? { cursor } : {},
|
|
602
743
|
hasMore: false,
|
|
@@ -606,6 +747,7 @@ var EdgeWebhookReconciler = class {
|
|
|
606
747
|
}
|
|
607
748
|
return {
|
|
608
749
|
processed,
|
|
750
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
609
751
|
pages,
|
|
610
752
|
...cursor ? { cursor } : {},
|
|
611
753
|
hasMore,
|
|
@@ -1367,6 +1509,16 @@ function readEnvironment(env) {
|
|
|
1367
1509
|
}
|
|
1368
1510
|
return value;
|
|
1369
1511
|
}
|
|
1512
|
+
function readBooleanEnv(env, name) {
|
|
1513
|
+
const value = readEnv(env, name);
|
|
1514
|
+
if (!value) return void 0;
|
|
1515
|
+
const normalized = value.toLowerCase();
|
|
1516
|
+
if (["true", "1", "yes"].includes(normalized)) return true;
|
|
1517
|
+
if (["false", "0", "no"].includes(normalized)) return false;
|
|
1518
|
+
throw new EdgeValidationError4(`Invalid ${name}`, {
|
|
1519
|
+
[name]: ["Must be one of: true, false, 1, 0, yes, no"]
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1370
1522
|
function readTimeout(env, override) {
|
|
1371
1523
|
if (override !== void 0) return override;
|
|
1372
1524
|
const value = readFirstEnv(env, ["EDGE_TIMEOUT_MS", "EDGE_REQUEST_TIMEOUT_MS"]);
|
|
@@ -1382,7 +1534,12 @@ function readTimeout(env, override) {
|
|
|
1382
1534
|
function buildMtlsConfig(env, requireMtls, warnings) {
|
|
1383
1535
|
const cert = readPemValue(env, "EDGE_MTLS_CERT", "EDGE_MTLS_CERT_PATH", warnings);
|
|
1384
1536
|
const key = readPemValue(env, "EDGE_MTLS_KEY", "EDGE_MTLS_KEY_PATH", warnings);
|
|
1385
|
-
const
|
|
1537
|
+
const serverCa = readPemValue(env, "EDGE_SERVER_CA_PEM", "EDGE_SERVER_CA_PEM_PATH", warnings);
|
|
1538
|
+
const hasLegacyCa = !!readFirstEnv(env, ["EDGE_MTLS_CA", "EDGE_MTLS_CA_PATH"]);
|
|
1539
|
+
if (serverCa && hasLegacyCa) {
|
|
1540
|
+
warnings.push("EDGE_SERVER_CA_PEM and EDGE_MTLS_CA are both set; using EDGE_SERVER_CA_PEM");
|
|
1541
|
+
}
|
|
1542
|
+
const ca = serverCa || readPemValue(env, "EDGE_MTLS_CA", "EDGE_MTLS_CA_PATH", warnings);
|
|
1386
1543
|
if (!cert && !key && !ca) {
|
|
1387
1544
|
if (requireMtls) {
|
|
1388
1545
|
throw new EdgeValidationError4("mTLS is required but no EDGE_MTLS_CERT or EDGE_MTLS_KEY was provided", {
|
|
@@ -1421,7 +1578,7 @@ function createEdgeConnectServerFromEnv(options = {}) {
|
|
|
1421
1578
|
const environment = readEnvironment(env);
|
|
1422
1579
|
const apiBaseUrl = readEnv(env, "EDGE_API_BASE_URL");
|
|
1423
1580
|
const mtlsRequiredByTarget = inferMtlsRequirement(environment, apiBaseUrl);
|
|
1424
|
-
const requireMtls = options.requireMtls ?? false;
|
|
1581
|
+
const requireMtls = options.requireMtls ?? readBooleanEnv(env, "EDGE_REQUIRE_MTLS") ?? false;
|
|
1425
1582
|
const mtls = buildMtlsConfig(env, requireMtls, warnings);
|
|
1426
1583
|
const timeout = readTimeout(env, options.timeout);
|
|
1427
1584
|
const clientId = readFirstEnv(env, ["EDGE_CLIENT_ID", "EDGE_CONNECT_CLIENT_ID"]) || "";
|
|
@@ -1476,6 +1633,7 @@ function createEdgeConnectServerFromEnv(options = {}) {
|
|
|
1476
1633
|
import {
|
|
1477
1634
|
EdgeConsentRequiredError as EdgeConsentRequiredError2,
|
|
1478
1635
|
EdgeError as EdgeError2,
|
|
1636
|
+
EdgeFeatureUnavailableError as EdgeFeatureUnavailableError3,
|
|
1479
1637
|
EdgeIdentityVerificationError as EdgeIdentityVerificationError2,
|
|
1480
1638
|
EdgeNetworkError as EdgeNetworkError2,
|
|
1481
1639
|
EdgeValidationError as EdgeValidationError5
|
|
@@ -1501,6 +1659,9 @@ function toEdgeProblemJson(error) {
|
|
|
1501
1659
|
fieldErrors: error.fieldErrors
|
|
1502
1660
|
});
|
|
1503
1661
|
}
|
|
1662
|
+
if (error instanceof EdgeFeatureUnavailableError3) {
|
|
1663
|
+
return problem(error, "Feature unavailable", error.statusCode ?? 501);
|
|
1664
|
+
}
|
|
1504
1665
|
if (error instanceof EdgeNetworkError2) {
|
|
1505
1666
|
return problem(error, "EDGE network error", error.statusCode ?? 502);
|
|
1506
1667
|
}
|
|
@@ -1530,8 +1691,229 @@ function edgeTitle(error) {
|
|
|
1530
1691
|
return error.code.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1531
1692
|
}
|
|
1532
1693
|
|
|
1694
|
+
// src/http-helpers.ts
|
|
1695
|
+
import { EdgeValidationError as EdgeValidationError7 } from "@edge-markets/connect";
|
|
1696
|
+
|
|
1697
|
+
// src/webhook-parser.ts
|
|
1698
|
+
import {
|
|
1699
|
+
EDGE_WEBHOOK_EVENT_TYPES,
|
|
1700
|
+
EdgeAuthenticationError as EdgeAuthenticationError3,
|
|
1701
|
+
EdgeValidationError as EdgeValidationError6
|
|
1702
|
+
} from "@edge-markets/connect";
|
|
1703
|
+
|
|
1704
|
+
// src/webhook-signing.ts
|
|
1705
|
+
import crypto2 from "crypto";
|
|
1706
|
+
function verifyWebhookSignature(header, body, secret, options = {}) {
|
|
1707
|
+
if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
const requestedTolerance = options.toleranceSeconds;
|
|
1711
|
+
const toleranceSeconds = typeof requestedTolerance === "number" && Number.isFinite(requestedTolerance) && requestedTolerance >= 0 ? requestedTolerance : 300;
|
|
1712
|
+
const parts = header.split(",").map((p) => p.trim());
|
|
1713
|
+
const tPart = parts.find((p) => p.startsWith("t="));
|
|
1714
|
+
const v1Part = parts.find((p) => p.startsWith("v1="));
|
|
1715
|
+
if (!tPart || !v1Part) return false;
|
|
1716
|
+
const timestamp = parseInt(tPart.slice(2), 10);
|
|
1717
|
+
const signature = v1Part.slice(3);
|
|
1718
|
+
if (isNaN(timestamp) || !signature) return false;
|
|
1719
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1720
|
+
if (Math.abs(now - timestamp) > toleranceSeconds) return false;
|
|
1721
|
+
const signedContent = `${timestamp}.${body}`;
|
|
1722
|
+
const expectedHmac = crypto2.createHmac("sha256", secret).update(signedContent).digest("hex");
|
|
1723
|
+
try {
|
|
1724
|
+
const sigBuf = Buffer.from(signature, "hex");
|
|
1725
|
+
const expBuf = Buffer.from(expectedHmac, "hex");
|
|
1726
|
+
if (sigBuf.length !== expBuf.length) return false;
|
|
1727
|
+
return crypto2.timingSafeEqual(sigBuf, expBuf);
|
|
1728
|
+
} catch {
|
|
1729
|
+
return false;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// src/webhook-parser.ts
|
|
1734
|
+
var EVENT_TYPES = new Set(EDGE_WEBHOOK_EVENT_TYPES);
|
|
1735
|
+
var TRANSFER_TYPES = /* @__PURE__ */ new Set(["debit", "credit"]);
|
|
1736
|
+
function parseAndVerifyWebhook(options) {
|
|
1737
|
+
const body = normalizeRawBody(options.rawBody);
|
|
1738
|
+
const header = normalizeSignatureHeader(options.signatureHeader);
|
|
1739
|
+
const signatureTimestamp = extractWebhookSignatureTimestamp(header);
|
|
1740
|
+
if (!header || !signatureTimestamp) {
|
|
1741
|
+
throw new EdgeAuthenticationError3("Missing or malformed EDGE webhook signature");
|
|
1742
|
+
}
|
|
1743
|
+
if (!verifyWebhookSignature(header, body, options.secret, { toleranceSeconds: options.toleranceSeconds })) {
|
|
1744
|
+
throw new EdgeAuthenticationError3("Invalid EDGE webhook signature");
|
|
1745
|
+
}
|
|
1746
|
+
let parsed;
|
|
1747
|
+
try {
|
|
1748
|
+
parsed = JSON.parse(body);
|
|
1749
|
+
} catch {
|
|
1750
|
+
throw new EdgeValidationError6("EDGE webhook body must be valid JSON", {
|
|
1751
|
+
rawBody: ["Unable to parse JSON after signature verification"]
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
const event = validateEdgeWebhookEvent(parsed);
|
|
1755
|
+
return {
|
|
1756
|
+
event,
|
|
1757
|
+
eventId: event.id,
|
|
1758
|
+
eventType: event.type,
|
|
1759
|
+
signatureTimestamp,
|
|
1760
|
+
receivedAt: options.receivedAt ?? /* @__PURE__ */ new Date()
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
function extractWebhookSignatureTimestamp(header) {
|
|
1764
|
+
const normalizedHeader = normalizeSignatureHeader(header);
|
|
1765
|
+
if (!normalizedHeader) return void 0;
|
|
1766
|
+
const part = normalizedHeader.split(",").map((value) => value.trim()).find((value) => value.startsWith("t="));
|
|
1767
|
+
if (!part) return void 0;
|
|
1768
|
+
const timestamp = Number(part.slice(2));
|
|
1769
|
+
if (!Number.isInteger(timestamp) || timestamp <= 0) return void 0;
|
|
1770
|
+
return timestamp;
|
|
1771
|
+
}
|
|
1772
|
+
function isEdgeWebhookEvent(value) {
|
|
1773
|
+
try {
|
|
1774
|
+
validateEdgeWebhookEvent(value);
|
|
1775
|
+
return true;
|
|
1776
|
+
} catch {
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function validateEdgeWebhookEvent(value) {
|
|
1781
|
+
const errors = {};
|
|
1782
|
+
if (!isRecord(value)) {
|
|
1783
|
+
throw new EdgeValidationError6("EDGE webhook event must be an object", {
|
|
1784
|
+
event: ["Expected JSON object"]
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
const id = value.id;
|
|
1788
|
+
const type = value.type;
|
|
1789
|
+
const createdAt = value.created_at;
|
|
1790
|
+
const data = value.data;
|
|
1791
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
1792
|
+
errors.id = ["Required string"];
|
|
1793
|
+
}
|
|
1794
|
+
if (typeof type !== "string" || !EVENT_TYPES.has(type)) {
|
|
1795
|
+
errors.type = [`Must be one of: ${EDGE_WEBHOOK_EVENT_TYPES.join(", ")}`];
|
|
1796
|
+
}
|
|
1797
|
+
if (typeof createdAt !== "string" || !createdAt.trim() || Number.isNaN(Date.parse(createdAt))) {
|
|
1798
|
+
errors.created_at = ["Required ISO 8601 timestamp string"];
|
|
1799
|
+
}
|
|
1800
|
+
if (!isRecord(data)) {
|
|
1801
|
+
errors.data = ["Required object"];
|
|
1802
|
+
}
|
|
1803
|
+
if (Object.keys(errors).length === 0) {
|
|
1804
|
+
validateKnownEventData(type, data, errors);
|
|
1805
|
+
}
|
|
1806
|
+
if (Object.keys(errors).length > 0) {
|
|
1807
|
+
throw new EdgeValidationError6("Invalid EDGE webhook event", errors);
|
|
1808
|
+
}
|
|
1809
|
+
return value;
|
|
1810
|
+
}
|
|
1811
|
+
function validateKnownEventData(type, data, errors) {
|
|
1812
|
+
switch (type) {
|
|
1813
|
+
case "transfer.completed":
|
|
1814
|
+
validateTransferEventData(data, "completed", errors);
|
|
1815
|
+
return;
|
|
1816
|
+
case "transfer.failed":
|
|
1817
|
+
validateTransferEventData(data, "failed", errors);
|
|
1818
|
+
if (data.reason !== void 0 && typeof data.reason !== "string") {
|
|
1819
|
+
errors["data.reason"] = ["Must be a string when provided"];
|
|
1820
|
+
}
|
|
1821
|
+
return;
|
|
1822
|
+
case "transfer.expired":
|
|
1823
|
+
validateTransferEventData(data, "expired", errors);
|
|
1824
|
+
return;
|
|
1825
|
+
case "transfer.processing":
|
|
1826
|
+
validateTransferEventData(data, "processing", errors);
|
|
1827
|
+
return;
|
|
1828
|
+
case "consent.revoked":
|
|
1829
|
+
validateRequiredString(data, "userId", errors);
|
|
1830
|
+
validateRequiredString(data, "clientId", errors);
|
|
1831
|
+
validateRequiredIsoTimestamp(data, "revokedAt", errors);
|
|
1832
|
+
return;
|
|
1833
|
+
default: {
|
|
1834
|
+
const exhaustive = type;
|
|
1835
|
+
throw new EdgeValidationError6("Unsupported EDGE webhook event type", {
|
|
1836
|
+
type: [`Unsupported event type: ${exhaustive}`]
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function validateTransferEventData(data, status, errors) {
|
|
1842
|
+
validateRequiredString(data, "transferId", errors);
|
|
1843
|
+
if (data.status !== status) {
|
|
1844
|
+
errors["data.status"] = [`Must be "${status}" for this event type`];
|
|
1845
|
+
}
|
|
1846
|
+
if (typeof data.type !== "string" || !TRANSFER_TYPES.has(data.type)) {
|
|
1847
|
+
errors["data.type"] = ['Must be "debit" or "credit"'];
|
|
1848
|
+
}
|
|
1849
|
+
validateRequiredString(data, "amount", errors);
|
|
1850
|
+
}
|
|
1851
|
+
function validateRequiredString(data, field, errors) {
|
|
1852
|
+
if (typeof data[field] !== "string" || !data[field].trim()) {
|
|
1853
|
+
errors[`data.${field}`] = ["Required string"];
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
function validateRequiredIsoTimestamp(data, field, errors) {
|
|
1857
|
+
if (typeof data[field] !== "string" || !data[field].trim() || Number.isNaN(Date.parse(data[field]))) {
|
|
1858
|
+
errors[`data.${field}`] = ["Required ISO 8601 timestamp string"];
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function normalizeRawBody(rawBody) {
|
|
1862
|
+
if (typeof rawBody === "string") {
|
|
1863
|
+
return rawBody;
|
|
1864
|
+
}
|
|
1865
|
+
if (Buffer.isBuffer(rawBody) || rawBody instanceof Uint8Array) {
|
|
1866
|
+
return Buffer.from(rawBody).toString("utf8");
|
|
1867
|
+
}
|
|
1868
|
+
throw new EdgeValidationError6("EDGE webhook raw body is required", {
|
|
1869
|
+
rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
function normalizeSignatureHeader(header) {
|
|
1873
|
+
if (Array.isArray(header)) {
|
|
1874
|
+
return header.map((value) => value.trim()).filter(Boolean).join(",");
|
|
1875
|
+
}
|
|
1876
|
+
if (typeof header !== "string") return void 0;
|
|
1877
|
+
const trimmed = header.trim();
|
|
1878
|
+
return trimmed || void 0;
|
|
1879
|
+
}
|
|
1880
|
+
function isRecord(value) {
|
|
1881
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// src/http-helpers.ts
|
|
1885
|
+
function getEdgeWebhookRawBody(request) {
|
|
1886
|
+
if (typeof request.rawBody === "string" || Buffer.isBuffer(request.rawBody) || request.rawBody instanceof Uint8Array) {
|
|
1887
|
+
return request.rawBody;
|
|
1888
|
+
}
|
|
1889
|
+
throw new EdgeValidationError7("EDGE webhook raw body is required", {
|
|
1890
|
+
rawBody: [
|
|
1891
|
+
"Configure your framework to expose the raw UTF-8 request body before JSON parsing. Parsed JSON bodies cannot be used for HMAC verification."
|
|
1892
|
+
]
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
function getEdgeWebhookSignatureHeader(request, headerName = "x-edge-signature") {
|
|
1896
|
+
const headers = request.headers ?? {};
|
|
1897
|
+
const lowerHeaderName = headerName.toLowerCase();
|
|
1898
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1899
|
+
if (name.toLowerCase() === lowerHeaderName) {
|
|
1900
|
+
if (typeof value === "string" || Array.isArray(value)) return value;
|
|
1901
|
+
if (value === void 0 || value === null) return void 0;
|
|
1902
|
+
return String(value);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return void 0;
|
|
1906
|
+
}
|
|
1907
|
+
function parseWebhookHttpRequest(options) {
|
|
1908
|
+
return parseAndVerifyWebhook({
|
|
1909
|
+
...options,
|
|
1910
|
+
rawBody: getEdgeWebhookRawBody(options.request),
|
|
1911
|
+
signatureHeader: getEdgeWebhookSignatureHeader(options.request, options.signatureHeaderName)
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1533
1915
|
// src/identity.ts
|
|
1534
|
-
import { EdgeValidationError as
|
|
1916
|
+
import { EdgeValidationError as EdgeValidationError8 } from "@edge-markets/connect";
|
|
1535
1917
|
var DEFAULT_IDENTITY_POLICY = {
|
|
1536
1918
|
minNameScore: 70,
|
|
1537
1919
|
minAddressScore: 65,
|
|
@@ -1559,7 +1941,7 @@ function buildVerifyIdentityPayload(profile) {
|
|
|
1559
1941
|
errors.address = ["At least one address field is required"];
|
|
1560
1942
|
}
|
|
1561
1943
|
if (Object.keys(errors).length > 0) {
|
|
1562
|
-
throw new
|
|
1944
|
+
throw new EdgeValidationError8("Partner identity profile is incomplete", errors);
|
|
1563
1945
|
}
|
|
1564
1946
|
return payload;
|
|
1565
1947
|
}
|
|
@@ -1596,7 +1978,7 @@ function validatePolicy(policy) {
|
|
|
1596
1978
|
if (!isScore(policy.minNameScore)) errors.minNameScore = ["Must be between 0 and 100"];
|
|
1597
1979
|
if (!isScore(policy.minAddressScore)) errors.minAddressScore = ["Must be between 0 and 100"];
|
|
1598
1980
|
if (Object.keys(errors).length > 0) {
|
|
1599
|
-
throw new
|
|
1981
|
+
throw new EdgeValidationError8("Invalid identity policy", errors);
|
|
1600
1982
|
}
|
|
1601
1983
|
}
|
|
1602
1984
|
function isScore(value) {
|
|
@@ -1612,9 +1994,155 @@ function firstNonEmpty(...values) {
|
|
|
1612
1994
|
return "";
|
|
1613
1995
|
}
|
|
1614
1996
|
|
|
1997
|
+
// src/session-store.ts
|
|
1998
|
+
import { EdgeValidationError as EdgeValidationError9 } from "@edge-markets/connect";
|
|
1999
|
+
function createSessionTokenRecord(subjectId, tokens, tokenVault, options = {}) {
|
|
2000
|
+
const normalizedSubjectId = normalizeRequiredString(subjectId, "subjectId");
|
|
2001
|
+
validateTokens2(tokens);
|
|
2002
|
+
return {
|
|
2003
|
+
subjectId: normalizedSubjectId,
|
|
2004
|
+
encryptedTokens: tokenVault.encryptTokens(tokens),
|
|
2005
|
+
expiresAt: normalizeSessionExpiresAt(tokens.expiresAt),
|
|
2006
|
+
scopes: options.scopes ?? splitScopes(tokens.scope),
|
|
2007
|
+
...options.edgeUserId ? { edgeUserId: options.edgeUserId } : {},
|
|
2008
|
+
...options.keyId ? { keyId: options.keyId } : {},
|
|
2009
|
+
...options.metadata ? { metadata: options.metadata } : {}
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
function parseSessionTokenRecord(record) {
|
|
2013
|
+
if (!isRecord2(record)) {
|
|
2014
|
+
throw new EdgeValidationError9("EDGE session token record must be an object", {
|
|
2015
|
+
record: ["Expected object"]
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
const subjectId = normalizeRequiredString(record.subjectId, "subjectId");
|
|
2019
|
+
const encryptedTokens = normalizeRequiredString(record.encryptedTokens, "encryptedTokens");
|
|
2020
|
+
const expiresAt = normalizeSessionExpiresAt(record.expiresAt);
|
|
2021
|
+
const scopes = record.scopes === void 0 ? void 0 : normalizeScopes(record.scopes);
|
|
2022
|
+
return {
|
|
2023
|
+
subjectId,
|
|
2024
|
+
encryptedTokens,
|
|
2025
|
+
expiresAt,
|
|
2026
|
+
...scopes ? { scopes } : {},
|
|
2027
|
+
...typeof record.edgeUserId === "string" && record.edgeUserId.trim() ? { edgeUserId: record.edgeUserId.trim() } : {},
|
|
2028
|
+
...typeof record.keyId === "string" && record.keyId.trim() ? { keyId: record.keyId.trim() } : {},
|
|
2029
|
+
...isRecord2(record.metadata) ? { metadata: record.metadata } : {}
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
function encryptLegacySessionTokens(legacyRecord, tokenVault, options) {
|
|
2033
|
+
const subjectId = normalizeRequiredString(options.subjectId || legacyRecord.subjectId, "subjectId");
|
|
2034
|
+
const accessToken = normalizeRequiredString(legacyRecord.accessToken, "accessToken");
|
|
2035
|
+
const refreshToken = normalizeRequiredString(legacyRecord.refreshToken, "refreshToken");
|
|
2036
|
+
if (!options.allowPlaintext && !tokenVault.isEncrypted(accessToken) && !tokenVault.isEncrypted(refreshToken)) {
|
|
2037
|
+
throw new EdgeValidationError9("Plaintext token migration requires allowPlaintext: true", {
|
|
2038
|
+
allowPlaintext: ["Set allowPlaintext only inside an explicit migration path"]
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
const scope = normalizeScopeString(legacyRecord.scope ?? legacyRecord.scopes);
|
|
2042
|
+
const tokens = {
|
|
2043
|
+
accessToken,
|
|
2044
|
+
refreshToken,
|
|
2045
|
+
...typeof legacyRecord.idToken === "string" && legacyRecord.idToken.trim() ? { idToken: legacyRecord.idToken.trim() } : {},
|
|
2046
|
+
expiresIn: normalizeExpiresIn(legacyRecord.expiresIn),
|
|
2047
|
+
expiresAt: normalizeSessionExpiresAt(legacyRecord.expiresAt),
|
|
2048
|
+
scope
|
|
2049
|
+
};
|
|
2050
|
+
return createSessionTokenRecord(subjectId, tokens, tokenVault, {
|
|
2051
|
+
scopes: splitScopes(scope),
|
|
2052
|
+
...legacyRecord.edgeUserId ? { edgeUserId: legacyRecord.edgeUserId } : {},
|
|
2053
|
+
...options.keyId ? { keyId: options.keyId } : {},
|
|
2054
|
+
...legacyRecord.metadata ? { metadata: legacyRecord.metadata } : {}
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
function normalizeSessionExpiresAt(value) {
|
|
2058
|
+
if (value instanceof Date) {
|
|
2059
|
+
const time = value.getTime();
|
|
2060
|
+
if (!Number.isFinite(time) || time <= 0) {
|
|
2061
|
+
throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
|
|
2062
|
+
expiresAt: ["Date must be valid"]
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
return time;
|
|
2066
|
+
}
|
|
2067
|
+
if (typeof value === "string") {
|
|
2068
|
+
const trimmed = value.trim();
|
|
2069
|
+
if (!trimmed) {
|
|
2070
|
+
throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
|
|
2071
|
+
expiresAt: ["Required"]
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
if (/^\d+$/.test(trimmed)) {
|
|
2075
|
+
return normalizeSessionExpiresAt(Number(trimmed));
|
|
2076
|
+
}
|
|
2077
|
+
const time = Date.parse(trimmed);
|
|
2078
|
+
if (!Number.isNaN(time) && time > 0) return time;
|
|
2079
|
+
}
|
|
2080
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2081
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2082
|
+
throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
|
|
2083
|
+
expiresAt: ["Must be a positive integer timestamp in milliseconds"]
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
if (value < 1e10) {
|
|
2087
|
+
throw new EdgeValidationError9("Ambiguous EDGE session expiresAt", {
|
|
2088
|
+
expiresAt: ["Use Unix milliseconds, not seconds"]
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
return value;
|
|
2092
|
+
}
|
|
2093
|
+
throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
|
|
2094
|
+
expiresAt: ["Use Unix milliseconds, ISO string, or Date"]
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
function normalizeExpiresIn(value) {
|
|
2098
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
2099
|
+
if (typeof value === "string" && value.trim()) {
|
|
2100
|
+
const parsed = Number(value);
|
|
2101
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
2102
|
+
}
|
|
2103
|
+
return 600;
|
|
2104
|
+
}
|
|
2105
|
+
function normalizeScopeString(value) {
|
|
2106
|
+
if (Array.isArray(value)) return normalizeScopes(value).join(" ");
|
|
2107
|
+
if (typeof value === "string") return splitScopes(value).join(" ");
|
|
2108
|
+
return "";
|
|
2109
|
+
}
|
|
2110
|
+
function normalizeScopes(value) {
|
|
2111
|
+
if (Array.isArray(value)) {
|
|
2112
|
+
return value.map((scope) => typeof scope === "string" ? scope.trim() : "").filter(Boolean);
|
|
2113
|
+
}
|
|
2114
|
+
if (typeof value === "string") {
|
|
2115
|
+
return splitScopes(value);
|
|
2116
|
+
}
|
|
2117
|
+
throw new EdgeValidationError9("Invalid EDGE session scopes", {
|
|
2118
|
+
scopes: ["Expected array or space-delimited string"]
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
function splitScopes(value) {
|
|
2122
|
+
return value.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
|
|
2123
|
+
}
|
|
2124
|
+
function normalizeRequiredString(value, field) {
|
|
2125
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2126
|
+
throw new EdgeValidationError9("Invalid EDGE session token record", {
|
|
2127
|
+
[field]: ["Required string"]
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
return value.trim();
|
|
2131
|
+
}
|
|
2132
|
+
function validateTokens2(tokens) {
|
|
2133
|
+
normalizeRequiredString(tokens.accessToken, "accessToken");
|
|
2134
|
+
normalizeRequiredString(tokens.refreshToken, "refreshToken");
|
|
2135
|
+
normalizeSessionExpiresAt(tokens.expiresAt);
|
|
2136
|
+
normalizeExpiresIn(tokens.expiresIn);
|
|
2137
|
+
normalizeScopeString(tokens.scope);
|
|
2138
|
+
}
|
|
2139
|
+
function isRecord2(value) {
|
|
2140
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
1615
2143
|
// src/token-vault.ts
|
|
1616
|
-
import { EdgeValidationError as
|
|
1617
|
-
import
|
|
2144
|
+
import { EdgeValidationError as EdgeValidationError10 } from "@edge-markets/connect";
|
|
2145
|
+
import crypto3 from "crypto";
|
|
1618
2146
|
var TOKEN_VAULT_VERSION = "edge_vault_v1";
|
|
1619
2147
|
var IV_BYTES = 12;
|
|
1620
2148
|
var TAG_BYTES = 16;
|
|
@@ -1626,7 +2154,7 @@ var EdgeTokenVault = class {
|
|
|
1626
2154
|
for (const [index, key] of (options.previousKeys ?? []).entries()) {
|
|
1627
2155
|
const normalized = normalizeVaultKey(key, `previousKeys[${index}]`);
|
|
1628
2156
|
if (this.keysById.has(normalized.id)) {
|
|
1629
|
-
throw new
|
|
2157
|
+
throw new EdgeValidationError10("Duplicate token vault key ID", {
|
|
1630
2158
|
keyId: [`Duplicate key ID "${normalized.id}"`]
|
|
1631
2159
|
});
|
|
1632
2160
|
}
|
|
@@ -1635,9 +2163,9 @@ var EdgeTokenVault = class {
|
|
|
1635
2163
|
}
|
|
1636
2164
|
encryptTokens(tokens) {
|
|
1637
2165
|
validateEdgeTokens(tokens);
|
|
1638
|
-
const iv =
|
|
2166
|
+
const iv = crypto3.randomBytes(IV_BYTES);
|
|
1639
2167
|
const aad = Buffer.from(`${TOKEN_VAULT_VERSION}.${this.currentKey.id}`, "utf8");
|
|
1640
|
-
const cipher =
|
|
2168
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", this.currentKey.key, iv);
|
|
1641
2169
|
cipher.setAAD(aad);
|
|
1642
2170
|
const plaintext = Buffer.from(JSON.stringify(tokens), "utf8");
|
|
1643
2171
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
@@ -1654,12 +2182,12 @@ var EdgeTokenVault = class {
|
|
|
1654
2182
|
const parsed = parseEnvelope(envelope);
|
|
1655
2183
|
const key = this.keysById.get(parsed.keyId);
|
|
1656
2184
|
if (!key) {
|
|
1657
|
-
throw new
|
|
2185
|
+
throw new EdgeValidationError10("Unknown token vault key ID", {
|
|
1658
2186
|
keyId: [`No key configured for "${parsed.keyId}"`]
|
|
1659
2187
|
});
|
|
1660
2188
|
}
|
|
1661
2189
|
try {
|
|
1662
|
-
const decipher =
|
|
2190
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", key, parsed.iv);
|
|
1663
2191
|
decipher.setAAD(Buffer.from(`${TOKEN_VAULT_VERSION}.${parsed.keyId}`, "utf8"));
|
|
1664
2192
|
decipher.setAuthTag(parsed.tag);
|
|
1665
2193
|
const plaintext = Buffer.concat([decipher.update(parsed.ciphertext), decipher.final()]).toString("utf8");
|
|
@@ -1667,8 +2195,8 @@ var EdgeTokenVault = class {
|
|
|
1667
2195
|
validateEdgeTokens(tokens);
|
|
1668
2196
|
return tokens;
|
|
1669
2197
|
} catch (error) {
|
|
1670
|
-
if (error instanceof
|
|
1671
|
-
throw new
|
|
2198
|
+
if (error instanceof EdgeValidationError10) throw error;
|
|
2199
|
+
throw new EdgeValidationError10("Failed to decrypt EDGE tokens", {
|
|
1672
2200
|
tokenEnvelope: ["Envelope is malformed, corrupted, or encrypted with a different key"]
|
|
1673
2201
|
});
|
|
1674
2202
|
}
|
|
@@ -1686,7 +2214,7 @@ function isEdgeTokenVaultEnvelope(value) {
|
|
|
1686
2214
|
function normalizeVaultKey(input, fieldName) {
|
|
1687
2215
|
const id = typeof input.id === "string" ? input.id.trim() : "";
|
|
1688
2216
|
if (!id) {
|
|
1689
|
-
throw new
|
|
2217
|
+
throw new EdgeValidationError10("Token vault key ID is required", {
|
|
1690
2218
|
[`${fieldName}.id`]: ["Required"]
|
|
1691
2219
|
});
|
|
1692
2220
|
}
|
|
@@ -1717,7 +2245,7 @@ function normalizeKeyMaterial(input, fieldName) {
|
|
|
1717
2245
|
}
|
|
1718
2246
|
const key = candidates.find((candidate) => candidate.length === KEY_BYTES);
|
|
1719
2247
|
if (!key) {
|
|
1720
|
-
throw new
|
|
2248
|
+
throw new EdgeValidationError10("Token vault key must decode to 32 bytes", {
|
|
1721
2249
|
[fieldName]: ["Provide a 32-byte key as base64, base64url, hex, Buffer, or Uint8Array"]
|
|
1722
2250
|
});
|
|
1723
2251
|
}
|
|
@@ -1748,18 +2276,18 @@ function validateEdgeTokens(tokens) {
|
|
|
1748
2276
|
}
|
|
1749
2277
|
}
|
|
1750
2278
|
if (Object.keys(errors).length > 0) {
|
|
1751
|
-
throw new
|
|
2279
|
+
throw new EdgeValidationError10("Invalid EDGE token payload", errors);
|
|
1752
2280
|
}
|
|
1753
2281
|
}
|
|
1754
2282
|
function parseEnvelope(envelope) {
|
|
1755
2283
|
if (typeof envelope !== "string") {
|
|
1756
|
-
throw new
|
|
2284
|
+
throw new EdgeValidationError10("Token envelope must be a string", {
|
|
1757
2285
|
tokenEnvelope: ["Expected string"]
|
|
1758
2286
|
});
|
|
1759
2287
|
}
|
|
1760
2288
|
const parts = envelope.split(".");
|
|
1761
2289
|
if (parts.length !== 5 || parts[0] !== TOKEN_VAULT_VERSION) {
|
|
1762
|
-
throw new
|
|
2290
|
+
throw new EdgeValidationError10("Invalid EDGE token vault envelope", {
|
|
1763
2291
|
tokenEnvelope: [`Expected ${TOKEN_VAULT_VERSION}.<keyId>.<iv>.<tag>.<ciphertext>`]
|
|
1764
2292
|
});
|
|
1765
2293
|
}
|
|
@@ -1774,7 +2302,7 @@ function parseEnvelope(envelope) {
|
|
|
1774
2302
|
if (tag.length !== TAG_BYTES) errors.tag = [`Must be ${TAG_BYTES} bytes`];
|
|
1775
2303
|
if (ciphertext.length === 0) errors.ciphertext = ["Required"];
|
|
1776
2304
|
if (Object.keys(errors).length > 0) {
|
|
1777
|
-
throw new
|
|
2305
|
+
throw new EdgeValidationError10("Invalid EDGE token vault envelope", errors);
|
|
1778
2306
|
}
|
|
1779
2307
|
return { keyId, iv, tag, ciphertext };
|
|
1780
2308
|
}
|
|
@@ -1788,13 +2316,13 @@ function fromBase64Url2(value) {
|
|
|
1788
2316
|
}
|
|
1789
2317
|
|
|
1790
2318
|
// src/webhook-guards.ts
|
|
1791
|
-
import { EdgeValidationError as
|
|
2319
|
+
import { EdgeValidationError as EdgeValidationError11 } from "@edge-markets/connect";
|
|
1792
2320
|
function isTransferWebhookEvent(event) {
|
|
1793
2321
|
return event.type.startsWith("transfer.");
|
|
1794
2322
|
}
|
|
1795
2323
|
function assertTransferEventMatchesIntent(event, intent) {
|
|
1796
2324
|
if (!isTransferWebhookEvent(event)) {
|
|
1797
|
-
throw new
|
|
2325
|
+
throw new EdgeValidationError11("Webhook event is not a transfer event", {
|
|
1798
2326
|
type: [`Received ${event.type}`]
|
|
1799
2327
|
});
|
|
1800
2328
|
}
|
|
@@ -1817,229 +2345,285 @@ function assertTransferEventMatchesIntent(event, intent) {
|
|
|
1817
2345
|
}
|
|
1818
2346
|
}
|
|
1819
2347
|
if (Object.keys(errors).length > 0) {
|
|
1820
|
-
throw new
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
// src/webhook-parser.ts
|
|
1825
|
-
import {
|
|
1826
|
-
EDGE_WEBHOOK_EVENT_TYPES,
|
|
1827
|
-
EdgeAuthenticationError as EdgeAuthenticationError3,
|
|
1828
|
-
EdgeValidationError as EdgeValidationError9
|
|
1829
|
-
} from "@edge-markets/connect";
|
|
1830
|
-
|
|
1831
|
-
// src/webhook-signing.ts
|
|
1832
|
-
import crypto3 from "crypto";
|
|
1833
|
-
function verifyWebhookSignature(header, body, secret, options = {}) {
|
|
1834
|
-
if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
|
|
1835
|
-
return false;
|
|
1836
|
-
}
|
|
1837
|
-
const requestedTolerance = options.toleranceSeconds;
|
|
1838
|
-
const toleranceSeconds = typeof requestedTolerance === "number" && Number.isFinite(requestedTolerance) && requestedTolerance >= 0 ? requestedTolerance : 300;
|
|
1839
|
-
const parts = header.split(",").map((p) => p.trim());
|
|
1840
|
-
const tPart = parts.find((p) => p.startsWith("t="));
|
|
1841
|
-
const v1Part = parts.find((p) => p.startsWith("v1="));
|
|
1842
|
-
if (!tPart || !v1Part) return false;
|
|
1843
|
-
const timestamp = parseInt(tPart.slice(2), 10);
|
|
1844
|
-
const signature = v1Part.slice(3);
|
|
1845
|
-
if (isNaN(timestamp) || !signature) return false;
|
|
1846
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
1847
|
-
if (Math.abs(now - timestamp) > toleranceSeconds) return false;
|
|
1848
|
-
const signedContent = `${timestamp}.${body}`;
|
|
1849
|
-
const expectedHmac = crypto3.createHmac("sha256", secret).update(signedContent).digest("hex");
|
|
1850
|
-
try {
|
|
1851
|
-
const sigBuf = Buffer.from(signature, "hex");
|
|
1852
|
-
const expBuf = Buffer.from(expectedHmac, "hex");
|
|
1853
|
-
if (sigBuf.length !== expBuf.length) return false;
|
|
1854
|
-
return crypto3.timingSafeEqual(sigBuf, expBuf);
|
|
1855
|
-
} catch {
|
|
1856
|
-
return false;
|
|
2348
|
+
throw new EdgeValidationError11("Webhook transfer event does not match expected transfer intent", errors);
|
|
1857
2349
|
}
|
|
1858
2350
|
}
|
|
2351
|
+
var assertWebhookMatchesTransfer = assertTransferEventMatchesIntent;
|
|
1859
2352
|
|
|
1860
|
-
// src/webhook-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2353
|
+
// src/webhook-inbox.ts
|
|
2354
|
+
import { EdgeValidationError as EdgeValidationError12 } from "@edge-markets/connect";
|
|
2355
|
+
import crypto4 from "crypto";
|
|
2356
|
+
var DEFAULT_PROCESSING_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2357
|
+
var EdgeWebhookInbox = class {
|
|
2358
|
+
constructor(options) {
|
|
2359
|
+
if (!options.store?.insert || !options.store?.find || !options.store?.markProcessing) {
|
|
2360
|
+
throw new EdgeValidationError12("EdgeWebhookInbox store is incomplete", {
|
|
2361
|
+
store: ["Provide insert, find, markProcessing, markProcessed, and markFailed callbacks"]
|
|
2362
|
+
});
|
|
2363
|
+
}
|
|
2364
|
+
if (!options.store.markProcessed || !options.store.markFailed) {
|
|
2365
|
+
throw new EdgeValidationError12("EdgeWebhookInbox store is incomplete", {
|
|
2366
|
+
store: ["Provide markProcessed and markFailed callbacks"]
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
if (!options.processEvent) {
|
|
2370
|
+
throw new EdgeValidationError12("EdgeWebhookInbox processEvent callback is required", {
|
|
2371
|
+
processEvent: ["Required"]
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
if (options.processingTimeoutMs !== void 0 && (!Number.isFinite(options.processingTimeoutMs) || options.processingTimeoutMs <= 0)) {
|
|
2375
|
+
throw new EdgeValidationError12("processingTimeoutMs must be positive", {
|
|
2376
|
+
processingTimeoutMs: ["Must be greater than 0"]
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
this.store = options.store;
|
|
2380
|
+
this.processEvent = options.processEvent;
|
|
2381
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2382
|
+
this.storeRawBody = options.storeRawBody ?? false;
|
|
2383
|
+
this.processingTimeoutMs = options.processingTimeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
|
|
2384
|
+
this.onEvent = options.onEvent;
|
|
2385
|
+
}
|
|
2386
|
+
async accept(parsed) {
|
|
2387
|
+
return this.acceptEvent(parsed.event, {
|
|
2388
|
+
source: "live_webhook",
|
|
2389
|
+
receivedAt: parsed.receivedAt,
|
|
2390
|
+
signatureTimestamp: parsed.signatureTimestamp,
|
|
2391
|
+
signatureHeader: void 0,
|
|
2392
|
+
rawBody: void 0
|
|
1879
2393
|
});
|
|
1880
2394
|
}
|
|
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"]
|
|
2395
|
+
async acceptRaw(parsed, rawBody, signatureHeader) {
|
|
2396
|
+
return this.acceptEvent(parsed.event, {
|
|
2397
|
+
source: "live_webhook",
|
|
2398
|
+
receivedAt: parsed.receivedAt,
|
|
2399
|
+
signatureTimestamp: parsed.signatureTimestamp,
|
|
2400
|
+
signatureHeader,
|
|
2401
|
+
rawBody: this.storeRawBody ? rawBody : void 0
|
|
1912
2402
|
});
|
|
1913
2403
|
}
|
|
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"];
|
|
2404
|
+
async acceptSyncedEvent(event, receivedAt = this.now()) {
|
|
2405
|
+
return this.acceptEvent(event, {
|
|
2406
|
+
source: "sync",
|
|
2407
|
+
receivedAt,
|
|
2408
|
+
signatureTimestamp: null,
|
|
2409
|
+
signatureHeader: null,
|
|
2410
|
+
rawBody: null
|
|
2411
|
+
});
|
|
1929
2412
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
2413
|
+
async process(eventId, options = {}) {
|
|
2414
|
+
const normalizedEventId = normalizeEventId(eventId);
|
|
2415
|
+
const existing = await this.store.find(normalizedEventId);
|
|
2416
|
+
if (!existing) {
|
|
2417
|
+
return { eventId: normalizedEventId, status: "failed", processed: false, skipped: true, reason: "not_found" };
|
|
2418
|
+
}
|
|
2419
|
+
if (existing.status === "processed" && !options.force) {
|
|
2420
|
+
return {
|
|
2421
|
+
eventId: normalizedEventId,
|
|
2422
|
+
status: existing.status,
|
|
2423
|
+
processed: false,
|
|
2424
|
+
skipped: true,
|
|
2425
|
+
reason: "already_processed",
|
|
2426
|
+
record: existing
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
const now = this.now();
|
|
2430
|
+
const processing = await this.store.markProcessing(normalizedEventId, { now });
|
|
2431
|
+
if (!processing) {
|
|
2432
|
+
return {
|
|
2433
|
+
eventId: normalizedEventId,
|
|
2434
|
+
status: existing.status,
|
|
2435
|
+
processed: false,
|
|
2436
|
+
skipped: true,
|
|
2437
|
+
reason: "locked",
|
|
2438
|
+
record: existing
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
await this.emit({
|
|
2442
|
+
type: "webhook.processing_started",
|
|
2443
|
+
eventId: normalizedEventId,
|
|
2444
|
+
eventType: processing.eventType,
|
|
2445
|
+
status: "processing",
|
|
2446
|
+
source: processing.source,
|
|
2447
|
+
attempts: processing.attempts
|
|
2448
|
+
});
|
|
2449
|
+
try {
|
|
2450
|
+
await this.processEvent(processing.event, processing);
|
|
2451
|
+
const processed = await this.store.markProcessed(normalizedEventId, { now: this.now() }) ?? processing;
|
|
2452
|
+
await this.emit({
|
|
2453
|
+
type: "webhook.processing_succeeded",
|
|
2454
|
+
eventId: normalizedEventId,
|
|
2455
|
+
eventType: processing.eventType,
|
|
2456
|
+
status: "processed",
|
|
2457
|
+
source: processing.source,
|
|
2458
|
+
attempts: processed.attempts
|
|
2459
|
+
});
|
|
2460
|
+
return {
|
|
2461
|
+
eventId: normalizedEventId,
|
|
2462
|
+
status: "processed",
|
|
2463
|
+
processed: true,
|
|
2464
|
+
skipped: false,
|
|
2465
|
+
record: processed
|
|
2466
|
+
};
|
|
2467
|
+
} catch (error) {
|
|
2468
|
+
const message = error instanceof Error ? error.message : "Unknown webhook processing error";
|
|
2469
|
+
const failed = await this.store.markFailed(normalizedEventId, { now: this.now(), error: message }) ?? processing;
|
|
2470
|
+
await this.emit({
|
|
2471
|
+
type: "webhook.processing_failed",
|
|
2472
|
+
eventId: normalizedEventId,
|
|
2473
|
+
eventType: processing.eventType,
|
|
2474
|
+
status: "failed",
|
|
2475
|
+
source: processing.source,
|
|
2476
|
+
attempts: failed.attempts,
|
|
2477
|
+
error: message
|
|
2478
|
+
});
|
|
2479
|
+
throw error;
|
|
2480
|
+
}
|
|
1932
2481
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
2482
|
+
async replay(eventId) {
|
|
2483
|
+
return this.process(eventId, { force: true });
|
|
1935
2484
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2485
|
+
async recoverStaleProcessing() {
|
|
2486
|
+
if (!this.store.list) {
|
|
2487
|
+
return { recovered: 0, skipped: true, reason: "list_not_configured" };
|
|
2488
|
+
}
|
|
2489
|
+
if (!this.store.markPending) {
|
|
2490
|
+
return { recovered: 0, skipped: true, reason: "mark_pending_not_configured" };
|
|
2491
|
+
}
|
|
2492
|
+
const updatedBefore = new Date(this.now().getTime() - this.processingTimeoutMs);
|
|
2493
|
+
const records = await this.store.list({ status: "processing", updatedBefore, limit: 100 });
|
|
2494
|
+
let recovered = 0;
|
|
2495
|
+
for (const record of records) {
|
|
2496
|
+
const next = await this.store.markPending(record.eventId, {
|
|
2497
|
+
now: this.now(),
|
|
2498
|
+
reason: "stale_processing_recovery"
|
|
2499
|
+
});
|
|
2500
|
+
if (next) {
|
|
2501
|
+
recovered++;
|
|
2502
|
+
await this.emit({
|
|
2503
|
+
type: "webhook.recovered_stale",
|
|
2504
|
+
eventId: record.eventId,
|
|
2505
|
+
eventType: record.eventType,
|
|
2506
|
+
status: next.status,
|
|
2507
|
+
source: record.source,
|
|
2508
|
+
attempts: next.attempts
|
|
2509
|
+
});
|
|
1947
2510
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2511
|
+
}
|
|
2512
|
+
return { recovered, skipped: false };
|
|
2513
|
+
}
|
|
2514
|
+
async acceptEvent(event, options) {
|
|
2515
|
+
const payloadFingerprint = fingerprintWebhookEvent(event);
|
|
2516
|
+
const result = await this.store.insert({
|
|
2517
|
+
eventId: event.id,
|
|
2518
|
+
eventType: event.type,
|
|
2519
|
+
event,
|
|
2520
|
+
payloadFingerprint,
|
|
2521
|
+
source: options.source,
|
|
2522
|
+
receivedAt: options.receivedAt,
|
|
2523
|
+
signatureTimestamp: options.signatureTimestamp ?? null,
|
|
2524
|
+
signatureHeader: options.signatureHeader ?? null,
|
|
2525
|
+
rawBody: options.rawBody ?? null
|
|
2526
|
+
});
|
|
2527
|
+
if (result.record.payloadFingerprint !== payloadFingerprint) {
|
|
2528
|
+
await this.emit({
|
|
2529
|
+
type: "webhook.payload_mismatch",
|
|
2530
|
+
eventId: event.id,
|
|
2531
|
+
eventType: event.type,
|
|
2532
|
+
status: result.record.status,
|
|
2533
|
+
source: options.source
|
|
2534
|
+
});
|
|
2535
|
+
throw new EdgeValidationError12("Duplicate webhook event payload does not match stored event", {
|
|
2536
|
+
eventId: [event.id]
|
|
1964
2537
|
});
|
|
1965
2538
|
}
|
|
2539
|
+
await this.emit({
|
|
2540
|
+
type: result.inserted ? "webhook.accepted" : "webhook.duplicate",
|
|
2541
|
+
eventId: event.id,
|
|
2542
|
+
eventType: event.type,
|
|
2543
|
+
status: result.record.status,
|
|
2544
|
+
source: options.source,
|
|
2545
|
+
attempts: result.record.attempts
|
|
2546
|
+
});
|
|
2547
|
+
return {
|
|
2548
|
+
eventId: event.id,
|
|
2549
|
+
eventType: event.type,
|
|
2550
|
+
inserted: result.inserted,
|
|
2551
|
+
duplicate: !result.inserted,
|
|
2552
|
+
status: result.record.status,
|
|
2553
|
+
record: result.record
|
|
2554
|
+
};
|
|
1966
2555
|
}
|
|
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"'];
|
|
2556
|
+
async emit(event) {
|
|
2557
|
+
try {
|
|
2558
|
+
await this.onEvent?.(event);
|
|
2559
|
+
} catch {
|
|
2560
|
+
}
|
|
1975
2561
|
}
|
|
1976
|
-
|
|
2562
|
+
};
|
|
2563
|
+
function createWebhookInbox(options) {
|
|
2564
|
+
return new EdgeWebhookInbox(options);
|
|
1977
2565
|
}
|
|
1978
|
-
function
|
|
1979
|
-
|
|
1980
|
-
errors[`data.${field}`] = ["Required string"];
|
|
1981
|
-
}
|
|
2566
|
+
function fingerprintWebhookEvent(event) {
|
|
2567
|
+
return crypto4.createHash("sha256").update(stableStringify(event)).digest("hex");
|
|
1982
2568
|
}
|
|
1983
|
-
function
|
|
1984
|
-
if (typeof
|
|
1985
|
-
|
|
2569
|
+
function normalizeEventId(eventId) {
|
|
2570
|
+
if (typeof eventId !== "string" || !eventId.trim()) {
|
|
2571
|
+
throw new EdgeValidationError12("Webhook event ID is required", {
|
|
2572
|
+
eventId: ["Required"]
|
|
2573
|
+
});
|
|
1986
2574
|
}
|
|
2575
|
+
return eventId.trim();
|
|
1987
2576
|
}
|
|
1988
|
-
function
|
|
1989
|
-
if (
|
|
1990
|
-
return
|
|
2577
|
+
function stableStringify(value) {
|
|
2578
|
+
if (Array.isArray(value)) {
|
|
2579
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
1991
2580
|
}
|
|
1992
|
-
if (
|
|
1993
|
-
return
|
|
2581
|
+
if (value && typeof value === "object") {
|
|
2582
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
1994
2583
|
}
|
|
1995
|
-
|
|
1996
|
-
rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
|
|
1997
|
-
});
|
|
1998
|
-
}
|
|
1999
|
-
function normalizeSignatureHeader(header) {
|
|
2000
|
-
if (Array.isArray(header)) {
|
|
2001
|
-
return header.map((value) => value.trim()).filter(Boolean).join(",");
|
|
2002
|
-
}
|
|
2003
|
-
if (typeof header !== "string") return void 0;
|
|
2004
|
-
const trimmed = header.trim();
|
|
2005
|
-
return trimmed || void 0;
|
|
2006
|
-
}
|
|
2007
|
-
function isRecord(value) {
|
|
2008
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2584
|
+
return JSON.stringify(value);
|
|
2009
2585
|
}
|
|
2010
2586
|
|
|
2011
2587
|
// src/index.ts
|
|
2012
2588
|
import {
|
|
2589
|
+
EDGE_CONNECT_FEATURE_UNAVAILABLE_MESSAGE,
|
|
2013
2590
|
EdgeApiError as EdgeApiError2,
|
|
2014
2591
|
EdgeAuthenticationError as EdgeAuthenticationError4,
|
|
2015
2592
|
EdgeConsentRequiredError as EdgeConsentRequiredError3,
|
|
2016
2593
|
EdgeError as EdgeError3,
|
|
2594
|
+
EdgeFeatureUnavailableError as EdgeFeatureUnavailableError4,
|
|
2017
2595
|
EdgeIdentityVerificationError as EdgeIdentityVerificationError3,
|
|
2018
2596
|
EdgeInsufficientScopeError as EdgeInsufficientScopeError2,
|
|
2019
2597
|
EdgeNetworkError as EdgeNetworkError3,
|
|
2020
2598
|
EdgeNotFoundError as EdgeNotFoundError2,
|
|
2021
2599
|
EdgeTokenExchangeError as EdgeTokenExchangeError2,
|
|
2022
|
-
EdgeValidationError as
|
|
2600
|
+
EdgeValidationError as EdgeValidationError13,
|
|
2023
2601
|
isApiError,
|
|
2024
2602
|
isAuthenticationError,
|
|
2025
2603
|
isConsentRequiredError,
|
|
2026
2604
|
isEdgeError,
|
|
2605
|
+
isFeatureUnavailableError,
|
|
2027
2606
|
isIdentityVerificationError,
|
|
2028
2607
|
isNetworkError
|
|
2029
2608
|
} from "@edge-markets/connect";
|
|
2030
2609
|
import {
|
|
2610
|
+
ACTIVE_EDGE_SCOPES,
|
|
2031
2611
|
EDGE_WEBHOOK_EVENT_TYPES as EDGE_WEBHOOK_EVENT_TYPES2,
|
|
2032
2612
|
TRANSFER_CATEGORIES,
|
|
2033
2613
|
getEnvironmentConfig as getEnvironmentConfig3,
|
|
2034
2614
|
isProductionEnvironment
|
|
2035
2615
|
} from "@edge-markets/connect";
|
|
2036
2616
|
export {
|
|
2617
|
+
ACTIVE_EDGE_SCOPES,
|
|
2618
|
+
CONNECT_TRANSFER_DIRECTIONS,
|
|
2619
|
+
EDGE_CONNECT_FEATURE_UNAVAILABLE_MESSAGE,
|
|
2037
2620
|
EDGE_WEBHOOK_EVENT_TYPES2 as EDGE_WEBHOOK_EVENT_TYPES,
|
|
2038
2621
|
EdgeApiError2 as EdgeApiError,
|
|
2039
2622
|
EdgeAuthenticationError4 as EdgeAuthenticationError,
|
|
2040
2623
|
EdgeConnectServer,
|
|
2041
2624
|
EdgeConsentRequiredError3 as EdgeConsentRequiredError,
|
|
2042
2625
|
EdgeError3 as EdgeError,
|
|
2626
|
+
EdgeFeatureUnavailableError4 as EdgeFeatureUnavailableError,
|
|
2043
2627
|
EdgeIdentityVerificationError3 as EdgeIdentityVerificationError,
|
|
2044
2628
|
EdgeInsufficientScopeError2 as EdgeInsufficientScopeError,
|
|
2045
2629
|
EdgeNetworkError3 as EdgeNetworkError,
|
|
@@ -2048,19 +2632,29 @@ export {
|
|
|
2048
2632
|
EdgeTokenVault,
|
|
2049
2633
|
EdgeUserClient,
|
|
2050
2634
|
EdgeUserSession,
|
|
2051
|
-
|
|
2635
|
+
EdgeValidationError13 as EdgeValidationError,
|
|
2636
|
+
EdgeWebhookInbox,
|
|
2052
2637
|
EdgeWebhookReconciler,
|
|
2053
2638
|
TRANSFER_CATEGORIES,
|
|
2054
2639
|
assertSameTransferIntent,
|
|
2055
2640
|
assertTransferEventMatchesIntent,
|
|
2641
|
+
assertWebhookMatchesTransfer,
|
|
2056
2642
|
buildVerifyIdentityPayload,
|
|
2057
2643
|
createEdgeConnectServerFromEnv,
|
|
2058
2644
|
createEdgeTokenVault,
|
|
2059
2645
|
createEdgeUserSession,
|
|
2646
|
+
createSessionTokenRecord,
|
|
2060
2647
|
createTransferIdempotencyKey,
|
|
2648
|
+
createWebhookInbox,
|
|
2061
2649
|
createWebhookReconciler,
|
|
2650
|
+
encryptLegacySessionTokens,
|
|
2062
2651
|
evaluateIdentityResult,
|
|
2063
2652
|
extractWebhookSignatureTimestamp,
|
|
2653
|
+
fingerprintTransferIntent,
|
|
2654
|
+
fingerprintWebhookEvent,
|
|
2655
|
+
getConnectTransferDirection,
|
|
2656
|
+
getEdgeWebhookRawBody,
|
|
2657
|
+
getEdgeWebhookSignatureHeader,
|
|
2064
2658
|
getEnvironmentConfig3 as getEnvironmentConfig,
|
|
2065
2659
|
isApiError,
|
|
2066
2660
|
isAuthenticationError,
|
|
@@ -2068,12 +2662,18 @@ export {
|
|
|
2068
2662
|
isEdgeError,
|
|
2069
2663
|
isEdgeTokenVaultEnvelope,
|
|
2070
2664
|
isEdgeWebhookEvent,
|
|
2665
|
+
isFeatureUnavailableError,
|
|
2071
2666
|
isIdentityVerificationError,
|
|
2072
2667
|
isNetworkError,
|
|
2073
2668
|
isProductionEnvironment,
|
|
2074
2669
|
isTransferWebhookEvent,
|
|
2670
|
+
mapTransferStatusToPartnerState,
|
|
2671
|
+
moneyAmountsEqual,
|
|
2075
2672
|
normalizeMoneyAmount,
|
|
2673
|
+
normalizeSessionExpiresAt,
|
|
2076
2674
|
parseAndVerifyWebhook,
|
|
2675
|
+
parseSessionTokenRecord,
|
|
2676
|
+
parseWebhookHttpRequest,
|
|
2077
2677
|
startTransferVerification,
|
|
2078
2678
|
toEdgeHttpError,
|
|
2079
2679
|
toEdgeProblemJson,
|