vesant-sdk 1.7.0 → 1.7.1-dev.301c6df
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/{client-DtH2RLuy.d.ts → client-B3DCAHlg.d.ts} +2 -2
- package/dist/{client-DrjgZoH_.d.mts → client-BwXMQA2a.d.mts} +64 -42
- package/dist/{client-B0qhE2kr.d.mts → client-D23KhWeh.d.mts} +2 -2
- package/dist/{client-DF7hlMEz.d.ts → client-fy3trF2K.d.ts} +64 -42
- package/dist/{client-BolQlL5e.d.mts → client-q9fN8Hhf.d.mts} +103 -1
- package/dist/{client-BolQlL5e.d.ts → client-q9fN8Hhf.d.ts} +103 -1
- package/dist/compliance/index.d.mts +3 -3
- package/dist/compliance/index.d.ts +3 -3
- package/dist/compliance/index.js +181 -91
- package/dist/compliance/index.js.map +1 -1
- package/dist/compliance/index.mjs +181 -91
- package/dist/compliance/index.mjs.map +1 -1
- package/dist/decisions/index.d.mts +1 -1
- package/dist/decisions/index.d.ts +1 -1
- package/dist/decisions/index.js +69 -8
- package/dist/decisions/index.js.map +1 -1
- package/dist/decisions/index.mjs +69 -8
- package/dist/decisions/index.mjs.map +1 -1
- package/dist/geolocation/index.d.mts +68 -7
- package/dist/geolocation/index.d.ts +68 -7
- package/dist/geolocation/index.js +186 -91
- package/dist/geolocation/index.js.map +1 -1
- package/dist/geolocation/index.mjs +183 -92
- package/dist/geolocation/index.mjs.map +1 -1
- package/dist/index-C3zUKydT.d.mts +264 -0
- package/dist/index-DSDgS09k.d.ts +264 -0
- package/dist/index.d.mts +26 -9
- package/dist/index.d.ts +26 -9
- package/dist/index.js +354 -120
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +346 -121
- package/dist/index.mjs.map +1 -1
- package/dist/kyc/core.d.mts +1 -1
- package/dist/kyc/core.d.ts +1 -1
- package/dist/kyc/core.js +75 -14
- package/dist/kyc/core.js.map +1 -1
- package/dist/kyc/core.mjs +75 -14
- package/dist/kyc/core.mjs.map +1 -1
- package/dist/kyc/index.d.mts +4 -4
- package/dist/kyc/index.d.ts +4 -4
- package/dist/kyc/index.js +75 -14
- package/dist/kyc/index.js.map +1 -1
- package/dist/kyc/index.mjs +75 -14
- package/dist/kyc/index.mjs.map +1 -1
- package/dist/react.d.mts +7 -3
- package/dist/react.d.ts +7 -3
- package/dist/react.js +70 -33
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +70 -33
- package/dist/react.mjs.map +1 -1
- package/dist/risk-profile/index.d.mts +1 -1
- package/dist/risk-profile/index.d.ts +1 -1
- package/dist/risk-profile/index.js +69 -8
- package/dist/risk-profile/index.js.map +1 -1
- package/dist/risk-profile/index.mjs +69 -8
- package/dist/risk-profile/index.mjs.map +1 -1
- package/dist/scores/index.d.mts +1 -1
- package/dist/scores/index.d.ts +1 -1
- package/dist/scores/index.js +69 -8
- package/dist/scores/index.js.map +1 -1
- package/dist/scores/index.mjs +69 -8
- package/dist/scores/index.mjs.map +1 -1
- package/dist/tax/index.d.mts +2 -2
- package/dist/tax/index.d.ts +2 -2
- package/dist/tax/index.js +74 -10
- package/dist/tax/index.js.map +1 -1
- package/dist/tax/index.mjs +74 -10
- package/dist/tax/index.mjs.map +1 -1
- package/dist/webhooks/index.d.mts +2 -189
- package/dist/webhooks/index.d.ts +2 -189
- package/dist/webhooks/index.js +85 -21
- package/dist/webhooks/index.js.map +1 -1
- package/dist/webhooks/index.mjs +85 -22
- package/dist/webhooks/index.mjs.map +1 -1
- package/package.json +4 -1
package/dist/webhooks/index.d.ts
CHANGED
|
@@ -1,189 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Webhook signature verification utilities.
|
|
5
|
-
*
|
|
6
|
-
* Supports both Web Crypto API (browsers, Deno, Cloudflare Workers)
|
|
7
|
-
* and Node.js crypto module with automatic runtime detection.
|
|
8
|
-
* Uses constant-time comparison to prevent timing attacks.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Verify an HMAC-SHA256 webhook signature.
|
|
12
|
-
*
|
|
13
|
-
* @param payload - The raw request body string
|
|
14
|
-
* @param signature - The signature from the webhook header
|
|
15
|
-
* @param secret - The webhook signing secret
|
|
16
|
-
* @returns true if the signature is valid
|
|
17
|
-
*/
|
|
18
|
-
declare function verifyWebhookSignature(payload: string, signature: string, secret: string): Promise<boolean>;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Webhook event types and payload definitions.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
type WebhookEventType = 'risk.score.changed' | 'risk.category.changed' | 'profile.created' | 'profile.updated' | 'geolocation.verified' | 'geolocation.blocked' | 'kyc.status.changed' | 'kyc.approved' | 'kyc.declined' | 'decision.recorded' | 'label.applied' | 'location_request.completed' | 'compliance.check.failed' | 'tax.reminder.sent';
|
|
25
|
-
interface BaseWebhookEvent<T extends WebhookEventType = WebhookEventType, P = unknown> {
|
|
26
|
-
id: UUID;
|
|
27
|
-
type: T;
|
|
28
|
-
tenant_id: string;
|
|
29
|
-
environment: 'production' | 'sandbox';
|
|
30
|
-
customer_id: string;
|
|
31
|
-
payload: P;
|
|
32
|
-
timestamp: Timestamp;
|
|
33
|
-
}
|
|
34
|
-
interface RiskScoreChangedPayload {
|
|
35
|
-
previous_score: number;
|
|
36
|
-
new_score: number;
|
|
37
|
-
risk_level: RiskLevel;
|
|
38
|
-
factors: string[];
|
|
39
|
-
}
|
|
40
|
-
interface RiskCategoryChangedPayload {
|
|
41
|
-
previous_category: string;
|
|
42
|
-
new_category: string;
|
|
43
|
-
risk_level: RiskLevel;
|
|
44
|
-
}
|
|
45
|
-
interface ProfileCreatedPayload {
|
|
46
|
-
customer_id: string;
|
|
47
|
-
entity_type: string;
|
|
48
|
-
source: string;
|
|
49
|
-
}
|
|
50
|
-
interface ProfileUpdatedPayload {
|
|
51
|
-
customer_id: string;
|
|
52
|
-
updated_fields: string[];
|
|
53
|
-
}
|
|
54
|
-
interface GeolocationVerifiedPayload {
|
|
55
|
-
latitude: number;
|
|
56
|
-
longitude: number;
|
|
57
|
-
country: string;
|
|
58
|
-
region?: string;
|
|
59
|
-
is_compliant: boolean;
|
|
60
|
-
}
|
|
61
|
-
interface GeolocationBlockedPayload {
|
|
62
|
-
latitude: number;
|
|
63
|
-
longitude: number;
|
|
64
|
-
country: string;
|
|
65
|
-
region?: string;
|
|
66
|
-
reasons: string[];
|
|
67
|
-
}
|
|
68
|
-
interface KycStatusChangedPayload {
|
|
69
|
-
previous_status: string;
|
|
70
|
-
new_status: string;
|
|
71
|
-
provider?: string;
|
|
72
|
-
}
|
|
73
|
-
interface DecisionRecordedPayload {
|
|
74
|
-
decision_id: string;
|
|
75
|
-
decision_type: string;
|
|
76
|
-
category: string;
|
|
77
|
-
source: string;
|
|
78
|
-
risk_score?: number;
|
|
79
|
-
}
|
|
80
|
-
interface LabelAppliedPayload {
|
|
81
|
-
label_type: string;
|
|
82
|
-
reasons?: string[];
|
|
83
|
-
}
|
|
84
|
-
interface LocationRequestCompletedPayload {
|
|
85
|
-
request_id: string;
|
|
86
|
-
status: string;
|
|
87
|
-
latitude?: number;
|
|
88
|
-
longitude?: number;
|
|
89
|
-
}
|
|
90
|
-
interface ComplianceCheckFailedPayload {
|
|
91
|
-
check_type: string;
|
|
92
|
-
reasons: string[];
|
|
93
|
-
severity: string;
|
|
94
|
-
}
|
|
95
|
-
interface TaxReminderSentPayload {
|
|
96
|
-
/** W-form the reminder is for (e.g. "W-9", "W-8BEN", "W-8BEN-E") */
|
|
97
|
-
form_type: string;
|
|
98
|
-
/** Sequential count of reminders sent in the current solicitation cycle */
|
|
99
|
-
reminder_count: number;
|
|
100
|
-
/** Configured maximum reminders per cycle (reminder_max_count) */
|
|
101
|
-
max_count: number;
|
|
102
|
-
}
|
|
103
|
-
type WebhookEvent = BaseWebhookEvent<'risk.score.changed', RiskScoreChangedPayload> | BaseWebhookEvent<'risk.category.changed', RiskCategoryChangedPayload> | BaseWebhookEvent<'profile.created', ProfileCreatedPayload> | BaseWebhookEvent<'profile.updated', ProfileUpdatedPayload> | BaseWebhookEvent<'geolocation.verified', GeolocationVerifiedPayload> | BaseWebhookEvent<'geolocation.blocked', GeolocationBlockedPayload> | BaseWebhookEvent<'kyc.status.changed', KycStatusChangedPayload> | BaseWebhookEvent<'kyc.approved', KycStatusChangedPayload> | BaseWebhookEvent<'kyc.declined', KycStatusChangedPayload> | BaseWebhookEvent<'decision.recorded', DecisionRecordedPayload> | BaseWebhookEvent<'label.applied', LabelAppliedPayload> | BaseWebhookEvent<'location_request.completed', LocationRequestCompletedPayload> | BaseWebhookEvent<'compliance.check.failed', ComplianceCheckFailedPayload> | BaseWebhookEvent<'tax.reminder.sent', TaxReminderSentPayload>;
|
|
104
|
-
interface WebhookHandlerConfig {
|
|
105
|
-
/** Webhook signing secret for signature verification */
|
|
106
|
-
secret: string;
|
|
107
|
-
/** Header name containing the signature (default: 'x-webhook-signature') */
|
|
108
|
-
signatureHeader?: string;
|
|
109
|
-
/** Maximum age of webhook event in ms (default: 300000 = 5 minutes) */
|
|
110
|
-
tolerance?: number;
|
|
111
|
-
/** Enable replay protection to reject duplicate event IDs (default: true) */
|
|
112
|
-
replayProtection?: boolean;
|
|
113
|
-
}
|
|
114
|
-
type WebhookEventHandler<T extends WebhookEventType = WebhookEventType> = (event: Extract<WebhookEvent, {
|
|
115
|
-
type: T;
|
|
116
|
-
}>) => void | Promise<void>;
|
|
117
|
-
type WebhookAnyHandler = (event: WebhookEvent) => void | Promise<void>;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Webhook event handler with signature verification and typed dispatch.
|
|
121
|
-
*/
|
|
122
|
-
|
|
123
|
-
declare class WebhookHandler {
|
|
124
|
-
private handlers;
|
|
125
|
-
private anyHandlers;
|
|
126
|
-
private readonly secret;
|
|
127
|
-
private readonly tolerance;
|
|
128
|
-
private readonly replayProtection;
|
|
129
|
-
private seenEventIds;
|
|
130
|
-
constructor(config: WebhookHandlerConfig);
|
|
131
|
-
/**
|
|
132
|
-
* Register a handler for a specific event type.
|
|
133
|
-
*/
|
|
134
|
-
on<T extends WebhookEventType>(eventType: T, handler: WebhookEventHandler<T>): this;
|
|
135
|
-
/**
|
|
136
|
-
* Register a catch-all handler for all event types.
|
|
137
|
-
*/
|
|
138
|
-
onAny(handler: WebhookAnyHandler): this;
|
|
139
|
-
/**
|
|
140
|
-
* Verify signature and parse the webhook body.
|
|
141
|
-
*/
|
|
142
|
-
verifyAndParse(body: string, signature: string): Promise<WebhookEvent>;
|
|
143
|
-
/**
|
|
144
|
-
* Verify signature, parse, and dispatch to registered handlers.
|
|
145
|
-
*/
|
|
146
|
-
handle(body: string, signature: string): Promise<void>;
|
|
147
|
-
/**
|
|
148
|
-
* Parse an event without signature verification (for testing).
|
|
149
|
-
*/
|
|
150
|
-
parseEvent(body: string): WebhookEvent;
|
|
151
|
-
private parseAndValidate;
|
|
152
|
-
private dispatch;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Framework middleware helpers for webhook handling.
|
|
157
|
-
*
|
|
158
|
-
* Provides pre-built integrations for Express/Connect and Next.js App Router.
|
|
159
|
-
*/
|
|
160
|
-
|
|
161
|
-
interface WebhookMiddlewareOptions extends WebhookHandlerConfig {
|
|
162
|
-
/** Event handlers to register */
|
|
163
|
-
handlers?: Partial<Record<WebhookEventType, WebhookEventHandler>>;
|
|
164
|
-
/** Catch-all handler */
|
|
165
|
-
onAny?: WebhookAnyHandler;
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Create an Express/Connect-compatible middleware for webhook handling.
|
|
169
|
-
*
|
|
170
|
-
* Expects the raw body to be available as `req.body` (string).
|
|
171
|
-
* Use `express.raw({ type: 'application/json' })` or similar middleware upstream.
|
|
172
|
-
*/
|
|
173
|
-
declare function createWebhookMiddleware(options: WebhookMiddlewareOptions): (req: {
|
|
174
|
-
body: string | Buffer;
|
|
175
|
-
headers: Record<string, string | string[] | undefined>;
|
|
176
|
-
}, res: {
|
|
177
|
-
status(code: number): {
|
|
178
|
-
json(body: unknown): void;
|
|
179
|
-
};
|
|
180
|
-
json?(body: unknown): void;
|
|
181
|
-
}, next?: (err?: unknown) => void) => Promise<void>;
|
|
182
|
-
/**
|
|
183
|
-
* Create a Next.js App Router-compatible webhook handler.
|
|
184
|
-
*
|
|
185
|
-
* Returns an async function `(request: Request) => Promise<Response>`.
|
|
186
|
-
*/
|
|
187
|
-
declare function createNextWebhookHandler(options: WebhookMiddlewareOptions): (request: Request) => Promise<Response>;
|
|
188
|
-
|
|
189
|
-
export { type BaseWebhookEvent, type ComplianceCheckFailedPayload, type DecisionRecordedPayload, type GeolocationBlockedPayload, type GeolocationVerifiedPayload, type KycStatusChangedPayload, type LabelAppliedPayload, type LocationRequestCompletedPayload, type ProfileCreatedPayload, type ProfileUpdatedPayload, type RiskCategoryChangedPayload, type RiskScoreChangedPayload, type TaxReminderSentPayload, type WebhookAnyHandler, type WebhookEvent, type WebhookEventHandler, type WebhookEventType, WebhookHandler, type WebhookHandlerConfig, type WebhookMiddlewareOptions, createNextWebhookHandler, createWebhookMiddleware, verifyWebhookSignature };
|
|
1
|
+
export { B as BaseWebhookEvent, C as ComplianceCheckFailedPayload, D as DecisionRecordedPayload, G as GeolocationBlockedPayload, a as GeolocationVerifiedPayload, I as InMemoryDedupStore, b as InMemoryDedupStoreOptions, K as KycStatusChangedPayload, L as LabelAppliedPayload, c as LocationRequestCompletedPayload, N as NotificationWebhookEvent, P as ProfileCreatedPayload, d as ProfileUpdatedPayload, R as RiskCategoryChangedPayload, e as RiskScoreChangedPayload, T as TaxReminderSentPayload, W as WebhookAnyHandler, f as WebhookDedupStore, g as WebhookEvent, h as WebhookEventHandler, i as WebhookEventType, j as WebhookHandler, k as WebhookHandlerConfig, l as WebhookMiddlewareOptions, m as WebhookNotification, n as createNextWebhookHandler, o as createWebhookMiddleware, v as verifyWebhookSignature } from '../index-DSDgS09k.js';
|
|
2
|
+
import '../types-CBQRNL-l.js';
|
package/dist/webhooks/index.js
CHANGED
|
@@ -20,7 +20,20 @@ var ValidationError = class _ValidationError extends VesantError {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
// src/core/webhook-utils.ts
|
|
23
|
-
async function verifyWebhookSignature(payload, signature, secret) {
|
|
23
|
+
async function verifyWebhookSignature(payload, signature, secret, opts = {}) {
|
|
24
|
+
const signed = parseSignedTimestamp(signature);
|
|
25
|
+
if (signed) {
|
|
26
|
+
const tolerance = opts.tolerance ?? 3e5;
|
|
27
|
+
const now = opts.now?.() ?? Date.now();
|
|
28
|
+
if (tolerance > 0 && Math.abs(now - signed.t * 1e3) > tolerance) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const expected = await computeHmacSha256(`${signed.t}.${payload}`, secret);
|
|
32
|
+
return constantTimeEqual(signed.v1, expected);
|
|
33
|
+
}
|
|
34
|
+
if (opts.allowLegacy === false) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
24
37
|
const hexDigest = await computeHmacSha256(payload, secret);
|
|
25
38
|
const expectedPrefixed = `sha256=${hexDigest}`;
|
|
26
39
|
if (signature.startsWith("sha256=")) {
|
|
@@ -28,6 +41,25 @@ async function verifyWebhookSignature(payload, signature, secret) {
|
|
|
28
41
|
}
|
|
29
42
|
return constantTimeEqual(signature, hexDigest);
|
|
30
43
|
}
|
|
44
|
+
function parseSignedTimestamp(signature) {
|
|
45
|
+
if (!signature.includes("t=") || !signature.includes("v1=")) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
let t;
|
|
49
|
+
let v1;
|
|
50
|
+
for (const part of signature.split(",")) {
|
|
51
|
+
const eq = part.indexOf("=");
|
|
52
|
+
if (eq === -1) continue;
|
|
53
|
+
const key = part.slice(0, eq).trim();
|
|
54
|
+
const value = part.slice(eq + 1).trim();
|
|
55
|
+
if (key === "t") t = Number(value);
|
|
56
|
+
else if (key === "v1") v1 = value;
|
|
57
|
+
}
|
|
58
|
+
if (t === void 0 || Number.isNaN(t) || !v1) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return { t, v1 };
|
|
62
|
+
}
|
|
31
63
|
async function computeHmacSha256(message, secret) {
|
|
32
64
|
if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.subtle) {
|
|
33
65
|
const encoder = new TextEncoder();
|
|
@@ -62,15 +94,48 @@ function constantTimeEqual(a, b) {
|
|
|
62
94
|
return result === 0;
|
|
63
95
|
}
|
|
64
96
|
|
|
97
|
+
// src/core/dedup.ts
|
|
98
|
+
var InMemoryDedupStore = class {
|
|
99
|
+
constructor(opts = {}) {
|
|
100
|
+
this.expiryById = /* @__PURE__ */ new Map();
|
|
101
|
+
this.maxEntries = opts.maxEntries ?? 1e3;
|
|
102
|
+
this.now = opts.now ?? (() => Date.now());
|
|
103
|
+
}
|
|
104
|
+
async seen(id) {
|
|
105
|
+
return this.expiryById.has(id);
|
|
106
|
+
}
|
|
107
|
+
async mark(id, ttlMs) {
|
|
108
|
+
const now = this.now();
|
|
109
|
+
this.expiryById.set(id, now + ttlMs);
|
|
110
|
+
if (this.expiryById.size > this.maxEntries) {
|
|
111
|
+
for (const [key, expiresAt] of this.expiryById) {
|
|
112
|
+
if (now > expiresAt) {
|
|
113
|
+
this.expiryById.delete(key);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
65
120
|
// src/webhooks/handler.ts
|
|
121
|
+
function resolveEventId(event) {
|
|
122
|
+
if (event.id) {
|
|
123
|
+
return event.id;
|
|
124
|
+
}
|
|
125
|
+
if (event.event_type === "notification.created") {
|
|
126
|
+
return event.notification?.id;
|
|
127
|
+
}
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
66
130
|
var WebhookHandler = class {
|
|
67
131
|
constructor(config) {
|
|
68
132
|
this.handlers = /* @__PURE__ */ new Map();
|
|
69
133
|
this.anyHandlers = [];
|
|
70
|
-
this.seenEventIds = /* @__PURE__ */ new Map();
|
|
71
134
|
this.secret = config.secret;
|
|
72
135
|
this.tolerance = config.tolerance ?? 3e5;
|
|
73
136
|
this.replayProtection = config.replayProtection ?? true;
|
|
137
|
+
this.allowLegacySignature = config.allowLegacySignature ?? true;
|
|
138
|
+
this.dedupStore = config.dedupStore ?? new InMemoryDedupStore();
|
|
74
139
|
}
|
|
75
140
|
/**
|
|
76
141
|
* Register a handler for a specific event type.
|
|
@@ -92,7 +157,10 @@ var WebhookHandler = class {
|
|
|
92
157
|
* Verify signature and parse the webhook body.
|
|
93
158
|
*/
|
|
94
159
|
async verifyAndParse(body, signature) {
|
|
95
|
-
const isValid = await verifyWebhookSignature(body, signature, this.secret
|
|
160
|
+
const isValid = await verifyWebhookSignature(body, signature, this.secret, {
|
|
161
|
+
tolerance: this.tolerance,
|
|
162
|
+
allowLegacy: this.allowLegacySignature
|
|
163
|
+
});
|
|
96
164
|
if (!isValid) {
|
|
97
165
|
throw new ValidationError("Invalid webhook signature", ["signature"]);
|
|
98
166
|
}
|
|
@@ -108,18 +176,21 @@ var WebhookHandler = class {
|
|
|
108
176
|
/**
|
|
109
177
|
* Parse an event without signature verification (for testing).
|
|
110
178
|
*/
|
|
111
|
-
parseEvent(body) {
|
|
179
|
+
async parseEvent(body) {
|
|
112
180
|
return this.parseAndValidate(body);
|
|
113
181
|
}
|
|
114
|
-
parseAndValidate(body) {
|
|
182
|
+
async parseAndValidate(body) {
|
|
115
183
|
const event = JSON.parse(body);
|
|
116
|
-
|
|
117
|
-
|
|
184
|
+
const eventId = resolveEventId(event);
|
|
185
|
+
if (!event.event_type || !eventId || !event.timestamp) {
|
|
186
|
+
throw new ValidationError(
|
|
187
|
+
"Invalid webhook event: missing required fields (event_type, id, timestamp)",
|
|
188
|
+
["event_type", "id", "timestamp"]
|
|
189
|
+
);
|
|
118
190
|
}
|
|
119
191
|
if (this.tolerance > 0) {
|
|
120
192
|
const eventTime = new Date(event.timestamp).getTime();
|
|
121
|
-
|
|
122
|
-
if (Math.abs(now - eventTime) > this.tolerance) {
|
|
193
|
+
if (Math.abs(Date.now() - eventTime) > this.tolerance) {
|
|
123
194
|
throw new ValidationError(
|
|
124
195
|
`Webhook event timestamp is outside tolerance window (${this.tolerance}ms)`,
|
|
125
196
|
["timestamp"]
|
|
@@ -127,26 +198,18 @@ var WebhookHandler = class {
|
|
|
127
198
|
}
|
|
128
199
|
}
|
|
129
200
|
if (this.replayProtection) {
|
|
130
|
-
if (this.
|
|
201
|
+
if (await this.dedupStore.seen(eventId)) {
|
|
131
202
|
throw new ValidationError(
|
|
132
|
-
`Duplicate webhook event: ${
|
|
203
|
+
`Duplicate webhook event: ${eventId} has already been processed`,
|
|
133
204
|
["id"]
|
|
134
205
|
);
|
|
135
206
|
}
|
|
136
|
-
|
|
137
|
-
this.seenEventIds.set(event.id, now);
|
|
138
|
-
if (this.seenEventIds.size > 1e3) {
|
|
139
|
-
for (const [id, seenAt] of this.seenEventIds) {
|
|
140
|
-
if (now - seenAt > this.tolerance) {
|
|
141
|
-
this.seenEventIds.delete(id);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
207
|
+
await this.dedupStore.mark(eventId, this.tolerance);
|
|
145
208
|
}
|
|
146
209
|
return event;
|
|
147
210
|
}
|
|
148
211
|
async dispatch(event) {
|
|
149
|
-
const typeHandlers = this.handlers.get(event.
|
|
212
|
+
const typeHandlers = this.handlers.get(event.event_type) || [];
|
|
150
213
|
const allHandlers = [...typeHandlers, ...this.anyHandlers];
|
|
151
214
|
for (const handler of allHandlers) {
|
|
152
215
|
await handler(event);
|
|
@@ -227,6 +290,7 @@ function buildHandler(options) {
|
|
|
227
290
|
return handler;
|
|
228
291
|
}
|
|
229
292
|
|
|
293
|
+
exports.InMemoryDedupStore = InMemoryDedupStore;
|
|
230
294
|
exports.WebhookHandler = WebhookHandler;
|
|
231
295
|
exports.createNextWebhookHandler = createNextWebhookHandler;
|
|
232
296
|
exports.createWebhookMiddleware = createWebhookMiddleware;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/errors.ts","../../src/core/webhook-utils.ts","../../src/webhooks/handler.ts","../../src/webhooks/middleware.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Error hierarchy for Vesant SDK\n *\n * Provides structured error handling with specific error types\n * for different failure scenarios.\n */\n\nexport class VesantError extends Error {\n public requestId?: string;\n\n constructor(\n message: string,\n public code: string,\n public statusCode?: number,\n public details?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'VesantError';\n Object.setPrototypeOf(this, VesantError.prototype);\n }\n}\n\n/** @deprecated Use VesantError instead */\nexport const CGSError = VesantError;\n/** @deprecated Use VesantError instead */\nexport type CGSError = VesantError;\n\nexport class NetworkError extends VesantError {\n constructor(message: string, public originalError?: unknown) {\n super(message, 'NETWORK_ERROR');\n this.name = 'NetworkError';\n Object.setPrototypeOf(this, NetworkError.prototype);\n }\n}\n\nexport class ValidationError extends VesantError {\n constructor(message: string, fields?: string[]) {\n super(message, 'VALIDATION_ERROR', 400, { fields });\n this.name = 'ValidationError';\n Object.setPrototypeOf(this, ValidationError.prototype);\n }\n}\n\nexport class ServiceUnavailableError extends VesantError {\n constructor(message: string = 'Service unavailable') {\n super(message, 'SERVICE_UNAVAILABLE', 503);\n this.name = 'ServiceUnavailableError';\n Object.setPrototypeOf(this, ServiceUnavailableError.prototype);\n }\n}\n\nexport class ComplianceBlockedError extends VesantError {\n constructor(reasons: string[]) {\n super('Access blocked due to compliance rules', 'COMPLIANCE_BLOCKED', 403, { reasons });\n this.name = 'ComplianceBlockedError';\n Object.setPrototypeOf(this, ComplianceBlockedError.prototype);\n }\n}\n\nexport class AuthenticationError extends VesantError {\n constructor(message: string = 'Authentication failed') {\n super(message, 'AUTHENTICATION_ERROR', 401);\n this.name = 'AuthenticationError';\n Object.setPrototypeOf(this, AuthenticationError.prototype);\n }\n}\n\nexport class RateLimitError extends VesantError {\n constructor(public retryAfter?: number) {\n super('Rate limit exceeded', 'RATE_LIMIT_EXCEEDED', 429, { retryAfter });\n this.name = 'RateLimitError';\n Object.setPrototypeOf(this, RateLimitError.prototype);\n }\n}\n\nexport class TimeoutError extends VesantError {\n constructor(public timeout: number) {\n super(`Request timeout after ${timeout}ms`, 'TIMEOUT', 408, { timeout });\n this.name = 'TimeoutError';\n Object.setPrototypeOf(this, TimeoutError.prototype);\n }\n}\n\nexport class ComplianceError extends VesantError {\n constructor(\n message: string,\n public originalError?: unknown,\n code: string = 'COMPLIANCE_ERROR'\n ) {\n super(message, code);\n this.name = 'ComplianceError';\n Object.setPrototypeOf(this, ComplianceError.prototype);\n }\n}\n\nexport class CircuitBreakerOpenError extends VesantError {\n constructor() {\n super('Circuit breaker is open — requests are temporarily blocked', 'CIRCUIT_BREAKER_OPEN', 503);\n this.name = 'CircuitBreakerOpenError';\n Object.setPrototypeOf(this, CircuitBreakerOpenError.prototype);\n }\n}\n","/**\n * Webhook signature verification utilities.\n *\n * Supports both Web Crypto API (browsers, Deno, Cloudflare Workers)\n * and Node.js crypto module with automatic runtime detection.\n * Uses constant-time comparison to prevent timing attacks.\n */\n\nimport { VesantError } from './errors';\n\n/**\n * Verify an HMAC-SHA256 webhook signature.\n *\n * @param payload - The raw request body string\n * @param signature - The signature from the webhook header\n * @param secret - The webhook signing secret\n * @returns true if the signature is valid\n */\nexport async function verifyWebhookSignature(\n payload: string,\n signature: string,\n secret: string\n): Promise<boolean> {\n const hexDigest = await computeHmacSha256(payload, secret);\n\n // Backend sends \"sha256=<hex>\" format — support both prefixed and raw signatures\n const expectedPrefixed = `sha256=${hexDigest}`;\n\n if (signature.startsWith('sha256=')) {\n return constantTimeEqual(signature, expectedPrefixed);\n }\n\n return constantTimeEqual(signature, hexDigest);\n}\n\n/**\n * Compute HMAC-SHA256 hex digest.\n * Automatically selects Web Crypto API or Node.js crypto.\n */\nasync function computeHmacSha256(message: string, secret: string): Promise<string> {\n // Try Web Crypto API first (browsers, Deno, Cloudflare Workers, Node 18+)\n if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.subtle) {\n const encoder = new TextEncoder();\n const key = await globalThis.crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n const sig = await globalThis.crypto.subtle.sign('HMAC', key, encoder.encode(message));\n return Array.from(new Uint8Array(sig))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n }\n\n // Fallback to Node.js crypto module\n try {\n const { createHmac } = await import('crypto');\n return createHmac('sha256', secret).update(message).digest('hex');\n } catch {\n throw new VesantError(\n 'No crypto implementation available. Requires Web Crypto API or Node.js crypto module.',\n 'CRYPTO_UNAVAILABLE'\n );\n }\n}\n\n/**\n * Constant-time string comparison to prevent timing attacks.\n */\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < a.length; i++) {\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return result === 0;\n}\n","/**\n * Webhook event handler with signature verification and typed dispatch.\n */\n\nimport { verifyWebhookSignature } from '../core/webhook-utils';\nimport { ValidationError } from '../core/errors';\nimport type {\n WebhookEvent,\n WebhookEventType,\n WebhookEventHandler,\n WebhookAnyHandler,\n WebhookHandlerConfig,\n} from './types';\n\nexport class WebhookHandler {\n private handlers = new Map<WebhookEventType, WebhookEventHandler[]>();\n private anyHandlers: WebhookAnyHandler[] = [];\n private readonly secret: string;\n private readonly tolerance: number;\n private readonly replayProtection: boolean;\n private seenEventIds = new Map<string, number>();\n\n constructor(config: WebhookHandlerConfig) {\n this.secret = config.secret;\n this.tolerance = config.tolerance ?? 300000; // 5 minutes\n this.replayProtection = config.replayProtection ?? true;\n }\n\n /**\n * Register a handler for a specific event type.\n */\n on<T extends WebhookEventType>(\n eventType: T,\n handler: WebhookEventHandler<T>\n ): this {\n const existing = this.handlers.get(eventType) || [];\n existing.push(handler as unknown as WebhookEventHandler);\n this.handlers.set(eventType, existing);\n return this;\n }\n\n /**\n * Register a catch-all handler for all event types.\n */\n onAny(handler: WebhookAnyHandler): this {\n this.anyHandlers.push(handler);\n return this;\n }\n\n /**\n * Verify signature and parse the webhook body.\n */\n async verifyAndParse(body: string, signature: string): Promise<WebhookEvent> {\n const isValid = await verifyWebhookSignature(body, signature, this.secret);\n if (!isValid) {\n throw new ValidationError('Invalid webhook signature', ['signature']);\n }\n\n return this.parseAndValidate(body);\n }\n\n /**\n * Verify signature, parse, and dispatch to registered handlers.\n */\n async handle(body: string, signature: string): Promise<void> {\n const event = await this.verifyAndParse(body, signature);\n await this.dispatch(event);\n }\n\n /**\n * Parse an event without signature verification (for testing).\n */\n parseEvent(body: string): WebhookEvent {\n return this.parseAndValidate(body);\n }\n\n private parseAndValidate(body: string): WebhookEvent {\n const event = JSON.parse(body) as WebhookEvent;\n\n if (!event.type || !event.id || !event.timestamp) {\n throw new ValidationError('Invalid webhook event: missing required fields (type, id, timestamp)', ['type', 'id', 'timestamp']);\n }\n\n // Timestamp tolerance check\n if (this.tolerance > 0) {\n const eventTime = new Date(event.timestamp).getTime();\n const now = Date.now();\n if (Math.abs(now - eventTime) > this.tolerance) {\n throw new ValidationError(\n `Webhook event timestamp is outside tolerance window (${this.tolerance}ms)`,\n ['timestamp']\n );\n }\n }\n\n // Replay protection: reject duplicate event IDs within tolerance window\n if (this.replayProtection) {\n if (this.seenEventIds.has(event.id)) {\n throw new ValidationError(\n `Duplicate webhook event: ${event.id} has already been processed`,\n ['id']\n );\n }\n\n const now = Date.now();\n this.seenEventIds.set(event.id, now);\n\n // Purge expired entries to prevent unbounded memory growth\n if (this.seenEventIds.size > 1000) {\n for (const [id, seenAt] of this.seenEventIds) {\n if (now - seenAt > this.tolerance) {\n this.seenEventIds.delete(id);\n }\n }\n }\n }\n\n return event;\n }\n\n private async dispatch(event: WebhookEvent): Promise<void> {\n const typeHandlers = this.handlers.get(event.type) || [];\n const allHandlers = [...typeHandlers, ...this.anyHandlers];\n\n for (const handler of allHandlers) {\n await (handler as WebhookAnyHandler)(event);\n }\n }\n}\n","/**\n * Framework middleware helpers for webhook handling.\n *\n * Provides pre-built integrations for Express/Connect and Next.js App Router.\n */\n\nimport { WebhookHandler } from './handler';\nimport type { WebhookHandlerConfig, WebhookEventType, WebhookEventHandler, WebhookAnyHandler } from './types';\n\nexport interface WebhookMiddlewareOptions extends WebhookHandlerConfig {\n /** Event handlers to register */\n handlers?: Partial<Record<WebhookEventType, WebhookEventHandler>>;\n /** Catch-all handler */\n onAny?: WebhookAnyHandler;\n}\n\n/**\n * Create an Express/Connect-compatible middleware for webhook handling.\n *\n * Expects the raw body to be available as `req.body` (string).\n * Use `express.raw({ type: 'application/json' })` or similar middleware upstream.\n */\nexport function createWebhookMiddleware(options: WebhookMiddlewareOptions) {\n const handler = buildHandler(options);\n const signatureHeader = options.signatureHeader || 'x-webhook-signature';\n\n return async (\n req: { body: string | Buffer; headers: Record<string, string | string[] | undefined> },\n res: { status(code: number): { json(body: unknown): void }; json?(body: unknown): void },\n next?: (err?: unknown) => void\n ) => {\n try {\n const body = typeof req.body === 'string' ? req.body : req.body.toString('utf-8');\n const signature = req.headers[signatureHeader];\n\n if (!signature || typeof signature !== 'string') {\n res.status(401).json({ error: 'Missing webhook signature' });\n return;\n }\n\n await handler.handle(body, signature);\n res.status(200).json({ received: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Webhook processing failed';\n\n if (message.includes('signature')) {\n res.status(401).json({ error: message });\n } else if (message.includes('tolerance') || message.includes('timestamp')) {\n res.status(400).json({ error: message });\n } else if (message.includes('Duplicate')) {\n res.status(409).json({ error: message });\n } else if (next) {\n next(error);\n } else {\n res.status(500).json({ error: message });\n }\n }\n };\n}\n\n/**\n * Create a Next.js App Router-compatible webhook handler.\n *\n * Returns an async function `(request: Request) => Promise<Response>`.\n */\nexport function createNextWebhookHandler(\n options: WebhookMiddlewareOptions\n): (request: Request) => Promise<Response> {\n const handler = buildHandler(options);\n const signatureHeader = options.signatureHeader || 'x-webhook-signature';\n\n return async (request: Request): Promise<Response> => {\n try {\n const body = await request.text();\n const signature = request.headers.get(signatureHeader);\n\n if (!signature) {\n return new Response(JSON.stringify({ error: 'Missing webhook signature' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n\n await handler.handle(body, signature);\n\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Webhook processing failed';\n const status = message.includes('signature')\n ? 401\n : message.includes('tolerance') || message.includes('timestamp')\n ? 400\n : message.includes('Duplicate')\n ? 409\n : 500;\n\n return new Response(JSON.stringify({ error: message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n };\n}\n\nfunction buildHandler(options: WebhookMiddlewareOptions): WebhookHandler {\n const handler = new WebhookHandler(options);\n\n if (options.handlers) {\n for (const [eventType, eventHandler] of Object.entries(options.handlers)) {\n if (eventHandler) {\n handler.on(eventType as WebhookEventType, eventHandler);\n }\n }\n }\n\n if (options.onAny) {\n handler.onAny(options.onAny);\n }\n\n return handler;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/errors.ts","../../src/core/webhook-utils.ts","../../src/core/dedup.ts","../../src/webhooks/handler.ts","../../src/webhooks/middleware.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Error hierarchy for Vesant SDK\n *\n * Provides structured error handling with specific error types\n * for different failure scenarios.\n */\n\nexport class VesantError extends Error {\n public requestId?: string;\n\n constructor(\n message: string,\n public code: string,\n public statusCode?: number,\n public details?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'VesantError';\n Object.setPrototypeOf(this, VesantError.prototype);\n }\n}\n\n/** @deprecated Use VesantError instead */\nexport const CGSError = VesantError;\n/** @deprecated Use VesantError instead */\nexport type CGSError = VesantError;\n\nexport class NetworkError extends VesantError {\n constructor(message: string, public originalError?: unknown) {\n super(message, 'NETWORK_ERROR');\n this.name = 'NetworkError';\n Object.setPrototypeOf(this, NetworkError.prototype);\n }\n}\n\nexport class ValidationError extends VesantError {\n constructor(message: string, fields?: string[]) {\n super(message, 'VALIDATION_ERROR', 400, { fields });\n this.name = 'ValidationError';\n Object.setPrototypeOf(this, ValidationError.prototype);\n }\n}\n\nexport class ServiceUnavailableError extends VesantError {\n constructor(message: string = 'Service unavailable') {\n super(message, 'SERVICE_UNAVAILABLE', 503);\n this.name = 'ServiceUnavailableError';\n Object.setPrototypeOf(this, ServiceUnavailableError.prototype);\n }\n}\n\nexport class ComplianceBlockedError extends VesantError {\n constructor(reasons: string[]) {\n super('Access blocked due to compliance rules', 'COMPLIANCE_BLOCKED', 403, { reasons });\n this.name = 'ComplianceBlockedError';\n Object.setPrototypeOf(this, ComplianceBlockedError.prototype);\n }\n}\n\nexport class AuthenticationError extends VesantError {\n constructor(message: string = 'Authentication failed') {\n super(message, 'AUTHENTICATION_ERROR', 401);\n this.name = 'AuthenticationError';\n Object.setPrototypeOf(this, AuthenticationError.prototype);\n }\n}\n\nexport class RateLimitError extends VesantError {\n constructor(public retryAfter?: number) {\n super('Rate limit exceeded', 'RATE_LIMIT_EXCEEDED', 429, { retryAfter });\n this.name = 'RateLimitError';\n Object.setPrototypeOf(this, RateLimitError.prototype);\n }\n}\n\nexport class TimeoutError extends VesantError {\n constructor(public timeout: number) {\n super(`Request timeout after ${timeout}ms`, 'TIMEOUT', 408, { timeout });\n this.name = 'TimeoutError';\n Object.setPrototypeOf(this, TimeoutError.prototype);\n }\n}\n\nexport class ComplianceError extends VesantError {\n constructor(\n message: string,\n public originalError?: unknown,\n code: string = 'COMPLIANCE_ERROR'\n ) {\n super(message, code);\n this.name = 'ComplianceError';\n Object.setPrototypeOf(this, ComplianceError.prototype);\n }\n}\n\nexport class CircuitBreakerOpenError extends VesantError {\n constructor() {\n super('Circuit breaker is open — requests are temporarily blocked', 'CIRCUIT_BREAKER_OPEN', 503);\n this.name = 'CircuitBreakerOpenError';\n Object.setPrototypeOf(this, CircuitBreakerOpenError.prototype);\n }\n}\n","/**\n * Webhook signature verification utilities.\n *\n * Supports both Web Crypto API (browsers, Deno, Cloudflare Workers)\n * and Node.js crypto module with automatic runtime detection.\n * Uses constant-time comparison to prevent timing attacks.\n */\n\nimport { VesantError } from './errors';\n\nexport interface VerifyWebhookSignatureOptions {\n /** Max allowed clock skew in ms for the signed-timestamp scheme (default: 300000). */\n tolerance?: number;\n /** Clock source for the tolerance check (default: Date.now). */\n now?: () => number;\n /** Accept the legacy body-only `sha256=<hex>` format (default: true, for migration). */\n allowLegacy?: boolean;\n}\n\n/**\n * Verify an HMAC-SHA256 webhook signature.\n *\n * Supports two schemes:\n * - Signed-timestamp (preferred): `t=<unix>,v1=<hex>` where the HMAC is computed\n * over `${t}.${payload}`. The timestamp is inside the signed material, so a\n * replayed body is rejected once `|now - t|` exceeds the tolerance — the check\n * is done BEFORE the HMAC compare and authenticates the timestamp.\n * - Legacy (body-only): `sha256=<hex>` (or raw hex) over the payload alone. Kept\n * for the migration window; disable via `allowLegacy: false`.\n *\n * @returns true if the signature is valid\n */\nexport async function verifyWebhookSignature(\n payload: string,\n signature: string,\n secret: string,\n opts: VerifyWebhookSignatureOptions = {}\n): Promise<boolean> {\n const signed = parseSignedTimestamp(signature);\n if (signed) {\n const tolerance = opts.tolerance ?? 300000;\n const now = opts.now?.() ?? Date.now();\n // Authenticate the timestamp first — reject replays before any HMAC compare.\n if (tolerance > 0 && Math.abs(now - signed.t * 1000) > tolerance) {\n return false;\n }\n const expected = await computeHmacSha256(`${signed.t}.${payload}`, secret);\n return constantTimeEqual(signed.v1, expected);\n }\n\n if (opts.allowLegacy === false) {\n return false;\n }\n\n const hexDigest = await computeHmacSha256(payload, secret);\n // Backend sends \"sha256=<hex>\" format — support both prefixed and raw signatures\n const expectedPrefixed = `sha256=${hexDigest}`;\n\n if (signature.startsWith('sha256=')) {\n return constantTimeEqual(signature, expectedPrefixed);\n }\n\n return constantTimeEqual(signature, hexDigest);\n}\n\n/**\n * Parse the `t=<unix>,v1=<hex>` signed-timestamp scheme; returns null if the\n * signature is not in that form (e.g. legacy `sha256=<hex>`).\n */\nfunction parseSignedTimestamp(signature: string): { t: number; v1: string } | null {\n if (!signature.includes('t=') || !signature.includes('v1=')) {\n return null;\n }\n let t: number | undefined;\n let v1: string | undefined;\n for (const part of signature.split(',')) {\n const eq = part.indexOf('=');\n if (eq === -1) continue;\n const key = part.slice(0, eq).trim();\n const value = part.slice(eq + 1).trim();\n if (key === 't') t = Number(value);\n else if (key === 'v1') v1 = value;\n }\n if (t === undefined || Number.isNaN(t) || !v1) {\n return null;\n }\n return { t, v1 };\n}\n\n/**\n * Compute HMAC-SHA256 hex digest.\n * Automatically selects Web Crypto API or Node.js crypto.\n */\nasync function computeHmacSha256(message: string, secret: string): Promise<string> {\n // Try Web Crypto API first (browsers, Deno, Cloudflare Workers, Node 18+)\n if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.subtle) {\n const encoder = new TextEncoder();\n const key = await globalThis.crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n const sig = await globalThis.crypto.subtle.sign('HMAC', key, encoder.encode(message));\n return Array.from(new Uint8Array(sig))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n }\n\n // Fallback to Node.js crypto module\n try {\n const { createHmac } = await import('crypto');\n return createHmac('sha256', secret).update(message).digest('hex');\n } catch {\n throw new VesantError(\n 'No crypto implementation available. Requires Web Crypto API or Node.js crypto module.',\n 'CRYPTO_UNAVAILABLE'\n );\n }\n}\n\n/**\n * Constant-time string comparison to prevent timing attacks.\n */\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n let result = 0;\n for (let i = 0; i < a.length; i++) {\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return result === 0;\n}\n","/**\n * Pluggable dedup store for webhook replay protection.\n *\n * The default InMemoryDedupStore is process-local — correct for a single\n * long-lived process, but useless across replicas / serverless invocations where\n * the backend's at-least-once retries land on different instances. Supply a\n * shared implementation (e.g. Redis with SETNX + TTL) via WebhookHandlerConfig\n * to dedup reliably in those deployments.\n */\n\nexport interface WebhookDedupStore {\n /** Has this event id already been processed? */\n seen(id: string): Promise<boolean>;\n /** Record this event id as processed, retained for at least ttlMs. */\n mark(id: string, ttlMs: number): Promise<void>;\n}\n\nexport interface InMemoryDedupStoreOptions {\n /** Purge expired entries once the map grows past this size (default: 1000). */\n maxEntries?: number;\n /** Injectable clock (default: Date.now). */\n now?: () => number;\n}\n\nexport class InMemoryDedupStore implements WebhookDedupStore {\n private readonly expiryById = new Map<string, number>();\n private readonly maxEntries: number;\n private readonly now: () => number;\n\n constructor(opts: InMemoryDedupStoreOptions = {}) {\n this.maxEntries = opts.maxEntries ?? 1000;\n this.now = opts.now ?? (() => Date.now());\n }\n\n async seen(id: string): Promise<boolean> {\n return this.expiryById.has(id);\n }\n\n async mark(id: string, ttlMs: number): Promise<void> {\n const now = this.now();\n this.expiryById.set(id, now + ttlMs);\n\n // Bounded memory: purge expired entries once the map grows too large.\n if (this.expiryById.size > this.maxEntries) {\n for (const [key, expiresAt] of this.expiryById) {\n if (now > expiresAt) {\n this.expiryById.delete(key);\n }\n }\n }\n }\n}\n","/**\n * Webhook event handler with signature verification and typed dispatch.\n */\n\nimport { verifyWebhookSignature } from '../core/webhook-utils';\nimport { ValidationError } from '../core/errors';\nimport { InMemoryDedupStore, type WebhookDedupStore } from '../core';\nimport type {\n WebhookEvent,\n WebhookEventType,\n WebhookEventHandler,\n WebhookAnyHandler,\n WebhookHandlerConfig,\n} from './types';\n\n/** Resolve the event id: the top-level `id`, or `notification.id` for the notification envelope. */\nfunction resolveEventId(event: WebhookEvent): string | undefined {\n if (event.id) {\n return event.id;\n }\n if (event.event_type === 'notification.created') {\n return event.notification?.id;\n }\n return undefined;\n}\n\nexport class WebhookHandler {\n private handlers = new Map<WebhookEventType, WebhookEventHandler[]>();\n private anyHandlers: WebhookAnyHandler[] = [];\n private readonly secret: string;\n private readonly tolerance: number;\n private readonly replayProtection: boolean;\n private readonly allowLegacySignature: boolean;\n private readonly dedupStore: WebhookDedupStore;\n\n constructor(config: WebhookHandlerConfig) {\n this.secret = config.secret;\n this.tolerance = config.tolerance ?? 300000; // 5 minutes\n this.replayProtection = config.replayProtection ?? true;\n this.allowLegacySignature = config.allowLegacySignature ?? true;\n this.dedupStore = config.dedupStore ?? new InMemoryDedupStore();\n }\n\n /**\n * Register a handler for a specific event type.\n */\n on<T extends WebhookEventType>(\n eventType: T,\n handler: WebhookEventHandler<T>\n ): this {\n const existing = this.handlers.get(eventType) || [];\n existing.push(handler as unknown as WebhookEventHandler);\n this.handlers.set(eventType, existing);\n return this;\n }\n\n /**\n * Register a catch-all handler for all event types.\n */\n onAny(handler: WebhookAnyHandler): this {\n this.anyHandlers.push(handler);\n return this;\n }\n\n /**\n * Verify signature and parse the webhook body.\n */\n async verifyAndParse(body: string, signature: string): Promise<WebhookEvent> {\n const isValid = await verifyWebhookSignature(body, signature, this.secret, {\n tolerance: this.tolerance,\n allowLegacy: this.allowLegacySignature,\n });\n if (!isValid) {\n throw new ValidationError('Invalid webhook signature', ['signature']);\n }\n\n return this.parseAndValidate(body);\n }\n\n /**\n * Verify signature, parse, and dispatch to registered handlers.\n */\n async handle(body: string, signature: string): Promise<void> {\n const event = await this.verifyAndParse(body, signature);\n await this.dispatch(event);\n }\n\n /**\n * Parse an event without signature verification (for testing).\n */\n async parseEvent(body: string): Promise<WebhookEvent> {\n return this.parseAndValidate(body);\n }\n\n private async parseAndValidate(body: string): Promise<WebhookEvent> {\n const event = JSON.parse(body) as WebhookEvent;\n const eventId = resolveEventId(event);\n\n if (!event.event_type || !eventId || !event.timestamp) {\n throw new ValidationError(\n 'Invalid webhook event: missing required fields (event_type, id, timestamp)',\n ['event_type', 'id', 'timestamp']\n );\n }\n\n // Timestamp tolerance check (defense in depth; the signed-timestamp scheme\n // also authenticates the timestamp in verifyWebhookSignature).\n if (this.tolerance > 0) {\n const eventTime = new Date(event.timestamp).getTime();\n if (Math.abs(Date.now() - eventTime) > this.tolerance) {\n throw new ValidationError(\n `Webhook event timestamp is outside tolerance window (${this.tolerance}ms)`,\n ['timestamp']\n );\n }\n }\n\n // Replay protection via the (pluggable) dedup store.\n if (this.replayProtection) {\n if (await this.dedupStore.seen(eventId)) {\n throw new ValidationError(\n `Duplicate webhook event: ${eventId} has already been processed`,\n ['id']\n );\n }\n await this.dedupStore.mark(eventId, this.tolerance);\n }\n\n return event;\n }\n\n private async dispatch(event: WebhookEvent): Promise<void> {\n const typeHandlers = this.handlers.get(event.event_type) || [];\n const allHandlers = [...typeHandlers, ...this.anyHandlers];\n\n for (const handler of allHandlers) {\n await (handler as WebhookAnyHandler)(event);\n }\n }\n}\n","/**\n * Framework middleware helpers for webhook handling.\n *\n * Provides pre-built integrations for Express/Connect and Next.js App Router.\n */\n\nimport { WebhookHandler } from './handler';\nimport type { WebhookHandlerConfig, WebhookEventType, WebhookEventHandler, WebhookAnyHandler } from './types';\n\nexport interface WebhookMiddlewareOptions extends WebhookHandlerConfig {\n /** Event handlers to register */\n handlers?: Partial<Record<WebhookEventType, WebhookEventHandler>>;\n /** Catch-all handler */\n onAny?: WebhookAnyHandler;\n}\n\n/**\n * Create an Express/Connect-compatible middleware for webhook handling.\n *\n * Expects the raw body to be available as `req.body` (string).\n * Use `express.raw({ type: 'application/json' })` or similar middleware upstream.\n */\nexport function createWebhookMiddleware(options: WebhookMiddlewareOptions) {\n const handler = buildHandler(options);\n const signatureHeader = options.signatureHeader || 'x-webhook-signature';\n\n return async (\n req: { body: string | Buffer; headers: Record<string, string | string[] | undefined> },\n res: { status(code: number): { json(body: unknown): void }; json?(body: unknown): void },\n next?: (err?: unknown) => void\n ) => {\n try {\n const body = typeof req.body === 'string' ? req.body : req.body.toString('utf-8');\n const signature = req.headers[signatureHeader];\n\n if (!signature || typeof signature !== 'string') {\n res.status(401).json({ error: 'Missing webhook signature' });\n return;\n }\n\n await handler.handle(body, signature);\n res.status(200).json({ received: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Webhook processing failed';\n\n if (message.includes('signature')) {\n res.status(401).json({ error: message });\n } else if (message.includes('tolerance') || message.includes('timestamp')) {\n res.status(400).json({ error: message });\n } else if (message.includes('Duplicate')) {\n res.status(409).json({ error: message });\n } else if (next) {\n next(error);\n } else {\n res.status(500).json({ error: message });\n }\n }\n };\n}\n\n/**\n * Create a Next.js App Router-compatible webhook handler.\n *\n * Returns an async function `(request: Request) => Promise<Response>`.\n */\nexport function createNextWebhookHandler(\n options: WebhookMiddlewareOptions\n): (request: Request) => Promise<Response> {\n const handler = buildHandler(options);\n const signatureHeader = options.signatureHeader || 'x-webhook-signature';\n\n return async (request: Request): Promise<Response> => {\n try {\n const body = await request.text();\n const signature = request.headers.get(signatureHeader);\n\n if (!signature) {\n return new Response(JSON.stringify({ error: 'Missing webhook signature' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n\n await handler.handle(body, signature);\n\n return new Response(JSON.stringify({ received: true }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Webhook processing failed';\n const status = message.includes('signature')\n ? 401\n : message.includes('tolerance') || message.includes('timestamp')\n ? 400\n : message.includes('Duplicate')\n ? 409\n : 500;\n\n return new Response(JSON.stringify({ error: message }), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n };\n}\n\nfunction buildHandler(options: WebhookMiddlewareOptions): WebhookHandler {\n const handler = new WebhookHandler(options);\n\n if (options.handlers) {\n for (const [eventType, eventHandler] of Object.entries(options.handlers)) {\n if (eventHandler) {\n handler.on(eventType as WebhookEventType, eventHandler);\n }\n }\n }\n\n if (options.onAny) {\n handler.onAny(options.onAny);\n }\n\n return handler;\n}\n"]}
|
package/dist/webhooks/index.mjs
CHANGED
|
@@ -18,7 +18,20 @@ var ValidationError = class _ValidationError extends VesantError {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
// src/core/webhook-utils.ts
|
|
21
|
-
async function verifyWebhookSignature(payload, signature, secret) {
|
|
21
|
+
async function verifyWebhookSignature(payload, signature, secret, opts = {}) {
|
|
22
|
+
const signed = parseSignedTimestamp(signature);
|
|
23
|
+
if (signed) {
|
|
24
|
+
const tolerance = opts.tolerance ?? 3e5;
|
|
25
|
+
const now = opts.now?.() ?? Date.now();
|
|
26
|
+
if (tolerance > 0 && Math.abs(now - signed.t * 1e3) > tolerance) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const expected = await computeHmacSha256(`${signed.t}.${payload}`, secret);
|
|
30
|
+
return constantTimeEqual(signed.v1, expected);
|
|
31
|
+
}
|
|
32
|
+
if (opts.allowLegacy === false) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
22
35
|
const hexDigest = await computeHmacSha256(payload, secret);
|
|
23
36
|
const expectedPrefixed = `sha256=${hexDigest}`;
|
|
24
37
|
if (signature.startsWith("sha256=")) {
|
|
@@ -26,6 +39,25 @@ async function verifyWebhookSignature(payload, signature, secret) {
|
|
|
26
39
|
}
|
|
27
40
|
return constantTimeEqual(signature, hexDigest);
|
|
28
41
|
}
|
|
42
|
+
function parseSignedTimestamp(signature) {
|
|
43
|
+
if (!signature.includes("t=") || !signature.includes("v1=")) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
let t;
|
|
47
|
+
let v1;
|
|
48
|
+
for (const part of signature.split(",")) {
|
|
49
|
+
const eq = part.indexOf("=");
|
|
50
|
+
if (eq === -1) continue;
|
|
51
|
+
const key = part.slice(0, eq).trim();
|
|
52
|
+
const value = part.slice(eq + 1).trim();
|
|
53
|
+
if (key === "t") t = Number(value);
|
|
54
|
+
else if (key === "v1") v1 = value;
|
|
55
|
+
}
|
|
56
|
+
if (t === void 0 || Number.isNaN(t) || !v1) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return { t, v1 };
|
|
60
|
+
}
|
|
29
61
|
async function computeHmacSha256(message, secret) {
|
|
30
62
|
if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.subtle) {
|
|
31
63
|
const encoder = new TextEncoder();
|
|
@@ -60,15 +92,48 @@ function constantTimeEqual(a, b) {
|
|
|
60
92
|
return result === 0;
|
|
61
93
|
}
|
|
62
94
|
|
|
95
|
+
// src/core/dedup.ts
|
|
96
|
+
var InMemoryDedupStore = class {
|
|
97
|
+
constructor(opts = {}) {
|
|
98
|
+
this.expiryById = /* @__PURE__ */ new Map();
|
|
99
|
+
this.maxEntries = opts.maxEntries ?? 1e3;
|
|
100
|
+
this.now = opts.now ?? (() => Date.now());
|
|
101
|
+
}
|
|
102
|
+
async seen(id) {
|
|
103
|
+
return this.expiryById.has(id);
|
|
104
|
+
}
|
|
105
|
+
async mark(id, ttlMs) {
|
|
106
|
+
const now = this.now();
|
|
107
|
+
this.expiryById.set(id, now + ttlMs);
|
|
108
|
+
if (this.expiryById.size > this.maxEntries) {
|
|
109
|
+
for (const [key, expiresAt] of this.expiryById) {
|
|
110
|
+
if (now > expiresAt) {
|
|
111
|
+
this.expiryById.delete(key);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
63
118
|
// src/webhooks/handler.ts
|
|
119
|
+
function resolveEventId(event) {
|
|
120
|
+
if (event.id) {
|
|
121
|
+
return event.id;
|
|
122
|
+
}
|
|
123
|
+
if (event.event_type === "notification.created") {
|
|
124
|
+
return event.notification?.id;
|
|
125
|
+
}
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
64
128
|
var WebhookHandler = class {
|
|
65
129
|
constructor(config) {
|
|
66
130
|
this.handlers = /* @__PURE__ */ new Map();
|
|
67
131
|
this.anyHandlers = [];
|
|
68
|
-
this.seenEventIds = /* @__PURE__ */ new Map();
|
|
69
132
|
this.secret = config.secret;
|
|
70
133
|
this.tolerance = config.tolerance ?? 3e5;
|
|
71
134
|
this.replayProtection = config.replayProtection ?? true;
|
|
135
|
+
this.allowLegacySignature = config.allowLegacySignature ?? true;
|
|
136
|
+
this.dedupStore = config.dedupStore ?? new InMemoryDedupStore();
|
|
72
137
|
}
|
|
73
138
|
/**
|
|
74
139
|
* Register a handler for a specific event type.
|
|
@@ -90,7 +155,10 @@ var WebhookHandler = class {
|
|
|
90
155
|
* Verify signature and parse the webhook body.
|
|
91
156
|
*/
|
|
92
157
|
async verifyAndParse(body, signature) {
|
|
93
|
-
const isValid = await verifyWebhookSignature(body, signature, this.secret
|
|
158
|
+
const isValid = await verifyWebhookSignature(body, signature, this.secret, {
|
|
159
|
+
tolerance: this.tolerance,
|
|
160
|
+
allowLegacy: this.allowLegacySignature
|
|
161
|
+
});
|
|
94
162
|
if (!isValid) {
|
|
95
163
|
throw new ValidationError("Invalid webhook signature", ["signature"]);
|
|
96
164
|
}
|
|
@@ -106,18 +174,21 @@ var WebhookHandler = class {
|
|
|
106
174
|
/**
|
|
107
175
|
* Parse an event without signature verification (for testing).
|
|
108
176
|
*/
|
|
109
|
-
parseEvent(body) {
|
|
177
|
+
async parseEvent(body) {
|
|
110
178
|
return this.parseAndValidate(body);
|
|
111
179
|
}
|
|
112
|
-
parseAndValidate(body) {
|
|
180
|
+
async parseAndValidate(body) {
|
|
113
181
|
const event = JSON.parse(body);
|
|
114
|
-
|
|
115
|
-
|
|
182
|
+
const eventId = resolveEventId(event);
|
|
183
|
+
if (!event.event_type || !eventId || !event.timestamp) {
|
|
184
|
+
throw new ValidationError(
|
|
185
|
+
"Invalid webhook event: missing required fields (event_type, id, timestamp)",
|
|
186
|
+
["event_type", "id", "timestamp"]
|
|
187
|
+
);
|
|
116
188
|
}
|
|
117
189
|
if (this.tolerance > 0) {
|
|
118
190
|
const eventTime = new Date(event.timestamp).getTime();
|
|
119
|
-
|
|
120
|
-
if (Math.abs(now - eventTime) > this.tolerance) {
|
|
191
|
+
if (Math.abs(Date.now() - eventTime) > this.tolerance) {
|
|
121
192
|
throw new ValidationError(
|
|
122
193
|
`Webhook event timestamp is outside tolerance window (${this.tolerance}ms)`,
|
|
123
194
|
["timestamp"]
|
|
@@ -125,26 +196,18 @@ var WebhookHandler = class {
|
|
|
125
196
|
}
|
|
126
197
|
}
|
|
127
198
|
if (this.replayProtection) {
|
|
128
|
-
if (this.
|
|
199
|
+
if (await this.dedupStore.seen(eventId)) {
|
|
129
200
|
throw new ValidationError(
|
|
130
|
-
`Duplicate webhook event: ${
|
|
201
|
+
`Duplicate webhook event: ${eventId} has already been processed`,
|
|
131
202
|
["id"]
|
|
132
203
|
);
|
|
133
204
|
}
|
|
134
|
-
|
|
135
|
-
this.seenEventIds.set(event.id, now);
|
|
136
|
-
if (this.seenEventIds.size > 1e3) {
|
|
137
|
-
for (const [id, seenAt] of this.seenEventIds) {
|
|
138
|
-
if (now - seenAt > this.tolerance) {
|
|
139
|
-
this.seenEventIds.delete(id);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
205
|
+
await this.dedupStore.mark(eventId, this.tolerance);
|
|
143
206
|
}
|
|
144
207
|
return event;
|
|
145
208
|
}
|
|
146
209
|
async dispatch(event) {
|
|
147
|
-
const typeHandlers = this.handlers.get(event.
|
|
210
|
+
const typeHandlers = this.handlers.get(event.event_type) || [];
|
|
148
211
|
const allHandlers = [...typeHandlers, ...this.anyHandlers];
|
|
149
212
|
for (const handler of allHandlers) {
|
|
150
213
|
await handler(event);
|
|
@@ -225,6 +288,6 @@ function buildHandler(options) {
|
|
|
225
288
|
return handler;
|
|
226
289
|
}
|
|
227
290
|
|
|
228
|
-
export { WebhookHandler, createNextWebhookHandler, createWebhookMiddleware, verifyWebhookSignature };
|
|
291
|
+
export { InMemoryDedupStore, WebhookHandler, createNextWebhookHandler, createWebhookMiddleware, verifyWebhookSignature };
|
|
229
292
|
//# sourceMappingURL=index.mjs.map
|
|
230
293
|
//# sourceMappingURL=index.mjs.map
|