@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
@@ -1,412 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import {
3
- signWebhookPayload,
4
- verifyWebhookSignature,
5
- createWebhookEnvelope,
6
- resolvePayloadTemplate,
7
- matchWebhookSubscriptions,
8
- getRetryDelay,
9
- isSuccessResponse,
10
- validateWebhookSubscription,
11
- WebhookValidationError,
12
- DEFAULT_RETRY_CONFIG,
13
- WEBHOOK_TRIGGERS,
14
- type WebhookSubscription,
15
- type WebhookPayload,
16
- type WebhookTrigger,
17
- } from "../webhooks.js";
18
-
19
- // ---------------------------------------------------------------------------
20
- // Fixtures
21
- // ---------------------------------------------------------------------------
22
-
23
- const testSecret = "whsec_test_secret_key_123";
24
- const testPayload = '{"triggerEvent":"BOOKING_CREATED","payload":{}}';
25
-
26
- const samplePayload: WebhookPayload = {
27
- bookingId: "bk-1",
28
- eventType: "consultation",
29
- startTime: "2026-03-15T14:00:00.000Z",
30
- endTime: "2026-03-15T14:30:00.000Z",
31
- organizer: { name: "Dr. Smith", email: "smith@example.com" },
32
- attendees: [{ email: "jane@example.com", name: "Jane Doe" }],
33
- status: "confirmed",
34
- };
35
-
36
- function makeSub(
37
- overrides?: Partial<WebhookSubscription>,
38
- ): WebhookSubscription {
39
- return {
40
- id: "wh-1",
41
- subscriberUrl: "https://api.example.com/webhooks",
42
- triggers: ["BOOKING_CREATED"],
43
- isActive: true,
44
- ...overrides,
45
- };
46
- }
47
-
48
- // ---------------------------------------------------------------------------
49
- // signWebhookPayload
50
- // ---------------------------------------------------------------------------
51
-
52
- describe("signWebhookPayload", () => {
53
- it("produces a hex-encoded HMAC-SHA256 signature", () => {
54
- const sig = signWebhookPayload(testPayload, testSecret, 1710000000);
55
- expect(sig).toMatch(/^[0-9a-f]{64}$/);
56
- });
57
-
58
- it("produces different signatures for different payloads", () => {
59
- const sig1 = signWebhookPayload("body1", testSecret, 1710000000);
60
- const sig2 = signWebhookPayload("body2", testSecret, 1710000000);
61
- expect(sig1).not.toBe(sig2);
62
- });
63
-
64
- it("produces different signatures for different timestamps", () => {
65
- const sig1 = signWebhookPayload(testPayload, testSecret, 1710000000);
66
- const sig2 = signWebhookPayload(testPayload, testSecret, 1710000001);
67
- expect(sig1).not.toBe(sig2);
68
- });
69
-
70
- it("produces different signatures for different secrets", () => {
71
- const sig1 = signWebhookPayload(testPayload, "secret1", 1710000000);
72
- const sig2 = signWebhookPayload(testPayload, "secret2", 1710000000);
73
- expect(sig1).not.toBe(sig2);
74
- });
75
- });
76
-
77
- // ---------------------------------------------------------------------------
78
- // verifyWebhookSignature
79
- // ---------------------------------------------------------------------------
80
-
81
- describe("verifyWebhookSignature", () => {
82
- beforeEach(() => {
83
- vi.useFakeTimers();
84
- });
85
-
86
- afterEach(() => {
87
- vi.useRealTimers();
88
- });
89
-
90
- it("returns valid for a correct signature within tolerance", () => {
91
- const now = 1710000000;
92
- vi.setSystemTime(now * 1000);
93
- const sig = signWebhookPayload(testPayload, testSecret, now);
94
-
95
- const result = verifyWebhookSignature(
96
- testPayload,
97
- sig,
98
- now,
99
- testSecret,
100
- );
101
- expect(result).toEqual({ valid: true });
102
- });
103
-
104
- it("returns signature_mismatch for tampered payload", () => {
105
- const now = 1710000000;
106
- vi.setSystemTime(now * 1000);
107
- const sig = signWebhookPayload(testPayload, testSecret, now);
108
-
109
- const result = verifyWebhookSignature(
110
- "tampered_body",
111
- sig,
112
- now,
113
- testSecret,
114
- );
115
- expect(result).toEqual({ valid: false, reason: "signature_mismatch" });
116
- });
117
-
118
- it("returns signature_mismatch for wrong secret", () => {
119
- const now = 1710000000;
120
- vi.setSystemTime(now * 1000);
121
- const sig = signWebhookPayload(testPayload, testSecret, now);
122
-
123
- const result = verifyWebhookSignature(
124
- testPayload,
125
- sig,
126
- now,
127
- "wrong_secret",
128
- );
129
- expect(result).toEqual({ valid: false, reason: "signature_mismatch" });
130
- });
131
-
132
- it("returns timestamp_expired for old timestamp (>5 min)", () => {
133
- const now = 1710000600; // 10 min later
134
- vi.setSystemTime(now * 1000);
135
- const oldTimestamp = now - 301; // 5 min 1 sec ago
136
- const sig = signWebhookPayload(testPayload, testSecret, oldTimestamp);
137
-
138
- const result = verifyWebhookSignature(
139
- testPayload,
140
- sig,
141
- oldTimestamp,
142
- testSecret,
143
- );
144
- expect(result).toEqual({ valid: false, reason: "timestamp_expired" });
145
- });
146
-
147
- it("accepts timestamp within tolerance", () => {
148
- const now = 1710000000;
149
- vi.setSystemTime(now * 1000);
150
- const recentTimestamp = now - 299; // 4 min 59 sec ago
151
- const sig = signWebhookPayload(testPayload, testSecret, recentTimestamp);
152
-
153
- const result = verifyWebhookSignature(
154
- testPayload,
155
- sig,
156
- recentTimestamp,
157
- testSecret,
158
- );
159
- expect(result).toEqual({ valid: true });
160
- });
161
-
162
- it("supports custom tolerance", () => {
163
- const now = 1710000000;
164
- vi.setSystemTime(now * 1000);
165
- const oldTimestamp = now - 61;
166
- const sig = signWebhookPayload(testPayload, testSecret, oldTimestamp);
167
-
168
- const result = verifyWebhookSignature(
169
- testPayload,
170
- sig,
171
- oldTimestamp,
172
- testSecret,
173
- { toleranceSeconds: 60 },
174
- );
175
- expect(result).toEqual({ valid: false, reason: "timestamp_expired" });
176
- });
177
-
178
- it("rejects future timestamps beyond tolerance", () => {
179
- const now = 1710000000;
180
- vi.setSystemTime(now * 1000);
181
- const futureTimestamp = now + 400;
182
- const sig = signWebhookPayload(testPayload, testSecret, futureTimestamp);
183
-
184
- const result = verifyWebhookSignature(
185
- testPayload,
186
- sig,
187
- futureTimestamp,
188
- testSecret,
189
- );
190
- expect(result).toEqual({ valid: false, reason: "timestamp_expired" });
191
- });
192
- });
193
-
194
- // ---------------------------------------------------------------------------
195
- // createWebhookEnvelope
196
- // ---------------------------------------------------------------------------
197
-
198
- describe("createWebhookEnvelope", () => {
199
- it("creates a valid envelope with ISO 8601 timestamp", () => {
200
- const envelope = createWebhookEnvelope("BOOKING_CREATED", samplePayload);
201
-
202
- expect(envelope.triggerEvent).toBe("BOOKING_CREATED");
203
- expect(envelope.payload).toBe(samplePayload);
204
- expect(new Date(envelope.createdAt).toISOString()).toBe(
205
- envelope.createdAt,
206
- );
207
- });
208
- });
209
-
210
- // ---------------------------------------------------------------------------
211
- // resolvePayloadTemplate
212
- // ---------------------------------------------------------------------------
213
-
214
- describe("resolvePayloadTemplate", () => {
215
- const envelope = createWebhookEnvelope("BOOKING_CREATED", samplePayload);
216
-
217
- it("resolves all standard template variables", () => {
218
- const template = '{"event":"{{triggerEvent}}","booking":"{{bookingId}}","type":"{{eventType}}","start":"{{startTime}}","end":"{{endTime}}","status":"{{status}}","org":"{{organizerName}}","email":"{{organizerEmail}}"}';
219
- const result = resolvePayloadTemplate(template, envelope);
220
- const parsed = JSON.parse(result);
221
-
222
- expect(parsed.event).toBe("BOOKING_CREATED");
223
- expect(parsed.booking).toBe("bk-1");
224
- expect(parsed.type).toBe("consultation");
225
- expect(parsed.start).toBe("2026-03-15T14:00:00.000Z");
226
- expect(parsed.status).toBe("confirmed");
227
- expect(parsed.org).toBe("Dr. Smith");
228
- expect(parsed.email).toBe("smith@example.com");
229
- });
230
-
231
- it("handles template with no variables", () => {
232
- const result = resolvePayloadTemplate('{"static":"value"}', envelope);
233
- expect(result).toBe('{"static":"value"}');
234
- });
235
- });
236
-
237
- // ---------------------------------------------------------------------------
238
- // matchWebhookSubscriptions
239
- // ---------------------------------------------------------------------------
240
-
241
- describe("matchWebhookSubscriptions", () => {
242
- const subs: WebhookSubscription[] = [
243
- makeSub({ id: "wh-1", triggers: ["BOOKING_CREATED", "BOOKING_CANCELLED"] }),
244
- makeSub({ id: "wh-2", triggers: ["BOOKING_CREATED"], isActive: false }),
245
- makeSub({
246
- id: "wh-3",
247
- triggers: ["BOOKING_CREATED"],
248
- eventTypeId: "evt-1",
249
- }),
250
- makeSub({
251
- id: "wh-4",
252
- triggers: ["BOOKING_CREATED"],
253
- teamId: "team-1",
254
- }),
255
- ];
256
-
257
- it("matches active webhooks with matching trigger", () => {
258
- const matched = matchWebhookSubscriptions(subs, "BOOKING_CREATED");
259
- expect(matched.map((s) => s.id)).toContain("wh-1");
260
- });
261
-
262
- it("excludes inactive webhooks", () => {
263
- const matched = matchWebhookSubscriptions(subs, "BOOKING_CREATED");
264
- expect(matched.find((s) => s.id === "wh-2")).toBeUndefined();
265
- });
266
-
267
- it("matches scoped webhooks when scope matches", () => {
268
- const matched = matchWebhookSubscriptions(subs, "BOOKING_CREATED", {
269
- eventTypeId: "evt-1",
270
- });
271
- expect(matched.find((s) => s.id === "wh-3")).toBeDefined();
272
- });
273
-
274
- it("excludes scoped webhooks when scope doesn't match", () => {
275
- const matched = matchWebhookSubscriptions(subs, "BOOKING_CREATED", {
276
- eventTypeId: "evt-99",
277
- });
278
- expect(matched.find((s) => s.id === "wh-3")).toBeUndefined();
279
- });
280
-
281
- it("matches team-scoped webhooks", () => {
282
- const matched = matchWebhookSubscriptions(subs, "BOOKING_CREATED", {
283
- teamId: "team-1",
284
- });
285
- expect(matched.find((s) => s.id === "wh-4")).toBeDefined();
286
- });
287
-
288
- it("returns empty for unsubscribed trigger", () => {
289
- const matched = matchWebhookSubscriptions(subs, "OOO_CREATED");
290
- expect(matched).toHaveLength(0);
291
- });
292
- });
293
-
294
- // ---------------------------------------------------------------------------
295
- // getRetryDelay
296
- // ---------------------------------------------------------------------------
297
-
298
- describe("getRetryDelay", () => {
299
- it("returns correct delays for default config", () => {
300
- expect(getRetryDelay(0)).toBe(10);
301
- expect(getRetryDelay(1)).toBe(60);
302
- expect(getRetryDelay(2)).toBe(300);
303
- });
304
-
305
- it("returns null when max retries exceeded", () => {
306
- expect(getRetryDelay(3)).toBeNull();
307
- expect(getRetryDelay(10)).toBeNull();
308
- });
309
-
310
- it("uses custom config", () => {
311
- const config = { maxRetries: 2, backoffSeconds: [5, 30] };
312
- expect(getRetryDelay(0, config)).toBe(5);
313
- expect(getRetryDelay(1, config)).toBe(30);
314
- expect(getRetryDelay(2, config)).toBeNull();
315
- });
316
- });
317
-
318
- // ---------------------------------------------------------------------------
319
- // isSuccessResponse
320
- // ---------------------------------------------------------------------------
321
-
322
- describe("isSuccessResponse", () => {
323
- it("returns true for 200", () => {
324
- expect(isSuccessResponse(200)).toBe(true);
325
- });
326
-
327
- it("returns true for 201", () => {
328
- expect(isSuccessResponse(201)).toBe(true);
329
- });
330
-
331
- it("returns true for 204", () => {
332
- expect(isSuccessResponse(204)).toBe(true);
333
- });
334
-
335
- it("returns false for 400", () => {
336
- expect(isSuccessResponse(400)).toBe(false);
337
- });
338
-
339
- it("returns false for 500", () => {
340
- expect(isSuccessResponse(500)).toBe(false);
341
- });
342
-
343
- it("returns false for 301 redirect", () => {
344
- expect(isSuccessResponse(301)).toBe(false);
345
- });
346
- });
347
-
348
- // ---------------------------------------------------------------------------
349
- // validateWebhookSubscription
350
- // ---------------------------------------------------------------------------
351
-
352
- describe("validateWebhookSubscription", () => {
353
- it("accepts valid subscription", () => {
354
- expect(() =>
355
- validateWebhookSubscription({
356
- subscriberUrl: "https://example.com/webhook",
357
- triggers: ["BOOKING_CREATED"],
358
- isActive: true,
359
- }),
360
- ).not.toThrow();
361
- });
362
-
363
- it("rejects missing URL", () => {
364
- expect(() =>
365
- validateWebhookSubscription({
366
- subscriberUrl: "",
367
- triggers: ["BOOKING_CREATED"],
368
- isActive: true,
369
- }),
370
- ).toThrow("Subscriber URL is required");
371
- });
372
-
373
- it("rejects invalid URL", () => {
374
- expect(() =>
375
- validateWebhookSubscription({
376
- subscriberUrl: "not-a-url",
377
- triggers: ["BOOKING_CREATED"],
378
- isActive: true,
379
- }),
380
- ).toThrow("Invalid subscriber URL");
381
- });
382
-
383
- it("rejects empty triggers array", () => {
384
- expect(() =>
385
- validateWebhookSubscription({
386
- subscriberUrl: "https://example.com/webhook",
387
- triggers: [],
388
- isActive: true,
389
- }),
390
- ).toThrow("At least one trigger");
391
- });
392
-
393
- it("rejects invalid trigger", () => {
394
- expect(() =>
395
- validateWebhookSubscription({
396
- subscriberUrl: "https://example.com/webhook",
397
- triggers: ["INVALID_TRIGGER" as WebhookTrigger],
398
- isActive: true,
399
- }),
400
- ).toThrow('Invalid trigger: "INVALID_TRIGGER"');
401
- });
402
-
403
- it("accepts all valid triggers", () => {
404
- expect(() =>
405
- validateWebhookSubscription({
406
- subscriberUrl: "https://example.com/webhook",
407
- triggers: [...WEBHOOK_TRIGGERS],
408
- isActive: true,
409
- }),
410
- ).not.toThrow();
411
- });
412
- });