Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,19 @@ AUTH_BEARER_TOKEN_VERCEL=
E2E_TEST_APPLE_CALENDAR_EMAIL=""
E2E_TEST_APPLE_CALENDAR_PASSWORD=""

# - APP CREDENTIAL SYNC ***********************************************************************************
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
# You can use: `openssl rand -base64 32` to generate one
CALCOM_WEBHOOK_SECRET=""
# This is the header name that will be used to verify the webhook secret. Should be in lowercase
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
# Key should match on Cal.com and your application
# must be 32 bytes for AES256 encryption algorithm
# You can use: `openssl rand -base64 24` to generate one
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=""

# - OIDC E2E TEST *******************************************************************************************

# Ensure this ADMIN EMAIL is present in the SAML_ADMINS list
Expand All @@ -243,4 +256,4 @@ E2E_TEST_OIDC_PROVIDER_DOMAIN=
E2E_TEST_OIDC_USER_EMAIL=
E2E_TEST_OIDC_USER_PASSWORD=

# ***********************************************************************************************************
# ***********************************************************************************************************
93 changes: 93 additions & 0 deletions apps/web/pages/api/webhook/app-credential.ts
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);
Comment on lines +17 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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
In @apps/web/pages/api/webhook/app-credential.ts around lines 17 - 31, The
handler currently allows any method and calls
appCredentialWebhookRequestBodySchema.parse directly which can throw and produce
a 500; update the exported handler to first reject non-POST methods (respond 405
with Allow: POST) and then wrap the call to
appCredentialWebhookRequestBodySchema.parse in a try/catch that returns a 400
with validation error details on parse failure; keep existing checks for
APP_CREDENTIAL_SHARING_ENABLED and the
CALCOM_WEBHOOK_HEADER_NAME/CALCOM_WEBHOOK_SECRET comparison, and reference the
same symbols (handler, APP_CREDENTIAL_SHARING_ENABLED,
CALCOM_WEBHOOK_HEADER_NAME, CALCOM_WEBHOOK_SECRET,
appCredentialWebhookRequestBodySchema.parse) when making the changes.


// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling for decryption and JSON parsing.

Both symmetricDecrypt and JSON.parse can throw exceptions. Unhandled failures would result in 500 errors and could potentially expose sensitive error details.

🔒 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Decrypt the keys
const keys = JSON.parse(
symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
);
// Decrypt the keys
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" });
}
🤖 Prompt for AI Agents
In @apps/web/pages/api/webhook/app-credential.ts around lines 56 - 59, Wrap the
decryption+parsing of reqBody.keys (the call to symmetricDecrypt and JSON.parse
that assigns keys) in a try-catch; on failure catch the error, log only a
sanitized message (no secret content) including the operation context (e.g.,
"app credential decrypt/parse failed"), and return an appropriate client error
response (400 or 422) instead of letting the exception bubble to a 500. Ensure
you still reference process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY when
calling symmetricDecrypt, and avoid returning or logging the decrypted payload
or raw error stack.


// 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
Expand Up @@ -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";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix incorrect import path for decodeOAuthState.

The import path ../oauth/decodeOAuthState is incorrect. Since both createOAuthAppCredential.ts and decodeOAuthState.ts are in the same _utils/oauth/ directory, the path should be ./decodeOAuthState.

The current path goes up to _utils/ and then back into oauth/, which is unnecessarily circuitous and may cause import resolution issues.

🔧 Proposed fix
-import { decodeOAuthState } from "../oauth/decodeOAuthState";
+import { decodeOAuthState } from "./decodeOAuthState";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { decodeOAuthState } from "../oauth/decodeOAuthState";
import { decodeOAuthState } from "./decodeOAuthState";
🤖 Prompt for AI Agents
In @packages/app-store/_utils/oauth/createOAuthAppCredential.ts at line 6, The
import for decodeOAuthState in createOAuthAppCredential.ts uses the wrong
relative path; update the import statement that references
"../oauth/decodeOAuthState" to use "./decodeOAuthState" so it imports the
decodeOAuthState function from the same _utils/oauth directory (look for the
import line in createOAuthAppCredential.ts and change the module specifier
accordingly).

import { throwIfNotHaveAdminAccessToTeam } from "../throwIfNotHaveAdminAccessToTeam";

/**
* This function is used to create app credentials for either a user or a team
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NextApiRequest } from "next";

import type { IntegrationOAuthCallbackState } from "../types";
import type { IntegrationOAuthCallbackState } from "../../types";

export function decodeOAuthState(req: NextApiRequest) {
if (typeof req.query.state !== "string") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NextApiRequest } from "next";

import type { IntegrationOAuthCallbackState } from "../types";
import type { IntegrationOAuthCallbackState } from "../../types";

export function encodeOAuthState(req: NextApiRequest) {
if (typeof req.query.state !== "string") {
Expand Down
32 changes: 32 additions & 0 deletions packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -20

Repository: 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 -100

Repository: 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 z.string().toString() which returns the literal string "[object Object]" in JavaScript. This creates a schema that expects properties literally named [object Object] instead of matching dynamic property names. Real OAuth token responses (e.g., {access_token: "xyz", expires_in: 3600}) will fail validation against this schema.

The intended behavior—accepting string access_token plus any additional properties—requires using .passthrough() or .catchall() instead. Replace the schema definition with:

 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 access_token (required) plus any additional properties, matching the pattern used elsewhere in the codebase.

🤖 Prompt for AI Agents
In @packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts around lines 5
- 11, The minimumTokenResponseSchema is invalid because using
z.string().toString() creates a literal property name; replace the object schema
with one that requires access_token and allows arbitrary additional
properties—e.g., define minimumTokenResponseSchema = z.object({ access_token:
z.string() }).passthrough() or use .catchall(z.unknown()) so dynamic fields like
expires_in are accepted; update any validation usage of
minimumTokenResponseSchema to work with the new passthrough/catchall behavior.


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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the file
find . -path "*packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts" -type f

Repository: 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 3

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 10852


🏁 Script executed:

cat -n packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts

Repository: 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 3

Repository: 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 -100

Repository: 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 -20

Repository: 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 -80

Repository: 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 5

Repository: 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 8

Repository: 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 -30

Repository: 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 -60

Repository: 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.ts

Repository: 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 -10

Repository: 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 -80

Repository: 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 -100

Repository: 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 refresh_token to the literal string "refresh_token" when missing from the response. This value gets stored in the database and reused in subsequent token refresh attempts, causing OAuth providers to reject it as an invalid token and breaking the refresh flow.

When APP_CREDENTIAL_SHARING_ENABLED is true, the minimumTokenResponseSchema does not require refresh_token from the response, but the code still assigns this dummy value unconditionally. This is problematic for integrations like Zoom that store and reuse this token value.

Consider one of these approaches:

  1. Make refresh_token optional in the schema instead of defaulting to a dummy value
  2. Throw an error if refresh_token is required but missing (non-shared credential mode)
  3. Conditionally handle it based on whether credential sharing is enabled, ensuring the dummy value isn't used downstream
🤖 Prompt for AI Agents
In @packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts around lines 25
- 27, The code unconditionally sets refreshTokenResponse.data.refresh_token =
"refresh_token" which writes a hardcoded dummy into the DB; instead, update
parseRefreshTokenResponse to stop injecting that literal and handle missing
refresh_token based on APP_CREDENTIAL_SHARING_ENABLED and the
minimumTokenResponseSchema: if credential sharing is disabled and refresh_token
is required, throw a clear error; if credential sharing is enabled, treat
refresh_token as optional (do not set a dummy) and ensure downstream code can
handle its absence; update any validation logic around
minimumTokenResponseSchema and the function refreshTokenResponse handling to
reflect the conditional behavior (refer to refreshTokenResponse,
minimumTokenResponseSchema, and APP_CREDENTIAL_SHARING_ENABLED).


return refreshTokenResponse;
};

export default parseRefreshTokenResponse;
22 changes: 22 additions & 0 deletions packages/app-store/_utils/oauth/refreshOAuthTokens.ts
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) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Improve type safety for refreshFunction parameter.

The refreshFunction parameter returns any, which bypasses TypeScript's type checking and could lead to runtime errors if the wrong type is returned.

♻️ 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const refreshOAuthTokens = async (refreshFunction: () => any, appSlug: string, userId: number | null) => {
const refreshOAuthTokens = async (refreshFunction: () => Promise<Response>, appSlug: string, userId: number | null): Promise<Response> => {

// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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:

  1. No error handling: Network failures or invalid responses will bubble up unhandled
  2. No timeout: The request could hang indefinitely on request threads
  3. No response validation: The function returns the response without checking response.ok, potentially returning error responses (4xx/5xx) that downstream code expects to be successful
🔧 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;
+    }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @packages/app-store/_utils/oauth/refreshOAuthTokens.ts around lines 5 - 15,
The fetch to process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT inside the
APP_CREDENTIAL_SHARING_ENABLED branch lacks error handling, timeout, and
response validation; wrap the request in a try/catch, use an AbortController
with a reasonable timeout to abort the fetch, await and check response.ok and
throw a descriptive error for non-2xx responses, parse the response JSON and
validate required fields (e.g., access token and expiry) before returning them
(instead of returning the raw Response), and ensure any thrown errors include
context (endpoint and userId/appSlug) so callers can handle failures from
refreshOAuthTokens safely.

} else {
const response = await refreshFunction();
return response;
}
};

export default refreshOAuthTokens;
2 changes: 1 addition & 1 deletion packages/app-store/googlecalendar/api/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";

import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";

import { encodeOAuthState } from "../../_utils/encodeOAuthState";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";

const scopes = [
"https://www.googleapis.com/auth/calendar.readonly",
Expand Down
2 changes: 1 addition & 1 deletion packages/app-store/googlecalendar/api/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { WEBAPP_URL_FOR_OAUTH, CAL_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";

import { decodeOAuthState } from "../../_utils/decodeOAuthState";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";

let client_id = "";
let client_secret = "";
Expand Down
13 changes: 11 additions & 2 deletions packages/app-store/googlecalendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find the refreshOAuthTokens function and understand its implementation
rg -n "refreshOAuthTokens" -A 15 -B 2 | head -100

Repository: 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 -120

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 5527


🏁 Script executed:

# Look for parseRefreshTokenResponse
rg -n "parseRefreshTokenResponse" -A 10 -B 2

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 11774


🏁 Script executed:

# Find minimumTokenResponseSchema definition
rg -n "minimumTokenResponseSchema" -B 5 -A 10

Repository: 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 -5

Repository: 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 -80

Repository: 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.ts

Repository: 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 res?.data exists, but refreshOAuthTokens returns incompatible response types:

  • Sync endpoint enabled: Returns a fetch Response object (no .data property)
  • Sync endpoint disabled: Returns the OAuth provider's response object with .data

When the sync endpoint is used, accessing res?.data returns undefined, causing token.access_token and token.expiry_date (lines 95-96) to fail with a TypeError.

The response must be handled differently based on which path is taken—either by calling .json() on Response objects or by checking the response type before accessing properties.

🤖 Prompt for AI Agents
In @packages/app-store/googlecalendar/lib/CalendarService.ts around lines 86 -
96, The code assumes refreshOAuthTokens always returns an object with a .data
field but when the sync endpoint is enabled it returns a fetch Response; update
the handling after calling refreshOAuthTokens (the res variable returned by
refreshOAuthTokens in this block) to detect whether res is a fetch Response or
an OAuth provider response, and extract token accordingly: if res has a .json
method (or instanceof Response) call await res.json() to get the payload,
otherwise use res.data; then assign googleCredentials.access_token and
googleCredentials.expiry_date from the extracted token object (referencing
refreshOAuthTokens, myGoogleAuth.refreshToken, googleCredentials, and
credential.userId to locate the code).

const key = googleCredentialSchema.parse(googleCredentials);
const key = parseRefreshTokenResponse(googleCredentials, googleCredentialSchema);
await prisma.credential.update({
where: { id: credential.id },
data: { key },
Expand Down
2 changes: 1 addition & 1 deletion packages/app-store/hubspot/api/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { NextApiRequest, NextApiResponse } from "next";

import { WEBAPP_URL } from "@calcom/lib/constants";

import { encodeOAuthState } from "../../_utils/encodeOAuthState";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";

const scopes = ["crm.objects.contacts.read", "crm.objects.contacts.write"];

Expand Down
4 changes: 2 additions & 2 deletions packages/app-store/hubspot/api/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";

import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";

let client_id = "";
let client_secret = "";
Expand Down
20 changes: 13 additions & 7 deletions packages/app-store/hubspot/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find the CalendarService.ts file and check the imports
find . -name "CalendarService.ts" -type f

Repository: 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 5

Repository: 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 10

Repository: 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 15

Repository: 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.ts

Repository: 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 15

Repository: 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.ts

Repository: 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.ts

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 1099


🏁 Script executed:

# Find TokenResponseIF definition
rg -n "type TokenResponseIF|interface TokenResponseIF" -A 20

Repository: 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 -50

Repository: 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.ts

Repository: 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.ts

Repository: AI-Code-Review-Evals/coderabbit-cal_dot_com

Length of output: 693


Critical: refreshOAuthTokens returns inconsistent types that will cause runtime errors in HubSpot integration.

The function has two code paths with different return types:

  • When the shared credential endpoint is enabled: returns Response object from fetch()
  • Otherwise: returns the raw result from refreshFunction()

At line 192, the HubSpot code directly accesses hubspotRefreshToken.expiresIn expecting a token object. If the credential sync endpoint is enabled, refreshOAuthTokens returns a Response object instead, causing a runtime error when trying to access .expiresIn (undefined).

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
In @packages/app-store/hubspot/lib/CalendarService.ts around lines 177 - 189,
The refreshOAuthTokens implementation returns inconsistent types (fetch Response
vs raw token) causing callers like the HubSpot flow to crash when accessing
fields like expiresIn; update refreshOAuthTokens so both code paths parse the
fetch Response (await response.json()) and normalize/validate the resulting
object into a consistent token shape (include expiresIn,
accessToken/refreshToken names matching callers) before returning, and ensure
any error responses are turned into thrown errors or a consistent error object
so callers like the hubspotRefreshToken usage can safely read properties.


// set expiry date as offset from current time.
Expand Down
2 changes: 1 addition & 1 deletion packages/app-store/larkcalendar/api/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { z } from "zod";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";

import { encodeOAuthState } from "../../_utils/encodeOAuthState";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
import { LARK_HOST } from "../common";

const larkKeysSchema = z.object({
Expand Down
2 changes: 1 addition & 1 deletion packages/app-store/larkcalendar/api/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import logger from "@calcom/lib/logger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";

import { decodeOAuthState } from "../../_utils/decodeOAuthState";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
import { LARK_HOST } from "../common";
import { getAppAccessToken } from "../lib/AppAccessToken";
import type { LarkAuthCredentials } from "../types/LarkCalendar";
Expand Down
28 changes: 17 additions & 11 deletions packages/app-store/larkcalendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
} from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";

import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens";
import { handleLarkError, isExpired, LARK_HOST } from "../common";
import type {
CreateAttendeesResp,
Expand Down Expand Up @@ -63,17 +64,22 @@ export default class LarkCalendarService implements Calendar {
}
try {
const appAccessToken = await getAppAccessToken();
const resp = await fetch(`${this.url}/authen/v1/refresh_access_token`, {
method: "POST",
headers: {
Authorization: `Bearer ${appAccessToken}`,
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
}),
});
const resp = await refreshOAuthTokens(
async () =>
await fetch(`${this.url}/authen/v1/refresh_access_token`, {
method: "POST",
headers: {
Authorization: `Bearer ${appAccessToken}`,
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
}),
}),
"lark-calendar",
credential.userId
);

const data = await handleLarkError<RefreshTokenResp>(resp, this.log);
this.log.debug(
Expand Down
2 changes: 1 addition & 1 deletion packages/app-store/office365calendar/api/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { stringify } from "querystring";

import { WEBAPP_URL } from "@calcom/lib/constants";

import { encodeOAuthState } from "../../_utils/encodeOAuthState";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";

const scopes = ["User.Read", "Calendars.Read", "Calendars.ReadWrite", "offline_access"];

Expand Down
2 changes: 1 addition & 1 deletion packages/app-store/office365calendar/api/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";

import { decodeOAuthState } from "../../_utils/decodeOAuthState";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";

const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"];

Expand Down
Loading