@edge-markets/connect-node 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,40 +30,241 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- EDGE_WEBHOOK_EVENT_TYPES: () => import_connect4.EDGE_WEBHOOK_EVENT_TYPES,
34
- EdgeApiError: () => import_connect3.EdgeApiError,
35
- EdgeAuthenticationError: () => import_connect3.EdgeAuthenticationError,
33
+ CONNECT_TRANSFER_DIRECTIONS: () => CONNECT_TRANSFER_DIRECTIONS,
34
+ EDGE_WEBHOOK_EVENT_TYPES: () => import_connect15.EDGE_WEBHOOK_EVENT_TYPES,
35
+ EdgeApiError: () => import_connect14.EdgeApiError,
36
+ EdgeAuthenticationError: () => import_connect14.EdgeAuthenticationError,
36
37
  EdgeConnectServer: () => EdgeConnectServer,
37
- EdgeConsentRequiredError: () => import_connect3.EdgeConsentRequiredError,
38
- EdgeError: () => import_connect3.EdgeError,
39
- EdgeIdentityVerificationError: () => import_connect3.EdgeIdentityVerificationError,
40
- EdgeInsufficientScopeError: () => import_connect3.EdgeInsufficientScopeError,
41
- EdgeNetworkError: () => import_connect3.EdgeNetworkError,
42
- EdgeNotFoundError: () => import_connect3.EdgeNotFoundError,
43
- EdgeTokenExchangeError: () => import_connect3.EdgeTokenExchangeError,
38
+ EdgeConsentRequiredError: () => import_connect14.EdgeConsentRequiredError,
39
+ EdgeError: () => import_connect14.EdgeError,
40
+ EdgeIdentityVerificationError: () => import_connect14.EdgeIdentityVerificationError,
41
+ EdgeInsufficientScopeError: () => import_connect14.EdgeInsufficientScopeError,
42
+ EdgeNetworkError: () => import_connect14.EdgeNetworkError,
43
+ EdgeNotFoundError: () => import_connect14.EdgeNotFoundError,
44
+ EdgeTokenExchangeError: () => import_connect14.EdgeTokenExchangeError,
45
+ EdgeTokenVault: () => EdgeTokenVault,
44
46
  EdgeUserClient: () => EdgeUserClient,
45
- TRANSFER_CATEGORIES: () => import_connect4.TRANSFER_CATEGORIES,
46
- getEnvironmentConfig: () => import_connect4.getEnvironmentConfig,
47
- isApiError: () => import_connect3.isApiError,
48
- isAuthenticationError: () => import_connect3.isAuthenticationError,
49
- isConsentRequiredError: () => import_connect3.isConsentRequiredError,
50
- isEdgeError: () => import_connect3.isEdgeError,
51
- isIdentityVerificationError: () => import_connect3.isIdentityVerificationError,
52
- isNetworkError: () => import_connect3.isNetworkError,
53
- isProductionEnvironment: () => import_connect4.isProductionEnvironment,
47
+ EdgeUserSession: () => EdgeUserSession,
48
+ EdgeValidationError: () => import_connect14.EdgeValidationError,
49
+ EdgeWebhookInbox: () => EdgeWebhookInbox,
50
+ EdgeWebhookReconciler: () => EdgeWebhookReconciler,
51
+ TRANSFER_CATEGORIES: () => import_connect15.TRANSFER_CATEGORIES,
52
+ assertSameTransferIntent: () => assertSameTransferIntent,
53
+ assertTransferEventMatchesIntent: () => assertTransferEventMatchesIntent,
54
+ assertWebhookMatchesTransfer: () => assertWebhookMatchesTransfer,
55
+ buildVerifyIdentityPayload: () => buildVerifyIdentityPayload,
56
+ createEdgeConnectServerFromEnv: () => createEdgeConnectServerFromEnv,
57
+ createEdgeTokenVault: () => createEdgeTokenVault,
58
+ createEdgeUserSession: () => createEdgeUserSession,
59
+ createSessionTokenRecord: () => createSessionTokenRecord,
60
+ createTransferIdempotencyKey: () => createTransferIdempotencyKey,
61
+ createWebhookInbox: () => createWebhookInbox,
62
+ createWebhookReconciler: () => createWebhookReconciler,
63
+ encryptLegacySessionTokens: () => encryptLegacySessionTokens,
64
+ evaluateIdentityResult: () => evaluateIdentityResult,
65
+ extractWebhookSignatureTimestamp: () => extractWebhookSignatureTimestamp,
66
+ fingerprintTransferIntent: () => fingerprintTransferIntent,
67
+ fingerprintWebhookEvent: () => fingerprintWebhookEvent,
68
+ getConnectTransferDirection: () => getConnectTransferDirection,
69
+ getEdgeWebhookRawBody: () => getEdgeWebhookRawBody,
70
+ getEdgeWebhookSignatureHeader: () => getEdgeWebhookSignatureHeader,
71
+ getEnvironmentConfig: () => import_connect15.getEnvironmentConfig,
72
+ isApiError: () => import_connect14.isApiError,
73
+ isAuthenticationError: () => import_connect14.isAuthenticationError,
74
+ isConsentRequiredError: () => import_connect14.isConsentRequiredError,
75
+ isEdgeError: () => import_connect14.isEdgeError,
76
+ isEdgeTokenVaultEnvelope: () => isEdgeTokenVaultEnvelope,
77
+ isEdgeWebhookEvent: () => isEdgeWebhookEvent,
78
+ isIdentityVerificationError: () => import_connect14.isIdentityVerificationError,
79
+ isNetworkError: () => import_connect14.isNetworkError,
80
+ isProductionEnvironment: () => import_connect15.isProductionEnvironment,
81
+ isTransferWebhookEvent: () => isTransferWebhookEvent,
82
+ mapTransferStatusToPartnerState: () => mapTransferStatusToPartnerState,
83
+ moneyAmountsEqual: () => moneyAmountsEqual,
84
+ normalizeMoneyAmount: () => normalizeMoneyAmount,
85
+ normalizeSessionExpiresAt: () => normalizeSessionExpiresAt,
86
+ parseAndVerifyWebhook: () => parseAndVerifyWebhook,
87
+ parseSessionTokenRecord: () => parseSessionTokenRecord,
88
+ parseWebhookHttpRequest: () => parseWebhookHttpRequest,
89
+ startTransferVerification: () => startTransferVerification,
90
+ toEdgeHttpError: () => toEdgeHttpError,
91
+ toEdgeProblemJson: () => toEdgeProblemJson,
92
+ validateEdgeWebhookEvent: () => validateEdgeWebhookEvent,
93
+ validateTransferAmount: () => validateTransferAmount,
54
94
  verifyWebhookSignature: () => verifyWebhookSignature
55
95
  });
56
96
  module.exports = __toCommonJS(index_exports);
57
97
 
58
98
  // src/edge-connect-server.ts
59
- var import_connect2 = require("@edge-markets/connect");
99
+ var import_connect4 = require("@edge-markets/connect");
60
100
 
61
- // src/validators.ts
101
+ // src/transfer-helpers.ts
62
102
  var import_connect = require("@edge-markets/connect");
103
+ var import_node_crypto = __toESM(require("crypto"));
104
+ var DEFAULT_MIN_AMOUNT = "1.00";
105
+ var DEFAULT_MAX_AMOUNT = "10000.00";
106
+ var CONNECT_TRANSFER_DIRECTIONS = {
107
+ debit: "user_to_partner",
108
+ credit: "partner_to_user"
109
+ };
110
+ function getConnectTransferDirection(type) {
111
+ if (type !== "debit" && type !== "credit") {
112
+ throw new import_connect.EdgeValidationError("Invalid Connect transfer type", {
113
+ type: ['Must be "debit" or "credit"']
114
+ });
115
+ }
116
+ return CONNECT_TRANSFER_DIRECTIONS[type];
117
+ }
118
+ function normalizeMoneyAmount(value, options = {}) {
119
+ const cents = parseMoneyToCents(value, "amount", options);
120
+ const dollars = cents / 100n;
121
+ const remainder = cents % 100n;
122
+ return `${dollars.toString()}.${remainder.toString().padStart(2, "0")}`;
123
+ }
124
+ function validateTransferAmount(value, limits = {}, options = {}) {
125
+ const amount = parseMoneyToCents(value, "amount", options);
126
+ const min = parseMoneyToCents(limits.min ?? DEFAULT_MIN_AMOUNT, "min", { allowNumber: true });
127
+ const max = parseMoneyToCents(limits.max ?? DEFAULT_MAX_AMOUNT, "max", { allowNumber: true });
128
+ const errors = {};
129
+ if (min <= 0n) errors.min = ["Minimum amount must be greater than 0"];
130
+ if (max < min) errors.max = ["Maximum amount must be greater than or equal to minimum amount"];
131
+ if (amount < min) errors.amount = [`Must be at least ${formatCents(min)}`];
132
+ if (amount > max) errors.amount = [`Must be at most ${formatCents(max)}`];
133
+ if (Object.keys(errors).length > 0) {
134
+ throw new import_connect.EdgeValidationError("Invalid transfer amount", errors);
135
+ }
136
+ return formatCents(amount);
137
+ }
138
+ function createTransferIdempotencyKey(options) {
139
+ const namespace = sanitizeKeyPart(options.namespace || "edge");
140
+ const partnerUserId = requireKeyPart(options.partnerUserId, "partnerUserId");
141
+ const externalId = options.externalId ? sanitizeKeyPart(options.externalId) : void 0;
142
+ const nonce = options.nonce ? sanitizeKeyPart(options.nonce) : void 0;
143
+ const amount = normalizeMoneyAmount(options.amount);
144
+ const digest = import_node_crypto.default.createHash("sha256").update(
145
+ JSON.stringify({
146
+ partnerUserId,
147
+ type: options.type,
148
+ amount,
149
+ category: options.category ?? null,
150
+ externalId: externalId ?? null,
151
+ nonce: nonce ?? null
152
+ })
153
+ ).digest("hex").slice(0, 32);
154
+ return [namespace, options.type, digest].join("_");
155
+ }
156
+ function assertSameTransferIntent(existing, requested) {
157
+ const errors = {};
158
+ if (existing.type !== requested.type) {
159
+ errors.type = [`Expected ${existing.type}, received ${requested.type}`];
160
+ }
161
+ const existingAmount = normalizeMoneyAmount(existing.amount);
162
+ const requestedAmount = normalizeMoneyAmount(requested.amount);
163
+ if (existingAmount !== requestedAmount) {
164
+ errors.amount = [`Expected ${existingAmount}, received ${requestedAmount}`];
165
+ }
166
+ const existingCategory = existing.category ?? void 0;
167
+ const requestedCategory = requested.category ?? void 0;
168
+ if (existingCategory !== requestedCategory) {
169
+ errors.category = [`Expected ${existingCategory || "unset"}, received ${requestedCategory || "unset"}`];
170
+ }
171
+ if (Object.keys(errors).length > 0) {
172
+ throw new import_connect.EdgeValidationError("Transfer intent does not match existing idempotent transfer", errors);
173
+ }
174
+ }
175
+ function fingerprintTransferIntent(intent) {
176
+ return import_node_crypto.default.createHash("sha256").update(
177
+ JSON.stringify({
178
+ type: intent.type,
179
+ amount: normalizeMoneyAmount(intent.amount),
180
+ category: intent.category ?? null
181
+ })
182
+ ).digest("hex");
183
+ }
184
+ function moneyAmountsEqual(left, right, options = {}) {
185
+ return normalizeMoneyAmount(left, options) === normalizeMoneyAmount(right, options);
186
+ }
187
+ function mapTransferStatusToPartnerState(status, mapping) {
188
+ const partnerState = mapping[status];
189
+ if (!partnerState) {
190
+ throw new import_connect.EdgeValidationError("Unsupported transfer status", {
191
+ status: [`Unsupported status: ${status}`]
192
+ });
193
+ }
194
+ return partnerState;
195
+ }
196
+ async function startTransferVerification(client, options) {
197
+ const origin = typeof options.origin === "string" ? options.origin.trim() : "";
198
+ if (!origin) {
199
+ throw new import_connect.EdgeValidationError("Verification origin is required", {
200
+ origin: ["Required"]
201
+ });
202
+ }
203
+ const transfer = await client.initiateTransfer({
204
+ ...options.transfer,
205
+ amount: normalizeMoneyAmount(options.transfer.amount)
206
+ });
207
+ const transferId = transfer.transferId;
208
+ if (!transferId) {
209
+ throw new import_connect.EdgeValidationError("Transfer response missing transferId", {
210
+ transferId: ["Required to create a verification session"]
211
+ });
212
+ }
213
+ const verificationSession = await client.createVerificationSession(transferId, { origin });
214
+ return { transfer, verificationSession };
215
+ }
216
+ function parseMoneyToCents(value, field, options = {}) {
217
+ let input;
218
+ if (typeof value === "number") {
219
+ if (options.allowNumber === false) {
220
+ throw new import_connect.EdgeValidationError("Money amount must be a string in strict mode", {
221
+ [field]: ['Pass a decimal string like "25.00"']
222
+ });
223
+ }
224
+ if (!Number.isFinite(value)) {
225
+ throw new import_connect.EdgeValidationError("Money amount must be finite", { [field]: ["Must be a finite number"] });
226
+ }
227
+ input = value.toFixed(2);
228
+ } else if (typeof value === "string") {
229
+ input = value.trim();
230
+ } else {
231
+ throw new import_connect.EdgeValidationError("Money amount must be a string or number", { [field]: ["Invalid type"] });
232
+ }
233
+ if (!/^\d+(\.\d{0,2})?$/.test(input)) {
234
+ throw new import_connect.EdgeValidationError("Money amount must be a positive decimal with at most 2 places", {
235
+ [field]: ['Use a decimal string like "25.00"']
236
+ });
237
+ }
238
+ const [dollars, cents = ""] = input.split(".");
239
+ const normalizedDollars = dollars.replace(/^0+(?=\d)/, "") || "0";
240
+ const amount = BigInt(normalizedDollars) * 100n + BigInt(cents.padEnd(2, "0"));
241
+ if (amount <= 0n) {
242
+ throw new import_connect.EdgeValidationError("Money amount must be greater than 0", { [field]: ["Must be greater than 0"] });
243
+ }
244
+ return amount;
245
+ }
246
+ function formatCents(cents) {
247
+ return `${(cents / 100n).toString()}.${(cents % 100n).toString().padStart(2, "0")}`;
248
+ }
249
+ function requireKeyPart(value, field) {
250
+ const sanitized = sanitizeKeyPart(value);
251
+ if (!sanitized) {
252
+ throw new import_connect.EdgeValidationError("Idempotency key input is incomplete", {
253
+ [field]: ["Required"]
254
+ });
255
+ }
256
+ return sanitized;
257
+ }
258
+ function sanitizeKeyPart(value) {
259
+ return value.trim().replace(/[^a-zA-Z0-9_.:-]/g, "_").slice(0, 64);
260
+ }
261
+
262
+ // src/validators.ts
263
+ var import_connect2 = require("@edge-markets/connect");
63
264
  var UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
64
265
  function validateTransferId(transferId) {
65
266
  if (typeof transferId !== "string" || !UUID_V4_REGEX.test(transferId)) {
66
- throw new import_connect.EdgeValidationError("transferId must be a UUID v4", {
267
+ throw new import_connect2.EdgeValidationError("transferId must be a UUID v4", {
67
268
  transferId: ["Must be a valid UUID v4"]
68
269
  });
69
270
  }
@@ -83,7 +284,7 @@ function validateVerifyIdentityOptions(options) {
83
284
  }
84
285
  if (Object.keys(errors).length > 0) {
85
286
  const firstMessage = Object.values(errors)[0][0];
86
- throw new import_connect.EdgeValidationError(firstMessage, errors);
287
+ throw new import_connect2.EdgeValidationError(firstMessage, errors);
87
288
  }
88
289
  }
89
290
  function validateTransferOptions(options) {
@@ -112,7 +313,7 @@ function validateTransferOptions(options) {
112
313
  errors.category = ["Must be one of: sportsbook, casino, dfs, sweepstakes"];
113
314
  }
114
315
  if (Object.keys(errors).length > 0) {
115
- throw new import_connect.EdgeValidationError("Invalid transfer options", errors);
316
+ throw new import_connect2.EdgeValidationError("Invalid transfer options", errors);
116
317
  }
117
318
  }
118
319
 
@@ -144,6 +345,9 @@ var EdgeUserClient = class {
144
345
  }
145
346
  return this.server._apiRequest("POST", "/transfer", this.accessToken, body);
146
347
  }
348
+ async startTransferVerification(options) {
349
+ return startTransferVerification(this, options);
350
+ }
147
351
  async getTransfer(transferId) {
148
352
  validateTransferId(transferId);
149
353
  return this.server._apiRequest("GET", `/transfer/${encodeURIComponent(transferId)}`, this.accessToken);
@@ -264,16 +468,46 @@ function fromBase64Url(value) {
264
468
  }
265
469
 
266
470
  // src/mtls.ts
471
+ var import_node_fs = __toESM(require("fs"));
267
472
  var import_node_https = __toESM(require("https"));
473
+ var import_node_tls = __toESM(require("tls"));
474
+ var CERT_PATTERN = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g;
475
+ function normalizeCaBundle(ca) {
476
+ if (!ca) return [];
477
+ const values = Array.isArray(ca) ? ca : [ca];
478
+ return values.flatMap((value) => {
479
+ const normalized = value.replace(/\\n/g, "\n").trim();
480
+ if (!normalized) return [];
481
+ const certificates = normalized.match(CERT_PATTERN);
482
+ return certificates ? certificates.map((certificate) => certificate.trim()) : [normalized];
483
+ });
484
+ }
485
+ function getNodeExtraCaCerts() {
486
+ const extraCaPath = process.env.NODE_EXTRA_CA_CERTS?.trim();
487
+ if (!extraCaPath) return [];
488
+ try {
489
+ return normalizeCaBundle(import_node_fs.default.readFileSync(extraCaPath, "utf8"));
490
+ } catch (error) {
491
+ const message = error instanceof Error ? error.message : String(error);
492
+ throw new Error(`EdgeConnectServer: failed to read NODE_EXTRA_CA_CERTS from "${extraCaPath}": ${message}`);
493
+ }
494
+ }
495
+ function buildServerCaBundle(ca) {
496
+ const customCa = normalizeCaBundle(ca);
497
+ if (customCa.length === 0) return void 0;
498
+ return Array.from(/* @__PURE__ */ new Set([...import_node_tls.default.rootCertificates, ...getNodeExtraCaCerts(), ...customCa]));
499
+ }
268
500
  function createHttpsAgent(config) {
501
+ const ca = buildServerCaBundle(config.ca);
269
502
  return new import_node_https.default.Agent({
270
503
  cert: config.cert,
271
504
  key: config.key,
272
505
  rejectUnauthorized: true,
273
- ...config.ca ? { ca: config.ca } : {}
506
+ ...ca ? { ca } : {}
274
507
  });
275
508
  }
276
509
  function createUndiciDispatcher(config) {
510
+ const ca = buildServerCaBundle(config.ca);
277
511
  const moduleName = "undici";
278
512
  const { Agent } = require(moduleName);
279
513
  return new Agent({
@@ -281,7 +515,7 @@ function createUndiciDispatcher(config) {
281
515
  cert: config.cert,
282
516
  key: config.key,
283
517
  rejectUnauthorized: true,
284
- ...config.ca ? { ca: config.ca } : {}
518
+ ...ca ? { ca } : {}
285
519
  }
286
520
  });
287
521
  }
@@ -294,6 +528,177 @@ function validateMtlsConfig(config) {
294
528
  }
295
529
  }
296
530
 
531
+ // src/session.ts
532
+ var import_connect3 = require("@edge-markets/connect");
533
+ var EdgeUserSession = class {
534
+ constructor(options) {
535
+ this.refreshPromise = null;
536
+ if (!options.edge?.forUser || !options.edge?.refreshTokens) {
537
+ throw new import_connect3.EdgeValidationError("EdgeUserSession requires an EdgeConnectServer instance", {
538
+ edge: ["Provide edge.forUser and edge.refreshTokens"]
539
+ });
540
+ }
541
+ const subjectId = typeof options.subjectId === "string" ? options.subjectId.trim() : "";
542
+ if (!subjectId) {
543
+ throw new import_connect3.EdgeValidationError("EdgeUserSession subjectId is required", {
544
+ subjectId: ["Required"]
545
+ });
546
+ }
547
+ if (!options.tokenStore?.load || !options.tokenStore?.save) {
548
+ throw new import_connect3.EdgeValidationError("EdgeUserSession tokenStore is required", {
549
+ tokenStore: ["Provide load(subjectId) and save(subjectId, value, context)"]
550
+ });
551
+ }
552
+ if (options.refreshSkewMs !== void 0 && (!Number.isFinite(options.refreshSkewMs) || options.refreshSkewMs < 0)) {
553
+ throw new import_connect3.EdgeValidationError("refreshSkewMs must be a non-negative number", {
554
+ refreshSkewMs: ["Must be greater than or equal to 0"]
555
+ });
556
+ }
557
+ this.edge = options.edge;
558
+ this.subjectId = subjectId;
559
+ this.tokenStore = options.tokenStore;
560
+ this.tokenVault = options.tokenVault;
561
+ this.refreshSkewMs = options.refreshSkewMs ?? 6e4;
562
+ this.now = options.now ?? Date.now;
563
+ this.onRefresh = options.onRefresh;
564
+ this.onEvent = options.onEvent;
565
+ this.singleFlightRefresh = options.singleFlightRefresh ?? true;
566
+ }
567
+ async getValidTokens(options = {}) {
568
+ const tokens = await this.loadTokens();
569
+ if (!options.forceRefresh && tokens.expiresAt > this.now() + this.refreshSkewMs) {
570
+ return tokens;
571
+ }
572
+ if (this.singleFlightRefresh) {
573
+ if (!this.refreshPromise) {
574
+ this.refreshPromise = this.refreshTokens(tokens).finally(() => {
575
+ this.refreshPromise = null;
576
+ });
577
+ }
578
+ return this.refreshPromise;
579
+ }
580
+ return this.refreshTokens(tokens);
581
+ }
582
+ async getClient(options = {}) {
583
+ const tokens = await this.getValidTokens(options);
584
+ return this.edge.forUser(tokens.accessToken);
585
+ }
586
+ async withClient(callback, options = {}) {
587
+ const tokens = await this.getValidTokens(options);
588
+ return callback(this.edge.forUser(tokens.accessToken), tokens);
589
+ }
590
+ async saveTokens(tokens, refreshed = false) {
591
+ validateTokens(tokens);
592
+ const value = this.tokenVault ? this.tokenVault.encryptTokens(tokens) : tokens;
593
+ await this.tokenStore.save(this.subjectId, value, { tokens, refreshed });
594
+ await this.emit({
595
+ type: "session.saved",
596
+ subjectId: this.subjectId,
597
+ refreshed,
598
+ encrypted: typeof value === "string",
599
+ expiresAt: tokens.expiresAt,
600
+ scope: tokens.scope
601
+ });
602
+ }
603
+ async loadTokens() {
604
+ const value = await this.tokenStore.load(this.subjectId);
605
+ if (!value) {
606
+ throw new import_connect3.EdgeAuthenticationError("No EDGE tokens are stored for this subject", {
607
+ subjectId: this.subjectId
608
+ });
609
+ }
610
+ if (typeof value === "string") {
611
+ if (!this.tokenVault) {
612
+ throw new import_connect3.EdgeAuthenticationError("Encrypted EDGE token envelope loaded but no tokenVault was configured", {
613
+ subjectId: this.subjectId
614
+ });
615
+ }
616
+ const tokens = this.tokenVault.decryptTokens(value);
617
+ validateTokens(tokens);
618
+ await this.emitLoaded(tokens, true);
619
+ return tokens;
620
+ }
621
+ validateTokens(value);
622
+ await this.emitLoaded(value, false);
623
+ return value;
624
+ }
625
+ async refreshTokens(currentTokens) {
626
+ await this.emit({
627
+ type: "session.refresh_started",
628
+ subjectId: this.subjectId,
629
+ expiresAt: currentTokens.expiresAt,
630
+ scope: currentTokens.scope
631
+ });
632
+ try {
633
+ const refreshed = await this.edge.refreshTokens(currentTokens.refreshToken);
634
+ const tokens = {
635
+ ...refreshed,
636
+ refreshToken: refreshed.refreshToken || currentTokens.refreshToken
637
+ };
638
+ await this.saveTokens(tokens, true);
639
+ await this.onRefresh?.(tokens);
640
+ await this.emit({
641
+ type: "session.refresh_succeeded",
642
+ subjectId: this.subjectId,
643
+ refreshed: true,
644
+ expiresAt: tokens.expiresAt,
645
+ scope: tokens.scope
646
+ });
647
+ return tokens;
648
+ } catch (error) {
649
+ await this.emit({
650
+ type: "session.refresh_failed",
651
+ subjectId: this.subjectId,
652
+ error: error instanceof Error ? error.message : "Unknown token refresh error"
653
+ });
654
+ throw error;
655
+ }
656
+ }
657
+ async emitLoaded(tokens, encrypted) {
658
+ await this.emit({
659
+ type: "session.loaded",
660
+ subjectId: this.subjectId,
661
+ encrypted,
662
+ expiresAt: tokens.expiresAt,
663
+ scope: tokens.scope
664
+ });
665
+ }
666
+ async emit(event) {
667
+ try {
668
+ await this.onEvent?.(event);
669
+ } catch {
670
+ }
671
+ }
672
+ };
673
+ function createEdgeUserSession(options) {
674
+ return new EdgeUserSession(options);
675
+ }
676
+ function validateTokens(tokens) {
677
+ const errors = {};
678
+ if (!tokens || typeof tokens !== "object") {
679
+ errors.tokens = ["Expected token object"];
680
+ } else {
681
+ if (typeof tokens.accessToken !== "string" || !tokens.accessToken.trim()) {
682
+ errors.accessToken = ["Required"];
683
+ }
684
+ if (typeof tokens.refreshToken !== "string" || !tokens.refreshToken.trim()) {
685
+ errors.refreshToken = ["Required"];
686
+ }
687
+ if (typeof tokens.expiresAt !== "number" || !Number.isFinite(tokens.expiresAt) || tokens.expiresAt <= 0) {
688
+ errors.expiresAt = ["Must be a positive Unix timestamp in milliseconds"];
689
+ }
690
+ if (typeof tokens.expiresIn !== "number" || !Number.isFinite(tokens.expiresIn) || tokens.expiresIn <= 0) {
691
+ errors.expiresIn = ["Must be a positive number"];
692
+ }
693
+ if (typeof tokens.scope !== "string") {
694
+ errors.scope = ["Required"];
695
+ }
696
+ }
697
+ if (Object.keys(errors).length > 0) {
698
+ throw new import_connect3.EdgeValidationError("Invalid EDGE token payload", errors);
699
+ }
700
+ }
701
+
297
702
  // src/types.ts
298
703
  var DEFAULT_TIMEOUT = 3e4;
299
704
  var USER_AGENT = "@edge-markets/connect-node/1.0.0";
@@ -304,12 +709,126 @@ var DEFAULT_RETRY_CONFIG = {
304
709
  baseDelayMs: 1e3
305
710
  };
306
711
 
712
+ // src/webhook-reconciler.ts
713
+ var DEFAULT_MAX_PAGES = 10;
714
+ var EdgeWebhookReconciler = class {
715
+ constructor(options) {
716
+ this.running = false;
717
+ if (!options.edge?.syncWebhookEvents) {
718
+ throw new Error("EdgeWebhookReconciler: edge.syncWebhookEvents is required");
719
+ }
720
+ if (!options.cursorStore?.load || !options.cursorStore?.save) {
721
+ throw new Error("EdgeWebhookReconciler: cursorStore.load and cursorStore.save are required");
722
+ }
723
+ if (!options.processEvent && !options.inbox) {
724
+ throw new Error("EdgeWebhookReconciler: processEvent or inbox is required");
725
+ }
726
+ if (options.maxPages !== void 0 && (!Number.isInteger(options.maxPages) || options.maxPages <= 0)) {
727
+ throw new Error("EdgeWebhookReconciler: maxPages must be a positive integer");
728
+ }
729
+ this.options = options;
730
+ }
731
+ async run() {
732
+ if (this.running) {
733
+ return {
734
+ processed: 0,
735
+ pages: 0,
736
+ hasMore: false,
737
+ skipped: true,
738
+ reason: "already_running"
739
+ };
740
+ }
741
+ this.running = true;
742
+ try {
743
+ return await this.runInternal();
744
+ } finally {
745
+ this.running = false;
746
+ }
747
+ }
748
+ async runInternal() {
749
+ let cursor = normalizeCursor(await this.options.cursorStore.load());
750
+ let processed = 0;
751
+ let accepted = 0;
752
+ let duplicates = 0;
753
+ let pages = 0;
754
+ let hasMore = false;
755
+ const maxPages = this.options.maxPages ?? DEFAULT_MAX_PAGES;
756
+ while (pages < maxPages) {
757
+ const result = await this.options.edge.syncWebhookEvents({
758
+ ...cursor ? { afterEventId: cursor } : {},
759
+ ...this.options.limit !== void 0 ? { limit: this.options.limit } : {}
760
+ });
761
+ pages++;
762
+ hasMore = result.hasMore;
763
+ if (result.events.length === 0) {
764
+ return {
765
+ processed,
766
+ ...this.options.inbox ? { accepted, duplicates } : {},
767
+ pages,
768
+ ...cursor ? { cursor } : {},
769
+ hasMore,
770
+ skipped: false,
771
+ ...hasMore ? { reason: "empty_page_with_has_more" } : {}
772
+ };
773
+ }
774
+ for (const event of result.events) {
775
+ if (this.options.inbox) {
776
+ const acceptResult = await this.options.inbox.acceptSyncedEvent(event);
777
+ accepted++;
778
+ if (acceptResult.duplicate) duplicates++;
779
+ const processResult = await this.options.inbox.process(event.id);
780
+ if (processResult.processed) processed++;
781
+ } else {
782
+ await this.options.processEvent?.(event);
783
+ processed++;
784
+ }
785
+ cursor = event.id;
786
+ await this.options.cursorStore.save(cursor);
787
+ }
788
+ if (!result.hasMore) {
789
+ return {
790
+ processed,
791
+ ...this.options.inbox ? { accepted, duplicates } : {},
792
+ pages,
793
+ ...cursor ? { cursor } : {},
794
+ hasMore: false,
795
+ skipped: false
796
+ };
797
+ }
798
+ }
799
+ return {
800
+ processed,
801
+ ...this.options.inbox ? { accepted, duplicates } : {},
802
+ pages,
803
+ ...cursor ? { cursor } : {},
804
+ hasMore,
805
+ skipped: false,
806
+ ...hasMore ? { reason: "max_pages_reached" } : {}
807
+ };
808
+ }
809
+ };
810
+ function createWebhookReconciler(options) {
811
+ return new EdgeWebhookReconciler(options);
812
+ }
813
+ function normalizeCursor(cursor) {
814
+ if (typeof cursor !== "string") return void 0;
815
+ const trimmed = cursor.trim();
816
+ return trimmed || void 0;
817
+ }
818
+
307
819
  // src/edge-connect-server.ts
308
820
  var instances = /* @__PURE__ */ new Map();
309
821
  function getInstanceKey(config) {
310
- return `${config.clientId}:${config.environment}`;
822
+ return JSON.stringify([
823
+ config.clientId,
824
+ config.environment,
825
+ config.apiBaseUrl || "",
826
+ config.oauthBaseUrl || "",
827
+ config.partnerApiBaseUrl || "",
828
+ config.partnerClientId || ""
829
+ ]);
311
830
  }
312
- var MTLS_ENFORCED_HOSTS = /* @__PURE__ */ new Set(["connect-staging.edgeboost.io", "api.edgeboost.com"]);
831
+ var MTLS_ENFORCED_HOSTS = /* @__PURE__ */ new Set(["connect-staging.edgeboost.io", "connect.edgeboost.io"]);
313
832
  function isMtlsEnforcedHost(host) {
314
833
  if (MTLS_ENFORCED_HOSTS.has(host)) {
315
834
  return true;
@@ -331,6 +850,29 @@ function environmentRequiresMtls(environment, apiBaseUrl) {
331
850
  return false;
332
851
  }
333
852
  }
853
+ var CONNECT_API_BASE_PATH = "/connect/v1";
854
+ function stripTrailingSlash(value) {
855
+ let end = value.length;
856
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
857
+ end--;
858
+ }
859
+ return value.slice(0, end);
860
+ }
861
+ function derivePartnerApiBaseUrl(apiBaseUrl) {
862
+ const url = new URL(apiBaseUrl);
863
+ const normalizedPath = stripTrailingSlash(url.pathname);
864
+ if (normalizedPath.endsWith(CONNECT_API_BASE_PATH)) {
865
+ const prefix = normalizedPath.slice(0, -CONNECT_API_BASE_PATH.length);
866
+ url.pathname = `${prefix}/v1`;
867
+ } else {
868
+ url.pathname = normalizedPath || "/";
869
+ }
870
+ url.search = "";
871
+ url.hash = "";
872
+ return stripTrailingSlash(url.toString());
873
+ }
874
+ var SYNC_WEBHOOK_EVENTS_MIN_LIMIT = 1;
875
+ var SYNC_WEBHOOK_EVENTS_MAX_LIMIT = 100;
334
876
  var EdgeConnectServer = class _EdgeConnectServer {
335
877
  constructor(config) {
336
878
  this.partnerTokenCache = null;
@@ -344,8 +886,9 @@ var EdgeConnectServer = class _EdgeConnectServer {
344
886
  throw new Error("EdgeConnectServer: environment is required");
345
887
  }
346
888
  this.config = config;
347
- const envConfig = (0, import_connect2.getEnvironmentConfig)(config.environment);
889
+ const envConfig = (0, import_connect4.getEnvironmentConfig)(config.environment);
348
890
  this.apiBaseUrl = config.apiBaseUrl || envConfig.apiBaseUrl;
891
+ this.partnerApiBaseUrl = stripTrailingSlash(config.partnerApiBaseUrl || derivePartnerApiBaseUrl(this.apiBaseUrl));
349
892
  this.oauthBaseUrl = config.oauthBaseUrl || envConfig.oauthBaseUrl;
350
893
  this.timeout = config.timeout || DEFAULT_TIMEOUT;
351
894
  this.retryConfig = {
@@ -388,10 +931,13 @@ var EdgeConnectServer = class _EdgeConnectServer {
388
931
  */
389
932
  forUser(accessToken) {
390
933
  if (!accessToken) {
391
- throw new import_connect2.EdgeAuthenticationError("accessToken is required when calling forUser()");
934
+ throw new import_connect4.EdgeAuthenticationError("accessToken is required when calling forUser()");
392
935
  }
393
936
  return new EdgeUserClient(this, accessToken);
394
937
  }
938
+ createUserSession(options) {
939
+ return new EdgeUserSession({ ...options, edge: this });
940
+ }
395
941
  async exchangeCode(code, codeVerifier, redirectUri) {
396
942
  const tokenUrl = `${this.oauthBaseUrl}/token`;
397
943
  const body = {
@@ -422,8 +968,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
422
968
  const data = await response.json();
423
969
  return this.parseTokenResponse(data);
424
970
  } catch (error) {
425
- if (error instanceof import_connect2.EdgeError) throw error;
426
- throw new import_connect2.EdgeNetworkError("Failed to exchange code", error);
971
+ if (error instanceof import_connect4.EdgeError) throw error;
972
+ throw new import_connect4.EdgeNetworkError("Failed to exchange code", error);
427
973
  }
428
974
  }
429
975
  async refreshTokens(refreshToken) {
@@ -449,7 +995,7 @@ var EdgeConnectServer = class _EdgeConnectServer {
449
995
  );
450
996
  if (!response.ok) {
451
997
  const error = await response.json().catch(() => ({}));
452
- throw new import_connect2.EdgeAuthenticationError(
998
+ throw new import_connect4.EdgeAuthenticationError(
453
999
  error.message || error.error_description || "Token refresh failed. User may need to reconnect.",
454
1000
  { tokenError: error }
455
1001
  );
@@ -457,8 +1003,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
457
1003
  const data = await response.json();
458
1004
  return this.parseTokenResponse(data, refreshToken);
459
1005
  } catch (error) {
460
- if (error instanceof import_connect2.EdgeError) throw error;
461
- throw new import_connect2.EdgeNetworkError("Failed to refresh tokens", error);
1006
+ if (error instanceof import_connect4.EdgeError) throw error;
1007
+ throw new import_connect4.EdgeNetworkError("Failed to refresh tokens", error);
462
1008
  }
463
1009
  }
464
1010
  /** @internal Called by {@link EdgeUserClient} — not part of the public API. */
@@ -500,14 +1046,14 @@ var EdgeConnectServer = class _EdgeConnectServer {
500
1046
  if (mleEnabled && typeof rawResponseBody?.jwe === "string") {
501
1047
  responseBody = decryptMle(rawResponseBody.jwe, mleConfig);
502
1048
  } else if (!mleEnabled && typeof rawResponseBody?.jwe === "string") {
503
- throw new import_connect2.EdgeApiError(
1049
+ throw new import_connect4.EdgeApiError(
504
1050
  "mle_required",
505
1051
  "The API responded with message-level encryption. Enable MLE in SDK config.",
506
1052
  response.status,
507
1053
  rawResponseBody
508
1054
  );
509
1055
  } else if (mleEnabled && mleConfig?.strictResponseEncryption !== false && response.ok && typeof rawResponseBody?.jwe !== "string") {
510
- throw new import_connect2.EdgeApiError(
1056
+ throw new import_connect4.EdgeApiError(
511
1057
  "mle_response_missing",
512
1058
  "Expected encrypted response payload but received plaintext.",
513
1059
  response.status,
@@ -525,8 +1071,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
525
1071
  }
526
1072
  return responseBody;
527
1073
  } catch (error) {
528
- if (error instanceof import_connect2.EdgeError) {
529
- if (!(error instanceof import_connect2.EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
1074
+ if (error instanceof import_connect4.EdgeError) {
1075
+ if (!(error instanceof import_connect4.EdgeNetworkError) || attempt >= this.retryConfig.maxRetries) {
530
1076
  throw error;
531
1077
  }
532
1078
  lastError = error;
@@ -534,11 +1080,11 @@ var EdgeConnectServer = class _EdgeConnectServer {
534
1080
  }
535
1081
  lastError = error;
536
1082
  if (attempt >= this.retryConfig.maxRetries) {
537
- throw new import_connect2.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
1083
+ throw new import_connect4.EdgeNetworkError(`API request failed: ${method} ${path}`, lastError);
538
1084
  }
539
1085
  }
540
1086
  }
541
- throw new import_connect2.EdgeNetworkError(
1087
+ throw new import_connect4.EdgeNetworkError(
542
1088
  `API request failed after ${this.retryConfig.maxRetries} retries: ${method} ${path}`,
543
1089
  lastError
544
1090
  );
@@ -582,21 +1128,41 @@ var EdgeConnectServer = class _EdgeConnectServer {
582
1128
  */
583
1129
  async syncWebhookEvents(options = {}) {
584
1130
  if (!this.config.partnerClientId) {
585
- throw new import_connect2.EdgeAuthenticationError("syncWebhookEvents requires `partnerClientId` in EdgeConnectServer config");
1131
+ throw new import_connect4.EdgeAuthenticationError("syncWebhookEvents requires `partnerClientId` in EdgeConnectServer config");
586
1132
  }
587
1133
  if (!this.config.partnerClientSecret) {
588
- throw new import_connect2.EdgeAuthenticationError("syncWebhookEvents requires `partnerClientSecret` in EdgeConnectServer config");
1134
+ throw new import_connect4.EdgeAuthenticationError("syncWebhookEvents requires `partnerClientSecret` in EdgeConnectServer config");
589
1135
  }
590
- const url = this.buildSyncUrl(options);
1136
+ const normalizedOptions = this.normalizeSyncWebhookEventsOptions(options);
1137
+ const url = this.buildSyncUrl(normalizedOptions);
591
1138
  let token = await this.getPartnerToken();
592
- let response = await this.partnerFetch(url, token);
1139
+ let { response, durationMs, attempt } = await this.partnerFetch(url, token);
593
1140
  if (response.status === 401) {
1141
+ await this.drainResponseBody(response);
1142
+ this.config.onResponse?.({
1143
+ status: response.status,
1144
+ body: { error: "unauthorized" },
1145
+ durationMs,
1146
+ method: "GET",
1147
+ url,
1148
+ endpoint: "partner_webhook_sync",
1149
+ attempt
1150
+ });
594
1151
  this.partnerTokenCache = null;
595
1152
  token = await this.getPartnerToken();
596
- response = await this.partnerFetch(url, token);
1153
+ ({ response, durationMs, attempt } = await this.partnerFetch(url, token));
597
1154
  if (response.status === 401) {
598
1155
  const body = await response.json().catch(() => ({}));
599
- throw new import_connect2.EdgeAuthenticationError(
1156
+ this.config.onResponse?.({
1157
+ status: response.status,
1158
+ body: this.redactPartnerSyncResponseBody(body),
1159
+ durationMs,
1160
+ method: "GET",
1161
+ url,
1162
+ endpoint: "partner_webhook_sync",
1163
+ attempt
1164
+ });
1165
+ throw new import_connect4.EdgeAuthenticationError(
600
1166
  body.message || "Partner token rejected after refresh \u2014 check partnerClientId/Secret",
601
1167
  body
602
1168
  );
@@ -604,7 +1170,16 @@ var EdgeConnectServer = class _EdgeConnectServer {
604
1170
  }
605
1171
  if (!response.ok) {
606
1172
  const body = await response.json().catch(() => ({}));
607
- throw new import_connect2.EdgeApiError(
1173
+ this.config.onResponse?.({
1174
+ status: response.status,
1175
+ body: this.redactPartnerSyncResponseBody(body),
1176
+ durationMs,
1177
+ method: "GET",
1178
+ url,
1179
+ endpoint: "partner_webhook_sync",
1180
+ attempt
1181
+ });
1182
+ throw new import_connect4.EdgeApiError(
608
1183
  body.error || "sync_failed",
609
1184
  body.message || `syncWebhookEvents failed with status ${response.status}`,
610
1185
  response.status,
@@ -612,17 +1187,57 @@ var EdgeConnectServer = class _EdgeConnectServer {
612
1187
  );
613
1188
  }
614
1189
  const wire = await response.json().catch(() => ({}));
1190
+ this.config.onResponse?.({
1191
+ status: response.status,
1192
+ body: {
1193
+ eventCount: wire.events?.length ?? 0,
1194
+ hasMore: !!wire.has_more
1195
+ },
1196
+ durationMs,
1197
+ method: "GET",
1198
+ url,
1199
+ endpoint: "partner_webhook_sync",
1200
+ attempt
1201
+ });
615
1202
  const events = (wire.events ?? []).map((entry) => entry.payload).filter((p) => !!p);
616
1203
  return {
617
1204
  events,
618
1205
  hasMore: !!wire.has_more
619
1206
  };
620
1207
  }
1208
+ /**
1209
+ * Creates a cursor-based webhook reconciler bound to this SDK instance.
1210
+ *
1211
+ * Use this from cron jobs to process events returned by
1212
+ * {@link EdgeConnectServer.syncWebhookEvents} without hand-writing
1213
+ * pagination, cursor advancement, or process-level concurrency guards.
1214
+ */
1215
+ createWebhookReconciler(options) {
1216
+ return new EdgeWebhookReconciler({ ...options, edge: this });
1217
+ }
621
1218
  /**
622
1219
  * Builds the sync URL with normalized query parameters.
623
1220
  */
1221
+ normalizeSyncWebhookEventsOptions(options) {
1222
+ const normalized = {};
1223
+ if (typeof options.afterEventId === "string" && options.afterEventId.trim()) {
1224
+ normalized.afterEventId = options.afterEventId.trim();
1225
+ }
1226
+ if (options.limit !== void 0) {
1227
+ if (!Number.isInteger(options.limit) || options.limit < SYNC_WEBHOOK_EVENTS_MIN_LIMIT || options.limit > SYNC_WEBHOOK_EVENTS_MAX_LIMIT) {
1228
+ throw new import_connect4.EdgeApiError(
1229
+ "invalid_sync_options",
1230
+ `syncWebhookEvents limit must be an integer between ${SYNC_WEBHOOK_EVENTS_MIN_LIMIT} and ${SYNC_WEBHOOK_EVENTS_MAX_LIMIT}`,
1231
+ 400,
1232
+ { limit: options.limit }
1233
+ );
1234
+ }
1235
+ normalized.limit = options.limit;
1236
+ }
1237
+ return normalized;
1238
+ }
624
1239
  buildSyncUrl(options) {
625
- const url = new URL(`${this.apiBaseUrl}/partner/webhooks/events/sync`);
1240
+ const url = new URL(`${this.partnerApiBaseUrl}/partner/webhooks/events/sync`);
626
1241
  if (options.afterEventId) {
627
1242
  url.searchParams.set("after_event_id", options.afterEventId);
628
1243
  }
@@ -637,22 +1252,57 @@ var EdgeConnectServer = class _EdgeConnectServer {
637
1252
  * lifecycle (process-wide, client_credentials) than user tokens.
638
1253
  */
639
1254
  async partnerFetch(url, token) {
640
- try {
641
- return await this.fetchWithTimeout(
1255
+ let lastError = null;
1256
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
1257
+ if (attempt > 0) {
1258
+ await this.sleep(this.getRetryDelay(attempt - 1));
1259
+ }
1260
+ const startTime = Date.now();
1261
+ this.config.onRequest?.({
1262
+ method: "GET",
642
1263
  url,
643
- {
644
- method: "GET",
645
- headers: {
646
- Authorization: `Bearer ${token}`,
647
- "Content-Type": "application/json",
648
- "User-Agent": USER_AGENT
649
- }
650
- },
651
- this.dispatcher
652
- );
653
- } catch (error) {
654
- throw new import_connect2.EdgeNetworkError("syncWebhookEvents network failure", error);
1264
+ endpoint: "partner_webhook_sync",
1265
+ attempt
1266
+ });
1267
+ try {
1268
+ const response = await this.fetchWithTimeout(
1269
+ url,
1270
+ {
1271
+ method: "GET",
1272
+ headers: {
1273
+ Authorization: `Bearer ${token}`,
1274
+ "Content-Type": "application/json",
1275
+ "User-Agent": USER_AGENT
1276
+ }
1277
+ },
1278
+ this.dispatcher
1279
+ );
1280
+ const durationMs = Date.now() - startTime;
1281
+ if (response.status !== 401 && this.retryConfig.retryOn.includes(response.status) && attempt < this.retryConfig.maxRetries) {
1282
+ await this.drainResponseBody(response);
1283
+ this.config.onResponse?.({
1284
+ status: response.status,
1285
+ body: { retrying: true },
1286
+ durationMs,
1287
+ method: "GET",
1288
+ url,
1289
+ endpoint: "partner_webhook_sync",
1290
+ attempt
1291
+ });
1292
+ continue;
1293
+ }
1294
+ return { response, durationMs, attempt };
1295
+ } catch (error) {
1296
+ lastError = error;
1297
+ if (attempt >= this.retryConfig.maxRetries) {
1298
+ throw new import_connect4.EdgeNetworkError("syncWebhookEvents network failure", lastError);
1299
+ }
1300
+ }
655
1301
  }
1302
+ throw new import_connect4.EdgeNetworkError(
1303
+ `syncWebhookEvents network failure after ${this.retryConfig.maxRetries} retries`,
1304
+ lastError
1305
+ );
656
1306
  }
657
1307
  /**
658
1308
  * Fetches (or returns the cached) partner token via the
@@ -668,6 +1318,17 @@ var EdgeConnectServer = class _EdgeConnectServer {
668
1318
  }
669
1319
  const tokenUrl = `${this.oauthBaseUrl}/token`;
670
1320
  let response;
1321
+ const startTime = Date.now();
1322
+ this.config.onRequest?.({
1323
+ method: "POST",
1324
+ url: tokenUrl,
1325
+ endpoint: "oauth_token",
1326
+ body: {
1327
+ grant_type: "client_credentials",
1328
+ client_id: this.config.partnerClientId,
1329
+ client_secret: "[REDACTED]"
1330
+ }
1331
+ });
671
1332
  try {
672
1333
  response = await this.fetchWithTimeout(
673
1334
  tokenUrl,
@@ -686,18 +1347,26 @@ var EdgeConnectServer = class _EdgeConnectServer {
686
1347
  this.dispatcher
687
1348
  );
688
1349
  } catch (error) {
689
- throw new import_connect2.EdgeNetworkError("Partner token request network failure", error);
1350
+ throw new import_connect4.EdgeNetworkError("Partner token request network failure", error);
690
1351
  }
1352
+ const durationMs = Date.now() - startTime;
1353
+ const data = await response.json().catch(() => ({}));
1354
+ this.config.onResponse?.({
1355
+ status: response.status,
1356
+ body: this.redactPartnerTokenResponseBody(data),
1357
+ durationMs,
1358
+ method: "POST",
1359
+ url: tokenUrl,
1360
+ endpoint: "oauth_token"
1361
+ });
691
1362
  if (!response.ok) {
692
- const body = await response.json().catch(() => ({}));
693
- throw new import_connect2.EdgeAuthenticationError(
694
- body.message || body.error_description || `Partner token request failed with status ${response.status}`,
695
- body
1363
+ throw new import_connect4.EdgeAuthenticationError(
1364
+ data.message || data.error_description || `Partner token request failed with status ${response.status}`,
1365
+ data
696
1366
  );
697
1367
  }
698
- const data = await response.json().catch(() => ({}));
699
1368
  if (!data.access_token || typeof data.expires_in !== "number") {
700
- throw new import_connect2.EdgeAuthenticationError("Partner token response missing access_token or expires_in");
1369
+ throw new import_connect4.EdgeAuthenticationError("Partner token response missing access_token or expires_in");
701
1370
  }
702
1371
  this.partnerTokenCache = {
703
1372
  token: data.access_token,
@@ -712,6 +1381,26 @@ var EdgeConnectServer = class _EdgeConnectServer {
712
1381
  _resetPartnerTokenCache() {
713
1382
  this.partnerTokenCache = null;
714
1383
  }
1384
+ redactPartnerTokenResponseBody(body) {
1385
+ return {
1386
+ ...body,
1387
+ ...typeof body.access_token === "string" ? { access_token: "[REDACTED]" } : {},
1388
+ ...typeof body.refresh_token === "string" ? { refresh_token: "[REDACTED]" } : {},
1389
+ ...typeof body.id_token === "string" ? { id_token: "[REDACTED]" } : {}
1390
+ };
1391
+ }
1392
+ redactPartnerSyncResponseBody(body) {
1393
+ if (Array.isArray(body.events)) {
1394
+ return {
1395
+ ...body,
1396
+ events: `[REDACTED ${body.events.length} events]`
1397
+ };
1398
+ }
1399
+ return body;
1400
+ }
1401
+ async drainResponseBody(response) {
1402
+ await response.arrayBuffer().catch(() => void 0);
1403
+ }
715
1404
  getRetryDelay(attempt) {
716
1405
  const { backoff, baseDelayMs } = this.retryConfig;
717
1406
  if (backoff === "exponential") {
@@ -752,37 +1441,37 @@ var EdgeConnectServer = class _EdgeConnectServer {
752
1441
  const errorCode = error.error;
753
1442
  const errorMessage = error.message || error.error_description;
754
1443
  if (errorCode === "invalid_grant" || errorMessage?.includes("Invalid or expired")) {
755
- return new import_connect2.EdgeTokenExchangeError("Authorization code is invalid, expired, or already used. Please try again.", {
1444
+ return new import_connect4.EdgeTokenExchangeError("Authorization code is invalid, expired, or already used. Please try again.", {
756
1445
  edgeBoostError: error
757
1446
  });
758
1447
  }
759
1448
  if (errorCode === "invalid_client" || errorMessage?.includes("Invalid client")) {
760
- return new import_connect2.EdgeAuthenticationError("Invalid client credentials. Check your client ID and secret.", {
1449
+ return new import_connect4.EdgeAuthenticationError("Invalid client credentials. Check your client ID and secret.", {
761
1450
  edgeBoostError: error
762
1451
  });
763
1452
  }
764
1453
  if (errorMessage?.includes("code_verifier") || errorMessage?.includes("PKCE")) {
765
- return new import_connect2.EdgeTokenExchangeError("PKCE verification failed. Please try again.", { edgeBoostError: error });
1454
+ return new import_connect4.EdgeTokenExchangeError("PKCE verification failed. Please try again.", { edgeBoostError: error });
766
1455
  }
767
- return new import_connect2.EdgeTokenExchangeError(errorMessage || "Failed to exchange authorization code", {
1456
+ return new import_connect4.EdgeTokenExchangeError(errorMessage || "Failed to exchange authorization code", {
768
1457
  edgeBoostError: error,
769
1458
  statusCode: status
770
1459
  });
771
1460
  }
772
1461
  async handleApiErrorFromBody(error, status, path) {
773
1462
  if (status === 401) {
774
- return new import_connect2.EdgeAuthenticationError(error.message || "Access token is invalid or expired", error);
1463
+ return new import_connect4.EdgeAuthenticationError(error.message || "Access token is invalid or expired", error);
775
1464
  }
776
1465
  if (status === 403) {
777
1466
  if (error.error === "consent_required") {
778
- return new import_connect2.EdgeConsentRequiredError(
1467
+ return new import_connect4.EdgeConsentRequiredError(
779
1468
  this.config.clientId,
780
1469
  error.consentUrl,
781
1470
  error.message
782
1471
  );
783
1472
  }
784
1473
  if (error.error === "insufficient_scope" || error.error === "insufficient_consent") {
785
- return new import_connect2.EdgeInsufficientScopeError(
1474
+ return new import_connect4.EdgeInsufficientScopeError(
786
1475
  error.missing_scopes || error.missingScopes || [],
787
1476
  error.message
788
1477
  );
@@ -804,15 +1493,15 @@ var EdgeConnectServer = class _EdgeConnectServer {
804
1493
  } else if (path.includes("/transfer")) {
805
1494
  resourceType = "Transfer";
806
1495
  }
807
- return new import_connect2.EdgeNotFoundError(resourceType, resourceId);
1496
+ return new import_connect4.EdgeNotFoundError(resourceType, resourceId);
808
1497
  }
809
1498
  if (status === 422 && error.error === "identity_verification_failed") {
810
- return new import_connect2.EdgeIdentityVerificationError(
1499
+ return new import_connect4.EdgeIdentityVerificationError(
811
1500
  error.fieldErrors || {},
812
1501
  error.message
813
1502
  );
814
1503
  }
815
- return new import_connect2.EdgeApiError(
1504
+ return new import_connect4.EdgeApiError(
816
1505
  error.error || "api_error",
817
1506
  error.message || error.error_description || `Request failed with status ${status}`,
818
1507
  status,
@@ -821,8 +1510,221 @@ var EdgeConnectServer = class _EdgeConnectServer {
821
1510
  }
822
1511
  };
823
1512
 
1513
+ // src/env.ts
1514
+ var import_connect5 = require("@edge-markets/connect");
1515
+ var import_node_fs2 = __toESM(require("fs"));
1516
+ var ENVIRONMENT_NAMES = new Set(Object.keys(import_connect5.EDGE_ENVIRONMENTS));
1517
+ function readEnv(env, name) {
1518
+ const value = env[name];
1519
+ if (typeof value !== "string") return void 0;
1520
+ const trimmed = value.trim();
1521
+ return trimmed ? trimmed : void 0;
1522
+ }
1523
+ function readFirstEnv(env, names) {
1524
+ for (const name of names) {
1525
+ const value = readEnv(env, name);
1526
+ if (value) return value;
1527
+ }
1528
+ return void 0;
1529
+ }
1530
+ function normalizePem(value) {
1531
+ return value.replace(/\\n/g, "\n").trim();
1532
+ }
1533
+ function readPemValue(env, valueName, pathName, warnings) {
1534
+ const inlineValue = readEnv(env, valueName);
1535
+ const filePath = readEnv(env, pathName);
1536
+ if (inlineValue && filePath) {
1537
+ warnings.push(`${valueName} and ${pathName} are both set; using ${valueName}`);
1538
+ }
1539
+ if (inlineValue) {
1540
+ return normalizePem(inlineValue);
1541
+ }
1542
+ if (!filePath) {
1543
+ return void 0;
1544
+ }
1545
+ try {
1546
+ return normalizePem(import_node_fs2.default.readFileSync(filePath, "utf8"));
1547
+ } catch (error) {
1548
+ const message = error instanceof Error ? error.message : String(error);
1549
+ throw new import_connect5.EdgeValidationError(`Failed to read ${pathName}`, {
1550
+ [pathName]: [`Unable to read PEM file "${filePath}": ${message}`]
1551
+ });
1552
+ }
1553
+ }
1554
+ function readEnvironment(env) {
1555
+ const value = readEnv(env, "EDGE_ENVIRONMENT") || "staging";
1556
+ if (!ENVIRONMENT_NAMES.has(value)) {
1557
+ throw new import_connect5.EdgeValidationError("Invalid EDGE_ENVIRONMENT", {
1558
+ EDGE_ENVIRONMENT: [`Must be one of: ${Array.from(ENVIRONMENT_NAMES).join(", ")}`]
1559
+ });
1560
+ }
1561
+ return value;
1562
+ }
1563
+ function readTimeout(env, override) {
1564
+ if (override !== void 0) return override;
1565
+ const value = readFirstEnv(env, ["EDGE_TIMEOUT_MS", "EDGE_REQUEST_TIMEOUT_MS"]);
1566
+ if (!value) return void 0;
1567
+ const timeout = Number(value);
1568
+ if (!Number.isInteger(timeout) || timeout <= 0) {
1569
+ throw new import_connect5.EdgeValidationError("Invalid EDGE_TIMEOUT_MS", {
1570
+ EDGE_TIMEOUT_MS: ["Must be a positive integer number of milliseconds"]
1571
+ });
1572
+ }
1573
+ return timeout;
1574
+ }
1575
+ function buildMtlsConfig(env, requireMtls, warnings) {
1576
+ const cert = readPemValue(env, "EDGE_MTLS_CERT", "EDGE_MTLS_CERT_PATH", warnings);
1577
+ const key = readPemValue(env, "EDGE_MTLS_KEY", "EDGE_MTLS_KEY_PATH", warnings);
1578
+ const ca = readPemValue(env, "EDGE_MTLS_CA", "EDGE_MTLS_CA_PATH", warnings);
1579
+ if (!cert && !key && !ca) {
1580
+ if (requireMtls) {
1581
+ throw new import_connect5.EdgeValidationError("mTLS is required but no EDGE_MTLS_CERT or EDGE_MTLS_KEY was provided", {
1582
+ EDGE_MTLS_CERT: ["Required when requireMtls is true"],
1583
+ EDGE_MTLS_KEY: ["Required when requireMtls is true"]
1584
+ });
1585
+ }
1586
+ return void 0;
1587
+ }
1588
+ if (!cert || !key) {
1589
+ throw new import_connect5.EdgeValidationError("Incomplete mTLS configuration", {
1590
+ EDGE_MTLS_CERT: cert ? [] : ["Required when mTLS is configured"],
1591
+ EDGE_MTLS_KEY: key ? [] : ["Required when mTLS is configured"]
1592
+ });
1593
+ }
1594
+ return {
1595
+ enabled: true,
1596
+ cert,
1597
+ key,
1598
+ ...ca ? { ca } : {}
1599
+ };
1600
+ }
1601
+ function inferMtlsRequirement(environment, apiBaseUrl) {
1602
+ if (environment === "staging" || environment === "production") return true;
1603
+ const resolvedApiBaseUrl = apiBaseUrl || (0, import_connect5.getEnvironmentConfig)(environment).apiBaseUrl;
1604
+ try {
1605
+ const host = new URL(resolvedApiBaseUrl).host;
1606
+ return host === "connect-staging.edgeboost.io" || host === "connect.edgeboost.io";
1607
+ } catch {
1608
+ return false;
1609
+ }
1610
+ }
1611
+ function createEdgeConnectServerFromEnv(options = {}) {
1612
+ const env = options.env ?? process.env;
1613
+ const warnings = [];
1614
+ const environment = readEnvironment(env);
1615
+ const apiBaseUrl = readEnv(env, "EDGE_API_BASE_URL");
1616
+ const mtlsRequiredByTarget = inferMtlsRequirement(environment, apiBaseUrl);
1617
+ const requireMtls = options.requireMtls ?? false;
1618
+ const mtls = buildMtlsConfig(env, requireMtls, warnings);
1619
+ const timeout = readTimeout(env, options.timeout);
1620
+ const clientId = readFirstEnv(env, ["EDGE_CLIENT_ID", "EDGE_CONNECT_CLIENT_ID"]) || "";
1621
+ const clientSecret = readFirstEnv(env, ["EDGE_CLIENT_SECRET", "EDGE_CONNECT_CLIENT_SECRET"]) || "";
1622
+ const oauthBaseUrl = readEnv(env, "EDGE_OAUTH_BASE_URL");
1623
+ const partnerApiBaseUrl = readEnv(env, "EDGE_PARTNER_API_BASE_URL");
1624
+ const partnerClientId = readFirstEnv(env, ["EDGE_PARTNER_CLIENT_ID", "EDGE_DASHBOARD_CLIENT_ID"]);
1625
+ const partnerClientSecret = readFirstEnv(env, ["EDGE_PARTNER_CLIENT_SECRET", "EDGE_DASHBOARD_CLIENT_SECRET"]);
1626
+ const validationErrors = {};
1627
+ if (!clientId) validationErrors.EDGE_CLIENT_ID = ["Required"];
1628
+ if (!clientSecret) validationErrors.EDGE_CLIENT_SECRET = ["Required"];
1629
+ if (Object.keys(validationErrors).length > 0) {
1630
+ throw new import_connect5.EdgeValidationError("Missing required EDGE Connect environment variables", validationErrors);
1631
+ }
1632
+ if (mtlsRequiredByTarget && !mtls) {
1633
+ warnings.push(
1634
+ `EDGE_ENVIRONMENT=${environment} uses an mTLS-enforced EDGE Connect target; configure EDGE_MTLS_CERT and EDGE_MTLS_KEY before making API calls.`
1635
+ );
1636
+ }
1637
+ const config = {
1638
+ clientId,
1639
+ clientSecret,
1640
+ environment,
1641
+ ...apiBaseUrl ? { apiBaseUrl } : {},
1642
+ ...oauthBaseUrl ? { oauthBaseUrl } : {},
1643
+ ...partnerApiBaseUrl ? { partnerApiBaseUrl } : {},
1644
+ ...partnerClientId ? { partnerClientId } : {},
1645
+ ...partnerClientSecret ? { partnerClientSecret } : {},
1646
+ ...timeout ? { timeout } : {},
1647
+ ...options.retry ? { retry: options.retry } : {},
1648
+ ...options.onRequest ? { onRequest: options.onRequest } : {},
1649
+ ...options.onResponse ? { onResponse: options.onResponse } : {},
1650
+ ...options.mle ? { mle: options.mle } : {},
1651
+ ...mtls ? { mtls } : {}
1652
+ };
1653
+ const server = new EdgeConnectServer(config);
1654
+ return {
1655
+ server,
1656
+ config,
1657
+ capabilities: {
1658
+ mtlsEnabled: !!config.mtls?.enabled,
1659
+ webhookSyncConfigured: !!config.partnerClientId && !!config.partnerClientSecret,
1660
+ customApiBaseUrl: !!config.apiBaseUrl,
1661
+ customOAuthBaseUrl: !!config.oauthBaseUrl,
1662
+ customPartnerApiBaseUrl: !!config.partnerApiBaseUrl
1663
+ },
1664
+ warnings
1665
+ };
1666
+ }
1667
+
1668
+ // src/error-adapter.ts
1669
+ var import_connect6 = require("@edge-markets/connect");
1670
+ function toEdgeHttpError(error) {
1671
+ const body = toEdgeProblemJson(error);
1672
+ return { status: body.status, body };
1673
+ }
1674
+ function toEdgeProblemJson(error) {
1675
+ if (error instanceof import_connect6.EdgeConsentRequiredError) {
1676
+ return problem(error, "Consent required", error.statusCode ?? 403, {
1677
+ clientId: error.clientId,
1678
+ consentUrl: error.consentUrl
1679
+ });
1680
+ }
1681
+ if (error instanceof import_connect6.EdgeValidationError) {
1682
+ return problem(error, "Validation failed", error.statusCode ?? 400, {
1683
+ validationErrors: error.validationErrors
1684
+ });
1685
+ }
1686
+ if (error instanceof import_connect6.EdgeIdentityVerificationError) {
1687
+ return problem(error, "Identity verification failed", error.statusCode ?? 422, {
1688
+ fieldErrors: error.fieldErrors
1689
+ });
1690
+ }
1691
+ if (error instanceof import_connect6.EdgeNetworkError) {
1692
+ return problem(error, "EDGE network error", error.statusCode ?? 502);
1693
+ }
1694
+ if (error instanceof import_connect6.EdgeError) {
1695
+ return problem(error, edgeTitle(error), error.statusCode ?? 500, error.details);
1696
+ }
1697
+ const message = error instanceof Error ? error.message : "Unexpected error";
1698
+ return {
1699
+ type: "https://developer.edgemarkets.io/errors/internal_error",
1700
+ title: "Internal error",
1701
+ status: 500,
1702
+ detail: message,
1703
+ code: "internal_error"
1704
+ };
1705
+ }
1706
+ function problem(error, title, status, details) {
1707
+ return {
1708
+ type: `https://developer.edgemarkets.io/errors/${error.code}`,
1709
+ title,
1710
+ status,
1711
+ detail: error.message,
1712
+ code: error.code,
1713
+ ...details && Object.keys(details).length > 0 ? { details } : {}
1714
+ };
1715
+ }
1716
+ function edgeTitle(error) {
1717
+ return error.code.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1718
+ }
1719
+
1720
+ // src/http-helpers.ts
1721
+ var import_connect8 = require("@edge-markets/connect");
1722
+
1723
+ // src/webhook-parser.ts
1724
+ var import_connect7 = require("@edge-markets/connect");
1725
+
824
1726
  // src/webhook-signing.ts
825
- var import_node_crypto = __toESM(require("crypto"));
1727
+ var import_node_crypto2 = __toESM(require("crypto"));
826
1728
  function verifyWebhookSignature(header, body, secret, options = {}) {
827
1729
  if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
828
1730
  return false;
@@ -839,22 +1741,877 @@ function verifyWebhookSignature(header, body, secret, options = {}) {
839
1741
  const now = Math.floor(Date.now() / 1e3);
840
1742
  if (Math.abs(now - timestamp) > toleranceSeconds) return false;
841
1743
  const signedContent = `${timestamp}.${body}`;
842
- const expectedHmac = import_node_crypto.default.createHmac("sha256", secret).update(signedContent).digest("hex");
1744
+ const expectedHmac = import_node_crypto2.default.createHmac("sha256", secret).update(signedContent).digest("hex");
843
1745
  try {
844
1746
  const sigBuf = Buffer.from(signature, "hex");
845
1747
  const expBuf = Buffer.from(expectedHmac, "hex");
846
1748
  if (sigBuf.length !== expBuf.length) return false;
847
- return import_node_crypto.default.timingSafeEqual(sigBuf, expBuf);
1749
+ return import_node_crypto2.default.timingSafeEqual(sigBuf, expBuf);
848
1750
  } catch {
849
1751
  return false;
850
1752
  }
851
1753
  }
852
1754
 
1755
+ // src/webhook-parser.ts
1756
+ var EVENT_TYPES = new Set(import_connect7.EDGE_WEBHOOK_EVENT_TYPES);
1757
+ var TRANSFER_TYPES = /* @__PURE__ */ new Set(["debit", "credit"]);
1758
+ function parseAndVerifyWebhook(options) {
1759
+ const body = normalizeRawBody(options.rawBody);
1760
+ const header = normalizeSignatureHeader(options.signatureHeader);
1761
+ const signatureTimestamp = extractWebhookSignatureTimestamp(header);
1762
+ if (!header || !signatureTimestamp) {
1763
+ throw new import_connect7.EdgeAuthenticationError("Missing or malformed EDGE webhook signature");
1764
+ }
1765
+ if (!verifyWebhookSignature(header, body, options.secret, { toleranceSeconds: options.toleranceSeconds })) {
1766
+ throw new import_connect7.EdgeAuthenticationError("Invalid EDGE webhook signature");
1767
+ }
1768
+ let parsed;
1769
+ try {
1770
+ parsed = JSON.parse(body);
1771
+ } catch {
1772
+ throw new import_connect7.EdgeValidationError("EDGE webhook body must be valid JSON", {
1773
+ rawBody: ["Unable to parse JSON after signature verification"]
1774
+ });
1775
+ }
1776
+ const event = validateEdgeWebhookEvent(parsed);
1777
+ return {
1778
+ event,
1779
+ eventId: event.id,
1780
+ eventType: event.type,
1781
+ signatureTimestamp,
1782
+ receivedAt: options.receivedAt ?? /* @__PURE__ */ new Date()
1783
+ };
1784
+ }
1785
+ function extractWebhookSignatureTimestamp(header) {
1786
+ const normalizedHeader = normalizeSignatureHeader(header);
1787
+ if (!normalizedHeader) return void 0;
1788
+ const part = normalizedHeader.split(",").map((value) => value.trim()).find((value) => value.startsWith("t="));
1789
+ if (!part) return void 0;
1790
+ const timestamp = Number(part.slice(2));
1791
+ if (!Number.isInteger(timestamp) || timestamp <= 0) return void 0;
1792
+ return timestamp;
1793
+ }
1794
+ function isEdgeWebhookEvent(value) {
1795
+ try {
1796
+ validateEdgeWebhookEvent(value);
1797
+ return true;
1798
+ } catch {
1799
+ return false;
1800
+ }
1801
+ }
1802
+ function validateEdgeWebhookEvent(value) {
1803
+ const errors = {};
1804
+ if (!isRecord(value)) {
1805
+ throw new import_connect7.EdgeValidationError("EDGE webhook event must be an object", {
1806
+ event: ["Expected JSON object"]
1807
+ });
1808
+ }
1809
+ const id = value.id;
1810
+ const type = value.type;
1811
+ const createdAt = value.created_at;
1812
+ const data = value.data;
1813
+ if (typeof id !== "string" || !id.trim()) {
1814
+ errors.id = ["Required string"];
1815
+ }
1816
+ if (typeof type !== "string" || !EVENT_TYPES.has(type)) {
1817
+ errors.type = [`Must be one of: ${import_connect7.EDGE_WEBHOOK_EVENT_TYPES.join(", ")}`];
1818
+ }
1819
+ if (typeof createdAt !== "string" || !createdAt.trim() || Number.isNaN(Date.parse(createdAt))) {
1820
+ errors.created_at = ["Required ISO 8601 timestamp string"];
1821
+ }
1822
+ if (!isRecord(data)) {
1823
+ errors.data = ["Required object"];
1824
+ }
1825
+ if (Object.keys(errors).length === 0) {
1826
+ validateKnownEventData(type, data, errors);
1827
+ }
1828
+ if (Object.keys(errors).length > 0) {
1829
+ throw new import_connect7.EdgeValidationError("Invalid EDGE webhook event", errors);
1830
+ }
1831
+ return value;
1832
+ }
1833
+ function validateKnownEventData(type, data, errors) {
1834
+ switch (type) {
1835
+ case "transfer.completed":
1836
+ validateTransferEventData(data, "completed", errors);
1837
+ return;
1838
+ case "transfer.failed":
1839
+ validateTransferEventData(data, "failed", errors);
1840
+ if (data.reason !== void 0 && typeof data.reason !== "string") {
1841
+ errors["data.reason"] = ["Must be a string when provided"];
1842
+ }
1843
+ return;
1844
+ case "transfer.expired":
1845
+ validateTransferEventData(data, "expired", errors);
1846
+ return;
1847
+ case "transfer.processing":
1848
+ validateTransferEventData(data, "processing", errors);
1849
+ return;
1850
+ case "consent.revoked":
1851
+ validateRequiredString(data, "userId", errors);
1852
+ validateRequiredString(data, "clientId", errors);
1853
+ validateRequiredIsoTimestamp(data, "revokedAt", errors);
1854
+ return;
1855
+ default: {
1856
+ const exhaustive = type;
1857
+ throw new import_connect7.EdgeValidationError("Unsupported EDGE webhook event type", {
1858
+ type: [`Unsupported event type: ${exhaustive}`]
1859
+ });
1860
+ }
1861
+ }
1862
+ }
1863
+ function validateTransferEventData(data, status, errors) {
1864
+ validateRequiredString(data, "transferId", errors);
1865
+ if (data.status !== status) {
1866
+ errors["data.status"] = [`Must be "${status}" for this event type`];
1867
+ }
1868
+ if (typeof data.type !== "string" || !TRANSFER_TYPES.has(data.type)) {
1869
+ errors["data.type"] = ['Must be "debit" or "credit"'];
1870
+ }
1871
+ validateRequiredString(data, "amount", errors);
1872
+ }
1873
+ function validateRequiredString(data, field, errors) {
1874
+ if (typeof data[field] !== "string" || !data[field].trim()) {
1875
+ errors[`data.${field}`] = ["Required string"];
1876
+ }
1877
+ }
1878
+ function validateRequiredIsoTimestamp(data, field, errors) {
1879
+ if (typeof data[field] !== "string" || !data[field].trim() || Number.isNaN(Date.parse(data[field]))) {
1880
+ errors[`data.${field}`] = ["Required ISO 8601 timestamp string"];
1881
+ }
1882
+ }
1883
+ function normalizeRawBody(rawBody) {
1884
+ if (typeof rawBody === "string") {
1885
+ return rawBody;
1886
+ }
1887
+ if (Buffer.isBuffer(rawBody) || rawBody instanceof Uint8Array) {
1888
+ return Buffer.from(rawBody).toString("utf8");
1889
+ }
1890
+ throw new import_connect7.EdgeValidationError("EDGE webhook raw body is required", {
1891
+ rawBody: ["Pass the raw UTF-8 request body, not a parsed JSON object"]
1892
+ });
1893
+ }
1894
+ function normalizeSignatureHeader(header) {
1895
+ if (Array.isArray(header)) {
1896
+ return header.map((value) => value.trim()).filter(Boolean).join(",");
1897
+ }
1898
+ if (typeof header !== "string") return void 0;
1899
+ const trimmed = header.trim();
1900
+ return trimmed || void 0;
1901
+ }
1902
+ function isRecord(value) {
1903
+ return !!value && typeof value === "object" && !Array.isArray(value);
1904
+ }
1905
+
1906
+ // src/http-helpers.ts
1907
+ function getEdgeWebhookRawBody(request) {
1908
+ if (typeof request.rawBody === "string" || Buffer.isBuffer(request.rawBody) || request.rawBody instanceof Uint8Array) {
1909
+ return request.rawBody;
1910
+ }
1911
+ throw new import_connect8.EdgeValidationError("EDGE webhook raw body is required", {
1912
+ rawBody: [
1913
+ "Configure your framework to expose the raw UTF-8 request body before JSON parsing. Parsed JSON bodies cannot be used for HMAC verification."
1914
+ ]
1915
+ });
1916
+ }
1917
+ function getEdgeWebhookSignatureHeader(request, headerName = "x-edge-signature") {
1918
+ const headers = request.headers ?? {};
1919
+ const lowerHeaderName = headerName.toLowerCase();
1920
+ for (const [name, value] of Object.entries(headers)) {
1921
+ if (name.toLowerCase() === lowerHeaderName) {
1922
+ if (typeof value === "string" || Array.isArray(value)) return value;
1923
+ if (value === void 0 || value === null) return void 0;
1924
+ return String(value);
1925
+ }
1926
+ }
1927
+ return void 0;
1928
+ }
1929
+ function parseWebhookHttpRequest(options) {
1930
+ return parseAndVerifyWebhook({
1931
+ ...options,
1932
+ rawBody: getEdgeWebhookRawBody(options.request),
1933
+ signatureHeader: getEdgeWebhookSignatureHeader(options.request, options.signatureHeaderName)
1934
+ });
1935
+ }
1936
+
1937
+ // src/identity.ts
1938
+ var import_connect9 = require("@edge-markets/connect");
1939
+ var DEFAULT_IDENTITY_POLICY = {
1940
+ minNameScore: 70,
1941
+ minAddressScore: 65,
1942
+ requireZipMatch: true,
1943
+ requireApiVerified: true
1944
+ };
1945
+ function buildVerifyIdentityPayload(profile) {
1946
+ const firstName = firstNonEmpty(profile.firstName, profile.givenName);
1947
+ const lastName = firstNonEmpty(profile.lastName, profile.familyName);
1948
+ const address = profile.address ?? {};
1949
+ const payload = {
1950
+ firstName,
1951
+ lastName,
1952
+ address: {
1953
+ street: firstNonEmpty(address.street, address.line1, address.address1),
1954
+ city: firstNonEmpty(address.city),
1955
+ state: firstNonEmpty(address.state),
1956
+ zip: firstNonEmpty(address.zip, address.postalCode)
1957
+ }
1958
+ };
1959
+ const errors = {};
1960
+ if (!payload.firstName) errors.firstName = ["Required"];
1961
+ if (!payload.lastName) errors.lastName = ["Required"];
1962
+ if (!Object.values(payload.address).some((value) => !!value)) {
1963
+ errors.address = ["At least one address field is required"];
1964
+ }
1965
+ if (Object.keys(errors).length > 0) {
1966
+ throw new import_connect9.EdgeValidationError("Partner identity profile is incomplete", errors);
1967
+ }
1968
+ return payload;
1969
+ }
1970
+ function evaluateIdentityResult(result, policy = {}) {
1971
+ const resolved = { ...DEFAULT_IDENTITY_POLICY, ...policy };
1972
+ validatePolicy(resolved);
1973
+ const reasons = [];
1974
+ const verifiedFlag = result.verified;
1975
+ if (resolved.requireApiVerified && verifiedFlag !== true) {
1976
+ reasons.push("api_not_verified");
1977
+ }
1978
+ const scores = result.scores;
1979
+ if (!scores) {
1980
+ reasons.push("scores_missing");
1981
+ } else {
1982
+ if (typeof scores.name === "number" && scores.name < resolved.minNameScore) {
1983
+ reasons.push("name_score_below_threshold");
1984
+ }
1985
+ if (typeof scores.address === "number" && scores.address < resolved.minAddressScore) {
1986
+ reasons.push("address_score_below_threshold");
1987
+ }
1988
+ if (resolved.requireZipMatch && scores.zipMatch !== true) {
1989
+ reasons.push("zip_mismatch");
1990
+ }
1991
+ }
1992
+ return {
1993
+ verified: reasons.length === 0,
1994
+ reasons,
1995
+ ...scores ? { scores } : {}
1996
+ };
1997
+ }
1998
+ function validatePolicy(policy) {
1999
+ const errors = {};
2000
+ if (!isScore(policy.minNameScore)) errors.minNameScore = ["Must be between 0 and 100"];
2001
+ if (!isScore(policy.minAddressScore)) errors.minAddressScore = ["Must be between 0 and 100"];
2002
+ if (Object.keys(errors).length > 0) {
2003
+ throw new import_connect9.EdgeValidationError("Invalid identity policy", errors);
2004
+ }
2005
+ }
2006
+ function isScore(value) {
2007
+ return Number.isFinite(value) && value >= 0 && value <= 100;
2008
+ }
2009
+ function firstNonEmpty(...values) {
2010
+ for (const value of values) {
2011
+ if (typeof value === "string") {
2012
+ const trimmed = value.trim();
2013
+ if (trimmed) return trimmed;
2014
+ }
2015
+ }
2016
+ return "";
2017
+ }
2018
+
2019
+ // src/session-store.ts
2020
+ var import_connect10 = require("@edge-markets/connect");
2021
+ function createSessionTokenRecord(subjectId, tokens, tokenVault, options = {}) {
2022
+ const normalizedSubjectId = normalizeRequiredString(subjectId, "subjectId");
2023
+ validateTokens2(tokens);
2024
+ return {
2025
+ subjectId: normalizedSubjectId,
2026
+ encryptedTokens: tokenVault.encryptTokens(tokens),
2027
+ expiresAt: normalizeSessionExpiresAt(tokens.expiresAt),
2028
+ scopes: options.scopes ?? splitScopes(tokens.scope),
2029
+ ...options.edgeUserId ? { edgeUserId: options.edgeUserId } : {},
2030
+ ...options.keyId ? { keyId: options.keyId } : {},
2031
+ ...options.metadata ? { metadata: options.metadata } : {}
2032
+ };
2033
+ }
2034
+ function parseSessionTokenRecord(record) {
2035
+ if (!isRecord2(record)) {
2036
+ throw new import_connect10.EdgeValidationError("EDGE session token record must be an object", {
2037
+ record: ["Expected object"]
2038
+ });
2039
+ }
2040
+ const subjectId = normalizeRequiredString(record.subjectId, "subjectId");
2041
+ const encryptedTokens = normalizeRequiredString(record.encryptedTokens, "encryptedTokens");
2042
+ const expiresAt = normalizeSessionExpiresAt(record.expiresAt);
2043
+ const scopes = record.scopes === void 0 ? void 0 : normalizeScopes(record.scopes);
2044
+ return {
2045
+ subjectId,
2046
+ encryptedTokens,
2047
+ expiresAt,
2048
+ ...scopes ? { scopes } : {},
2049
+ ...typeof record.edgeUserId === "string" && record.edgeUserId.trim() ? { edgeUserId: record.edgeUserId.trim() } : {},
2050
+ ...typeof record.keyId === "string" && record.keyId.trim() ? { keyId: record.keyId.trim() } : {},
2051
+ ...isRecord2(record.metadata) ? { metadata: record.metadata } : {}
2052
+ };
2053
+ }
2054
+ function encryptLegacySessionTokens(legacyRecord, tokenVault, options) {
2055
+ const subjectId = normalizeRequiredString(options.subjectId || legacyRecord.subjectId, "subjectId");
2056
+ const accessToken = normalizeRequiredString(legacyRecord.accessToken, "accessToken");
2057
+ const refreshToken = normalizeRequiredString(legacyRecord.refreshToken, "refreshToken");
2058
+ if (!options.allowPlaintext && !tokenVault.isEncrypted(accessToken) && !tokenVault.isEncrypted(refreshToken)) {
2059
+ throw new import_connect10.EdgeValidationError("Plaintext token migration requires allowPlaintext: true", {
2060
+ allowPlaintext: ["Set allowPlaintext only inside an explicit migration path"]
2061
+ });
2062
+ }
2063
+ const scope = normalizeScopeString(legacyRecord.scope ?? legacyRecord.scopes);
2064
+ const tokens = {
2065
+ accessToken,
2066
+ refreshToken,
2067
+ ...typeof legacyRecord.idToken === "string" && legacyRecord.idToken.trim() ? { idToken: legacyRecord.idToken.trim() } : {},
2068
+ expiresIn: normalizeExpiresIn(legacyRecord.expiresIn),
2069
+ expiresAt: normalizeSessionExpiresAt(legacyRecord.expiresAt),
2070
+ scope
2071
+ };
2072
+ return createSessionTokenRecord(subjectId, tokens, tokenVault, {
2073
+ scopes: splitScopes(scope),
2074
+ ...legacyRecord.edgeUserId ? { edgeUserId: legacyRecord.edgeUserId } : {},
2075
+ ...options.keyId ? { keyId: options.keyId } : {},
2076
+ ...legacyRecord.metadata ? { metadata: legacyRecord.metadata } : {}
2077
+ });
2078
+ }
2079
+ function normalizeSessionExpiresAt(value) {
2080
+ if (value instanceof Date) {
2081
+ const time = value.getTime();
2082
+ if (!Number.isFinite(time) || time <= 0) {
2083
+ throw new import_connect10.EdgeValidationError("Invalid EDGE session expiresAt", {
2084
+ expiresAt: ["Date must be valid"]
2085
+ });
2086
+ }
2087
+ return time;
2088
+ }
2089
+ if (typeof value === "string") {
2090
+ const trimmed = value.trim();
2091
+ if (!trimmed) {
2092
+ throw new import_connect10.EdgeValidationError("Invalid EDGE session expiresAt", {
2093
+ expiresAt: ["Required"]
2094
+ });
2095
+ }
2096
+ if (/^\d+$/.test(trimmed)) {
2097
+ return normalizeSessionExpiresAt(Number(trimmed));
2098
+ }
2099
+ const time = Date.parse(trimmed);
2100
+ if (!Number.isNaN(time) && time > 0) return time;
2101
+ }
2102
+ if (typeof value === "number" && Number.isFinite(value)) {
2103
+ if (!Number.isInteger(value) || value <= 0) {
2104
+ throw new import_connect10.EdgeValidationError("Invalid EDGE session expiresAt", {
2105
+ expiresAt: ["Must be a positive integer timestamp in milliseconds"]
2106
+ });
2107
+ }
2108
+ if (value < 1e10) {
2109
+ throw new import_connect10.EdgeValidationError("Ambiguous EDGE session expiresAt", {
2110
+ expiresAt: ["Use Unix milliseconds, not seconds"]
2111
+ });
2112
+ }
2113
+ return value;
2114
+ }
2115
+ throw new import_connect10.EdgeValidationError("Invalid EDGE session expiresAt", {
2116
+ expiresAt: ["Use Unix milliseconds, ISO string, or Date"]
2117
+ });
2118
+ }
2119
+ function normalizeExpiresIn(value) {
2120
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
2121
+ if (typeof value === "string" && value.trim()) {
2122
+ const parsed = Number(value);
2123
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
2124
+ }
2125
+ return 600;
2126
+ }
2127
+ function normalizeScopeString(value) {
2128
+ if (Array.isArray(value)) return normalizeScopes(value).join(" ");
2129
+ if (typeof value === "string") return splitScopes(value).join(" ");
2130
+ return "";
2131
+ }
2132
+ function normalizeScopes(value) {
2133
+ if (Array.isArray(value)) {
2134
+ return value.map((scope) => typeof scope === "string" ? scope.trim() : "").filter(Boolean);
2135
+ }
2136
+ if (typeof value === "string") {
2137
+ return splitScopes(value);
2138
+ }
2139
+ throw new import_connect10.EdgeValidationError("Invalid EDGE session scopes", {
2140
+ scopes: ["Expected array or space-delimited string"]
2141
+ });
2142
+ }
2143
+ function splitScopes(value) {
2144
+ return value.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
2145
+ }
2146
+ function normalizeRequiredString(value, field) {
2147
+ if (typeof value !== "string" || !value.trim()) {
2148
+ throw new import_connect10.EdgeValidationError("Invalid EDGE session token record", {
2149
+ [field]: ["Required string"]
2150
+ });
2151
+ }
2152
+ return value.trim();
2153
+ }
2154
+ function validateTokens2(tokens) {
2155
+ normalizeRequiredString(tokens.accessToken, "accessToken");
2156
+ normalizeRequiredString(tokens.refreshToken, "refreshToken");
2157
+ normalizeSessionExpiresAt(tokens.expiresAt);
2158
+ normalizeExpiresIn(tokens.expiresIn);
2159
+ normalizeScopeString(tokens.scope);
2160
+ }
2161
+ function isRecord2(value) {
2162
+ return !!value && typeof value === "object" && !Array.isArray(value);
2163
+ }
2164
+
2165
+ // src/token-vault.ts
2166
+ var import_connect11 = require("@edge-markets/connect");
2167
+ var import_node_crypto3 = __toESM(require("crypto"));
2168
+ var TOKEN_VAULT_VERSION = "edge_vault_v1";
2169
+ var IV_BYTES = 12;
2170
+ var TAG_BYTES = 16;
2171
+ var KEY_BYTES = 32;
2172
+ var EdgeTokenVault = class {
2173
+ constructor(options) {
2174
+ this.currentKey = normalizeVaultKey(options.currentKey, "currentKey");
2175
+ this.keysById = /* @__PURE__ */ new Map([[this.currentKey.id, this.currentKey.key]]);
2176
+ for (const [index, key] of (options.previousKeys ?? []).entries()) {
2177
+ const normalized = normalizeVaultKey(key, `previousKeys[${index}]`);
2178
+ if (this.keysById.has(normalized.id)) {
2179
+ throw new import_connect11.EdgeValidationError("Duplicate token vault key ID", {
2180
+ keyId: [`Duplicate key ID "${normalized.id}"`]
2181
+ });
2182
+ }
2183
+ this.keysById.set(normalized.id, normalized.key);
2184
+ }
2185
+ }
2186
+ encryptTokens(tokens) {
2187
+ validateEdgeTokens(tokens);
2188
+ const iv = import_node_crypto3.default.randomBytes(IV_BYTES);
2189
+ const aad = Buffer.from(`${TOKEN_VAULT_VERSION}.${this.currentKey.id}`, "utf8");
2190
+ const cipher = import_node_crypto3.default.createCipheriv("aes-256-gcm", this.currentKey.key, iv);
2191
+ cipher.setAAD(aad);
2192
+ const plaintext = Buffer.from(JSON.stringify(tokens), "utf8");
2193
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
2194
+ const tag = cipher.getAuthTag();
2195
+ return [
2196
+ TOKEN_VAULT_VERSION,
2197
+ toBase64Url2(Buffer.from(this.currentKey.id, "utf8")),
2198
+ toBase64Url2(iv),
2199
+ toBase64Url2(tag),
2200
+ toBase64Url2(ciphertext)
2201
+ ].join(".");
2202
+ }
2203
+ decryptTokens(envelope) {
2204
+ const parsed = parseEnvelope(envelope);
2205
+ const key = this.keysById.get(parsed.keyId);
2206
+ if (!key) {
2207
+ throw new import_connect11.EdgeValidationError("Unknown token vault key ID", {
2208
+ keyId: [`No key configured for "${parsed.keyId}"`]
2209
+ });
2210
+ }
2211
+ try {
2212
+ const decipher = import_node_crypto3.default.createDecipheriv("aes-256-gcm", key, parsed.iv);
2213
+ decipher.setAAD(Buffer.from(`${TOKEN_VAULT_VERSION}.${parsed.keyId}`, "utf8"));
2214
+ decipher.setAuthTag(parsed.tag);
2215
+ const plaintext = Buffer.concat([decipher.update(parsed.ciphertext), decipher.final()]).toString("utf8");
2216
+ const tokens = JSON.parse(plaintext);
2217
+ validateEdgeTokens(tokens);
2218
+ return tokens;
2219
+ } catch (error) {
2220
+ if (error instanceof import_connect11.EdgeValidationError) throw error;
2221
+ throw new import_connect11.EdgeValidationError("Failed to decrypt EDGE tokens", {
2222
+ tokenEnvelope: ["Envelope is malformed, corrupted, or encrypted with a different key"]
2223
+ });
2224
+ }
2225
+ }
2226
+ isEncrypted(value) {
2227
+ return typeof value === "string" && value.startsWith(`${TOKEN_VAULT_VERSION}.`);
2228
+ }
2229
+ };
2230
+ function createEdgeTokenVault(options) {
2231
+ return new EdgeTokenVault(options);
2232
+ }
2233
+ function isEdgeTokenVaultEnvelope(value) {
2234
+ return typeof value === "string" && value.startsWith(`${TOKEN_VAULT_VERSION}.`);
2235
+ }
2236
+ function normalizeVaultKey(input, fieldName) {
2237
+ const id = typeof input.id === "string" ? input.id.trim() : "";
2238
+ if (!id) {
2239
+ throw new import_connect11.EdgeValidationError("Token vault key ID is required", {
2240
+ [`${fieldName}.id`]: ["Required"]
2241
+ });
2242
+ }
2243
+ return {
2244
+ id,
2245
+ key: normalizeKeyMaterial(input.key, `${fieldName}.key`)
2246
+ };
2247
+ }
2248
+ function normalizeKeyMaterial(input, fieldName) {
2249
+ const candidates = [];
2250
+ if (Buffer.isBuffer(input)) {
2251
+ candidates.push(input);
2252
+ } else if (input instanceof Uint8Array) {
2253
+ candidates.push(Buffer.from(input));
2254
+ } else if (typeof input === "string") {
2255
+ const value = input.trim();
2256
+ if (value.startsWith("base64:")) {
2257
+ candidates.push(Buffer.from(value.slice("base64:".length), "base64"));
2258
+ } else if (value.startsWith("base64url:")) {
2259
+ candidates.push(fromBase64Url2(value.slice("base64url:".length)));
2260
+ } else if (/^[0-9a-f]{64}$/i.test(value)) {
2261
+ candidates.push(Buffer.from(value, "hex"));
2262
+ } else {
2263
+ candidates.push(Buffer.from(value, "base64"));
2264
+ candidates.push(fromBase64Url2(value));
2265
+ candidates.push(Buffer.from(value, "utf8"));
2266
+ }
2267
+ }
2268
+ const key = candidates.find((candidate) => candidate.length === KEY_BYTES);
2269
+ if (!key) {
2270
+ throw new import_connect11.EdgeValidationError("Token vault key must decode to 32 bytes", {
2271
+ [fieldName]: ["Provide a 32-byte key as base64, base64url, hex, Buffer, or Uint8Array"]
2272
+ });
2273
+ }
2274
+ return key;
2275
+ }
2276
+ function validateEdgeTokens(tokens) {
2277
+ const errors = {};
2278
+ if (!tokens || typeof tokens !== "object") {
2279
+ errors.tokens = ["Expected token object"];
2280
+ } else {
2281
+ if (typeof tokens.accessToken !== "string" || !tokens.accessToken) {
2282
+ errors.accessToken = ["Required"];
2283
+ }
2284
+ if (typeof tokens.refreshToken !== "string" || !tokens.refreshToken) {
2285
+ errors.refreshToken = ["Required"];
2286
+ }
2287
+ if (typeof tokens.expiresIn !== "number" || !Number.isFinite(tokens.expiresIn) || tokens.expiresIn <= 0) {
2288
+ errors.expiresIn = ["Must be a positive number"];
2289
+ }
2290
+ if (typeof tokens.expiresAt !== "number" || !Number.isFinite(tokens.expiresAt) || tokens.expiresAt <= 0) {
2291
+ errors.expiresAt = ["Must be a positive Unix timestamp in milliseconds"];
2292
+ }
2293
+ if (typeof tokens.scope !== "string") {
2294
+ errors.scope = ["Required"];
2295
+ }
2296
+ if (tokens.idToken !== void 0 && typeof tokens.idToken !== "string") {
2297
+ errors.idToken = ["Must be a string when provided"];
2298
+ }
2299
+ }
2300
+ if (Object.keys(errors).length > 0) {
2301
+ throw new import_connect11.EdgeValidationError("Invalid EDGE token payload", errors);
2302
+ }
2303
+ }
2304
+ function parseEnvelope(envelope) {
2305
+ if (typeof envelope !== "string") {
2306
+ throw new import_connect11.EdgeValidationError("Token envelope must be a string", {
2307
+ tokenEnvelope: ["Expected string"]
2308
+ });
2309
+ }
2310
+ const parts = envelope.split(".");
2311
+ if (parts.length !== 5 || parts[0] !== TOKEN_VAULT_VERSION) {
2312
+ throw new import_connect11.EdgeValidationError("Invalid EDGE token vault envelope", {
2313
+ tokenEnvelope: [`Expected ${TOKEN_VAULT_VERSION}.<keyId>.<iv>.<tag>.<ciphertext>`]
2314
+ });
2315
+ }
2316
+ const [, encodedKeyId, encodedIv, encodedTag, encodedCiphertext] = parts;
2317
+ const keyId = fromBase64Url2(encodedKeyId).toString("utf8");
2318
+ const iv = fromBase64Url2(encodedIv);
2319
+ const tag = fromBase64Url2(encodedTag);
2320
+ const ciphertext = fromBase64Url2(encodedCiphertext);
2321
+ const errors = {};
2322
+ if (!keyId) errors.keyId = ["Required"];
2323
+ if (iv.length !== IV_BYTES) errors.iv = [`Must be ${IV_BYTES} bytes`];
2324
+ if (tag.length !== TAG_BYTES) errors.tag = [`Must be ${TAG_BYTES} bytes`];
2325
+ if (ciphertext.length === 0) errors.ciphertext = ["Required"];
2326
+ if (Object.keys(errors).length > 0) {
2327
+ throw new import_connect11.EdgeValidationError("Invalid EDGE token vault envelope", errors);
2328
+ }
2329
+ return { keyId, iv, tag, ciphertext };
2330
+ }
2331
+ function toBase64Url2(value) {
2332
+ return value.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
2333
+ }
2334
+ function fromBase64Url2(value) {
2335
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
2336
+ const padding = normalized.length % 4 === 0 ? "" : "=".repeat(4 - normalized.length % 4);
2337
+ return Buffer.from(normalized + padding, "base64");
2338
+ }
2339
+
2340
+ // src/webhook-guards.ts
2341
+ var import_connect12 = require("@edge-markets/connect");
2342
+ function isTransferWebhookEvent(event) {
2343
+ return event.type.startsWith("transfer.");
2344
+ }
2345
+ function assertTransferEventMatchesIntent(event, intent) {
2346
+ if (!isTransferWebhookEvent(event)) {
2347
+ throw new import_connect12.EdgeValidationError("Webhook event is not a transfer event", {
2348
+ type: [`Received ${event.type}`]
2349
+ });
2350
+ }
2351
+ const data = event.data;
2352
+ const errors = {};
2353
+ if (data.transferId !== intent.transferId) {
2354
+ errors.transferId = [`Expected ${intent.transferId}, received ${data.transferId}`];
2355
+ }
2356
+ if (intent.type && data.type !== intent.type) {
2357
+ errors.type = [`Expected ${intent.type}, received ${data.type}`];
2358
+ }
2359
+ if (intent.status && data.status !== intent.status) {
2360
+ errors.status = [`Expected ${intent.status}, received ${data.status}`];
2361
+ }
2362
+ if (intent.amount !== void 0) {
2363
+ const expectedAmount = normalizeMoneyAmount(intent.amount);
2364
+ const receivedAmount = normalizeMoneyAmount(data.amount);
2365
+ if (receivedAmount !== expectedAmount) {
2366
+ errors.amount = [`Expected ${expectedAmount}, received ${receivedAmount}`];
2367
+ }
2368
+ }
2369
+ if (Object.keys(errors).length > 0) {
2370
+ throw new import_connect12.EdgeValidationError("Webhook transfer event does not match expected transfer intent", errors);
2371
+ }
2372
+ }
2373
+ var assertWebhookMatchesTransfer = assertTransferEventMatchesIntent;
2374
+
2375
+ // src/webhook-inbox.ts
2376
+ var import_connect13 = require("@edge-markets/connect");
2377
+ var import_node_crypto4 = __toESM(require("crypto"));
2378
+ var DEFAULT_PROCESSING_TIMEOUT_MS = 10 * 60 * 1e3;
2379
+ var EdgeWebhookInbox = class {
2380
+ constructor(options) {
2381
+ if (!options.store?.insert || !options.store?.find || !options.store?.markProcessing) {
2382
+ throw new import_connect13.EdgeValidationError("EdgeWebhookInbox store is incomplete", {
2383
+ store: ["Provide insert, find, markProcessing, markProcessed, and markFailed callbacks"]
2384
+ });
2385
+ }
2386
+ if (!options.store.markProcessed || !options.store.markFailed) {
2387
+ throw new import_connect13.EdgeValidationError("EdgeWebhookInbox store is incomplete", {
2388
+ store: ["Provide markProcessed and markFailed callbacks"]
2389
+ });
2390
+ }
2391
+ if (!options.processEvent) {
2392
+ throw new import_connect13.EdgeValidationError("EdgeWebhookInbox processEvent callback is required", {
2393
+ processEvent: ["Required"]
2394
+ });
2395
+ }
2396
+ if (options.processingTimeoutMs !== void 0 && (!Number.isFinite(options.processingTimeoutMs) || options.processingTimeoutMs <= 0)) {
2397
+ throw new import_connect13.EdgeValidationError("processingTimeoutMs must be positive", {
2398
+ processingTimeoutMs: ["Must be greater than 0"]
2399
+ });
2400
+ }
2401
+ this.store = options.store;
2402
+ this.processEvent = options.processEvent;
2403
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
2404
+ this.storeRawBody = options.storeRawBody ?? false;
2405
+ this.processingTimeoutMs = options.processingTimeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
2406
+ this.onEvent = options.onEvent;
2407
+ }
2408
+ async accept(parsed) {
2409
+ return this.acceptEvent(parsed.event, {
2410
+ source: "live_webhook",
2411
+ receivedAt: parsed.receivedAt,
2412
+ signatureTimestamp: parsed.signatureTimestamp,
2413
+ signatureHeader: void 0,
2414
+ rawBody: void 0
2415
+ });
2416
+ }
2417
+ async acceptRaw(parsed, rawBody, signatureHeader) {
2418
+ return this.acceptEvent(parsed.event, {
2419
+ source: "live_webhook",
2420
+ receivedAt: parsed.receivedAt,
2421
+ signatureTimestamp: parsed.signatureTimestamp,
2422
+ signatureHeader,
2423
+ rawBody: this.storeRawBody ? rawBody : void 0
2424
+ });
2425
+ }
2426
+ async acceptSyncedEvent(event, receivedAt = this.now()) {
2427
+ return this.acceptEvent(event, {
2428
+ source: "sync",
2429
+ receivedAt,
2430
+ signatureTimestamp: null,
2431
+ signatureHeader: null,
2432
+ rawBody: null
2433
+ });
2434
+ }
2435
+ async process(eventId, options = {}) {
2436
+ const normalizedEventId = normalizeEventId(eventId);
2437
+ const existing = await this.store.find(normalizedEventId);
2438
+ if (!existing) {
2439
+ return { eventId: normalizedEventId, status: "failed", processed: false, skipped: true, reason: "not_found" };
2440
+ }
2441
+ if (existing.status === "processed" && !options.force) {
2442
+ return {
2443
+ eventId: normalizedEventId,
2444
+ status: existing.status,
2445
+ processed: false,
2446
+ skipped: true,
2447
+ reason: "already_processed",
2448
+ record: existing
2449
+ };
2450
+ }
2451
+ const now = this.now();
2452
+ const processing = await this.store.markProcessing(normalizedEventId, { now });
2453
+ if (!processing) {
2454
+ return {
2455
+ eventId: normalizedEventId,
2456
+ status: existing.status,
2457
+ processed: false,
2458
+ skipped: true,
2459
+ reason: "locked",
2460
+ record: existing
2461
+ };
2462
+ }
2463
+ await this.emit({
2464
+ type: "webhook.processing_started",
2465
+ eventId: normalizedEventId,
2466
+ eventType: processing.eventType,
2467
+ status: "processing",
2468
+ source: processing.source,
2469
+ attempts: processing.attempts
2470
+ });
2471
+ try {
2472
+ await this.processEvent(processing.event, processing);
2473
+ const processed = await this.store.markProcessed(normalizedEventId, { now: this.now() }) ?? processing;
2474
+ await this.emit({
2475
+ type: "webhook.processing_succeeded",
2476
+ eventId: normalizedEventId,
2477
+ eventType: processing.eventType,
2478
+ status: "processed",
2479
+ source: processing.source,
2480
+ attempts: processed.attempts
2481
+ });
2482
+ return {
2483
+ eventId: normalizedEventId,
2484
+ status: "processed",
2485
+ processed: true,
2486
+ skipped: false,
2487
+ record: processed
2488
+ };
2489
+ } catch (error) {
2490
+ const message = error instanceof Error ? error.message : "Unknown webhook processing error";
2491
+ const failed = await this.store.markFailed(normalizedEventId, { now: this.now(), error: message }) ?? processing;
2492
+ await this.emit({
2493
+ type: "webhook.processing_failed",
2494
+ eventId: normalizedEventId,
2495
+ eventType: processing.eventType,
2496
+ status: "failed",
2497
+ source: processing.source,
2498
+ attempts: failed.attempts,
2499
+ error: message
2500
+ });
2501
+ throw error;
2502
+ }
2503
+ }
2504
+ async replay(eventId) {
2505
+ return this.process(eventId, { force: true });
2506
+ }
2507
+ async recoverStaleProcessing() {
2508
+ if (!this.store.list) {
2509
+ return { recovered: 0, skipped: true, reason: "list_not_configured" };
2510
+ }
2511
+ if (!this.store.markPending) {
2512
+ return { recovered: 0, skipped: true, reason: "mark_pending_not_configured" };
2513
+ }
2514
+ const updatedBefore = new Date(this.now().getTime() - this.processingTimeoutMs);
2515
+ const records = await this.store.list({ status: "processing", updatedBefore, limit: 100 });
2516
+ let recovered = 0;
2517
+ for (const record of records) {
2518
+ const next = await this.store.markPending(record.eventId, {
2519
+ now: this.now(),
2520
+ reason: "stale_processing_recovery"
2521
+ });
2522
+ if (next) {
2523
+ recovered++;
2524
+ await this.emit({
2525
+ type: "webhook.recovered_stale",
2526
+ eventId: record.eventId,
2527
+ eventType: record.eventType,
2528
+ status: next.status,
2529
+ source: record.source,
2530
+ attempts: next.attempts
2531
+ });
2532
+ }
2533
+ }
2534
+ return { recovered, skipped: false };
2535
+ }
2536
+ async acceptEvent(event, options) {
2537
+ const payloadFingerprint = fingerprintWebhookEvent(event);
2538
+ const result = await this.store.insert({
2539
+ eventId: event.id,
2540
+ eventType: event.type,
2541
+ event,
2542
+ payloadFingerprint,
2543
+ source: options.source,
2544
+ receivedAt: options.receivedAt,
2545
+ signatureTimestamp: options.signatureTimestamp ?? null,
2546
+ signatureHeader: options.signatureHeader ?? null,
2547
+ rawBody: options.rawBody ?? null
2548
+ });
2549
+ if (result.record.payloadFingerprint !== payloadFingerprint) {
2550
+ await this.emit({
2551
+ type: "webhook.payload_mismatch",
2552
+ eventId: event.id,
2553
+ eventType: event.type,
2554
+ status: result.record.status,
2555
+ source: options.source
2556
+ });
2557
+ throw new import_connect13.EdgeValidationError("Duplicate webhook event payload does not match stored event", {
2558
+ eventId: [event.id]
2559
+ });
2560
+ }
2561
+ await this.emit({
2562
+ type: result.inserted ? "webhook.accepted" : "webhook.duplicate",
2563
+ eventId: event.id,
2564
+ eventType: event.type,
2565
+ status: result.record.status,
2566
+ source: options.source,
2567
+ attempts: result.record.attempts
2568
+ });
2569
+ return {
2570
+ eventId: event.id,
2571
+ eventType: event.type,
2572
+ inserted: result.inserted,
2573
+ duplicate: !result.inserted,
2574
+ status: result.record.status,
2575
+ record: result.record
2576
+ };
2577
+ }
2578
+ async emit(event) {
2579
+ try {
2580
+ await this.onEvent?.(event);
2581
+ } catch {
2582
+ }
2583
+ }
2584
+ };
2585
+ function createWebhookInbox(options) {
2586
+ return new EdgeWebhookInbox(options);
2587
+ }
2588
+ function fingerprintWebhookEvent(event) {
2589
+ return import_node_crypto4.default.createHash("sha256").update(stableStringify(event)).digest("hex");
2590
+ }
2591
+ function normalizeEventId(eventId) {
2592
+ if (typeof eventId !== "string" || !eventId.trim()) {
2593
+ throw new import_connect13.EdgeValidationError("Webhook event ID is required", {
2594
+ eventId: ["Required"]
2595
+ });
2596
+ }
2597
+ return eventId.trim();
2598
+ }
2599
+ function stableStringify(value) {
2600
+ if (Array.isArray(value)) {
2601
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
2602
+ }
2603
+ if (value && typeof value === "object") {
2604
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
2605
+ }
2606
+ return JSON.stringify(value);
2607
+ }
2608
+
853
2609
  // src/index.ts
854
- var import_connect3 = require("@edge-markets/connect");
855
- var import_connect4 = require("@edge-markets/connect");
2610
+ var import_connect14 = require("@edge-markets/connect");
2611
+ var import_connect15 = require("@edge-markets/connect");
856
2612
  // Annotate the CommonJS export names for ESM import in node:
857
2613
  0 && (module.exports = {
2614
+ CONNECT_TRANSFER_DIRECTIONS,
858
2615
  EDGE_WEBHOOK_EVENT_TYPES,
859
2616
  EdgeApiError,
860
2617
  EdgeAuthenticationError,
@@ -866,15 +2623,54 @@ var import_connect4 = require("@edge-markets/connect");
866
2623
  EdgeNetworkError,
867
2624
  EdgeNotFoundError,
868
2625
  EdgeTokenExchangeError,
2626
+ EdgeTokenVault,
869
2627
  EdgeUserClient,
2628
+ EdgeUserSession,
2629
+ EdgeValidationError,
2630
+ EdgeWebhookInbox,
2631
+ EdgeWebhookReconciler,
870
2632
  TRANSFER_CATEGORIES,
2633
+ assertSameTransferIntent,
2634
+ assertTransferEventMatchesIntent,
2635
+ assertWebhookMatchesTransfer,
2636
+ buildVerifyIdentityPayload,
2637
+ createEdgeConnectServerFromEnv,
2638
+ createEdgeTokenVault,
2639
+ createEdgeUserSession,
2640
+ createSessionTokenRecord,
2641
+ createTransferIdempotencyKey,
2642
+ createWebhookInbox,
2643
+ createWebhookReconciler,
2644
+ encryptLegacySessionTokens,
2645
+ evaluateIdentityResult,
2646
+ extractWebhookSignatureTimestamp,
2647
+ fingerprintTransferIntent,
2648
+ fingerprintWebhookEvent,
2649
+ getConnectTransferDirection,
2650
+ getEdgeWebhookRawBody,
2651
+ getEdgeWebhookSignatureHeader,
871
2652
  getEnvironmentConfig,
872
2653
  isApiError,
873
2654
  isAuthenticationError,
874
2655
  isConsentRequiredError,
875
2656
  isEdgeError,
2657
+ isEdgeTokenVaultEnvelope,
2658
+ isEdgeWebhookEvent,
876
2659
  isIdentityVerificationError,
877
2660
  isNetworkError,
878
2661
  isProductionEnvironment,
2662
+ isTransferWebhookEvent,
2663
+ mapTransferStatusToPartnerState,
2664
+ moneyAmountsEqual,
2665
+ normalizeMoneyAmount,
2666
+ normalizeSessionExpiresAt,
2667
+ parseAndVerifyWebhook,
2668
+ parseSessionTokenRecord,
2669
+ parseWebhookHttpRequest,
2670
+ startTransferVerification,
2671
+ toEdgeHttpError,
2672
+ toEdgeProblemJson,
2673
+ validateEdgeWebhookEvent,
2674
+ validateTransferAmount,
879
2675
  verifyWebhookSignature
880
2676
  });