Skip to content

Automated Test: oauth-security-enhanced #371

Closed

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 4, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

  • New Features
    • Introduced credential sharing and synchronization capabilities via webhook integration, enabling centralized OAuth token management across calendar and video integrations.
    • Enhanced OAuth token refresh handling for improved stability and credential sync support.

…11059)

* Add credential sync .env variables

* Add webhook to send app credentials

* Upsert credentials when webhook called

* Refresh oauth token from a specific endpoint

* Pass appSlug

* Add credential encryption

* Move oauth helps into a folder

* Create parse token response wrapper

* Add OAuth helpers to apps

* Clean up

* Refactor `appDirName` to `appSlug`

* Address feedback

* Change to safe parse

* Remove console.log

---------

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This PR implements OAuth credential sharing infrastructure by introducing environment configuration, a webhook endpoint for credential synchronization, reorganizing OAuth utilities into a dedicated directory, creating centralized token refresh and parsing helpers, and updating token refresh flows across multiple integrations.

Changes

Cohort / File(s) Summary
Environment and Configuration Setup
.env.example, turbo.json, packages/lib/constants.ts
Added new environment variables for webhook secret, header name, credential sync endpoint, and encryption key. Introduced APP_CREDENTIAL_SHARING_ENABLED feature flag constant derived from environment variables.
New Webhook Credential Sync Endpoint
apps/web/pages/api/webhook/app-credential.ts
Added new webhook handler that validates feature flag, verifies webhook secret header, parses request schema, confirms user and app existence, decrypts keys, and upserts credentials with error handling for various failure scenarios.
OAuth Utilities Directory Reorganization
packages/app-store/_utils/oauth/*
Moved OAuth utilities (createOAuthAppCredential, decodeOAuthState, encodeOAuthState) into dedicated oauth subdirectory. Updated import paths across all dependent files to reference new oauth subdirectory location.
New OAuth Helper Utilities
packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts, packages/app-store/_utils/oauth/refreshOAuthTokens.ts
Added parseRefreshTokenResponse for environment-aware token validation with minimum schema fallback and default refresh_token handling. Added refreshOAuthTokens to delegate token refreshing to credential sync endpoint when enabled, or fallback to provided refresh function.
Calendar Service Token Refresh Integration
packages/app-store/.../lib/CalendarService.ts (Google, Lark, Office 365, Salesforce, Zoho CRM)
Updated token refresh flows to use refreshOAuthTokens wrapper and parseRefreshTokenResponse for token parsing, replacing direct API calls with centralized OAuth handling.
Video Adapter Token Refresh Integration
packages/app-store/.../lib/VideoApiAdapter.ts (Office 365 Video, Webex, Zoom), packages/app-store/hubspot/lib/CalendarService.ts
Replaced direct token endpoint calls with refreshOAuthTokens wrapper and updated token response parsing with parseRefreshTokenResponse for standardized credential handling.
OAuth Callback Handler Path Updates (encodeOAuthState)
packages/app-store/.../api/add.ts (Google Calendar, Lark Calendar, HubSpot, Office 365, Office 365 Video, Salesforce, Webex, Zoho Bigin, Zoho CRM, Zoom Video)
Updated import paths for encodeOAuthState from generic utils to oauth subdirectory.
OAuth Callback Handler Path Updates (decodeOAuthState)
packages/app-store/.../api/callback.ts (Google Calendar, Lark Calendar, Office 365, Salesforce, Zoho Bigin, Zoho CRM)
Updated import paths for decodeOAuthState from generic utils to oauth subdirectory.
OAuth Callback Handler Path Updates (createOAuthAppCredential)
packages/app-store/.../api/callback.ts (HubSpot, Office 365 Video, Stripe, Tandem Video, Webex, Zoho Bigin, Zoho CRM, Zoom Video)
Updated import paths for createOAuthAppCredential from generic utils to oauth subdirectory.

Sequence Diagram

sequenceDiagram
    participant External as External System
    participant Webhook as Webhook Handler
    participant DB as Database
    participant Crypto as Encryption Service
    
    External->>Webhook: POST /api/webhook/app-credential<br/>(with CALCOM_WEBHOOK_SECRET header)
    Webhook->>Webhook: Validate feature flag<br/>(APP_CREDENTIAL_SHARING_ENABLED)
    Webhook->>Webhook: Verify webhook secret header
    Webhook->>Webhook: Parse request body schema<br/>(userId, appSlug, keys)
    Webhook->>DB: Verify user exists
    Webhook->>DB: Verify app exists
    Webhook->>Crypto: Decrypt provided keys
    Webhook->>DB: Check for existing<br/>user+app credential
    alt Credential exists
        Webhook->>DB: Update existing credential<br/>with decrypted keys
    else Credential not found
        Webhook->>DB: Create new credential<br/>with userId, appId, keys
    end
    DB-->>Webhook: Success
    Webhook-->>External: 200 OK / 404 / 403<br/>(with descriptive message)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Poem

🐰 Through OAuth's winding paths so vast and wide,
We bundle tokens in a secure tide,
With webhooks whispering credentials divine,
Each provider's refresh now aligned,
Sharing credentials in harmony fine! 🌟

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is vague and generic, using 'Automated Test' which does not convey specific information about the actual changes made. Revise the title to clearly describe the main change. For example: 'Add OAuth credential synchronization webhook and security enhancements' or 'Implement centralized OAuth token refresh with credential sharing feature flag'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch oauth-security-enhanced

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 4, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/app-store/salesforce/lib/CalendarService.ts (1)

90-108: ⚠️ Potential issue | 🔴 Critical

Use refreshed tokens when creating the jsforce connection.

The connection is created with stale tokens from the original credential (credentialKey) instead of using the newly refreshed tokens from the API response. Although the refreshed tokens are correctly saved to the database (line 96-99), the jsforce Connection instantiated on line 101-108 uses the expired access_token and old instance_url, so the refresh won't apply to the current request.

🛠️ Proposed fix
     await prisma.credential.update({
       where: { id: credential.id },
       data: { key: { ...accessTokenParsed.data, refresh_token: credentialKey.refresh_token } },
     });
 
-    return new jsforce.Connection({
+    const refreshedKey = accessTokenParsed.data;
+    return new jsforce.Connection({
       clientId: consumer_key,
       clientSecret: consumer_secret,
       redirectUri: WEBAPP_URL + "/api/integrations/salesforce/callback",
-      instanceUrl: credentialKey.instance_url,
-      accessToken: credentialKey.access_token,
-      refreshToken: credentialKey.refresh_token,
+      instanceUrl: refreshedKey.instance_url ?? credentialKey.instance_url,
+      accessToken: refreshedKey.access_token,
+      refreshToken: refreshedKey.refresh_token ?? credentialKey.refresh_token,
     });
packages/app-store/googlecalendar/lib/CalendarService.ts (1)

95-101: ⚠️ Potential issue | 🟠 Major

Persist the parsed token payload, not the SafeParse wrapper.

parseRefreshTokenResponse returns a Zod SafeParse result with structure { success, data, error }. Storing it directly persists the wrapper object instead of the credential fields, corrupting the credential shape and breaking downstream token usage.

🛠️ Proposed fix
-        const key = parseRefreshTokenResponse(googleCredentials, googleCredentialSchema);
+        const key = parseRefreshTokenResponse(googleCredentials, googleCredentialSchema).data;
🤖 Fix all issues with AI agents
In @.env.example:
- Around line 233-244: The dotenv-linter warnings come from quoted empty values
and key ordering; update the CALCOM_* block by removing the surrounding double
quotes for the empty values (e.g., CALCOM_WEBHOOK_SECRET=,
CALCOM_WEBHOOK_HEADER_NAME=calcom-webhook-secret,
CALCOM_CREDENTIAL_SYNC_ENDPOINT=, CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=) and
reorder the keys into the expected alphabetical order
(CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY, CALCOM_CREDENTIAL_SYNC_ENDPOINT,
CALCOM_WEBHOOK_HEADER_NAME, CALCOM_WEBHOOK_SECRET) so QuoteCharacter and
UnorderedKey warnings are resolved.

In `@apps/web/pages/api/webhook/app-credential.ts`:
- Around line 56-59: The code currently calls symmetricDecrypt and JSON.parse
without validating the encryption key or catching errors; update the handler to
first verify process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY is present and
non-empty, then wrap the symmetricDecrypt(reqBody.keys, ...) and JSON.parse(...)
sequence in a try/catch; on failure log the error (including the exception
message) and return a client error (e.g., 400/422) rather than letting an
unhandled 500 propagate. Reference symmetricDecrypt, JSON.parse, reqBody.keys
and CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY when making the checks and error
handling.
- Around line 17-32: The handler currently lacks an HTTP method guard and uses
parse() which throws on invalid JSON/body; update the exported handler to
early-return 405 for non-POST methods (check req.method === 'POST') and replace
appCredentialWebhookRequestBodySchema.parse(req.body) with safeParse and return
a 400 with validation errors when safeParse fails; additionally wrap any
explicit JSON.parse usage in the handler (the JSON.parse call you added) in a
try-catch and return a 400 with a clear message on parse errors instead of
letting exceptions bubble.

In `@packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts`:
- Around line 25-27: In parseRefreshTokenResponse, stop assigning the literal
placeholder "refresh_token" to refreshTokenResponse.data.refresh_token; this
corrupts stored credentials. Instead, remove that assignment so the field
remains undefined when absent, or if a fallback is available use a
caller-supplied existing stored credential value (pass it into
parseRefreshTokenResponse and apply it only when present). Update references to
refreshTokenResponse.data.refresh_token accordingly so callers handle the
optional/absent case rather than relying on a hardcoded string.
- Around line 5-11: minimumTokenResponseSchema currently uses invalid computed
keys (evaluating to "[object Object]") and should be replaced with a proper Zod
schema that explicitly validates known OAuth fields and permits other
properties; update minimumTokenResponseSchema to require access_token:
z.string(), expires_in: z.number().optional() (and token_type:
z.string().optional() if desired) and allow unknown keys via .passthrough() or
.catchall(z.unknown()) so real token responses are accepted. Also remove the
placeholder assignment that sets the refresh token to the literal string
"refresh_token" when missing (the code that assigns that fallback in
parseRefreshTokenResponse); instead return or throw an error / handle the
missing refresh token explicitly so no invalid token value persists.

In `@packages/app-store/salesforce/lib/CalendarService.ts`:
- Around line 96-99: The file uses the Prisma client (await
prisma.credential.update(...)) but never imports it; add an import for the
Prisma client at the top of CalendarService.ts matching how other calendar
services import it (the module that exports the shared prisma instance used by
office365calendar/zohocrm), e.g. import { prisma } from the shared Prisma client
module, and ensure the imported symbol name is prisma so the existing call to
prisma.credential.update resolves.
- Around line 75-87: In CalendarService.ts replace the current response check
that uses response.statusText with a check using response.ok (e.g., after the
fetch that sets const response) so successful HTTP/2 or non-200 2xx responses
are treated as success; if !response.ok, throw an HttpError that incorporates
response.status and response.statusText (or response.statusText fallback) to
preserve useful diagnostics while relying on response.ok for correctness.

In `@packages/app-store/zoho-bigin/lib/CalendarService.ts`:
- Around line 85-94: Call refreshOAuthTokens with the Cal.com user id
(credential.userId) instead of credentialId and normalize the returned token
payload so both AxiosResponse and fetch Response work: pass credential.userId
into the refreshOAuthTokens call, then after the call extract the token data by
checking for tokenInfo.data first, and if absent await tokenInfo.json() (and
handle a null/undefined result) before using the tokens; reference the
refreshOAuthTokens call site and the tokenInfo variable when making these
changes.

In `@packages/lib/constants.ts`:
- Around line 103-104: APP_CREDENTIAL_SHARING_ENABLED currently evaluates to the
raw encryption key string when both env vars exist; change it to return a
boolean instead by coercing the check for process.env.CALCOM_WEBHOOK_SECRET and
process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY (e.g., use Boolean(...) or
double-bang) so APP_CREDENTIAL_SHARING_ENABLED is true/false and never exposes
secret values.

Comment on lines +233 to +244
# - 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=""

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix dotenv-linter warnings for the new CALCOM_ entries.*

The linter flags QuoteCharacter and UnorderedKey for this block; reordering and removing quotes resolves it.

🛠️ Proposed fix
-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=""
+# 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=
+CALCOM_CREDENTIAL_SYNC_ENDPOINT=
+# 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_WEBHOOK_SECRET=
📝 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
# - 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=""
# - 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
# 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=
CALCOM_CREDENTIAL_SYNC_ENDPOINT=
# 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_WEBHOOK_SECRET=
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 237-237: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 239-239: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 239-239: [UnorderedKey] The CALCOM_WEBHOOK_HEADER_NAME key should go before the CALCOM_WEBHOOK_SECRET key

(UnorderedKey)


[warning] 240-240: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 240-240: [UnorderedKey] The CALCOM_CREDENTIAL_SYNC_ENDPOINT key should go before the CALCOM_WEBHOOK_HEADER_NAME key

(UnorderedKey)


[warning] 244-244: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 244-244: [UnorderedKey] The CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY key should go before the CALCOM_CREDENTIAL_SYNC_ENDPOINT key

(UnorderedKey)

🤖 Prompt for AI Agents
In @.env.example around lines 233 - 244, The dotenv-linter warnings come from
quoted empty values and key ordering; update the CALCOM_* block by removing the
surrounding double quotes for the empty values (e.g., CALCOM_WEBHOOK_SECRET=,
CALCOM_WEBHOOK_HEADER_NAME=calcom-webhook-secret,
CALCOM_CREDENTIAL_SYNC_ENDPOINT=, CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY=) and
reorder the keys into the expected alphabetical order
(CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY, CALCOM_CREDENTIAL_SYNC_ENDPOINT,
CALCOM_WEBHOOK_HEADER_NAME, CALCOM_WEBHOOK_SECRET) so QuoteCharacter and
UnorderedKey warnings are resolved.

Comment on lines +17 to +32
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);

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/pages/api/webhook/app-credential.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 3523


🏁 Script executed:

# Check for other webhook handlers to see if method guards are common
find apps/web/pages/api/webhook -name "*.ts" -type f | head -10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 134


🏁 Script executed:

# Look at a few other webhook handlers to see patterns
head -35 apps/web/pages/api/webhook/*.ts | grep -E "(method|POST|safeParse|parse)" | head -20

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 162


🏁 Script executed:

# Check if there's a pattern of using safeParse vs parse in the codebase
rg "\.safeParse|\.parse\(" apps/web/pages/api --type ts -A 2 | head -40

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2696


🏁 Script executed:

# Check if there's a global error handler or middleware in the API routes
find apps/web/pages/api -name "_*.ts" -o -name "middleware.ts" | xargs cat 2>/dev/null | head -50

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

# Verify Zod parse behavior - check if parse throws
rg "parse\(" apps/web/pages/api --type ts -B 2 -A 3 | grep -E "(try|catch|parse)" | head -30

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2955


🏁 Script executed:

# Check if other webhook handlers have method guards
fd "webhook" apps/web/pages/api --type f -name "*.ts" -exec grep -l "req.method" {} \;

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 320


🏁 Script executed:

# Check if webhook handlers have method guards
grep -r "req.method" apps/web/pages/api/webhook/ --include="*.ts"

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

# Check the actual error handling in Next.js API routes
# Look at recorded-daily-video.ts which uses safeParse
cat -n apps/web/pages/api/recorded-daily-video.ts | head -50

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2222


🏁 Script executed:

# Verify JSON.parse error handling pattern
rg "JSON\.parse" apps/web/pages/api --type ts -B 2 -A 2 | head -50

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2559


Add method guard and handle validation errors without throwing.

Invalid request bodies or JSON parsing failures will throw unhandled exceptions and surface as 500s. Add a method guard and use safeParse to ensure webhook handlers return deterministic error responses.

🛠️ Proposed fix
 export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+  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" });
   }
@@
-  const reqBody = appCredentialWebhookRequestBodySchema.parse(req.body);
+  const parsed = appCredentialWebhookRequestBodySchema.safeParse(req.body);
+  if (!parsed.success) {
+    return res.status(400).json({ message: "Invalid request body" });
+  }
+  const reqBody = parsed.data;
   
   // Check that the user exists
   const user = await prisma.user.findUnique({ where: { id: reqBody.userId } });

Also wrap the JSON.parse on line 57-59 in a try-catch:

-  const keys = JSON.parse(
-    symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
-  );
+  let keys;
+  try {
+    keys = JSON.parse(
+      symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
+    );
+  } catch {
+    return res.status(400).json({ message: "Failed to decrypt credentials" });
+  }
🤖 Prompt for AI Agents
In `@apps/web/pages/api/webhook/app-credential.ts` around lines 17 - 32, The
handler currently lacks an HTTP method guard and uses parse() which throws on
invalid JSON/body; update the exported handler to early-return 405 for non-POST
methods (check req.method === 'POST') and replace
appCredentialWebhookRequestBodySchema.parse(req.body) with safeParse and return
a 400 with validation errors when safeParse fails; additionally wrap any
explicit JSON.parse usage in the handler (the JSON.parse call you added) in a
try-catch and return a 400 with a clear message on parse errors instead of
letting exceptions bubble.

Comment on lines +56 to +59
// Decrypt the keys
const keys = JSON.parse(
symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
);

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/pages/api/webhook/app-credential.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 3523


Add error handling for decryption and validate the encryption key.

symmetricDecrypt and JSON.parse can throw on invalid payloads or missing configuration, causing unhandled 500 errors. The encryption key must be explicitly validated before use.

🛠️ Proposed fix
-  const keys = JSON.parse(
-    symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
-  );
+  const encryptionKey = process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY;
+  if (!encryptionKey) {
+    return res.status(500).json({ message: "Encryption key not configured" });
+  }
+  let keys: Record<string, unknown>;
+  try {
+    keys = JSON.parse(symmetricDecrypt(reqBody.keys, encryptionKey));
+  } catch {
+    return res.status(400).json({ message: "Invalid encrypted keys payload" });
+  }
📝 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
const encryptionKey = process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY;
if (!encryptionKey) {
return res.status(500).json({ message: "Encryption key not configured" });
}
let keys: Record<string, unknown>;
try {
keys = JSON.parse(symmetricDecrypt(reqBody.keys, encryptionKey));
} catch {
return res.status(400).json({ message: "Invalid encrypted keys payload" });
}
🤖 Prompt for AI Agents
In `@apps/web/pages/api/webhook/app-credential.ts` around lines 56 - 59, The code
currently calls symmetricDecrypt and JSON.parse without validating the
encryption key or catching errors; update the handler to first verify
process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY is present and non-empty, then
wrap the symmetricDecrypt(reqBody.keys, ...) and JSON.parse(...) sequence in a
try/catch; on failure log the error (including the exception message) and return
a client error (e.g., 400/422) rather than letting an unhandled 500 propagate.
Reference symmetricDecrypt, JSON.parse, reqBody.keys and
CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY when making the checks and error handling.

Comment on lines +5 to +11
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(),
});

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:

find . -type f -name "parseRefreshTokenResponse.ts" | head -20

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 152


🏁 Script executed:

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

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1344


🏁 Script executed:

rg -A 5 -B 5 "minimumTokenResponseSchema" --type ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2138


🏁 Script executed:

# Let's check what z.string().toString() actually returns
# First, let's look for any tests related to this schema
find . -type f -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "minimumTokenResponseSchema\|parseRefreshTokenResponse" 2>/dev/null | head -5

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

# Let's understand what z.string().toString() returns
# Check if there are any similar patterns in the codebase
rg "z\.string\(\)\.toString\(\)" --type ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 189


🏁 Script executed:

cd packages/app-store/_utils/oauth && node -e "
const { z } = require('zod');
console.log('z.string().toString():', z.string().toString());
console.log('z.string().optional().toString():', z.string().optional().toString());

// Test the schema
const minimumTokenResponseSchema = z.object({
  access_token: z.string(),
  [z.string().toString()]: z.number(),
  [z.string().optional().toString()]: z.unknown().optional(),
});

console.log('\nSchema keys:', Object.keys(minimumTokenResponseSchema.shape));

// Test with a real token response
const testResponse = {
  access_token: 'token123',
  expires_in: 3600,
  refresh_token: 'refresh123'
};

console.log('\nTesting with real response:', testResponse);
const result = minimumTokenResponseSchema.safeParse(testResponse);
console.log('Parse result:', result);
"
</parameter>

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1092


🏁 Script executed:

# Let me check what toString() returns for Zod objects
# First, let's look at the package.json to understand dependencies
cat packages/package.json | head -30

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 143


🏁 Script executed:

# Find package.json files
find . -name "package.json" -type f | head -5

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 218


🏁 Script executed:

# Let me search for Zod documentation or examples in the codebase
rg "z\.object" --type ts -A 3 | head -50

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 3415


🏁 Script executed:

# Let me search for how token responses look in the codebase
rg "access_token\|refresh_token\|expires_in" --type ts -B 2 -A 2 | head -80

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

# Let's verify what z.string().toString() actually produces
python3 << 'EOF'
# Simulate what happens with z.string().toString()
# In JavaScript, calling toString() on an object typically returns something like "[object Object]"

class MockZodSchema:
    def __str__(self):
        return "[object Object]"
    
    def __repr__(self):
        return "[object Object]"

mock_schema = MockZodSchema()
print("What z.string().toString() likely returns:", str(mock_schema))

# Now let's think about what the schema would look like:
schema_keys = {
    "access_token": "z.string()",
    "[object Object]": "z.number()",  # This is the computed key from z.string().toString()
    "[object Object]": "z.unknown().optional()"  # This would overwrite the previous
}

print("\nSchema keys after evaluation:", list(schema_keys.keys()))
print("This means the schema expects these literal property names:")
print("1. 'access_token'")
print("2. '[object Object]'")

# A real token response would have:
real_token = {
    "access_token": "token123",
    "expires_in": 3600,
    "token_type": "Bearer"
}

print("\nReal token response keys:", list(real_token.keys()))
print("\nWould schema match real token?", set(real_token.keys()) == set(schema_keys.keys()))
EOF

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 424


🏁 Script executed:

# Let's check if there are any tests or usages that show what the schema is supposed to do
rg "APP_CREDENTIAL_SHARING_ENABLED" --type ts -B 3 -A 3

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 3992


minimumTokenResponseSchema uses invalid computed keys that won't match real token responses.

The expressions [z.string().toString()] and [z.string().optional().toString()] evaluate to literal property names "[object Object]" at runtime, so the schema will reject all valid OAuth token responses. Real responses have properties like access_token, expires_in, and token_type, which don't match these literal keys.

Additionally, line 26 assigns the placeholder string "refresh_token" when the refresh token is missing, which creates an invalid token that persists through the system.

🛠️ Proposed fix
-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(),
-});
+const minimumTokenResponseSchema = z
+  .object({ access_token: z.string() })
+  .catchall(z.unknown());

And remove the placeholder fallback or return an error instead:

  if (!refreshTokenResponse.data.refresh_token) {
-   refreshTokenResponse.data.refresh_token = "refresh_token";
+   throw new Error("Missing refresh_token in response");
  }
🤖 Prompt for AI Agents
In `@packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts` around lines 5
- 11, minimumTokenResponseSchema currently uses invalid computed keys
(evaluating to "[object Object]") and should be replaced with a proper Zod
schema that explicitly validates known OAuth fields and permits other
properties; update minimumTokenResponseSchema to require access_token:
z.string(), expires_in: z.number().optional() (and token_type:
z.string().optional() if desired) and allow unknown keys via .passthrough() or
.catchall(z.unknown()) so real token responses are accepted. Also remove the
placeholder assignment that sets the refresh token to the literal string
"refresh_token" when missing (the code that assigns that fallback in
parseRefreshTokenResponse); instead return or throw an error / handle the
missing refresh token explicitly so no invalid token value persists.

Comment on lines +25 to +27
if (!refreshTokenResponse.data.refresh_token) {
refreshTokenResponse.data.refresh_token = "refresh_token";
}

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and locate the file
fd "parseRefreshTokenResponse" --type f

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 150


🏁 Script executed:

# Read the file to see the actual code
cat -n packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1344


🏁 Script executed:

# Search for usages of this function to understand the impact
rg "parseRefreshTokenResponse" --type ts --type tsx -B 2 -A 2

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 117


🏁 Script executed:

# Search for usages of parseRefreshTokenResponse without file type restriction
rg "parseRefreshTokenResponse" -B 2 -A 2

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 5206


🏁 Script executed:

# Check if refresh_token is used in other places and how
rg "refresh_token" packages/app-store/_utils/oauth/ -B 1 -A 1

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 451


🏁 Script executed:

# Search for where the return value of parseRefreshTokenResponse is used
rg "parseRefreshTokenResponse" --type ts -l

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 372


🏁 Script executed:

# Let's examine how the parsed token is used in the calling code
# Look at the office365calendar usage since it showed the usage pattern
sed -n '1,80p' packages/app-store/office365calendar/lib/CalendarService.ts | head -50

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1523


🏁 Script executed:

# Get more context around the parseRefreshTokenResponse call in office365calendar
rg -A 10 "parseRefreshTokenResponse\(responseJson" packages/app-store/office365calendar/lib/CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 508


🏁 Script executed:

# Check what happens with the parsed token - specifically the refresh_token field
rg -A 20 "const tokenResponse = parseRefreshTokenResponse" packages/app-store/office365calendar/lib/CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 806


🏁 Script executed:

# Let's also check the zoom usage to see the pattern
rg -A 15 "const parsedToken = parseRefreshTokenResponse" packages/app-store/zoomvideo/lib/VideoApiAdapter.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 907


🏁 Script executed:

# Let's check the schema definitions to see if refresh_token is expected
rg "refreshTokenResponseSchema\|zoomRefreshedTokenSchema\|salesforceTokenSchema" packages/app-store/ -B 2 -A 5

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

# Check if refresh_token is defined in minimumTokenResponseSchema
cat -n packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1344


🏁 Script executed:

# Look for where these schemas are defined
find packages/app-store -name "*.ts" -type f | xargs grep -l "refreshTokenResponseSchema = \|zoomRefreshedTokenSchema = " | head -5

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 201


🏁 Script executed:

# Check office365 schema definition
rg "refreshTokenResponseSchema" packages/app-store/office365calendar/ -B 5 -A 10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 4348


🏁 Script executed:

# Check the zoom and google schema definitions as well
rg "zoomRefreshedTokenSchema\|googleCredentialSchema\|salesforceTokenSchema" packages/app-store/ -B 2 -A 10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

# Let's trace through a specific case - how is the fake token used if refresh_token is needed?
# Check if refresh_token is actually used after being set
rg "refresh_token" packages/app-store/office365calendar/ -B 2 -A 2

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1599


🏁 Script executed:

# Check what happens in the zoom case with the fake token
rg "refresh_token" packages/app-store/zoomvideo/ -B 2 -A 2

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2271


🏁 Script executed:

# Verify the context around where the fake token is used - check the refresh flow
rg -B 5 -A 5 "grant_type.*refresh_token" packages/app-store/office365calendar/lib/CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 523


Do not set missing refresh_token to a placeholder string.

This corrupts credentials stored in the database. The literal "refresh_token" string will be sent to OAuth providers (Microsoft, Zoom, etc.) on the next token refresh attempt, causing the request to fail with an invalid grant error and breaking the refresh flow.

Either use a caller-supplied fallback (from an existing stored credential) or leave refresh_token unset—schemas already mark it as optional where appropriate.

🛠️ Proposed fix
-const parseRefreshTokenResponse = (response: any, schema: z.ZodTypeAny) => {
+const parseRefreshTokenResponse = (
+  response: any,
+  schema: z.ZodTypeAny,
+  opts?: { fallbackRefreshToken?: string }
+) => {
@@
-  if (!refreshTokenResponse.data.refresh_token) {
-    refreshTokenResponse.data.refresh_token = "refresh_token";
-  }
+  if (!refreshTokenResponse.data.refresh_token && opts?.fallbackRefreshToken) {
+    refreshTokenResponse.data.refresh_token = opts.fallbackRefreshToken;
+  }
🤖 Prompt for AI Agents
In `@packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts` around lines 25
- 27, In parseRefreshTokenResponse, stop assigning the literal placeholder
"refresh_token" to refreshTokenResponse.data.refresh_token; this corrupts stored
credentials. Instead, remove that assignment so the field remains undefined when
absent, or if a fallback is available use a caller-supplied existing stored
credential value (pass it into parseRefreshTokenResponse and apply it only when
present). Update references to refreshTokenResponse.data.refresh_token
accordingly so callers handle the optional/absent case rather than relying on a
hardcoded string.

Comment on lines +75 to +87
const response = await fetch("https://login.salesforce.com/services/oauth2/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: consumer_key,
client_secret: consumer_secret,
refresh_token: credentialKey.refresh_token,
format: "json",
}),
});

if (response.statusText !== "OK") throw new HttpError({ statusCode: 400, message: response.statusText });

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n packages/app-store/salesforce/lib/CalendarService.ts | sed -n '70,90p'

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1048


🌐 Web query:

Fetch API response.statusText different status codes behavior HTTP 200 201 202

💡 Result:

In the Fetch API, response.statusText is the HTTP “status message” / reason phrase associated with response.status (e.g., "OK" for 200). However, it’s not reliable across protocols and environments. [1][2]

Typical values (when a reason phrase is available, e.g., HTTP/1.1)

  • 200"OK" [3]
  • 201"Created" [4]
  • 202"Accepted" [5]

Why you may see "" (empty string)

  • The Fetch standard defines a response’s status message default as empty, and statusText returns that status message. [2]
  • HTTP/2 does not support reason phrases, so responses over HTTP/2 will have an empty status message (so response.statusText === "" even if response.status is 200/201/202). [1][2]

Practical guidance

Use response.status (number) and/or response.ok instead of statusText for logic; treat statusText as best-effort/debug-only. [2][1]

Sources: [1] MDN Response.statusText • [2] Fetch Standard • [3] MDN 200 OK • [4] MDN 201 Created • [5] MDN 202 Accepted


Use response.ok instead of statusText for error detection.

The statusText value is unreliable across protocols: HTTP/2 does not include reason phrases (resulting in empty string), and other 2xx responses have different phrases (e.g., "Created" for 201). The current code will incorrectly throw errors for successful responses over HTTP/2 or non-200 success codes.

🛠️ Proposed fix
-    if (response.statusText !== "OK") throw new HttpError({ statusCode: 400, message: response.statusText });
+    if (!response.ok) {
+      const errorText = await response.text();
+      throw new HttpError({ statusCode: response.status, message: errorText || response.statusText });
+    }
📝 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 response = await fetch("https://login.salesforce.com/services/oauth2/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: consumer_key,
client_secret: consumer_secret,
refresh_token: credentialKey.refresh_token,
format: "json",
}),
});
if (response.statusText !== "OK") throw new HttpError({ statusCode: 400, message: response.statusText });
const response = await fetch("https://login.salesforce.com/services/oauth2/token", {
method: "POST",
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: consumer_key,
client_secret: consumer_secret,
refresh_token: credentialKey.refresh_token,
format: "json",
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new HttpError({ statusCode: response.status, message: errorText || response.statusText });
}
🤖 Prompt for AI Agents
In `@packages/app-store/salesforce/lib/CalendarService.ts` around lines 75 - 87,
In CalendarService.ts replace the current response check that uses
response.statusText with a check using response.ok (e.g., after the fetch that
sets const response) so successful HTTP/2 or non-200 2xx responses are treated
as success; if !response.ok, throw an HttpError that incorporates
response.status and response.statusText (or response.statusText fallback) to
preserve useful diagnostics while relying on response.ok for correctness.

Comment on lines +96 to +99
await prisma.credential.update({
where: { id: credential.id },
data: { key: { ...accessTokenParsed.data, refresh_token: credentialKey.refresh_token } },
});

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:

cd packages/app-store/salesforce/lib && wc -l CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 112


🏁 Script executed:

cd packages/app-store/salesforce/lib && head -30 CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 940


🏁 Script executed:

cd packages/app-store/salesforce/lib && rg "import.*prisma" CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

cd packages/app-store/salesforce/lib && sed -n '90,105p' CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 715


🏁 Script executed:

cd packages/app-store/salesforce/lib && rg "prisma" CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 126


🏁 Script executed:

cd packages/app-store/salesforce/lib && grep -n "import.*from.*prisma" CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

cd packages/app-store && find . -name "*.ts" -type f -exec grep -l "import prisma from.*@calcom/prisma" {} \; | head -5

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2438


🏁 Script executed:

cd packages/app-store && rg "import prisma from" --type ts | head -10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 754


🏁 Script executed:

cd packages/app-store && rg "CalendarService.ts" --type ts | grep -E "(import|salesforce|office365)" | head -10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

cd packages/app-store/office365calendar/lib && head -30 CalendarService.ts | grep -E "^import"

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 953


🏁 Script executed:

cd packages/app-store/zohocrm/lib && head -30 CalendarService.ts | grep -E "^import"

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 544


Add the missing Prisma import for the credential update.

The prisma object is used at lines 96-99 but not imported in this file, which will fail compilation. Other CalendarService implementations in the codebase (e.g., office365calendar, zohocrm) include this import.

Proposed fix
 import { RRule } from "rrule";
 import { z } from "zod";
+import prisma from "@calcom/prisma";
🤖 Prompt for AI Agents
In `@packages/app-store/salesforce/lib/CalendarService.ts` around lines 96 - 99,
The file uses the Prisma client (await prisma.credential.update(...)) but never
imports it; add an import for the Prisma client at the top of CalendarService.ts
matching how other calendar services import it (the module that exports the
shared prisma instance used by office365calendar/zohocrm), e.g. import { prisma
} from the shared Prisma client module, and ensure the imported symbol name is
prisma so the existing call to prisma.credential.update resolves.

Comment on lines +85 to +94
const tokenInfo = await refreshOAuthTokens(
async () =>
await axios.post(accountsUrl, qs.stringify(formData), {
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
},
}),
"zoho-bigin",
credentialId
);

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the file and check its structure
find . -type f -name "CalendarService.ts" | head -5

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 378


🏁 Script executed:

# Search for refreshOAuthTokens definition
rg -n "refreshOAuthTokens" --type ts --type tsx -A 5 | head -50

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 117


🏁 Script executed:

# Read the specific file around the mentioned lines
fd "CalendarService.ts" --exec cat -n {} \; | sed -n '75,110p'

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1491


🏁 Script executed:

# Find the zoho-bigin CalendarService.ts file
find . -path "*zoho-bigin*" -name "CalendarService.ts" -type f

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 144


🏁 Script executed:

# Also search for refreshOAuthTokens to understand its definition
rg -n "export.*refreshOAuthTokens|function refreshOAuthTokens|const refreshOAuthTokens" -A 10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1432


🏁 Script executed:

# Search for the zoho-bigin directory structure
find . -path "*zoho-bigin*" -type f | head -20

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 681


🏁 Script executed:

cat -n ./packages/app-store/zoho-bigin/lib/CalendarService.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 12695


🏁 Script executed:

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

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1094


🏁 Script executed:

# Verify that CredentialPayload has userId field
rg "type CredentialPayload|interface CredentialPayload" -A 15

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 659


🏁 Script executed:

# Check if credential.userId is used elsewhere in zoho-bigin or similar services
rg "credential\.userId" packages/app-store/ | head -20

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1153


🏁 Script executed:

# Check how zohocrm and other similar services pass userId to refreshOAuthTokens
rg -B 5 -A 5 "refreshOAuthTokens.*credential\.userId" packages/app-store/

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

# Also verify the CredentialPayload structure from the Prisma select
cat packages/prisma/selects/credential.ts | grep -A 50 "credentialForCalendarServiceSelect"

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 637


🏁 Script executed:

# Search for how zohocrm and other services call refreshOAuthTokens
rg "refreshOAuthTokens" packages/app-store/zohocrm/ -B 3 -A 3

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1289


🏁 Script executed:

# Also check hubspot and googlecalendar
rg "refreshOAuthTokens" packages/app-store/hubspot/ packages/app-store/googlecalendar/ -B 3 -A 3

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2834


🏁 Script executed:

# Get more context on zohocrm, hubspot and googlecalendar calls to refreshOAuthTokens
rg "refreshOAuthTokens\(" packages/app-store/zohocrm/lib/CalendarService.ts -A 10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 456


🏁 Script executed:

rg "refreshOAuthTokens\(" packages/app-store/hubspot/lib/CalendarService.ts -A 10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 496


🏁 Script executed:

rg "refreshOAuthTokens\(" packages/app-store/googlecalendar/lib/CalendarService.ts -A 10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 527


🏁 Script executed:

# Get the full refreshOAuthTokens call in zohocrm to see what it passes as third param
rg "refreshOAuthTokens\(" packages/app-store/zohocrm/lib/CalendarService.ts -A 15 | head -25

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 676


🏁 Script executed:

# Get the full refreshOAuthTokens call in hubspot to see what it passes as third param
rg "refreshOAuthTokens\(" packages/app-store/hubspot/lib/CalendarService.ts -A 15 | head -25

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 696


🏁 Script executed:

# Check how googlecalendar handles the response
rg -B 2 -A 5 "const token = res\?\.data" packages/app-store/googlecalendar/

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 908


🏁 Script executed:

# Check hubspot response handling more carefully - why does it access properties directly?
cat -n packages/app-store/hubspot/lib/CalendarService.ts | sed -n '50,100p'

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2170


🏁 Script executed:

# Look at the hubspot HubspotToken type definition
rg "type HubspotToken" packages/app-store/hubspot/ -A 10

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

# Look for HubspotToken definition in callback.ts
cat -n packages/app-store/hubspot/api/callback.ts | head -100

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 2665


🏁 Script executed:

# Also check how hubspot's refreshAccessToken is being called and what it returns
rg -B 10 "const hubspotRefreshToken" packages/app-store/hubspot/lib/CalendarService.ts | head -30

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 513


🏁 Script executed:

# Confirm the issue by checking what happens when refreshOAuthTokens returns a fetch Response
# Let me see if there's any response normalization in other services
rg "parseRefreshTokenResponse|\.json\(\)" packages/app-store/googlecalendar/lib/CalendarService.ts -B 3 -A 3

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 822


🏁 Script executed:

# Check if there's any other handling of fetch Response in the codebase
rg "response\.json\(\)" packages/app-store/_utils/oauth/ -B 3 -A 3

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


🏁 Script executed:

# Check the APP_CREDENTIAL_SHARING_ENABLED constant
rg "APP_CREDENTIAL_SHARING_ENABLED" packages/lib/constants.ts -B 2 -A 2

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 305


🏁 Script executed:

# Look at parseRefreshTokenResponse to understand the pattern
cat -n packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 1344


🏁 Script executed:

# Final verification: confirm that credential.userId exists in zoho-bigin's constructor
cat -n packages/app-store/zoho-bigin/lib/CalendarService.ts | sed -n '43,65p'

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 999


🏁 Script executed:

# Verify that the proposed fix logic for response normalization is sound
# by checking what a fetch Response looks like vs AxiosResponse
rg "const tokenData.*data.*json" packages/app-store/ --type ts -B 2 -A 2

Repository: admin-coderabbit/coderabbit-cal_dot_com-mirror

Length of output: 72


Pass credential.userId and normalize response handling to support credential sharing.

refreshOAuthTokens expects a Cal.com userId, but credentialId is passed instead. More critically, when APP_CREDENTIAL_SHARING_ENABLED is true, the helper returns a fetch Response object (not an AxiosResponse), so accessing tokenInfo.data will fail with a TypeError. The code must handle both response types.

🔧 Proposed fix
   private biginAuth(credential: CredentialPayload) {
     const credentialKey = credential.key as unknown as BiginToken;
     const credentialId = credential.id;
+    const credentialUserId = credential.userId ?? null;
@@
-          : this.refreshAccessToken(credentialId, credentialKey),
+          : this.refreshAccessToken(credentialId, credentialKey, credentialUserId),
     };
   }
@@
-  private async refreshAccessToken(credentialId: number, credentialKey: BiginToken) {
+  private async refreshAccessToken(
+    credentialId: number,
+    credentialKey: BiginToken,
+    credentialUserId: number | null
+  ) {
@@
-    const tokenInfo = await refreshOAuthTokens(
+    const tokenInfo = await refreshOAuthTokens(
       async () =>
         await axios.post(accountsUrl, qs.stringify(formData), {
           headers: {
             "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
           },
         }),
       "zoho-bigin",
-      credentialId
+      credentialUserId
     );
 
-    if (!tokenInfo.data.error) {
+    const tokenData = "data" in tokenInfo ? tokenInfo.data : await tokenInfo.json();
+    if (!tokenData.error) {
       // set expiry date as offset from current time.
-      tokenInfo.data.expiryDate = Math.round(Date.now() + tokenInfo.data.expires_in);
-      tokenInfo.data.accountServer = credentialKey.accountServer;
-      tokenInfo.data.refresh_token = credentialKey.refresh_token;
+      tokenData.expiryDate = Math.round(Date.now() + tokenData.expires_in);
+      tokenData.accountServer = credentialKey.accountServer;
+      tokenData.refresh_token = credentialKey.refresh_token;
@@
-        data: {
-          key: tokenInfo.data as BiginToken,
-        },
+        data: { key: tokenData as BiginToken },
       });
-      this.log.debug("Fetched token", tokenInfo.data.access_token);
+      this.log.debug("Fetched token", tokenData.access_token);
     } else {
-      this.log.error(tokenInfo.data);
+      this.log.error(tokenData);
     }
 
-    return tokenInfo.data as BiginToken;
+    return tokenData as BiginToken;
   }
🤖 Prompt for AI Agents
In `@packages/app-store/zoho-bigin/lib/CalendarService.ts` around lines 85 - 94,
Call refreshOAuthTokens with the Cal.com user id (credential.userId) instead of
credentialId and normalize the returned token payload so both AxiosResponse and
fetch Response work: pass credential.userId into the refreshOAuthTokens call,
then after the call extract the token data by checking for tokenInfo.data first,
and if absent await tokenInfo.json() (and handle a null/undefined result) before
using the tokens; reference the refreshOAuthTokens call site and the tokenInfo
variable when making these changes.

Comment on lines +103 to +104
export const APP_CREDENTIAL_SHARING_ENABLED =
process.env.CALCOM_WEBHOOK_SECRET && process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY;

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Coerce to boolean to avoid exporting a secret value.

Line 103-104 currently resolves to the actual encryption key string when enabled, which risks accidental exposure if this constant is ever serialized or bundled client-side. Please coerce to a boolean.

🔒 Proposed fix
-export const APP_CREDENTIAL_SHARING_ENABLED =
-  process.env.CALCOM_WEBHOOK_SECRET && process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY;
+export const APP_CREDENTIAL_SHARING_ENABLED = Boolean(
+  process.env.CALCOM_WEBHOOK_SECRET && process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY
+);
📝 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
export const APP_CREDENTIAL_SHARING_ENABLED =
process.env.CALCOM_WEBHOOK_SECRET && process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY;
export const APP_CREDENTIAL_SHARING_ENABLED = Boolean(
process.env.CALCOM_WEBHOOK_SECRET && process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY
);
🤖 Prompt for AI Agents
In `@packages/lib/constants.ts` around lines 103 - 104,
APP_CREDENTIAL_SHARING_ENABLED currently evaluates to the raw encryption key
string when both env vars exist; change it to return a boolean instead by
coercing the check for process.env.CALCOM_WEBHOOK_SECRET and
process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY (e.g., use Boolean(...) or
double-bang) so APP_CREDENTIAL_SHARING_ENABLED is true/false and never exposes
secret values.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant