@edge-markets/connect-node 1.8.0 → 1.9.0

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