@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/README.md CHANGED
@@ -9,6 +9,8 @@ Server SDK for EDGE Connect token exchange and API calls.
9
9
  - 📡 **Full API client** - User, balance, transfers via `forUser()` pattern
10
10
  - 🛡️ **Typed errors** - Specific error classes for each scenario
11
11
  - 📝 **TypeScript first** - Complete type definitions
12
+ - 🪝 **Webhook signature verification** - Constant-time HMAC-SHA256 with replay protection
13
+ - 🔁 **Webhook reconciliation** - Cursor-based sync for catching missed events
12
14
 
13
15
  ## Installation
14
16
 
@@ -51,10 +53,10 @@ Never expose your client secret to browsers or client-side code.
51
53
 
52
54
  ```typescript
53
55
  interface EdgeConnectServerConfig {
54
- clientId: string // Your OAuth client ID
55
- clientSecret: string // Your OAuth client secret (keep secret!)
56
+ clientId: string // Your user-facing OAuth client ID
57
+ clientSecret: string // Your user-facing OAuth client secret (keep secret!)
56
58
  environment: EdgeEnvironment // 'production' | 'staging' | 'sandbox'
57
-
59
+
58
60
  // Optional
59
61
  apiBaseUrl?: string // Custom API URL (dev only)
60
62
  oauthBaseUrl?: string // Custom OAuth URL (dev only)
@@ -63,6 +65,13 @@ interface EdgeConnectServerConfig {
63
65
  onRequest?: (info) => void // Hook called before each request
64
66
  onResponse?: (info) => void // Hook called after each response
65
67
 
68
+ // Optional: Partner-level credentials for non-user-scoped endpoints.
69
+ // Required ONLY when calling syncWebhookEvents — the rest of the SDK
70
+ // works without them. These are a DIFFERENT OAuth client than clientId
71
+ // (machine-to-machine, not user-facing).
72
+ partnerClientId?: string
73
+ partnerClientSecret?: string
74
+
66
75
  // Optional: Message Level Encryption (Connect endpoints only)
67
76
  mle?: {
68
77
  enabled: boolean
@@ -175,7 +184,7 @@ const balance = await client.getBalance()
175
184
  ### Transfers
176
185
 
177
186
  ```typescript
178
- // Initiate transfer (requires OTP verification)
187
+ // 1. Initiate the transfer — returns a pending transfer that requires verification.
179
188
  const transfer = await client.initiateTransfer({
180
189
  type: 'debit', // 'debit' = pull from user, 'credit' = push to user
181
190
  amount: '100.00',
@@ -183,12 +192,25 @@ const transfer = await client.initiateTransfer({
183
192
  })
184
193
  // Returns: { transferId, status: 'pending_verification', otpMethod }
185
194
 
186
- // Verify with OTP
187
- const result = await client.verifyTransfer(transfer.transferId, userOtp)
188
- // Returns: { transferId, status: 'completed' | 'failed' }
195
+ // 2. Create an EDGE-hosted verification session. The returned `verificationUrl`
196
+ // is a single-use, short-lived URL you embed in an iframe or popup on your
197
+ // frontend. The user enters their OTP inside the EDGE-hosted UI — OTP secrets
198
+ // never touch your infrastructure.
199
+ const session = await client.createVerificationSession(transfer.transferId, {
200
+ origin: 'https://your-site.example.com',
201
+ })
202
+ // Returns: { sessionId, verificationUrl, expiresAt }
203
+
204
+ // 3. Listen for the postMessage event from the iframe (or poll
205
+ // getVerificationSessionStatus) to learn when verification completes.
206
+ // A `transfer.completed` webhook will be delivered to your configured
207
+ // webhook URL once the transfer settles. For browser-crash recovery,
208
+ // your backend should also reconcile against
209
+ // GET /v1/partner/webhooks/events/sync using a dashboard
210
+ // client_credentials token from POST /connect/oauth/token.
189
211
 
190
212
  // Get transfer status
191
- const status = await client.getTransfer(transferId)
213
+ const status = await client.getTransfer(transfer.transferId)
192
214
 
193
215
  // List transfers
194
216
  const { transfers, total } = await client.listTransfers({
@@ -208,6 +230,172 @@ await client.revokeConsent()
208
230
  await db.edgeConnections.delete(userId)
209
231
  ```
210
232
 
233
+ ## Webhooks
234
+
235
+ EDGE Connect delivers webhook events to your server when transfers complete,
236
+ fail, expire, or when a user revokes consent. The SDK provides two helpers:
237
+ **signature verification** for the live HTTP delivery channel, and a
238
+ **reconciliation sync method** for the cron-driven safety net.
239
+
240
+ ### Verify webhook signatures
241
+
242
+ Every webhook arrives with an `X-Edge-Signature` header in `t=...,v1=...`
243
+ format — HMAC-SHA256 over `${timestamp}.${rawBody}` keyed by your webhook
244
+ secret. Use `verifyWebhookSignature` to check it. The function does
245
+ constant-time comparison and rejects events older than 5 minutes by default
246
+ to prevent replay attacks.
247
+
248
+ > ⚠️ **The `body` argument MUST be the raw request bytes**, not a parsed
249
+ > object that you re-stringified. `JSON.stringify(req.body)` reorders keys
250
+ > and silently breaks verification with no diagnostic. Always capture the
251
+ > raw body via your framework's raw-body middleware.
252
+
253
+ ```typescript
254
+ import { verifyWebhookSignature, type EdgeWebhookEvent } from '@edge-markets/connect-node'
255
+ import express from 'express'
256
+
257
+ const app = express()
258
+
259
+ app.post(
260
+ '/webhooks/edge',
261
+ // 👇 Critical: this gives you req.body as a Buffer, not a parsed object
262
+ express.raw({ type: 'application/json' }),
263
+ (req, res) => {
264
+ const ok = verifyWebhookSignature(
265
+ req.headers['x-edge-signature'] as string,
266
+ req.body.toString('utf8'),
267
+ process.env.EDGE_WEBHOOK_SECRET!,
268
+ )
269
+ if (!ok) return res.status(401).send('invalid signature')
270
+
271
+ const event: EdgeWebhookEvent = JSON.parse(req.body.toString('utf8'))
272
+ // Process asynchronously and return 200 immediately — see "Best Practices"
273
+ void processEdgeEvent(event)
274
+ res.status(200).send('ok')
275
+ },
276
+ )
277
+ ```
278
+
279
+ #### NestJS
280
+
281
+ ```typescript
282
+ // main.ts
283
+ const app = await NestFactory.create(AppModule, { rawBody: true })
284
+
285
+ // edge-webhook.controller.ts
286
+ import { Controller, Post, Req, HttpCode, type RawBodyRequest } from '@nestjs/common'
287
+ import type { Request } from 'express'
288
+ import { verifyWebhookSignature } from '@edge-markets/connect-node'
289
+
290
+ @Controller('webhooks')
291
+ export class EdgeWebhookController {
292
+ @Post('edge')
293
+ @HttpCode(200)
294
+ handle(@Req() req: RawBodyRequest<Request>) {
295
+ const ok = verifyWebhookSignature(
296
+ req.headers['x-edge-signature'] as string,
297
+ req.rawBody?.toString('utf8') ?? '',
298
+ process.env.EDGE_WEBHOOK_SECRET!,
299
+ )
300
+ if (!ok) throw new UnauthorizedException('invalid signature')
301
+ // ...
302
+ }
303
+ }
304
+ ```
305
+
306
+ ### Type-safe event handling with `EdgeWebhookEvent`
307
+
308
+ `EdgeWebhookEvent` is a discriminated union over `event.type`. Switching
309
+ narrows `event.data` automatically — no casts required.
310
+
311
+ ```typescript
312
+ import type { EdgeWebhookEvent } from '@edge-markets/connect-node'
313
+
314
+ function processEdgeEvent(event: EdgeWebhookEvent) {
315
+ switch (event.type) {
316
+ case 'transfer.completed':
317
+ // event.data is { transferId, status: 'completed', type, amount }
318
+ creditWallet(event.data.transferId, event.data.amount)
319
+ break
320
+ case 'transfer.failed':
321
+ // event.data.reason is `string | undefined`
322
+ logFailure(event.data.transferId, event.data.reason ?? 'unknown')
323
+ break
324
+ case 'transfer.expired':
325
+ cancelPending(event.data.transferId)
326
+ break
327
+ case 'transfer.processing':
328
+ // @experimental — reserved for ledger dual-write hand-off
329
+ break
330
+ case 'consent.revoked':
331
+ deactivateLink(event.data.userId, event.data.clientId)
332
+ break
333
+ default: {
334
+ const _exhaustive: never = event
335
+ return _exhaustive
336
+ }
337
+ }
338
+ }
339
+ ```
340
+
341
+ `event.data.amount` is intentionally a `string` (e.g. `'100.00'`), not a
342
+ number, to preserve decimal precision — parse it with a decimal-aware
343
+ library on your side.
344
+
345
+ ### Reconcile missed events with `syncWebhookEvents`
346
+
347
+ Even with retries and a dead-letter queue, network or partner outages can
348
+ drop webhook events. Run `syncWebhookEvents` from a cron (every ~5 minutes
349
+ is typical) to catch anything your primary receiver missed.
350
+
351
+ This method requires the partner-level credentials (`partnerClientId` /
352
+ `partnerClientSecret`) on `EdgeConnectServer`. It uses the OAuth
353
+ `client_credentials` grant, caches the partner token internally, and
354
+ handles a 401 by refreshing the token once and retrying.
355
+
356
+ ```typescript
357
+ import { EdgeConnectServer } from '@edge-markets/connect-node'
358
+
359
+ const edge = new EdgeConnectServer({
360
+ clientId: process.env.EDGE_CLIENT_ID!,
361
+ clientSecret: process.env.EDGE_CLIENT_SECRET!,
362
+ environment: 'staging',
363
+ partnerClientId: process.env.EDGE_PARTNER_CLIENT_ID!,
364
+ partnerClientSecret: process.env.EDGE_PARTNER_CLIENT_SECRET!,
365
+ })
366
+
367
+ async function reconcile() {
368
+ let cursor = await db.cursors.get('edge') // your stored watermark
369
+ while (true) {
370
+ const { events, hasMore } = await edge.syncWebhookEvents({ afterEventId: cursor })
371
+ for (const event of events) {
372
+ await processEdgeEvent(event) // same idempotent handler as live webhooks
373
+ cursor = event.id
374
+ }
375
+ if (events.length > 0) await db.cursors.set('edge', cursor!)
376
+ if (!hasMore) break
377
+ }
378
+ }
379
+
380
+ setInterval(reconcile, 5 * 60 * 1000)
381
+ ```
382
+
383
+ The server enforces a **30-day lookback**. If your cursor is older than 30
384
+ days, you will receive events from 30 days ago — re-bootstrap from a known
385
+ good state via `getTransfer` if you have been offline for longer.
386
+
387
+ ### Best practices
388
+
389
+ 1. **Verify signatures first** — reject mis-signed events with 401 before
390
+ doing any work.
391
+ 2. **Return 200 immediately** — process events asynchronously. EDGE retries
392
+ on non-2xx and on responses slower than 10s.
393
+ 3. **Be idempotent** — the same event may be delivered more than once
394
+ (at-least-once delivery). Key your wallet credit / status update on
395
+ `event.id` or `event.data.transferId`.
396
+ 4. **Run reconciliation as a backstop** — webhooks are the fast path,
397
+ `syncWebhookEvents` is the safety net. They share the same handler.
398
+
211
399
  ## Error Handling
212
400
 
213
401
  ```typescript
package/dist/index.d.mts 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 };