Skip to content

Commit bf38eb8

Browse files
refactor: Use sessionStorage instead of localStorage for decoy booking data (calcom#24450)
* feat: Add async spam check integration and decoy booking response - Integrate SpamCheckService with handleNewBooking workflow - Implement parallel spam check execution for minimal performance impact - Add decoy booking response with localStorage-based success page - Extract organization ID from event type for org-specific blocking - Add comprehensive test coverage for spam detection scenarios - Create reusable components for booking success cards - Implement fail-open behavior to never block legitimate bookings This builds on the spam blocker DI infrastructure from PR calcom#24040 by adding the actual integration into the booking flow and implementing the decoy response mechanism to avoid revealing spam detection to malicious actors. Related: calcom#24040 Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Do checks in paralle * Fix leaking host name in title * Reduce expoiry time localstorage * refactor: Use sessionStorage instead of localStorage for decoy booking data - Replace localStorage with sessionStorage for automatic expiration on tab close - Remove timestamp tracking and TTL logic (no longer needed) - Improve privacy by auto-clearing data when browser tab/window closes - Update documentation to reflect sessionStorage behavior This change addresses privacy concerns by ensuring decoy booking data (including attendee email) is automatically removed when the user closes the tab, rather than persisting for 5 minutes or requiring manual cleanup. Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * feat: Add sessionStorage wrapper to webstorage module Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * Reset RegularBookingService.ts to main's version exactly * feat: Add 5-minute expiration timeout to decoy booking data - Adds timestamp to DecoyBookingData interface - Checks expiration when retrieving booking data - Automatically removes expired data from sessionStorage - Provides defense-in-depth against potential misuse - Works alongside sessionStorage auto-clear on tab close --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 010ee0d commit bf38eb8

2 files changed

Lines changed: 14 additions & 24 deletions

File tree

packages/features/bookings/lib/client/decoyBookingStore.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { localStorage } from "@calcom/lib/webstorage";
1+
import { sessionStorage } from "@calcom/lib/webstorage";
22

33
const BOOKING_SUCCESS_STORAGE_KEY_PREFIX = "cal.successfulBooking";
4+
const DECOY_BOOKING_EXPIRATION_MS = 5 * 60 * 1000;
45

56
interface DecoyBookingData {
67
booking: {
@@ -20,7 +21,8 @@ function getStorageKey(uid: string): string {
2021
}
2122

2223
/**
23-
* Stores decoy booking data in localStorage using the booking's uid
24+
* Stores decoy booking data in sessionStorage using the booking's uid
25+
* Data automatically expires when the browser tab/window is closed or after 5 minutes
2426
* @param booking - The booking data to store (must include uid)
2527
*/
2628
export function storeDecoyBooking(booking: Record<string, unknown> & { uid: string }): void {
@@ -29,11 +31,11 @@ export function storeDecoyBooking(booking: Record<string, unknown> & { uid: stri
2931
timestamp: Date.now(),
3032
};
3133
const storageKey = getStorageKey(booking.uid);
32-
localStorage.setItem(storageKey, JSON.stringify(bookingSuccessData));
34+
sessionStorage.setItem(storageKey, JSON.stringify(bookingSuccessData));
3335
}
3436

3537
/**
36-
* Retrieves decoy booking data from localStorage
38+
* Retrieves decoy booking data from sessionStorage
3739
* @param uid - The booking uid
3840
* @returns The stored booking data or null if not found or expired
3941
*/
@@ -43,7 +45,7 @@ export function getDecoyBooking(uid: string): DecoyBookingData | null {
4345
}
4446

4547
const storageKey = getStorageKey(uid);
46-
const dataStr = localStorage.getItem(storageKey);
48+
const dataStr = sessionStorage.getItem(storageKey);
4749

4850
if (!dataStr) {
4951
return null;
@@ -52,26 +54,22 @@ export function getDecoyBooking(uid: string): DecoyBookingData | null {
5254
try {
5355
const data: DecoyBookingData = JSON.parse(dataStr);
5456

55-
// Check if the data is too old (5 min)
56-
const dataAge = Date.now() - data.timestamp;
57-
const maxAge = 5 * 60 * 1000; // 5 minutes in milliseconds
58-
59-
if (dataAge > maxAge) {
60-
// Remove the data from localStorage if expired
61-
localStorage.removeItem(storageKey);
57+
const isExpired = Date.now() - data.timestamp > DECOY_BOOKING_EXPIRATION_MS;
58+
if (isExpired) {
59+
sessionStorage.removeItem(storageKey);
6260
return null;
6361
}
6462

6563
return data;
6664
} catch {
6765
// If parsing fails, remove the corrupted data
68-
localStorage.removeItem(storageKey);
66+
sessionStorage.removeItem(storageKey);
6967
return null;
7068
}
7169
}
7270

7371
/**
74-
* Removes decoy booking data from localStorage
72+
* Removes decoy booking data from sessionStorage
7573
* @param uid - The booking uid
7674
*/
7775
export function removeDecoyBooking(uid: string): void {
@@ -80,7 +78,7 @@ export function removeDecoyBooking(uid: string): void {
8078
}
8179

8280
const storageKey = getStorageKey(uid);
83-
localStorage.removeItem(storageKey);
81+
sessionStorage.removeItem(storageKey);
8482
}
8583

8684
export type { DecoyBookingData };

packages/lib/webstorage.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
/**
2-
* Provides a wrapper around localStorage to avoid errors in case of restricted storage access.
2+
* Provides a wrapper around localStorage and sessionStorage to avoid errors in case of restricted storage access.
33
*
44
* TODO: In case of an embed if localStorage is not available(third party), use localStorage of parent(first party) that contains the iframe.
55
*/
66
export const localStorage = {
77
getItem(key: string) {
88
try {
9-
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
109
return window.localStorage.getItem(key);
1110
} catch {
1211
// In case storage is restricted. Possible reasons
@@ -16,7 +15,6 @@ export const localStorage = {
1615
},
1716
setItem(key: string, value: string) {
1817
try {
19-
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
2018
window.localStorage.setItem(key, value);
2119
} catch {
2220
// In case storage is restricted. Possible reasons
@@ -27,19 +25,13 @@ export const localStorage = {
2725
},
2826
removeItem: (key: string) => {
2927
try {
30-
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
3128
window.localStorage.removeItem(key);
3229
} catch {
3330
return;
3431
}
3532
},
3633
};
3734

38-
/**
39-
* Provides a wrapper around localStorage to avoid errors in case of restricted storage access.
40-
*
41-
* TODO: In case of an embed if localStorage is not available(third party), use localStorage of parent(first party) that contains the iframe.
42-
*/
4335
export const sessionStorage = {
4436
getItem(key: string) {
4537
try {

0 commit comments

Comments
 (0)