@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.mjs CHANGED
@@ -26,16 +26,28 @@ import {
26
26
  import crypto from "crypto";
27
27
  var DEFAULT_MIN_AMOUNT = "1.00";
28
28
  var DEFAULT_MAX_AMOUNT = "10000.00";
29
- function normalizeMoneyAmount(value) {
30
- const cents = parseMoneyToCents(value, "amount");
29
+ var CONNECT_TRANSFER_DIRECTIONS = {
30
+ debit: "user_to_partner",
31
+ credit: "partner_to_user"
32
+ };
33
+ function getConnectTransferDirection(type) {
34
+ if (type !== "debit" && type !== "credit") {
35
+ throw new EdgeValidationError("Invalid Connect transfer type", {
36
+ type: ['Must be "debit" or "credit"']
37
+ });
38
+ }
39
+ return CONNECT_TRANSFER_DIRECTIONS[type];
40
+ }
41
+ function normalizeMoneyAmount(value, options = {}) {
42
+ const cents = parseMoneyToCents(value, "amount", options);
31
43
  const dollars = cents / 100n;
32
44
  const remainder = cents % 100n;
33
45
  return `${dollars.toString()}.${remainder.toString().padStart(2, "0")}`;
34
46
  }
35
- function validateTransferAmount(value, limits = {}) {
36
- const amount = parseMoneyToCents(value, "amount");
37
- const min = parseMoneyToCents(limits.min ?? DEFAULT_MIN_AMOUNT, "min");
38
- const max = parseMoneyToCents(limits.max ?? DEFAULT_MAX_AMOUNT, "max");
47
+ function validateTransferAmount(value, limits = {}, options = {}) {
48
+ const amount = parseMoneyToCents(value, "amount", options);
49
+ const min = parseMoneyToCents(limits.min ?? DEFAULT_MIN_AMOUNT, "min", { allowNumber: true });
50
+ const max = parseMoneyToCents(limits.max ?? DEFAULT_MAX_AMOUNT, "max", { allowNumber: true });
39
51
  const errors = {};
40
52
  if (min <= 0n) errors.min = ["Minimum amount must be greater than 0"];
41
53
  if (max < min) errors.max = ["Maximum amount must be greater than or equal to minimum amount"];
@@ -83,6 +95,27 @@ function assertSameTransferIntent(existing, requested) {
83
95
  throw new EdgeValidationError("Transfer intent does not match existing idempotent transfer", errors);
84
96
  }
85
97
  }
98
+ function fingerprintTransferIntent(intent) {
99
+ return crypto.createHash("sha256").update(
100
+ JSON.stringify({
101
+ type: intent.type,
102
+ amount: normalizeMoneyAmount(intent.amount),
103
+ category: intent.category ?? null
104
+ })
105
+ ).digest("hex");
106
+ }
107
+ function moneyAmountsEqual(left, right, options = {}) {
108
+ return normalizeMoneyAmount(left, options) === normalizeMoneyAmount(right, options);
109
+ }
110
+ function mapTransferStatusToPartnerState(status, mapping) {
111
+ const partnerState = mapping[status];
112
+ if (!partnerState) {
113
+ throw new EdgeValidationError("Unsupported transfer status", {
114
+ status: [`Unsupported status: ${status}`]
115
+ });
116
+ }
117
+ return partnerState;
118
+ }
86
119
  async function startTransferVerification(client, options) {
87
120
  const origin = typeof options.origin === "string" ? options.origin.trim() : "";
88
121
  if (!origin) {
@@ -103,9 +136,14 @@ async function startTransferVerification(client, options) {
103
136
  const verificationSession = await client.createVerificationSession(transferId, { origin });
104
137
  return { transfer, verificationSession };
105
138
  }
106
- function parseMoneyToCents(value, field) {
139
+ function parseMoneyToCents(value, field, options = {}) {
107
140
  let input;
108
141
  if (typeof value === "number") {
142
+ if (options.allowNumber === false) {
143
+ throw new EdgeValidationError("Money amount must be a string in strict mode", {
144
+ [field]: ['Pass a decimal string like "25.00"']
145
+ });
146
+ }
109
147
  if (!Number.isFinite(value)) {
110
148
  throw new EdgeValidationError("Money amount must be finite", { [field]: ["Must be a finite number"] });
111
149
  }
@@ -417,6 +455,7 @@ function validateMtlsConfig(config) {
417
455
  import { EdgeAuthenticationError, EdgeValidationError as EdgeValidationError3 } from "@edge-markets/connect";
418
456
  var EdgeUserSession = class {
419
457
  constructor(options) {
458
+ this.refreshPromise = null;
420
459
  if (!options.edge?.forUser || !options.edge?.refreshTokens) {
421
460
  throw new EdgeValidationError3("EdgeUserSession requires an EdgeConnectServer instance", {
422
461
  edge: ["Provide edge.forUser and edge.refreshTokens"]
@@ -445,29 +484,44 @@ var EdgeUserSession = class {
445
484
  this.refreshSkewMs = options.refreshSkewMs ?? 6e4;
446
485
  this.now = options.now ?? Date.now;
447
486
  this.onRefresh = options.onRefresh;
487
+ this.onEvent = options.onEvent;
488
+ this.singleFlightRefresh = options.singleFlightRefresh ?? true;
448
489
  }
449
490
  async getValidTokens(options = {}) {
450
491
  const tokens = await this.loadTokens();
451
492
  if (!options.forceRefresh && tokens.expiresAt > this.now() + this.refreshSkewMs) {
452
493
  return tokens;
453
494
  }
454
- const refreshed = await this.edge.refreshTokens(tokens.refreshToken);
455
- await this.saveTokens(refreshed, true);
456
- await this.onRefresh?.(refreshed);
457
- return refreshed;
495
+ if (this.singleFlightRefresh) {
496
+ if (!this.refreshPromise) {
497
+ this.refreshPromise = this.refreshTokens(tokens).finally(() => {
498
+ this.refreshPromise = null;
499
+ });
500
+ }
501
+ return this.refreshPromise;
502
+ }
503
+ return this.refreshTokens(tokens);
458
504
  }
459
505
  async getClient(options = {}) {
460
506
  const tokens = await this.getValidTokens(options);
461
507
  return this.edge.forUser(tokens.accessToken);
462
508
  }
463
- async withClient(callback) {
464
- const tokens = await this.getValidTokens();
509
+ async withClient(callback, options = {}) {
510
+ const tokens = await this.getValidTokens(options);
465
511
  return callback(this.edge.forUser(tokens.accessToken), tokens);
466
512
  }
467
513
  async saveTokens(tokens, refreshed = false) {
468
514
  validateTokens(tokens);
469
515
  const value = this.tokenVault ? this.tokenVault.encryptTokens(tokens) : tokens;
470
516
  await this.tokenStore.save(this.subjectId, value, { tokens, refreshed });
517
+ await this.emit({
518
+ type: "session.saved",
519
+ subjectId: this.subjectId,
520
+ refreshed,
521
+ encrypted: typeof value === "string",
522
+ expiresAt: tokens.expiresAt,
523
+ scope: tokens.scope
524
+ });
471
525
  }
472
526
  async loadTokens() {
473
527
  const value = await this.tokenStore.load(this.subjectId);
@@ -484,11 +538,60 @@ var EdgeUserSession = class {
484
538
  }
485
539
  const tokens = this.tokenVault.decryptTokens(value);
486
540
  validateTokens(tokens);
541
+ await this.emitLoaded(tokens, true);
487
542
  return tokens;
488
543
  }
489
544
  validateTokens(value);
545
+ await this.emitLoaded(value, false);
490
546
  return value;
491
547
  }
548
+ async refreshTokens(currentTokens) {
549
+ await this.emit({
550
+ type: "session.refresh_started",
551
+ subjectId: this.subjectId,
552
+ expiresAt: currentTokens.expiresAt,
553
+ scope: currentTokens.scope
554
+ });
555
+ try {
556
+ const refreshed = await this.edge.refreshTokens(currentTokens.refreshToken);
557
+ const tokens = {
558
+ ...refreshed,
559
+ refreshToken: refreshed.refreshToken || currentTokens.refreshToken
560
+ };
561
+ await this.saveTokens(tokens, true);
562
+ await this.onRefresh?.(tokens);
563
+ await this.emit({
564
+ type: "session.refresh_succeeded",
565
+ subjectId: this.subjectId,
566
+ refreshed: true,
567
+ expiresAt: tokens.expiresAt,
568
+ scope: tokens.scope
569
+ });
570
+ return tokens;
571
+ } catch (error) {
572
+ await this.emit({
573
+ type: "session.refresh_failed",
574
+ subjectId: this.subjectId,
575
+ error: error instanceof Error ? error.message : "Unknown token refresh error"
576
+ });
577
+ throw error;
578
+ }
579
+ }
580
+ async emitLoaded(tokens, encrypted) {
581
+ await this.emit({
582
+ type: "session.loaded",
583
+ subjectId: this.subjectId,
584
+ encrypted,
585
+ expiresAt: tokens.expiresAt,
586
+ scope: tokens.scope
587
+ });
588
+ }
589
+ async emit(event) {
590
+ try {
591
+ await this.onEvent?.(event);
592
+ } catch {
593
+ }
594
+ }
492
595
  };
493
596
  function createEdgeUserSession(options) {
494
597
  return new EdgeUserSession(options);
@@ -540,8 +643,8 @@ var EdgeWebhookReconciler = class {
540
643
  if (!options.cursorStore?.load || !options.cursorStore?.save) {
541
644
  throw new Error("EdgeWebhookReconciler: cursorStore.load and cursorStore.save are required");
542
645
  }
543
- if (!options.processEvent) {
544
- throw new Error("EdgeWebhookReconciler: processEvent is required");
646
+ if (!options.processEvent && !options.inbox) {
647
+ throw new Error("EdgeWebhookReconciler: processEvent or inbox is required");
545
648
  }
546
649
  if (options.maxPages !== void 0 && (!Number.isInteger(options.maxPages) || options.maxPages <= 0)) {
547
650
  throw new Error("EdgeWebhookReconciler: maxPages must be a positive integer");
@@ -568,6 +671,8 @@ var EdgeWebhookReconciler = class {
568
671
  async runInternal() {
569
672
  let cursor = normalizeCursor(await this.options.cursorStore.load());
570
673
  let processed = 0;
674
+ let accepted = 0;
675
+ let duplicates = 0;
571
676
  let pages = 0;
572
677
  let hasMore = false;
573
678
  const maxPages = this.options.maxPages ?? DEFAULT_MAX_PAGES;
@@ -581,6 +686,7 @@ var EdgeWebhookReconciler = class {
581
686
  if (result.events.length === 0) {
582
687
  return {
583
688
  processed,
689
+ ...this.options.inbox ? { accepted, duplicates } : {},
584
690
  pages,
585
691
  ...cursor ? { cursor } : {},
586
692
  hasMore,
@@ -589,14 +695,23 @@ var EdgeWebhookReconciler = class {
589
695
  };
590
696
  }
591
697
  for (const event of result.events) {
592
- await this.options.processEvent(event);
698
+ if (this.options.inbox) {
699
+ const acceptResult = await this.options.inbox.acceptSyncedEvent(event);
700
+ accepted++;
701
+ if (acceptResult.duplicate) duplicates++;
702
+ const processResult = await this.options.inbox.process(event.id);
703
+ if (processResult.processed) processed++;
704
+ } else {
705
+ await this.options.processEvent?.(event);
706
+ processed++;
707
+ }
593
708
  cursor = event.id;
594
709
  await this.options.cursorStore.save(cursor);
595
- processed++;
596
710
  }
597
711
  if (!result.hasMore) {
598
712
  return {
599
713
  processed,
714
+ ...this.options.inbox ? { accepted, duplicates } : {},
600
715
  pages,
601
716
  ...cursor ? { cursor } : {},
602
717
  hasMore: false,
@@ -606,6 +721,7 @@ var EdgeWebhookReconciler = class {
606
721
  }
607
722
  return {
608
723
  processed,
724
+ ...this.options.inbox ? { accepted, duplicates } : {},
609
725
  pages,
610
726
  ...cursor ? { cursor } : {},
611
727
  hasMore,
@@ -1530,8 +1646,229 @@ function edgeTitle(error) {
1530
1646
  return error.code.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1531
1647
  }
1532
1648
 
1649
+ // src/http-helpers.ts
1650
+ import { EdgeValidationError as EdgeValidationError7 } from "@edge-markets/connect";
1651
+
1652
+ // src/webhook-parser.ts
1653
+ import {
1654
+ EDGE_WEBHOOK_EVENT_TYPES,
1655
+ EdgeAuthenticationError as EdgeAuthenticationError3,
1656
+ EdgeValidationError as EdgeValidationError6
1657
+ } from "@edge-markets/connect";
1658
+
1659
+ // src/webhook-signing.ts
1660
+ import crypto2 from "crypto";
1661
+ function verifyWebhookSignature(header, body, secret, options = {}) {
1662
+ if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
1663
+ return false;
1664
+ }
1665
+ const requestedTolerance = options.toleranceSeconds;
1666
+ const toleranceSeconds = typeof requestedTolerance === "number" && Number.isFinite(requestedTolerance) && requestedTolerance >= 0 ? requestedTolerance : 300;
1667
+ const parts = header.split(",").map((p) => p.trim());
1668
+ const tPart = parts.find((p) => p.startsWith("t="));
1669
+ const v1Part = parts.find((p) => p.startsWith("v1="));
1670
+ if (!tPart || !v1Part) return false;
1671
+ const timestamp = parseInt(tPart.slice(2), 10);
1672
+ const signature = v1Part.slice(3);
1673
+ if (isNaN(timestamp) || !signature) return false;
1674
+ const now = Math.floor(Date.now() / 1e3);
1675
+ if (Math.abs(now - timestamp) > toleranceSeconds) return false;
1676
+ const signedContent = `${timestamp}.${body}`;
1677
+ const expectedHmac = crypto2.createHmac("sha256", secret).update(signedContent).digest("hex");
1678
+ try {
1679
+ const sigBuf = Buffer.from(signature, "hex");
1680
+ const expBuf = Buffer.from(expectedHmac, "hex");
1681
+ if (sigBuf.length !== expBuf.length) return false;
1682
+ return crypto2.timingSafeEqual(sigBuf, expBuf);
1683
+ } catch {
1684
+ return false;
1685
+ }
1686
+ }
1687
+
1688
+ // src/webhook-parser.ts
1689
+ var EVENT_TYPES = new Set(EDGE_WEBHOOK_EVENT_TYPES);
1690
+ var TRANSFER_TYPES = /* @__PURE__ */ new Set(["debit", "credit"]);
1691
+ function parseAndVerifyWebhook(options) {
1692
+ const body = normalizeRawBody(options.rawBody);
1693
+ const header = normalizeSignatureHeader(options.signatureHeader);
1694
+ const signatureTimestamp = extractWebhookSignatureTimestamp(header);
1695
+ if (!header || !signatureTimestamp) {
1696
+ throw new EdgeAuthenticationError3("Missing or malformed EDGE webhook signature");
1697
+ }
1698
+ if (!verifyWebhookSignature(header, body, options.secret, { toleranceSeconds: options.toleranceSeconds })) {
1699
+ throw new EdgeAuthenticationError3("Invalid EDGE webhook signature");
1700
+ }
1701
+ let parsed;
1702
+ try {
1703
+ parsed = JSON.parse(body);
1704
+ } catch {
1705
+ throw new EdgeValidationError6("EDGE webhook body must be valid JSON", {
1706
+ rawBody: ["Unable to parse JSON after signature verification"]
1707
+ });
1708
+ }
1709
+ const event = validateEdgeWebhookEvent(parsed);
1710
+ return {
1711
+ event,
1712
+ eventId: event.id,
1713
+ eventType: event.type,
1714
+ signatureTimestamp,
1715
+ receivedAt: options.receivedAt ?? /* @__PURE__ */ new Date()
1716
+ };
1717
+ }
1718
+ function extractWebhookSignatureTimestamp(header) {
1719
+ const normalizedHeader = normalizeSignatureHeader(header);
1720
+ if (!normalizedHeader) return void 0;
1721
+ const part = normalizedHeader.split(",").map((value) => value.trim()).find((value) => value.startsWith("t="));
1722
+ if (!part) return void 0;
1723
+ const timestamp = Number(part.slice(2));
1724
+ if (!Number.isInteger(timestamp) || timestamp <= 0) return void 0;
1725
+ return timestamp;
1726
+ }
1727
+ function isEdgeWebhookEvent(value) {
1728
+ try {
1729
+ validateEdgeWebhookEvent(value);
1730
+ return true;
1731
+ } catch {
1732
+ return false;
1733
+ }
1734
+ }
1735
+ function validateEdgeWebhookEvent(value) {
1736
+ const errors = {};
1737
+ if (!isRecord(value)) {
1738
+ throw new EdgeValidationError6("EDGE webhook event must be an object", {
1739
+ event: ["Expected JSON object"]
1740
+ });
1741
+ }
1742
+ const id = value.id;
1743
+ const type = value.type;
1744
+ const createdAt = value.created_at;
1745
+ const data = value.data;
1746
+ if (typeof id !== "string" || !id.trim()) {
1747
+ errors.id = ["Required string"];
1748
+ }
1749
+ if (typeof type !== "string" || !EVENT_TYPES.has(type)) {
1750
+ errors.type = [`Must be one of: ${EDGE_WEBHOOK_EVENT_TYPES.join(", ")}`];
1751
+ }
1752
+ if (typeof createdAt !== "string" || !createdAt.trim() || Number.isNaN(Date.parse(createdAt))) {
1753
+ errors.created_at = ["Required ISO 8601 timestamp string"];
1754
+ }
1755
+ if (!isRecord(data)) {
1756
+ errors.data = ["Required object"];
1757
+ }
1758
+ if (Object.keys(errors).length === 0) {
1759
+ validateKnownEventData(type, data, errors);
1760
+ }
1761
+ if (Object.keys(errors).length > 0) {
1762
+ throw new EdgeValidationError6("Invalid EDGE webhook event", errors);
1763
+ }
1764
+ return value;
1765
+ }
1766
+ function validateKnownEventData(type, data, errors) {
1767
+ switch (type) {
1768
+ case "transfer.completed":
1769
+ validateTransferEventData(data, "completed", errors);
1770
+ return;
1771
+ case "transfer.failed":
1772
+ validateTransferEventData(data, "failed", errors);
1773
+ if (data.reason !== void 0 && typeof data.reason !== "string") {
1774
+ errors["data.reason"] = ["Must be a string when provided"];
1775
+ }
1776
+ return;
1777
+ case "transfer.expired":
1778
+ validateTransferEventData(data, "expired", errors);
1779
+ return;
1780
+ case "transfer.processing":
1781
+ validateTransferEventData(data, "processing", errors);
1782
+ return;
1783
+ case "consent.revoked":
1784
+ validateRequiredString(data, "userId", errors);
1785
+ validateRequiredString(data, "clientId", errors);
1786
+ validateRequiredIsoTimestamp(data, "revokedAt", errors);
1787
+ return;
1788
+ default: {
1789
+ const exhaustive = type;
1790
+ throw new EdgeValidationError6("Unsupported EDGE webhook event type", {
1791
+ type: [`Unsupported event type: ${exhaustive}`]
1792
+ });
1793
+ }
1794
+ }
1795
+ }
1796
+ function validateTransferEventData(data, status, errors) {
1797
+ validateRequiredString(data, "transferId", errors);
1798
+ if (data.status !== status) {
1799
+ errors["data.status"] = [`Must be "${status}" for this event type`];
1800
+ }
1801
+ if (typeof data.type !== "string" || !TRANSFER_TYPES.has(data.type)) {
1802
+ errors["data.type"] = ['Must be "debit" or "credit"'];
1803
+ }
1804
+ validateRequiredString(data, "amount", errors);
1805
+ }
1806
+ function validateRequiredString(data, field, errors) {
1807
+ if (typeof data[field] !== "string" || !data[field].trim()) {
1808
+ errors[`data.${field}`] = ["Required string"];
1809
+ }
1810
+ }
1811
+ function validateRequiredIsoTimestamp(data, field, errors) {
1812
+ if (typeof data[field] !== "string" || !data[field].trim() || Number.isNaN(Date.parse(data[field]))) {
1813
+ errors[`data.${field}`] = ["Required ISO 8601 timestamp string"];
1814
+ }
1815
+ }
1816
+ function normalizeRawBody(rawBody) {
1817
+ if (typeof rawBody === "string") {
1818
+ return rawBody;
1819
+ }
1820
+ if (Buffer.isBuffer(rawBody) || rawBody instanceof Uint8Array) {
1821
+ return Buffer.from(rawBody).toString("utf8");
1822
+ }
1823
+ throw new EdgeValidationError6("EDGE webhook raw body is required", {
1824
+ rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
1825
+ });
1826
+ }
1827
+ function normalizeSignatureHeader(header) {
1828
+ if (Array.isArray(header)) {
1829
+ return header.map((value) => value.trim()).filter(Boolean).join(",");
1830
+ }
1831
+ if (typeof header !== "string") return void 0;
1832
+ const trimmed = header.trim();
1833
+ return trimmed || void 0;
1834
+ }
1835
+ function isRecord(value) {
1836
+ return !!value && typeof value === "object" && !Array.isArray(value);
1837
+ }
1838
+
1839
+ // src/http-helpers.ts
1840
+ function getEdgeWebhookRawBody(request) {
1841
+ if (typeof request.rawBody === "string" || Buffer.isBuffer(request.rawBody) || request.rawBody instanceof Uint8Array) {
1842
+ return request.rawBody;
1843
+ }
1844
+ throw new EdgeValidationError7("EDGE webhook raw body is required", {
1845
+ rawBody: [
1846
+ "Configure your framework to expose the raw UTF-8 request body before JSON parsing. Parsed JSON bodies cannot be used for HMAC verification."
1847
+ ]
1848
+ });
1849
+ }
1850
+ function getEdgeWebhookSignatureHeader(request, headerName = "x-edge-signature") {
1851
+ const headers = request.headers ?? {};
1852
+ const lowerHeaderName = headerName.toLowerCase();
1853
+ for (const [name, value] of Object.entries(headers)) {
1854
+ if (name.toLowerCase() === lowerHeaderName) {
1855
+ if (typeof value === "string" || Array.isArray(value)) return value;
1856
+ if (value === void 0 || value === null) return void 0;
1857
+ return String(value);
1858
+ }
1859
+ }
1860
+ return void 0;
1861
+ }
1862
+ function parseWebhookHttpRequest(options) {
1863
+ return parseAndVerifyWebhook({
1864
+ ...options,
1865
+ rawBody: getEdgeWebhookRawBody(options.request),
1866
+ signatureHeader: getEdgeWebhookSignatureHeader(options.request, options.signatureHeaderName)
1867
+ });
1868
+ }
1869
+
1533
1870
  // src/identity.ts
1534
- import { EdgeValidationError as EdgeValidationError6 } from "@edge-markets/connect";
1871
+ import { EdgeValidationError as EdgeValidationError8 } from "@edge-markets/connect";
1535
1872
  var DEFAULT_IDENTITY_POLICY = {
1536
1873
  minNameScore: 70,
1537
1874
  minAddressScore: 65,
@@ -1559,7 +1896,7 @@ function buildVerifyIdentityPayload(profile) {
1559
1896
  errors.address = ["At least one address field is required"];
1560
1897
  }
1561
1898
  if (Object.keys(errors).length > 0) {
1562
- throw new EdgeValidationError6("Partner identity profile is incomplete", errors);
1899
+ throw new EdgeValidationError8("Partner identity profile is incomplete", errors);
1563
1900
  }
1564
1901
  return payload;
1565
1902
  }
@@ -1596,7 +1933,7 @@ function validatePolicy(policy) {
1596
1933
  if (!isScore(policy.minNameScore)) errors.minNameScore = ["Must be between 0 and 100"];
1597
1934
  if (!isScore(policy.minAddressScore)) errors.minAddressScore = ["Must be between 0 and 100"];
1598
1935
  if (Object.keys(errors).length > 0) {
1599
- throw new EdgeValidationError6("Invalid identity policy", errors);
1936
+ throw new EdgeValidationError8("Invalid identity policy", errors);
1600
1937
  }
1601
1938
  }
1602
1939
  function isScore(value) {
@@ -1612,9 +1949,155 @@ function firstNonEmpty(...values) {
1612
1949
  return "";
1613
1950
  }
1614
1951
 
1952
+ // src/session-store.ts
1953
+ import { EdgeValidationError as EdgeValidationError9 } from "@edge-markets/connect";
1954
+ function createSessionTokenRecord(subjectId, tokens, tokenVault, options = {}) {
1955
+ const normalizedSubjectId = normalizeRequiredString(subjectId, "subjectId");
1956
+ validateTokens2(tokens);
1957
+ return {
1958
+ subjectId: normalizedSubjectId,
1959
+ encryptedTokens: tokenVault.encryptTokens(tokens),
1960
+ expiresAt: normalizeSessionExpiresAt(tokens.expiresAt),
1961
+ scopes: options.scopes ?? splitScopes(tokens.scope),
1962
+ ...options.edgeUserId ? { edgeUserId: options.edgeUserId } : {},
1963
+ ...options.keyId ? { keyId: options.keyId } : {},
1964
+ ...options.metadata ? { metadata: options.metadata } : {}
1965
+ };
1966
+ }
1967
+ function parseSessionTokenRecord(record) {
1968
+ if (!isRecord2(record)) {
1969
+ throw new EdgeValidationError9("EDGE session token record must be an object", {
1970
+ record: ["Expected object"]
1971
+ });
1972
+ }
1973
+ const subjectId = normalizeRequiredString(record.subjectId, "subjectId");
1974
+ const encryptedTokens = normalizeRequiredString(record.encryptedTokens, "encryptedTokens");
1975
+ const expiresAt = normalizeSessionExpiresAt(record.expiresAt);
1976
+ const scopes = record.scopes === void 0 ? void 0 : normalizeScopes(record.scopes);
1977
+ return {
1978
+ subjectId,
1979
+ encryptedTokens,
1980
+ expiresAt,
1981
+ ...scopes ? { scopes } : {},
1982
+ ...typeof record.edgeUserId === "string" && record.edgeUserId.trim() ? { edgeUserId: record.edgeUserId.trim() } : {},
1983
+ ...typeof record.keyId === "string" && record.keyId.trim() ? { keyId: record.keyId.trim() } : {},
1984
+ ...isRecord2(record.metadata) ? { metadata: record.metadata } : {}
1985
+ };
1986
+ }
1987
+ function encryptLegacySessionTokens(legacyRecord, tokenVault, options) {
1988
+ const subjectId = normalizeRequiredString(options.subjectId || legacyRecord.subjectId, "subjectId");
1989
+ const accessToken = normalizeRequiredString(legacyRecord.accessToken, "accessToken");
1990
+ const refreshToken = normalizeRequiredString(legacyRecord.refreshToken, "refreshToken");
1991
+ if (!options.allowPlaintext && !tokenVault.isEncrypted(accessToken) && !tokenVault.isEncrypted(refreshToken)) {
1992
+ throw new EdgeValidationError9("Plaintext token migration requires allowPlaintext: true", {
1993
+ allowPlaintext: ["Set allowPlaintext only inside an explicit migration path"]
1994
+ });
1995
+ }
1996
+ const scope = normalizeScopeString(legacyRecord.scope ?? legacyRecord.scopes);
1997
+ const tokens = {
1998
+ accessToken,
1999
+ refreshToken,
2000
+ ...typeof legacyRecord.idToken === "string" && legacyRecord.idToken.trim() ? { idToken: legacyRecord.idToken.trim() } : {},
2001
+ expiresIn: normalizeExpiresIn(legacyRecord.expiresIn),
2002
+ expiresAt: normalizeSessionExpiresAt(legacyRecord.expiresAt),
2003
+ scope
2004
+ };
2005
+ return createSessionTokenRecord(subjectId, tokens, tokenVault, {
2006
+ scopes: splitScopes(scope),
2007
+ ...legacyRecord.edgeUserId ? { edgeUserId: legacyRecord.edgeUserId } : {},
2008
+ ...options.keyId ? { keyId: options.keyId } : {},
2009
+ ...legacyRecord.metadata ? { metadata: legacyRecord.metadata } : {}
2010
+ });
2011
+ }
2012
+ function normalizeSessionExpiresAt(value) {
2013
+ if (value instanceof Date) {
2014
+ const time = value.getTime();
2015
+ if (!Number.isFinite(time) || time <= 0) {
2016
+ throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
2017
+ expiresAt: ["Date must be valid"]
2018
+ });
2019
+ }
2020
+ return time;
2021
+ }
2022
+ if (typeof value === "string") {
2023
+ const trimmed = value.trim();
2024
+ if (!trimmed) {
2025
+ throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
2026
+ expiresAt: ["Required"]
2027
+ });
2028
+ }
2029
+ if (/^\d+$/.test(trimmed)) {
2030
+ return normalizeSessionExpiresAt(Number(trimmed));
2031
+ }
2032
+ const time = Date.parse(trimmed);
2033
+ if (!Number.isNaN(time) && time > 0) return time;
2034
+ }
2035
+ if (typeof value === "number" && Number.isFinite(value)) {
2036
+ if (!Number.isInteger(value) || value <= 0) {
2037
+ throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
2038
+ expiresAt: ["Must be a positive integer timestamp in milliseconds"]
2039
+ });
2040
+ }
2041
+ if (value < 1e10) {
2042
+ throw new EdgeValidationError9("Ambiguous EDGE session expiresAt", {
2043
+ expiresAt: ["Use Unix milliseconds, not seconds"]
2044
+ });
2045
+ }
2046
+ return value;
2047
+ }
2048
+ throw new EdgeValidationError9("Invalid EDGE session expiresAt", {
2049
+ expiresAt: ["Use Unix milliseconds, ISO string, or Date"]
2050
+ });
2051
+ }
2052
+ function normalizeExpiresIn(value) {
2053
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
2054
+ if (typeof value === "string" && value.trim()) {
2055
+ const parsed = Number(value);
2056
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
2057
+ }
2058
+ return 600;
2059
+ }
2060
+ function normalizeScopeString(value) {
2061
+ if (Array.isArray(value)) return normalizeScopes(value).join(" ");
2062
+ if (typeof value === "string") return splitScopes(value).join(" ");
2063
+ return "";
2064
+ }
2065
+ function normalizeScopes(value) {
2066
+ if (Array.isArray(value)) {
2067
+ return value.map((scope) => typeof scope === "string" ? scope.trim() : "").filter(Boolean);
2068
+ }
2069
+ if (typeof value === "string") {
2070
+ return splitScopes(value);
2071
+ }
2072
+ throw new EdgeValidationError9("Invalid EDGE session scopes", {
2073
+ scopes: ["Expected array or space-delimited string"]
2074
+ });
2075
+ }
2076
+ function splitScopes(value) {
2077
+ return value.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
2078
+ }
2079
+ function normalizeRequiredString(value, field) {
2080
+ if (typeof value !== "string" || !value.trim()) {
2081
+ throw new EdgeValidationError9("Invalid EDGE session token record", {
2082
+ [field]: ["Required string"]
2083
+ });
2084
+ }
2085
+ return value.trim();
2086
+ }
2087
+ function validateTokens2(tokens) {
2088
+ normalizeRequiredString(tokens.accessToken, "accessToken");
2089
+ normalizeRequiredString(tokens.refreshToken, "refreshToken");
2090
+ normalizeSessionExpiresAt(tokens.expiresAt);
2091
+ normalizeExpiresIn(tokens.expiresIn);
2092
+ normalizeScopeString(tokens.scope);
2093
+ }
2094
+ function isRecord2(value) {
2095
+ return !!value && typeof value === "object" && !Array.isArray(value);
2096
+ }
2097
+
1615
2098
  // src/token-vault.ts
1616
- import { EdgeValidationError as EdgeValidationError7 } from "@edge-markets/connect";
1617
- import crypto2 from "crypto";
2099
+ import { EdgeValidationError as EdgeValidationError10 } from "@edge-markets/connect";
2100
+ import crypto3 from "crypto";
1618
2101
  var TOKEN_VAULT_VERSION = "edge_vault_v1";
1619
2102
  var IV_BYTES = 12;
1620
2103
  var TAG_BYTES = 16;
@@ -1626,7 +2109,7 @@ var EdgeTokenVault = class {
1626
2109
  for (const [index, key] of (options.previousKeys ?? []).entries()) {
1627
2110
  const normalized = normalizeVaultKey(key, `previousKeys[${index}]`);
1628
2111
  if (this.keysById.has(normalized.id)) {
1629
- throw new EdgeValidationError7("Duplicate token vault key ID", {
2112
+ throw new EdgeValidationError10("Duplicate token vault key ID", {
1630
2113
  keyId: [`Duplicate key ID "${normalized.id}"`]
1631
2114
  });
1632
2115
  }
@@ -1635,9 +2118,9 @@ var EdgeTokenVault = class {
1635
2118
  }
1636
2119
  encryptTokens(tokens) {
1637
2120
  validateEdgeTokens(tokens);
1638
- const iv = crypto2.randomBytes(IV_BYTES);
2121
+ const iv = crypto3.randomBytes(IV_BYTES);
1639
2122
  const aad = Buffer.from(`${TOKEN_VAULT_VERSION}.${this.currentKey.id}`, "utf8");
1640
- const cipher = crypto2.createCipheriv("aes-256-gcm", this.currentKey.key, iv);
2123
+ const cipher = crypto3.createCipheriv("aes-256-gcm", this.currentKey.key, iv);
1641
2124
  cipher.setAAD(aad);
1642
2125
  const plaintext = Buffer.from(JSON.stringify(tokens), "utf8");
1643
2126
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
@@ -1654,12 +2137,12 @@ var EdgeTokenVault = class {
1654
2137
  const parsed = parseEnvelope(envelope);
1655
2138
  const key = this.keysById.get(parsed.keyId);
1656
2139
  if (!key) {
1657
- throw new EdgeValidationError7("Unknown token vault key ID", {
2140
+ throw new EdgeValidationError10("Unknown token vault key ID", {
1658
2141
  keyId: [`No key configured for "${parsed.keyId}"`]
1659
2142
  });
1660
2143
  }
1661
2144
  try {
1662
- const decipher = crypto2.createDecipheriv("aes-256-gcm", key, parsed.iv);
2145
+ const decipher = crypto3.createDecipheriv("aes-256-gcm", key, parsed.iv);
1663
2146
  decipher.setAAD(Buffer.from(`${TOKEN_VAULT_VERSION}.${parsed.keyId}`, "utf8"));
1664
2147
  decipher.setAuthTag(parsed.tag);
1665
2148
  const plaintext = Buffer.concat([decipher.update(parsed.ciphertext), decipher.final()]).toString("utf8");
@@ -1667,8 +2150,8 @@ var EdgeTokenVault = class {
1667
2150
  validateEdgeTokens(tokens);
1668
2151
  return tokens;
1669
2152
  } catch (error) {
1670
- if (error instanceof EdgeValidationError7) throw error;
1671
- throw new EdgeValidationError7("Failed to decrypt EDGE tokens", {
2153
+ if (error instanceof EdgeValidationError10) throw error;
2154
+ throw new EdgeValidationError10("Failed to decrypt EDGE tokens", {
1672
2155
  tokenEnvelope: ["Envelope is malformed, corrupted, or encrypted with a different key"]
1673
2156
  });
1674
2157
  }
@@ -1686,7 +2169,7 @@ function isEdgeTokenVaultEnvelope(value) {
1686
2169
  function normalizeVaultKey(input, fieldName) {
1687
2170
  const id = typeof input.id === "string" ? input.id.trim() : "";
1688
2171
  if (!id) {
1689
- throw new EdgeValidationError7("Token vault key ID is required", {
2172
+ throw new EdgeValidationError10("Token vault key ID is required", {
1690
2173
  [`${fieldName}.id`]: ["Required"]
1691
2174
  });
1692
2175
  }
@@ -1717,7 +2200,7 @@ function normalizeKeyMaterial(input, fieldName) {
1717
2200
  }
1718
2201
  const key = candidates.find((candidate) => candidate.length === KEY_BYTES);
1719
2202
  if (!key) {
1720
- throw new EdgeValidationError7("Token vault key must decode to 32 bytes", {
2203
+ throw new EdgeValidationError10("Token vault key must decode to 32 bytes", {
1721
2204
  [fieldName]: ["Provide a 32-byte key as base64, base64url, hex, Buffer, or Uint8Array"]
1722
2205
  });
1723
2206
  }
@@ -1748,18 +2231,18 @@ function validateEdgeTokens(tokens) {
1748
2231
  }
1749
2232
  }
1750
2233
  if (Object.keys(errors).length > 0) {
1751
- throw new EdgeValidationError7("Invalid EDGE token payload", errors);
2234
+ throw new EdgeValidationError10("Invalid EDGE token payload", errors);
1752
2235
  }
1753
2236
  }
1754
2237
  function parseEnvelope(envelope) {
1755
2238
  if (typeof envelope !== "string") {
1756
- throw new EdgeValidationError7("Token envelope must be a string", {
2239
+ throw new EdgeValidationError10("Token envelope must be a string", {
1757
2240
  tokenEnvelope: ["Expected string"]
1758
2241
  });
1759
2242
  }
1760
2243
  const parts = envelope.split(".");
1761
2244
  if (parts.length !== 5 || parts[0] !== TOKEN_VAULT_VERSION) {
1762
- throw new EdgeValidationError7("Invalid EDGE token vault envelope", {
2245
+ throw new EdgeValidationError10("Invalid EDGE token vault envelope", {
1763
2246
  tokenEnvelope: [`Expected ${TOKEN_VAULT_VERSION}.<keyId>.<iv>.<tag>.<ciphertext>`]
1764
2247
  });
1765
2248
  }
@@ -1774,7 +2257,7 @@ function parseEnvelope(envelope) {
1774
2257
  if (tag.length !== TAG_BYTES) errors.tag = [`Must be ${TAG_BYTES} bytes`];
1775
2258
  if (ciphertext.length === 0) errors.ciphertext = ["Required"];
1776
2259
  if (Object.keys(errors).length > 0) {
1777
- throw new EdgeValidationError7("Invalid EDGE token vault envelope", errors);
2260
+ throw new EdgeValidationError10("Invalid EDGE token vault envelope", errors);
1778
2261
  }
1779
2262
  return { keyId, iv, tag, ciphertext };
1780
2263
  }
@@ -1788,13 +2271,13 @@ function fromBase64Url2(value) {
1788
2271
  }
1789
2272
 
1790
2273
  // src/webhook-guards.ts
1791
- import { EdgeValidationError as EdgeValidationError8 } from "@edge-markets/connect";
2274
+ import { EdgeValidationError as EdgeValidationError11 } from "@edge-markets/connect";
1792
2275
  function isTransferWebhookEvent(event) {
1793
2276
  return event.type.startsWith("transfer.");
1794
2277
  }
1795
2278
  function assertTransferEventMatchesIntent(event, intent) {
1796
2279
  if (!isTransferWebhookEvent(event)) {
1797
- throw new EdgeValidationError8("Webhook event is not a transfer event", {
2280
+ throw new EdgeValidationError11("Webhook event is not a transfer event", {
1798
2281
  type: [`Received ${event.type}`]
1799
2282
  });
1800
2283
  }
@@ -1817,195 +2300,243 @@ function assertTransferEventMatchesIntent(event, intent) {
1817
2300
  }
1818
2301
  }
1819
2302
  if (Object.keys(errors).length > 0) {
1820
- throw new EdgeValidationError8("Webhook transfer event does not match expected transfer intent", errors);
2303
+ throw new EdgeValidationError11("Webhook transfer event does not match expected transfer intent", errors);
1821
2304
  }
1822
2305
  }
2306
+ var assertWebhookMatchesTransfer = assertTransferEventMatchesIntent;
1823
2307
 
1824
- // src/webhook-parser.ts
1825
- import {
1826
- EDGE_WEBHOOK_EVENT_TYPES,
1827
- EdgeAuthenticationError as EdgeAuthenticationError3,
1828
- EdgeValidationError as EdgeValidationError9
1829
- } from "@edge-markets/connect";
1830
-
1831
- // src/webhook-signing.ts
1832
- import crypto3 from "crypto";
1833
- function verifyWebhookSignature(header, body, secret, options = {}) {
1834
- if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
1835
- return false;
1836
- }
1837
- const requestedTolerance = options.toleranceSeconds;
1838
- const toleranceSeconds = typeof requestedTolerance === "number" && Number.isFinite(requestedTolerance) && requestedTolerance >= 0 ? requestedTolerance : 300;
1839
- const parts = header.split(",").map((p) => p.trim());
1840
- const tPart = parts.find((p) => p.startsWith("t="));
1841
- const v1Part = parts.find((p) => p.startsWith("v1="));
1842
- if (!tPart || !v1Part) return false;
1843
- const timestamp = parseInt(tPart.slice(2), 10);
1844
- const signature = v1Part.slice(3);
1845
- if (isNaN(timestamp) || !signature) return false;
1846
- const now = Math.floor(Date.now() / 1e3);
1847
- if (Math.abs(now - timestamp) > toleranceSeconds) return false;
1848
- const signedContent = `${timestamp}.${body}`;
1849
- const expectedHmac = crypto3.createHmac("sha256", secret).update(signedContent).digest("hex");
1850
- try {
1851
- const sigBuf = Buffer.from(signature, "hex");
1852
- const expBuf = Buffer.from(expectedHmac, "hex");
1853
- if (sigBuf.length !== expBuf.length) return false;
1854
- return crypto3.timingSafeEqual(sigBuf, expBuf);
1855
- } catch {
1856
- return false;
1857
- }
1858
- }
1859
-
1860
- // src/webhook-parser.ts
1861
- var EVENT_TYPES = new Set(EDGE_WEBHOOK_EVENT_TYPES);
1862
- var TRANSFER_TYPES = /* @__PURE__ */ new Set(["debit", "credit"]);
1863
- function parseAndVerifyWebhook(options) {
1864
- const body = normalizeRawBody(options.rawBody);
1865
- const header = normalizeSignatureHeader(options.signatureHeader);
1866
- const signatureTimestamp = extractWebhookSignatureTimestamp(header);
1867
- if (!header || !signatureTimestamp) {
1868
- throw new EdgeAuthenticationError3("Missing or malformed EDGE webhook signature");
1869
- }
1870
- if (!verifyWebhookSignature(header, body, options.secret, { toleranceSeconds: options.toleranceSeconds })) {
1871
- throw new EdgeAuthenticationError3("Invalid EDGE webhook signature");
1872
- }
1873
- let parsed;
1874
- try {
1875
- parsed = JSON.parse(body);
1876
- } catch {
1877
- throw new EdgeValidationError9("EDGE webhook body must be valid JSON", {
1878
- rawBody: ["Unable to parse JSON after signature verification"]
2308
+ // src/webhook-inbox.ts
2309
+ import { EdgeValidationError as EdgeValidationError12 } from "@edge-markets/connect";
2310
+ import crypto4 from "crypto";
2311
+ var DEFAULT_PROCESSING_TIMEOUT_MS = 10 * 60 * 1e3;
2312
+ var EdgeWebhookInbox = class {
2313
+ constructor(options) {
2314
+ if (!options.store?.insert || !options.store?.find || !options.store?.markProcessing) {
2315
+ throw new EdgeValidationError12("EdgeWebhookInbox store is incomplete", {
2316
+ store: ["Provide insert, find, markProcessing, markProcessed, and markFailed callbacks"]
2317
+ });
2318
+ }
2319
+ if (!options.store.markProcessed || !options.store.markFailed) {
2320
+ throw new EdgeValidationError12("EdgeWebhookInbox store is incomplete", {
2321
+ store: ["Provide markProcessed and markFailed callbacks"]
2322
+ });
2323
+ }
2324
+ if (!options.processEvent) {
2325
+ throw new EdgeValidationError12("EdgeWebhookInbox processEvent callback is required", {
2326
+ processEvent: ["Required"]
2327
+ });
2328
+ }
2329
+ if (options.processingTimeoutMs !== void 0 && (!Number.isFinite(options.processingTimeoutMs) || options.processingTimeoutMs <= 0)) {
2330
+ throw new EdgeValidationError12("processingTimeoutMs must be positive", {
2331
+ processingTimeoutMs: ["Must be greater than 0"]
2332
+ });
2333
+ }
2334
+ this.store = options.store;
2335
+ this.processEvent = options.processEvent;
2336
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2337
+ this.storeRawBody = options.storeRawBody ?? false;
2338
+ this.processingTimeoutMs = options.processingTimeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
2339
+ this.onEvent = options.onEvent;
2340
+ }
2341
+ async accept(parsed) {
2342
+ return this.acceptEvent(parsed.event, {
2343
+ source: "live_webhook",
2344
+ receivedAt: parsed.receivedAt,
2345
+ signatureTimestamp: parsed.signatureTimestamp,
2346
+ signatureHeader: void 0,
2347
+ rawBody: void 0
1879
2348
  });
1880
2349
  }
1881
- const event = validateEdgeWebhookEvent(parsed);
1882
- return {
1883
- event,
1884
- eventId: event.id,
1885
- eventType: event.type,
1886
- signatureTimestamp,
1887
- receivedAt: options.receivedAt ?? /* @__PURE__ */ new Date()
1888
- };
1889
- }
1890
- function extractWebhookSignatureTimestamp(header) {
1891
- const normalizedHeader = normalizeSignatureHeader(header);
1892
- if (!normalizedHeader) return void 0;
1893
- const part = normalizedHeader.split(",").map((value) => value.trim()).find((value) => value.startsWith("t="));
1894
- if (!part) return void 0;
1895
- const timestamp = Number(part.slice(2));
1896
- if (!Number.isInteger(timestamp) || timestamp <= 0) return void 0;
1897
- return timestamp;
1898
- }
1899
- function isEdgeWebhookEvent(value) {
1900
- try {
1901
- validateEdgeWebhookEvent(value);
1902
- return true;
1903
- } catch {
1904
- return false;
1905
- }
1906
- }
1907
- function validateEdgeWebhookEvent(value) {
1908
- const errors = {};
1909
- if (!isRecord(value)) {
1910
- throw new EdgeValidationError9("EDGE webhook event must be an object", {
1911
- event: ["Expected JSON object"]
2350
+ async acceptRaw(parsed, rawBody, signatureHeader) {
2351
+ return this.acceptEvent(parsed.event, {
2352
+ source: "live_webhook",
2353
+ receivedAt: parsed.receivedAt,
2354
+ signatureTimestamp: parsed.signatureTimestamp,
2355
+ signatureHeader,
2356
+ rawBody: this.storeRawBody ? rawBody : void 0
1912
2357
  });
1913
2358
  }
1914
- const id = value.id;
1915
- const type = value.type;
1916
- const createdAt = value.created_at;
1917
- const data = value.data;
1918
- if (typeof id !== "string" || !id.trim()) {
1919
- errors.id = ["Required string"];
1920
- }
1921
- if (typeof type !== "string" || !EVENT_TYPES.has(type)) {
1922
- errors.type = [`Must be one of: ${EDGE_WEBHOOK_EVENT_TYPES.join(", ")}`];
1923
- }
1924
- if (typeof createdAt !== "string" || !createdAt.trim() || Number.isNaN(Date.parse(createdAt))) {
1925
- errors.created_at = ["Required ISO 8601 timestamp string"];
1926
- }
1927
- if (!isRecord(data)) {
1928
- errors.data = ["Required object"];
2359
+ async acceptSyncedEvent(event, receivedAt = this.now()) {
2360
+ return this.acceptEvent(event, {
2361
+ source: "sync",
2362
+ receivedAt,
2363
+ signatureTimestamp: null,
2364
+ signatureHeader: null,
2365
+ rawBody: null
2366
+ });
1929
2367
  }
1930
- if (Object.keys(errors).length === 0) {
1931
- validateKnownEventData(type, data, errors);
2368
+ async process(eventId, options = {}) {
2369
+ const normalizedEventId = normalizeEventId(eventId);
2370
+ const existing = await this.store.find(normalizedEventId);
2371
+ if (!existing) {
2372
+ return { eventId: normalizedEventId, status: "failed", processed: false, skipped: true, reason: "not_found" };
2373
+ }
2374
+ if (existing.status === "processed" && !options.force) {
2375
+ return {
2376
+ eventId: normalizedEventId,
2377
+ status: existing.status,
2378
+ processed: false,
2379
+ skipped: true,
2380
+ reason: "already_processed",
2381
+ record: existing
2382
+ };
2383
+ }
2384
+ const now = this.now();
2385
+ const processing = await this.store.markProcessing(normalizedEventId, { now });
2386
+ if (!processing) {
2387
+ return {
2388
+ eventId: normalizedEventId,
2389
+ status: existing.status,
2390
+ processed: false,
2391
+ skipped: true,
2392
+ reason: "locked",
2393
+ record: existing
2394
+ };
2395
+ }
2396
+ await this.emit({
2397
+ type: "webhook.processing_started",
2398
+ eventId: normalizedEventId,
2399
+ eventType: processing.eventType,
2400
+ status: "processing",
2401
+ source: processing.source,
2402
+ attempts: processing.attempts
2403
+ });
2404
+ try {
2405
+ await this.processEvent(processing.event, processing);
2406
+ const processed = await this.store.markProcessed(normalizedEventId, { now: this.now() }) ?? processing;
2407
+ await this.emit({
2408
+ type: "webhook.processing_succeeded",
2409
+ eventId: normalizedEventId,
2410
+ eventType: processing.eventType,
2411
+ status: "processed",
2412
+ source: processing.source,
2413
+ attempts: processed.attempts
2414
+ });
2415
+ return {
2416
+ eventId: normalizedEventId,
2417
+ status: "processed",
2418
+ processed: true,
2419
+ skipped: false,
2420
+ record: processed
2421
+ };
2422
+ } catch (error) {
2423
+ const message = error instanceof Error ? error.message : "Unknown webhook processing error";
2424
+ const failed = await this.store.markFailed(normalizedEventId, { now: this.now(), error: message }) ?? processing;
2425
+ await this.emit({
2426
+ type: "webhook.processing_failed",
2427
+ eventId: normalizedEventId,
2428
+ eventType: processing.eventType,
2429
+ status: "failed",
2430
+ source: processing.source,
2431
+ attempts: failed.attempts,
2432
+ error: message
2433
+ });
2434
+ throw error;
2435
+ }
1932
2436
  }
1933
- if (Object.keys(errors).length > 0) {
1934
- throw new EdgeValidationError9("Invalid EDGE webhook event", errors);
2437
+ async replay(eventId) {
2438
+ return this.process(eventId, { force: true });
1935
2439
  }
1936
- return value;
1937
- }
1938
- function validateKnownEventData(type, data, errors) {
1939
- switch (type) {
1940
- case "transfer.completed":
1941
- validateTransferEventData(data, "completed", errors);
1942
- return;
1943
- case "transfer.failed":
1944
- validateTransferEventData(data, "failed", errors);
1945
- if (data.reason !== void 0 && typeof data.reason !== "string") {
1946
- errors["data.reason"] = ["Must be a string when provided"];
2440
+ async recoverStaleProcessing() {
2441
+ if (!this.store.list) {
2442
+ return { recovered: 0, skipped: true, reason: "list_not_configured" };
2443
+ }
2444
+ if (!this.store.markPending) {
2445
+ return { recovered: 0, skipped: true, reason: "mark_pending_not_configured" };
2446
+ }
2447
+ const updatedBefore = new Date(this.now().getTime() - this.processingTimeoutMs);
2448
+ const records = await this.store.list({ status: "processing", updatedBefore, limit: 100 });
2449
+ let recovered = 0;
2450
+ for (const record of records) {
2451
+ const next = await this.store.markPending(record.eventId, {
2452
+ now: this.now(),
2453
+ reason: "stale_processing_recovery"
2454
+ });
2455
+ if (next) {
2456
+ recovered++;
2457
+ await this.emit({
2458
+ type: "webhook.recovered_stale",
2459
+ eventId: record.eventId,
2460
+ eventType: record.eventType,
2461
+ status: next.status,
2462
+ source: record.source,
2463
+ attempts: next.attempts
2464
+ });
1947
2465
  }
1948
- return;
1949
- case "transfer.expired":
1950
- validateTransferEventData(data, "expired", errors);
1951
- return;
1952
- case "transfer.processing":
1953
- validateTransferEventData(data, "processing", errors);
1954
- return;
1955
- case "consent.revoked":
1956
- validateRequiredString(data, "userId", errors);
1957
- validateRequiredString(data, "clientId", errors);
1958
- validateRequiredIsoTimestamp(data, "revokedAt", errors);
1959
- return;
1960
- default: {
1961
- const exhaustive = type;
1962
- throw new EdgeValidationError9("Unsupported EDGE webhook event type", {
1963
- type: [`Unsupported event type: ${exhaustive}`]
2466
+ }
2467
+ return { recovered, skipped: false };
2468
+ }
2469
+ async acceptEvent(event, options) {
2470
+ const payloadFingerprint = fingerprintWebhookEvent(event);
2471
+ const result = await this.store.insert({
2472
+ eventId: event.id,
2473
+ eventType: event.type,
2474
+ event,
2475
+ payloadFingerprint,
2476
+ source: options.source,
2477
+ receivedAt: options.receivedAt,
2478
+ signatureTimestamp: options.signatureTimestamp ?? null,
2479
+ signatureHeader: options.signatureHeader ?? null,
2480
+ rawBody: options.rawBody ?? null
2481
+ });
2482
+ if (result.record.payloadFingerprint !== payloadFingerprint) {
2483
+ await this.emit({
2484
+ type: "webhook.payload_mismatch",
2485
+ eventId: event.id,
2486
+ eventType: event.type,
2487
+ status: result.record.status,
2488
+ source: options.source
2489
+ });
2490
+ throw new EdgeValidationError12("Duplicate webhook event payload does not match stored event", {
2491
+ eventId: [event.id]
1964
2492
  });
1965
2493
  }
2494
+ await this.emit({
2495
+ type: result.inserted ? "webhook.accepted" : "webhook.duplicate",
2496
+ eventId: event.id,
2497
+ eventType: event.type,
2498
+ status: result.record.status,
2499
+ source: options.source,
2500
+ attempts: result.record.attempts
2501
+ });
2502
+ return {
2503
+ eventId: event.id,
2504
+ eventType: event.type,
2505
+ inserted: result.inserted,
2506
+ duplicate: !result.inserted,
2507
+ status: result.record.status,
2508
+ record: result.record
2509
+ };
1966
2510
  }
1967
- }
1968
- function validateTransferEventData(data, status, errors) {
1969
- validateRequiredString(data, "transferId", errors);
1970
- if (data.status !== status) {
1971
- errors["data.status"] = [`Must be "${status}" for this event type`];
1972
- }
1973
- if (typeof data.type !== "string" || !TRANSFER_TYPES.has(data.type)) {
1974
- errors["data.type"] = ['Must be "debit" or "credit"'];
2511
+ async emit(event) {
2512
+ try {
2513
+ await this.onEvent?.(event);
2514
+ } catch {
2515
+ }
1975
2516
  }
1976
- validateRequiredString(data, "amount", errors);
2517
+ };
2518
+ function createWebhookInbox(options) {
2519
+ return new EdgeWebhookInbox(options);
1977
2520
  }
1978
- function validateRequiredString(data, field, errors) {
1979
- if (typeof data[field] !== "string" || !data[field].trim()) {
1980
- errors[`data.${field}`] = ["Required string"];
1981
- }
2521
+ function fingerprintWebhookEvent(event) {
2522
+ return crypto4.createHash("sha256").update(stableStringify(event)).digest("hex");
1982
2523
  }
1983
- function validateRequiredIsoTimestamp(data, field, errors) {
1984
- if (typeof data[field] !== "string" || !data[field].trim() || Number.isNaN(Date.parse(data[field]))) {
1985
- errors[`data.${field}`] = ["Required ISO 8601 timestamp string"];
2524
+ function normalizeEventId(eventId) {
2525
+ if (typeof eventId !== "string" || !eventId.trim()) {
2526
+ throw new EdgeValidationError12("Webhook event ID is required", {
2527
+ eventId: ["Required"]
2528
+ });
1986
2529
  }
2530
+ return eventId.trim();
1987
2531
  }
1988
- function normalizeRawBody(rawBody) {
1989
- if (typeof rawBody === "string") {
1990
- return rawBody;
2532
+ function stableStringify(value) {
2533
+ if (Array.isArray(value)) {
2534
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
1991
2535
  }
1992
- if (Buffer.isBuffer(rawBody) || rawBody instanceof Uint8Array) {
1993
- return Buffer.from(rawBody).toString("utf8");
1994
- }
1995
- throw new EdgeValidationError9("EDGE webhook raw body is required", {
1996
- rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
1997
- });
1998
- }
1999
- function normalizeSignatureHeader(header) {
2000
- if (Array.isArray(header)) {
2001
- return header.map((value) => value.trim()).filter(Boolean).join(",");
2536
+ if (value && typeof value === "object") {
2537
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
2002
2538
  }
2003
- if (typeof header !== "string") return void 0;
2004
- const trimmed = header.trim();
2005
- return trimmed || void 0;
2006
- }
2007
- function isRecord(value) {
2008
- return !!value && typeof value === "object" && !Array.isArray(value);
2539
+ return JSON.stringify(value);
2009
2540
  }
2010
2541
 
2011
2542
  // src/index.ts
@@ -2019,7 +2550,7 @@ import {
2019
2550
  EdgeNetworkError as EdgeNetworkError3,
2020
2551
  EdgeNotFoundError as EdgeNotFoundError2,
2021
2552
  EdgeTokenExchangeError as EdgeTokenExchangeError2,
2022
- EdgeValidationError as EdgeValidationError10,
2553
+ EdgeValidationError as EdgeValidationError13,
2023
2554
  isApiError,
2024
2555
  isAuthenticationError,
2025
2556
  isConsentRequiredError,
@@ -2034,6 +2565,7 @@ import {
2034
2565
  isProductionEnvironment
2035
2566
  } from "@edge-markets/connect";
2036
2567
  export {
2568
+ CONNECT_TRANSFER_DIRECTIONS,
2037
2569
  EDGE_WEBHOOK_EVENT_TYPES2 as EDGE_WEBHOOK_EVENT_TYPES,
2038
2570
  EdgeApiError2 as EdgeApiError,
2039
2571
  EdgeAuthenticationError4 as EdgeAuthenticationError,
@@ -2048,19 +2580,29 @@ export {
2048
2580
  EdgeTokenVault,
2049
2581
  EdgeUserClient,
2050
2582
  EdgeUserSession,
2051
- EdgeValidationError10 as EdgeValidationError,
2583
+ EdgeValidationError13 as EdgeValidationError,
2584
+ EdgeWebhookInbox,
2052
2585
  EdgeWebhookReconciler,
2053
2586
  TRANSFER_CATEGORIES,
2054
2587
  assertSameTransferIntent,
2055
2588
  assertTransferEventMatchesIntent,
2589
+ assertWebhookMatchesTransfer,
2056
2590
  buildVerifyIdentityPayload,
2057
2591
  createEdgeConnectServerFromEnv,
2058
2592
  createEdgeTokenVault,
2059
2593
  createEdgeUserSession,
2594
+ createSessionTokenRecord,
2060
2595
  createTransferIdempotencyKey,
2596
+ createWebhookInbox,
2061
2597
  createWebhookReconciler,
2598
+ encryptLegacySessionTokens,
2062
2599
  evaluateIdentityResult,
2063
2600
  extractWebhookSignatureTimestamp,
2601
+ fingerprintTransferIntent,
2602
+ fingerprintWebhookEvent,
2603
+ getConnectTransferDirection,
2604
+ getEdgeWebhookRawBody,
2605
+ getEdgeWebhookSignatureHeader,
2064
2606
  getEnvironmentConfig3 as getEnvironmentConfig,
2065
2607
  isApiError,
2066
2608
  isAuthenticationError,
@@ -2072,8 +2614,13 @@ export {
2072
2614
  isNetworkError,
2073
2615
  isProductionEnvironment,
2074
2616
  isTransferWebhookEvent,
2617
+ mapTransferStatusToPartnerState,
2618
+ moneyAmountsEqual,
2075
2619
  normalizeMoneyAmount,
2620
+ normalizeSessionExpiresAt,
2076
2621
  parseAndVerifyWebhook,
2622
+ parseSessionTokenRecord,
2623
+ parseWebhookHttpRequest,
2077
2624
  startTransferVerification,
2078
2625
  toEdgeHttpError,
2079
2626
  toEdgeProblemJson,