@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.
Files changed (95) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/calendar-adapter.d.ts +47 -0
  3. package/dist/adapters/calendar-adapter.d.ts.map +1 -0
  4. package/dist/adapters/calendar-adapter.js +2 -0
  5. package/dist/adapters/calendar-adapter.js.map +1 -0
  6. package/dist/adapters/email-adapter.d.ts +65 -0
  7. package/dist/adapters/email-adapter.d.ts.map +1 -0
  8. package/dist/adapters/email-adapter.js +41 -0
  9. package/dist/adapters/email-adapter.js.map +1 -0
  10. package/dist/adapters/index.d.ts +9 -0
  11. package/dist/adapters/index.d.ts.map +1 -0
  12. package/dist/adapters/index.js +3 -0
  13. package/dist/adapters/index.js.map +1 -0
  14. package/dist/adapters/job-adapter.d.ts +26 -0
  15. package/dist/adapters/job-adapter.d.ts.map +1 -0
  16. package/dist/adapters/job-adapter.js +13 -0
  17. package/dist/adapters/job-adapter.js.map +1 -0
  18. package/dist/adapters/payment-adapter.d.ts +106 -0
  19. package/dist/adapters/payment-adapter.d.ts.map +1 -0
  20. package/dist/adapters/payment-adapter.js +8 -0
  21. package/dist/adapters/payment-adapter.js.map +1 -0
  22. package/dist/adapters/sms-adapter.d.ts +33 -0
  23. package/dist/adapters/sms-adapter.d.ts.map +1 -0
  24. package/dist/adapters/sms-adapter.js +8 -0
  25. package/dist/adapters/sms-adapter.js.map +1 -0
  26. package/{src/adapters/storage-adapter.ts → dist/adapters/storage-adapter.d.ts} +5 -4
  27. package/dist/adapters/storage-adapter.d.ts.map +1 -0
  28. package/dist/adapters/storage-adapter.js +2 -0
  29. package/dist/adapters/storage-adapter.js.map +1 -0
  30. package/dist/api.d.ts +223 -0
  31. package/dist/api.d.ts.map +1 -0
  32. package/dist/api.js +286 -0
  33. package/dist/api.js.map +1 -0
  34. package/dist/auth.d.ts +71 -0
  35. package/dist/auth.d.ts.map +1 -0
  36. package/dist/auth.js +90 -0
  37. package/dist/auth.js.map +1 -0
  38. package/dist/booking-tokens.d.ts +23 -0
  39. package/dist/booking-tokens.d.ts.map +1 -0
  40. package/dist/booking-tokens.js +52 -0
  41. package/dist/booking-tokens.js.map +1 -0
  42. package/dist/email-templates.d.ts +32 -0
  43. package/dist/email-templates.d.ts.map +1 -0
  44. package/{src/email-templates.ts → dist/email-templates.js} +14 -34
  45. package/dist/email-templates.js.map +1 -0
  46. package/dist/index.d.ts +13 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +22 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/multi-tenancy.d.ts +123 -0
  51. package/dist/multi-tenancy.d.ts.map +1 -0
  52. package/dist/multi-tenancy.js +197 -0
  53. package/dist/multi-tenancy.js.map +1 -0
  54. package/dist/notification-jobs.d.ts +143 -0
  55. package/dist/notification-jobs.d.ts.map +1 -0
  56. package/dist/notification-jobs.js +278 -0
  57. package/dist/notification-jobs.js.map +1 -0
  58. package/dist/serialization-retry.d.ts +28 -0
  59. package/dist/serialization-retry.d.ts.map +1 -0
  60. package/dist/serialization-retry.js +71 -0
  61. package/dist/serialization-retry.js.map +1 -0
  62. package/dist/webhooks.d.ts +164 -0
  63. package/dist/webhooks.d.ts.map +1 -0
  64. package/dist/webhooks.js +239 -0
  65. package/dist/webhooks.js.map +1 -0
  66. package/dist/workflows.d.ts +169 -0
  67. package/dist/workflows.d.ts.map +1 -0
  68. package/dist/workflows.js +271 -0
  69. package/dist/workflows.js.map +1 -0
  70. package/package.json +21 -2
  71. package/CHANGELOG.md +0 -9
  72. package/src/__tests__/api.test.ts +0 -354
  73. package/src/__tests__/auth.test.ts +0 -111
  74. package/src/__tests__/concurrent-booking.test.ts +0 -170
  75. package/src/__tests__/multi-tenancy.test.ts +0 -267
  76. package/src/__tests__/serialization-retry.test.ts +0 -76
  77. package/src/__tests__/webhooks.test.ts +0 -412
  78. package/src/__tests__/workflows.test.ts +0 -422
  79. package/src/adapters/calendar-adapter.ts +0 -49
  80. package/src/adapters/email-adapter.ts +0 -108
  81. package/src/adapters/index.ts +0 -36
  82. package/src/adapters/job-adapter.ts +0 -26
  83. package/src/adapters/payment-adapter.ts +0 -118
  84. package/src/adapters/sms-adapter.ts +0 -35
  85. package/src/api.ts +0 -446
  86. package/src/auth.ts +0 -146
  87. package/src/booking-tokens.ts +0 -61
  88. package/src/index.ts +0 -192
  89. package/src/multi-tenancy.ts +0 -301
  90. package/src/notification-jobs.ts +0 -428
  91. package/src/serialization-retry.ts +0 -94
  92. package/src/webhooks.ts +0 -378
  93. package/src/workflows.ts +0 -441
  94. package/tsconfig.json +0 -9
  95. package/vitest.config.ts +0 -7
package/src/auth.ts DELETED
@@ -1,146 +0,0 @@
1
- import { UnauthorizedError, ForbiddenError } from "@thebookingkit/core";
2
-
3
- /** Represents an authenticated user in the system */
4
- export interface AuthUser {
5
- id: string;
6
- email: string;
7
- name?: string;
8
- role?: "admin" | "provider" | "customer";
9
- }
10
-
11
- /** Session returned by the auth adapter */
12
- export interface AuthSession {
13
- user: AuthUser;
14
- expires: Date;
15
- }
16
-
17
- /**
18
- * Pluggable authentication adapter interface.
19
- * Default implementation uses NextAuth.js.
20
- * Swap to Supabase Auth, Clerk, or Lucia by implementing this interface.
21
- */
22
- export interface AuthAdapter {
23
- /** Get the currently authenticated user from the request */
24
- getCurrentUser(request: Request): Promise<AuthUser | null>;
25
- /** Get the full session */
26
- getSession(request: Request): Promise<AuthSession | null>;
27
- /** Verify an API token or signed booking token */
28
- verifyToken(token: string): Promise<AuthUser | null>;
29
- }
30
-
31
- /** Request with injected auth context */
32
- export interface AuthenticatedRequest extends Request {
33
- user: AuthUser;
34
- }
35
-
36
- /** Options for the withAuth middleware */
37
- export interface WithAuthOptions {
38
- /** Require a specific role */
39
- requiredRole?: "admin" | "provider" | "customer";
40
- }
41
-
42
- /**
43
- * Middleware wrapper that injects the authenticated user into every request.
44
- *
45
- * - Rejects unauthenticated requests with 401.
46
- * - Optionally checks user role.
47
- * - Passes the authenticated user to the handler.
48
- *
49
- * @example
50
- * ```ts
51
- * // In a Next.js API route
52
- * export const GET = withAuth(authAdapter, async (req) => {
53
- * const userId = req.user.id;
54
- * // Provider can only access their own data
55
- * const bookings = await db.query.bookings.findMany({
56
- * where: eq(bookings.providerId, userId)
57
- * });
58
- * return Response.json(bookings);
59
- * });
60
- * ```
61
- */
62
- export function withAuth(
63
- adapter: AuthAdapter,
64
- handler: (req: AuthenticatedRequest) => Promise<Response>,
65
- options?: WithAuthOptions,
66
- ): (req: Request) => Promise<Response> {
67
- return async (req: Request): Promise<Response> => {
68
- try {
69
- // Try to get user from session first
70
- let user = await adapter.getCurrentUser(req);
71
-
72
- // If no session, try Bearer token
73
- if (!user) {
74
- const authHeader = req.headers.get("authorization");
75
- if (authHeader?.startsWith("Bearer ")) {
76
- const token = authHeader.slice(7);
77
- user = await adapter.verifyToken(token);
78
- }
79
- }
80
-
81
- if (!user) {
82
- throw new UnauthorizedError();
83
- }
84
-
85
- // Check required role if specified
86
- if (options?.requiredRole && user.role !== options.requiredRole) {
87
- throw new ForbiddenError();
88
- }
89
-
90
- // Inject user into request
91
- const authReq = req as AuthenticatedRequest;
92
- authReq.user = user;
93
-
94
- return await handler(authReq);
95
- } catch (error) {
96
- if (error instanceof UnauthorizedError) {
97
- return Response.json(
98
- { error: error.message, code: error.code },
99
- { status: 401 },
100
- );
101
- }
102
- if (error instanceof ForbiddenError) {
103
- return Response.json(
104
- { error: error.message, code: error.code },
105
- { status: 403 },
106
- );
107
- }
108
- throw error;
109
- }
110
- };
111
- }
112
-
113
- /**
114
- * Helper to scope database queries to the authenticated user.
115
- * Providers can only access their own rows (user_id matches).
116
- *
117
- * @example
118
- * ```ts
119
- * const provider = await assertOwnership(db, providers, req.user.id, providerId);
120
- * ```
121
- */
122
- export function assertProviderOwnership(
123
- userId: string,
124
- resourceUserId: string,
125
- ): void {
126
- if (userId !== resourceUserId) {
127
- throw new ForbiddenError(
128
- "You do not have permission to access this provider's data.",
129
- );
130
- }
131
- }
132
-
133
- /**
134
- * Helper to verify customer access to their own bookings.
135
- * Customers can only access bookings where customer_email matches.
136
- */
137
- export function assertCustomerAccess(
138
- userEmail: string,
139
- bookingCustomerEmail: string,
140
- ): void {
141
- if (userEmail !== bookingCustomerEmail) {
142
- throw new ForbiddenError(
143
- "You do not have permission to access this booking.",
144
- );
145
- }
146
- }
@@ -1,61 +0,0 @@
1
- import { createHmac } from "crypto";
2
-
3
- /**
4
- * Generate a signed booking management token.
5
- *
6
- * The token is a signed payload containing the booking ID and expiry time.
7
- * It allows customers to view/manage their booking without authentication.
8
- *
9
- * @param bookingId - The booking UUID
10
- * @param expiresAt - When the token expires
11
- * @param secret - HMAC signing secret
12
- */
13
- export function generateBookingToken(
14
- bookingId: string,
15
- expiresAt: Date,
16
- secret: string,
17
- ): string {
18
- const payload = `${bookingId}:${expiresAt.getTime()}`;
19
- const signature = createHmac("sha256", secret)
20
- .update(payload)
21
- .digest("hex")
22
- .slice(0, 16);
23
- return Buffer.from(`${payload}:${signature}`).toString("base64url");
24
- }
25
-
26
- /**
27
- * Verify and decode a booking management token.
28
- *
29
- * @param token - The base64url-encoded token
30
- * @param secret - HMAC signing secret (must match generation)
31
- * @returns The booking ID if valid, null if invalid or expired
32
- */
33
- export function verifyBookingToken(
34
- token: string,
35
- secret: string,
36
- ): { bookingId: string; expiresAt: Date } | null {
37
- try {
38
- const decoded = Buffer.from(token, "base64url").toString("utf-8");
39
- const parts = decoded.split(":");
40
- if (parts.length !== 3) return null;
41
-
42
- const [bookingId, expiresAtStr, signature] = parts;
43
- const expiresAt = new Date(Number(expiresAtStr));
44
-
45
- // Check expiry
46
- if (expiresAt < new Date()) return null;
47
-
48
- // Verify signature
49
- const payload = `${bookingId}:${expiresAtStr}`;
50
- const expectedSig = createHmac("sha256", secret)
51
- .update(payload)
52
- .digest("hex")
53
- .slice(0, 16);
54
-
55
- if (signature !== expectedSig) return null;
56
-
57
- return { bookingId, expiresAt };
58
- } catch {
59
- return null;
60
- }
61
- }
package/src/index.ts DELETED
@@ -1,192 +0,0 @@
1
- // Re-export core errors used by server modules
2
- export {
3
- BookingConflictError,
4
- SerializationRetryExhaustedError,
5
- UnauthorizedError,
6
- ForbiddenError,
7
- } from "@thebookingkit/core";
8
-
9
- // Serialization retry utility
10
- export {
11
- withSerializableRetry,
12
- type SerializableRetryOptions,
13
- } from "./serialization-retry.js";
14
-
15
- // Auth middleware & adapters
16
- export {
17
- withAuth,
18
- assertProviderOwnership,
19
- assertCustomerAccess,
20
- type AuthUser,
21
- type AuthSession,
22
- type AuthAdapter,
23
- type AuthenticatedRequest,
24
- type WithAuthOptions,
25
- } from "./auth.js";
26
-
27
- // Adapters
28
- export type {
29
- EmailAdapter,
30
- SendEmailOptions,
31
- EmailResult,
32
- EmailDeliveryStatus,
33
- EmailAttachment,
34
- CalendarAdapter,
35
- CalendarEventOptions,
36
- CalendarEventResult,
37
- CalendarConflict,
38
- JobAdapter,
39
- StorageAdapter,
40
- SmsAdapter,
41
- SendSmsOptions,
42
- SmsResult,
43
- PaymentAdapter,
44
- CreatePaymentIntentOptions,
45
- CreatePaymentIntentResult,
46
- CreateSetupIntentOptions,
47
- CreateSetupIntentResult,
48
- CaptureResult,
49
- RefundResult,
50
- } from "./adapters/index.js";
51
- export { generateICSAttachment, JOB_NAMES } from "./adapters/index.js";
52
-
53
- // Booking Tokens
54
- export {
55
- generateBookingToken,
56
- verifyBookingToken,
57
- } from "./booking-tokens.js";
58
-
59
- // Notification Jobs
60
- export {
61
- sendConfirmationEmail,
62
- sendReminderEmail,
63
- sendCancellationEmail,
64
- sendRescheduleEmail,
65
- scheduleAutoReject,
66
- syncBookingToCalendar,
67
- deleteBookingFromCalendar,
68
- formatDateTimeForEmail,
69
- formatDurationForEmail,
70
- type NotificationBookingData,
71
- type ConfirmationEmailPayload,
72
- type ReminderEmailPayload,
73
- type CancellationEmailPayload,
74
- type RescheduleEmailPayload,
75
- type CalendarSyncPayload,
76
- type CalendarDeletePayload,
77
- type AutoRejectPendingPayload,
78
- } from "./notification-jobs.js";
79
-
80
- // Email Templates
81
- export {
82
- interpolateTemplate,
83
- CONFIRMATION_EMAIL_HTML,
84
- CONFIRMATION_EMAIL_TEXT,
85
- REMINDER_EMAIL_HTML,
86
- CANCELLATION_EMAIL_HTML,
87
- RESCHEDULE_EMAIL_HTML,
88
- type EmailTemplateVars,
89
- } from "./email-templates.js";
90
-
91
- // Workflows
92
- export {
93
- resolveTemplateVariables,
94
- evaluateConditions,
95
- validateWorkflow,
96
- matchWorkflows,
97
- DEFAULT_TEMPLATES,
98
- TEMPLATE_VARIABLES,
99
- WorkflowValidationError,
100
- type WorkflowTrigger,
101
- type WorkflowActionType,
102
- type ConditionOperator,
103
- type WorkflowCondition,
104
- type EmailActionConfig,
105
- type SmsActionConfig,
106
- type WebhookActionConfig,
107
- type StatusUpdateActionConfig,
108
- type CalendarEventActionConfig,
109
- type WorkflowAction,
110
- type WorkflowDefinition,
111
- type WorkflowContext,
112
- type WorkflowLogEntry,
113
- } from "./workflows.js";
114
-
115
- // Webhooks
116
- export {
117
- signWebhookPayload,
118
- verifyWebhookSignature,
119
- createWebhookEnvelope,
120
- resolvePayloadTemplate,
121
- matchWebhookSubscriptions,
122
- getRetryDelay,
123
- isSuccessResponse,
124
- validateWebhookSubscription,
125
- WebhookValidationError,
126
- DEFAULT_RETRY_CONFIG,
127
- WEBHOOK_TRIGGERS,
128
- SIGNATURE_HEADER,
129
- TIMESTAMP_HEADER,
130
- DEFAULT_TOLERANCE_SECONDS,
131
- type WebhookTrigger,
132
- type WebhookAttendee,
133
- type WebhookPayload,
134
- type WebhookEnvelope,
135
- type WebhookSubscription,
136
- type WebhookDeliveryResult,
137
- type WebhookRetryConfig,
138
- type WebhookVerificationResult,
139
- } from "./webhooks.js";
140
-
141
- // REST API Utilities
142
- export {
143
- createErrorResponse,
144
- createSuccessResponse,
145
- createPaginatedResponse,
146
- generateApiKey,
147
- hashApiKey,
148
- verifyApiKey,
149
- hasScope,
150
- isKeyExpired,
151
- checkRateLimit,
152
- encodeCursor,
153
- decodeCursor,
154
- validateSlotQueryParams,
155
- parseSortParam,
156
- API_ERROR_CODES,
157
- type ApiError,
158
- type ApiErrorResponse,
159
- type ApiSuccessResponse,
160
- type ApiMeta,
161
- type PaginatedResponse,
162
- type ApiErrorCode,
163
- type ApiKeyRecord,
164
- type ApiKeyScope,
165
- type GeneratedApiKey,
166
- type RateLimitState,
167
- type RateLimitResult,
168
- type ValidationDetail,
169
- type ValidationResult,
170
- } from "./api.js";
171
-
172
- // Multi-Tenancy
173
- export {
174
- resolveEffectiveSettings,
175
- getRolePermissions,
176
- roleHasPermission,
177
- assertOrgPermission,
178
- assertTenantScope,
179
- buildOrgBookingUrl,
180
- parseOrgBookingPath,
181
- TenantAuthorizationError,
182
- GLOBAL_DEFAULTS,
183
- type OrgRole,
184
- type OrgMember,
185
- type OrgBranding,
186
- type OrgSettings,
187
- type ProviderSettings,
188
- type EventTypeSettings,
189
- type GlobalDefaults,
190
- type ResolvedSettings,
191
- type OrgPermission,
192
- } from "./multi-tenancy.js";
@@ -1,301 +0,0 @@
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
- }