@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
package/dist/api.js
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API utilities for SlotKit.
|
|
3
|
+
*
|
|
4
|
+
* Provides standardized response formatting, API key management,
|
|
5
|
+
* rate limiting, pagination, and request validation helpers
|
|
6
|
+
* for Next.js API routes.
|
|
7
|
+
*/
|
|
8
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Error Codes
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/** Standard API error codes */
|
|
13
|
+
export const API_ERROR_CODES = {
|
|
14
|
+
NOT_FOUND: "NOT_FOUND",
|
|
15
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
16
|
+
FORBIDDEN: "FORBIDDEN",
|
|
17
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
18
|
+
CONFLICT: "CONFLICT",
|
|
19
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
20
|
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
21
|
+
PAYMENT_REQUIRED: "PAYMENT_REQUIRED",
|
|
22
|
+
};
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Response Helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Create a standardized API error response.
|
|
28
|
+
*
|
|
29
|
+
* @param code - Error code from API_ERROR_CODES
|
|
30
|
+
* @param message - Human-readable error message
|
|
31
|
+
* @param details - Optional additional details
|
|
32
|
+
* @returns Standardized error response object
|
|
33
|
+
*/
|
|
34
|
+
export function createErrorResponse(code, message, details) {
|
|
35
|
+
return {
|
|
36
|
+
error: {
|
|
37
|
+
code,
|
|
38
|
+
message,
|
|
39
|
+
...(details ? { details } : {}),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a standardized API success response.
|
|
45
|
+
*
|
|
46
|
+
* @param data - The response data
|
|
47
|
+
* @param meta - Optional pagination metadata
|
|
48
|
+
* @returns Standardized success response object
|
|
49
|
+
*/
|
|
50
|
+
export function createSuccessResponse(data, meta) {
|
|
51
|
+
return {
|
|
52
|
+
data,
|
|
53
|
+
...(meta ? { meta } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create a paginated list response.
|
|
58
|
+
*
|
|
59
|
+
* @param items - The page of items
|
|
60
|
+
* @param nextCursor - Cursor for the next page (null if last page)
|
|
61
|
+
* @param total - Optional total count
|
|
62
|
+
* @returns Paginated response
|
|
63
|
+
*/
|
|
64
|
+
export function createPaginatedResponse(items, nextCursor, total) {
|
|
65
|
+
return {
|
|
66
|
+
data: items,
|
|
67
|
+
meta: {
|
|
68
|
+
nextCursor,
|
|
69
|
+
hasMore: nextCursor !== null,
|
|
70
|
+
...(total !== undefined ? { total } : {}),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Generate a new API key.
|
|
76
|
+
*
|
|
77
|
+
* The full key is only returned once and should be displayed to the user.
|
|
78
|
+
* Only the hash is stored in the database.
|
|
79
|
+
*
|
|
80
|
+
* @param prefix - Key prefix (e.g., "sk_live_" or "sk_test_")
|
|
81
|
+
* @returns Generated key with hash for storage
|
|
82
|
+
*/
|
|
83
|
+
export function generateApiKey(prefix = "sk_live_") {
|
|
84
|
+
const rawKey = randomBytes(32).toString("hex");
|
|
85
|
+
const key = `${prefix}${rawKey}`;
|
|
86
|
+
const hash = hashApiKey(key);
|
|
87
|
+
const displayPrefix = key.slice(0, prefix.length + 8) + "...";
|
|
88
|
+
return { key, prefix: displayPrefix, hash };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Hash an API key for secure storage.
|
|
92
|
+
*
|
|
93
|
+
* Uses HMAC-SHA256 with a secret from the SLOTKIT_API_KEY_SECRET
|
|
94
|
+
* environment variable. Throws if the secret is not configured.
|
|
95
|
+
*
|
|
96
|
+
* @param key - The full API key
|
|
97
|
+
* @param secret - Optional HMAC secret (defaults to SLOTKIT_API_KEY_SECRET env var)
|
|
98
|
+
* @returns The hex-encoded hash
|
|
99
|
+
*/
|
|
100
|
+
export function hashApiKey(key, secret) {
|
|
101
|
+
const hmacSecret = secret ?? process.env.SLOTKIT_API_KEY_SECRET;
|
|
102
|
+
if (!hmacSecret) {
|
|
103
|
+
throw new Error("SLOTKIT_API_KEY_SECRET environment variable is required for API key hashing. " +
|
|
104
|
+
"Set it to a random 32+ character string.");
|
|
105
|
+
}
|
|
106
|
+
return createHmac("sha256", hmacSecret).update(key).digest("hex");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Verify an API key against a stored hash.
|
|
110
|
+
*
|
|
111
|
+
* @param key - The key to verify
|
|
112
|
+
* @param storedHash - The hash stored in the database
|
|
113
|
+
* @returns Whether the key is valid
|
|
114
|
+
*/
|
|
115
|
+
export function verifyApiKey(key, storedHash) {
|
|
116
|
+
const hash = hashApiKey(key);
|
|
117
|
+
if (hash.length !== storedHash.length)
|
|
118
|
+
return false;
|
|
119
|
+
// Constant-time comparison
|
|
120
|
+
const a = Buffer.from(hash, "hex");
|
|
121
|
+
const b = Buffer.from(storedHash, "hex");
|
|
122
|
+
if (a.length !== b.length)
|
|
123
|
+
return false;
|
|
124
|
+
let mismatch = 0;
|
|
125
|
+
for (let i = 0; i < a.length; i++) {
|
|
126
|
+
mismatch |= a[i] ^ b[i];
|
|
127
|
+
}
|
|
128
|
+
return mismatch === 0;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Check if an API key has a required scope.
|
|
132
|
+
*
|
|
133
|
+
* Admin scope grants all permissions.
|
|
134
|
+
*
|
|
135
|
+
* @param keyScopes - The scopes granted to the API key
|
|
136
|
+
* @param requiredScope - The scope to check for
|
|
137
|
+
* @returns Whether the key has the required scope
|
|
138
|
+
*/
|
|
139
|
+
export function hasScope(keyScopes, requiredScope) {
|
|
140
|
+
return keyScopes.includes("admin") || keyScopes.includes(requiredScope);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check if an API key has expired.
|
|
144
|
+
*
|
|
145
|
+
* @param expiresAt - Optional expiry timestamp
|
|
146
|
+
* @returns Whether the key is expired
|
|
147
|
+
*/
|
|
148
|
+
export function isKeyExpired(expiresAt) {
|
|
149
|
+
if (!expiresAt)
|
|
150
|
+
return false;
|
|
151
|
+
return expiresAt.getTime() < Date.now();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Check if a request should be allowed under rate limiting.
|
|
155
|
+
*
|
|
156
|
+
* Uses a fixed 1-minute window.
|
|
157
|
+
*
|
|
158
|
+
* @param state - Current rate limit state
|
|
159
|
+
* @param limit - Maximum requests per minute
|
|
160
|
+
* @param nowMs - Current timestamp in milliseconds
|
|
161
|
+
* @returns Rate limit check result
|
|
162
|
+
*/
|
|
163
|
+
export function checkRateLimit(state, limit, nowMs = Date.now()) {
|
|
164
|
+
const windowMs = 60 * 1000; // 1 minute
|
|
165
|
+
const windowStart = nowMs - (nowMs % windowMs);
|
|
166
|
+
// New window or no existing state
|
|
167
|
+
if (!state || state.windowStartMs !== windowStart) {
|
|
168
|
+
return {
|
|
169
|
+
result: {
|
|
170
|
+
allowed: true,
|
|
171
|
+
remaining: limit - 1,
|
|
172
|
+
resetMs: windowStart + windowMs,
|
|
173
|
+
limit,
|
|
174
|
+
},
|
|
175
|
+
newState: { count: 1, windowStartMs: windowStart },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const newCount = state.count + 1;
|
|
179
|
+
const allowed = newCount <= limit;
|
|
180
|
+
return {
|
|
181
|
+
result: {
|
|
182
|
+
allowed,
|
|
183
|
+
remaining: Math.max(0, limit - newCount),
|
|
184
|
+
resetMs: windowStart + windowMs,
|
|
185
|
+
limit,
|
|
186
|
+
},
|
|
187
|
+
newState: { count: newCount, windowStartMs: windowStart },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Pagination
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
/**
|
|
194
|
+
* Encode a cursor for pagination (base64 JSON).
|
|
195
|
+
*
|
|
196
|
+
* @param data - The cursor data to encode
|
|
197
|
+
* @returns Base64-encoded cursor string
|
|
198
|
+
*/
|
|
199
|
+
export function encodeCursor(data) {
|
|
200
|
+
return Buffer.from(JSON.stringify(data)).toString("base64url");
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Decode a pagination cursor.
|
|
204
|
+
*
|
|
205
|
+
* @param cursor - The base64-encoded cursor string
|
|
206
|
+
* @returns The decoded cursor data, or null if invalid
|
|
207
|
+
*/
|
|
208
|
+
export function decodeCursor(cursor) {
|
|
209
|
+
try {
|
|
210
|
+
const json = Buffer.from(cursor, "base64url").toString("utf-8");
|
|
211
|
+
return JSON.parse(json);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Validate API request parameters for slot queries.
|
|
219
|
+
*
|
|
220
|
+
* @param params - Query parameters
|
|
221
|
+
* @returns Validation result
|
|
222
|
+
*/
|
|
223
|
+
export function validateSlotQueryParams(params) {
|
|
224
|
+
const errors = [];
|
|
225
|
+
if (!params.providerId && !params.teamId) {
|
|
226
|
+
errors.push({
|
|
227
|
+
field: "providerId",
|
|
228
|
+
message: "Either providerId or teamId is required",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (!params.start) {
|
|
232
|
+
errors.push({ field: "start", message: "start date is required" });
|
|
233
|
+
}
|
|
234
|
+
else if (isNaN(Date.parse(params.start))) {
|
|
235
|
+
errors.push({
|
|
236
|
+
field: "start",
|
|
237
|
+
message: `Invalid date: "${params.start}"`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (!params.end) {
|
|
241
|
+
errors.push({ field: "end", message: "end date is required" });
|
|
242
|
+
}
|
|
243
|
+
else if (isNaN(Date.parse(params.end))) {
|
|
244
|
+
errors.push({ field: "end", message: `Invalid date: "${params.end}"` });
|
|
245
|
+
}
|
|
246
|
+
if (params.start && params.end) {
|
|
247
|
+
const start = new Date(params.start);
|
|
248
|
+
const end = new Date(params.end);
|
|
249
|
+
if (!isNaN(start.getTime()) && !isNaN(end.getTime()) && end <= start) {
|
|
250
|
+
errors.push({ field: "end", message: "end must be after start" });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { valid: errors.length === 0, errors };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Parse and validate a sort parameter.
|
|
257
|
+
*
|
|
258
|
+
* @param sortParam - Sort string (e.g., "-createdAt" or "startsAt")
|
|
259
|
+
* @param allowedFields - Fields that can be sorted by
|
|
260
|
+
* @returns Parsed sort field and direction
|
|
261
|
+
*/
|
|
262
|
+
export function parseSortParam(sortParam, allowedFields) {
|
|
263
|
+
if (!sortParam)
|
|
264
|
+
return null;
|
|
265
|
+
const descending = sortParam.startsWith("-");
|
|
266
|
+
const field = descending ? sortParam.slice(1) : sortParam;
|
|
267
|
+
if (!allowedFields.includes(field))
|
|
268
|
+
return null;
|
|
269
|
+
return { field, direction: descending ? "desc" : "asc" };
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Represents an authenticated user in the system */
|
|
2
|
+
export interface AuthUser {
|
|
3
|
+
id: string;
|
|
4
|
+
email: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
role?: "admin" | "provider" | "customer";
|
|
7
|
+
}
|
|
8
|
+
/** Session returned by the auth adapter */
|
|
9
|
+
export interface AuthSession {
|
|
10
|
+
user: AuthUser;
|
|
11
|
+
expires: Date;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Pluggable authentication adapter interface.
|
|
15
|
+
* Default implementation uses NextAuth.js.
|
|
16
|
+
* Swap to Supabase Auth, Clerk, or Lucia by implementing this interface.
|
|
17
|
+
*/
|
|
18
|
+
export interface AuthAdapter {
|
|
19
|
+
/** Get the currently authenticated user from the request */
|
|
20
|
+
getCurrentUser(request: Request): Promise<AuthUser | null>;
|
|
21
|
+
/** Get the full session */
|
|
22
|
+
getSession(request: Request): Promise<AuthSession | null>;
|
|
23
|
+
/** Verify an API token or signed booking token */
|
|
24
|
+
verifyToken(token: string): Promise<AuthUser | null>;
|
|
25
|
+
}
|
|
26
|
+
/** Request with injected auth context */
|
|
27
|
+
export interface AuthenticatedRequest extends Request {
|
|
28
|
+
user: AuthUser;
|
|
29
|
+
}
|
|
30
|
+
/** Options for the withAuth middleware */
|
|
31
|
+
export interface WithAuthOptions {
|
|
32
|
+
/** Require a specific role */
|
|
33
|
+
requiredRole?: "admin" | "provider" | "customer";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Middleware wrapper that injects the authenticated user into every request.
|
|
37
|
+
*
|
|
38
|
+
* - Rejects unauthenticated requests with 401.
|
|
39
|
+
* - Optionally checks user role.
|
|
40
|
+
* - Passes the authenticated user to the handler.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* // In a Next.js API route
|
|
45
|
+
* export const GET = withAuth(authAdapter, async (req) => {
|
|
46
|
+
* const userId = req.user.id;
|
|
47
|
+
* // Provider can only access their own data
|
|
48
|
+
* const bookings = await db.query.bookings.findMany({
|
|
49
|
+
* where: eq(bookings.providerId, userId)
|
|
50
|
+
* });
|
|
51
|
+
* return Response.json(bookings);
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function withAuth(adapter: AuthAdapter, handler: (req: AuthenticatedRequest) => Promise<Response>, options?: WithAuthOptions): (req: Request) => Promise<Response>;
|
|
56
|
+
/**
|
|
57
|
+
* Helper to scope database queries to the authenticated user.
|
|
58
|
+
* Providers can only access their own rows (user_id matches).
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const provider = await assertOwnership(db, providers, req.user.id, providerId);
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function assertProviderOwnership(userId: string, resourceUserId: string): void;
|
|
66
|
+
/**
|
|
67
|
+
* Helper to verify customer access to their own bookings.
|
|
68
|
+
* Customers can only access bookings where customer_email matches.
|
|
69
|
+
*/
|
|
70
|
+
export declare function assertCustomerAccess(userEmail: string, bookingCustomerEmail: string): void;
|
|
71
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAEA,qDAAqD;AACrD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,UAAU,CAAC;CAC1C;AAED,2CAA2C;AAC3C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,IAAI,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;IAC3D,2BAA2B;IAC3B,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC1D,kDAAkD;IAClD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;CACtD;AAED,yCAAyC;AACzC,MAAM,WAAW,oBAAqB,SAAQ,OAAO;IACnD,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,8BAA8B;IAC9B,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,UAAU,CAAC;CAClD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,CAAC,GAAG,EAAE,oBAAoB,KAAK,OAAO,CAAC,QAAQ,CAAC,EACzD,OAAO,CAAC,EAAE,eAAe,GACxB,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA6CrC;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,IAAI,CAMN;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,oBAAoB,EAAE,MAAM,GAC3B,IAAI,CAMN"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { UnauthorizedError, ForbiddenError } from "@thebookingkit/core";
|
|
2
|
+
/**
|
|
3
|
+
* Middleware wrapper that injects the authenticated user into every request.
|
|
4
|
+
*
|
|
5
|
+
* - Rejects unauthenticated requests with 401.
|
|
6
|
+
* - Optionally checks user role.
|
|
7
|
+
* - Passes the authenticated user to the handler.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* // In a Next.js API route
|
|
12
|
+
* export const GET = withAuth(authAdapter, async (req) => {
|
|
13
|
+
* const userId = req.user.id;
|
|
14
|
+
* // Provider can only access their own data
|
|
15
|
+
* const bookings = await db.query.bookings.findMany({
|
|
16
|
+
* where: eq(bookings.providerId, userId)
|
|
17
|
+
* });
|
|
18
|
+
* return Response.json(bookings);
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function withAuth(adapter, handler, options) {
|
|
23
|
+
return async (req) => {
|
|
24
|
+
try {
|
|
25
|
+
// Try to get user from session first
|
|
26
|
+
let user = await adapter.getCurrentUser(req);
|
|
27
|
+
// If no session, try Bearer token
|
|
28
|
+
if (!user) {
|
|
29
|
+
const authHeader = req.headers.get("authorization");
|
|
30
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
31
|
+
const token = authHeader.slice(7);
|
|
32
|
+
user = await adapter.verifyToken(token);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!user) {
|
|
36
|
+
throw new UnauthorizedError();
|
|
37
|
+
}
|
|
38
|
+
// Check required role if specified
|
|
39
|
+
if (options?.requiredRole && user.role !== options.requiredRole) {
|
|
40
|
+
throw new ForbiddenError();
|
|
41
|
+
}
|
|
42
|
+
// Inject user into request
|
|
43
|
+
const authReq = req;
|
|
44
|
+
authReq.user = user;
|
|
45
|
+
return await handler(authReq);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof UnauthorizedError) {
|
|
49
|
+
return Response.json({ error: error.message, code: error.code }, { status: 401 });
|
|
50
|
+
}
|
|
51
|
+
if (error instanceof ForbiddenError) {
|
|
52
|
+
return Response.json({ error: error.message, code: error.code }, { status: 403 });
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Helper to scope database queries to the authenticated user.
|
|
60
|
+
* Providers can only access their own rows (user_id matches).
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const provider = await assertOwnership(db, providers, req.user.id, providerId);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function assertProviderOwnership(userId, resourceUserId) {
|
|
68
|
+
if (userId !== resourceUserId) {
|
|
69
|
+
throw new ForbiddenError("You do not have permission to access this provider's data.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Helper to verify customer access to their own bookings.
|
|
74
|
+
* Customers can only access bookings where customer_email matches.
|
|
75
|
+
*/
|
|
76
|
+
export function assertCustomerAccess(userEmail, bookingCustomerEmail) {
|
|
77
|
+
if (userEmail !== bookingCustomerEmail) {
|
|
78
|
+
throw new ForbiddenError("You do not have permission to access this booking.");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAyCxE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,QAAQ,CACtB,OAAoB,EACpB,OAAyD,EACzD,OAAyB;IAEzB,OAAO,KAAK,EAAE,GAAY,EAAqB,EAAE;QAC/C,IAAI,CAAC;YACH,qCAAqC;YACrC,IAAI,IAAI,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;YAE7C,kCAAkC;YAClC,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;gBACpD,IAAI,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;oBACtC,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;oBAClC,IAAI,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;gBAC1C,CAAC;YACH,CAAC;YAED,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,iBAAiB,EAAE,CAAC;YAChC,CAAC;YAED,mCAAmC;YACnC,IAAI,OAAO,EAAE,YAAY,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC;gBAChE,MAAM,IAAI,cAAc,EAAE,CAAC;YAC7B,CAAC;YAED,2BAA2B;YAC3B,MAAM,OAAO,GAAG,GAA2B,CAAC;YAC5C,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;YAEpB,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,iBAAiB,EAAE,CAAC;gBACvC,OAAO,QAAQ,CAAC,IAAI,CAClB,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,EAC1C,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAC;YACJ,CAAC;YACD,IAAI,KAAK,YAAY,cAAc,EAAE,CAAC;gBACpC,OAAO,QAAQ,CAAC,IAAI,CAClB,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,EAC1C,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAC;YACJ,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAc,EACd,cAAsB;IAEtB,IAAI,MAAM,KAAK,cAAc,EAAE,CAAC;QAC9B,MAAM,IAAI,cAAc,CACtB,4DAA4D,CAC7D,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,SAAiB,EACjB,oBAA4B;IAE5B,IAAI,SAAS,KAAK,oBAAoB,EAAE,CAAC;QACvC,MAAM,IAAI,cAAc,CACtB,oDAAoD,CACrD,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a signed booking management token.
|
|
3
|
+
*
|
|
4
|
+
* The token is a signed payload containing the booking ID and expiry time.
|
|
5
|
+
* It allows customers to view/manage their booking without authentication.
|
|
6
|
+
*
|
|
7
|
+
* @param bookingId - The booking UUID
|
|
8
|
+
* @param expiresAt - When the token expires
|
|
9
|
+
* @param secret - HMAC signing secret
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateBookingToken(bookingId: string, expiresAt: Date, secret: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Verify and decode a booking management token.
|
|
14
|
+
*
|
|
15
|
+
* @param token - The base64url-encoded token
|
|
16
|
+
* @param secret - HMAC signing secret (must match generation)
|
|
17
|
+
* @returns The booking ID if valid, null if invalid or expired
|
|
18
|
+
*/
|
|
19
|
+
export declare function verifyBookingToken(token: string, secret: string): {
|
|
20
|
+
bookingId: string;
|
|
21
|
+
expiresAt: Date;
|
|
22
|
+
} | null;
|
|
23
|
+
//# sourceMappingURL=booking-tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-tokens.d.ts","sourceRoot":"","sources":["../src/booking-tokens.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,IAAI,EACf,MAAM,EAAE,MAAM,GACb,MAAM,CAOR;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,GAAG,IAAI,CAyB/C"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHmac } from "crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Generate a signed booking management token.
|
|
4
|
+
*
|
|
5
|
+
* The token is a signed payload containing the booking ID and expiry time.
|
|
6
|
+
* It allows customers to view/manage their booking without authentication.
|
|
7
|
+
*
|
|
8
|
+
* @param bookingId - The booking UUID
|
|
9
|
+
* @param expiresAt - When the token expires
|
|
10
|
+
* @param secret - HMAC signing secret
|
|
11
|
+
*/
|
|
12
|
+
export function generateBookingToken(bookingId, expiresAt, secret) {
|
|
13
|
+
const payload = `${bookingId}:${expiresAt.getTime()}`;
|
|
14
|
+
const signature = createHmac("sha256", secret)
|
|
15
|
+
.update(payload)
|
|
16
|
+
.digest("hex")
|
|
17
|
+
.slice(0, 16);
|
|
18
|
+
return Buffer.from(`${payload}:${signature}`).toString("base64url");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Verify and decode a booking management token.
|
|
22
|
+
*
|
|
23
|
+
* @param token - The base64url-encoded token
|
|
24
|
+
* @param secret - HMAC signing secret (must match generation)
|
|
25
|
+
* @returns The booking ID if valid, null if invalid or expired
|
|
26
|
+
*/
|
|
27
|
+
export function verifyBookingToken(token, secret) {
|
|
28
|
+
try {
|
|
29
|
+
const decoded = Buffer.from(token, "base64url").toString("utf-8");
|
|
30
|
+
const parts = decoded.split(":");
|
|
31
|
+
if (parts.length !== 3)
|
|
32
|
+
return null;
|
|
33
|
+
const [bookingId, expiresAtStr, signature] = parts;
|
|
34
|
+
const expiresAt = new Date(Number(expiresAtStr));
|
|
35
|
+
// Check expiry
|
|
36
|
+
if (expiresAt < new Date())
|
|
37
|
+
return null;
|
|
38
|
+
// Verify signature
|
|
39
|
+
const payload = `${bookingId}:${expiresAtStr}`;
|
|
40
|
+
const expectedSig = createHmac("sha256", secret)
|
|
41
|
+
.update(payload)
|
|
42
|
+
.digest("hex")
|
|
43
|
+
.slice(0, 16);
|
|
44
|
+
if (signature !== expectedSig)
|
|
45
|
+
return null;
|
|
46
|
+
return { bookingId, expiresAt };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=booking-tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"booking-tokens.js","sourceRoot":"","sources":["../src/booking-tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAClC,SAAiB,EACjB,SAAe,EACf,MAAc;IAEd,MAAM,OAAO,GAAG,GAAG,SAAS,IAAI,SAAS,CAAC,OAAO,EAAE,EAAE,CAAC;IACtD,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC;SAC3C,MAAM,CAAC,OAAO,CAAC;SACf,MAAM,CAAC,KAAK,CAAC;SACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAChB,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,IAAI,SAAS,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,KAAa,EACb,MAAc;IAEd,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QAClE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEpC,MAAM,CAAC,SAAS,EAAE,YAAY,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC;QACnD,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;QAEjD,eAAe;QACf,IAAI,SAAS,GAAG,IAAI,IAAI,EAAE;YAAE,OAAO,IAAI,CAAC;QAExC,mBAAmB;QACnB,MAAM,OAAO,GAAG,GAAG,SAAS,IAAI,YAAY,EAAE,CAAC;QAC/C,MAAM,WAAW,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC;aAC7C,MAAM,CAAC,OAAO,CAAC;aACf,MAAM,CAAC,KAAK,CAAC;aACb,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAEhB,IAAI,SAAS,KAAK,WAAW;YAAE,OAAO,IAAI,CAAC;QAE3C,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Variables available in email templates */
|
|
2
|
+
export interface EmailTemplateVars {
|
|
3
|
+
bookingId: string;
|
|
4
|
+
eventTitle: string;
|
|
5
|
+
providerName: string;
|
|
6
|
+
customerName: string;
|
|
7
|
+
customerEmail: string;
|
|
8
|
+
date: string;
|
|
9
|
+
time: string;
|
|
10
|
+
duration: string;
|
|
11
|
+
timezone: string;
|
|
12
|
+
location?: string;
|
|
13
|
+
managementUrl?: string;
|
|
14
|
+
unsubscribeUrl?: string;
|
|
15
|
+
cancelReason?: string;
|
|
16
|
+
oldDate?: string;
|
|
17
|
+
oldTime?: string;
|
|
18
|
+
newDate?: string;
|
|
19
|
+
newTime?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Interpolate template variables into a template string.
|
|
23
|
+
* Variables use the format `{variableName}`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function interpolateTemplate(template: string, vars: EmailTemplateVars): string;
|
|
26
|
+
/** Default booking confirmation email template (HTML) */
|
|
27
|
+
export declare const CONFIRMATION_EMAIL_HTML: string;
|
|
28
|
+
/** Default booking confirmation email (plain text) */
|
|
29
|
+
export declare const CONFIRMATION_EMAIL_TEXT: string;
|
|
30
|
+
/** Default reminder email template */
|
|
31
|
+
export declare const REMINDER_EMAIL_HTML: string;
|
|
32
|
+
/** Default cancellation email template */
|
|
33
|
+
export declare const CANCELLATION_EMAIL_HTML: string;
|
|
34
|
+
/** Default reschedule email template */
|
|
35
|
+
export declare const RESCHEDULE_EMAIL_HTML: string;
|
|
36
|
+
//# sourceMappingURL=email-templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-templates.d.ts","sourceRoot":"","sources":["../src/email-templates.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,iBAAiB,GACtB,MAAM,CAKR;AAED,yDAAyD;AACzD,eAAO,MAAM,uBAAuB,QAqB5B,CAAC;AAET,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,QAgB5B,CAAC;AAET,sCAAsC;AACtC,eAAO,MAAM,mBAAmB,QAoBxB,CAAC;AAET,0CAA0C;AAC1C,eAAO,MAAM,uBAAuB,QAc5B,CAAC;AAET,wCAAwC;AACxC,eAAO,MAAM,qBAAqB,QAoB1B,CAAC"}
|