-
Notifications
You must be signed in to change notification settings - Fork 0
OAuth credential sync and app integration enhancements #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: oauth-security-base
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,93 @@ | ||||||||||||||||||||||||||
| import type { NextApiRequest, NextApiResponse } from "next"; | ||||||||||||||||||||||||||
| import z from "zod"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; | ||||||||||||||||||||||||||
| import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants"; | ||||||||||||||||||||||||||
| import { symmetricDecrypt } from "@calcom/lib/crypto"; | ||||||||||||||||||||||||||
| import prisma from "@calcom/prisma"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const appCredentialWebhookRequestBodySchema = z.object({ | ||||||||||||||||||||||||||
| // UserId of the cal.com user | ||||||||||||||||||||||||||
| userId: z.number().int(), | ||||||||||||||||||||||||||
| appSlug: z.string(), | ||||||||||||||||||||||||||
| // Keys should be AES256 encrypted with the CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY | ||||||||||||||||||||||||||
| keys: z.string(), | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| /** */ | ||||||||||||||||||||||||||
| export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||||||||||||||||||||||||||
| // Check that credential sharing is enabled | ||||||||||||||||||||||||||
| if (!APP_CREDENTIAL_SHARING_ENABLED) { | ||||||||||||||||||||||||||
| return res.status(403).json({ message: "Credential sharing is not enabled" }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Check that the webhook secret matches | ||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||
| req.headers[process.env.CALCOM_WEBHOOK_HEADER_NAME || "calcom-webhook-secret"] !== | ||||||||||||||||||||||||||
| process.env.CALCOM_WEBHOOK_SECRET | ||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||
| return res.status(403).json({ message: "Invalid webhook secret" }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const reqBody = appCredentialWebhookRequestBodySchema.parse(req.body); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Check that the user exists | ||||||||||||||||||||||||||
| const user = await prisma.user.findUnique({ where: { id: reqBody.userId } }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!user) { | ||||||||||||||||||||||||||
| return res.status(404).json({ message: "User not found" }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const app = await prisma.app.findUnique({ | ||||||||||||||||||||||||||
| where: { slug: reqBody.appSlug }, | ||||||||||||||||||||||||||
| select: { slug: true }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!app) { | ||||||||||||||||||||||||||
| return res.status(404).json({ message: "App not found" }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Search for the app's slug and type | ||||||||||||||||||||||||||
| const appMetadata = appStoreMetadata[app.slug as keyof typeof appStoreMetadata]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (!appMetadata) { | ||||||||||||||||||||||||||
| return res.status(404).json({ message: "App not found. Ensure that you have the correct app slug" }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Decrypt the keys | ||||||||||||||||||||||||||
| const keys = JSON.parse( | ||||||||||||||||||||||||||
| symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "") | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
Comment on lines
+56
to
+59
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for decryption and JSON parsing. Both 🔒 Proposed fix // Decrypt the keys
- const keys = JSON.parse(
- symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
- );
+ let keys;
+ try {
+ const decrypted = symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "");
+ keys = JSON.parse(decrypted);
+ } catch (error) {
+ return res.status(400).json({ message: "Failed to decrypt or parse credentials" });
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Can't use prisma upsert as we don't know the id of the credential | ||||||||||||||||||||||||||
| const appCredential = await prisma.credential.findFirst({ | ||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||
| userId: reqBody.userId, | ||||||||||||||||||||||||||
| appId: appMetadata.slug, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| select: { | ||||||||||||||||||||||||||
| id: true, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (appCredential) { | ||||||||||||||||||||||||||
| await prisma.credential.update({ | ||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||
| id: appCredential.id, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||||
| key: keys, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| return res.status(200).json({ message: `Credentials updated for userId: ${reqBody.userId}` }); | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| await prisma.credential.create({ | ||||||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||||
| key: keys, | ||||||||||||||||||||||||||
| userId: reqBody.userId, | ||||||||||||||||||||||||||
| appId: appMetadata.slug, | ||||||||||||||||||||||||||
| type: appMetadata.type, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| return res.status(200).json({ message: `Credentials created for userId: ${reqBody.userId}` }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,8 +3,8 @@ import type { NextApiRequest } from "next"; | |||||
| import { HttpError } from "@calcom/lib/http-error"; | ||||||
| import prisma from "@calcom/prisma"; | ||||||
|
|
||||||
| import { decodeOAuthState } from "./decodeOAuthState"; | ||||||
| import { throwIfNotHaveAdminAccessToTeam } from "./throwIfNotHaveAdminAccessToTeam"; | ||||||
| import { decodeOAuthState } from "../oauth/decodeOAuthState"; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix incorrect import path for decodeOAuthState. The import path The current path goes up to 🔧 Proposed fix-import { decodeOAuthState } from "../oauth/decodeOAuthState";
+import { decodeOAuthState } from "./decodeOAuthState";📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| import { throwIfNotHaveAdminAccessToTeam } from "../throwIfNotHaveAdminAccessToTeam"; | ||||||
|
|
||||||
| /** | ||||||
| * This function is used to create app credentials for either a user or a team | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { z } from "zod"; | ||
|
|
||
| import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants"; | ||
|
|
||
| const minimumTokenResponseSchema = z.object({ | ||
| access_token: z.string(), | ||
| // Assume that any property with a number is the expiry | ||
| [z.string().toString()]: z.number(), | ||
| // Allow other properties in the token response | ||
| [z.string().optional().toString()]: z.unknown().optional(), | ||
| }); | ||
|
Comment on lines
+5
to
+11
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Step 1: Locate and examine the parseRefreshTokenResponse.ts file
echo "=== Searching for parseRefreshTokenResponse.ts ==="
fd -t f "parseRefreshTokenResponse" packages/app-store --type ts
# Step 2: Check file exists and read its content
echo -e "\n=== File content ==="
cat -n packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts 2>/dev/null || echo "File not found at expected path"
# Step 3: Search for tests
echo -e "\n=== Searching for tests ==="
fd -t f "parseRefreshTokenResponse" packages/app-store --type ts --type tsx
# Step 4: Check for references to minimumTokenResponseSchema
echo -e "\n=== Searching for minimumTokenResponseSchema references ==="
rg "minimumTokenResponseSchema" packages/app-store --type ts -A 2 -B 2
# Step 5: Check overall file structure
echo -e "\n=== Full directory listing in _utils/oauth ==="
ls -la packages/app-store/_utils/oauth/ 2>/dev/null || echo "Directory not found"Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 3461 🏁 Script executed: #!/bin/bash
# Check APP_CREDENTIAL_SHARING_ENABLED constant definition and default value
echo "=== APP_CREDENTIAL_SHARING_ENABLED definition ==="
rg "APP_CREDENTIAL_SHARING_ENABLED" packages/lib --type ts -A 2 -B 2
# Check if there are any tests in the app-store directory that use parseRefreshTokenResponse
echo -e "\n=== Test files in app-store ==="
find packages/app-store -type f \( -name "*.test.ts" -o -name "*.test.tsx" -o -name "*.spec.ts" -o -name "*.spec.tsx" \)
# Check what Zod version is used
echo -e "\n=== Check Zod version ==="
rg '"zod"' --type json -A 1 | head -20Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 1349 🏁 Script executed: #!/bin/bash
# Let's check the Zod documentation pattern by examining how z.record() or other methods are used
echo "=== Searching for Zod usage patterns in codebase ==="
rg "z\.object\(" packages --type ts -A 5 | head -80
echo -e "\n=== Check for z.record or z.catchall usage ==="
rg "z\.record|z\.catchall|passthrough|discriminatedUnion" packages --type ts -B 2 -A 2 | head -100Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 12952 🏁 Script executed: #!/bin/bash
# Create a quick test to see what z.string().toString() produces
cat > /tmp/test_zod.js << 'EOF'
// Test what z.string().toString() returns
const zod = require('zod');
const z = zod.z;
console.log("z.string().toString():", z.string().toString());
console.log("Type:", typeof z.string().toString());
// Try to create a schema with the computed property
try {
const schema = z.object({
access_token: z.string(),
[z.string().toString()]: z.number(),
});
console.log("\nSchema created successfully");
console.log("Schema keys:", Object.keys(schema.shape));
// Test parsing
const testData = {
access_token: "test",
"[object Object]": 123,
};
const result = schema.safeParse(testData);
console.log("Parse result:", result);
} catch (e) {
console.error("\nError creating schema:", e.message);
}
EOF
# Try to run it if Node and zod are available
cd /tmp && npm install zod@3.22.2 > /dev/null 2>&1 && node test_zod.js 2>&1 || echo "Cannot test directly - Node/npm not available in sandbox"Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 310 Critical: Invalid Zod schema logic prevents proper OAuth response validation. Lines 8 and 10 use The intended behavior—accepting string const minimumTokenResponseSchema = z.object({
access_token: z.string(),
- // Assume that any property with a number is the expiry
- [z.string().toString()]: z.number(),
- // Allow other properties in the token response
- [z.string().optional().toString()]: z.unknown().optional(),
-});
+}).passthrough();This allows the schema to accept 🤖 Prompt for AI Agents |
||
|
|
||
| const parseRefreshTokenResponse = (response: any, schema: z.ZodTypeAny) => { | ||
| let refreshTokenResponse; | ||
| if (APP_CREDENTIAL_SHARING_ENABLED && process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT) { | ||
| refreshTokenResponse = minimumTokenResponseSchema.safeParse(response); | ||
| } else { | ||
| refreshTokenResponse = schema.safeParse(response); | ||
| } | ||
|
|
||
| if (!refreshTokenResponse.success) { | ||
| throw new Error("Invalid refreshed tokens were returned"); | ||
| } | ||
|
|
||
| if (!refreshTokenResponse.data.refresh_token) { | ||
| refreshTokenResponse.data.refresh_token = "refresh_token"; | ||
| } | ||
|
Comment on lines
+25
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, locate and read the file
find . -path "*packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts" -type fRepository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 149 🏁 Script executed: # Search for how refresh_token is used after being returned
rg "parseRefreshTokenResponse" packages/app-store --type ts -A 5 -B 2
# Look for tests related to this function
fd "parseRefreshTokenResponse" packages --type ts | grep -i test
# Search for credential sharing patterns
rg "APP_CREDENTIAL_SHARING_ENABLED|CALCOM_CREDENTIAL_SYNC" packages --type ts -C 3Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 10852 🏁 Script executed: cat -n packages/app-store/_utils/oauth/parseRefreshTokenResponse.tsRepository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 1341 🏁 Script executed: # Look at refreshOAuthTokens.ts to understand usage
cat -n packages/app-store/_utils/oauth/refreshOAuthTokens.ts | head -80
# Search for how the returned refresh_token is used in callers
rg "parseRefreshTokenResponse.*data" packages/app-store --type ts -A 3Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 1091 🏁 Script executed: # Check how the parsed result is used in actual integrations
cat -n packages/app-store/googlecalendar/lib/CalendarService.ts | sed -n '100,130p'
# Look for usage of refresh_token from parseRefreshTokenResponse
rg "parseRefreshTokenResponse\(" packages/app-store -A 10 -B 2 | head -100Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 6615 🏁 Script executed: # Check how refresh_token is handled in each integration more carefully
cat -n packages/app-store/salesforce/lib/CalendarService.ts | sed -n '65,95p'
# Check Office365 more carefully
cat -n packages/app-store/office365calendar/lib/CalendarService.ts | sed -n '180,220p'
# Check Zoom integration
cat -n packages/app-store/zoomvideo/lib/VideoApiAdapter.ts | sed -n '140,180p'
# Look for tests
find packages/app-store -name "*.test.ts" -o -name "*.spec.ts" | head -20Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 4917 🏁 Script executed: # Check what happens with the newTokens in Zoom
cat -n packages/app-store/zoomvideo/lib/VideoApiAdapter.ts | sed -n '252,300p'
# Check the Salesforce line more carefully - how is the parsed data used
cat -n packages/app-store/salesforce/lib/CalendarService.ts | sed -n '90,105p'
# Check if refresh_token is ever actually used from the returned parse result
rg "\.refresh_token" packages/app-store --type ts -B 2 -A 2 | head -80Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 9078 🏁 Script executed: # Check the complete Zoom flow where refresh_token is used
cat -n packages/app-store/zoomvideo/lib/VideoApiAdapter.ts | sed -n '220,250p'
# See the full context of how newTokens is obtained and used
cat -n packages/app-store/zoomvideo/lib/VideoApiAdapter.ts | sed -n '200,240p'
# Check what zoomTokenSchema requires
rg "zoomTokenSchema\|zoomRefreshedTokenSchema" packages/app-store/zoomvideo --type ts -B 2 -A 5Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 2986 🏁 Script executed: # Find where newTokens is used after parseRefreshTokenResponse
cat -n packages/app-store/zoomvideo/lib/VideoApiAdapter.ts | sed -n '305,330p'
# Check zoomTokenSchema definition
rg "const zoomTokenSchema\|const zoomRefreshedTokenSchema" packages/app-store/zoomvideo --type ts -A 8Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 1103 🏁 Script executed: # Find the zoomAuth function and where tokens are updated
cat -n packages/app-store/zoomvideo/lib/VideoApiAdapter.ts | sed -n '325,365p'
# Look for zoomTokenSchema and zoomRefreshedTokenSchema more directly
cat packages/app-store/zoomvideo/lib/VideoApiAdapter.ts | grep -n "Schema\|schema" | head -30Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 2270 🏁 Script executed: # Get the actual schema definitions
cat -n packages/app-store/zoomvideo/lib/VideoApiAdapter.ts | sed -n '52,75p'Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 1112 🏁 Script executed: # Look at how refreshAccessToken uses the refresh_token parameter
cat -n packages/app-store/zoomvideo/lib/VideoApiAdapter.ts | sed -n '74,110p'
# Check if there are any comments or documentation about credential sharing
rg "credential.?sharing\|APP_CREDENTIAL_SHARING" packages/app-store --type ts -i -B 2 -A 2 | head -60Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 1849 🏁 Script executed: # See the complete refreshAccessToken function and what happens after parsing
cat -n packages/app-store/zoomvideo/lib/VideoApiAdapter.ts | sed -n '74,140p'
# Check if minimumTokenResponseSchema requires refresh_token or makes it optional
cat -n packages/app-store/_utils/oauth/parseRefreshTokenResponse.tsRepository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 4493 🏁 Script executed: # Verify there are no tests that might show intended behavior
find packages/app-store -path "*.test.ts" -o -path "*.spec.ts" | xargs grep -l "parseRefreshTokenResponse\|refresh_token" 2>/dev/null | head -10Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 148 🏁 Script executed: # Check the Google Calendar test file for any tests related to parseRefreshTokenResponse
cat -n packages/app-store/googlecalendar/lib/CalendarService.test.ts | grep -A 10 -B 10 "parseRefreshTokenResponse\|refresh_token" | head -80Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 728 🏁 Script executed: # Search for any comments or documentation explaining the dummy refresh_token assignment
rg "refresh_token.*=.*refresh_token\|dummy\|placeholder\|TODO.*refresh" packages/app-store --type ts -B 5 -A 2
# Check git history or any related issues/docs about credential sharing
rg "credential.*sharing\|CALCOM_CREDENTIAL_SYNC" packages --type ts -i -B 3 -A 3 | head -100Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 69 Critical: Hardcoded dummy refresh token will break OAuth flows. Lines 25-27 set When Consider one of these approaches:
🤖 Prompt for AI Agents |
||
|
|
||
| return refreshTokenResponse; | ||
| }; | ||
|
|
||
| export default parseRefreshTokenResponse; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,22 @@ | ||||||
| import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants"; | ||||||
|
|
||||||
| const refreshOAuthTokens = async (refreshFunction: () => any, appSlug: string, userId: number | null) => { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Improve type safety for The ♻️ Proposed fix to improve type safety-const refreshOAuthTokens = async (refreshFunction: () => any, appSlug: string, userId: number | null) => {
+const refreshOAuthTokens = async (refreshFunction: () => Promise<Response>, appSlug: string, userId: number | null): Promise<Response> => {📝 Committable suggestion
Suggested change
|
||||||
| // Check that app syncing is enabled and that the credential belongs to a user | ||||||
| if (APP_CREDENTIAL_SHARING_ENABLED && process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT && userId) { | ||||||
| // Customize the payload based on what your endpoint requires | ||||||
| // The response should only contain the access token and expiry date | ||||||
| const response = await fetch(process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT, { | ||||||
| method: "POST", | ||||||
| body: new URLSearchParams({ | ||||||
| calcomUserId: userId.toString(), | ||||||
| appSlug, | ||||||
| }), | ||||||
| }); | ||||||
| return response; | ||||||
|
Comment on lines
+5
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling, timeout, and response validation to the fetch call. The fetch call to the external credential sync endpoint has several resilience and correctness gaps:
🔧 Proposed fix to add error handling and validation if (APP_CREDENTIAL_SHARING_ENABLED && process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT && userId) {
- // Customize the payload based on what your endpoint requires
- // The response should only contain the access token and expiry date
- const response = await fetch(process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT, {
- method: "POST",
- body: new URLSearchParams({
- calcomUserId: userId.toString(),
- appSlug,
- }),
- });
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
+
+ const response = await fetch(process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT, {
+ method: "POST",
+ body: new URLSearchParams({
+ calcomUserId: userId.toString(),
+ appSlug,
+ }),
+ signal: controller.signal,
+ });
+
+ clearTimeout(timeoutId);
+
+ if (!response.ok) {
+ throw new Error(`Credential sync endpoint returned ${response.status}: ${response.statusText}`);
+ }
+
- return response;
+ return response;
+ } catch (error) {
+ // Log and fallback to local refresh on sync endpoint failure
+ console.error("Failed to sync credentials via external endpoint:", error);
+ const response = await refreshFunction();
+ return response;
+ }
🤖 Prompt for AI Agents |
||||||
| } else { | ||||||
| const response = await refreshFunction(); | ||||||
| return response; | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| export default refreshOAuthTokens; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,8 @@ import type { | |
| } from "@calcom/types/Calendar"; | ||
| import type { CredentialPayload } from "@calcom/types/Credential"; | ||
|
|
||
| import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse"; | ||
| import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens"; | ||
| import { getGoogleAppKeys } from "./getGoogleAppKeys"; | ||
| import { googleCredentialSchema } from "./googleCredentialSchema"; | ||
|
|
||
|
|
@@ -81,11 +83,18 @@ export default class GoogleCalendarService implements Calendar { | |
|
|
||
| const refreshAccessToken = async (myGoogleAuth: Awaited<ReturnType<typeof getGoogleAuth>>) => { | ||
| try { | ||
| const { res } = await myGoogleAuth.refreshToken(googleCredentials.refresh_token); | ||
| const res = await refreshOAuthTokens( | ||
| async () => { | ||
| const fetchTokens = await myGoogleAuth.refreshToken(googleCredentials.refresh_token); | ||
| return fetchTokens.res; | ||
| }, | ||
| "google-calendar", | ||
| credential.userId | ||
| ); | ||
| const token = res?.data; | ||
| googleCredentials.access_token = token.access_token; | ||
| googleCredentials.expiry_date = token.expiry_date; | ||
|
Comment on lines
+86
to
96
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's find the refreshOAuthTokens function and understand its implementation
rg -n "refreshOAuthTokens" -A 15 -B 2 | head -100Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 8203 🏁 Script executed: # Check the complete CalendarService.ts file structure
cat -n packages/app-store/googlecalendar/lib/CalendarService.ts | head -120Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 5527 🏁 Script executed: # Look for parseRefreshTokenResponse
rg -n "parseRefreshTokenResponse" -A 10 -B 2Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 11774 🏁 Script executed: # Find minimumTokenResponseSchema definition
rg -n "minimumTokenResponseSchema" -B 5 -A 10Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 2690 🏁 Script executed: # Find refreshOAuthTokens implementation
find . -name "refreshOAuthTokens.ts" -o -name "refreshOAuthTokens.js" | head -5Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 142 🏁 Script executed: # Check googleCredentialSchema structure
rg -n "googleCredentialSchema" -B 2 -A 15 | head -80Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 6483 🏁 Script executed: # Read refreshOAuthTokens implementation
cat -n packages/app-store/_utils/oauth/refreshOAuthTokens.tsRepository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 1091 Response structure incompatibility will cause runtime errors when credential sync endpoint is enabled. The code at lines 94-96 assumes
When the sync endpoint is used, accessing The response must be handled differently based on which path is taken—either by calling 🤖 Prompt for AI Agents |
||
| const key = googleCredentialSchema.parse(googleCredentials); | ||
| const key = parseRefreshTokenResponse(googleCredentials, googleCredentialSchema); | ||
| await prisma.credential.update({ | ||
| where: { id: credential.id }, | ||
| data: { key }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ import type { | |
| import type { CredentialPayload } from "@calcom/types/Credential"; | ||
|
|
||
| import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; | ||
| import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens"; | ||
| import type { HubspotToken } from "../api/callback"; | ||
|
|
||
| const hubspotClient = new hubspot.Client(); | ||
|
|
@@ -173,13 +174,18 @@ export default class HubspotCalendarService implements Calendar { | |
|
|
||
| const refreshAccessToken = async (refreshToken: string) => { | ||
| try { | ||
| const hubspotRefreshToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken( | ||
| "refresh_token", | ||
| undefined, | ||
| WEBAPP_URL + "/api/integrations/hubspot/callback", | ||
| this.client_id, | ||
| this.client_secret, | ||
| refreshToken | ||
| const hubspotRefreshToken: HubspotToken = await refreshOAuthTokens( | ||
| async () => | ||
| await hubspotClient.oauth.tokensApi.createToken( | ||
| "refresh_token", | ||
| undefined, | ||
| WEBAPP_URL + "/api/integrations/hubspot/callback", | ||
| this.client_id, | ||
| this.client_secret, | ||
| refreshToken | ||
| ), | ||
| "hubspot", | ||
| credential.userId | ||
| ); | ||
|
Comment on lines
+177
to
189
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find the CalendarService.ts file and check the imports
find . -name "CalendarService.ts" -type fRepository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 980 🏁 Script executed: # Search for refreshOAuthTokens function definition
rg -n "function refreshOAuthTokens|const refreshOAuthTokens|export.*refreshOAuthTokens" --type ts -A 5Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 1021 🏁 Script executed: # Search for HubspotToken interface
rg -n "interface HubspotToken|type HubspotToken" --type ts -A 10Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 912 🏁 Script executed: # Search for TokenResponseIF interface
rg -n "interface TokenResponseIF|type TokenResponseIF" --type ts -A 15Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 69 🏁 Script executed: # Get full refreshOAuthTokens implementation
cat -n packages/app-store/_utils/oauth/refreshOAuthTokens.tsRepository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 1091 🏁 Script executed: # Search for TokenResponseIF interface definition (from any source)
rg -n "interface TokenResponseIF" --type ts -A 15Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 69 🏁 Script executed: # Get the CalendarService.ts file and read lines around 177-189
wc -l packages/app-store/hubspot/lib/CalendarService.tsRepository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 140 🏁 Script executed: # Get relevant sections from CalendarService.ts
sed -n '170,200p' packages/app-store/hubspot/lib/CalendarService.tsRepository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 1099 🏁 Script executed: # Find TokenResponseIF definition
rg -n "type TokenResponseIF|interface TokenResponseIF" -A 20Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 69 🏁 Script executed: # Check what other apps import/use from refreshOAuthTokens to see if this bug affects them
rg -n "refreshOAuthTokens" --type ts -B 2 -A 2 | head -50Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 3943 🏁 Script executed: # Check the webex usage to see how it handles the Response object
sed -n '60,75p' packages/app-store/webex/lib/VideoApiAdapter.tsRepository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 628 🏁 Script executed: # Check zoho-bigin usage to see how it expects token object
sed -n '85,100p' packages/app-store/zoho-bigin/lib/CalendarService.tsRepository: AI-Code-Review-Evals/coderabbit-cal_dot_com Length of output: 693 Critical: The function has two code paths with different return types:
At line 192, the HubSpot code directly accesses The function should parse and normalize all responses to a consistent token object format before returning, ensuring callers always receive the expected interface regardless of which code path executes. 🤖 Prompt for AI Agents |
||
|
|
||
| // set expiry date as offset from current time. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add HTTP method restriction and wrap schema parsing in try-catch.
The endpoint should restrict to POST requests and handle parsing errors gracefully to return proper 400 responses instead of 500s.
🔒 Proposed fix
export default async function handler(req: NextApiRequest, res: NextApiResponse) { + // Only allow POST requests + if (req.method !== "POST") { + return res.status(405).json({ message: "Method not allowed" }); + } + // Check that credential sharing is enabled if (!APP_CREDENTIAL_SHARING_ENABLED) { return res.status(403).json({ message: "Credential sharing is not enabled" }); } // Check that the webhook secret matches if ( req.headers[process.env.CALCOM_WEBHOOK_HEADER_NAME || "calcom-webhook-secret"] !== process.env.CALCOM_WEBHOOK_SECRET ) { return res.status(403).json({ message: "Invalid webhook secret" }); } - const reqBody = appCredentialWebhookRequestBodySchema.parse(req.body); + const parseResult = appCredentialWebhookRequestBodySchema.safeParse(req.body); + if (!parseResult.success) { + return res.status(400).json({ message: "Invalid request body", errors: parseResult.error.flatten() }); + } + const reqBody = parseResult.data;🤖 Prompt for AI Agents