@thebookingkit/server 0.1.1
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/.turbo/turbo-build.log +6 -0
- package/.turbo/turbo-test.log +20 -0
- package/CHANGELOG.md +9 -0
- package/dist/__tests__/api.test.d.ts +2 -0
- package/dist/__tests__/api.test.d.ts.map +1 -0
- package/dist/__tests__/api.test.js +280 -0
- package/dist/__tests__/api.test.js.map +1 -0
- package/dist/__tests__/auth.test.d.ts +2 -0
- package/dist/__tests__/auth.test.d.ts.map +1 -0
- package/dist/__tests__/auth.test.js +78 -0
- package/dist/__tests__/auth.test.js.map +1 -0
- package/dist/__tests__/concurrent-booking.test.d.ts +2 -0
- package/dist/__tests__/concurrent-booking.test.d.ts.map +1 -0
- package/dist/__tests__/concurrent-booking.test.js +111 -0
- package/dist/__tests__/concurrent-booking.test.js.map +1 -0
- package/dist/__tests__/multi-tenancy.test.d.ts +2 -0
- package/dist/__tests__/multi-tenancy.test.d.ts.map +1 -0
- package/dist/__tests__/multi-tenancy.test.js +196 -0
- package/dist/__tests__/multi-tenancy.test.js.map +1 -0
- package/dist/__tests__/serialization-retry.test.d.ts +2 -0
- package/dist/__tests__/serialization-retry.test.d.ts.map +1 -0
- package/dist/__tests__/serialization-retry.test.js +53 -0
- package/dist/__tests__/serialization-retry.test.js.map +1 -0
- package/dist/__tests__/webhooks.test.d.ts +2 -0
- package/dist/__tests__/webhooks.test.d.ts.map +1 -0
- package/dist/__tests__/webhooks.test.js +286 -0
- package/dist/__tests__/webhooks.test.js.map +1 -0
- package/dist/__tests__/workflows.test.d.ts +2 -0
- package/dist/__tests__/workflows.test.d.ts.map +1 -0
- package/dist/__tests__/workflows.test.js +299 -0
- package/dist/__tests__/workflows.test.js.map +1 -0
- 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 +40 -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/dist/adapters/storage-adapter.d.ts +12 -0
- 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 +271 -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 +81 -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 +36 -0
- package/dist/email-templates.d.ts.map +1 -0
- package/dist/email-templates.js +112 -0
- 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 +132 -0
- package/dist/multi-tenancy.d.ts.map +1 -0
- package/dist/multi-tenancy.js +188 -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 +228 -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 +251 -0
- package/dist/workflows.js.map +1 -0
- package/package.json +32 -0
- package/src/__tests__/api.test.ts +354 -0
- package/src/__tests__/auth.test.ts +111 -0
- package/src/__tests__/concurrent-booking.test.ts +170 -0
- package/src/__tests__/multi-tenancy.test.ts +267 -0
- package/src/__tests__/serialization-retry.test.ts +76 -0
- package/src/__tests__/webhooks.test.ts +412 -0
- package/src/__tests__/workflows.test.ts +422 -0
- package/src/adapters/calendar-adapter.ts +49 -0
- package/src/adapters/email-adapter.ts +108 -0
- package/src/adapters/index.ts +36 -0
- package/src/adapters/job-adapter.ts +26 -0
- package/src/adapters/payment-adapter.ts +118 -0
- package/src/adapters/sms-adapter.ts +35 -0
- package/src/adapters/storage-adapter.ts +11 -0
- package/src/api.ts +446 -0
- package/src/auth.ts +146 -0
- package/src/booking-tokens.ts +61 -0
- package/src/email-templates.ts +140 -0
- package/src/index.ts +192 -0
- package/src/multi-tenancy.ts +301 -0
- package/src/notification-jobs.ts +428 -0
- package/src/serialization-retry.ts +94 -0
- package/src/webhooks.ts +378 -0
- package/src/workflows.ts +441 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-tenancy utilities for organization-scoped deployments.
|
|
3
|
+
*
|
|
4
|
+
* Provides organization settings resolution, cascading defaults,
|
|
5
|
+
* role-based access control, and tenant authorization helpers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** Organization member role */
|
|
13
|
+
export type OrgRole = "owner" | "admin" | "member";
|
|
14
|
+
|
|
15
|
+
/** Organization member */
|
|
16
|
+
export interface OrgMember {
|
|
17
|
+
userId: string;
|
|
18
|
+
organizationId: string;
|
|
19
|
+
role: OrgRole;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Organization branding */
|
|
23
|
+
export interface OrgBranding {
|
|
24
|
+
logoUrl?: string;
|
|
25
|
+
primaryColor?: string;
|
|
26
|
+
accentColor?: string;
|
|
27
|
+
fontFamily?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Organization-level settings */
|
|
31
|
+
export interface OrgSettings {
|
|
32
|
+
defaultTimezone?: string;
|
|
33
|
+
defaultCurrency?: string;
|
|
34
|
+
branding?: OrgBranding;
|
|
35
|
+
defaultBufferMinutes?: number;
|
|
36
|
+
defaultBookingLimits?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Provider-level settings that can override org defaults */
|
|
40
|
+
export interface ProviderSettings {
|
|
41
|
+
timezone?: string;
|
|
42
|
+
currency?: string;
|
|
43
|
+
branding?: Partial<OrgBranding>;
|
|
44
|
+
bufferMinutes?: number;
|
|
45
|
+
bookingLimits?: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Event type settings that can override provider defaults */
|
|
49
|
+
export interface EventTypeSettings {
|
|
50
|
+
timezone?: string;
|
|
51
|
+
currency?: string;
|
|
52
|
+
bufferBefore?: number;
|
|
53
|
+
bufferAfter?: number;
|
|
54
|
+
bookingLimits?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Global SlotKit defaults */
|
|
58
|
+
export interface GlobalDefaults {
|
|
59
|
+
timezone: string;
|
|
60
|
+
currency: string;
|
|
61
|
+
bufferMinutes: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Resolved effective settings after cascading resolution */
|
|
65
|
+
export interface ResolvedSettings {
|
|
66
|
+
timezone: string;
|
|
67
|
+
currency: string;
|
|
68
|
+
bufferMinutes: number;
|
|
69
|
+
branding: OrgBranding;
|
|
70
|
+
bookingLimits: Record<string, unknown>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Permissions available in the system */
|
|
74
|
+
export type OrgPermission =
|
|
75
|
+
| "manage:members"
|
|
76
|
+
| "manage:teams"
|
|
77
|
+
| "manage:event-types"
|
|
78
|
+
| "view:all-bookings"
|
|
79
|
+
| "view:own-bookings"
|
|
80
|
+
| "manage:own-availability"
|
|
81
|
+
| "view:analytics"
|
|
82
|
+
| "manage:organization";
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Errors
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
/** Error thrown for multi-tenancy authorization violations */
|
|
89
|
+
export class TenantAuthorizationError extends Error {
|
|
90
|
+
constructor(message: string) {
|
|
91
|
+
super(message);
|
|
92
|
+
this.name = "TenantAuthorizationError";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Global Defaults
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/** System-wide defaults used as the base for cascading resolution */
|
|
101
|
+
export const GLOBAL_DEFAULTS: GlobalDefaults = {
|
|
102
|
+
timezone: "UTC",
|
|
103
|
+
currency: "USD",
|
|
104
|
+
bufferMinutes: 0,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Settings Resolution
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolve effective settings via the cascade:
|
|
113
|
+
* `event_type > provider > organization > global defaults`
|
|
114
|
+
*
|
|
115
|
+
* @param orgSettings - Organization-level settings
|
|
116
|
+
* @param providerSettings - Provider-level settings (overrides org)
|
|
117
|
+
* @param eventTypeSettings - Event type settings (overrides provider)
|
|
118
|
+
* @returns Fully resolved effective settings
|
|
119
|
+
*/
|
|
120
|
+
export function resolveEffectiveSettings(
|
|
121
|
+
orgSettings?: OrgSettings | null,
|
|
122
|
+
providerSettings?: ProviderSettings | null,
|
|
123
|
+
eventTypeSettings?: EventTypeSettings | null,
|
|
124
|
+
): ResolvedSettings {
|
|
125
|
+
// Start with global defaults
|
|
126
|
+
let timezone = GLOBAL_DEFAULTS.timezone;
|
|
127
|
+
let currency = GLOBAL_DEFAULTS.currency;
|
|
128
|
+
let bufferMinutes = GLOBAL_DEFAULTS.bufferMinutes;
|
|
129
|
+
let branding: OrgBranding = {};
|
|
130
|
+
let bookingLimits: Record<string, unknown> = {};
|
|
131
|
+
|
|
132
|
+
// Apply org settings
|
|
133
|
+
if (orgSettings) {
|
|
134
|
+
if (orgSettings.defaultTimezone) timezone = orgSettings.defaultTimezone;
|
|
135
|
+
if (orgSettings.defaultCurrency) currency = orgSettings.defaultCurrency;
|
|
136
|
+
if (orgSettings.defaultBufferMinutes !== undefined)
|
|
137
|
+
bufferMinutes = orgSettings.defaultBufferMinutes;
|
|
138
|
+
if (orgSettings.branding) branding = { ...branding, ...orgSettings.branding };
|
|
139
|
+
if (orgSettings.defaultBookingLimits)
|
|
140
|
+
bookingLimits = { ...orgSettings.defaultBookingLimits };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Apply provider settings (override org)
|
|
144
|
+
if (providerSettings) {
|
|
145
|
+
if (providerSettings.timezone) timezone = providerSettings.timezone;
|
|
146
|
+
if (providerSettings.currency) currency = providerSettings.currency;
|
|
147
|
+
if (providerSettings.bufferMinutes !== undefined)
|
|
148
|
+
bufferMinutes = providerSettings.bufferMinutes;
|
|
149
|
+
if (providerSettings.branding)
|
|
150
|
+
branding = { ...branding, ...providerSettings.branding };
|
|
151
|
+
if (providerSettings.bookingLimits)
|
|
152
|
+
bookingLimits = { ...bookingLimits, ...providerSettings.bookingLimits };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Apply event type settings (override provider)
|
|
156
|
+
if (eventTypeSettings) {
|
|
157
|
+
if (eventTypeSettings.timezone) timezone = eventTypeSettings.timezone;
|
|
158
|
+
if (eventTypeSettings.currency) currency = eventTypeSettings.currency;
|
|
159
|
+
if (eventTypeSettings.bufferBefore !== undefined)
|
|
160
|
+
bufferMinutes = eventTypeSettings.bufferBefore;
|
|
161
|
+
if (eventTypeSettings.bookingLimits)
|
|
162
|
+
bookingLimits = { ...bookingLimits, ...eventTypeSettings.bookingLimits };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { timezone, currency, bufferMinutes, branding, bookingLimits };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Role-Based Access Control
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
/** Permissions granted to each role */
|
|
173
|
+
const ROLE_PERMISSIONS: Record<OrgRole, OrgPermission[]> = {
|
|
174
|
+
owner: [
|
|
175
|
+
"manage:members",
|
|
176
|
+
"manage:teams",
|
|
177
|
+
"manage:event-types",
|
|
178
|
+
"view:all-bookings",
|
|
179
|
+
"view:own-bookings",
|
|
180
|
+
"manage:own-availability",
|
|
181
|
+
"view:analytics",
|
|
182
|
+
"manage:organization",
|
|
183
|
+
],
|
|
184
|
+
admin: [
|
|
185
|
+
"manage:teams",
|
|
186
|
+
"manage:event-types",
|
|
187
|
+
"view:all-bookings",
|
|
188
|
+
"view:own-bookings",
|
|
189
|
+
"manage:own-availability",
|
|
190
|
+
"view:analytics",
|
|
191
|
+
],
|
|
192
|
+
member: [
|
|
193
|
+
"view:own-bookings",
|
|
194
|
+
"manage:own-availability",
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get all permissions granted to a role.
|
|
200
|
+
*
|
|
201
|
+
* @param role - The organization role
|
|
202
|
+
* @returns Array of permissions
|
|
203
|
+
*/
|
|
204
|
+
export function getRolePermissions(role: OrgRole): OrgPermission[] {
|
|
205
|
+
return ROLE_PERMISSIONS[role] ?? [];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check if a role has a specific permission.
|
|
210
|
+
*
|
|
211
|
+
* @param role - The organization role
|
|
212
|
+
* @param permission - The permission to check
|
|
213
|
+
* @returns Whether the role has the permission
|
|
214
|
+
*/
|
|
215
|
+
export function roleHasPermission(
|
|
216
|
+
role: OrgRole,
|
|
217
|
+
permission: OrgPermission,
|
|
218
|
+
): boolean {
|
|
219
|
+
return getRolePermissions(role).includes(permission);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Assert that an org member has a required permission.
|
|
224
|
+
*
|
|
225
|
+
* @param member - The org member
|
|
226
|
+
* @param permission - The required permission
|
|
227
|
+
* @throws {TenantAuthorizationError} If the member lacks the permission
|
|
228
|
+
*/
|
|
229
|
+
export function assertOrgPermission(
|
|
230
|
+
member: OrgMember,
|
|
231
|
+
permission: OrgPermission,
|
|
232
|
+
): void {
|
|
233
|
+
if (!roleHasPermission(member.role, permission)) {
|
|
234
|
+
throw new TenantAuthorizationError(
|
|
235
|
+
`Role "${member.role}" does not have permission: "${permission}"`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Tenant Scoping
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Validate that a resource belongs to the expected organization.
|
|
246
|
+
*
|
|
247
|
+
* @param resourceOrgId - Organization ID on the resource
|
|
248
|
+
* @param expectedOrgId - The org ID from the authenticated context
|
|
249
|
+
* @throws {TenantAuthorizationError} If there is a tenant mismatch
|
|
250
|
+
*/
|
|
251
|
+
export function assertTenantScope(
|
|
252
|
+
resourceOrgId: string | null | undefined,
|
|
253
|
+
expectedOrgId: string,
|
|
254
|
+
): void {
|
|
255
|
+
if (resourceOrgId && resourceOrgId !== expectedOrgId) {
|
|
256
|
+
throw new TenantAuthorizationError(
|
|
257
|
+
"Resource does not belong to the current organization",
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Generate the public booking URL for an organization's provider/event type.
|
|
264
|
+
*
|
|
265
|
+
* @param orgSlug - Organization slug
|
|
266
|
+
* @param providerSlug - Provider slug or ID
|
|
267
|
+
* @param eventTypeSlug - Event type slug
|
|
268
|
+
* @param baseUrl - Base URL of the application
|
|
269
|
+
* @returns Full booking URL
|
|
270
|
+
*/
|
|
271
|
+
export function buildOrgBookingUrl(
|
|
272
|
+
orgSlug: string,
|
|
273
|
+
providerSlug: string,
|
|
274
|
+
eventTypeSlug: string,
|
|
275
|
+
baseUrl: string,
|
|
276
|
+
): string {
|
|
277
|
+
return `${baseUrl}/${orgSlug}/${providerSlug}/${eventTypeSlug}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Parse an organization slug from a booking URL path.
|
|
282
|
+
*
|
|
283
|
+
* Expected format: `/{orgSlug}/{providerSlug}/{eventTypeSlug}`
|
|
284
|
+
*
|
|
285
|
+
* @param pathname - URL pathname
|
|
286
|
+
* @returns Parsed segments, or null if format is invalid
|
|
287
|
+
*/
|
|
288
|
+
export function parseOrgBookingPath(pathname: string): {
|
|
289
|
+
orgSlug: string;
|
|
290
|
+
providerSlug: string;
|
|
291
|
+
eventTypeSlug: string;
|
|
292
|
+
} | null {
|
|
293
|
+
const match = pathname.match(/^\/([^/]+)\/([^/]+)\/([^/]+)$/);
|
|
294
|
+
if (!match) return null;
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
orgSlug: match[1],
|
|
298
|
+
providerSlug: match[2],
|
|
299
|
+
eventTypeSlug: match[3],
|
|
300
|
+
};
|
|
301
|
+
}
|