@edge-markets/connect-node 1.8.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/README.md +146 -24
- package/dist/index.d.mts +301 -90
- package/dist/index.d.ts +301 -90
- package/dist/index.js +786 -223
- package/dist/index.mjs +754 -207
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,9 +12,16 @@ Server SDK for EDGE Connect token exchange and API calls.
|
|
|
12
12
|
- 🪝 **Webhook signature verification** - Constant-time HMAC-SHA256 with replay protection
|
|
13
13
|
- ✅ **Webhook parsing + validation** - Verify raw bodies and fail closed on malformed event payloads
|
|
14
14
|
- 🔁 **Webhook reconciliation** - Cursor-based sync for catching missed events
|
|
15
|
+
- 📥 **Webhook inbox orchestration** - Durable accept/process/replay control flow with partner-owned storage
|
|
15
16
|
- 🔐 **Token vault** - Versioned AES-256-GCM token encryption with key rotation
|
|
17
|
+
- 🔄 **User sessions** - Token-store callbacks with refresh, encryption, and single-flight refresh protection
|
|
16
18
|
- ⚙️ **Env config helper** - Build SDK config from standard EDGE env vars
|
|
17
19
|
|
|
20
|
+
These helpers are framework-neutral: the SDK owns repeatable Connect mechanics
|
|
21
|
+
such as token refresh, webhook validation, replay control flow, and transfer
|
|
22
|
+
intent checks, while partner applications keep ownership of persistence, wallet
|
|
23
|
+
mutation, admin authorization, and business rules.
|
|
24
|
+
|
|
18
25
|
## Installation
|
|
19
26
|
|
|
20
27
|
```bash
|
|
@@ -248,35 +255,46 @@ const decrypted = tokenVault.decryptTokens(connection.encryptedTokens)
|
|
|
248
255
|
const client = edge.forUser(decrypted.accessToken)
|
|
249
256
|
```
|
|
250
257
|
|
|
251
|
-
## Token Refresh
|
|
258
|
+
## Token Refresh And User Sessions
|
|
252
259
|
|
|
253
|
-
|
|
260
|
+
Use `EdgeUserSession` when you store encrypted tokens and want the SDK to
|
|
261
|
+
handle decrypt-refresh-save-client lifecycle. You still own the database table
|
|
262
|
+
and user mapping.
|
|
254
263
|
|
|
255
264
|
```typescript
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return decrypt(connection.accessToken)
|
|
277
|
-
}
|
|
265
|
+
const session = edge.createUserSession({
|
|
266
|
+
subjectId: userId,
|
|
267
|
+
tokenVault,
|
|
268
|
+
tokenStore: {
|
|
269
|
+
load: async (subjectId) => {
|
|
270
|
+
const row = await db.edgeConnections.get(subjectId)
|
|
271
|
+
return row?.encryptedTokens
|
|
272
|
+
},
|
|
273
|
+
save: async (subjectId, encryptedTokens, { tokens }) => {
|
|
274
|
+
await db.edgeConnections.update(subjectId, {
|
|
275
|
+
encryptedTokens,
|
|
276
|
+
expiresAt: new Date(tokens.expiresAt),
|
|
277
|
+
})
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
refreshSkewMs: 60_000,
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const balance = await session.withClient((client) => client.getBalance())
|
|
278
284
|
```
|
|
279
285
|
|
|
286
|
+
`EdgeUserSession` refreshes near-expiry tokens, preserves refresh tokens when
|
|
287
|
+
the token endpoint does not rotate them, saves the refreshed token envelope
|
|
288
|
+
before returning a client, and single-flights concurrent refreshes on the same
|
|
289
|
+
process. In horizontally scaled services, keep your storage callbacks
|
|
290
|
+
idempotent or add DB-level optimistic locking.
|
|
291
|
+
|
|
292
|
+
For migrations from split token columns, use `createSessionTokenRecord`,
|
|
293
|
+
`parseSessionTokenRecord`, `normalizeSessionExpiresAt`, and
|
|
294
|
+
`encryptLegacySessionTokens`. Plaintext migration requires an explicit
|
|
295
|
+
`allowPlaintext: true` option so production request paths do not silently accept
|
|
296
|
+
raw tokens.
|
|
297
|
+
|
|
280
298
|
## API Methods
|
|
281
299
|
|
|
282
300
|
All user-scoped API methods live on `EdgeUserClient`, created via `edge.forUser(accessToken)`:
|
|
@@ -526,6 +544,110 @@ calls, fetches and caches the partner `client_credentials` token internally,
|
|
|
526
544
|
retries transient sync failures with the SDK retry policy, and redacts token
|
|
527
545
|
material from observability hooks.
|
|
528
546
|
|
|
547
|
+
### Durable webhook inbox with `createWebhookInbox`
|
|
548
|
+
|
|
549
|
+
For production integrations, accept a verified webhook into durable storage
|
|
550
|
+
before returning `200`, then process it asynchronously. The SDK can own the
|
|
551
|
+
state machine while your app owns the database callbacks and wallet mutation.
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
import {
|
|
555
|
+
createWebhookInbox,
|
|
556
|
+
parseWebhookHttpRequest,
|
|
557
|
+
} from '@edge-markets/connect-node'
|
|
558
|
+
|
|
559
|
+
const inbox = createWebhookInbox({
|
|
560
|
+
store: {
|
|
561
|
+
insert: db.webhookInbox.insertIdempotently,
|
|
562
|
+
find: db.webhookInbox.find,
|
|
563
|
+
markProcessing: db.webhookInbox.markProcessing,
|
|
564
|
+
markProcessed: db.webhookInbox.markProcessed,
|
|
565
|
+
markFailed: db.webhookInbox.markFailed,
|
|
566
|
+
markPending: db.webhookInbox.markPending,
|
|
567
|
+
list: db.webhookInbox.list,
|
|
568
|
+
},
|
|
569
|
+
processEvent: async (event) => {
|
|
570
|
+
// Partner-owned business logic. Keep wallet mutation idempotent.
|
|
571
|
+
await processEdgeEvent(event)
|
|
572
|
+
},
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
app.post('/webhooks/edge', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
576
|
+
const parsed = parseWebhookHttpRequest({
|
|
577
|
+
request: { headers: req.headers, rawBody: req.body },
|
|
578
|
+
secret: process.env.EDGE_WEBHOOK_SECRET!,
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
const accepted = await inbox.acceptRaw(
|
|
582
|
+
parsed,
|
|
583
|
+
Buffer.from(req.body).toString('utf8'),
|
|
584
|
+
String(req.headers['x-edge-signature'] ?? ''),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
res.status(200).json({
|
|
588
|
+
received: true,
|
|
589
|
+
duplicate: accepted.duplicate,
|
|
590
|
+
status: accepted.status,
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
void inbox.process(accepted.eventId).catch((error) => {
|
|
594
|
+
logger.error('EDGE webhook processing failed', { eventId: accepted.eventId, error })
|
|
595
|
+
})
|
|
596
|
+
})
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
The inbox statuses are `pending`, `processing`, `processed`, and `failed`.
|
|
600
|
+
Duplicate `event.id` deliveries are idempotent if the payload fingerprint
|
|
601
|
+
matches. A duplicate event ID with different payload fails closed.
|
|
602
|
+
|
|
603
|
+
Sync reconciliation can use the same inbox path:
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
const reconciler = edge.createWebhookReconciler({
|
|
607
|
+
cursorStore,
|
|
608
|
+
inbox,
|
|
609
|
+
limit: 100,
|
|
610
|
+
})
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
The reconciler accepts each synced event into the inbox, processes it, and saves
|
|
614
|
+
the cursor only after processing succeeds or the event is already processed.
|
|
615
|
+
Use `inbox.replay(eventId)` for explicit admin replays and
|
|
616
|
+
`inbox.recoverStaleProcessing()` to make crashed `processing` rows replayable.
|
|
617
|
+
|
|
618
|
+
### Transfer safety helpers
|
|
619
|
+
|
|
620
|
+
Use transfer helpers to avoid decimal and direction mistakes:
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
import {
|
|
624
|
+
assertWebhookMatchesTransfer,
|
|
625
|
+
createTransferIdempotencyKey,
|
|
626
|
+
getConnectTransferDirection,
|
|
627
|
+
mapTransferStatusToPartnerState,
|
|
628
|
+
normalizeMoneyAmount,
|
|
629
|
+
validateTransferAmount,
|
|
630
|
+
} from '@edge-markets/connect-node'
|
|
631
|
+
|
|
632
|
+
const amount = validateTransferAmount(input.amount, { min: '1.00', max: '10000.00' }, { allowNumber: false })
|
|
633
|
+
const idempotencyKey = createTransferIdempotencyKey({
|
|
634
|
+
partnerUserId: user.id,
|
|
635
|
+
type: 'debit',
|
|
636
|
+
amount,
|
|
637
|
+
category: 'sportsbook',
|
|
638
|
+
externalId: bet.id,
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
// debit = EDGE user to partner; credit = partner to EDGE user
|
|
642
|
+
const direction = getConnectTransferDirection('debit')
|
|
643
|
+
|
|
644
|
+
assertWebhookMatchesTransfer(event, {
|
|
645
|
+
transferId: localTransfer.edgeTransferId,
|
|
646
|
+
type: 'debit',
|
|
647
|
+
amount: localTransfer.amount,
|
|
648
|
+
})
|
|
649
|
+
```
|
|
650
|
+
|
|
529
651
|
## Error Handling
|
|
530
652
|
|
|
531
653
|
```typescript
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as node_https from 'node:https';
|
|
2
|
-
import { EdgeEnvironment, TransferCategory, Transfer, VerificationSession, TransferType, User, VerifyIdentityOptions, VerifyIdentityResult, Balance, ListTransfersParams, TransferList, CreateVerificationSessionRequest, VerificationSessionStatusResponse, EdgeTokens, EdgeWebhookEvent,
|
|
2
|
+
import { EdgeEnvironment, TransferCategory, Transfer, VerificationSession, TransferType, TransferStatus, User, VerifyIdentityOptions, VerifyIdentityResult, Balance, ListTransfersParams, TransferList, CreateVerificationSessionRequest, VerificationSessionStatusResponse, EdgeTokens, EdgeWebhookEvent, EdgeWebhookEventType } from '@edge-markets/connect';
|
|
3
3
|
export { Balance, BaseWebhookEvent, ConsentRevokedEventData, CreateVerificationSessionRequest, EDGE_WEBHOOK_EVENT_TYPES, EdgeApiError, EdgeAuthenticationError, EdgeConsentRequiredError, EdgeEnvironment, EdgeError, EdgeIdentityVerificationError, EdgeInsufficientScopeError, EdgeNetworkError, EdgeNotFoundError, EdgeTokenExchangeError, EdgeTokens, EdgeValidationError, 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
|
type RequestEndpoint = 'connect_api' | 'oauth_token' | 'partner_webhook_sync';
|
|
@@ -107,6 +107,9 @@ interface TransferAmountLimits {
|
|
|
107
107
|
min?: string | number;
|
|
108
108
|
max?: string | number;
|
|
109
109
|
}
|
|
110
|
+
interface MoneyAmountOptions {
|
|
111
|
+
allowNumber?: boolean;
|
|
112
|
+
}
|
|
110
113
|
interface CreateTransferIdempotencyKeyOptions {
|
|
111
114
|
partnerUserId: string;
|
|
112
115
|
type: TransferType;
|
|
@@ -121,6 +124,14 @@ interface TransferIntent {
|
|
|
121
124
|
amount: string | number;
|
|
122
125
|
category?: TransferCategory | null;
|
|
123
126
|
}
|
|
127
|
+
type ConnectTransferDirection = 'user_to_partner' | 'partner_to_user';
|
|
128
|
+
interface TransferStatusMapping<TPartnerState extends string = string> {
|
|
129
|
+
pending_verification: TPartnerState;
|
|
130
|
+
processing: TPartnerState;
|
|
131
|
+
completed: TPartnerState;
|
|
132
|
+
failed: TPartnerState;
|
|
133
|
+
expired: TPartnerState;
|
|
134
|
+
}
|
|
124
135
|
interface StartTransferVerificationOptions {
|
|
125
136
|
transfer: TransferOptions;
|
|
126
137
|
origin: string;
|
|
@@ -129,10 +140,18 @@ interface StartTransferVerificationResult {
|
|
|
129
140
|
transfer: Transfer;
|
|
130
141
|
verificationSession: VerificationSession;
|
|
131
142
|
}
|
|
132
|
-
declare
|
|
133
|
-
|
|
143
|
+
declare const CONNECT_TRANSFER_DIRECTIONS: {
|
|
144
|
+
readonly debit: "user_to_partner";
|
|
145
|
+
readonly credit: "partner_to_user";
|
|
146
|
+
};
|
|
147
|
+
declare function getConnectTransferDirection(type: TransferType): ConnectTransferDirection;
|
|
148
|
+
declare function normalizeMoneyAmount(value: string | number, options?: MoneyAmountOptions): string;
|
|
149
|
+
declare function validateTransferAmount(value: string | number, limits?: TransferAmountLimits, options?: MoneyAmountOptions): string;
|
|
134
150
|
declare function createTransferIdempotencyKey(options: CreateTransferIdempotencyKeyOptions): string;
|
|
135
151
|
declare function assertSameTransferIntent(existing: TransferIntent, requested: TransferIntent): void;
|
|
152
|
+
declare function fingerprintTransferIntent(intent: TransferIntent): string;
|
|
153
|
+
declare function moneyAmountsEqual(left: string | number, right: string | number, options?: MoneyAmountOptions): boolean;
|
|
154
|
+
declare function mapTransferStatusToPartnerState<TPartnerState extends string>(status: TransferStatus, mapping: TransferStatusMapping<TPartnerState>): TPartnerState;
|
|
136
155
|
declare function startTransferVerification(client: Pick<EdgeUserClient, 'initiateTransfer' | 'createVerificationSession'>, options: StartTransferVerificationOptions): Promise<StartTransferVerificationResult>;
|
|
137
156
|
|
|
138
157
|
/**
|
|
@@ -205,10 +224,22 @@ interface EdgeUserSessionOptions {
|
|
|
205
224
|
refreshSkewMs?: number;
|
|
206
225
|
now?: () => number;
|
|
207
226
|
onRefresh?: (tokens: EdgeTokens) => Promise<void> | void;
|
|
227
|
+
onEvent?: (event: EdgeUserSessionEvent) => Promise<void> | void;
|
|
228
|
+
singleFlightRefresh?: boolean;
|
|
208
229
|
}
|
|
209
230
|
interface GetValidTokensOptions {
|
|
210
231
|
forceRefresh?: boolean;
|
|
211
232
|
}
|
|
233
|
+
type EdgeUserSessionEventType = 'session.loaded' | 'session.refresh_started' | 'session.refresh_succeeded' | 'session.refresh_failed' | 'session.saved';
|
|
234
|
+
interface EdgeUserSessionEvent {
|
|
235
|
+
type: EdgeUserSessionEventType;
|
|
236
|
+
subjectId: string;
|
|
237
|
+
refreshed?: boolean;
|
|
238
|
+
encrypted?: boolean;
|
|
239
|
+
expiresAt?: number;
|
|
240
|
+
scope?: string;
|
|
241
|
+
error?: string;
|
|
242
|
+
}
|
|
212
243
|
declare class EdgeUserSession {
|
|
213
244
|
private readonly edge;
|
|
214
245
|
private readonly subjectId;
|
|
@@ -217,15 +248,225 @@ declare class EdgeUserSession {
|
|
|
217
248
|
private readonly refreshSkewMs;
|
|
218
249
|
private readonly now;
|
|
219
250
|
private readonly onRefresh?;
|
|
251
|
+
private readonly onEvent?;
|
|
252
|
+
private readonly singleFlightRefresh;
|
|
253
|
+
private refreshPromise;
|
|
220
254
|
constructor(options: EdgeUserSessionOptions);
|
|
221
255
|
getValidTokens(options?: GetValidTokensOptions): Promise<EdgeTokens>;
|
|
222
256
|
getClient(options?: GetValidTokensOptions): Promise<EdgeUserClient>;
|
|
223
|
-
withClient<T>(callback: (client: EdgeUserClient, tokens: EdgeTokens) => Promise<T> | T): Promise<T>;
|
|
257
|
+
withClient<T>(callback: (client: EdgeUserClient, tokens: EdgeTokens) => Promise<T> | T, options?: GetValidTokensOptions): Promise<T>;
|
|
224
258
|
saveTokens(tokens: EdgeTokens, refreshed?: boolean): Promise<void>;
|
|
225
259
|
private loadTokens;
|
|
260
|
+
private refreshTokens;
|
|
261
|
+
private emitLoaded;
|
|
262
|
+
private emit;
|
|
226
263
|
}
|
|
227
264
|
declare function createEdgeUserSession(options: EdgeUserSessionOptions): EdgeUserSession;
|
|
228
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Webhook Signature Verification (SDK-001)
|
|
268
|
+
*
|
|
269
|
+
* HMAC-SHA256 verification for EDGE Connect webhook payloads.
|
|
270
|
+
*
|
|
271
|
+
* Signature header format: `t={timestamp},v1={hex_hmac}`
|
|
272
|
+
* Signed message: `{timestamp}.{raw_json_body}`
|
|
273
|
+
*
|
|
274
|
+
* The timestamp prefix prevents replay attacks — events older than the
|
|
275
|
+
* tolerance window (default 5 minutes) are rejected. The HMAC is verified
|
|
276
|
+
* with constant-time comparison to prevent timing attacks.
|
|
277
|
+
*/
|
|
278
|
+
/**
|
|
279
|
+
* Options for {@link verifyWebhookSignature}.
|
|
280
|
+
*/
|
|
281
|
+
interface VerifyWebhookSignatureOptions {
|
|
282
|
+
/**
|
|
283
|
+
* Maximum age of the signature timestamp, in seconds.
|
|
284
|
+
*
|
|
285
|
+
* Signatures older than this are rejected as potential replays.
|
|
286
|
+
* Defaults to 300 seconds (5 minutes), matching the producer.
|
|
287
|
+
*/
|
|
288
|
+
toleranceSeconds?: number;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Verifies an EDGE Connect webhook signature.
|
|
292
|
+
*
|
|
293
|
+
* **Always pass the raw request body bytes** — never `JSON.stringify(req.body)`.
|
|
294
|
+
* Re-stringifying after the framework parses JSON will reorder keys, change
|
|
295
|
+
* whitespace, and silently break verification with no diagnostic. Capture
|
|
296
|
+
* the raw body via your framework's raw-body middleware:
|
|
297
|
+
*
|
|
298
|
+
* - Express: `express.raw({ type: 'application/json' })`
|
|
299
|
+
* - Fastify: built-in (use `request.rawBody`)
|
|
300
|
+
* - NestJS: `{ rawBody: true }` on `NestFactory.create` + `RawBodyRequest<Request>`
|
|
301
|
+
*
|
|
302
|
+
* @param header - The `X-Edge-Signature` header value (`t=...,v1=...`)
|
|
303
|
+
* @param body - The raw request body as a UTF-8 string (NOT a parsed object)
|
|
304
|
+
* @param secret - The partner's webhook signing secret
|
|
305
|
+
* @param options - Optional overrides
|
|
306
|
+
* @returns `true` if the signature is valid and within the tolerance window,
|
|
307
|
+
* `false` otherwise. Never throws — malformed input returns `false`.
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* ```typescript
|
|
311
|
+
* import { verifyWebhookSignature } from '@edge-markets/connect-node'
|
|
312
|
+
*
|
|
313
|
+
* app.post('/webhooks/edge', express.raw({ type: 'application/json' }), (req, res) => {
|
|
314
|
+
* const ok = verifyWebhookSignature(
|
|
315
|
+
* req.headers['x-edge-signature'] as string,
|
|
316
|
+
* req.body.toString('utf8'),
|
|
317
|
+
* process.env.EDGE_WEBHOOK_SECRET!,
|
|
318
|
+
* )
|
|
319
|
+
*
|
|
320
|
+
* if (!ok) return res.status(401).send('invalid signature')
|
|
321
|
+
*
|
|
322
|
+
* const event = JSON.parse(req.body.toString('utf8'))
|
|
323
|
+
* // ... process event
|
|
324
|
+
* res.status(200).send('ok')
|
|
325
|
+
* })
|
|
326
|
+
* ```
|
|
327
|
+
*/
|
|
328
|
+
declare function verifyWebhookSignature(header: string, body: string, secret: string, options?: VerifyWebhookSignatureOptions): boolean;
|
|
329
|
+
|
|
330
|
+
type EdgeWebhookRawBody = string | Buffer | Uint8Array;
|
|
331
|
+
type EdgeWebhookSignatureHeader = string | string[] | undefined | null;
|
|
332
|
+
interface ParseAndVerifyWebhookOptions extends VerifyWebhookSignatureOptions {
|
|
333
|
+
rawBody: EdgeWebhookRawBody;
|
|
334
|
+
signatureHeader: EdgeWebhookSignatureHeader;
|
|
335
|
+
secret: string;
|
|
336
|
+
receivedAt?: Date;
|
|
337
|
+
}
|
|
338
|
+
interface ParsedEdgeWebhook {
|
|
339
|
+
event: EdgeWebhookEvent;
|
|
340
|
+
eventId: string;
|
|
341
|
+
eventType: EdgeWebhookEventType;
|
|
342
|
+
signatureTimestamp: number;
|
|
343
|
+
receivedAt: Date;
|
|
344
|
+
}
|
|
345
|
+
declare function parseAndVerifyWebhook(options: ParseAndVerifyWebhookOptions): ParsedEdgeWebhook;
|
|
346
|
+
declare function extractWebhookSignatureTimestamp(header: EdgeWebhookSignatureHeader): number | undefined;
|
|
347
|
+
declare function isEdgeWebhookEvent(value: unknown): value is EdgeWebhookEvent;
|
|
348
|
+
declare function validateEdgeWebhookEvent(value: unknown): EdgeWebhookEvent;
|
|
349
|
+
|
|
350
|
+
type WebhookInboxStatus = 'pending' | 'processing' | 'processed' | 'failed';
|
|
351
|
+
type WebhookInboxSource = 'live_webhook' | 'sync';
|
|
352
|
+
interface WebhookInboxRecord {
|
|
353
|
+
eventId: string;
|
|
354
|
+
eventType: EdgeWebhookEventType;
|
|
355
|
+
event: EdgeWebhookEvent;
|
|
356
|
+
payloadFingerprint: string;
|
|
357
|
+
status: WebhookInboxStatus;
|
|
358
|
+
source: WebhookInboxSource;
|
|
359
|
+
attempts: number;
|
|
360
|
+
receivedAt: Date;
|
|
361
|
+
updatedAt?: Date;
|
|
362
|
+
processedAt?: Date | null;
|
|
363
|
+
lastError?: string | null;
|
|
364
|
+
signatureTimestamp?: number | null;
|
|
365
|
+
signatureHeader?: string | null;
|
|
366
|
+
rawBody?: string | null;
|
|
367
|
+
}
|
|
368
|
+
interface WebhookInboxInsertRecord {
|
|
369
|
+
eventId: string;
|
|
370
|
+
eventType: EdgeWebhookEventType;
|
|
371
|
+
event: EdgeWebhookEvent;
|
|
372
|
+
payloadFingerprint: string;
|
|
373
|
+
source: WebhookInboxSource;
|
|
374
|
+
receivedAt: Date;
|
|
375
|
+
signatureTimestamp?: number | null;
|
|
376
|
+
signatureHeader?: string | null;
|
|
377
|
+
rawBody?: string | null;
|
|
378
|
+
}
|
|
379
|
+
type WebhookInboxInsertResult = {
|
|
380
|
+
inserted: true;
|
|
381
|
+
record: WebhookInboxRecord;
|
|
382
|
+
} | {
|
|
383
|
+
inserted: false;
|
|
384
|
+
record: WebhookInboxRecord;
|
|
385
|
+
};
|
|
386
|
+
interface WebhookInboxListOptions {
|
|
387
|
+
status?: WebhookInboxStatus;
|
|
388
|
+
updatedBefore?: Date;
|
|
389
|
+
limit?: number;
|
|
390
|
+
}
|
|
391
|
+
interface WebhookInboxStore {
|
|
392
|
+
insert: (record: WebhookInboxInsertRecord) => Promise<WebhookInboxInsertResult> | WebhookInboxInsertResult;
|
|
393
|
+
find: (eventId: string) => Promise<WebhookInboxRecord | null | undefined> | WebhookInboxRecord | null | undefined;
|
|
394
|
+
markProcessing: (eventId: string, context: {
|
|
395
|
+
now: Date;
|
|
396
|
+
}) => Promise<WebhookInboxRecord | null | undefined> | WebhookInboxRecord | null | undefined;
|
|
397
|
+
markProcessed: (eventId: string, context: {
|
|
398
|
+
now: Date;
|
|
399
|
+
}) => Promise<WebhookInboxRecord | null | undefined> | WebhookInboxRecord | null | undefined;
|
|
400
|
+
markFailed: (eventId: string, context: {
|
|
401
|
+
now: Date;
|
|
402
|
+
error: string;
|
|
403
|
+
}) => Promise<WebhookInboxRecord | null | undefined> | WebhookInboxRecord | null | undefined;
|
|
404
|
+
markPending?: (eventId: string, context: {
|
|
405
|
+
now: Date;
|
|
406
|
+
reason: string;
|
|
407
|
+
}) => Promise<WebhookInboxRecord | null | undefined> | WebhookInboxRecord | null | undefined;
|
|
408
|
+
list?: (options: WebhookInboxListOptions) => Promise<WebhookInboxRecord[]> | WebhookInboxRecord[];
|
|
409
|
+
}
|
|
410
|
+
interface WebhookInboxOptions {
|
|
411
|
+
store: WebhookInboxStore;
|
|
412
|
+
processEvent: (event: EdgeWebhookEvent, record: WebhookInboxRecord) => Promise<void> | void;
|
|
413
|
+
now?: () => Date;
|
|
414
|
+
storeRawBody?: boolean;
|
|
415
|
+
processingTimeoutMs?: number;
|
|
416
|
+
onEvent?: (event: WebhookInboxLifecycleEvent) => Promise<void> | void;
|
|
417
|
+
}
|
|
418
|
+
interface WebhookInboxLifecycleEvent {
|
|
419
|
+
type: 'webhook.accepted' | 'webhook.duplicate' | 'webhook.payload_mismatch' | 'webhook.processing_started' | 'webhook.processing_succeeded' | 'webhook.processing_failed' | 'webhook.recovered_stale';
|
|
420
|
+
eventId: string;
|
|
421
|
+
eventType?: EdgeWebhookEventType;
|
|
422
|
+
status?: WebhookInboxStatus;
|
|
423
|
+
source?: WebhookInboxSource;
|
|
424
|
+
attempts?: number;
|
|
425
|
+
error?: string;
|
|
426
|
+
}
|
|
427
|
+
interface WebhookInboxAcceptResult {
|
|
428
|
+
eventId: string;
|
|
429
|
+
eventType: EdgeWebhookEventType;
|
|
430
|
+
inserted: boolean;
|
|
431
|
+
duplicate: boolean;
|
|
432
|
+
status: WebhookInboxStatus;
|
|
433
|
+
record: WebhookInboxRecord;
|
|
434
|
+
}
|
|
435
|
+
interface WebhookInboxProcessResult {
|
|
436
|
+
eventId: string;
|
|
437
|
+
status: WebhookInboxStatus;
|
|
438
|
+
processed: boolean;
|
|
439
|
+
skipped: boolean;
|
|
440
|
+
reason?: 'already_processed' | 'not_found' | 'locked';
|
|
441
|
+
record?: WebhookInboxRecord;
|
|
442
|
+
}
|
|
443
|
+
interface WebhookInboxRecoverResult {
|
|
444
|
+
recovered: number;
|
|
445
|
+
skipped: boolean;
|
|
446
|
+
reason?: 'list_not_configured' | 'mark_pending_not_configured';
|
|
447
|
+
}
|
|
448
|
+
declare class EdgeWebhookInbox {
|
|
449
|
+
private readonly store;
|
|
450
|
+
private readonly processEvent;
|
|
451
|
+
private readonly now;
|
|
452
|
+
private readonly storeRawBody;
|
|
453
|
+
private readonly processingTimeoutMs;
|
|
454
|
+
private readonly onEvent?;
|
|
455
|
+
constructor(options: WebhookInboxOptions);
|
|
456
|
+
accept(parsed: ParsedEdgeWebhook): Promise<WebhookInboxAcceptResult>;
|
|
457
|
+
acceptRaw(parsed: ParsedEdgeWebhook, rawBody: string, signatureHeader: string): Promise<WebhookInboxAcceptResult>;
|
|
458
|
+
acceptSyncedEvent(event: EdgeWebhookEvent, receivedAt?: Date): Promise<WebhookInboxAcceptResult>;
|
|
459
|
+
process(eventId: string, options?: {
|
|
460
|
+
force?: boolean;
|
|
461
|
+
}): Promise<WebhookInboxProcessResult>;
|
|
462
|
+
replay(eventId: string): Promise<WebhookInboxProcessResult>;
|
|
463
|
+
recoverStaleProcessing(): Promise<WebhookInboxRecoverResult>;
|
|
464
|
+
private acceptEvent;
|
|
465
|
+
private emit;
|
|
466
|
+
}
|
|
467
|
+
declare function createWebhookInbox(options: WebhookInboxOptions): EdgeWebhookInbox;
|
|
468
|
+
declare function fingerprintWebhookEvent(event: EdgeWebhookEvent): string;
|
|
469
|
+
|
|
229
470
|
interface WebhookReconcilerCursorStore {
|
|
230
471
|
load: () => Promise<string | null | undefined> | string | null | undefined;
|
|
231
472
|
save: (cursor: string) => Promise<void> | void;
|
|
@@ -233,12 +474,15 @@ interface WebhookReconcilerCursorStore {
|
|
|
233
474
|
interface WebhookReconcilerOptions {
|
|
234
475
|
edge: Pick<EdgeConnectServer, 'syncWebhookEvents'>;
|
|
235
476
|
cursorStore: WebhookReconcilerCursorStore;
|
|
236
|
-
processEvent
|
|
477
|
+
processEvent?: (event: EdgeWebhookEvent) => Promise<void> | void;
|
|
478
|
+
inbox?: Pick<EdgeWebhookInbox, 'acceptSyncedEvent' | 'process'>;
|
|
237
479
|
limit?: SyncWebhookEventsOptions['limit'];
|
|
238
480
|
maxPages?: number;
|
|
239
481
|
}
|
|
240
482
|
interface WebhookReconcilerRunResult {
|
|
241
483
|
processed: number;
|
|
484
|
+
accepted?: number;
|
|
485
|
+
duplicates?: number;
|
|
242
486
|
pages: number;
|
|
243
487
|
cursor?: string;
|
|
244
488
|
hasMore: boolean;
|
|
@@ -437,6 +681,19 @@ interface EdgeHttpError {
|
|
|
437
681
|
declare function toEdgeHttpError(error: unknown): EdgeHttpError;
|
|
438
682
|
declare function toEdgeProblemJson(error: unknown): EdgeProblemJson;
|
|
439
683
|
|
|
684
|
+
interface EdgeWebhookHttpRequestLike {
|
|
685
|
+
headers?: Record<string, unknown>;
|
|
686
|
+
rawBody?: Buffer | Uint8Array | string;
|
|
687
|
+
body?: unknown;
|
|
688
|
+
}
|
|
689
|
+
interface ParseWebhookHttpRequestOptions extends Omit<ParseAndVerifyWebhookOptions, 'rawBody' | 'signatureHeader'> {
|
|
690
|
+
request: EdgeWebhookHttpRequestLike;
|
|
691
|
+
signatureHeaderName?: string;
|
|
692
|
+
}
|
|
693
|
+
declare function getEdgeWebhookRawBody(request: EdgeWebhookHttpRequestLike): EdgeWebhookRawBody;
|
|
694
|
+
declare function getEdgeWebhookSignatureHeader(request: EdgeWebhookHttpRequestLike, headerName?: string): EdgeWebhookSignatureHeader;
|
|
695
|
+
declare function parseWebhookHttpRequest(options: ParseWebhookHttpRequestOptions): ParsedEdgeWebhook;
|
|
696
|
+
|
|
440
697
|
interface PartnerIdentityAddress {
|
|
441
698
|
street?: string | null;
|
|
442
699
|
line1?: string | null;
|
|
@@ -467,6 +724,43 @@ interface IdentityPolicyEvaluation {
|
|
|
467
724
|
declare function buildVerifyIdentityPayload(profile: PartnerIdentityProfile): VerifyIdentityOptions;
|
|
468
725
|
declare function evaluateIdentityResult(result: VerifyIdentityResult, policy?: IdentityPolicy): IdentityPolicyEvaluation;
|
|
469
726
|
|
|
727
|
+
interface EdgeSessionTokenRecord {
|
|
728
|
+
subjectId: string;
|
|
729
|
+
encryptedTokens: string;
|
|
730
|
+
expiresAt: number;
|
|
731
|
+
scopes?: string[];
|
|
732
|
+
edgeUserId?: string;
|
|
733
|
+
keyId?: string;
|
|
734
|
+
metadata?: Record<string, unknown>;
|
|
735
|
+
}
|
|
736
|
+
interface CreateSessionTokenRecordOptions {
|
|
737
|
+
scopes?: string[];
|
|
738
|
+
edgeUserId?: string;
|
|
739
|
+
keyId?: string;
|
|
740
|
+
metadata?: Record<string, unknown>;
|
|
741
|
+
}
|
|
742
|
+
interface LegacySessionTokenRecord {
|
|
743
|
+
subjectId?: string;
|
|
744
|
+
accessToken?: string | null;
|
|
745
|
+
refreshToken?: string | null;
|
|
746
|
+
idToken?: string | null;
|
|
747
|
+
expiresAt?: unknown;
|
|
748
|
+
expiresIn?: unknown;
|
|
749
|
+
scope?: unknown;
|
|
750
|
+
scopes?: unknown;
|
|
751
|
+
edgeUserId?: string | null;
|
|
752
|
+
metadata?: Record<string, unknown>;
|
|
753
|
+
}
|
|
754
|
+
interface EncryptLegacySessionTokensOptions {
|
|
755
|
+
subjectId: string;
|
|
756
|
+
keyId?: string;
|
|
757
|
+
allowPlaintext?: boolean;
|
|
758
|
+
}
|
|
759
|
+
declare function createSessionTokenRecord(subjectId: string, tokens: EdgeTokens, tokenVault: Pick<EdgeTokenVault, 'encryptTokens'>, options?: CreateSessionTokenRecordOptions): EdgeSessionTokenRecord;
|
|
760
|
+
declare function parseSessionTokenRecord(record: unknown): EdgeSessionTokenRecord;
|
|
761
|
+
declare function encryptLegacySessionTokens(legacyRecord: LegacySessionTokenRecord, tokenVault: Pick<EdgeTokenVault, 'encryptTokens' | 'isEncrypted'>, options: EncryptLegacySessionTokensOptions): EdgeSessionTokenRecord;
|
|
762
|
+
declare function normalizeSessionExpiresAt(value: unknown): number;
|
|
763
|
+
|
|
470
764
|
interface TransferWebhookIntent {
|
|
471
765
|
transferId: string;
|
|
472
766
|
type?: TransferType;
|
|
@@ -477,89 +771,6 @@ declare function isTransferWebhookEvent(event: EdgeWebhookEvent): event is Extra
|
|
|
477
771
|
type: `transfer.${string}`;
|
|
478
772
|
}>;
|
|
479
773
|
declare function assertTransferEventMatchesIntent(event: EdgeWebhookEvent, intent: TransferWebhookIntent): void;
|
|
774
|
+
declare const assertWebhookMatchesTransfer: typeof assertTransferEventMatchesIntent;
|
|
480
775
|
|
|
481
|
-
|
|
482
|
-
* Webhook Signature Verification (SDK-001)
|
|
483
|
-
*
|
|
484
|
-
* HMAC-SHA256 verification for EDGE Connect webhook payloads.
|
|
485
|
-
*
|
|
486
|
-
* Signature header format: `t={timestamp},v1={hex_hmac}`
|
|
487
|
-
* Signed message: `{timestamp}.{raw_json_body}`
|
|
488
|
-
*
|
|
489
|
-
* The timestamp prefix prevents replay attacks — events older than the
|
|
490
|
-
* tolerance window (default 5 minutes) are rejected. The HMAC is verified
|
|
491
|
-
* with constant-time comparison to prevent timing attacks.
|
|
492
|
-
*/
|
|
493
|
-
/**
|
|
494
|
-
* Options for {@link verifyWebhookSignature}.
|
|
495
|
-
*/
|
|
496
|
-
interface VerifyWebhookSignatureOptions {
|
|
497
|
-
/**
|
|
498
|
-
* Maximum age of the signature timestamp, in seconds.
|
|
499
|
-
*
|
|
500
|
-
* Signatures older than this are rejected as potential replays.
|
|
501
|
-
* Defaults to 300 seconds (5 minutes), matching the producer.
|
|
502
|
-
*/
|
|
503
|
-
toleranceSeconds?: number;
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Verifies an EDGE Connect webhook signature.
|
|
507
|
-
*
|
|
508
|
-
* **Always pass the raw request body bytes** — never `JSON.stringify(req.body)`.
|
|
509
|
-
* Re-stringifying after the framework parses JSON will reorder keys, change
|
|
510
|
-
* whitespace, and silently break verification with no diagnostic. Capture
|
|
511
|
-
* the raw body via your framework's raw-body middleware:
|
|
512
|
-
*
|
|
513
|
-
* - Express: `express.raw({ type: 'application/json' })`
|
|
514
|
-
* - Fastify: built-in (use `request.rawBody`)
|
|
515
|
-
* - NestJS: `{ rawBody: true }` on `NestFactory.create` + `RawBodyRequest<Request>`
|
|
516
|
-
*
|
|
517
|
-
* @param header - The `X-Edge-Signature` header value (`t=...,v1=...`)
|
|
518
|
-
* @param body - The raw request body as a UTF-8 string (NOT a parsed object)
|
|
519
|
-
* @param secret - The partner's webhook signing secret
|
|
520
|
-
* @param options - Optional overrides
|
|
521
|
-
* @returns `true` if the signature is valid and within the tolerance window,
|
|
522
|
-
* `false` otherwise. Never throws — malformed input returns `false`.
|
|
523
|
-
*
|
|
524
|
-
* @example
|
|
525
|
-
* ```typescript
|
|
526
|
-
* import { verifyWebhookSignature } from '@edge-markets/connect-node'
|
|
527
|
-
*
|
|
528
|
-
* app.post('/webhooks/edge', express.raw({ type: 'application/json' }), (req, res) => {
|
|
529
|
-
* const ok = verifyWebhookSignature(
|
|
530
|
-
* req.headers['x-edge-signature'] as string,
|
|
531
|
-
* req.body.toString('utf8'),
|
|
532
|
-
* process.env.EDGE_WEBHOOK_SECRET!,
|
|
533
|
-
* )
|
|
534
|
-
*
|
|
535
|
-
* if (!ok) return res.status(401).send('invalid signature')
|
|
536
|
-
*
|
|
537
|
-
* const event = JSON.parse(req.body.toString('utf8'))
|
|
538
|
-
* // ... process event
|
|
539
|
-
* res.status(200).send('ok')
|
|
540
|
-
* })
|
|
541
|
-
* ```
|
|
542
|
-
*/
|
|
543
|
-
declare function verifyWebhookSignature(header: string, body: string, secret: string, options?: VerifyWebhookSignatureOptions): boolean;
|
|
544
|
-
|
|
545
|
-
type EdgeWebhookRawBody = string | Buffer | Uint8Array;
|
|
546
|
-
type EdgeWebhookSignatureHeader = string | string[] | undefined | null;
|
|
547
|
-
interface ParseAndVerifyWebhookOptions extends VerifyWebhookSignatureOptions {
|
|
548
|
-
rawBody: EdgeWebhookRawBody;
|
|
549
|
-
signatureHeader: EdgeWebhookSignatureHeader;
|
|
550
|
-
secret: string;
|
|
551
|
-
receivedAt?: Date;
|
|
552
|
-
}
|
|
553
|
-
interface ParsedEdgeWebhook {
|
|
554
|
-
event: EdgeWebhookEvent;
|
|
555
|
-
eventId: string;
|
|
556
|
-
eventType: EdgeWebhookEventType;
|
|
557
|
-
signatureTimestamp: number;
|
|
558
|
-
receivedAt: Date;
|
|
559
|
-
}
|
|
560
|
-
declare function parseAndVerifyWebhook(options: ParseAndVerifyWebhookOptions): ParsedEdgeWebhook;
|
|
561
|
-
declare function extractWebhookSignatureTimestamp(header: EdgeWebhookSignatureHeader): number | undefined;
|
|
562
|
-
declare function isEdgeWebhookEvent(value: unknown): value is EdgeWebhookEvent;
|
|
563
|
-
declare function validateEdgeWebhookEvent(value: unknown): EdgeWebhookEvent;
|
|
564
|
-
|
|
565
|
-
export { type CreateEdgeConnectServerFromEnvOptions, type CreateTransferIdempotencyKeyOptions, type EdgeConnectEnv, EdgeConnectServer, type EdgeConnectServerConfig, type EdgeConnectServerFromEnvCapabilities, type EdgeConnectServerFromEnvResult, type EdgeHttpError, type EdgeProblemJson, type EdgeTokenStore, type EdgeTokenStoreSaveContext, type EdgeTokenStoreValue, EdgeTokenVault, type EdgeTokenVaultKey, type EdgeTokenVaultKeyMaterial, type EdgeTokenVaultOptions, EdgeUserClient, EdgeUserSession, type EdgeUserSessionOptions, type EdgeWebhookRawBody, EdgeWebhookReconciler, type EdgeWebhookSignatureHeader, type GetValidTokensOptions, type IdentityPolicy, type IdentityPolicyEvaluation, type MtlsConfig, type ParseAndVerifyWebhookOptions, type ParsedEdgeWebhook, type PartnerIdentityAddress, type PartnerIdentityProfile, type RequestInfo, type ResponseInfo, type RetryConfig, type StartTransferVerificationOptions, type StartTransferVerificationResult, type SyncWebhookEventsOptions, type SyncWebhookEventsResult, type TransferAmountLimits, type TransferIntent, type TransferOptions, type TransferWebhookIntent, type VerifyWebhookSignatureOptions, type WebhookReconcilerCursorStore, type WebhookReconcilerOptions, type WebhookReconcilerRunResult, assertSameTransferIntent, assertTransferEventMatchesIntent, buildVerifyIdentityPayload, createEdgeConnectServerFromEnv, createEdgeTokenVault, createEdgeUserSession, createTransferIdempotencyKey, createWebhookReconciler, evaluateIdentityResult, extractWebhookSignatureTimestamp, isEdgeTokenVaultEnvelope, isEdgeWebhookEvent, isTransferWebhookEvent, normalizeMoneyAmount, parseAndVerifyWebhook, startTransferVerification, toEdgeHttpError, toEdgeProblemJson, validateEdgeWebhookEvent, validateTransferAmount, verifyWebhookSignature };
|
|
776
|
+
export { CONNECT_TRANSFER_DIRECTIONS, type ConnectTransferDirection, type CreateEdgeConnectServerFromEnvOptions, type CreateSessionTokenRecordOptions, type CreateTransferIdempotencyKeyOptions, type EdgeConnectEnv, EdgeConnectServer, type EdgeConnectServerConfig, type EdgeConnectServerFromEnvCapabilities, type EdgeConnectServerFromEnvResult, type EdgeHttpError, type EdgeProblemJson, type EdgeSessionTokenRecord, type EdgeTokenStore, type EdgeTokenStoreSaveContext, type EdgeTokenStoreValue, EdgeTokenVault, type EdgeTokenVaultKey, type EdgeTokenVaultKeyMaterial, type EdgeTokenVaultOptions, EdgeUserClient, EdgeUserSession, type EdgeUserSessionEvent, type EdgeUserSessionEventType, type EdgeUserSessionOptions, type EdgeWebhookHttpRequestLike, EdgeWebhookInbox, type EdgeWebhookRawBody, EdgeWebhookReconciler, type EdgeWebhookSignatureHeader, type EncryptLegacySessionTokensOptions, type GetValidTokensOptions, type IdentityPolicy, type IdentityPolicyEvaluation, type LegacySessionTokenRecord, type MoneyAmountOptions, type MtlsConfig, type ParseAndVerifyWebhookOptions, type ParseWebhookHttpRequestOptions, type ParsedEdgeWebhook, type PartnerIdentityAddress, type PartnerIdentityProfile, type RequestInfo, type ResponseInfo, type RetryConfig, type StartTransferVerificationOptions, type StartTransferVerificationResult, type SyncWebhookEventsOptions, type SyncWebhookEventsResult, type TransferAmountLimits, type TransferIntent, type TransferOptions, type TransferStatusMapping, type TransferWebhookIntent, type VerifyWebhookSignatureOptions, type WebhookInboxAcceptResult, type WebhookInboxInsertRecord, type WebhookInboxInsertResult, type WebhookInboxLifecycleEvent, type WebhookInboxListOptions, type WebhookInboxOptions, type WebhookInboxProcessResult, type WebhookInboxRecord, type WebhookInboxRecoverResult, type WebhookInboxSource, type WebhookInboxStatus, type WebhookInboxStore, type WebhookReconcilerCursorStore, type WebhookReconcilerOptions, type WebhookReconcilerRunResult, assertSameTransferIntent, assertTransferEventMatchesIntent, assertWebhookMatchesTransfer, buildVerifyIdentityPayload, createEdgeConnectServerFromEnv, createEdgeTokenVault, createEdgeUserSession, createSessionTokenRecord, createTransferIdempotencyKey, createWebhookInbox, createWebhookReconciler, encryptLegacySessionTokens, evaluateIdentityResult, extractWebhookSignatureTimestamp, fingerprintTransferIntent, fingerprintWebhookEvent, getConnectTransferDirection, getEdgeWebhookRawBody, getEdgeWebhookSignatureHeader, isEdgeTokenVaultEnvelope, isEdgeWebhookEvent, isTransferWebhookEvent, mapTransferStatusToPartnerState, moneyAmountsEqual, normalizeMoneyAmount, normalizeSessionExpiresAt, parseAndVerifyWebhook, parseSessionTokenRecord, parseWebhookHttpRequest, startTransferVerification, toEdgeHttpError, toEdgeProblemJson, validateEdgeWebhookEvent, validateTransferAmount, verifyWebhookSignature };
|