@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/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
- EDGE_WEBHOOK_EVENT_TYPES: () => import_connect12.EDGE_WEBHOOK_EVENT_TYPES,
34
- EdgeApiError: () => import_connect11.EdgeApiError,
35
- EdgeAuthenticationError: () => import_connect11.EdgeAuthenticationError,
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: () => import_connect11.EdgeConsentRequiredError,
38
- EdgeError: () => import_connect11.EdgeError,
39
- EdgeIdentityVerificationError: () => import_connect11.EdgeIdentityVerificationError,
40
- EdgeInsufficientScopeError: () => import_connect11.EdgeInsufficientScopeError,
41
- EdgeNetworkError: () => import_connect11.EdgeNetworkError,
42
- EdgeNotFoundError: () => import_connect11.EdgeNotFoundError,
43
- EdgeTokenExchangeError: () => import_connect11.EdgeTokenExchangeError,
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: () => import_connect11.EdgeValidationError,
51
+ EdgeValidationError: () => import_connect15.EdgeValidationError,
52
+ EdgeWebhookInbox: () => EdgeWebhookInbox,
48
53
  EdgeWebhookReconciler: () => EdgeWebhookReconciler,
49
- TRANSFER_CATEGORIES: () => import_connect12.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
- getEnvironmentConfig: () => import_connect12.getEnvironmentConfig,
61
- isApiError: () => import_connect11.isApiError,
62
- isAuthenticationError: () => import_connect11.isAuthenticationError,
63
- isConsentRequiredError: () => import_connect11.isConsentRequiredError,
64
- isEdgeError: () => import_connect11.isEdgeError,
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
- isIdentityVerificationError: () => import_connect11.isIdentityVerificationError,
68
- isNetworkError: () => import_connect11.isNetworkError,
69
- isProductionEnvironment: () => import_connect12.isProductionEnvironment,
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 import_connect4 = require("@edge-markets/connect");
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
- function normalizeMoneyAmount(value) {
91
- const cents = parseMoneyToCents(value, "amount");
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 parseMoneyToCents(value, field) {
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
- * Creates an EDGE-hosted transfer verification session.
315
- * Returns a `verificationUrl` to open in an iframe or popup.
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
- * Polls the status of a verification session.
329
- * Use as an alternative to postMessage events.
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 import_connect3 = require("@edge-markets/connect");
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 import_connect3.EdgeValidationError("EdgeUserSession requires an EdgeConnectServer instance", {
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 import_connect3.EdgeValidationError("EdgeUserSession subjectId is required", {
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 import_connect3.EdgeValidationError("EdgeUserSession tokenStore is required", {
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 import_connect3.EdgeValidationError("refreshSkewMs must be a non-negative number", {
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
- const refreshed = await this.edge.refreshTokens(tokens.refreshToken);
516
- await this.saveTokens(refreshed, true);
517
- await this.onRefresh?.(refreshed);
518
- return refreshed;
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 import_connect3.EdgeAuthenticationError("No EDGE tokens are stored for this subject", {
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 import_connect3.EdgeAuthenticationError("Encrypted EDGE token envelope loaded but no tokenVault was configured", {
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 import_connect3.EdgeValidationError("Invalid EDGE token payload", errors);
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
- await this.options.processEvent(event);
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, import_connect4.getEnvironmentConfig)(config.environment);
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 import_connect4.EdgeAuthenticationError("accessToken is required when calling forUser()");
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 import_connect4.EdgeError) throw error;
840
- throw new import_connect4.EdgeNetworkError("Failed to exchange code", error);
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 import_connect4.EdgeAuthenticationError(
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 import_connect4.EdgeError) throw error;
875
- throw new import_connect4.EdgeNetworkError("Failed to refresh tokens", error);
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 import_connect4.EdgeApiError(
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 import_connect4.EdgeApiError(
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 import_connect4.EdgeError) {
943
- if (!(error instanceof import_connect4.EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
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 import_connect4.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
1112
+ throw new import_connect5.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
952
1113
  }
953
1114
  }
954
1115
  }
955
- throw new import_connect4.EdgeNetworkError(
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 import_connect4.EdgeAuthenticationError("syncWebhookEvents requires `partnerClientId` in EdgeConnectServer config");
1160
+ throw new import_connect5.EdgeAuthenticationError("syncWebhookEvents requires `partnerClientId` in EdgeConnectServer config");
1000
1161
  }
1001
1162
  if (!this.config.partnerClientSecret) {
1002
- throw new import_connect4.EdgeAuthenticationError("syncWebhookEvents requires `partnerClientSecret` in EdgeConnectServer config");
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 import_connect4.EdgeAuthenticationError(
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 import_connect4.EdgeApiError(
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 import_connect4.EdgeApiError(
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 import_connect4.EdgeNetworkError("syncWebhookEvents network failure", lastError);
1327
+ throw new import_connect5.EdgeNetworkError("syncWebhookEvents network failure", lastError);
1167
1328
  }
1168
1329
  }
1169
1330
  }
1170
- throw new import_connect4.EdgeNetworkError(
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 import_connect4.EdgeNetworkError("Partner token request network failure", error);
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 import_connect4.EdgeAuthenticationError(
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 import_connect4.EdgeAuthenticationError("Partner token response missing access_token or expires_in");
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 import_connect4.EdgeTokenExchangeError("Authorization code is invalid, expired, or already used. Please try again.", {
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 import_connect4.EdgeAuthenticationError("Invalid client credentials. Check your client ID and secret.", {
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 import_connect4.EdgeTokenExchangeError("PKCE verification failed. Please try again.", { edgeBoostError: error });
1483
+ return new import_connect5.EdgeTokenExchangeError("PKCE verification failed. Please try again.", { edgeBoostError: error });
1323
1484
  }
1324
- return new import_connect4.EdgeTokenExchangeError(errorMessage || "Failed to exchange authorization code", {
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 import_connect4.EdgeAuthenticationError(error.message || "Access token is invalid or expired", error);
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 import_connect4.EdgeConsentRequiredError(
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 import_connect4.EdgeInsufficientScopeError(
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 import_connect4.EdgeNotFoundError(resourceType, resourceId);
1525
+ return new import_connect5.EdgeNotFoundError(resourceType, resourceId);
1365
1526
  }
1366
1527
  if (status === 422 && error.error === "identity_verification_failed") {
1367
- return new import_connect4.EdgeIdentityVerificationError(
1528
+ return new import_connect5.EdgeIdentityVerificationError(
1368
1529
  error.fieldErrors || {},
1369
1530
  error.message
1370
1531
  );
1371
1532
  }
1372
- return new import_connect4.EdgeApiError(
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 import_connect5 = require("@edge-markets/connect");
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(import_connect5.EDGE_ENVIRONMENTS));
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 import_connect5.EdgeValidationError(`Failed to read ${pathName}`, {
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 import_connect5.EdgeValidationError("Invalid EDGE_ENVIRONMENT", {
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 import_connect5.EdgeValidationError("Invalid EDGE_TIMEOUT_MS", {
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 ca = readPemValue(env, "EDGE_MTLS_CA", "EDGE_MTLS_CA_PATH", warnings);
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 import_connect5.EdgeValidationError("mTLS is required but no EDGE_MTLS_CERT or EDGE_MTLS_KEY was provided", {
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 import_connect5.EdgeValidationError("Incomplete mTLS configuration", {
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, import_connect5.getEnvironmentConfig)(environment).apiBaseUrl;
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 import_connect5.EdgeValidationError("Missing required EDGE Connect environment variables", validationErrors);
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 import_connect6 = require("@edge-markets/connect");
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 import_connect6.EdgeConsentRequiredError) {
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 import_connect6.EdgeValidationError) {
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 import_connect6.EdgeIdentityVerificationError) {
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 import_connect6.EdgeNetworkError) {
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 import_connect6.EdgeError) {
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 import_connect7 = require("@edge-markets/connect");
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 import_connect7.EdgeValidationError("Partner identity profile is incomplete", errors);
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 import_connect7.EdgeValidationError("Invalid identity policy", errors);
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 import_connect8 = require("@edge-markets/connect");
1672
- var import_node_crypto2 = __toESM(require("crypto"));
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 import_connect8.EdgeValidationError("Duplicate token vault key ID", {
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 = import_node_crypto2.default.randomBytes(IV_BYTES);
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 = import_node_crypto2.default.createCipheriv("aes-256-gcm", this.currentKey.key, iv);
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 import_connect8.EdgeValidationError("Unknown token vault key ID", {
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 = import_node_crypto2.default.createDecipheriv("aes-256-gcm", key, parsed.iv);
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 import_connect8.EdgeValidationError) throw error;
1726
- throw new import_connect8.EdgeValidationError("Failed to decrypt EDGE tokens", {
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 import_connect8.EdgeValidationError("Token vault key ID is required", {
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 import_connect8.EdgeValidationError("Token vault key must decode to 32 bytes", {
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 import_connect8.EdgeValidationError("Invalid EDGE token payload", errors);
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 import_connect8.EdgeValidationError("Token envelope must be a string", {
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 import_connect8.EdgeValidationError("Invalid EDGE token vault envelope", {
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 import_connect8.EdgeValidationError("Invalid EDGE token vault envelope", errors);
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 import_connect9 = require("@edge-markets/connect");
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 import_connect9.EdgeValidationError("Webhook event is not a transfer event", {
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 import_connect9.EdgeValidationError("Webhook transfer event does not match expected transfer intent", errors);
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-parser.ts
1912
- var EVENT_TYPES = new Set(import_connect10.EDGE_WEBHOOK_EVENT_TYPES);
1913
- var TRANSFER_TYPES = /* @__PURE__ */ new Set(["debit", "credit"]);
1914
- function parseAndVerifyWebhook(options) {
1915
- const body = normalizeRawBody(options.rawBody);
1916
- const header = normalizeSignatureHeader(options.signatureHeader);
1917
- const signatureTimestamp = extractWebhookSignatureTimestamp(header);
1918
- if (!header || !signatureTimestamp) {
1919
- throw new import_connect10.EdgeAuthenticationError("Missing or malformed EDGE webhook signature");
1920
- }
1921
- if (!verifyWebhookSignature(header, body, options.secret, { toleranceSeconds: options.toleranceSeconds })) {
1922
- throw new import_connect10.EdgeAuthenticationError("Invalid EDGE webhook signature");
1923
- }
1924
- let parsed;
1925
- try {
1926
- parsed = JSON.parse(body);
1927
- } catch {
1928
- throw new import_connect10.EdgeValidationError("EDGE webhook body must be valid JSON", {
1929
- rawBody: ["Unable to parse JSON after signature verification"]
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
- const event = validateEdgeWebhookEvent(parsed);
1933
- return {
1934
- event,
1935
- eventId: event.id,
1936
- eventType: event.type,
1937
- signatureTimestamp,
1938
- receivedAt: options.receivedAt ?? /* @__PURE__ */ new Date()
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
- const id = value.id;
1966
- const type = value.type;
1967
- const createdAt = value.created_at;
1968
- const data = value.data;
1969
- if (typeof id !== "string" || !id.trim()) {
1970
- errors.id = ["Required string"];
1971
- }
1972
- if (typeof type !== "string" || !EVENT_TYPES.has(type)) {
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
- if (Object.keys(errors).length === 0) {
1982
- validateKnownEventData(type, data, errors);
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
- if (Object.keys(errors).length > 0) {
1985
- throw new import_connect10.EdgeValidationError("Invalid EDGE webhook event", errors);
2551
+ async replay(eventId) {
2552
+ return this.process(eventId, { force: true });
1986
2553
  }
1987
- return value;
1988
- }
1989
- function validateKnownEventData(type, data, errors) {
1990
- switch (type) {
1991
- case "transfer.completed":
1992
- validateTransferEventData(data, "completed", errors);
1993
- return;
1994
- case "transfer.failed":
1995
- validateTransferEventData(data, "failed", errors);
1996
- if (data.reason !== void 0 && typeof data.reason !== "string") {
1997
- errors["data.reason"] = ["Must be a string when provided"];
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
- return;
2000
- case "transfer.expired":
2001
- validateTransferEventData(data, "expired", errors);
2002
- return;
2003
- case "transfer.processing":
2004
- validateTransferEventData(data, "processing", errors);
2005
- return;
2006
- case "consent.revoked":
2007
- validateRequiredString(data, "userId", errors);
2008
- validateRequiredString(data, "clientId", errors);
2009
- validateRequiredIsoTimestamp(data, "revokedAt", errors);
2010
- return;
2011
- default: {
2012
- const exhaustive = type;
2013
- throw new import_connect10.EdgeValidationError("Unsupported EDGE webhook event type", {
2014
- type: [`Unsupported event type: ${exhaustive}`]
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
- function validateTransferEventData(data, status, errors) {
2020
- validateRequiredString(data, "transferId", errors);
2021
- if (data.status !== status) {
2022
- errors["data.status"] = [`Must be "${status}" for this event type`];
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
- validateRequiredString(data, "amount", errors);
2631
+ };
2632
+ function createWebhookInbox(options) {
2633
+ return new EdgeWebhookInbox(options);
2028
2634
  }
2029
- function validateRequiredString(data, field, errors) {
2030
- if (typeof data[field] !== "string" || !data[field].trim()) {
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 validateRequiredIsoTimestamp(data, field, errors) {
2035
- if (typeof data[field] !== "string" || !data[field].trim() || Number.isNaN(Date.parse(data[field]))) {
2036
- errors[`data.${field}`] = ["Required ISO 8601 timestamp string"];
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 normalizeRawBody(rawBody) {
2040
- if (typeof rawBody === "string") {
2041
- return rawBody;
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
- throw new import_connect10.EdgeValidationError("EDGE webhook raw body is required", {
2047
- rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
2048
- });
2049
- }
2050
- function normalizeSignatureHeader(header) {
2051
- if (Array.isArray(header)) {
2052
- return header.map((value) => value.trim()).filter(Boolean).join(",");
2650
+ if (value && typeof value === "object") {
2651
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
2053
2652
  }
2054
- if (typeof header !== "string") return void 0;
2055
- const trimmed = header.trim();
2056
- return trimmed || void 0;
2057
- }
2058
- function isRecord(value) {
2059
- return !!value && typeof value === "object" && !Array.isArray(value);
2653
+ return JSON.stringify(value);
2060
2654
  }
2061
2655
 
2062
2656
  // src/index.ts
2063
- var import_connect11 = require("@edge-markets/connect");
2064
- var import_connect12 = require("@edge-markets/connect");
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,