@edge-markets/connect-node 1.8.0 → 1.10.0

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