@thebookingkit/server 0.1.1 → 0.1.3

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.
Files changed (68) hide show
  1. package/README.md +50 -0
  2. package/dist/adapters/email-adapter.js +2 -2
  3. package/dist/adapters/email-adapter.js.map +1 -1
  4. package/dist/adapters/job-adapter.d.ts +10 -10
  5. package/dist/adapters/job-adapter.d.ts.map +1 -1
  6. package/dist/adapters/job-adapter.js +10 -10
  7. package/dist/adapters/job-adapter.js.map +1 -1
  8. package/dist/api.d.ts +3 -3
  9. package/dist/api.js +5 -5
  10. package/dist/api.js.map +1 -1
  11. package/package.json +21 -2
  12. package/.turbo/turbo-build.log +0 -6
  13. package/.turbo/turbo-test.log +0 -20
  14. package/CHANGELOG.md +0 -9
  15. package/dist/__tests__/api.test.d.ts +0 -2
  16. package/dist/__tests__/api.test.d.ts.map +0 -1
  17. package/dist/__tests__/api.test.js +0 -280
  18. package/dist/__tests__/api.test.js.map +0 -1
  19. package/dist/__tests__/auth.test.d.ts +0 -2
  20. package/dist/__tests__/auth.test.d.ts.map +0 -1
  21. package/dist/__tests__/auth.test.js +0 -78
  22. package/dist/__tests__/auth.test.js.map +0 -1
  23. package/dist/__tests__/concurrent-booking.test.d.ts +0 -2
  24. package/dist/__tests__/concurrent-booking.test.d.ts.map +0 -1
  25. package/dist/__tests__/concurrent-booking.test.js +0 -111
  26. package/dist/__tests__/concurrent-booking.test.js.map +0 -1
  27. package/dist/__tests__/multi-tenancy.test.d.ts +0 -2
  28. package/dist/__tests__/multi-tenancy.test.d.ts.map +0 -1
  29. package/dist/__tests__/multi-tenancy.test.js +0 -196
  30. package/dist/__tests__/multi-tenancy.test.js.map +0 -1
  31. package/dist/__tests__/serialization-retry.test.d.ts +0 -2
  32. package/dist/__tests__/serialization-retry.test.d.ts.map +0 -1
  33. package/dist/__tests__/serialization-retry.test.js +0 -53
  34. package/dist/__tests__/serialization-retry.test.js.map +0 -1
  35. package/dist/__tests__/webhooks.test.d.ts +0 -2
  36. package/dist/__tests__/webhooks.test.d.ts.map +0 -1
  37. package/dist/__tests__/webhooks.test.js +0 -286
  38. package/dist/__tests__/webhooks.test.js.map +0 -1
  39. package/dist/__tests__/workflows.test.d.ts +0 -2
  40. package/dist/__tests__/workflows.test.d.ts.map +0 -1
  41. package/dist/__tests__/workflows.test.js +0 -299
  42. package/dist/__tests__/workflows.test.js.map +0 -1
  43. package/src/__tests__/api.test.ts +0 -354
  44. package/src/__tests__/auth.test.ts +0 -111
  45. package/src/__tests__/concurrent-booking.test.ts +0 -170
  46. package/src/__tests__/multi-tenancy.test.ts +0 -267
  47. package/src/__tests__/serialization-retry.test.ts +0 -76
  48. package/src/__tests__/webhooks.test.ts +0 -412
  49. package/src/__tests__/workflows.test.ts +0 -422
  50. package/src/adapters/calendar-adapter.ts +0 -49
  51. package/src/adapters/email-adapter.ts +0 -108
  52. package/src/adapters/index.ts +0 -36
  53. package/src/adapters/job-adapter.ts +0 -26
  54. package/src/adapters/payment-adapter.ts +0 -118
  55. package/src/adapters/sms-adapter.ts +0 -35
  56. package/src/adapters/storage-adapter.ts +0 -11
  57. package/src/api.ts +0 -446
  58. package/src/auth.ts +0 -146
  59. package/src/booking-tokens.ts +0 -61
  60. package/src/email-templates.ts +0 -140
  61. package/src/index.ts +0 -192
  62. package/src/multi-tenancy.ts +0 -301
  63. package/src/notification-jobs.ts +0 -428
  64. package/src/serialization-retry.ts +0 -94
  65. package/src/webhooks.ts +0 -378
  66. package/src/workflows.ts +0 -441
  67. package/tsconfig.json +0 -9
  68. package/vitest.config.ts +0 -7
package/src/webhooks.ts DELETED
@@ -1,378 +0,0 @@
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
-
8
- import { createHmac } from "node:crypto";
9
-
10
- // ---------------------------------------------------------------------------
11
- // Types
12
- // ---------------------------------------------------------------------------
13
-
14
- /** Supported webhook trigger events */
15
- export type WebhookTrigger =
16
- | "BOOKING_CREATED"
17
- | "BOOKING_CONFIRMED"
18
- | "BOOKING_CANCELLED"
19
- | "BOOKING_RESCHEDULED"
20
- | "BOOKING_REJECTED"
21
- | "BOOKING_PAID"
22
- | "BOOKING_NO_SHOW"
23
- | "FORM_SUBMITTED"
24
- | "OOO_CREATED";
25
-
26
- /** Attendee in a webhook payload */
27
- export interface WebhookAttendee {
28
- email: string;
29
- name: string;
30
- phone?: string;
31
- }
32
-
33
- /** Standard webhook payload body */
34
- export interface WebhookPayload {
35
- bookingId: string;
36
- eventType: string;
37
- startTime: string;
38
- endTime: string;
39
- organizer: { name: string; email: string };
40
- attendees: WebhookAttendee[];
41
- status: string;
42
- responses?: Record<string, unknown>;
43
- metadata?: Record<string, unknown>;
44
- }
45
-
46
- /** Full webhook envelope */
47
- export interface WebhookEnvelope {
48
- triggerEvent: WebhookTrigger;
49
- createdAt: string;
50
- payload: WebhookPayload;
51
- }
52
-
53
- /** A webhook subscription definition */
54
- export interface WebhookSubscription {
55
- id: string;
56
- subscriberUrl: string;
57
- triggers: WebhookTrigger[];
58
- secret?: string;
59
- isActive: boolean;
60
- /** Optional scope */
61
- eventTypeId?: string;
62
- teamId?: string;
63
- /** Optional custom payload template (JSON with {{variable}} placeholders) */
64
- payloadTemplate?: string;
65
- }
66
-
67
- /** Result of a webhook delivery attempt */
68
- export interface WebhookDeliveryResult {
69
- webhookId: string;
70
- trigger: WebhookTrigger;
71
- responseCode: number | null;
72
- success: boolean;
73
- attempt: number;
74
- deliveredAt: Date;
75
- error?: string;
76
- }
77
-
78
- /** Retry configuration for webhook delivery */
79
- export interface WebhookRetryConfig {
80
- /** Maximum number of retry attempts (default: 3) */
81
- maxRetries: number;
82
- /** Backoff delays in seconds for each retry (default: [10, 60, 300]) */
83
- backoffSeconds: number[];
84
- }
85
-
86
- /** Result of signature verification */
87
- export interface WebhookVerificationResult {
88
- valid: boolean;
89
- reason?: "timestamp_expired" | "signature_mismatch";
90
- }
91
-
92
- // ---------------------------------------------------------------------------
93
- // Constants
94
- // ---------------------------------------------------------------------------
95
-
96
- /** Default retry configuration */
97
- export const DEFAULT_RETRY_CONFIG: WebhookRetryConfig = {
98
- maxRetries: 3,
99
- backoffSeconds: [10, 60, 300],
100
- };
101
-
102
- /** All valid webhook triggers */
103
- export const WEBHOOK_TRIGGERS: WebhookTrigger[] = [
104
- "BOOKING_CREATED",
105
- "BOOKING_CONFIRMED",
106
- "BOOKING_CANCELLED",
107
- "BOOKING_RESCHEDULED",
108
- "BOOKING_REJECTED",
109
- "BOOKING_PAID",
110
- "BOOKING_NO_SHOW",
111
- "FORM_SUBMITTED",
112
- "OOO_CREATED",
113
- ];
114
-
115
- /** Signature header name */
116
- export const SIGNATURE_HEADER = "X-SlotKit-Signature";
117
-
118
- /** Timestamp header name */
119
- export const TIMESTAMP_HEADER = "X-SlotKit-Timestamp";
120
-
121
- /** Default tolerance window in seconds (5 minutes) */
122
- export const DEFAULT_TOLERANCE_SECONDS = 300;
123
-
124
- // ---------------------------------------------------------------------------
125
- // Errors
126
- // ---------------------------------------------------------------------------
127
-
128
- /** Error thrown when webhook validation fails */
129
- export class WebhookValidationError extends Error {
130
- constructor(message: string) {
131
- super(message);
132
- this.name = "WebhookValidationError";
133
- }
134
- }
135
-
136
- // ---------------------------------------------------------------------------
137
- // Signing & Verification
138
- // ---------------------------------------------------------------------------
139
-
140
- /**
141
- * Create an HMAC-SHA256 signature for a webhook payload.
142
- *
143
- * Signature = HMAC-SHA256(secret, timestamp + '.' + rawBody)
144
- *
145
- * @param rawBody - The raw JSON string of the payload
146
- * @param secret - The webhook secret key
147
- * @param timestampSeconds - Unix timestamp in seconds
148
- * @returns The hex-encoded HMAC signature
149
- */
150
- export function signWebhookPayload(
151
- rawBody: string,
152
- secret: string,
153
- timestampSeconds: number,
154
- ): string {
155
- const message = `${timestampSeconds}.${rawBody}`;
156
- return createHmac("sha256", secret).update(message).digest("hex");
157
- }
158
-
159
- /**
160
- * Verify a webhook signature with replay protection.
161
- *
162
- * @param rawBody - The raw JSON string of the received payload
163
- * @param signature - The value of the X-SlotKit-Signature header
164
- * @param timestampSeconds - The value of the X-SlotKit-Timestamp header (Unix seconds)
165
- * @param secret - The webhook secret key
166
- * @param options - Optional configuration
167
- * @param options.toleranceSeconds - Maximum age of the timestamp in seconds (default: 300)
168
- * @returns Verification result with reason if invalid
169
- */
170
- export function verifyWebhookSignature(
171
- rawBody: string,
172
- signature: string,
173
- timestampSeconds: number,
174
- secret: string,
175
- options?: { toleranceSeconds?: number },
176
- ): WebhookVerificationResult {
177
- const tolerance = options?.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
178
- const nowSeconds = Math.floor(Date.now() / 1000);
179
- const age = nowSeconds - timestampSeconds;
180
-
181
- // Check replay protection
182
- if (age > tolerance || age < -tolerance) {
183
- return { valid: false, reason: "timestamp_expired" };
184
- }
185
-
186
- // Verify HMAC
187
- const expectedSignature = signWebhookPayload(
188
- rawBody,
189
- secret,
190
- timestampSeconds,
191
- );
192
-
193
- // Constant-time comparison
194
- if (expectedSignature.length !== signature.length) {
195
- return { valid: false, reason: "signature_mismatch" };
196
- }
197
-
198
- const a = Buffer.from(expectedSignature, "hex");
199
- const b = Buffer.from(signature, "hex");
200
-
201
- if (a.length !== b.length) {
202
- return { valid: false, reason: "signature_mismatch" };
203
- }
204
-
205
- let mismatch = 0;
206
- for (let i = 0; i < a.length; i++) {
207
- mismatch |= a[i] ^ b[i];
208
- }
209
-
210
- if (mismatch !== 0) {
211
- return { valid: false, reason: "signature_mismatch" };
212
- }
213
-
214
- return { valid: true };
215
- }
216
-
217
- // ---------------------------------------------------------------------------
218
- // Envelope Construction
219
- // ---------------------------------------------------------------------------
220
-
221
- /**
222
- * Create a standard webhook envelope.
223
- *
224
- * @param trigger - The trigger event
225
- * @param payload - The webhook payload data
226
- * @returns The full webhook envelope
227
- */
228
- export function createWebhookEnvelope(
229
- trigger: WebhookTrigger,
230
- payload: WebhookPayload,
231
- ): WebhookEnvelope {
232
- return {
233
- triggerEvent: trigger,
234
- createdAt: new Date().toISOString(),
235
- payload,
236
- };
237
- }
238
-
239
- // ---------------------------------------------------------------------------
240
- // Custom Payload Templates
241
- // ---------------------------------------------------------------------------
242
-
243
- /**
244
- * Resolve a custom payload template with webhook data.
245
- *
246
- * Replaces `{{variable}}` placeholders with values from the envelope.
247
- * Supported variables: triggerEvent, createdAt, bookingId, eventType,
248
- * startTime, endTime, status, and any workflow template variables.
249
- *
250
- * @param template - The JSON template string with {{variable}} placeholders
251
- * @param envelope - The webhook envelope
252
- * @returns The resolved JSON string
253
- */
254
- export function resolvePayloadTemplate(
255
- template: string,
256
- envelope: WebhookEnvelope,
257
- ): string {
258
- const vars: Record<string, string> = {
259
- "{{triggerEvent}}": envelope.triggerEvent,
260
- "{{createdAt}}": envelope.createdAt,
261
- "{{bookingId}}": envelope.payload.bookingId,
262
- "{{eventType}}": envelope.payload.eventType,
263
- "{{startTime}}": envelope.payload.startTime,
264
- "{{endTime}}": envelope.payload.endTime,
265
- "{{status}}": envelope.payload.status,
266
- "{{organizerName}}": envelope.payload.organizer.name,
267
- "{{organizerEmail}}": envelope.payload.organizer.email,
268
- };
269
-
270
- let result = template;
271
- for (const [key, value] of Object.entries(vars)) {
272
- result = result.replaceAll(key, value);
273
- }
274
-
275
- return result;
276
- }
277
-
278
- // ---------------------------------------------------------------------------
279
- // Subscription Matching
280
- // ---------------------------------------------------------------------------
281
-
282
- /**
283
- * Find all active webhook subscriptions that match a trigger and optional scope.
284
- *
285
- * @param subscriptions - All available webhook subscriptions
286
- * @param trigger - The trigger event that occurred
287
- * @param scope - Optional scope filters (eventTypeId, teamId)
288
- * @returns Matching subscriptions
289
- */
290
- export function matchWebhookSubscriptions(
291
- subscriptions: WebhookSubscription[],
292
- trigger: WebhookTrigger,
293
- scope?: { eventTypeId?: string; teamId?: string },
294
- ): WebhookSubscription[] {
295
- return subscriptions.filter((sub) => {
296
- if (!sub.isActive) return false;
297
- if (!sub.triggers.includes(trigger)) return false;
298
-
299
- // Scope filtering: subscription must match if it has a scope
300
- if (sub.eventTypeId && scope?.eventTypeId !== sub.eventTypeId) {
301
- return false;
302
- }
303
- if (sub.teamId && scope?.teamId !== sub.teamId) {
304
- return false;
305
- }
306
-
307
- return true;
308
- });
309
- }
310
-
311
- // ---------------------------------------------------------------------------
312
- // Retry Logic
313
- // ---------------------------------------------------------------------------
314
-
315
- /**
316
- * Determine the delay before the next retry attempt.
317
- *
318
- * @param attempt - The current attempt number (0-indexed)
319
- * @param config - Retry configuration
320
- * @returns Delay in seconds, or null if max retries exceeded
321
- */
322
- export function getRetryDelay(
323
- attempt: number,
324
- config: WebhookRetryConfig = DEFAULT_RETRY_CONFIG,
325
- ): number | null {
326
- if (attempt >= config.maxRetries) return null;
327
- return config.backoffSeconds[attempt] ?? config.backoffSeconds[config.backoffSeconds.length - 1];
328
- }
329
-
330
- /**
331
- * Determine if a response code indicates success (2xx).
332
- *
333
- * @param statusCode - HTTP response status code
334
- * @returns Whether the delivery was successful
335
- */
336
- export function isSuccessResponse(statusCode: number): boolean {
337
- return statusCode >= 200 && statusCode < 300;
338
- }
339
-
340
- // ---------------------------------------------------------------------------
341
- // Validation
342
- // ---------------------------------------------------------------------------
343
-
344
- /**
345
- * Validate a webhook subscription.
346
- *
347
- * @param subscription - The subscription to validate
348
- * @throws {WebhookValidationError} If the subscription is invalid
349
- */
350
- export function validateWebhookSubscription(
351
- subscription: Omit<WebhookSubscription, "id">,
352
- ): void {
353
- if (!subscription.subscriberUrl) {
354
- throw new WebhookValidationError("Subscriber URL is required");
355
- }
356
-
357
- try {
358
- new URL(subscription.subscriberUrl);
359
- } catch {
360
- throw new WebhookValidationError(
361
- `Invalid subscriber URL: "${subscription.subscriberUrl}"`,
362
- );
363
- }
364
-
365
- if (!Array.isArray(subscription.triggers) || subscription.triggers.length === 0) {
366
- throw new WebhookValidationError(
367
- "At least one trigger is required",
368
- );
369
- }
370
-
371
- for (const trigger of subscription.triggers) {
372
- if (!WEBHOOK_TRIGGERS.includes(trigger)) {
373
- throw new WebhookValidationError(
374
- `Invalid trigger: "${trigger}". Must be one of: ${WEBHOOK_TRIGGERS.join(", ")}`,
375
- );
376
- }
377
- }
378
- }