@thebookingkit/server 0.1.2 → 0.1.5
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 +2 -2
- package/dist/adapters/calendar-adapter.d.ts +47 -0
- package/dist/adapters/calendar-adapter.d.ts.map +1 -0
- package/dist/adapters/calendar-adapter.js +2 -0
- package/dist/adapters/calendar-adapter.js.map +1 -0
- package/dist/adapters/email-adapter.d.ts +65 -0
- package/dist/adapters/email-adapter.d.ts.map +1 -0
- package/dist/adapters/email-adapter.js +41 -0
- package/dist/adapters/email-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/job-adapter.d.ts +26 -0
- package/dist/adapters/job-adapter.d.ts.map +1 -0
- package/dist/adapters/job-adapter.js +13 -0
- package/dist/adapters/job-adapter.js.map +1 -0
- package/dist/adapters/payment-adapter.d.ts +106 -0
- package/dist/adapters/payment-adapter.d.ts.map +1 -0
- package/dist/adapters/payment-adapter.js +8 -0
- package/dist/adapters/payment-adapter.js.map +1 -0
- package/dist/adapters/sms-adapter.d.ts +33 -0
- package/dist/adapters/sms-adapter.d.ts.map +1 -0
- package/dist/adapters/sms-adapter.js +8 -0
- package/dist/adapters/sms-adapter.js.map +1 -0
- package/{src/adapters/storage-adapter.ts → dist/adapters/storage-adapter.d.ts} +5 -4
- package/dist/adapters/storage-adapter.d.ts.map +1 -0
- package/dist/adapters/storage-adapter.js +2 -0
- package/dist/adapters/storage-adapter.js.map +1 -0
- package/dist/api.d.ts +223 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +286 -0
- package/dist/api.js.map +1 -0
- package/dist/auth.d.ts +71 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +90 -0
- package/dist/auth.js.map +1 -0
- package/dist/booking-tokens.d.ts +23 -0
- package/dist/booking-tokens.d.ts.map +1 -0
- package/dist/booking-tokens.js +52 -0
- package/dist/booking-tokens.js.map +1 -0
- package/dist/email-templates.d.ts +32 -0
- package/dist/email-templates.d.ts.map +1 -0
- package/{src/email-templates.ts → dist/email-templates.js} +14 -34
- package/dist/email-templates.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/multi-tenancy.d.ts +123 -0
- package/dist/multi-tenancy.d.ts.map +1 -0
- package/dist/multi-tenancy.js +197 -0
- package/dist/multi-tenancy.js.map +1 -0
- package/dist/notification-jobs.d.ts +143 -0
- package/dist/notification-jobs.d.ts.map +1 -0
- package/dist/notification-jobs.js +278 -0
- package/dist/notification-jobs.js.map +1 -0
- package/dist/serialization-retry.d.ts +28 -0
- package/dist/serialization-retry.d.ts.map +1 -0
- package/dist/serialization-retry.js +71 -0
- package/dist/serialization-retry.js.map +1 -0
- package/dist/webhooks.d.ts +164 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +239 -0
- package/dist/webhooks.js.map +1 -0
- package/dist/workflows.d.ts +169 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +271 -0
- package/dist/workflows.js.map +1 -0
- package/package.json +21 -2
- package/CHANGELOG.md +0 -9
- package/src/__tests__/api.test.ts +0 -354
- package/src/__tests__/auth.test.ts +0 -111
- package/src/__tests__/concurrent-booking.test.ts +0 -170
- package/src/__tests__/multi-tenancy.test.ts +0 -267
- package/src/__tests__/serialization-retry.test.ts +0 -76
- package/src/__tests__/webhooks.test.ts +0 -412
- package/src/__tests__/workflows.test.ts +0 -422
- package/src/adapters/calendar-adapter.ts +0 -49
- package/src/adapters/email-adapter.ts +0 -108
- package/src/adapters/index.ts +0 -36
- package/src/adapters/job-adapter.ts +0 -26
- package/src/adapters/payment-adapter.ts +0 -118
- package/src/adapters/sms-adapter.ts +0 -35
- package/src/api.ts +0 -446
- package/src/auth.ts +0 -146
- package/src/booking-tokens.ts +0 -61
- package/src/index.ts +0 -192
- package/src/multi-tenancy.ts +0 -301
- package/src/notification-jobs.ts +0 -428
- package/src/serialization-retry.ts +0 -94
- package/src/webhooks.ts +0 -378
- package/src/workflows.ts +0 -441
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -7
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialization-retry.d.ts","sourceRoot":"","sources":["../src/serialization-retry.ts"],"names":[],"mappings":"AAKA,kDAAkD;AAClD,MAAM,WAAW,wBAAwB;IACvC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAyBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAC3C,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,CAAC,EAAE,wBAAwB,GACjC,OAAO,CAAC,CAAC,CAAC,CAmCZ"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { BookingConflictError, SerializationRetryExhaustedError, } from "@thebookingkit/core";
|
|
2
|
+
/**
|
|
3
|
+
* Postgres SQLSTATE codes we handle:
|
|
4
|
+
* - 40001: serialization_failure (SERIALIZABLE transaction contention)
|
|
5
|
+
* - 23P01: exclusion_violation (EXCLUDE constraint — slot already taken)
|
|
6
|
+
*/
|
|
7
|
+
const SERIALIZATION_FAILURE = "40001";
|
|
8
|
+
const EXCLUSION_VIOLATION = "23P01";
|
|
9
|
+
/**
|
|
10
|
+
* Check if an error is a Postgres error with a specific code.
|
|
11
|
+
*/
|
|
12
|
+
function getPostgresErrorCode(error) {
|
|
13
|
+
if (error &&
|
|
14
|
+
typeof error === "object" &&
|
|
15
|
+
"code" in error &&
|
|
16
|
+
typeof error.code === "string") {
|
|
17
|
+
return error.code;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Wraps a database operation in serialization retry logic.
|
|
23
|
+
*
|
|
24
|
+
* If the operation fails with SQLSTATE 40001 (serialization_failure),
|
|
25
|
+
* retries up to `maxRetries` times with jittered exponential backoff.
|
|
26
|
+
*
|
|
27
|
+
* If the operation fails with SQLSTATE 23P01 (exclusion_violation),
|
|
28
|
+
* immediately throws a `BookingConflictError` (no retry — the slot is taken).
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const booking = await withSerializableRetry(
|
|
33
|
+
* () => db.transaction(async (tx) => {
|
|
34
|
+
* // insert booking in SERIALIZABLE isolation
|
|
35
|
+
* }),
|
|
36
|
+
* { maxRetries: 3 }
|
|
37
|
+
* );
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export async function withSerializableRetry(fn, options) {
|
|
41
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
42
|
+
const baseDelayMs = options?.baseDelayMs ?? 50;
|
|
43
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
44
|
+
try {
|
|
45
|
+
return await fn();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const code = getPostgresErrorCode(error);
|
|
49
|
+
// Exclusion violation — slot is taken, do not retry
|
|
50
|
+
if (code === EXCLUSION_VIOLATION) {
|
|
51
|
+
throw new BookingConflictError();
|
|
52
|
+
}
|
|
53
|
+
// Serialization failure — retry with backoff
|
|
54
|
+
if (code === SERIALIZATION_FAILURE && attempt < maxRetries) {
|
|
55
|
+
const delay = baseDelayMs * Math.pow(2, attempt);
|
|
56
|
+
const jitter = Math.random() * delay;
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// All retries exhausted for serialization failure
|
|
61
|
+
if (code === SERIALIZATION_FAILURE) {
|
|
62
|
+
throw new SerializationRetryExhaustedError(maxRetries);
|
|
63
|
+
}
|
|
64
|
+
// Unknown error — rethrow as-is
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Should never reach here, but TypeScript needs it
|
|
69
|
+
throw new SerializationRetryExhaustedError(maxRetries);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=serialization-retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialization-retry.js","sourceRoot":"","sources":["../src/serialization-retry.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,gCAAgC,GACjC,MAAM,qBAAqB,CAAC;AAU7B;;;;GAIG;AACH,MAAM,qBAAqB,GAAG,OAAO,CAAC;AACtC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAEpC;;GAEG;AACH,SAAS,oBAAoB,CAAC,KAAc;IAC1C,IACE,KAAK;QACL,OAAO,KAAK,KAAK,QAAQ;QACzB,MAAM,IAAI,KAAK;QACf,OAAQ,KAA2B,CAAC,IAAI,KAAK,QAAQ,EACrD,CAAC;QACD,OAAQ,KAA0B,CAAC,IAAI,CAAC;IAC1C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,EAAoB,EACpB,OAAkC;IAElC,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,CAAC,CAAC;IAC5C,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,EAAE,CAAC;IAE/C,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC;YAEzC,oDAAoD;YACpD,IAAI,IAAI,KAAK,mBAAmB,EAAE,CAAC;gBACjC,MAAM,IAAI,oBAAoB,EAAE,CAAC;YACnC,CAAC;YAED,6CAA6C;YAC7C,IAAI,IAAI,KAAK,qBAAqB,IAAI,OAAO,GAAG,UAAU,EAAE,CAAC;gBAC3D,MAAM,KAAK,GAAG,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;gBACjD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC;gBACrC,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC;gBACpE,SAAS;YACX,CAAC;YAED,kDAAkD;YAClD,IAAI,IAAI,KAAK,qBAAqB,EAAE,CAAC;gBACnC,MAAM,IAAI,gCAAgC,CAAC,UAAU,CAAC,CAAC;YACzD,CAAC;YAED,gCAAgC;YAChC,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,mDAAmD;IACnD,MAAM,IAAI,gCAAgC,CAAC,UAAU,CAAC,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook infrastructure for event-driven integrations.
|
|
3
|
+
*
|
|
4
|
+
* Includes typed payloads, HMAC-SHA256 signing with replay protection,
|
|
5
|
+
* retry logic, and custom payload templates.
|
|
6
|
+
*/
|
|
7
|
+
/** Supported webhook trigger events */
|
|
8
|
+
export type WebhookTrigger = "BOOKING_CREATED" | "BOOKING_CONFIRMED" | "BOOKING_CANCELLED" | "BOOKING_RESCHEDULED" | "BOOKING_REJECTED" | "BOOKING_PAID" | "BOOKING_NO_SHOW" | "FORM_SUBMITTED" | "OOO_CREATED";
|
|
9
|
+
/** Attendee in a webhook payload */
|
|
10
|
+
export interface WebhookAttendee {
|
|
11
|
+
email: string;
|
|
12
|
+
name: string;
|
|
13
|
+
phone?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Standard webhook payload body */
|
|
16
|
+
export interface WebhookPayload {
|
|
17
|
+
bookingId: string;
|
|
18
|
+
eventType: string;
|
|
19
|
+
startTime: string;
|
|
20
|
+
endTime: string;
|
|
21
|
+
organizer: {
|
|
22
|
+
name: string;
|
|
23
|
+
email: string;
|
|
24
|
+
};
|
|
25
|
+
attendees: WebhookAttendee[];
|
|
26
|
+
status: string;
|
|
27
|
+
responses?: Record<string, unknown>;
|
|
28
|
+
metadata?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/** Full webhook envelope */
|
|
31
|
+
export interface WebhookEnvelope {
|
|
32
|
+
triggerEvent: WebhookTrigger;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
payload: WebhookPayload;
|
|
35
|
+
}
|
|
36
|
+
/** A webhook subscription definition */
|
|
37
|
+
export interface WebhookSubscription {
|
|
38
|
+
id: string;
|
|
39
|
+
subscriberUrl: string;
|
|
40
|
+
triggers: WebhookTrigger[];
|
|
41
|
+
secret?: string;
|
|
42
|
+
isActive: boolean;
|
|
43
|
+
/** Optional scope */
|
|
44
|
+
eventTypeId?: string;
|
|
45
|
+
teamId?: string;
|
|
46
|
+
/** Optional custom payload template (JSON with {{variable}} placeholders) */
|
|
47
|
+
payloadTemplate?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Result of a webhook delivery attempt */
|
|
50
|
+
export interface WebhookDeliveryResult {
|
|
51
|
+
webhookId: string;
|
|
52
|
+
trigger: WebhookTrigger;
|
|
53
|
+
responseCode: number | null;
|
|
54
|
+
success: boolean;
|
|
55
|
+
attempt: number;
|
|
56
|
+
deliveredAt: Date;
|
|
57
|
+
error?: string;
|
|
58
|
+
}
|
|
59
|
+
/** Retry configuration for webhook delivery */
|
|
60
|
+
export interface WebhookRetryConfig {
|
|
61
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
62
|
+
maxRetries: number;
|
|
63
|
+
/** Backoff delays in seconds for each retry (default: [10, 60, 300]) */
|
|
64
|
+
backoffSeconds: number[];
|
|
65
|
+
}
|
|
66
|
+
/** Result of signature verification */
|
|
67
|
+
export interface WebhookVerificationResult {
|
|
68
|
+
valid: boolean;
|
|
69
|
+
reason?: "timestamp_expired" | "signature_mismatch";
|
|
70
|
+
}
|
|
71
|
+
/** Default retry configuration */
|
|
72
|
+
export declare const DEFAULT_RETRY_CONFIG: WebhookRetryConfig;
|
|
73
|
+
/** All valid webhook triggers */
|
|
74
|
+
export declare const WEBHOOK_TRIGGERS: WebhookTrigger[];
|
|
75
|
+
/** Signature header name */
|
|
76
|
+
export declare const SIGNATURE_HEADER = "X-SlotKit-Signature";
|
|
77
|
+
/** Timestamp header name */
|
|
78
|
+
export declare const TIMESTAMP_HEADER = "X-SlotKit-Timestamp";
|
|
79
|
+
/** Default tolerance window in seconds (5 minutes) */
|
|
80
|
+
export declare const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
81
|
+
/** Error thrown when webhook validation fails */
|
|
82
|
+
export declare class WebhookValidationError extends Error {
|
|
83
|
+
constructor(message: string);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create an HMAC-SHA256 signature for a webhook payload.
|
|
87
|
+
*
|
|
88
|
+
* Signature = HMAC-SHA256(secret, timestamp + '.' + rawBody)
|
|
89
|
+
*
|
|
90
|
+
* @param rawBody - The raw JSON string of the payload
|
|
91
|
+
* @param secret - The webhook secret key
|
|
92
|
+
* @param timestampSeconds - Unix timestamp in seconds
|
|
93
|
+
* @returns The hex-encoded HMAC signature
|
|
94
|
+
*/
|
|
95
|
+
export declare function signWebhookPayload(rawBody: string, secret: string, timestampSeconds: number): string;
|
|
96
|
+
/**
|
|
97
|
+
* Verify a webhook signature with replay protection.
|
|
98
|
+
*
|
|
99
|
+
* @param rawBody - The raw JSON string of the received payload
|
|
100
|
+
* @param signature - The value of the X-SlotKit-Signature header
|
|
101
|
+
* @param timestampSeconds - The value of the X-SlotKit-Timestamp header (Unix seconds)
|
|
102
|
+
* @param secret - The webhook secret key
|
|
103
|
+
* @param options - Optional configuration
|
|
104
|
+
* @param options.toleranceSeconds - Maximum age of the timestamp in seconds (default: 300)
|
|
105
|
+
* @returns Verification result with reason if invalid
|
|
106
|
+
*/
|
|
107
|
+
export declare function verifyWebhookSignature(rawBody: string, signature: string, timestampSeconds: number, secret: string, options?: {
|
|
108
|
+
toleranceSeconds?: number;
|
|
109
|
+
}): WebhookVerificationResult;
|
|
110
|
+
/**
|
|
111
|
+
* Create a standard webhook envelope.
|
|
112
|
+
*
|
|
113
|
+
* @param trigger - The trigger event
|
|
114
|
+
* @param payload - The webhook payload data
|
|
115
|
+
* @returns The full webhook envelope
|
|
116
|
+
*/
|
|
117
|
+
export declare function createWebhookEnvelope(trigger: WebhookTrigger, payload: WebhookPayload): WebhookEnvelope;
|
|
118
|
+
/**
|
|
119
|
+
* Resolve a custom payload template with webhook data.
|
|
120
|
+
*
|
|
121
|
+
* Replaces `{{variable}}` placeholders with values from the envelope.
|
|
122
|
+
* Supported variables: triggerEvent, createdAt, bookingId, eventType,
|
|
123
|
+
* startTime, endTime, status, and any workflow template variables.
|
|
124
|
+
*
|
|
125
|
+
* @param template - The JSON template string with {{variable}} placeholders
|
|
126
|
+
* @param envelope - The webhook envelope
|
|
127
|
+
* @returns The resolved JSON string
|
|
128
|
+
*/
|
|
129
|
+
export declare function resolvePayloadTemplate(template: string, envelope: WebhookEnvelope): string;
|
|
130
|
+
/**
|
|
131
|
+
* Find all active webhook subscriptions that match a trigger and optional scope.
|
|
132
|
+
*
|
|
133
|
+
* @param subscriptions - All available webhook subscriptions
|
|
134
|
+
* @param trigger - The trigger event that occurred
|
|
135
|
+
* @param scope - Optional scope filters (eventTypeId, teamId)
|
|
136
|
+
* @returns Matching subscriptions
|
|
137
|
+
*/
|
|
138
|
+
export declare function matchWebhookSubscriptions(subscriptions: WebhookSubscription[], trigger: WebhookTrigger, scope?: {
|
|
139
|
+
eventTypeId?: string;
|
|
140
|
+
teamId?: string;
|
|
141
|
+
}): WebhookSubscription[];
|
|
142
|
+
/**
|
|
143
|
+
* Determine the delay before the next retry attempt.
|
|
144
|
+
*
|
|
145
|
+
* @param attempt - The current attempt number (0-indexed)
|
|
146
|
+
* @param config - Retry configuration
|
|
147
|
+
* @returns Delay in seconds, or null if max retries exceeded
|
|
148
|
+
*/
|
|
149
|
+
export declare function getRetryDelay(attempt: number, config?: WebhookRetryConfig): number | null;
|
|
150
|
+
/**
|
|
151
|
+
* Determine if a response code indicates success (2xx).
|
|
152
|
+
*
|
|
153
|
+
* @param statusCode - HTTP response status code
|
|
154
|
+
* @returns Whether the delivery was successful
|
|
155
|
+
*/
|
|
156
|
+
export declare function isSuccessResponse(statusCode: number): boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Validate a webhook subscription.
|
|
159
|
+
*
|
|
160
|
+
* @param subscription - The subscription to validate
|
|
161
|
+
* @throws {WebhookValidationError} If the subscription is invalid
|
|
162
|
+
*/
|
|
163
|
+
export declare function validateWebhookSubscription(subscription: Omit<WebhookSubscription, "id">): void;
|
|
164
|
+
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook infrastructure for event-driven integrations.
|
|
3
|
+
*
|
|
4
|
+
* Includes typed payloads, HMAC-SHA256 signing with replay protection,
|
|
5
|
+
* retry logic, and custom payload templates.
|
|
6
|
+
*/
|
|
7
|
+
import { createHmac } from "node:crypto";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Constants
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/** Default retry configuration */
|
|
12
|
+
export const DEFAULT_RETRY_CONFIG = {
|
|
13
|
+
maxRetries: 3,
|
|
14
|
+
backoffSeconds: [10, 60, 300],
|
|
15
|
+
};
|
|
16
|
+
/** All valid webhook triggers */
|
|
17
|
+
export const WEBHOOK_TRIGGERS = [
|
|
18
|
+
"BOOKING_CREATED",
|
|
19
|
+
"BOOKING_CONFIRMED",
|
|
20
|
+
"BOOKING_CANCELLED",
|
|
21
|
+
"BOOKING_RESCHEDULED",
|
|
22
|
+
"BOOKING_REJECTED",
|
|
23
|
+
"BOOKING_PAID",
|
|
24
|
+
"BOOKING_NO_SHOW",
|
|
25
|
+
"FORM_SUBMITTED",
|
|
26
|
+
"OOO_CREATED",
|
|
27
|
+
];
|
|
28
|
+
/** Signature header name */
|
|
29
|
+
export const SIGNATURE_HEADER = "X-SlotKit-Signature";
|
|
30
|
+
/** Timestamp header name */
|
|
31
|
+
export const TIMESTAMP_HEADER = "X-SlotKit-Timestamp";
|
|
32
|
+
/** Default tolerance window in seconds (5 minutes) */
|
|
33
|
+
export const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Errors
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
/** Error thrown when webhook validation fails */
|
|
38
|
+
export class WebhookValidationError extends Error {
|
|
39
|
+
constructor(message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "WebhookValidationError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Signing & Verification
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/**
|
|
48
|
+
* Create an HMAC-SHA256 signature for a webhook payload.
|
|
49
|
+
*
|
|
50
|
+
* Signature = HMAC-SHA256(secret, timestamp + '.' + rawBody)
|
|
51
|
+
*
|
|
52
|
+
* @param rawBody - The raw JSON string of the payload
|
|
53
|
+
* @param secret - The webhook secret key
|
|
54
|
+
* @param timestampSeconds - Unix timestamp in seconds
|
|
55
|
+
* @returns The hex-encoded HMAC signature
|
|
56
|
+
*/
|
|
57
|
+
export function signWebhookPayload(rawBody, secret, timestampSeconds) {
|
|
58
|
+
const message = `${timestampSeconds}.${rawBody}`;
|
|
59
|
+
return createHmac("sha256", secret).update(message).digest("hex");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Verify a webhook signature with replay protection.
|
|
63
|
+
*
|
|
64
|
+
* @param rawBody - The raw JSON string of the received payload
|
|
65
|
+
* @param signature - The value of the X-SlotKit-Signature header
|
|
66
|
+
* @param timestampSeconds - The value of the X-SlotKit-Timestamp header (Unix seconds)
|
|
67
|
+
* @param secret - The webhook secret key
|
|
68
|
+
* @param options - Optional configuration
|
|
69
|
+
* @param options.toleranceSeconds - Maximum age of the timestamp in seconds (default: 300)
|
|
70
|
+
* @returns Verification result with reason if invalid
|
|
71
|
+
*/
|
|
72
|
+
export function verifyWebhookSignature(rawBody, signature, timestampSeconds, secret, options) {
|
|
73
|
+
const tolerance = options?.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
74
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
75
|
+
const age = nowSeconds - timestampSeconds;
|
|
76
|
+
// Check replay protection
|
|
77
|
+
if (age > tolerance || age < -tolerance) {
|
|
78
|
+
return { valid: false, reason: "timestamp_expired" };
|
|
79
|
+
}
|
|
80
|
+
// Verify HMAC
|
|
81
|
+
const expectedSignature = signWebhookPayload(rawBody, secret, timestampSeconds);
|
|
82
|
+
// Constant-time comparison
|
|
83
|
+
if (expectedSignature.length !== signature.length) {
|
|
84
|
+
return { valid: false, reason: "signature_mismatch" };
|
|
85
|
+
}
|
|
86
|
+
const a = Buffer.from(expectedSignature, "hex");
|
|
87
|
+
const b = Buffer.from(signature, "hex");
|
|
88
|
+
if (a.length !== b.length) {
|
|
89
|
+
return { valid: false, reason: "signature_mismatch" };
|
|
90
|
+
}
|
|
91
|
+
let mismatch = 0;
|
|
92
|
+
for (let i = 0; i < a.length; i++) {
|
|
93
|
+
mismatch |= a[i] ^ b[i];
|
|
94
|
+
}
|
|
95
|
+
if (mismatch !== 0) {
|
|
96
|
+
return { valid: false, reason: "signature_mismatch" };
|
|
97
|
+
}
|
|
98
|
+
return { valid: true };
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Envelope Construction
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
/**
|
|
104
|
+
* Create a standard webhook envelope.
|
|
105
|
+
*
|
|
106
|
+
* @param trigger - The trigger event
|
|
107
|
+
* @param payload - The webhook payload data
|
|
108
|
+
* @returns The full webhook envelope
|
|
109
|
+
*/
|
|
110
|
+
export function createWebhookEnvelope(trigger, payload) {
|
|
111
|
+
return {
|
|
112
|
+
triggerEvent: trigger,
|
|
113
|
+
createdAt: new Date().toISOString(),
|
|
114
|
+
payload,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Custom Payload Templates
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* Resolve a custom payload template with webhook data.
|
|
122
|
+
*
|
|
123
|
+
* Replaces `{{variable}}` placeholders with values from the envelope.
|
|
124
|
+
* Supported variables: triggerEvent, createdAt, bookingId, eventType,
|
|
125
|
+
* startTime, endTime, status, and any workflow template variables.
|
|
126
|
+
*
|
|
127
|
+
* @param template - The JSON template string with {{variable}} placeholders
|
|
128
|
+
* @param envelope - The webhook envelope
|
|
129
|
+
* @returns The resolved JSON string
|
|
130
|
+
*/
|
|
131
|
+
export function resolvePayloadTemplate(template, envelope) {
|
|
132
|
+
const vars = {
|
|
133
|
+
"{{triggerEvent}}": envelope.triggerEvent,
|
|
134
|
+
"{{createdAt}}": envelope.createdAt,
|
|
135
|
+
"{{bookingId}}": envelope.payload.bookingId,
|
|
136
|
+
"{{eventType}}": envelope.payload.eventType,
|
|
137
|
+
"{{startTime}}": envelope.payload.startTime,
|
|
138
|
+
"{{endTime}}": envelope.payload.endTime,
|
|
139
|
+
"{{status}}": envelope.payload.status,
|
|
140
|
+
"{{organizerName}}": envelope.payload.organizer.name,
|
|
141
|
+
"{{organizerEmail}}": envelope.payload.organizer.email,
|
|
142
|
+
};
|
|
143
|
+
let result = template;
|
|
144
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
145
|
+
const safeValue = value.replace(/\{/g, "\x00LBRACE\x00").replace(/\}/g, "\x00RBRACE\x00");
|
|
146
|
+
result = result.replaceAll(key, safeValue);
|
|
147
|
+
}
|
|
148
|
+
result = result.replace(/\x00LBRACE\x00/g, "{").replace(/\x00RBRACE\x00/g, "}");
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Subscription Matching
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
/**
|
|
155
|
+
* Find all active webhook subscriptions that match a trigger and optional scope.
|
|
156
|
+
*
|
|
157
|
+
* @param subscriptions - All available webhook subscriptions
|
|
158
|
+
* @param trigger - The trigger event that occurred
|
|
159
|
+
* @param scope - Optional scope filters (eventTypeId, teamId)
|
|
160
|
+
* @returns Matching subscriptions
|
|
161
|
+
*/
|
|
162
|
+
export function matchWebhookSubscriptions(subscriptions, trigger, scope) {
|
|
163
|
+
return subscriptions.filter((sub) => {
|
|
164
|
+
if (!sub.isActive)
|
|
165
|
+
return false;
|
|
166
|
+
if (!sub.triggers.includes(trigger))
|
|
167
|
+
return false;
|
|
168
|
+
// Scope filtering: subscription must match if it has a scope
|
|
169
|
+
if (sub.eventTypeId && scope?.eventTypeId !== sub.eventTypeId) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (sub.teamId && scope?.teamId !== sub.teamId) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Retry Logic
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
/**
|
|
182
|
+
* Determine the delay before the next retry attempt.
|
|
183
|
+
*
|
|
184
|
+
* @param attempt - The current attempt number (0-indexed)
|
|
185
|
+
* @param config - Retry configuration
|
|
186
|
+
* @returns Delay in seconds, or null if max retries exceeded
|
|
187
|
+
*/
|
|
188
|
+
export function getRetryDelay(attempt, config = DEFAULT_RETRY_CONFIG) {
|
|
189
|
+
if (attempt >= config.maxRetries)
|
|
190
|
+
return null;
|
|
191
|
+
return config.backoffSeconds[attempt] ?? config.backoffSeconds[config.backoffSeconds.length - 1];
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Determine if a response code indicates success (2xx).
|
|
195
|
+
*
|
|
196
|
+
* @param statusCode - HTTP response status code
|
|
197
|
+
* @returns Whether the delivery was successful
|
|
198
|
+
*/
|
|
199
|
+
export function isSuccessResponse(statusCode) {
|
|
200
|
+
return statusCode >= 200 && statusCode < 300;
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Validation
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
/**
|
|
206
|
+
* Validate a webhook subscription.
|
|
207
|
+
*
|
|
208
|
+
* @param subscription - The subscription to validate
|
|
209
|
+
* @throws {WebhookValidationError} If the subscription is invalid
|
|
210
|
+
*/
|
|
211
|
+
export function validateWebhookSubscription(subscription) {
|
|
212
|
+
if (!subscription.subscriberUrl) {
|
|
213
|
+
throw new WebhookValidationError("Subscriber URL is required");
|
|
214
|
+
}
|
|
215
|
+
let parsedUrl;
|
|
216
|
+
try {
|
|
217
|
+
parsedUrl = new URL(subscription.subscriberUrl);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
throw new WebhookValidationError(`Invalid subscriber URL: "${subscription.subscriberUrl}"`);
|
|
221
|
+
}
|
|
222
|
+
if (parsedUrl.protocol !== "https:") {
|
|
223
|
+
throw new WebhookValidationError("Subscriber URL must use HTTPS");
|
|
224
|
+
}
|
|
225
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
226
|
+
const ssrfPattern = /^(localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|\[::1\]|::1)$/;
|
|
227
|
+
if (ssrfPattern.test(hostname)) {
|
|
228
|
+
throw new WebhookValidationError(`Subscriber URL hostname is not allowed: "${hostname}"`);
|
|
229
|
+
}
|
|
230
|
+
if (!Array.isArray(subscription.triggers) || subscription.triggers.length === 0) {
|
|
231
|
+
throw new WebhookValidationError("At least one trigger is required");
|
|
232
|
+
}
|
|
233
|
+
for (const trigger of subscription.triggers) {
|
|
234
|
+
if (!WEBHOOK_TRIGGERS.includes(trigger)) {
|
|
235
|
+
throw new WebhookValidationError(`Invalid trigger: "${trigger}". Must be one of: ${WEBHOOK_TRIGGERS.join(", ")}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=webhooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow automation engine.
|
|
3
|
+
*
|
|
4
|
+
* Trigger-condition-action framework that automates tasks
|
|
5
|
+
* based on booking lifecycle events.
|
|
6
|
+
*/
|
|
7
|
+
/** Supported workflow trigger events */
|
|
8
|
+
export type WorkflowTrigger = "booking_created" | "booking_confirmed" | "booking_cancelled" | "booking_rescheduled" | "before_event" | "after_event" | "payment_received" | "payment_failed" | "no_show_confirmed" | "form_submitted";
|
|
9
|
+
/** Supported workflow action types */
|
|
10
|
+
export type WorkflowActionType = "send_email" | "send_sms" | "fire_webhook" | "update_status" | "create_calendar_event";
|
|
11
|
+
/** Condition operator for filtering */
|
|
12
|
+
export type ConditionOperator = "equals" | "not_equals" | "contains" | "in";
|
|
13
|
+
/** A single workflow condition */
|
|
14
|
+
export interface WorkflowCondition {
|
|
15
|
+
/** The field to check (e.g., "event_type_id", "status", "customer_email") */
|
|
16
|
+
field: string;
|
|
17
|
+
/** Comparison operator */
|
|
18
|
+
operator: ConditionOperator;
|
|
19
|
+
/** Value(s) to compare against */
|
|
20
|
+
value: string | string[];
|
|
21
|
+
}
|
|
22
|
+
/** Email action configuration */
|
|
23
|
+
export interface EmailActionConfig {
|
|
24
|
+
type: "send_email";
|
|
25
|
+
/** Recipient: "customer", "host", or a specific email address */
|
|
26
|
+
to: string;
|
|
27
|
+
/** Email subject (supports template variables) */
|
|
28
|
+
subject: string;
|
|
29
|
+
/** Email body template (supports template variables) */
|
|
30
|
+
body: string;
|
|
31
|
+
}
|
|
32
|
+
/** SMS action configuration */
|
|
33
|
+
export interface SmsActionConfig {
|
|
34
|
+
type: "send_sms";
|
|
35
|
+
/** Phone number field key or literal number */
|
|
36
|
+
to: string;
|
|
37
|
+
/** SMS body template (supports template variables) */
|
|
38
|
+
body: string;
|
|
39
|
+
}
|
|
40
|
+
/** Webhook action configuration */
|
|
41
|
+
export interface WebhookActionConfig {
|
|
42
|
+
type: "fire_webhook";
|
|
43
|
+
/** Target URL */
|
|
44
|
+
url: string;
|
|
45
|
+
/** Payload template (JSON string with template variables) */
|
|
46
|
+
payload?: string;
|
|
47
|
+
/** HTTP method (default: POST) */
|
|
48
|
+
method?: "POST" | "PUT" | "PATCH";
|
|
49
|
+
/** Custom headers */
|
|
50
|
+
headers?: Record<string, string>;
|
|
51
|
+
}
|
|
52
|
+
/** Status update action configuration */
|
|
53
|
+
export interface StatusUpdateActionConfig {
|
|
54
|
+
type: "update_status";
|
|
55
|
+
/** New status to set */
|
|
56
|
+
status: string;
|
|
57
|
+
}
|
|
58
|
+
/** Calendar event action configuration */
|
|
59
|
+
export interface CalendarEventActionConfig {
|
|
60
|
+
type: "create_calendar_event";
|
|
61
|
+
/** Additional notes for the calendar event */
|
|
62
|
+
notes?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Union of all action configurations */
|
|
65
|
+
export type WorkflowAction = EmailActionConfig | SmsActionConfig | WebhookActionConfig | StatusUpdateActionConfig | CalendarEventActionConfig;
|
|
66
|
+
/** A complete workflow definition */
|
|
67
|
+
export interface WorkflowDefinition {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
trigger: WorkflowTrigger;
|
|
71
|
+
conditions: WorkflowCondition[];
|
|
72
|
+
actions: WorkflowAction[];
|
|
73
|
+
isActive: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** Context data passed when evaluating a workflow */
|
|
76
|
+
export interface WorkflowContext {
|
|
77
|
+
bookingId?: string;
|
|
78
|
+
eventTypeId?: string;
|
|
79
|
+
providerId?: string;
|
|
80
|
+
customerEmail?: string;
|
|
81
|
+
customerName?: string;
|
|
82
|
+
customerPhone?: string;
|
|
83
|
+
status?: string;
|
|
84
|
+
startsAt?: Date;
|
|
85
|
+
endsAt?: Date;
|
|
86
|
+
hostName?: string;
|
|
87
|
+
eventTitle?: string;
|
|
88
|
+
eventDuration?: number;
|
|
89
|
+
eventLocation?: string;
|
|
90
|
+
managementUrl?: string;
|
|
91
|
+
/** Additional fields for condition evaluation */
|
|
92
|
+
[key: string]: unknown;
|
|
93
|
+
}
|
|
94
|
+
/** Result of a workflow execution log entry */
|
|
95
|
+
export interface WorkflowLogEntry {
|
|
96
|
+
workflowId: string;
|
|
97
|
+
bookingId?: string;
|
|
98
|
+
actionType: WorkflowActionType;
|
|
99
|
+
status: "success" | "error" | "skipped";
|
|
100
|
+
error?: string;
|
|
101
|
+
executedAt: Date;
|
|
102
|
+
}
|
|
103
|
+
/** Error thrown when workflow validation fails */
|
|
104
|
+
export declare class WorkflowValidationError extends Error {
|
|
105
|
+
constructor(message: string);
|
|
106
|
+
}
|
|
107
|
+
/** Standard template variables available in workflow messages */
|
|
108
|
+
export declare const TEMPLATE_VARIABLES: readonly ["{booking.title}", "{booking.startTime}", "{booking.endTime}", "{booking.date}", "{attendee.name}", "{attendee.email}", "{host.name}", "{event.location}", "{event.duration}", "{booking.managementUrl}"];
|
|
109
|
+
/**
|
|
110
|
+
* Resolve template variables in a string using workflow context.
|
|
111
|
+
*
|
|
112
|
+
* Missing variables are replaced with empty strings.
|
|
113
|
+
*
|
|
114
|
+
* @param template - The template string with `{variable}` placeholders
|
|
115
|
+
* @param context - The workflow context with booking/event data
|
|
116
|
+
* @returns The resolved string
|
|
117
|
+
*/
|
|
118
|
+
export declare function resolveTemplateVariables(template: string, context: WorkflowContext): string;
|
|
119
|
+
/** Default workflow templates for common scenarios */
|
|
120
|
+
export declare const DEFAULT_TEMPLATES: {
|
|
121
|
+
readonly confirmation: {
|
|
122
|
+
readonly subject: "Booking Confirmed: {booking.title}";
|
|
123
|
+
readonly body: "Hi {attendee.name},\n\nYour booking for {booking.title} on {booking.date} at {booking.startTime} has been confirmed.\n\nDuration: {event.duration}\nLocation: {event.location}\n\nManage your booking: {booking.managementUrl}\n\nBest regards,\n{host.name}";
|
|
124
|
+
};
|
|
125
|
+
readonly reminder_24h: {
|
|
126
|
+
readonly subject: "Reminder: {booking.title} tomorrow";
|
|
127
|
+
readonly body: "Hi {attendee.name},\n\nThis is a reminder that you have a booking for {booking.title} tomorrow at {booking.startTime}.\n\nLocation: {event.location}\n\nManage your booking: {booking.managementUrl}\n\nSee you soon,\n{host.name}";
|
|
128
|
+
};
|
|
129
|
+
readonly reminder_1h: {
|
|
130
|
+
readonly subject: "Reminder: {booking.title} in 1 hour";
|
|
131
|
+
readonly body: "Hi {attendee.name},\n\nYour booking for {booking.title} starts in 1 hour at {booking.startTime}.\n\nLocation: {event.location}\n\nSee you soon,\n{host.name}";
|
|
132
|
+
};
|
|
133
|
+
readonly cancellation: {
|
|
134
|
+
readonly subject: "Booking Cancelled: {booking.title}";
|
|
135
|
+
readonly body: "Hi {attendee.name},\n\nYour booking for {booking.title} on {booking.date} at {booking.startTime} has been cancelled.\n\nIf you'd like to rebook, please visit our booking page.\n\nBest regards,\n{host.name}";
|
|
136
|
+
};
|
|
137
|
+
readonly followup: {
|
|
138
|
+
readonly subject: "How was your {booking.title}?";
|
|
139
|
+
readonly body: "Hi {attendee.name},\n\nThank you for your recent {booking.title} with {host.name}.\n\nWe hope you had a great experience! If you'd like to book again, we'd love to see you.\n\nBest regards,\n{host.name}";
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Evaluate whether a workflow's conditions are met for the given context.
|
|
144
|
+
*
|
|
145
|
+
* If no conditions are defined, returns true (unconditional trigger).
|
|
146
|
+
* All conditions must match (AND logic).
|
|
147
|
+
*
|
|
148
|
+
* @param conditions - Array of workflow conditions
|
|
149
|
+
* @param context - The workflow context data
|
|
150
|
+
* @returns Whether all conditions are satisfied
|
|
151
|
+
*/
|
|
152
|
+
export declare function evaluateConditions(conditions: WorkflowCondition[], context: WorkflowContext): boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Validate a workflow definition.
|
|
155
|
+
*
|
|
156
|
+
* @param workflow - The workflow to validate
|
|
157
|
+
* @throws {WorkflowValidationError} If the workflow is invalid
|
|
158
|
+
*/
|
|
159
|
+
export declare function validateWorkflow(workflow: WorkflowDefinition): void;
|
|
160
|
+
/**
|
|
161
|
+
* Find all active workflows that match a given trigger and context.
|
|
162
|
+
*
|
|
163
|
+
* @param workflows - All available workflows
|
|
164
|
+
* @param trigger - The trigger event that occurred
|
|
165
|
+
* @param context - The workflow context data
|
|
166
|
+
* @returns Workflows that should be executed
|
|
167
|
+
*/
|
|
168
|
+
export declare function matchWorkflows(workflows: WorkflowDefinition[], trigger: WorkflowTrigger, context: WorkflowContext): WorkflowDefinition[];
|
|
169
|
+
//# sourceMappingURL=workflows.d.ts.map
|