@edge-markets/connect-node 1.6.0 → 1.7.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.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as node_https from 'node:https';
2
- import { EdgeEnvironment, TransferCategory, SdkGeolocation, User, VerifyIdentityOptions, VerifyIdentityResult, Balance, Transfer, ListTransfersParams, TransferList, CreateVerificationSessionRequest, VerificationSession, VerificationSessionStatusResponse, EdgeTokens } from '@edge-markets/connect';
3
- export { Balance, CreateVerificationSessionRequest, EdgeApiError, EdgeAuthenticationError, EdgeConsentRequiredError, EdgeEnvironment, EdgeError, EdgeIdentityVerificationError, EdgeInsufficientScopeError, EdgeNetworkError, EdgeNotFoundError, EdgeTokenExchangeError, EdgeTokens, ListTransfersParams, SdkGeolocation, TRANSFER_CATEGORIES, Transfer, TransferCategory, TransferList, TransferListItem, TransferStatus, TransferType, User, UserAddress, VerificationSession, VerificationSessionStatus, VerificationSessionStatusResponse, VerifyIdentityAddress, VerifyIdentityOptions, VerifyIdentityResult, VerifyIdentityScores, getEnvironmentConfig, isApiError, isAuthenticationError, isConsentRequiredError, isEdgeError, isIdentityVerificationError, isNetworkError, isProductionEnvironment } from '@edge-markets/connect';
2
+ import { EdgeEnvironment, TransferCategory, User, VerifyIdentityOptions, VerifyIdentityResult, Balance, Transfer, ListTransfersParams, TransferList, CreateVerificationSessionRequest, VerificationSession, VerificationSessionStatusResponse, EdgeTokens, EdgeWebhookEvent } from '@edge-markets/connect';
3
+ export { Balance, BaseWebhookEvent, ConsentRevokedEventData, CreateVerificationSessionRequest, EDGE_WEBHOOK_EVENT_TYPES, EdgeApiError, EdgeAuthenticationError, EdgeConsentRequiredError, EdgeEnvironment, EdgeError, EdgeIdentityVerificationError, EdgeInsufficientScopeError, EdgeNetworkError, EdgeNotFoundError, EdgeTokenExchangeError, EdgeTokens, EdgeWebhookEvent, EdgeWebhookEventType, ListTransfersParams, SdkGeolocation, TRANSFER_CATEGORIES, Transfer, TransferCategory, TransferCompletedEventData, TransferExpiredEventData, TransferFailedEventData, TransferList, TransferListItem, TransferProcessingEventData, TransferStatus, TransferType, User, UserAddress, VerificationSession, VerificationSessionStatus, VerificationSessionStatusResponse, VerifyIdentityAddress, VerifyIdentityOptions, VerifyIdentityResult, VerifyIdentityScores, getEnvironmentConfig, isApiError, isAuthenticationError, isConsentRequiredError, isEdgeError, isIdentityVerificationError, isNetworkError, isProductionEnvironment } from '@edge-markets/connect';
4
4
 
5
5
  interface RequestInfo {
6
6
  method: string;
@@ -42,6 +42,24 @@ interface EdgeConnectServerConfig {
42
42
  strictResponseEncryption?: boolean;
43
43
  };
44
44
  mtls?: MtlsConfig;
45
+ /**
46
+ * Partner-level OAuth client ID for the `client_credentials` grant.
47
+ *
48
+ * Required ONLY when calling {@link EdgeConnectServer.syncWebhookEvents}.
49
+ * This is a different OAuth client than {@link clientId} — it authenticates
50
+ * your backend (not your users) for partner-scoped surfaces like the
51
+ * webhook reconciliation sync endpoint.
52
+ *
53
+ * If omitted, `syncWebhookEvents()` throws on first call but the rest of
54
+ * the SDK (token exchange, user-scoped API calls) continues to work.
55
+ */
56
+ partnerClientId?: string;
57
+ /**
58
+ * Partner-level OAuth client secret paired with {@link partnerClientId}.
59
+ *
60
+ * Required ONLY when calling {@link EdgeConnectServer.syncWebhookEvents}.
61
+ */
62
+ partnerClientSecret?: string;
45
63
  }
46
64
  interface MtlsConfig {
47
65
  enabled: boolean;
@@ -56,13 +74,6 @@ interface TransferOptions {
56
74
  /** Optional transaction category for settlement window determination */
57
75
  category?: TransferCategory;
58
76
  }
59
- /**
60
- * Options for verifying a transfer with OTP.
61
- */
62
- interface VerifyTransferOptions {
63
- /** SDK-reported geolocation for cross-referencing against IP-based geo */
64
- sdkGeo?: SdkGeolocation;
65
- }
66
77
 
67
78
  /**
68
79
  * A lightweight, user-scoped API client created via {@link EdgeConnectServer.forUser}.
@@ -78,7 +89,6 @@ declare class EdgeUserClient {
78
89
  verifyIdentity(options: VerifyIdentityOptions): Promise<VerifyIdentityResult>;
79
90
  getBalance(): Promise<Balance>;
80
91
  initiateTransfer(options: TransferOptions): Promise<Transfer>;
81
- verifyTransfer(transferId: string, otp: string, options?: VerifyTransferOptions): Promise<Transfer>;
82
92
  getTransfer(transferId: string): Promise<Transfer>;
83
93
  listTransfers(params?: ListTransfersParams): Promise<TransferList>;
84
94
  revokeConsent(): Promise<{
@@ -97,6 +107,38 @@ declare class EdgeUserClient {
97
107
  getVerificationSessionStatus(transferId: string, sessionId: string): Promise<VerificationSessionStatusResponse>;
98
108
  }
99
109
 
110
+ /**
111
+ * Options for {@link EdgeConnectServer.syncWebhookEvents}.
112
+ */
113
+ interface SyncWebhookEventsOptions {
114
+ /**
115
+ * Cursor token. Pass the `id` of the last event you successfully processed
116
+ * to receive only newer events. Omit on the first call to start from the
117
+ * beginning of the 30-day server-side lookback window.
118
+ */
119
+ afterEventId?: string;
120
+ /**
121
+ * Maximum events per page (1–100). Defaults to 20 server-side.
122
+ */
123
+ limit?: number;
124
+ }
125
+ /**
126
+ * Result of {@link EdgeConnectServer.syncWebhookEvents}. Field names are
127
+ * normalized to camelCase from the wire format (`has_more` → `hasMore`).
128
+ */
129
+ interface SyncWebhookEventsResult {
130
+ /**
131
+ * Events newer than the supplied cursor, in ascending order. Each event
132
+ * is the unwrapped {@link EdgeWebhookEvent} payload (the SDK strips the
133
+ * sync API's outer envelope).
134
+ */
135
+ events: EdgeWebhookEvent[];
136
+ /**
137
+ * `true` if more events are available beyond this page. Loop while this
138
+ * is truthy, advancing your cursor to the `id` of the last processed event.
139
+ */
140
+ hasMore: boolean;
141
+ }
100
142
  declare class EdgeConnectServer {
101
143
  private readonly config;
102
144
  private readonly apiBaseUrl;
@@ -105,6 +147,7 @@ declare class EdgeConnectServer {
105
147
  private readonly retryConfig;
106
148
  private readonly httpsAgent;
107
149
  private readonly dispatcher;
150
+ private partnerTokenCache;
108
151
  static getInstance(config: EdgeConnectServerConfig): EdgeConnectServer;
109
152
  static clearInstances(): void;
110
153
  constructor(config: EdgeConnectServerConfig);
@@ -124,6 +167,67 @@ declare class EdgeConnectServer {
124
167
  refreshTokens(refreshToken: string): Promise<EdgeTokens>;
125
168
  /** @internal Called by {@link EdgeUserClient} — not part of the public API. */
126
169
  _apiRequest<T>(method: string, path: string, accessToken: string, body?: unknown): Promise<T>;
170
+ /**
171
+ * Reconcile webhook events from the partner sync endpoint.
172
+ *
173
+ * Use this from a scheduled cron (every ~5 minutes is typical) to catch
174
+ * events your primary webhook receiver missed — partner downtime, retry
175
+ * exhaustion, dead-letter queue. This is the third reliability layer
176
+ * ("Leg 3") on top of HTTP delivery + DLQ.
177
+ *
178
+ * **Authentication.** This method uses the OAuth `client_credentials`
179
+ * grant against {@link EdgeConnectServerConfig.partnerClientId} and
180
+ * {@link EdgeConnectServerConfig.partnerClientSecret}. The token is
181
+ * cached internally and refreshed automatically.
182
+ *
183
+ * **Pagination.** Pass `afterEventId` with the last `id` you processed.
184
+ * Loop while `hasMore` is true. The server enforces a 30-day lookback;
185
+ * cursors older than 30 days return events from 30 days ago, not an error.
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * let cursor: string | undefined = await loadCursorFromDb()
190
+ *
191
+ * while (true) {
192
+ * const { events, hasMore } = await edge.syncWebhookEvents({ afterEventId: cursor })
193
+ * for (const event of events) {
194
+ * await processEvent(event)
195
+ * cursor = event.id
196
+ * }
197
+ * if (events.length > 0) await saveCursorToDb(cursor!)
198
+ * if (!hasMore) break
199
+ * }
200
+ * ```
201
+ *
202
+ * @throws {EdgeAuthenticationError} If partner credentials are missing
203
+ * or the partner token cannot be obtained / refreshed
204
+ * @throws {EdgeNetworkError} On network failure
205
+ * @throws {EdgeApiError} For other non-2xx responses after retries
206
+ */
207
+ syncWebhookEvents(options?: SyncWebhookEventsOptions): Promise<SyncWebhookEventsResult>;
208
+ /**
209
+ * Builds the sync URL with normalized query parameters.
210
+ */
211
+ private buildSyncUrl;
212
+ /**
213
+ * Issues a GET to the sync endpoint with the partner Bearer token. Kept
214
+ * separate from `_apiRequest` because partner tokens have a different
215
+ * lifecycle (process-wide, client_credentials) than user tokens.
216
+ */
217
+ private partnerFetch;
218
+ /**
219
+ * Fetches (or returns the cached) partner token via the
220
+ * `client_credentials` grant. Caches with a 60s safety margin so the
221
+ * token is renewed *before* the server-side expiry, never racing it.
222
+ *
223
+ * Side-effects: writes `this.partnerTokenCache` on success.
224
+ */
225
+ private getPartnerToken;
226
+ /**
227
+ * @internal Test-only: clears the cached partner token so the next call
228
+ * to `syncWebhookEvents` re-fetches it. Not part of the public API.
229
+ */
230
+ _resetPartnerTokenCache(): void;
127
231
  private getRetryDelay;
128
232
  private sleep;
129
233
  private fetchWithTimeout;
@@ -132,4 +236,68 @@ declare class EdgeConnectServer {
132
236
  private handleApiErrorFromBody;
133
237
  }
134
238
 
135
- export { EdgeConnectServer, type EdgeConnectServerConfig, EdgeUserClient, type MtlsConfig, type RequestInfo, type ResponseInfo, type RetryConfig, type TransferOptions, type VerifyTransferOptions };
239
+ /**
240
+ * Webhook Signature Verification (SDK-001)
241
+ *
242
+ * HMAC-SHA256 verification for EDGE Connect webhook payloads.
243
+ *
244
+ * Signature header format: `t={timestamp},v1={hex_hmac}`
245
+ * Signed message: `{timestamp}.{raw_json_body}`
246
+ *
247
+ * The timestamp prefix prevents replay attacks — events older than the
248
+ * tolerance window (default 5 minutes) are rejected. The HMAC is verified
249
+ * with constant-time comparison to prevent timing attacks.
250
+ */
251
+ /**
252
+ * Options for {@link verifyWebhookSignature}.
253
+ */
254
+ interface VerifyWebhookSignatureOptions {
255
+ /**
256
+ * Maximum age of the signature timestamp, in seconds.
257
+ *
258
+ * Signatures older than this are rejected as potential replays.
259
+ * Defaults to 300 seconds (5 minutes), matching the producer.
260
+ */
261
+ toleranceSeconds?: number;
262
+ }
263
+ /**
264
+ * Verifies an EDGE Connect webhook signature.
265
+ *
266
+ * **Always pass the raw request body bytes** — never `JSON.stringify(req.body)`.
267
+ * Re-stringifying after the framework parses JSON will reorder keys, change
268
+ * whitespace, and silently break verification with no diagnostic. Capture
269
+ * the raw body via your framework's raw-body middleware:
270
+ *
271
+ * - Express: `express.raw({ type: 'application/json' })`
272
+ * - Fastify: built-in (use `request.rawBody`)
273
+ * - NestJS: `{ rawBody: true }` on `NestFactory.create` + `RawBodyRequest<Request>`
274
+ *
275
+ * @param header - The `X-Edge-Signature` header value (`t=...,v1=...`)
276
+ * @param body - The raw request body as a UTF-8 string (NOT a parsed object)
277
+ * @param secret - The partner's webhook signing secret
278
+ * @param options - Optional overrides
279
+ * @returns `true` if the signature is valid and within the tolerance window,
280
+ * `false` otherwise. Never throws — malformed input returns `false`.
281
+ *
282
+ * @example
283
+ * ```typescript
284
+ * import { verifyWebhookSignature } from '@edge-markets/connect-node'
285
+ *
286
+ * app.post('/webhooks/edge', express.raw({ type: 'application/json' }), (req, res) => {
287
+ * const ok = verifyWebhookSignature(
288
+ * req.headers['x-edge-signature'] as string,
289
+ * req.body.toString('utf8'),
290
+ * process.env.EDGE_WEBHOOK_SECRET!,
291
+ * )
292
+ *
293
+ * if (!ok) return res.status(401).send('invalid signature')
294
+ *
295
+ * const event = JSON.parse(req.body.toString('utf8'))
296
+ * // ... process event
297
+ * res.status(200).send('ok')
298
+ * })
299
+ * ```
300
+ */
301
+ declare function verifyWebhookSignature(header: string, body: string, secret: string, options?: VerifyWebhookSignatureOptions): boolean;
302
+
303
+ export { EdgeConnectServer, type EdgeConnectServerConfig, EdgeUserClient, type MtlsConfig, type RequestInfo, type ResponseInfo, type RetryConfig, type SyncWebhookEventsOptions, type SyncWebhookEventsResult, type TransferOptions, type VerifyWebhookSignatureOptions, verifyWebhookSignature };
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ 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,
33
34
  EdgeApiError: () => import_connect3.EdgeApiError,
34
35
  EdgeAuthenticationError: () => import_connect3.EdgeAuthenticationError,
35
36
  EdgeConnectServer: () => EdgeConnectServer,
@@ -49,7 +50,8 @@ __export(index_exports, {
49
50
  isEdgeError: () => import_connect3.isEdgeError,
50
51
  isIdentityVerificationError: () => import_connect3.isIdentityVerificationError,
51
52
  isNetworkError: () => import_connect3.isNetworkError,
52
- isProductionEnvironment: () => import_connect4.isProductionEnvironment
53
+ isProductionEnvironment: () => import_connect4.isProductionEnvironment,
54
+ verifyWebhookSignature: () => verifyWebhookSignature
53
55
  });
54
56
  module.exports = __toCommonJS(index_exports);
55
57
 
@@ -58,6 +60,14 @@ var import_connect2 = require("@edge-markets/connect");
58
60
 
59
61
  // src/validators.ts
60
62
  var import_connect = require("@edge-markets/connect");
63
+ 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
+ function validateTransferId(transferId) {
65
+ if (typeof transferId !== "string" || !UUID_V4_REGEX.test(transferId)) {
66
+ throw new import_connect.EdgeValidationError("transferId must be a UUID v4", {
67
+ transferId: ["Must be a valid UUID v4"]
68
+ });
69
+ }
70
+ }
61
71
  function validateVerifyIdentityOptions(options) {
62
72
  const errors = {};
63
73
  const firstName = typeof options.firstName === "string" ? options.firstName.trim() : "";
@@ -134,19 +144,8 @@ var EdgeUserClient = class {
134
144
  }
135
145
  return this.server._apiRequest("POST", "/transfer", this.accessToken, body);
136
146
  }
137
- async verifyTransfer(transferId, otp, options) {
138
- const body = { otp };
139
- if (options?.sdkGeo) {
140
- body.sdkGeo = options.sdkGeo;
141
- }
142
- return this.server._apiRequest(
143
- "POST",
144
- `/transfer/${encodeURIComponent(transferId)}/verify`,
145
- this.accessToken,
146
- body
147
- );
148
- }
149
147
  async getTransfer(transferId) {
148
+ validateTransferId(transferId);
150
149
  return this.server._apiRequest("GET", `/transfer/${encodeURIComponent(transferId)}`, this.accessToken);
151
150
  }
152
151
  async listTransfers(params) {
@@ -167,6 +166,7 @@ var EdgeUserClient = class {
167
166
  * The handoff token is single-use and expires in 120 seconds.
168
167
  */
169
168
  async createVerificationSession(transferId, options) {
169
+ validateTransferId(transferId);
170
170
  return this.server._apiRequest(
171
171
  "POST",
172
172
  `/transfer/${encodeURIComponent(transferId)}/verification-session`,
@@ -179,6 +179,7 @@ var EdgeUserClient = class {
179
179
  * Use as an alternative to postMessage events.
180
180
  */
181
181
  async getVerificationSessionStatus(transferId, sessionId) {
182
+ validateTransferId(transferId);
182
183
  return this.server._apiRequest(
183
184
  "GET",
184
185
  `/transfer/${encodeURIComponent(transferId)}/verification-session/${encodeURIComponent(sessionId)}`,
@@ -308,19 +309,31 @@ var instances = /* @__PURE__ */ new Map();
308
309
  function getInstanceKey(config) {
309
310
  return `${config.clientId}:${config.environment}`;
310
311
  }
311
- var EdgeConnectServer = class _EdgeConnectServer {
312
- static getInstance(config) {
313
- const key = getInstanceKey(config);
314
- const existing = instances.get(key);
315
- if (existing) return existing;
316
- const instance = new _EdgeConnectServer(config);
317
- instances.set(key, instance);
318
- return instance;
312
+ var MTLS_ENFORCED_HOSTS = /* @__PURE__ */ new Set(["connect-staging.edgeboost.io", "api.edgeboost.com"]);
313
+ function isMtlsEnforcedHost(host) {
314
+ if (MTLS_ENFORCED_HOSTS.has(host)) {
315
+ return true;
316
+ }
317
+ for (const enforced of MTLS_ENFORCED_HOSTS) {
318
+ if (host.endsWith("." + enforced)) {
319
+ return true;
320
+ }
319
321
  }
320
- static clearInstances() {
321
- instances.clear();
322
+ return false;
323
+ }
324
+ function environmentRequiresMtls(environment, apiBaseUrl) {
325
+ if (environment === "staging" || environment === "production") {
326
+ return true;
327
+ }
328
+ try {
329
+ return isMtlsEnforcedHost(new URL(apiBaseUrl).host);
330
+ } catch {
331
+ return false;
322
332
  }
333
+ }
334
+ var EdgeConnectServer = class _EdgeConnectServer {
323
335
  constructor(config) {
336
+ this.partnerTokenCache = null;
324
337
  if (!config.clientId) {
325
338
  throw new Error("EdgeConnectServer: clientId is required");
326
339
  }
@@ -343,8 +356,23 @@ var EdgeConnectServer = class _EdgeConnectServer {
343
356
  validateMtlsConfig(config.mtls);
344
357
  this.httpsAgent = createHttpsAgent(config.mtls);
345
358
  this.dispatcher = createUndiciDispatcher(config.mtls);
359
+ } else if (environmentRequiresMtls(config.environment, this.apiBaseUrl)) {
360
+ console.warn(
361
+ `[EdgeConnectServer] environment='${config.environment}' is served from an mTLS-enforced host (${this.apiBaseUrl}) but no mtls config was provided. Requests will fail at the TLS handshake with a connection reset. Pass mtls: { enabled: true, cert, key, ca? } to EdgeConnectServer or coordinate with EDGE DevRel to issue a partner client certificate.`
362
+ );
346
363
  }
347
364
  }
365
+ static getInstance(config) {
366
+ const key = getInstanceKey(config);
367
+ const existing = instances.get(key);
368
+ if (existing) return existing;
369
+ const instance = new _EdgeConnectServer(config);
370
+ instances.set(key, instance);
371
+ return instance;
372
+ }
373
+ static clearInstances() {
374
+ instances.clear();
375
+ }
348
376
  /**
349
377
  * Returns the `node:https.Agent` configured with mTLS client certificates.
350
378
  * Use this with axios, got, or other HTTP clients. Returns `undefined` if mTLS is not configured.
@@ -515,6 +543,175 @@ var EdgeConnectServer = class _EdgeConnectServer {
515
543
  lastError
516
544
  );
517
545
  }
546
+ /**
547
+ * Reconcile webhook events from the partner sync endpoint.
548
+ *
549
+ * Use this from a scheduled cron (every ~5 minutes is typical) to catch
550
+ * events your primary webhook receiver missed — partner downtime, retry
551
+ * exhaustion, dead-letter queue. This is the third reliability layer
552
+ * ("Leg 3") on top of HTTP delivery + DLQ.
553
+ *
554
+ * **Authentication.** This method uses the OAuth `client_credentials`
555
+ * grant against {@link EdgeConnectServerConfig.partnerClientId} and
556
+ * {@link EdgeConnectServerConfig.partnerClientSecret}. The token is
557
+ * cached internally and refreshed automatically.
558
+ *
559
+ * **Pagination.** Pass `afterEventId` with the last `id` you processed.
560
+ * Loop while `hasMore` is true. The server enforces a 30-day lookback;
561
+ * cursors older than 30 days return events from 30 days ago, not an error.
562
+ *
563
+ * @example
564
+ * ```typescript
565
+ * let cursor: string | undefined = await loadCursorFromDb()
566
+ *
567
+ * while (true) {
568
+ * const { events, hasMore } = await edge.syncWebhookEvents({ afterEventId: cursor })
569
+ * for (const event of events) {
570
+ * await processEvent(event)
571
+ * cursor = event.id
572
+ * }
573
+ * if (events.length > 0) await saveCursorToDb(cursor!)
574
+ * if (!hasMore) break
575
+ * }
576
+ * ```
577
+ *
578
+ * @throws {EdgeAuthenticationError} If partner credentials are missing
579
+ * or the partner token cannot be obtained / refreshed
580
+ * @throws {EdgeNetworkError} On network failure
581
+ * @throws {EdgeApiError} For other non-2xx responses after retries
582
+ */
583
+ async syncWebhookEvents(options = {}) {
584
+ if (!this.config.partnerClientId) {
585
+ throw new import_connect2.EdgeAuthenticationError("syncWebhookEvents requires `partnerClientId` in EdgeConnectServer config");
586
+ }
587
+ if (!this.config.partnerClientSecret) {
588
+ throw new import_connect2.EdgeAuthenticationError("syncWebhookEvents requires `partnerClientSecret` in EdgeConnectServer config");
589
+ }
590
+ const url = this.buildSyncUrl(options);
591
+ let token = await this.getPartnerToken();
592
+ let response = await this.partnerFetch(url, token);
593
+ if (response.status === 401) {
594
+ this.partnerTokenCache = null;
595
+ token = await this.getPartnerToken();
596
+ response = await this.partnerFetch(url, token);
597
+ if (response.status === 401) {
598
+ const body = await response.json().catch(() => ({}));
599
+ throw new import_connect2.EdgeAuthenticationError(
600
+ body.message || "Partner token rejected after refresh \u2014 check partnerClientId/Secret",
601
+ body
602
+ );
603
+ }
604
+ }
605
+ if (!response.ok) {
606
+ const body = await response.json().catch(() => ({}));
607
+ throw new import_connect2.EdgeApiError(
608
+ body.error || "sync_failed",
609
+ body.message || `syncWebhookEvents failed with status ${response.status}`,
610
+ response.status,
611
+ body
612
+ );
613
+ }
614
+ const wire = await response.json().catch(() => ({}));
615
+ const events = (wire.events ?? []).map((entry) => entry.payload).filter((p) => !!p);
616
+ return {
617
+ events,
618
+ hasMore: !!wire.has_more
619
+ };
620
+ }
621
+ /**
622
+ * Builds the sync URL with normalized query parameters.
623
+ */
624
+ buildSyncUrl(options) {
625
+ const url = new URL(`${this.apiBaseUrl}/partner/webhooks/events/sync`);
626
+ if (options.afterEventId) {
627
+ url.searchParams.set("after_event_id", options.afterEventId);
628
+ }
629
+ if (options.limit !== void 0) {
630
+ url.searchParams.set("limit", String(options.limit));
631
+ }
632
+ return url.toString();
633
+ }
634
+ /**
635
+ * Issues a GET to the sync endpoint with the partner Bearer token. Kept
636
+ * separate from `_apiRequest` because partner tokens have a different
637
+ * lifecycle (process-wide, client_credentials) than user tokens.
638
+ */
639
+ async partnerFetch(url, token) {
640
+ try {
641
+ return await this.fetchWithTimeout(
642
+ 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);
655
+ }
656
+ }
657
+ /**
658
+ * Fetches (or returns the cached) partner token via the
659
+ * `client_credentials` grant. Caches with a 60s safety margin so the
660
+ * token is renewed *before* the server-side expiry, never racing it.
661
+ *
662
+ * Side-effects: writes `this.partnerTokenCache` on success.
663
+ */
664
+ async getPartnerToken() {
665
+ const cached = this.partnerTokenCache;
666
+ if (cached && Date.now() < cached.expiresAt) {
667
+ return cached.token;
668
+ }
669
+ const tokenUrl = `${this.oauthBaseUrl}/token`;
670
+ let response;
671
+ try {
672
+ response = await this.fetchWithTimeout(
673
+ tokenUrl,
674
+ {
675
+ method: "POST",
676
+ headers: {
677
+ "Content-Type": "application/x-www-form-urlencoded",
678
+ "User-Agent": USER_AGENT
679
+ },
680
+ body: new URLSearchParams({
681
+ grant_type: "client_credentials",
682
+ client_id: this.config.partnerClientId,
683
+ client_secret: this.config.partnerClientSecret
684
+ }).toString()
685
+ },
686
+ this.dispatcher
687
+ );
688
+ } catch (error) {
689
+ throw new import_connect2.EdgeNetworkError("Partner token request network failure", error);
690
+ }
691
+ 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
696
+ );
697
+ }
698
+ const data = await response.json().catch(() => ({}));
699
+ if (!data.access_token || typeof data.expires_in !== "number") {
700
+ throw new import_connect2.EdgeAuthenticationError("Partner token response missing access_token or expires_in");
701
+ }
702
+ this.partnerTokenCache = {
703
+ token: data.access_token,
704
+ expiresAt: Date.now() + (data.expires_in - 60) * 1e3
705
+ };
706
+ return data.access_token;
707
+ }
708
+ /**
709
+ * @internal Test-only: clears the cached partner token so the next call
710
+ * to `syncWebhookEvents` re-fetches it. Not part of the public API.
711
+ */
712
+ _resetPartnerTokenCache() {
713
+ this.partnerTokenCache = null;
714
+ }
518
715
  getRetryDelay(attempt) {
519
716
  const { backoff, baseDelayMs } = this.retryConfig;
520
717
  if (backoff === "exponential") {
@@ -624,11 +821,41 @@ var EdgeConnectServer = class _EdgeConnectServer {
624
821
  }
625
822
  };
626
823
 
824
+ // src/webhook-signing.ts
825
+ var import_node_crypto = __toESM(require("crypto"));
826
+ function verifyWebhookSignature(header, body, secret, options = {}) {
827
+ if (typeof header !== "string" || typeof body !== "string" || typeof secret !== "string") {
828
+ return false;
829
+ }
830
+ const requestedTolerance = options.toleranceSeconds;
831
+ const toleranceSeconds = typeof requestedTolerance === "number" && Number.isFinite(requestedTolerance) && requestedTolerance >= 0 ? requestedTolerance : 300;
832
+ const parts = header.split(",").map((p) => p.trim());
833
+ const tPart = parts.find((p) => p.startsWith("t="));
834
+ const v1Part = parts.find((p) => p.startsWith("v1="));
835
+ if (!tPart || !v1Part) return false;
836
+ const timestamp = parseInt(tPart.slice(2), 10);
837
+ const signature = v1Part.slice(3);
838
+ if (isNaN(timestamp) || !signature) return false;
839
+ const now = Math.floor(Date.now() / 1e3);
840
+ if (Math.abs(now - timestamp) > toleranceSeconds) return false;
841
+ const signedContent = `${timestamp}.${body}`;
842
+ const expectedHmac = import_node_crypto.default.createHmac("sha256", secret).update(signedContent).digest("hex");
843
+ try {
844
+ const sigBuf = Buffer.from(signature, "hex");
845
+ const expBuf = Buffer.from(expectedHmac, "hex");
846
+ if (sigBuf.length !== expBuf.length) return false;
847
+ return import_node_crypto.default.timingSafeEqual(sigBuf, expBuf);
848
+ } catch {
849
+ return false;
850
+ }
851
+ }
852
+
627
853
  // src/index.ts
628
854
  var import_connect3 = require("@edge-markets/connect");
629
855
  var import_connect4 = require("@edge-markets/connect");
630
856
  // Annotate the CommonJS export names for ESM import in node:
631
857
  0 && (module.exports = {
858
+ EDGE_WEBHOOK_EVENT_TYPES,
632
859
  EdgeApiError,
633
860
  EdgeAuthenticationError,
634
861
  EdgeConnectServer,
@@ -648,5 +875,6 @@ var import_connect4 = require("@edge-markets/connect");
648
875
  isEdgeError,
649
876
  isIdentityVerificationError,
650
877
  isNetworkError,
651
- isProductionEnvironment
878
+ isProductionEnvironment,
879
+ verifyWebhookSignature
652
880
  });