@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 +196 -8
- package/dist/index.d.mts +179 -11
- package/dist/index.d.ts +179 -11
- package/dist/index.js +252 -24
- package/dist/index.mjs +255 -24
- package/package.json +2 -2
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
|
|
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
|
-
//
|
|
187
|
-
|
|
188
|
-
//
|
|
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,
|
|
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
|
-
|
|
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 };
|