@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.js
CHANGED
|
@@ -30,46 +30,66 @@ 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
|
+
ACTIVE_EDGE_SCOPES: () => import_connect16.ACTIVE_EDGE_SCOPES,
|
|
34
|
+
CONNECT_TRANSFER_DIRECTIONS: () => CONNECT_TRANSFER_DIRECTIONS,
|
|
35
|
+
EDGE_CONNECT_FEATURE_UNAVAILABLE_MESSAGE: () => import_connect15.EDGE_CONNECT_FEATURE_UNAVAILABLE_MESSAGE,
|
|
36
|
+
EDGE_WEBHOOK_EVENT_TYPES: () => import_connect16.EDGE_WEBHOOK_EVENT_TYPES,
|
|
37
|
+
EdgeApiError: () => import_connect15.EdgeApiError,
|
|
38
|
+
EdgeAuthenticationError: () => import_connect15.EdgeAuthenticationError,
|
|
36
39
|
EdgeConnectServer: () => EdgeConnectServer,
|
|
37
|
-
EdgeConsentRequiredError: () =>
|
|
38
|
-
EdgeError: () =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
EdgeConsentRequiredError: () => import_connect15.EdgeConsentRequiredError,
|
|
41
|
+
EdgeError: () => import_connect15.EdgeError,
|
|
42
|
+
EdgeFeatureUnavailableError: () => import_connect15.EdgeFeatureUnavailableError,
|
|
43
|
+
EdgeIdentityVerificationError: () => import_connect15.EdgeIdentityVerificationError,
|
|
44
|
+
EdgeInsufficientScopeError: () => import_connect15.EdgeInsufficientScopeError,
|
|
45
|
+
EdgeNetworkError: () => import_connect15.EdgeNetworkError,
|
|
46
|
+
EdgeNotFoundError: () => import_connect15.EdgeNotFoundError,
|
|
47
|
+
EdgeTokenExchangeError: () => import_connect15.EdgeTokenExchangeError,
|
|
44
48
|
EdgeTokenVault: () => EdgeTokenVault,
|
|
45
49
|
EdgeUserClient: () => EdgeUserClient,
|
|
46
50
|
EdgeUserSession: () => EdgeUserSession,
|
|
47
|
-
EdgeValidationError: () =>
|
|
51
|
+
EdgeValidationError: () => import_connect15.EdgeValidationError,
|
|
52
|
+
EdgeWebhookInbox: () => EdgeWebhookInbox,
|
|
48
53
|
EdgeWebhookReconciler: () => EdgeWebhookReconciler,
|
|
49
|
-
TRANSFER_CATEGORIES: () =>
|
|
54
|
+
TRANSFER_CATEGORIES: () => import_connect16.TRANSFER_CATEGORIES,
|
|
50
55
|
assertSameTransferIntent: () => assertSameTransferIntent,
|
|
51
56
|
assertTransferEventMatchesIntent: () => assertTransferEventMatchesIntent,
|
|
57
|
+
assertWebhookMatchesTransfer: () => assertWebhookMatchesTransfer,
|
|
52
58
|
buildVerifyIdentityPayload: () => buildVerifyIdentityPayload,
|
|
53
59
|
createEdgeConnectServerFromEnv: () => createEdgeConnectServerFromEnv,
|
|
54
60
|
createEdgeTokenVault: () => createEdgeTokenVault,
|
|
55
61
|
createEdgeUserSession: () => createEdgeUserSession,
|
|
62
|
+
createSessionTokenRecord: () => createSessionTokenRecord,
|
|
56
63
|
createTransferIdempotencyKey: () => createTransferIdempotencyKey,
|
|
64
|
+
createWebhookInbox: () => createWebhookInbox,
|
|
57
65
|
createWebhookReconciler: () => createWebhookReconciler,
|
|
66
|
+
encryptLegacySessionTokens: () => encryptLegacySessionTokens,
|
|
58
67
|
evaluateIdentityResult: () => evaluateIdentityResult,
|
|
59
68
|
extractWebhookSignatureTimestamp: () => extractWebhookSignatureTimestamp,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
fingerprintTransferIntent: () => fingerprintTransferIntent,
|
|
70
|
+
fingerprintWebhookEvent: () => fingerprintWebhookEvent,
|
|
71
|
+
getConnectTransferDirection: () => getConnectTransferDirection,
|
|
72
|
+
getEdgeWebhookRawBody: () => getEdgeWebhookRawBody,
|
|
73
|
+
getEdgeWebhookSignatureHeader: () => getEdgeWebhookSignatureHeader,
|
|
74
|
+
getEnvironmentConfig: () => import_connect16.getEnvironmentConfig,
|
|
75
|
+
isApiError: () => import_connect15.isApiError,
|
|
76
|
+
isAuthenticationError: () => import_connect15.isAuthenticationError,
|
|
77
|
+
isConsentRequiredError: () => import_connect15.isConsentRequiredError,
|
|
78
|
+
isEdgeError: () => import_connect15.isEdgeError,
|
|
65
79
|
isEdgeTokenVaultEnvelope: () => isEdgeTokenVaultEnvelope,
|
|
66
80
|
isEdgeWebhookEvent: () => isEdgeWebhookEvent,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
isFeatureUnavailableError: () => import_connect15.isFeatureUnavailableError,
|
|
82
|
+
isIdentityVerificationError: () => import_connect15.isIdentityVerificationError,
|
|
83
|
+
isNetworkError: () => import_connect15.isNetworkError,
|
|
84
|
+
isProductionEnvironment: () => import_connect16.isProductionEnvironment,
|
|
70
85
|
isTransferWebhookEvent: () => isTransferWebhookEvent,
|
|
86
|
+
mapTransferStatusToPartnerState: () => mapTransferStatusToPartnerState,
|
|
87
|
+
moneyAmountsEqual: () => moneyAmountsEqual,
|
|
71
88
|
normalizeMoneyAmount: () => normalizeMoneyAmount,
|
|
89
|
+
normalizeSessionExpiresAt: () => normalizeSessionExpiresAt,
|
|
72
90
|
parseAndVerifyWebhook: () => parseAndVerifyWebhook,
|
|
91
|
+
parseSessionTokenRecord: () => parseSessionTokenRecord,
|
|
92
|
+
parseWebhookHttpRequest: () => parseWebhookHttpRequest,
|
|
73
93
|
startTransferVerification: () => startTransferVerification,
|
|
74
94
|
toEdgeHttpError: () => toEdgeHttpError,
|
|
75
95
|
toEdgeProblemJson: () => toEdgeProblemJson,
|
|
@@ -80,23 +100,38 @@ __export(index_exports, {
|
|
|
80
100
|
module.exports = __toCommonJS(index_exports);
|
|
81
101
|
|
|
82
102
|
// src/edge-connect-server.ts
|
|
83
|
-
var
|
|
103
|
+
var import_connect5 = require("@edge-markets/connect");
|
|
104
|
+
|
|
105
|
+
// src/edge-user-client.ts
|
|
106
|
+
var import_connect3 = require("@edge-markets/connect");
|
|
84
107
|
|
|
85
108
|
// src/transfer-helpers.ts
|
|
86
109
|
var import_connect = require("@edge-markets/connect");
|
|
87
110
|
var import_node_crypto = __toESM(require("crypto"));
|
|
88
111
|
var DEFAULT_MIN_AMOUNT = "1.00";
|
|
89
112
|
var DEFAULT_MAX_AMOUNT = "10000.00";
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
var CONNECT_TRANSFER_DIRECTIONS = {
|
|
114
|
+
debit: "user_to_partner",
|
|
115
|
+
credit: "partner_to_user"
|
|
116
|
+
};
|
|
117
|
+
function getConnectTransferDirection(type) {
|
|
118
|
+
if (type !== "debit" && type !== "credit") {
|
|
119
|
+
throw new import_connect.EdgeValidationError("Invalid Connect transfer type", {
|
|
120
|
+
type: ['Must be "debit" or "credit"']
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return CONNECT_TRANSFER_DIRECTIONS[type];
|
|
124
|
+
}
|
|
125
|
+
function normalizeMoneyAmount(value, options = {}) {
|
|
126
|
+
const cents = parseMoneyToCents(value, "amount", options);
|
|
92
127
|
const dollars = cents / 100n;
|
|
93
128
|
const remainder = cents % 100n;
|
|
94
129
|
return `${dollars.toString()}.${remainder.toString().padStart(2, "0")}`;
|
|
95
130
|
}
|
|
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");
|
|
131
|
+
function validateTransferAmount(value, limits = {}, options = {}) {
|
|
132
|
+
const amount = parseMoneyToCents(value, "amount", options);
|
|
133
|
+
const min = parseMoneyToCents(limits.min ?? DEFAULT_MIN_AMOUNT, "min", { allowNumber: true });
|
|
134
|
+
const max = parseMoneyToCents(limits.max ?? DEFAULT_MAX_AMOUNT, "max", { allowNumber: true });
|
|
100
135
|
const errors = {};
|
|
101
136
|
if (min <= 0n) errors.min = ["Minimum amount must be greater than 0"];
|
|
102
137
|
if (max < min) errors.max = ["Maximum amount must be greater than or equal to minimum amount"];
|
|
@@ -144,7 +179,29 @@ function assertSameTransferIntent(existing, requested) {
|
|
|
144
179
|
throw new import_connect.EdgeValidationError("Transfer intent does not match existing idempotent transfer", errors);
|
|
145
180
|
}
|
|
146
181
|
}
|
|
182
|
+
function fingerprintTransferIntent(intent) {
|
|
183
|
+
return import_node_crypto.default.createHash("sha256").update(
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
type: intent.type,
|
|
186
|
+
amount: normalizeMoneyAmount(intent.amount),
|
|
187
|
+
category: intent.category ?? null
|
|
188
|
+
})
|
|
189
|
+
).digest("hex");
|
|
190
|
+
}
|
|
191
|
+
function moneyAmountsEqual(left, right, options = {}) {
|
|
192
|
+
return normalizeMoneyAmount(left, options) === normalizeMoneyAmount(right, options);
|
|
193
|
+
}
|
|
194
|
+
function mapTransferStatusToPartnerState(status, mapping) {
|
|
195
|
+
const partnerState = mapping[status];
|
|
196
|
+
if (!partnerState) {
|
|
197
|
+
throw new import_connect.EdgeValidationError("Unsupported transfer status", {
|
|
198
|
+
status: [`Unsupported status: ${status}`]
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return partnerState;
|
|
202
|
+
}
|
|
147
203
|
async function startTransferVerification(client, options) {
|
|
204
|
+
assertTransferFeatureAvailable();
|
|
148
205
|
const origin = typeof options.origin === "string" ? options.origin.trim() : "";
|
|
149
206
|
if (!origin) {
|
|
150
207
|
throw new import_connect.EdgeValidationError("Verification origin is required", {
|
|
@@ -164,9 +221,22 @@ async function startTransferVerification(client, options) {
|
|
|
164
221
|
const verificationSession = await client.createVerificationSession(transferId, { origin });
|
|
165
222
|
return { transfer, verificationSession };
|
|
166
223
|
}
|
|
167
|
-
function
|
|
224
|
+
function assertTransferFeatureAvailable() {
|
|
225
|
+
if (!isTransferFeatureAvailable()) {
|
|
226
|
+
throw new import_connect.EdgeFeatureUnavailableError();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function isTransferFeatureAvailable() {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
function parseMoneyToCents(value, field, options = {}) {
|
|
168
233
|
let input;
|
|
169
234
|
if (typeof value === "number") {
|
|
235
|
+
if (options.allowNumber === false) {
|
|
236
|
+
throw new import_connect.EdgeValidationError("Money amount must be a string in strict mode", {
|
|
237
|
+
[field]: ['Pass a decimal string like "25.00"']
|
|
238
|
+
});
|
|
239
|
+
}
|
|
170
240
|
if (!Number.isFinite(value)) {
|
|
171
241
|
throw new import_connect.EdgeValidationError("Money amount must be finite", { [field]: ["Must be a finite number"] });
|
|
172
242
|
}
|
|
@@ -280,6 +350,7 @@ var EdgeUserClient = class {
|
|
|
280
350
|
return this.server._apiRequest("GET", "/balance", this.accessToken);
|
|
281
351
|
}
|
|
282
352
|
async initiateTransfer(options) {
|
|
353
|
+
assertTransferFeatureAvailable2();
|
|
283
354
|
validateTransferOptions(options);
|
|
284
355
|
const body = {
|
|
285
356
|
type: options.type,
|
|
@@ -292,13 +363,16 @@ var EdgeUserClient = class {
|
|
|
292
363
|
return this.server._apiRequest("POST", "/transfer", this.accessToken, body);
|
|
293
364
|
}
|
|
294
365
|
async startTransferVerification(options) {
|
|
366
|
+
assertTransferFeatureAvailable2();
|
|
295
367
|
return startTransferVerification(this, options);
|
|
296
368
|
}
|
|
297
369
|
async getTransfer(transferId) {
|
|
370
|
+
assertTransferFeatureAvailable2();
|
|
298
371
|
validateTransferId(transferId);
|
|
299
372
|
return this.server._apiRequest("GET", `/transfer/${encodeURIComponent(transferId)}`, this.accessToken);
|
|
300
373
|
}
|
|
301
374
|
async listTransfers(params) {
|
|
375
|
+
assertTransferFeatureAvailable2();
|
|
302
376
|
const queryParams = new URLSearchParams();
|
|
303
377
|
if (params?.limit) queryParams.set("limit", String(params.limit));
|
|
304
378
|
if (params?.offset) queryParams.set("offset", String(params.offset));
|
|
@@ -311,11 +385,11 @@ var EdgeUserClient = class {
|
|
|
311
385
|
return this.server._apiRequest("DELETE", "/consent", this.accessToken);
|
|
312
386
|
}
|
|
313
387
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
* The handoff token is single-use and expires in 120 seconds.
|
|
388
|
+
* Reserved for a future transfer release.
|
|
389
|
+
* Currently throws EdgeFeatureUnavailableError before validation or network I/O.
|
|
317
390
|
*/
|
|
318
391
|
async createVerificationSession(transferId, options) {
|
|
392
|
+
assertTransferFeatureAvailable2();
|
|
319
393
|
validateTransferId(transferId);
|
|
320
394
|
return this.server._apiRequest(
|
|
321
395
|
"POST",
|
|
@@ -325,10 +399,11 @@ var EdgeUserClient = class {
|
|
|
325
399
|
);
|
|
326
400
|
}
|
|
327
401
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
402
|
+
* Reserved for a future transfer release.
|
|
403
|
+
* Currently throws EdgeFeatureUnavailableError before validation or network I/O.
|
|
330
404
|
*/
|
|
331
405
|
async getVerificationSessionStatus(transferId, sessionId) {
|
|
406
|
+
assertTransferFeatureAvailable2();
|
|
332
407
|
validateTransferId(transferId);
|
|
333
408
|
return this.server._apiRequest(
|
|
334
409
|
"GET",
|
|
@@ -337,6 +412,14 @@ var EdgeUserClient = class {
|
|
|
337
412
|
);
|
|
338
413
|
}
|
|
339
414
|
};
|
|
415
|
+
function assertTransferFeatureAvailable2() {
|
|
416
|
+
if (!isTransferFeatureAvailable2()) {
|
|
417
|
+
throw new import_connect3.EdgeFeatureUnavailableError();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function isTransferFeatureAvailable2() {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
340
423
|
|
|
341
424
|
// src/mle.ts
|
|
342
425
|
var import_crypto = require("crypto");
|
|
@@ -475,27 +558,28 @@ function validateMtlsConfig(config) {
|
|
|
475
558
|
}
|
|
476
559
|
|
|
477
560
|
// src/session.ts
|
|
478
|
-
var
|
|
561
|
+
var import_connect4 = require("@edge-markets/connect");
|
|
479
562
|
var EdgeUserSession = class {
|
|
480
563
|
constructor(options) {
|
|
564
|
+
this.refreshPromise = null;
|
|
481
565
|
if (!options.edge?.forUser || !options.edge?.refreshTokens) {
|
|
482
|
-
throw new
|
|
566
|
+
throw new import_connect4.EdgeValidationError("EdgeUserSession requires an EdgeConnectServer instance", {
|
|
483
567
|
edge: ["Provide edge.forUser and edge.refreshTokens"]
|
|
484
568
|
});
|
|
485
569
|
}
|
|
486
570
|
const subjectId = typeof options.subjectId === "string" ? options.subjectId.trim() : "";
|
|
487
571
|
if (!subjectId) {
|
|
488
|
-
throw new
|
|
572
|
+
throw new import_connect4.EdgeValidationError("EdgeUserSession subjectId is required", {
|
|
489
573
|
subjectId: ["Required"]
|
|
490
574
|
});
|
|
491
575
|
}
|
|
492
576
|
if (!options.tokenStore?.load || !options.tokenStore?.save) {
|
|
493
|
-
throw new
|
|
577
|
+
throw new import_connect4.EdgeValidationError("EdgeUserSession tokenStore is required", {
|
|
494
578
|
tokenStore: ["Provide load(subjectId) and save(subjectId, value, context)"]
|
|
495
579
|
});
|
|
496
580
|
}
|
|
497
581
|
if (options.refreshSkewMs !== void 0 && (!Number.isFinite(options.refreshSkewMs) || options.refreshSkewMs < 0)) {
|
|
498
|
-
throw new
|
|
582
|
+
throw new import_connect4.EdgeValidationError("refreshSkewMs must be a non-negative number", {
|
|
499
583
|
refreshSkewMs: ["Must be greater than or equal to 0"]
|
|
500
584
|
});
|
|
501
585
|
}
|
|
@@ -506,50 +590,114 @@ var EdgeUserSession = class {
|
|
|
506
590
|
this.refreshSkewMs = options.refreshSkewMs ?? 6e4;
|
|
507
591
|
this.now = options.now ?? Date.now;
|
|
508
592
|
this.onRefresh = options.onRefresh;
|
|
593
|
+
this.onEvent = options.onEvent;
|
|
594
|
+
this.singleFlightRefresh = options.singleFlightRefresh ?? true;
|
|
509
595
|
}
|
|
510
596
|
async getValidTokens(options = {}) {
|
|
511
597
|
const tokens = await this.loadTokens();
|
|
512
598
|
if (!options.forceRefresh && tokens.expiresAt > this.now() + this.refreshSkewMs) {
|
|
513
599
|
return tokens;
|
|
514
600
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
601
|
+
if (this.singleFlightRefresh) {
|
|
602
|
+
if (!this.refreshPromise) {
|
|
603
|
+
this.refreshPromise = this.refreshTokens(tokens).finally(() => {
|
|
604
|
+
this.refreshPromise = null;
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return this.refreshPromise;
|
|
608
|
+
}
|
|
609
|
+
return this.refreshTokens(tokens);
|
|
519
610
|
}
|
|
520
611
|
async getClient(options = {}) {
|
|
521
612
|
const tokens = await this.getValidTokens(options);
|
|
522
613
|
return this.edge.forUser(tokens.accessToken);
|
|
523
614
|
}
|
|
524
|
-
async withClient(callback) {
|
|
525
|
-
const tokens = await this.getValidTokens();
|
|
615
|
+
async withClient(callback, options = {}) {
|
|
616
|
+
const tokens = await this.getValidTokens(options);
|
|
526
617
|
return callback(this.edge.forUser(tokens.accessToken), tokens);
|
|
527
618
|
}
|
|
528
619
|
async saveTokens(tokens, refreshed = false) {
|
|
529
620
|
validateTokens(tokens);
|
|
530
621
|
const value = this.tokenVault ? this.tokenVault.encryptTokens(tokens) : tokens;
|
|
531
622
|
await this.tokenStore.save(this.subjectId, value, { tokens, refreshed });
|
|
623
|
+
await this.emit({
|
|
624
|
+
type: "session.saved",
|
|
625
|
+
subjectId: this.subjectId,
|
|
626
|
+
refreshed,
|
|
627
|
+
encrypted: typeof value === "string",
|
|
628
|
+
expiresAt: tokens.expiresAt,
|
|
629
|
+
scope: tokens.scope
|
|
630
|
+
});
|
|
532
631
|
}
|
|
533
632
|
async loadTokens() {
|
|
534
633
|
const value = await this.tokenStore.load(this.subjectId);
|
|
535
634
|
if (!value) {
|
|
536
|
-
throw new
|
|
635
|
+
throw new import_connect4.EdgeAuthenticationError("No EDGE tokens are stored for this subject", {
|
|
537
636
|
subjectId: this.subjectId
|
|
538
637
|
});
|
|
539
638
|
}
|
|
540
639
|
if (typeof value === "string") {
|
|
541
640
|
if (!this.tokenVault) {
|
|
542
|
-
throw new
|
|
641
|
+
throw new import_connect4.EdgeAuthenticationError("Encrypted EDGE token envelope loaded but no tokenVault was configured", {
|
|
543
642
|
subjectId: this.subjectId
|
|
544
643
|
});
|
|
545
644
|
}
|
|
546
645
|
const tokens = this.tokenVault.decryptTokens(value);
|
|
547
646
|
validateTokens(tokens);
|
|
647
|
+
await this.emitLoaded(tokens, true);
|
|
548
648
|
return tokens;
|
|
549
649
|
}
|
|
550
650
|
validateTokens(value);
|
|
651
|
+
await this.emitLoaded(value, false);
|
|
551
652
|
return value;
|
|
552
653
|
}
|
|
654
|
+
async refreshTokens(currentTokens) {
|
|
655
|
+
await this.emit({
|
|
656
|
+
type: "session.refresh_started",
|
|
657
|
+
subjectId: this.subjectId,
|
|
658
|
+
expiresAt: currentTokens.expiresAt,
|
|
659
|
+
scope: currentTokens.scope
|
|
660
|
+
});
|
|
661
|
+
try {
|
|
662
|
+
const refreshed = await this.edge.refreshTokens(currentTokens.refreshToken);
|
|
663
|
+
const tokens = {
|
|
664
|
+
...refreshed,
|
|
665
|
+
refreshToken: refreshed.refreshToken || currentTokens.refreshToken
|
|
666
|
+
};
|
|
667
|
+
await this.saveTokens(tokens, true);
|
|
668
|
+
await this.onRefresh?.(tokens);
|
|
669
|
+
await this.emit({
|
|
670
|
+
type: "session.refresh_succeeded",
|
|
671
|
+
subjectId: this.subjectId,
|
|
672
|
+
refreshed: true,
|
|
673
|
+
expiresAt: tokens.expiresAt,
|
|
674
|
+
scope: tokens.scope
|
|
675
|
+
});
|
|
676
|
+
return tokens;
|
|
677
|
+
} catch (error) {
|
|
678
|
+
await this.emit({
|
|
679
|
+
type: "session.refresh_failed",
|
|
680
|
+
subjectId: this.subjectId,
|
|
681
|
+
error: error instanceof Error ? error.message : "Unknown token refresh error"
|
|
682
|
+
});
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
async emitLoaded(tokens, encrypted) {
|
|
687
|
+
await this.emit({
|
|
688
|
+
type: "session.loaded",
|
|
689
|
+
subjectId: this.subjectId,
|
|
690
|
+
encrypted,
|
|
691
|
+
expiresAt: tokens.expiresAt,
|
|
692
|
+
scope: tokens.scope
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
async emit(event) {
|
|
696
|
+
try {
|
|
697
|
+
await this.onEvent?.(event);
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
}
|
|
553
701
|
};
|
|
554
702
|
function createEdgeUserSession(options) {
|
|
555
703
|
return new EdgeUserSession(options);
|
|
@@ -576,7 +724,7 @@ function validateTokens(tokens) {
|
|
|
576
724
|
}
|
|
577
725
|
}
|
|
578
726
|
if (Object.keys(errors).length > 0) {
|
|
579
|
-
throw new
|
|
727
|
+
throw new import_connect4.EdgeValidationError("Invalid EDGE token payload", errors);
|
|
580
728
|
}
|
|
581
729
|
}
|
|
582
730
|
|
|
@@ -601,8 +749,8 @@ var EdgeWebhookReconciler = class {
|
|
|
601
749
|
if (!options.cursorStore?.load || !options.cursorStore?.save) {
|
|
602
750
|
throw new Error("EdgeWebhookReconciler: cursorStore.load and cursorStore.save are required");
|
|
603
751
|
}
|
|
604
|
-
if (!options.processEvent) {
|
|
605
|
-
throw new Error("EdgeWebhookReconciler: processEvent is required");
|
|
752
|
+
if (!options.processEvent && !options.inbox) {
|
|
753
|
+
throw new Error("EdgeWebhookReconciler: processEvent or inbox is required");
|
|
606
754
|
}
|
|
607
755
|
if (options.maxPages !== void 0 && (!Number.isInteger(options.maxPages) || options.maxPages <= 0)) {
|
|
608
756
|
throw new Error("EdgeWebhookReconciler: maxPages must be a positive integer");
|
|
@@ -629,6 +777,8 @@ var EdgeWebhookReconciler = class {
|
|
|
629
777
|
async runInternal() {
|
|
630
778
|
let cursor = normalizeCursor(await this.options.cursorStore.load());
|
|
631
779
|
let processed = 0;
|
|
780
|
+
let accepted = 0;
|
|
781
|
+
let duplicates = 0;
|
|
632
782
|
let pages = 0;
|
|
633
783
|
let hasMore = false;
|
|
634
784
|
const maxPages = this.options.maxPages ?? DEFAULT_MAX_PAGES;
|
|
@@ -642,6 +792,7 @@ var EdgeWebhookReconciler = class {
|
|
|
642
792
|
if (result.events.length === 0) {
|
|
643
793
|
return {
|
|
644
794
|
processed,
|
|
795
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
645
796
|
pages,
|
|
646
797
|
...cursor ? { cursor } : {},
|
|
647
798
|
hasMore,
|
|
@@ -650,14 +801,23 @@ var EdgeWebhookReconciler = class {
|
|
|
650
801
|
};
|
|
651
802
|
}
|
|
652
803
|
for (const event of result.events) {
|
|
653
|
-
|
|
804
|
+
if (this.options.inbox) {
|
|
805
|
+
const acceptResult = await this.options.inbox.acceptSyncedEvent(event);
|
|
806
|
+
accepted++;
|
|
807
|
+
if (acceptResult.duplicate) duplicates++;
|
|
808
|
+
const processResult = await this.options.inbox.process(event.id);
|
|
809
|
+
if (processResult.processed) processed++;
|
|
810
|
+
} else {
|
|
811
|
+
await this.options.processEvent?.(event);
|
|
812
|
+
processed++;
|
|
813
|
+
}
|
|
654
814
|
cursor = event.id;
|
|
655
815
|
await this.options.cursorStore.save(cursor);
|
|
656
|
-
processed++;
|
|
657
816
|
}
|
|
658
817
|
if (!result.hasMore) {
|
|
659
818
|
return {
|
|
660
819
|
processed,
|
|
820
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
661
821
|
pages,
|
|
662
822
|
...cursor ? { cursor } : {},
|
|
663
823
|
hasMore: false,
|
|
@@ -667,6 +827,7 @@ var EdgeWebhookReconciler = class {
|
|
|
667
827
|
}
|
|
668
828
|
return {
|
|
669
829
|
processed,
|
|
830
|
+
...this.options.inbox ? { accepted, duplicates } : {},
|
|
670
831
|
pages,
|
|
671
832
|
...cursor ? { cursor } : {},
|
|
672
833
|
hasMore,
|
|
@@ -754,7 +915,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
754
915
|
throw new Error("EdgeConnectServer: environment is required");
|
|
755
916
|
}
|
|
756
917
|
this.config = config;
|
|
757
|
-
const envConfig = (0,
|
|
918
|
+
const envConfig = (0, import_connect5.getEnvironmentConfig)(config.environment);
|
|
758
919
|
this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
|
|
759
920
|
this.partnerApiBaseUrl = stripTrailingSlash(config.partnerApiBaseUrl || derivePartnerApiBaseUrl(this.apiBaseUrl));
|
|
760
921
|
this.oauthBaseUrl = config.oauthBaseUrl || envConfig.oauthBaseUrl;
|
|
@@ -799,7 +960,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
799
960
|
*/
|
|
800
961
|
forUser(accessToken) {
|
|
801
962
|
if (!accessToken) {
|
|
802
|
-
throw new
|
|
963
|
+
throw new import_connect5.EdgeAuthenticationError("accessToken is required when calling forUser()");
|
|
803
964
|
}
|
|
804
965
|
return new EdgeUserClient(this, accessToken);
|
|
805
966
|
}
|
|
@@ -836,8 +997,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
836
997
|
const data = await response.json();
|
|
837
998
|
return this.parseTokenResponse(data);
|
|
838
999
|
} catch (error) {
|
|
839
|
-
if (error instanceof
|
|
840
|
-
throw new
|
|
1000
|
+
if (error instanceof import_connect5.EdgeError) throw error;
|
|
1001
|
+
throw new import_connect5.EdgeNetworkError("Failed to exchange code", error);
|
|
841
1002
|
}
|
|
842
1003
|
}
|
|
843
1004
|
async refreshTokens(refreshToken) {
|
|
@@ -863,7 +1024,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
863
1024
|
);
|
|
864
1025
|
if (!response.ok) {
|
|
865
1026
|
const error = await response.json().catch(() => ({}));
|
|
866
|
-
throw new
|
|
1027
|
+
throw new import_connect5.EdgeAuthenticationError(
|
|
867
1028
|
error.message || error.error_description || "Token refresh failed. User may need to reconnect.",
|
|
868
1029
|
{ tokenError: error }
|
|
869
1030
|
);
|
|
@@ -871,8 +1032,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
871
1032
|
const data = await response.json();
|
|
872
1033
|
return this.parseTokenResponse(data, refreshToken);
|
|
873
1034
|
} catch (error) {
|
|
874
|
-
if (error instanceof
|
|
875
|
-
throw new
|
|
1035
|
+
if (error instanceof import_connect5.EdgeError) throw error;
|
|
1036
|
+
throw new import_connect5.EdgeNetworkError("Failed to refresh tokens", error);
|
|
876
1037
|
}
|
|
877
1038
|
}
|
|
878
1039
|
/** @internal Called by {@link EdgeUserClient} — not part of the public API. */
|
|
@@ -914,14 +1075,14 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
914
1075
|
if (mleEnabled && typeof rawResponseBody?.jwe === "string") {
|
|
915
1076
|
responseBody = decryptMle(rawResponseBody.jwe, mleConfig);
|
|
916
1077
|
} else if (!mleEnabled && typeof rawResponseBody?.jwe === "string") {
|
|
917
|
-
throw new
|
|
1078
|
+
throw new import_connect5.EdgeApiError(
|
|
918
1079
|
"mle_required",
|
|
919
1080
|
"The API responded with message-level encryption. Enable MLE in SDK config.",
|
|
920
1081
|
response.status,
|
|
921
1082
|
rawResponseBody
|
|
922
1083
|
);
|
|
923
1084
|
} else if (mleEnabled && mleConfig?.strictResponseEncryption !== false && response.ok && typeof rawResponseBody?.jwe !== "string") {
|
|
924
|
-
throw new
|
|
1085
|
+
throw new import_connect5.EdgeApiError(
|
|
925
1086
|
"mle_response_missing",
|
|
926
1087
|
"Expected encrypted response payload but received plaintext.",
|
|
927
1088
|
response.status,
|
|
@@ -939,8 +1100,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
939
1100
|
}
|
|
940
1101
|
return responseBody;
|
|
941
1102
|
} catch (error) {
|
|
942
|
-
if (error instanceof
|
|
943
|
-
if (!(error instanceof
|
|
1103
|
+
if (error instanceof import_connect5.EdgeError) {
|
|
1104
|
+
if (!(error instanceof import_connect5.EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
|
|
944
1105
|
throw error;
|
|
945
1106
|
}
|
|
946
1107
|
lastError = error;
|
|
@@ -948,11 +1109,11 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
948
1109
|
}
|
|
949
1110
|
lastError = error;
|
|
950
1111
|
if (attempt >= this.retryConfig.maxRetries) {
|
|
951
|
-
throw new
|
|
1112
|
+
throw new import_connect5.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
|
|
952
1113
|
}
|
|
953
1114
|
}
|
|
954
1115
|
}
|
|
955
|
-
throw new
|
|
1116
|
+
throw new import_connect5.EdgeNetworkError(
|
|
956
1117
|
`API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`,
|
|
957
1118
|
lastError
|
|
958
1119
|
);
|
|
@@ -996,10 +1157,10 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
996
1157
|
*/
|
|
997
1158
|
async syncWebhookEvents(options = {}) {
|
|
998
1159
|
if (!this.config.partnerClientId) {
|
|
999
|
-
throw new
|
|
1160
|
+
throw new import_connect5.EdgeAuthenticationError("syncWebhookEvents requires `partnerClientId` in EdgeConnectServer config");
|
|
1000
1161
|
}
|
|
1001
1162
|
if (!this.config.partnerClientSecret) {
|
|
1002
|
-
throw new
|
|
1163
|
+
throw new import_connect5.EdgeAuthenticationError("syncWebhookEvents requires `partnerClientSecret` in EdgeConnectServer config");
|
|
1003
1164
|
}
|
|
1004
1165
|
const normalizedOptions = this.normalizeSyncWebhookEventsOptions(options);
|
|
1005
1166
|
const url = this.buildSyncUrl(normalizedOptions);
|
|
@@ -1030,7 +1191,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
1030
1191
|
endpoint: "partner_webhook_sync",
|
|
1031
1192
|
attempt
|
|
1032
1193
|
});
|
|
1033
|
-
throw new
|
|
1194
|
+
throw new import_connect5.EdgeAuthenticationError(
|
|
1034
1195
|
body.message || "Partner token rejected after refresh \u2014 check partnerClientId/Secret",
|
|
1035
1196
|
body
|
|
1036
1197
|
);
|
|
@@ -1047,7 +1208,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
1047
1208
|
endpoint: "partner_webhook_sync",
|
|
1048
1209
|
attempt
|
|
1049
1210
|
});
|
|
1050
|
-
throw new
|
|
1211
|
+
throw new import_connect5.EdgeApiError(
|
|
1051
1212
|
body.error || "sync_failed",
|
|
1052
1213
|
body.message || `syncWebhookEvents failed with status ${response.status}`,
|
|
1053
1214
|
response.status,
|
|
@@ -1093,7 +1254,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
1093
1254
|
}
|
|
1094
1255
|
if (options.limit !== void 0) {
|
|
1095
1256
|
if (!Number.isInteger(options.limit) || options.limit < SYNC_WEBHOOK_EVENTS_MIN_LIMIT || options.limit > SYNC_WEBHOOK_EVENTS_MAX_LIMIT) {
|
|
1096
|
-
throw new
|
|
1257
|
+
throw new import_connect5.EdgeApiError(
|
|
1097
1258
|
"invalid_sync_options",
|
|
1098
1259
|
`syncWebhookEvents limit must be an integer between ${SYNC_WEBHOOK_EVENTS_MIN_LIMIT} and ${SYNC_WEBHOOK_EVENTS_MAX_LIMIT}`,
|
|
1099
1260
|
400,
|
|
@@ -1163,11 +1324,11 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
1163
1324
|
} catch (error) {
|
|
1164
1325
|
lastError = error;
|
|
1165
1326
|
if (attempt >= this.retryConfig.maxRetries) {
|
|
1166
|
-
throw new
|
|
1327
|
+
throw new import_connect5.EdgeNetworkError("syncWebhookEvents network failure", lastError);
|
|
1167
1328
|
}
|
|
1168
1329
|
}
|
|
1169
1330
|
}
|
|
1170
|
-
throw new
|
|
1331
|
+
throw new import_connect5.EdgeNetworkError(
|
|
1171
1332
|
`syncWebhookEvents network failure after ${this.retryConfig.maxRetries} retries`,
|
|
1172
1333
|
lastError
|
|
1173
1334
|
);
|
|
@@ -1215,7 +1376,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
1215
1376
|
this.dispatcher
|
|
1216
1377
|
);
|
|
1217
1378
|
} catch (error) {
|
|
1218
|
-
throw new
|
|
1379
|
+
throw new import_connect5.EdgeNetworkError("Partner token request network failure", error);
|
|
1219
1380
|
}
|
|
1220
1381
|
const durationMs = Date.now() - startTime;
|
|
1221
1382
|
const data = await response.json().catch(() => ({}));
|
|
@@ -1228,13 +1389,13 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
1228
1389
|
endpoint: "oauth_token"
|
|
1229
1390
|
});
|
|
1230
1391
|
if (!response.ok) {
|
|
1231
|
-
throw new
|
|
1392
|
+
throw new import_connect5.EdgeAuthenticationError(
|
|
1232
1393
|
data.message || data.error_description || `Partner token request failed with status ${response.status}`,
|
|
1233
1394
|
data
|
|
1234
1395
|
);
|
|
1235
1396
|
}
|
|
1236
1397
|
if (!data.access_token || typeof data.expires_in !== "number") {
|
|
1237
|
-
throw new
|
|
1398
|
+
throw new import_connect5.EdgeAuthenticationError("Partner token response missing access_token or expires_in");
|
|
1238
1399
|
}
|
|
1239
1400
|
this.partnerTokenCache = {
|
|
1240
1401
|
token: data.access_token,
|
|
@@ -1309,37 +1470,37 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
1309
1470
|
const errorCode = error.error;
|
|
1310
1471
|
const errorMessage = error.message || error.error_description;
|
|
1311
1472
|
if (errorCode === "invalid_grant" || errorMessage?.includes("Invalid or expired")) {
|
|
1312
|
-
return new
|
|
1473
|
+
return new import_connect5.EdgeTokenExchangeError("Authorization code is invalid, expired, or already used. Please try again.", {
|
|
1313
1474
|
edgeBoostError: error
|
|
1314
1475
|
});
|
|
1315
1476
|
}
|
|
1316
1477
|
if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
|
|
1317
|
-
return new
|
|
1478
|
+
return new import_connect5.EdgeAuthenticationError("Invalid client credentials. Check your client ID and secret.", {
|
|
1318
1479
|
edgeBoostError: error
|
|
1319
1480
|
});
|
|
1320
1481
|
}
|
|
1321
1482
|
if (errorMessage?.includes("code_verifier") || errorMessage?.includes("PKCE")) {
|
|
1322
|
-
return new
|
|
1483
|
+
return new import_connect5.EdgeTokenExchangeError("PKCE verification failed. Please try again.", { edgeBoostError: error });
|
|
1323
1484
|
}
|
|
1324
|
-
return new
|
|
1485
|
+
return new import_connect5.EdgeTokenExchangeError(errorMessage || "Failed to exchange authorization code", {
|
|
1325
1486
|
edgeBoostError: error,
|
|
1326
1487
|
statusCode: status
|
|
1327
1488
|
});
|
|
1328
1489
|
}
|
|
1329
1490
|
async handleApiErrorFromBody(error, status, path) {
|
|
1330
1491
|
if (status === 401) {
|
|
1331
|
-
return new
|
|
1492
|
+
return new import_connect5.EdgeAuthenticationError(error.message || "Access token is invalid or expired", error);
|
|
1332
1493
|
}
|
|
1333
1494
|
if (status === 403) {
|
|
1334
1495
|
if (error.error === "consent_required") {
|
|
1335
|
-
return new
|
|
1496
|
+
return new import_connect5.EdgeConsentRequiredError(
|
|
1336
1497
|
this.config.clientId,
|
|
1337
1498
|
error.consentUrl,
|
|
1338
1499
|
error.message
|
|
1339
1500
|
);
|
|
1340
1501
|
}
|
|
1341
1502
|
if (error.error === "insufficient_scope" || error.error === "insufficient_consent") {
|
|
1342
|
-
return new
|
|
1503
|
+
return new import_connect5.EdgeInsufficientScopeError(
|
|
1343
1504
|
error.missing_scopes || error.missingScopes || [],
|
|
1344
1505
|
error.message
|
|
1345
1506
|
);
|
|
@@ -1361,15 +1522,15 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
1361
1522
|
} else if (path.includes("/transfer")) {
|
|
1362
1523
|
resourceType = "Transfer";
|
|
1363
1524
|
}
|
|
1364
|
-
return new
|
|
1525
|
+
return new import_connect5.EdgeNotFoundError(resourceType, resourceId);
|
|
1365
1526
|
}
|
|
1366
1527
|
if (status === 422 && error.error === "identity_verification_failed") {
|
|
1367
|
-
return new
|
|
1528
|
+
return new import_connect5.EdgeIdentityVerificationError(
|
|
1368
1529
|
error.fieldErrors || {},
|
|
1369
1530
|
error.message
|
|
1370
1531
|
);
|
|
1371
1532
|
}
|
|
1372
|
-
return new
|
|
1533
|
+
return new import_connect5.EdgeApiError(
|
|
1373
1534
|
error.error || "api_error",
|
|
1374
1535
|
error.message || error.error_description || `Request failed with status ${status}`,
|
|
1375
1536
|
status,
|
|
@@ -1379,9 +1540,9 @@ var EdgeConnectServer = class _EdgeConnectServer {
|
|
|
1379
1540
|
};
|
|
1380
1541
|
|
|
1381
1542
|
// src/env.ts
|
|
1382
|
-
var
|
|
1543
|
+
var import_connect6 = require("@edge-markets/connect");
|
|
1383
1544
|
var import_node_fs2 = __toESM(require("fs"));
|
|
1384
|
-
var ENVIRONMENT_NAMES = new Set(Object.keys(
|
|
1545
|
+
var ENVIRONMENT_NAMES = new Set(Object.keys(import_connect6.EDGE_ENVIRONMENTS));
|
|
1385
1546
|
function readEnv(env, name) {
|
|
1386
1547
|
const value = env[name];
|
|
1387
1548
|
if (typeof value !== "string") return void 0;
|
|
@@ -1414,7 +1575,7 @@ function readPemValue(env, valueName, pathName, warnings) {
|
|
|
1414
1575
|
return normalizePem(import_node_fs2.default.readFileSync(filePath, "utf8"));
|
|
1415
1576
|
} catch (error) {
|
|
1416
1577
|
const message = error instanceof Error ? error.message : String(error);
|
|
1417
|
-
throw new
|
|
1578
|
+
throw new import_connect6.EdgeValidationError(`Failed to read ${pathName}`, {
|
|
1418
1579
|
[pathName]: [`Unable to read PEM file "${filePath}": ${message}`]
|
|
1419
1580
|
});
|
|
1420
1581
|
}
|
|
@@ -1422,19 +1583,29 @@ function readPemValue(env, valueName, pathName, warnings) {
|
|
|
1422
1583
|
function readEnvironment(env) {
|
|
1423
1584
|
const value = readEnv(env, "EDGE_ENVIRONMENT") || "staging";
|
|
1424
1585
|
if (!ENVIRONMENT_NAMES.has(value)) {
|
|
1425
|
-
throw new
|
|
1586
|
+
throw new import_connect6.EdgeValidationError("Invalid EDGE_ENVIRONMENT", {
|
|
1426
1587
|
EDGE_ENVIRONMENT: [`Must be one of: ${Array.from(ENVIRONMENT_NAMES).join(", ")}`]
|
|
1427
1588
|
});
|
|
1428
1589
|
}
|
|
1429
1590
|
return value;
|
|
1430
1591
|
}
|
|
1592
|
+
function readBooleanEnv(env, name) {
|
|
1593
|
+
const value = readEnv(env, name);
|
|
1594
|
+
if (!value) return void 0;
|
|
1595
|
+
const normalized = value.toLowerCase();
|
|
1596
|
+
if (["true", "1", "yes"].includes(normalized)) return true;
|
|
1597
|
+
if (["false", "0", "no"].includes(normalized)) return false;
|
|
1598
|
+
throw new import_connect6.EdgeValidationError(`Invalid ${name}`, {
|
|
1599
|
+
[name]: ["Must be one of: true, false, 1, 0, yes, no"]
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1431
1602
|
function readTimeout(env, override) {
|
|
1432
1603
|
if (override !== void 0) return override;
|
|
1433
1604
|
const value = readFirstEnv(env, ["EDGE_TIMEOUT_MS", "EDGE_REQUEST_TIMEOUT_MS"]);
|
|
1434
1605
|
if (!value) return void 0;
|
|
1435
1606
|
const timeout = Number(value);
|
|
1436
1607
|
if (!Number.isInteger(timeout) || timeout <= 0) {
|
|
1437
|
-
throw new
|
|
1608
|
+
throw new import_connect6.EdgeValidationError("Invalid EDGE_TIMEOUT_MS", {
|
|
1438
1609
|
EDGE_TIMEOUT_MS: ["Must be a positive integer number of milliseconds"]
|
|
1439
1610
|
});
|
|
1440
1611
|
}
|
|
@@ -1443,10 +1614,15 @@ function readTimeout(env, override) {
|
|
|
1443
1614
|
function buildMtlsConfig(env, requireMtls, warnings) {
|
|
1444
1615
|
const cert = readPemValue(env, "EDGE_MTLS_CERT", "EDGE_MTLS_CERT_PATH", warnings);
|
|
1445
1616
|
const key = readPemValue(env, "EDGE_MTLS_KEY", "EDGE_MTLS_KEY_PATH", warnings);
|
|
1446
|
-
const
|
|
1617
|
+
const serverCa = readPemValue(env, "EDGE_SERVER_CA_PEM", "EDGE_SERVER_CA_PEM_PATH", warnings);
|
|
1618
|
+
const hasLegacyCa = !!readFirstEnv(env, ["EDGE_MTLS_CA", "EDGE_MTLS_CA_PATH"]);
|
|
1619
|
+
if (serverCa && hasLegacyCa) {
|
|
1620
|
+
warnings.push("EDGE_SERVER_CA_PEM and EDGE_MTLS_CA are both set; using EDGE_SERVER_CA_PEM");
|
|
1621
|
+
}
|
|
1622
|
+
const ca = serverCa || readPemValue(env, "EDGE_MTLS_CA", "EDGE_MTLS_CA_PATH", warnings);
|
|
1447
1623
|
if (!cert && !key && !ca) {
|
|
1448
1624
|
if (requireMtls) {
|
|
1449
|
-
throw new
|
|
1625
|
+
throw new import_connect6.EdgeValidationError("mTLS is required but no EDGE_MTLS_CERT or EDGE_MTLS_KEY was provided", {
|
|
1450
1626
|
EDGE_MTLS_CERT: ["Required when requireMtls is true"],
|
|
1451
1627
|
EDGE_MTLS_KEY: ["Required when requireMtls is true"]
|
|
1452
1628
|
});
|
|
@@ -1454,7 +1630,7 @@ function buildMtlsConfig(env, requireMtls, warnings) {
|
|
|
1454
1630
|
return void 0;
|
|
1455
1631
|
}
|
|
1456
1632
|
if (!cert || !key) {
|
|
1457
|
-
throw new
|
|
1633
|
+
throw new import_connect6.EdgeValidationError("Incomplete mTLS configuration", {
|
|
1458
1634
|
EDGE_MTLS_CERT: cert ? [] : ["Required when mTLS is configured"],
|
|
1459
1635
|
EDGE_MTLS_KEY: key ? [] : ["Required when mTLS is configured"]
|
|
1460
1636
|
});
|
|
@@ -1468,7 +1644,7 @@ function buildMtlsConfig(env, requireMtls, warnings) {
|
|
|
1468
1644
|
}
|
|
1469
1645
|
function inferMtlsRequirement(environment, apiBaseUrl) {
|
|
1470
1646
|
if (environment === "staging" || environment === "production") return true;
|
|
1471
|
-
const resolvedApiBaseUrl = apiBaseUrl || (0,
|
|
1647
|
+
const resolvedApiBaseUrl = apiBaseUrl || (0, import_connect6.getEnvironmentConfig)(environment).apiBaseUrl;
|
|
1472
1648
|
try {
|
|
1473
1649
|
const host = new URL(resolvedApiBaseUrl).host;
|
|
1474
1650
|
return host === "connect-staging.edgeboost.io" || host === "connect.edgeboost.io";
|
|
@@ -1482,7 +1658,7 @@ function createEdgeConnectServerFromEnv(options = {}) {
|
|
|
1482
1658
|
const environment = readEnvironment(env);
|
|
1483
1659
|
const apiBaseUrl = readEnv(env, "EDGE_API_BASE_URL");
|
|
1484
1660
|
const mtlsRequiredByTarget = inferMtlsRequirement(environment, apiBaseUrl);
|
|
1485
|
-
const requireMtls = options.requireMtls ?? false;
|
|
1661
|
+
const requireMtls = options.requireMtls ?? readBooleanEnv(env, "EDGE_REQUIRE_MTLS") ?? false;
|
|
1486
1662
|
const mtls = buildMtlsConfig(env, requireMtls, warnings);
|
|
1487
1663
|
const timeout = readTimeout(env, options.timeout);
|
|
1488
1664
|
const clientId = readFirstEnv(env, ["EDGE_CLIENT_ID", "EDGE_CONNECT_CLIENT_ID"]) || "";
|
|
@@ -1495,7 +1671,7 @@ function createEdgeConnectServerFromEnv(options = {}) {
|
|
|
1495
1671
|
if (!clientId) validationErrors.EDGE_CLIENT_ID = ["Required"];
|
|
1496
1672
|
if (!clientSecret) validationErrors.EDGE_CLIENT_SECRET = ["Required"];
|
|
1497
1673
|
if (Object.keys(validationErrors).length > 0) {
|
|
1498
|
-
throw new
|
|
1674
|
+
throw new import_connect6.EdgeValidationError("Missing required EDGE Connect environment variables", validationErrors);
|
|
1499
1675
|
}
|
|
1500
1676
|
if (mtlsRequiredByTarget && !mtls) {
|
|
1501
1677
|
warnings.push(
|
|
@@ -1534,32 +1710,35 @@ function createEdgeConnectServerFromEnv(options = {}) {
|
|
|
1534
1710
|
}
|
|
1535
1711
|
|
|
1536
1712
|
// src/error-adapter.ts
|
|
1537
|
-
var
|
|
1713
|
+
var import_connect7 = require("@edge-markets/connect");
|
|
1538
1714
|
function toEdgeHttpError(error) {
|
|
1539
1715
|
const body = toEdgeProblemJson(error);
|
|
1540
1716
|
return { status: body.status, body };
|
|
1541
1717
|
}
|
|
1542
1718
|
function toEdgeProblemJson(error) {
|
|
1543
|
-
if (error instanceof
|
|
1719
|
+
if (error instanceof import_connect7.EdgeConsentRequiredError) {
|
|
1544
1720
|
return problem(error, "Consent required", error.statusCode ?? 403, {
|
|
1545
1721
|
clientId: error.clientId,
|
|
1546
1722
|
consentUrl: error.consentUrl
|
|
1547
1723
|
});
|
|
1548
1724
|
}
|
|
1549
|
-
if (error instanceof
|
|
1725
|
+
if (error instanceof import_connect7.EdgeValidationError) {
|
|
1550
1726
|
return problem(error, "Validation failed", error.statusCode ?? 400, {
|
|
1551
1727
|
validationErrors: error.validationErrors
|
|
1552
1728
|
});
|
|
1553
1729
|
}
|
|
1554
|
-
if (error instanceof
|
|
1730
|
+
if (error instanceof import_connect7.EdgeIdentityVerificationError) {
|
|
1555
1731
|
return problem(error, "Identity verification failed", error.statusCode ?? 422, {
|
|
1556
1732
|
fieldErrors: error.fieldErrors
|
|
1557
1733
|
});
|
|
1558
1734
|
}
|
|
1559
|
-
if (error instanceof
|
|
1735
|
+
if (error instanceof import_connect7.EdgeFeatureUnavailableError) {
|
|
1736
|
+
return problem(error, "Feature unavailable", error.statusCode ?? 501);
|
|
1737
|
+
}
|
|
1738
|
+
if (error instanceof import_connect7.EdgeNetworkError) {
|
|
1560
1739
|
return problem(error, "EDGE network error", error.statusCode ?? 502);
|
|
1561
1740
|
}
|
|
1562
|
-
if (error instanceof
|
|
1741
|
+
if (error instanceof import_connect7.EdgeError) {
|
|
1563
1742
|
return problem(error, edgeTitle(error), error.statusCode ?? 500, error.details);
|
|
1564
1743
|
}
|
|
1565
1744
|
const message = error instanceof Error ? error.message : "Unexpected error";
|
|
@@ -1585,8 +1764,225 @@ function edgeTitle(error) {
|
|
|
1585
1764
|
return error.code.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1586
1765
|
}
|
|
1587
1766
|
|
|
1767
|
+
// src/http-helpers.ts
|
|
1768
|
+
var import_connect9 = require("@edge-markets/connect");
|
|
1769
|
+
|
|
1770
|
+
// src/webhook-parser.ts
|
|
1771
|
+
var import_connect8 = require("@edge-markets/connect");
|
|
1772
|
+
|
|
1773
|
+
// src/webhook-signing.ts
|
|
1774
|
+
var import_node_crypto2 = __toESM(require("crypto"));
|
|
1775
|
+
function verifyWebhookSignature(header, body, secret, options = {}) {
|
|
1776
|
+
if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
const requestedTolerance = options.toleranceSeconds;
|
|
1780
|
+
const toleranceSeconds = typeof requestedTolerance === "number" && Number.isFinite(requestedTolerance) && requestedTolerance >= 0 ? requestedTolerance : 300;
|
|
1781
|
+
const parts = header.split(",").map((p) => p.trim());
|
|
1782
|
+
const tPart = parts.find((p) => p.startsWith("t="));
|
|
1783
|
+
const v1Part = parts.find((p) => p.startsWith("v1="));
|
|
1784
|
+
if (!tPart || !v1Part) return false;
|
|
1785
|
+
const timestamp = parseInt(tPart.slice(2), 10);
|
|
1786
|
+
const signature = v1Part.slice(3);
|
|
1787
|
+
if (isNaN(timestamp) || !signature) return false;
|
|
1788
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1789
|
+
if (Math.abs(now - timestamp) > toleranceSeconds) return false;
|
|
1790
|
+
const signedContent = `${timestamp}.${body}`;
|
|
1791
|
+
const expectedHmac = import_node_crypto2.default.createHmac("sha256", secret).update(signedContent).digest("hex");
|
|
1792
|
+
try {
|
|
1793
|
+
const sigBuf = Buffer.from(signature, "hex");
|
|
1794
|
+
const expBuf = Buffer.from(expectedHmac, "hex");
|
|
1795
|
+
if (sigBuf.length !== expBuf.length) return false;
|
|
1796
|
+
return import_node_crypto2.default.timingSafeEqual(sigBuf, expBuf);
|
|
1797
|
+
} catch {
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// src/webhook-parser.ts
|
|
1803
|
+
var EVENT_TYPES = new Set(import_connect8.EDGE_WEBHOOK_EVENT_TYPES);
|
|
1804
|
+
var TRANSFER_TYPES = /* @__PURE__ */ new Set(["debit", "credit"]);
|
|
1805
|
+
function parseAndVerifyWebhook(options) {
|
|
1806
|
+
const body = normalizeRawBody(options.rawBody);
|
|
1807
|
+
const header = normalizeSignatureHeader(options.signatureHeader);
|
|
1808
|
+
const signatureTimestamp = extractWebhookSignatureTimestamp(header);
|
|
1809
|
+
if (!header || !signatureTimestamp) {
|
|
1810
|
+
throw new import_connect8.EdgeAuthenticationError("Missing or malformed EDGE webhook signature");
|
|
1811
|
+
}
|
|
1812
|
+
if (!verifyWebhookSignature(header, body, options.secret, { toleranceSeconds: options.toleranceSeconds })) {
|
|
1813
|
+
throw new import_connect8.EdgeAuthenticationError("Invalid EDGE webhook signature");
|
|
1814
|
+
}
|
|
1815
|
+
let parsed;
|
|
1816
|
+
try {
|
|
1817
|
+
parsed = JSON.parse(body);
|
|
1818
|
+
} catch {
|
|
1819
|
+
throw new import_connect8.EdgeValidationError("EDGE webhook body must be valid JSON", {
|
|
1820
|
+
rawBody: ["Unable to parse JSON after signature verification"]
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
const event = validateEdgeWebhookEvent(parsed);
|
|
1824
|
+
return {
|
|
1825
|
+
event,
|
|
1826
|
+
eventId: event.id,
|
|
1827
|
+
eventType: event.type,
|
|
1828
|
+
signatureTimestamp,
|
|
1829
|
+
receivedAt: options.receivedAt ?? /* @__PURE__ */ new Date()
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
function extractWebhookSignatureTimestamp(header) {
|
|
1833
|
+
const normalizedHeader = normalizeSignatureHeader(header);
|
|
1834
|
+
if (!normalizedHeader) return void 0;
|
|
1835
|
+
const part = normalizedHeader.split(",").map((value) => value.trim()).find((value) => value.startsWith("t="));
|
|
1836
|
+
if (!part) return void 0;
|
|
1837
|
+
const timestamp = Number(part.slice(2));
|
|
1838
|
+
if (!Number.isInteger(timestamp) || timestamp <= 0) return void 0;
|
|
1839
|
+
return timestamp;
|
|
1840
|
+
}
|
|
1841
|
+
function isEdgeWebhookEvent(value) {
|
|
1842
|
+
try {
|
|
1843
|
+
validateEdgeWebhookEvent(value);
|
|
1844
|
+
return true;
|
|
1845
|
+
} catch {
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
function validateEdgeWebhookEvent(value) {
|
|
1850
|
+
const errors = {};
|
|
1851
|
+
if (!isRecord(value)) {
|
|
1852
|
+
throw new import_connect8.EdgeValidationError("EDGE webhook event must be an object", {
|
|
1853
|
+
event: ["Expected JSON object"]
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
const id = value.id;
|
|
1857
|
+
const type = value.type;
|
|
1858
|
+
const createdAt = value.created_at;
|
|
1859
|
+
const data = value.data;
|
|
1860
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
1861
|
+
errors.id = ["Required string"];
|
|
1862
|
+
}
|
|
1863
|
+
if (typeof type !== "string" || !EVENT_TYPES.has(type)) {
|
|
1864
|
+
errors.type = [`Must be one of: ${import_connect8.EDGE_WEBHOOK_EVENT_TYPES.join(", ")}`];
|
|
1865
|
+
}
|
|
1866
|
+
if (typeof createdAt !== "string" || !createdAt.trim() || Number.isNaN(Date.parse(createdAt))) {
|
|
1867
|
+
errors.created_at = ["Required ISO 8601 timestamp string"];
|
|
1868
|
+
}
|
|
1869
|
+
if (!isRecord(data)) {
|
|
1870
|
+
errors.data = ["Required object"];
|
|
1871
|
+
}
|
|
1872
|
+
if (Object.keys(errors).length === 0) {
|
|
1873
|
+
validateKnownEventData(type, data, errors);
|
|
1874
|
+
}
|
|
1875
|
+
if (Object.keys(errors).length > 0) {
|
|
1876
|
+
throw new import_connect8.EdgeValidationError("Invalid EDGE webhook event", errors);
|
|
1877
|
+
}
|
|
1878
|
+
return value;
|
|
1879
|
+
}
|
|
1880
|
+
function validateKnownEventData(type, data, errors) {
|
|
1881
|
+
switch (type) {
|
|
1882
|
+
case "transfer.completed":
|
|
1883
|
+
validateTransferEventData(data, "completed", errors);
|
|
1884
|
+
return;
|
|
1885
|
+
case "transfer.failed":
|
|
1886
|
+
validateTransferEventData(data, "failed", errors);
|
|
1887
|
+
if (data.reason !== void 0 && typeof data.reason !== "string") {
|
|
1888
|
+
errors["data.reason"] = ["Must be a string when provided"];
|
|
1889
|
+
}
|
|
1890
|
+
return;
|
|
1891
|
+
case "transfer.expired":
|
|
1892
|
+
validateTransferEventData(data, "expired", errors);
|
|
1893
|
+
return;
|
|
1894
|
+
case "transfer.processing":
|
|
1895
|
+
validateTransferEventData(data, "processing", errors);
|
|
1896
|
+
return;
|
|
1897
|
+
case "consent.revoked":
|
|
1898
|
+
validateRequiredString(data, "userId", errors);
|
|
1899
|
+
validateRequiredString(data, "clientId", errors);
|
|
1900
|
+
validateRequiredIsoTimestamp(data, "revokedAt", errors);
|
|
1901
|
+
return;
|
|
1902
|
+
default: {
|
|
1903
|
+
const exhaustive = type;
|
|
1904
|
+
throw new import_connect8.EdgeValidationError("Unsupported EDGE webhook event type", {
|
|
1905
|
+
type: [`Unsupported event type: ${exhaustive}`]
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
function validateTransferEventData(data, status, errors) {
|
|
1911
|
+
validateRequiredString(data, "transferId", errors);
|
|
1912
|
+
if (data.status !== status) {
|
|
1913
|
+
errors["data.status"] = [`Must be "${status}" for this event type`];
|
|
1914
|
+
}
|
|
1915
|
+
if (typeof data.type !== "string" || !TRANSFER_TYPES.has(data.type)) {
|
|
1916
|
+
errors["data.type"] = ['Must be "debit" or "credit"'];
|
|
1917
|
+
}
|
|
1918
|
+
validateRequiredString(data, "amount", errors);
|
|
1919
|
+
}
|
|
1920
|
+
function validateRequiredString(data, field, errors) {
|
|
1921
|
+
if (typeof data[field] !== "string" || !data[field].trim()) {
|
|
1922
|
+
errors[`data.${field}`] = ["Required string"];
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
function validateRequiredIsoTimestamp(data, field, errors) {
|
|
1926
|
+
if (typeof data[field] !== "string" || !data[field].trim() || Number.isNaN(Date.parse(data[field]))) {
|
|
1927
|
+
errors[`data.${field}`] = ["Required ISO 8601 timestamp string"];
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
function normalizeRawBody(rawBody) {
|
|
1931
|
+
if (typeof rawBody === "string") {
|
|
1932
|
+
return rawBody;
|
|
1933
|
+
}
|
|
1934
|
+
if (Buffer.isBuffer(rawBody) || rawBody instanceof Uint8Array) {
|
|
1935
|
+
return Buffer.from(rawBody).toString("utf8");
|
|
1936
|
+
}
|
|
1937
|
+
throw new import_connect8.EdgeValidationError("EDGE webhook raw body is required", {
|
|
1938
|
+
rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
function normalizeSignatureHeader(header) {
|
|
1942
|
+
if (Array.isArray(header)) {
|
|
1943
|
+
return header.map((value) => value.trim()).filter(Boolean).join(",");
|
|
1944
|
+
}
|
|
1945
|
+
if (typeof header !== "string") return void 0;
|
|
1946
|
+
const trimmed = header.trim();
|
|
1947
|
+
return trimmed || void 0;
|
|
1948
|
+
}
|
|
1949
|
+
function isRecord(value) {
|
|
1950
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/http-helpers.ts
|
|
1954
|
+
function getEdgeWebhookRawBody(request) {
|
|
1955
|
+
if (typeof request.rawBody === "string" || Buffer.isBuffer(request.rawBody) || request.rawBody instanceof Uint8Array) {
|
|
1956
|
+
return request.rawBody;
|
|
1957
|
+
}
|
|
1958
|
+
throw new import_connect9.EdgeValidationError("EDGE webhook raw body is required", {
|
|
1959
|
+
rawBody: [
|
|
1960
|
+
"Configure your framework to expose the raw UTF-8 request body before JSON parsing. Parsed JSON bodies cannot be used for HMAC verification."
|
|
1961
|
+
]
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
function getEdgeWebhookSignatureHeader(request, headerName = "x-edge-signature") {
|
|
1965
|
+
const headers = request.headers ?? {};
|
|
1966
|
+
const lowerHeaderName = headerName.toLowerCase();
|
|
1967
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1968
|
+
if (name.toLowerCase() === lowerHeaderName) {
|
|
1969
|
+
if (typeof value === "string" || Array.isArray(value)) return value;
|
|
1970
|
+
if (value === void 0 || value === null) return void 0;
|
|
1971
|
+
return String(value);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
return void 0;
|
|
1975
|
+
}
|
|
1976
|
+
function parseWebhookHttpRequest(options) {
|
|
1977
|
+
return parseAndVerifyWebhook({
|
|
1978
|
+
...options,
|
|
1979
|
+
rawBody: getEdgeWebhookRawBody(options.request),
|
|
1980
|
+
signatureHeader: getEdgeWebhookSignatureHeader(options.request, options.signatureHeaderName)
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1588
1984
|
// src/identity.ts
|
|
1589
|
-
var
|
|
1985
|
+
var import_connect10 = require("@edge-markets/connect");
|
|
1590
1986
|
var DEFAULT_IDENTITY_POLICY = {
|
|
1591
1987
|
minNameScore: 70,
|
|
1592
1988
|
minAddressScore: 65,
|
|
@@ -1614,7 +2010,7 @@ function buildVerifyIdentityPayload(profile) {
|
|
|
1614
2010
|
errors.address = ["At least one address field is required"];
|
|
1615
2011
|
}
|
|
1616
2012
|
if (Object.keys(errors).length > 0) {
|
|
1617
|
-
throw new
|
|
2013
|
+
throw new import_connect10.EdgeValidationError("Partner identity profile is incomplete", errors);
|
|
1618
2014
|
}
|
|
1619
2015
|
return payload;
|
|
1620
2016
|
}
|
|
@@ -1651,7 +2047,7 @@ function validatePolicy(policy) {
|
|
|
1651
2047
|
if (!isScore(policy.minNameScore)) errors.minNameScore = ["Must be between 0 and 100"];
|
|
1652
2048
|
if (!isScore(policy.minAddressScore)) errors.minAddressScore = ["Must be between 0 and 100"];
|
|
1653
2049
|
if (Object.keys(errors).length > 0) {
|
|
1654
|
-
throw new
|
|
2050
|
+
throw new import_connect10.EdgeValidationError("Invalid identity policy", errors);
|
|
1655
2051
|
}
|
|
1656
2052
|
}
|
|
1657
2053
|
function isScore(value) {
|
|
@@ -1667,9 +2063,155 @@ function firstNonEmpty(...values) {
|
|
|
1667
2063
|
return "";
|
|
1668
2064
|
}
|
|
1669
2065
|
|
|
2066
|
+
// src/session-store.ts
|
|
2067
|
+
var import_connect11 = require("@edge-markets/connect");
|
|
2068
|
+
function createSessionTokenRecord(subjectId, tokens, tokenVault, options = {}) {
|
|
2069
|
+
const normalizedSubjectId = normalizeRequiredString(subjectId, "subjectId");
|
|
2070
|
+
validateTokens2(tokens);
|
|
2071
|
+
return {
|
|
2072
|
+
subjectId: normalizedSubjectId,
|
|
2073
|
+
encryptedTokens: tokenVault.encryptTokens(tokens),
|
|
2074
|
+
expiresAt: normalizeSessionExpiresAt(tokens.expiresAt),
|
|
2075
|
+
scopes: options.scopes ?? splitScopes(tokens.scope),
|
|
2076
|
+
...options.edgeUserId ? { edgeUserId: options.edgeUserId } : {},
|
|
2077
|
+
...options.keyId ? { keyId: options.keyId } : {},
|
|
2078
|
+
...options.metadata ? { metadata: options.metadata } : {}
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
function parseSessionTokenRecord(record) {
|
|
2082
|
+
if (!isRecord2(record)) {
|
|
2083
|
+
throw new import_connect11.EdgeValidationError("EDGE session token record must be an object", {
|
|
2084
|
+
record: ["Expected object"]
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
const subjectId = normalizeRequiredString(record.subjectId, "subjectId");
|
|
2088
|
+
const encryptedTokens = normalizeRequiredString(record.encryptedTokens, "encryptedTokens");
|
|
2089
|
+
const expiresAt = normalizeSessionExpiresAt(record.expiresAt);
|
|
2090
|
+
const scopes = record.scopes === void 0 ? void 0 : normalizeScopes(record.scopes);
|
|
2091
|
+
return {
|
|
2092
|
+
subjectId,
|
|
2093
|
+
encryptedTokens,
|
|
2094
|
+
expiresAt,
|
|
2095
|
+
...scopes ? { scopes } : {},
|
|
2096
|
+
...typeof record.edgeUserId === "string" && record.edgeUserId.trim() ? { edgeUserId: record.edgeUserId.trim() } : {},
|
|
2097
|
+
...typeof record.keyId === "string" && record.keyId.trim() ? { keyId: record.keyId.trim() } : {},
|
|
2098
|
+
...isRecord2(record.metadata) ? { metadata: record.metadata } : {}
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
function encryptLegacySessionTokens(legacyRecord, tokenVault, options) {
|
|
2102
|
+
const subjectId = normalizeRequiredString(options.subjectId || legacyRecord.subjectId, "subjectId");
|
|
2103
|
+
const accessToken = normalizeRequiredString(legacyRecord.accessToken, "accessToken");
|
|
2104
|
+
const refreshToken = normalizeRequiredString(legacyRecord.refreshToken, "refreshToken");
|
|
2105
|
+
if (!options.allowPlaintext && !tokenVault.isEncrypted(accessToken) && !tokenVault.isEncrypted(refreshToken)) {
|
|
2106
|
+
throw new import_connect11.EdgeValidationError("Plaintext token migration requires allowPlaintext: true", {
|
|
2107
|
+
allowPlaintext: ["Set allowPlaintext only inside an explicit migration path"]
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
const scope = normalizeScopeString(legacyRecord.scope ?? legacyRecord.scopes);
|
|
2111
|
+
const tokens = {
|
|
2112
|
+
accessToken,
|
|
2113
|
+
refreshToken,
|
|
2114
|
+
...typeof legacyRecord.idToken === "string" && legacyRecord.idToken.trim() ? { idToken: legacyRecord.idToken.trim() } : {},
|
|
2115
|
+
expiresIn: normalizeExpiresIn(legacyRecord.expiresIn),
|
|
2116
|
+
expiresAt: normalizeSessionExpiresAt(legacyRecord.expiresAt),
|
|
2117
|
+
scope
|
|
2118
|
+
};
|
|
2119
|
+
return createSessionTokenRecord(subjectId, tokens, tokenVault, {
|
|
2120
|
+
scopes: splitScopes(scope),
|
|
2121
|
+
...legacyRecord.edgeUserId ? { edgeUserId: legacyRecord.edgeUserId } : {},
|
|
2122
|
+
...options.keyId ? { keyId: options.keyId } : {},
|
|
2123
|
+
...legacyRecord.metadata ? { metadata: legacyRecord.metadata } : {}
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
function normalizeSessionExpiresAt(value) {
|
|
2127
|
+
if (value instanceof Date) {
|
|
2128
|
+
const time = value.getTime();
|
|
2129
|
+
if (!Number.isFinite(time) || time <= 0) {
|
|
2130
|
+
throw new import_connect11.EdgeValidationError("Invalid EDGE session expiresAt", {
|
|
2131
|
+
expiresAt: ["Date must be valid"]
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
return time;
|
|
2135
|
+
}
|
|
2136
|
+
if (typeof value === "string") {
|
|
2137
|
+
const trimmed = value.trim();
|
|
2138
|
+
if (!trimmed) {
|
|
2139
|
+
throw new import_connect11.EdgeValidationError("Invalid EDGE session expiresAt", {
|
|
2140
|
+
expiresAt: ["Required"]
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
if (/^\d+$/.test(trimmed)) {
|
|
2144
|
+
return normalizeSessionExpiresAt(Number(trimmed));
|
|
2145
|
+
}
|
|
2146
|
+
const time = Date.parse(trimmed);
|
|
2147
|
+
if (!Number.isNaN(time) && time > 0) return time;
|
|
2148
|
+
}
|
|
2149
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2150
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2151
|
+
throw new import_connect11.EdgeValidationError("Invalid EDGE session expiresAt", {
|
|
2152
|
+
expiresAt: ["Must be a positive integer timestamp in milliseconds"]
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
if (value < 1e10) {
|
|
2156
|
+
throw new import_connect11.EdgeValidationError("Ambiguous EDGE session expiresAt", {
|
|
2157
|
+
expiresAt: ["Use Unix milliseconds, not seconds"]
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
return value;
|
|
2161
|
+
}
|
|
2162
|
+
throw new import_connect11.EdgeValidationError("Invalid EDGE session expiresAt", {
|
|
2163
|
+
expiresAt: ["Use Unix milliseconds, ISO string, or Date"]
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
function normalizeExpiresIn(value) {
|
|
2167
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
2168
|
+
if (typeof value === "string" && value.trim()) {
|
|
2169
|
+
const parsed = Number(value);
|
|
2170
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
2171
|
+
}
|
|
2172
|
+
return 600;
|
|
2173
|
+
}
|
|
2174
|
+
function normalizeScopeString(value) {
|
|
2175
|
+
if (Array.isArray(value)) return normalizeScopes(value).join(" ");
|
|
2176
|
+
if (typeof value === "string") return splitScopes(value).join(" ");
|
|
2177
|
+
return "";
|
|
2178
|
+
}
|
|
2179
|
+
function normalizeScopes(value) {
|
|
2180
|
+
if (Array.isArray(value)) {
|
|
2181
|
+
return value.map((scope) => typeof scope === "string" ? scope.trim() : "").filter(Boolean);
|
|
2182
|
+
}
|
|
2183
|
+
if (typeof value === "string") {
|
|
2184
|
+
return splitScopes(value);
|
|
2185
|
+
}
|
|
2186
|
+
throw new import_connect11.EdgeValidationError("Invalid EDGE session scopes", {
|
|
2187
|
+
scopes: ["Expected array or space-delimited string"]
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
function splitScopes(value) {
|
|
2191
|
+
return value.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
|
|
2192
|
+
}
|
|
2193
|
+
function normalizeRequiredString(value, field) {
|
|
2194
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
2195
|
+
throw new import_connect11.EdgeValidationError("Invalid EDGE session token record", {
|
|
2196
|
+
[field]: ["Required string"]
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
return value.trim();
|
|
2200
|
+
}
|
|
2201
|
+
function validateTokens2(tokens) {
|
|
2202
|
+
normalizeRequiredString(tokens.accessToken, "accessToken");
|
|
2203
|
+
normalizeRequiredString(tokens.refreshToken, "refreshToken");
|
|
2204
|
+
normalizeSessionExpiresAt(tokens.expiresAt);
|
|
2205
|
+
normalizeExpiresIn(tokens.expiresIn);
|
|
2206
|
+
normalizeScopeString(tokens.scope);
|
|
2207
|
+
}
|
|
2208
|
+
function isRecord2(value) {
|
|
2209
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
1670
2212
|
// src/token-vault.ts
|
|
1671
|
-
var
|
|
1672
|
-
var
|
|
2213
|
+
var import_connect12 = require("@edge-markets/connect");
|
|
2214
|
+
var import_node_crypto3 = __toESM(require("crypto"));
|
|
1673
2215
|
var TOKEN_VAULT_VERSION = "edge_vault_v1";
|
|
1674
2216
|
var IV_BYTES = 12;
|
|
1675
2217
|
var TAG_BYTES = 16;
|
|
@@ -1681,7 +2223,7 @@ var EdgeTokenVault = class {
|
|
|
1681
2223
|
for (const [index, key] of (options.previousKeys ?? []).entries()) {
|
|
1682
2224
|
const normalized = normalizeVaultKey(key, `previousKeys[${index}]`);
|
|
1683
2225
|
if (this.keysById.has(normalized.id)) {
|
|
1684
|
-
throw new
|
|
2226
|
+
throw new import_connect12.EdgeValidationError("Duplicate token vault key ID", {
|
|
1685
2227
|
keyId: [`Duplicate key ID "${normalized.id}"`]
|
|
1686
2228
|
});
|
|
1687
2229
|
}
|
|
@@ -1690,9 +2232,9 @@ var EdgeTokenVault = class {
|
|
|
1690
2232
|
}
|
|
1691
2233
|
encryptTokens(tokens) {
|
|
1692
2234
|
validateEdgeTokens(tokens);
|
|
1693
|
-
const iv =
|
|
2235
|
+
const iv = import_node_crypto3.default.randomBytes(IV_BYTES);
|
|
1694
2236
|
const aad = Buffer.from(`${TOKEN_VAULT_VERSION}.${this.currentKey.id}`, "utf8");
|
|
1695
|
-
const cipher =
|
|
2237
|
+
const cipher = import_node_crypto3.default.createCipheriv("aes-256-gcm", this.currentKey.key, iv);
|
|
1696
2238
|
cipher.setAAD(aad);
|
|
1697
2239
|
const plaintext = Buffer.from(JSON.stringify(tokens), "utf8");
|
|
1698
2240
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
@@ -1709,12 +2251,12 @@ var EdgeTokenVault = class {
|
|
|
1709
2251
|
const parsed = parseEnvelope(envelope);
|
|
1710
2252
|
const key = this.keysById.get(parsed.keyId);
|
|
1711
2253
|
if (!key) {
|
|
1712
|
-
throw new
|
|
2254
|
+
throw new import_connect12.EdgeValidationError("Unknown token vault key ID", {
|
|
1713
2255
|
keyId: [`No key configured for "${parsed.keyId}"`]
|
|
1714
2256
|
});
|
|
1715
2257
|
}
|
|
1716
2258
|
try {
|
|
1717
|
-
const decipher =
|
|
2259
|
+
const decipher = import_node_crypto3.default.createDecipheriv("aes-256-gcm", key, parsed.iv);
|
|
1718
2260
|
decipher.setAAD(Buffer.from(`${TOKEN_VAULT_VERSION}.${parsed.keyId}`, "utf8"));
|
|
1719
2261
|
decipher.setAuthTag(parsed.tag);
|
|
1720
2262
|
const plaintext = Buffer.concat([decipher.update(parsed.ciphertext), decipher.final()]).toString("utf8");
|
|
@@ -1722,8 +2264,8 @@ var EdgeTokenVault = class {
|
|
|
1722
2264
|
validateEdgeTokens(tokens);
|
|
1723
2265
|
return tokens;
|
|
1724
2266
|
} catch (error) {
|
|
1725
|
-
if (error instanceof
|
|
1726
|
-
throw new
|
|
2267
|
+
if (error instanceof import_connect12.EdgeValidationError) throw error;
|
|
2268
|
+
throw new import_connect12.EdgeValidationError("Failed to decrypt EDGE tokens", {
|
|
1727
2269
|
tokenEnvelope: ["Envelope is malformed, corrupted, or encrypted with a different key"]
|
|
1728
2270
|
});
|
|
1729
2271
|
}
|
|
@@ -1741,7 +2283,7 @@ function isEdgeTokenVaultEnvelope(value) {
|
|
|
1741
2283
|
function normalizeVaultKey(input, fieldName) {
|
|
1742
2284
|
const id = typeof input.id === "string" ? input.id.trim() : "";
|
|
1743
2285
|
if (!id) {
|
|
1744
|
-
throw new
|
|
2286
|
+
throw new import_connect12.EdgeValidationError("Token vault key ID is required", {
|
|
1745
2287
|
[`${fieldName}.id`]: ["Required"]
|
|
1746
2288
|
});
|
|
1747
2289
|
}
|
|
@@ -1772,7 +2314,7 @@ function normalizeKeyMaterial(input, fieldName) {
|
|
|
1772
2314
|
}
|
|
1773
2315
|
const key = candidates.find((candidate) => candidate.length === KEY_BYTES);
|
|
1774
2316
|
if (!key) {
|
|
1775
|
-
throw new
|
|
2317
|
+
throw new import_connect12.EdgeValidationError("Token vault key must decode to 32 bytes", {
|
|
1776
2318
|
[fieldName]: ["Provide a 32-byte key as base64, base64url, hex, Buffer, or Uint8Array"]
|
|
1777
2319
|
});
|
|
1778
2320
|
}
|
|
@@ -1803,18 +2345,18 @@ function validateEdgeTokens(tokens) {
|
|
|
1803
2345
|
}
|
|
1804
2346
|
}
|
|
1805
2347
|
if (Object.keys(errors).length > 0) {
|
|
1806
|
-
throw new
|
|
2348
|
+
throw new import_connect12.EdgeValidationError("Invalid EDGE token payload", errors);
|
|
1807
2349
|
}
|
|
1808
2350
|
}
|
|
1809
2351
|
function parseEnvelope(envelope) {
|
|
1810
2352
|
if (typeof envelope !== "string") {
|
|
1811
|
-
throw new
|
|
2353
|
+
throw new import_connect12.EdgeValidationError("Token envelope must be a string", {
|
|
1812
2354
|
tokenEnvelope: ["Expected string"]
|
|
1813
2355
|
});
|
|
1814
2356
|
}
|
|
1815
2357
|
const parts = envelope.split(".");
|
|
1816
2358
|
if (parts.length !== 5 || parts[0] !== TOKEN_VAULT_VERSION) {
|
|
1817
|
-
throw new
|
|
2359
|
+
throw new import_connect12.EdgeValidationError("Invalid EDGE token vault envelope", {
|
|
1818
2360
|
tokenEnvelope: [`Expected ${TOKEN_VAULT_VERSION}.<keyId>.<iv>.<tag>.<ciphertext>`]
|
|
1819
2361
|
});
|
|
1820
2362
|
}
|
|
@@ -1829,7 +2371,7 @@ function parseEnvelope(envelope) {
|
|
|
1829
2371
|
if (tag.length !== TAG_BYTES) errors.tag = [`Must be ${TAG_BYTES} bytes`];
|
|
1830
2372
|
if (ciphertext.length === 0) errors.ciphertext = ["Required"];
|
|
1831
2373
|
if (Object.keys(errors).length > 0) {
|
|
1832
|
-
throw new
|
|
2374
|
+
throw new import_connect12.EdgeValidationError("Invalid EDGE token vault envelope", errors);
|
|
1833
2375
|
}
|
|
1834
2376
|
return { keyId, iv, tag, ciphertext };
|
|
1835
2377
|
}
|
|
@@ -1843,13 +2385,13 @@ function fromBase64Url2(value) {
|
|
|
1843
2385
|
}
|
|
1844
2386
|
|
|
1845
2387
|
// src/webhook-guards.ts
|
|
1846
|
-
var
|
|
2388
|
+
var import_connect13 = require("@edge-markets/connect");
|
|
1847
2389
|
function isTransferWebhookEvent(event) {
|
|
1848
2390
|
return event.type.startsWith("transfer.");
|
|
1849
2391
|
}
|
|
1850
2392
|
function assertTransferEventMatchesIntent(event, intent) {
|
|
1851
2393
|
if (!isTransferWebhookEvent(event)) {
|
|
1852
|
-
throw new
|
|
2394
|
+
throw new import_connect13.EdgeValidationError("Webhook event is not a transfer event", {
|
|
1853
2395
|
type: [`Received ${event.type}`]
|
|
1854
2396
|
});
|
|
1855
2397
|
}
|
|
@@ -1872,204 +2414,260 @@ function assertTransferEventMatchesIntent(event, intent) {
|
|
|
1872
2414
|
}
|
|
1873
2415
|
}
|
|
1874
2416
|
if (Object.keys(errors).length > 0) {
|
|
1875
|
-
throw new
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
// src/webhook-parser.ts
|
|
1880
|
-
var import_connect10 = require("@edge-markets/connect");
|
|
1881
|
-
|
|
1882
|
-
// src/webhook-signing.ts
|
|
1883
|
-
var import_node_crypto3 = __toESM(require("crypto"));
|
|
1884
|
-
function verifyWebhookSignature(header, body, secret, options = {}) {
|
|
1885
|
-
if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
|
|
1886
|
-
return false;
|
|
1887
|
-
}
|
|
1888
|
-
const requestedTolerance = options.toleranceSeconds;
|
|
1889
|
-
const toleranceSeconds = typeof requestedTolerance === "number" && Number.isFinite(requestedTolerance) && requestedTolerance >= 0 ? requestedTolerance : 300;
|
|
1890
|
-
const parts = header.split(",").map((p) => p.trim());
|
|
1891
|
-
const tPart = parts.find((p) => p.startsWith("t="));
|
|
1892
|
-
const v1Part = parts.find((p) => p.startsWith("v1="));
|
|
1893
|
-
if (!tPart || !v1Part) return false;
|
|
1894
|
-
const timestamp = parseInt(tPart.slice(2), 10);
|
|
1895
|
-
const signature = v1Part.slice(3);
|
|
1896
|
-
if (isNaN(timestamp) || !signature) return false;
|
|
1897
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
1898
|
-
if (Math.abs(now - timestamp) > toleranceSeconds) return false;
|
|
1899
|
-
const signedContent = `${timestamp}.${body}`;
|
|
1900
|
-
const expectedHmac = import_node_crypto3.default.createHmac("sha256", secret).update(signedContent).digest("hex");
|
|
1901
|
-
try {
|
|
1902
|
-
const sigBuf = Buffer.from(signature, "hex");
|
|
1903
|
-
const expBuf = Buffer.from(expectedHmac, "hex");
|
|
1904
|
-
if (sigBuf.length !== expBuf.length) return false;
|
|
1905
|
-
return import_node_crypto3.default.timingSafeEqual(sigBuf, expBuf);
|
|
1906
|
-
} catch {
|
|
1907
|
-
return false;
|
|
2417
|
+
throw new import_connect13.EdgeValidationError("Webhook transfer event does not match expected transfer intent", errors);
|
|
1908
2418
|
}
|
|
1909
2419
|
}
|
|
2420
|
+
var assertWebhookMatchesTransfer = assertTransferEventMatchesIntent;
|
|
1910
2421
|
|
|
1911
|
-
// src/webhook-
|
|
1912
|
-
var
|
|
1913
|
-
var
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2422
|
+
// src/webhook-inbox.ts
|
|
2423
|
+
var import_connect14 = require("@edge-markets/connect");
|
|
2424
|
+
var import_node_crypto4 = __toESM(require("crypto"));
|
|
2425
|
+
var DEFAULT_PROCESSING_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2426
|
+
var EdgeWebhookInbox = class {
|
|
2427
|
+
constructor(options) {
|
|
2428
|
+
if (!options.store?.insert || !options.store?.find || !options.store?.markProcessing) {
|
|
2429
|
+
throw new import_connect14.EdgeValidationError("EdgeWebhookInbox store is incomplete", {
|
|
2430
|
+
store: ["Provide insert, find, markProcessing, markProcessed, and markFailed callbacks"]
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
if (!options.store.markProcessed || !options.store.markFailed) {
|
|
2434
|
+
throw new import_connect14.EdgeValidationError("EdgeWebhookInbox store is incomplete", {
|
|
2435
|
+
store: ["Provide markProcessed and markFailed callbacks"]
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
if (!options.processEvent) {
|
|
2439
|
+
throw new import_connect14.EdgeValidationError("EdgeWebhookInbox processEvent callback is required", {
|
|
2440
|
+
processEvent: ["Required"]
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
if (options.processingTimeoutMs !== void 0 && (!Number.isFinite(options.processingTimeoutMs) || options.processingTimeoutMs <= 0)) {
|
|
2444
|
+
throw new import_connect14.EdgeValidationError("processingTimeoutMs must be positive", {
|
|
2445
|
+
processingTimeoutMs: ["Must be greater than 0"]
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
this.store = options.store;
|
|
2449
|
+
this.processEvent = options.processEvent;
|
|
2450
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2451
|
+
this.storeRawBody = options.storeRawBody ?? false;
|
|
2452
|
+
this.processingTimeoutMs = options.processingTimeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
|
|
2453
|
+
this.onEvent = options.onEvent;
|
|
2454
|
+
}
|
|
2455
|
+
async accept(parsed) {
|
|
2456
|
+
return this.acceptEvent(parsed.event, {
|
|
2457
|
+
source: "live_webhook",
|
|
2458
|
+
receivedAt: parsed.receivedAt,
|
|
2459
|
+
signatureTimestamp: parsed.signatureTimestamp,
|
|
2460
|
+
signatureHeader: void 0,
|
|
2461
|
+
rawBody: void 0
|
|
1930
2462
|
});
|
|
1931
2463
|
}
|
|
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"]
|
|
2464
|
+
async acceptRaw(parsed, rawBody, signatureHeader) {
|
|
2465
|
+
return this.acceptEvent(parsed.event, {
|
|
2466
|
+
source: "live_webhook",
|
|
2467
|
+
receivedAt: parsed.receivedAt,
|
|
2468
|
+
signatureTimestamp: parsed.signatureTimestamp,
|
|
2469
|
+
signatureHeader,
|
|
2470
|
+
rawBody: this.storeRawBody ? rawBody : void 0
|
|
1963
2471
|
});
|
|
1964
2472
|
}
|
|
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"];
|
|
2473
|
+
async acceptSyncedEvent(event, receivedAt = this.now()) {
|
|
2474
|
+
return this.acceptEvent(event, {
|
|
2475
|
+
source: "sync",
|
|
2476
|
+
receivedAt,
|
|
2477
|
+
signatureTimestamp: null,
|
|
2478
|
+
signatureHeader: null,
|
|
2479
|
+
rawBody: null
|
|
2480
|
+
});
|
|
1980
2481
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
2482
|
+
async process(eventId, options = {}) {
|
|
2483
|
+
const normalizedEventId = normalizeEventId(eventId);
|
|
2484
|
+
const existing = await this.store.find(normalizedEventId);
|
|
2485
|
+
if (!existing) {
|
|
2486
|
+
return { eventId: normalizedEventId, status: "failed", processed: false, skipped: true, reason: "not_found" };
|
|
2487
|
+
}
|
|
2488
|
+
if (existing.status === "processed" && !options.force) {
|
|
2489
|
+
return {
|
|
2490
|
+
eventId: normalizedEventId,
|
|
2491
|
+
status: existing.status,
|
|
2492
|
+
processed: false,
|
|
2493
|
+
skipped: true,
|
|
2494
|
+
reason: "already_processed",
|
|
2495
|
+
record: existing
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
const now = this.now();
|
|
2499
|
+
const processing = await this.store.markProcessing(normalizedEventId, { now });
|
|
2500
|
+
if (!processing) {
|
|
2501
|
+
return {
|
|
2502
|
+
eventId: normalizedEventId,
|
|
2503
|
+
status: existing.status,
|
|
2504
|
+
processed: false,
|
|
2505
|
+
skipped: true,
|
|
2506
|
+
reason: "locked",
|
|
2507
|
+
record: existing
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
await this.emit({
|
|
2511
|
+
type: "webhook.processing_started",
|
|
2512
|
+
eventId: normalizedEventId,
|
|
2513
|
+
eventType: processing.eventType,
|
|
2514
|
+
status: "processing",
|
|
2515
|
+
source: processing.source,
|
|
2516
|
+
attempts: processing.attempts
|
|
2517
|
+
});
|
|
2518
|
+
try {
|
|
2519
|
+
await this.processEvent(processing.event, processing);
|
|
2520
|
+
const processed = await this.store.markProcessed(normalizedEventId, { now: this.now() }) ?? processing;
|
|
2521
|
+
await this.emit({
|
|
2522
|
+
type: "webhook.processing_succeeded",
|
|
2523
|
+
eventId: normalizedEventId,
|
|
2524
|
+
eventType: processing.eventType,
|
|
2525
|
+
status: "processed",
|
|
2526
|
+
source: processing.source,
|
|
2527
|
+
attempts: processed.attempts
|
|
2528
|
+
});
|
|
2529
|
+
return {
|
|
2530
|
+
eventId: normalizedEventId,
|
|
2531
|
+
status: "processed",
|
|
2532
|
+
processed: true,
|
|
2533
|
+
skipped: false,
|
|
2534
|
+
record: processed
|
|
2535
|
+
};
|
|
2536
|
+
} catch (error) {
|
|
2537
|
+
const message = error instanceof Error ? error.message : "Unknown webhook processing error";
|
|
2538
|
+
const failed = await this.store.markFailed(normalizedEventId, { now: this.now(), error: message }) ?? processing;
|
|
2539
|
+
await this.emit({
|
|
2540
|
+
type: "webhook.processing_failed",
|
|
2541
|
+
eventId: normalizedEventId,
|
|
2542
|
+
eventType: processing.eventType,
|
|
2543
|
+
status: "failed",
|
|
2544
|
+
source: processing.source,
|
|
2545
|
+
attempts: failed.attempts,
|
|
2546
|
+
error: message
|
|
2547
|
+
});
|
|
2548
|
+
throw error;
|
|
2549
|
+
}
|
|
1983
2550
|
}
|
|
1984
|
-
|
|
1985
|
-
|
|
2551
|
+
async replay(eventId) {
|
|
2552
|
+
return this.process(eventId, { force: true });
|
|
1986
2553
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2554
|
+
async recoverStaleProcessing() {
|
|
2555
|
+
if (!this.store.list) {
|
|
2556
|
+
return { recovered: 0, skipped: true, reason: "list_not_configured" };
|
|
2557
|
+
}
|
|
2558
|
+
if (!this.store.markPending) {
|
|
2559
|
+
return { recovered: 0, skipped: true, reason: "mark_pending_not_configured" };
|
|
2560
|
+
}
|
|
2561
|
+
const updatedBefore = new Date(this.now().getTime() - this.processingTimeoutMs);
|
|
2562
|
+
const records = await this.store.list({ status: "processing", updatedBefore, limit: 100 });
|
|
2563
|
+
let recovered = 0;
|
|
2564
|
+
for (const record of records) {
|
|
2565
|
+
const next = await this.store.markPending(record.eventId, {
|
|
2566
|
+
now: this.now(),
|
|
2567
|
+
reason: "stale_processing_recovery"
|
|
2568
|
+
});
|
|
2569
|
+
if (next) {
|
|
2570
|
+
recovered++;
|
|
2571
|
+
await this.emit({
|
|
2572
|
+
type: "webhook.recovered_stale",
|
|
2573
|
+
eventId: record.eventId,
|
|
2574
|
+
eventType: record.eventType,
|
|
2575
|
+
status: next.status,
|
|
2576
|
+
source: record.source,
|
|
2577
|
+
attempts: next.attempts
|
|
2578
|
+
});
|
|
1998
2579
|
}
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2580
|
+
}
|
|
2581
|
+
return { recovered, skipped: false };
|
|
2582
|
+
}
|
|
2583
|
+
async acceptEvent(event, options) {
|
|
2584
|
+
const payloadFingerprint = fingerprintWebhookEvent(event);
|
|
2585
|
+
const result = await this.store.insert({
|
|
2586
|
+
eventId: event.id,
|
|
2587
|
+
eventType: event.type,
|
|
2588
|
+
event,
|
|
2589
|
+
payloadFingerprint,
|
|
2590
|
+
source: options.source,
|
|
2591
|
+
receivedAt: options.receivedAt,
|
|
2592
|
+
signatureTimestamp: options.signatureTimestamp ?? null,
|
|
2593
|
+
signatureHeader: options.signatureHeader ?? null,
|
|
2594
|
+
rawBody: options.rawBody ?? null
|
|
2595
|
+
});
|
|
2596
|
+
if (result.record.payloadFingerprint !== payloadFingerprint) {
|
|
2597
|
+
await this.emit({
|
|
2598
|
+
type: "webhook.payload_mismatch",
|
|
2599
|
+
eventId: event.id,
|
|
2600
|
+
eventType: event.type,
|
|
2601
|
+
status: result.record.status,
|
|
2602
|
+
source: options.source
|
|
2603
|
+
});
|
|
2604
|
+
throw new import_connect14.EdgeValidationError("Duplicate webhook event payload does not match stored event", {
|
|
2605
|
+
eventId: [event.id]
|
|
2015
2606
|
});
|
|
2016
2607
|
}
|
|
2608
|
+
await this.emit({
|
|
2609
|
+
type: result.inserted ? "webhook.accepted" : "webhook.duplicate",
|
|
2610
|
+
eventId: event.id,
|
|
2611
|
+
eventType: event.type,
|
|
2612
|
+
status: result.record.status,
|
|
2613
|
+
source: options.source,
|
|
2614
|
+
attempts: result.record.attempts
|
|
2615
|
+
});
|
|
2616
|
+
return {
|
|
2617
|
+
eventId: event.id,
|
|
2618
|
+
eventType: event.type,
|
|
2619
|
+
inserted: result.inserted,
|
|
2620
|
+
duplicate: !result.inserted,
|
|
2621
|
+
status: result.record.status,
|
|
2622
|
+
record: result.record
|
|
2623
|
+
};
|
|
2017
2624
|
}
|
|
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"'];
|
|
2625
|
+
async emit(event) {
|
|
2626
|
+
try {
|
|
2627
|
+
await this.onEvent?.(event);
|
|
2628
|
+
} catch {
|
|
2629
|
+
}
|
|
2026
2630
|
}
|
|
2027
|
-
|
|
2631
|
+
};
|
|
2632
|
+
function createWebhookInbox(options) {
|
|
2633
|
+
return new EdgeWebhookInbox(options);
|
|
2028
2634
|
}
|
|
2029
|
-
function
|
|
2030
|
-
|
|
2031
|
-
errors[`data.${field}`] = ["Required string"];
|
|
2032
|
-
}
|
|
2635
|
+
function fingerprintWebhookEvent(event) {
|
|
2636
|
+
return import_node_crypto4.default.createHash("sha256").update(stableStringify(event)).digest("hex");
|
|
2033
2637
|
}
|
|
2034
|
-
function
|
|
2035
|
-
if (typeof
|
|
2036
|
-
|
|
2638
|
+
function normalizeEventId(eventId) {
|
|
2639
|
+
if (typeof eventId !== "string" || !eventId.trim()) {
|
|
2640
|
+
throw new import_connect14.EdgeValidationError("Webhook event ID is required", {
|
|
2641
|
+
eventId: ["Required"]
|
|
2642
|
+
});
|
|
2037
2643
|
}
|
|
2644
|
+
return eventId.trim();
|
|
2038
2645
|
}
|
|
2039
|
-
function
|
|
2040
|
-
if (
|
|
2041
|
-
return
|
|
2042
|
-
}
|
|
2043
|
-
if (Buffer.isBuffer(rawBody) || rawBody instanceof Uint8Array) {
|
|
2044
|
-
return Buffer.from(rawBody).toString("utf8");
|
|
2646
|
+
function stableStringify(value) {
|
|
2647
|
+
if (Array.isArray(value)) {
|
|
2648
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
2045
2649
|
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
});
|
|
2049
|
-
}
|
|
2050
|
-
function normalizeSignatureHeader(header) {
|
|
2051
|
-
if (Array.isArray(header)) {
|
|
2052
|
-
return header.map((value) => value.trim()).filter(Boolean).join(",");
|
|
2650
|
+
if (value && typeof value === "object") {
|
|
2651
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
2053
2652
|
}
|
|
2054
|
-
|
|
2055
|
-
const trimmed = header.trim();
|
|
2056
|
-
return trimmed || void 0;
|
|
2057
|
-
}
|
|
2058
|
-
function isRecord(value) {
|
|
2059
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2653
|
+
return JSON.stringify(value);
|
|
2060
2654
|
}
|
|
2061
2655
|
|
|
2062
2656
|
// src/index.ts
|
|
2063
|
-
var
|
|
2064
|
-
var
|
|
2657
|
+
var import_connect15 = require("@edge-markets/connect");
|
|
2658
|
+
var import_connect16 = require("@edge-markets/connect");
|
|
2065
2659
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2066
2660
|
0 && (module.exports = {
|
|
2661
|
+
ACTIVE_EDGE_SCOPES,
|
|
2662
|
+
CONNECT_TRANSFER_DIRECTIONS,
|
|
2663
|
+
EDGE_CONNECT_FEATURE_UNAVAILABLE_MESSAGE,
|
|
2067
2664
|
EDGE_WEBHOOK_EVENT_TYPES,
|
|
2068
2665
|
EdgeApiError,
|
|
2069
2666
|
EdgeAuthenticationError,
|
|
2070
2667
|
EdgeConnectServer,
|
|
2071
2668
|
EdgeConsentRequiredError,
|
|
2072
2669
|
EdgeError,
|
|
2670
|
+
EdgeFeatureUnavailableError,
|
|
2073
2671
|
EdgeIdentityVerificationError,
|
|
2074
2672
|
EdgeInsufficientScopeError,
|
|
2075
2673
|
EdgeNetworkError,
|
|
@@ -2079,18 +2677,28 @@ var import_connect12 = require("@edge-markets/connect");
|
|
|
2079
2677
|
EdgeUserClient,
|
|
2080
2678
|
EdgeUserSession,
|
|
2081
2679
|
EdgeValidationError,
|
|
2680
|
+
EdgeWebhookInbox,
|
|
2082
2681
|
EdgeWebhookReconciler,
|
|
2083
2682
|
TRANSFER_CATEGORIES,
|
|
2084
2683
|
assertSameTransferIntent,
|
|
2085
2684
|
assertTransferEventMatchesIntent,
|
|
2685
|
+
assertWebhookMatchesTransfer,
|
|
2086
2686
|
buildVerifyIdentityPayload,
|
|
2087
2687
|
createEdgeConnectServerFromEnv,
|
|
2088
2688
|
createEdgeTokenVault,
|
|
2089
2689
|
createEdgeUserSession,
|
|
2690
|
+
createSessionTokenRecord,
|
|
2090
2691
|
createTransferIdempotencyKey,
|
|
2692
|
+
createWebhookInbox,
|
|
2091
2693
|
createWebhookReconciler,
|
|
2694
|
+
encryptLegacySessionTokens,
|
|
2092
2695
|
evaluateIdentityResult,
|
|
2093
2696
|
extractWebhookSignatureTimestamp,
|
|
2697
|
+
fingerprintTransferIntent,
|
|
2698
|
+
fingerprintWebhookEvent,
|
|
2699
|
+
getConnectTransferDirection,
|
|
2700
|
+
getEdgeWebhookRawBody,
|
|
2701
|
+
getEdgeWebhookSignatureHeader,
|
|
2094
2702
|
getEnvironmentConfig,
|
|
2095
2703
|
isApiError,
|
|
2096
2704
|
isAuthenticationError,
|
|
@@ -2098,12 +2706,18 @@ var import_connect12 = require("@edge-markets/connect");
|
|
|
2098
2706
|
isEdgeError,
|
|
2099
2707
|
isEdgeTokenVaultEnvelope,
|
|
2100
2708
|
isEdgeWebhookEvent,
|
|
2709
|
+
isFeatureUnavailableError,
|
|
2101
2710
|
isIdentityVerificationError,
|
|
2102
2711
|
isNetworkError,
|
|
2103
2712
|
isProductionEnvironment,
|
|
2104
2713
|
isTransferWebhookEvent,
|
|
2714
|
+
mapTransferStatusToPartnerState,
|
|
2715
|
+
moneyAmountsEqual,
|
|
2105
2716
|
normalizeMoneyAmount,
|
|
2717
|
+
normalizeSessionExpiresAt,
|
|
2106
2718
|
parseAndVerifyWebhook,
|
|
2719
|
+
parseSessionTokenRecord,
|
|
2720
|
+
parseWebhookHttpRequest,
|
|
2107
2721
|
startTransferVerification,
|
|
2108
2722
|
toEdgeHttpError,
|
|
2109
2723
|
toEdgeProblemJson,
|