-
Notifications
You must be signed in to change notification settings - Fork 2.8k
refactor: add reusable OAuthProvider class #2898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…nd updating installation and uninstall logic
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughRefactors integrations to a shared OAuthProvider, migrating Slack, HubSpot, and PayPal flows (auth URL, token exchange, refresh, uninstall). Removes legacy env/install/refresh logic, updates API routes to provider-based flows, centralizes types/schemas, tweaks HubSpot API base URL, and minor UI/script updates. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant WebApp
participant Redis
participant Provider as OAuthProvider
participant IdP as OAuth Server
User->>WebApp: Click "Connect Integration"
WebApp->>Redis: SET state → contextId (exp)
WebApp->>IdP: Redirect to authorize?client_id&redirect_uri&scope&state
IdP-->>User: Consent + Redirect back with code,state
User->>WebApp: GET /callback?code&state
WebApp->>Redis: GET state → contextId
WebApp->>IdP: POST token (code + client auth)
IdP-->>WebApp: access_token (+meta)
WebApp->>Provider: Parse/validate token via schema
WebApp-->>User: Redirect to integration settings
sequenceDiagram
autonumber
participant HubSpot as HubSpot Webhook
participant WebApp
participant Provider as HubSpotOAuthProvider
participant DB as Prisma
HubSpot->>WebApp: Webhook (signed)
WebApp->>WebApp: Verify signature (HUBSPOT_CLIENT_SECRET)
WebApp->>DB: Load installation (+credentials)
WebApp->>Provider: refreshTokenForInstallation(installation)
Provider->>DB: Persist refreshed credentials (if needed)
Provider-->>WebApp: Valid auth token
WebApp->>HubSpot: Fetch/process entities
WebApp-->>HubSpot: 200 OK
sequenceDiagram
autonumber
actor User
participant WebApp
participant PayPal as PayPalOAuthProvider
participant PayPalAPI as PayPal
User->>WebApp: GET /partners/paypal/connect
WebApp->>PayPal: Redirect to PAYPAL_AUTHORIZE_URL with state
PayPal-->>WebApp: Redirect back with code,state
WebApp->>PayPal: Exchange code → access_token
WebApp->>PayPalAPI: GET userinfo (with access_token)
PayPalAPI-->>WebApp: { email, email_verified }
WebApp-->>User: Continue connect flow
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧠 Learnings (1)📚 Learning: 2025-06-19T01:46:45.723ZApplied to files:
🧬 Code graph analysis (2)apps/web/app/(ee)/api/paypal/callback/route.ts (1)
apps/web/app/api/slack/callback/route.ts (5)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (3)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…te redirect URI, and simplify schema structure
|
@coderabbitai full review please |
|
@devkiran I'll conduct a full review of the PR that adds the reusable OAuthProvider class and refactors the OAuth flows for Slack, HubSpot, and PayPal. ✅ Actions performedFull review triggered. |
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/lib/integrations/slack/commands.ts (1)
146-156: Access token property mismatch and non‑conformant Slack API call.
credentials.accessTokenlikely should becredentials.access_token(schema uses snake_case). Alsousers.profile.getis a GET; sending POST withContent-Type: application/x-www-form-urlencodedand no body is inconsistent.Apply:
- const response = await fetch( - `https://slack.com/api/users.profile.get?user=${userId}`, - { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Bearer ${credentials.accessToken}`, - }, - }, - ); + const response = await fetch( + `https://slack.com/api/users.profile.get?user=${userId}`, + { + headers: { + Authorization: `Bearer ${credentials.access_token}`, + }, + }, + );If your stored credential uses camelCase, align the schema/type instead and keep one naming convention across storage and code paths.
🧹 Nitpick comments (12)
apps/web/lib/integrations/hubspot/api.ts (1)
8-8: Avoid hard‑coding HubSpot API host; keep an env override for staging/mocks.Restoring an env fallback preserves flexibility (proxies, sandboxes, future region changes) without altering behavior in prod.
Apply:
- private readonly baseUrl = "https://api.hubapi.com/crm/v3"; + private readonly baseUrl = + process.env.HUBSPOT_API_HOST ?? "https://api.hubapi.com/crm/v3";apps/web/lib/integrations/hubspot/track-lead.ts (1)
5-5: Replace magic HubSpot objectTypeId literals with shared constants
• Define named constants (e.g. CONTACT_OBJECT_TYPE_ID and DEAL_OBJECT_TYPE_ID) inapps/web/lib/integrations/hubspot/constants.tsalongsideHUBSPOT_OBJECT_TYPE_IDS.
• Inapi.ts(line 114) andtrack-lead.ts(lines 25, 68), import and use those constants instead of the literal"0-1"/"0-3".apps/web/lib/paypal/schema.ts (1)
7-10: Strengthen user info validationValidate email format and consider normalizing case. Keep schema strict or passthrough per logging/PII policy.
-export const paypalUserInfoSchema = z.object({ - email: z.string(), - email_verified: z.boolean(), // Indicates whether the user's paypal email address is verified. -}); +export const paypalUserInfoSchema = z + .object({ + email: z.string().email().toLowerCase(), + email_verified: z.boolean(), // Indicates whether the user's paypal email address is verified. + }) + .strict();apps/web/app/api/integrations/uninstall/route.ts (1)
60-63: Avoid nulls inside Promise.allFilter non-link-level webhooks before mapping to Promises.
- ...webhooks.map((webhook) => - isLinkLevelWebhook(webhook) ? webhookCache.delete(webhook.id) : null, - ), + ...webhooks + .filter((w) => isLinkLevelWebhook(w)) + .map((w) => webhookCache.delete(w.id)),apps/web/lib/integrations/types.ts (1)
8-25: Derive SlackAuthToken from the Slack schema to avoid driftHard-coding this shape risks divergence from slack/schema.ts. Re-export a z.infer from the authoritative schema.
-import { z } from "zod"; -import { hubSpotAuthTokenSchema, hubSpotContactSchema } from "./hubspot/schema"; +import { z } from "zod"; +import { + hubSpotAuthTokenSchema, + hubSpotContactSchema, +} from "./hubspot/schema"; +import { slackAuthTokenSchema } from "./slack/schema"; @@ -export type SlackAuthToken = { - appId: string; - botUserId: string; - scope: string; - accessToken: string; - tokenType: string; - authUser: { - id: string; - }; - team: { - id: string; - name: string; - }; - incomingWebhook: { - channel: string; - channelId: string; - }; -}; +export type SlackAuthToken = z.infer<typeof slackAuthTokenSchema>;apps/web/app/(ee)/api/hubspot/webhook/route.ts (1)
95-97: Token refresh: ensure typed return and error handlingIf refreshTokenForInstallation can return null, guard before passing authToken to trackers, or make it throw on permanent failures.
- const authToken = - await hubSpotOAuthProvider.refreshTokenForInstallation(installation); + const authToken = + await hubSpotOAuthProvider.refreshTokenForInstallation(installation); + // Consider: if (!authToken) return early with 2xx to avoid retries storm.apps/web/lib/actions/get-integration-install-url.ts (1)
22-28: Use a provider map and narrowintegrationSlugwithz.enum
Switch to aprovidersmap keyed by az.enum(["slack", "hubspot"])forintegrationSlugto eliminate branching and catch invalid slugs at parse-time. PayPal is handled separately inapps/web/lib/actions/partners/generate-paypal-oauth-url.ts, so no changes needed there.apps/web/app/(ee)/api/hubspot/callback/route.ts (1)
98-109: Use stable object type name instead of numeric ID "0-1".HubSpot’s property APIs accept the canonical object type name ("contacts"); relying on numeric IDs is brittle.
Apply this diff:
- waitUntil( - hubSpotApi.createPropertiesBatch({ - objectType: "0-1", - properties: HUBSPOT_DUB_CONTACT_PROPERTIES, - }), - ); + waitUntil( + hubSpotApi.createPropertiesBatch({ + objectType: "contacts", + properties: HUBSPOT_DUB_CONTACT_PROPERTIES, + }), + );apps/web/lib/integrations/hubspot/oauth.ts (1)
55-73: Refine HubSpot OAuth configuration
- “oauth” is a valid HubSpot scope and is automatically added to public apps; you can omit it for clarity but it isn’t required.
- Consider increasing the token-refresh buffer to 2–5 minutes (instead of the default) if your integration issues bursty requests.
apps/web/lib/integrations/slack/oauth.ts (1)
41-64: Consider Basic auth for code exchange.Slack recommends HTTP Basic for oauth.v2.access. Your abstraction supports it; switching
authorizationMethodto"header"would align with guidance. Optional but safer default. (api.slack.com)tokenSchema: slackAuthTokenSchema, bodyFormat: "form", - authorizationMethod: "body", + authorizationMethod: "header",apps/web/lib/integrations/oauth-provider.ts (1)
28-44: Optional: add request timeouts for token calls.Prevent hung requests by adding AbortController to exchange/refresh paths with a sane default (e.g., 10s) or a configurable
timeoutMsinOAuthProviderConfig.Also applies to: 122-131, 132-138
apps/web/lib/paypal/oauth.ts (1)
14-34: Set proper headers for userinfo and keep the call lean.Use
Accept: application/jsonand dropContent-Typeon a GET.async getUserInfo(token: string) { const response = await fetch( `${paypalEnv.PAYPAL_API_HOST}/v1/identity/openidconnect/userinfo?schema=openid`, { headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Bearer ${token}`, + Accept: "application/json", }, }, );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (33)
apps/web/.env.example(0 hunks)apps/web/app/(ee)/api/hubspot/callback/route.ts(3 hunks)apps/web/app/(ee)/api/hubspot/webhook/route.ts(2 hunks)apps/web/app/(ee)/api/paypal/callback/route.ts(2 hunks)apps/web/app/api/integrations/uninstall/route.ts(2 hunks)apps/web/app/api/slack/callback/route.ts(3 hunks)apps/web/lib/actions/get-integration-install-url.ts(2 hunks)apps/web/lib/actions/partners/generate-paypal-oauth-url.ts(2 hunks)apps/web/lib/integrations/hubspot/api.ts(1 hunks)apps/web/lib/integrations/hubspot/constants.ts(0 hunks)apps/web/lib/integrations/hubspot/install.ts(0 hunks)apps/web/lib/integrations/hubspot/oauth.ts(1 hunks)apps/web/lib/integrations/hubspot/refresh-token.ts(0 hunks)apps/web/lib/integrations/hubspot/schema.ts(1 hunks)apps/web/lib/integrations/hubspot/track-lead.ts(1 hunks)apps/web/lib/integrations/hubspot/track-sale.ts(1 hunks)apps/web/lib/integrations/hubspot/types.ts(0 hunks)apps/web/lib/integrations/hubspot/ui/settings.tsx(1 hunks)apps/web/lib/integrations/oauth-provider.ts(1 hunks)apps/web/lib/integrations/slack/commands.ts(6 hunks)apps/web/lib/integrations/slack/env.ts(0 hunks)apps/web/lib/integrations/slack/install.ts(0 hunks)apps/web/lib/integrations/slack/oauth.ts(1 hunks)apps/web/lib/integrations/slack/schema.ts(1 hunks)apps/web/lib/integrations/slack/type.ts(0 hunks)apps/web/lib/integrations/slack/uninstall.ts(0 hunks)apps/web/lib/integrations/slack/verify-request.ts(1 hunks)apps/web/lib/integrations/types.ts(1 hunks)apps/web/lib/paypal/env.ts(1 hunks)apps/web/lib/paypal/oauth.ts(2 hunks)apps/web/lib/paypal/schema.ts(1 hunks)apps/web/scripts/create-integration.ts(1 hunks)apps/web/scripts/workflow.ts(0 hunks)
💤 Files with no reviewable changes (10)
- apps/web/lib/integrations/hubspot/constants.ts
- apps/web/lib/integrations/slack/type.ts
- apps/web/lib/integrations/slack/install.ts
- apps/web/lib/integrations/hubspot/types.ts
- apps/web/lib/integrations/hubspot/install.ts
- apps/web/lib/integrations/hubspot/refresh-token.ts
- apps/web/lib/integrations/slack/uninstall.ts
- apps/web/.env.example
- apps/web/scripts/workflow.ts
- apps/web/lib/integrations/slack/env.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-19T18:46:43.787Z
Learnt from: CR
PR: dubinc/dub#0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-09-19T18:46:43.787Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Do not use React components from hubspot/ui-extensions/crm in settings components
Applied to files:
apps/web/lib/integrations/hubspot/ui/settings.tsx
📚 Learning: 2025-09-19T18:46:43.787Z
Learnt from: CR
PR: dubinc/dub#0
File: packages/hubspot-app/CLAUDE.md:0-0
Timestamp: 2025-09-19T18:46:43.787Z
Learning: Applies to packages/hubspot-app/app/settings/**/*.{js,jsx,ts,tsx} : Only use components exported by hubspot/ui-extensions in settings components
Applied to files:
apps/web/lib/integrations/hubspot/ui/settings.tsx
📚 Learning: 2025-09-17T02:53:28.359Z
Learnt from: devkiran
PR: dubinc/dub#2839
File: apps/web/lib/integrations/hubspot/schema.ts:5-12
Timestamp: 2025-09-17T02:53:28.359Z
Learning: HubSpot's OAuth token response returns `scopes` as an array of strings, not as a space-delimited string. The schema `scopes: z.array(z.string())` in hubSpotAuthTokenSchema is correct for HubSpot's actual API response format.
Applied to files:
apps/web/lib/integrations/hubspot/schema.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (11)
apps/web/scripts/create-integration.ts (1)
10-12: Slack integration seed update LGTMSwitching the payload to Slack aligns with the refactor; no issues spotted here.
apps/web/lib/actions/partners/generate-paypal-oauth-url.ts (1)
24-25: Verify PayPal OAuth provider implements user-scoped state and PKCE in generateAuthUrl and validates them in exchangeCodeForToken to prevent CSRF/mix-upapps/web/lib/integrations/hubspot/track-sale.ts (1)
3-3: Shared HubSpotAuthToken import and schema confirmed – no action required.
HubSpotAuthTokenis exported withaccess_tokenfromapps/web/lib/integrations/types.ts, and no files import from./types.apps/web/lib/integrations/hubspot/schema.ts (1)
11-12: Optionalcreated_atusage is safe
The HubSpot OAuth logic guards withif (!token.created_at)before computingexpiresAt, causing a token refresh when missing rather than assuming its presence.apps/web/lib/paypal/schema.ts (1)
3-5: paypalAuthTokenSchema matches the actual PayPal response
The client-credentials flow in create-paypal-token.ts only returns access_token (expires_in is used internally for caching and not returned), so adding token_type/refresh_token/etc. would break validation.Likely an incorrect or invalid review comment.
apps/web/app/api/integrations/uninstall/route.ts (1)
56-58: No action needed:SlackOAuthProvider.uninstalluses only the passedinstallation.credentialsand makes no DB calls
Theuninstallmethod casts and usesinstallation.credentials(SlackAuthToken) to call the Slack API without re-querying the database, so deleting the record before invokinguninstallis safe.apps/web/app/(ee)/api/hubspot/callback/route.ts (1)
61-76: Nice: strict membership + owner checks before installation.Parallels the Slack route should follow. Good guardrails.
apps/web/app/api/slack/callback/route.ts (1)
49-56: Minimize stored secrets: avoid persisting user access token unless required
Slack returns a user token inauthed_user; dropauthUser.accessTokenif you don’t actually use it to reduce blast radius. Confirm no code paths reference it first (e.g. runrg -n -C2 'credentials\\.authUser' --type=tsto locate anycredentials.authUser.accessTokenusage).apps/web/lib/integrations/slack/oauth.ts (1)
13-38: Add timeout and prefer Bearer header for uninstall.
- Add an AbortController timeout to avoid hanging requests on Slack API hiccups.
- Prefer passing the token via Authorization header over a form param; Slack allows either, but headers are recommended and reduce accidental token logging. (api.slack.com)
[ suggest_recommended_refactor ]
Apply:async uninstall(installation: InstalledIntegration) { const credentials = installation.credentials as SlackAuthToken; - const response = await fetch("https://slack.com/api/apps.uninstall", { + const ac = new AbortController(); + const timeout = setTimeout(() => ac.abort(), 10_000); + const response = await fetch("https://slack.com/api/apps.uninstall", { method: "POST", headers: { - "Content-Type": "application/x-www-form-urlencoded", + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": `Bearer ${credentials.accessToken}`, }, body: new URLSearchParams({ - token: credentials.accessToken, client_id: process.env.SLACK_CLIENT_ID!, client_secret: process.env.SLACK_CLIENT_SECRET!, }), - }); + signal: ac.signal, + }).finally(() => clearTimeout(timeout));apps/web/lib/paypal/oauth.ts (1)
37-49: Confirm token refresh after the provider fix.With the
OAuthProvider.refreshTokenfix to honorauthorizationMethod: "header", PayPal refresh will use Basic auth as required. Please verify end-to-end in sandbox. (developer.paypal.com)apps/web/lib/integrations/oauth-provider.ts (1)
66-74: Remove Edge Buffer fallback—OAuth callbacks run in Node.js
All routes invokingexchangeCodeForToken(Slack, HubSpot, PayPal, partners online-presence) have noexport const runtime = "edge"and therefore run on the default Node runtime whereBufferis available; no Edge-compatible fallback is needed.Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
apps/web/lib/integrations/hubspot/track-sale.ts (1)
56-59: Harden amount parsing and rounding (money correctness).
HubSpot may return amount as a string; Number(...) can yield NaN or floating drift. Parse safely, validate, and round to cents.Apply this diff:
- if (!properties.amount) { - console.error(`[HubSpot] Amount is not set for deal ${dealId}`); - return; - } + const _amountStr = String(properties.amount ?? "").replace(/,/g, ""); + const _amountNum = Number.parseFloat(_amountStr); + if (!Number.isFinite(_amountNum)) { + console.error( + `[HubSpot] Invalid amount "${properties.amount}" for deal ${dealId}`, + ); + return; + } ... - amount: Number(properties.amount) * 100, + amount: Math.round(_amountNum * 100),Also applies to: 77-81
apps/web/lib/integrations/hubspot/track-lead.ts (1)
92-99: Avoid "null undefined" in customerName.
Mirror the earlier branch by filtering nullish parts.Apply this diff:
- const trackLeadResult = await trackLead({ + const customerName = + [contactInfo.properties.firstname, contactInfo.properties.lastname] + .filter(Boolean) + .join(" ") || null; + + const trackLeadResult = await trackLead({ clickId: "", eventName: `Deal ${properties.dealstage}`, customerExternalId: contactInfo.properties.email, - customerName: `${contactInfo.properties.firstname} ${contactInfo.properties.lastname}`, + customerName, customerEmail: contactInfo.properties.email, mode: "async",apps/web/lib/integrations/slack/verify-request.ts (1)
49-65: Use constant‑time comparison for HMAC to prevent timing attacks; also validate presence of signature.Compare full
v0=...signature usingtimingSafeEqual.Apply:
- const [signatureVersion, signatureHash] = signature.split("="); + if (!signature) { + throw new Error(`${verifyErrorPrefix}: missing x-slack-signature`); + } + const [signatureVersion] = signature.split("="); @@ - const expectedSignature = hmac.digest("hex"); - - if (!signatureHash || signatureHash !== expectedSignature) { - throw new Error(`${verifyErrorPrefix}: signature mismatch`); - } + const expectedSignature = `v0=${hmac.digest("hex")}`; + const a = Buffer.from(signature); + const b = Buffer.from(expectedSignature); + if (a.length !== b.length || !timingSafeEqual(a, b)) { + throw new Error(`${verifyErrorPrefix}: signature mismatch`); + }apps/web/lib/integrations/slack/commands.ts (2)
62-74: Handle empty/whitespace input correctlyBecause slackSlashCommandSchema transforms text -> string[], an empty "text" becomes [""] not []. Your check misses this and can lead to downstream crashes. Normalize before checking.
Apply:
- if (data.text.length === 0) { + const tokens = data.text.filter((t) => t && t.trim().length > 0); + if (tokens.length === 0) { return { blocks: [ { type: "section", text: { type: "plain_text", text: "Please provide a destination URL.", }, }, ], }; }
178-186: Guard zod parsing to avoid 500s on bad inputcreateLinkBodySchema.parse throws; with malformed inputs (e.g., url missing), this will bubble and 500 the Slack webhook.
Apply:
- const [url, key, domain] = data.text; - const body = createLinkBodySchema.parse({ url, key, domain }); + const [url, key, domain] = data.text.filter((t) => t && t.trim().length > 0); + const bodyResult = createLinkBodySchema.safeParse({ url, key, domain }); + if (!bodyResult.success) { + return { + blocks: [ + { + type: "section", + text: { + type: "plain_text", + text: "Invalid input. Usage: /shorten <url> [key] [domain]", + }, + }, + ], + }; + }And update the call site:
- const { link, error } = await processLink({ - payload: body, + const { link, error } = await processLink({ + payload: bodyResult.data,apps/web/app/api/slack/callback/route.ts (1)
82-83: Fix potential null dereference: move redirect inside try block.
workspacecan benullat type level; referencingworkspace.slugafter thetry/catchcan fail TS checks and is a latent runtime risk. Move the redirect into thetryafter all async work completes and remove the trailing redirect.Apply this diff:
@@ } catch (e: any) { return handleAndReturnErrorResponse(e); } - redirect(`/${workspace.slug}/settings/integrations/slack`); + // Unreachable if an error was thrown; safe to redirect here + // (place inside the try block right after createWebhook)Inside the try, right after
await createWebhook(...), add:+ redirect(`/${workspace.slug}/settings/integrations/slack`);
🧹 Nitpick comments (19)
apps/web/scripts/create-integration.ts (1)
10-13: Keep the integration description meaningfulPreviously this script seeded a full sentence describing the integration; dropping it to the single word “Slack” will make the integrations catalog less informative for admins. Let’s keep a short actionable description so the seed data stays consistent with the other entries.
Apply this diff to restore a descriptive summary:
- description: "Slack", + description: "Send Dub link activity to Slack.",apps/web/lib/integrations/hubspot/ui/settings.tsx (1)
38-41: Trim input on submit to avoid stray spaces.
Prevents saving " closedwon ".Apply this diff:
- closedWonDealStageId: closedWonDealStageId || null, + closedWonDealStageId: closedWonDealStageId?.trim() || null,apps/web/lib/integrations/hubspot/track-lead.ts (1)
127-163: Wrap background contact update with try/catch.
Prevents unhandled errors (e.g., partner not found) from failing the background task.Apply this diff:
-export const _updateHubSpotContact = async ({ +export const _updateHubSpotContact = async ({ hubSpotApi, contact, trackLeadResult, }: { hubSpotApi: HubSpotApi; contact: HubSpotContact; trackLeadResult: TrackLeadResponse; }) => { - if (contact.properties.dub_link && contact.properties.dub_partner_email) { - console.log( - `[HubSpot] Contact ${contact.id} already has dub_link and dub_partner_email. Skipping update.`, - ); - return; - } - - const properties: Record<string, string> = {}; - - if (trackLeadResult.link?.partnerId) { - const partner = await prisma.partner.findUniqueOrThrow({ - where: { - id: trackLeadResult.link.partnerId, - }, - select: { - email: true, - }, - }); - - if (partner.email) { - properties["dub_partner_email"] = partner.email; - } - } - - if (trackLeadResult.link?.shortLink) { - properties["dub_link"] = trackLeadResult.link.shortLink; - } - - if (Object.keys(properties).length === 0) { - return; - } - - await hubSpotApi.updateContact({ - contactId: contact.id, - properties, - }); + try { + if (contact.properties.dub_link && contact.properties.dub_partner_email) { + console.log( + `[HubSpot] Contact ${contact.id} already has dub_link and dub_partner_email. Skipping update.`, + ); + return; + } + + const properties: Record<string, string> = {}; + + if (trackLeadResult.link?.partnerId) { + const partner = await prisma.partner.findUnique({ + where: { id: trackLeadResult.link.partnerId }, + select: { email: true }, + }); + if (partner?.email) { + properties["dub_partner_email"] = partner.email; + } + } + + if (trackLeadResult.link?.shortLink) { + properties["dub_link"] = trackLeadResult.link.shortLink; + } + + if (Object.keys(properties).length === 0) { + return; + } + + await hubSpotApi.updateContact({ + contactId: contact.id, + properties, + }); + } catch (err) { + console.error( + `[HubSpot] Failed to update contact ${contact.id}:`, + err, + ); + } };apps/web/lib/integrations/hubspot/api.ts (2)
7-13: Make baseUrl configurable for testing/overrides.
Allows pointing to mocks or future API host changes without edits.Apply this diff:
export class HubSpotApi { - private readonly baseUrl = "https://api.hubapi.com/crm/v3"; + private readonly baseUrl: string; private readonly token: string; - constructor({ token }: { token: string }) { - this.token = token; - } + constructor({ + token, + baseUrl = "https://api.hubapi.com/crm/v3", + }: { + token: string; + baseUrl?: string; + }) { + this.token = token; + this.baseUrl = baseUrl; + }
3-5: Add request timeout to avoid hanging fetches.
Prevents indefinite waits on HubSpot outages.Apply this diff:
-type FetchOptions = Omit<RequestInit, "body"> & { - body?: Record<string, unknown>; -}; +type FetchOptions = Omit<RequestInit, "body" | "signal"> & { + body?: Record<string, unknown>; + timeoutMs?: number; +}; private async fetch<T>( input: string, options: FetchOptions = {}, ): Promise<T> { - const { body, headers, ...rest } = options; + const { body, headers, timeoutMs, ...rest } = options; const url = `${this.baseUrl}${input}`; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs ?? 10_000); const fetchOptions: RequestInit = { ...rest, + signal: controller.signal, headers: { Authorization: `Bearer ${this.token}`, ...(body ? { "Content-Type": "application/json" } : {}), ...headers, }, body: body ? JSON.stringify(body) : undefined, }; - const response = await fetch(url, fetchOptions); + const response = await fetch(url, fetchOptions).finally(() => + clearTimeout(timer), + );Also applies to: 15-33
apps/web/lib/integrations/slack/verify-request.ts (1)
36-47: Check absolute timestamp drift (future and past), not just staleness.Use absolute delta to match Slack guidance and avoid clock-skew edge cases.
Apply:
- const requestTimestampMaxDeltaMin = 5; - const fiveMinutesAgoSec = - Math.floor(nowMs / 1000) - 60 * requestTimestampMaxDeltaMin; + const requestTimestampMaxDeltaMin = 5; + const nowSec = Math.floor(nowMs / 1000); + const deltaSec = Math.abs(nowSec - requestTimestampSec); @@ - if (requestTimestampSec < fiveMinutesAgoSec) { + if (deltaSec > 60 * requestTimestampMaxDeltaMin) { throw new Error( `${verifyErrorPrefix}: x-slack-request-timestamp must differ from system time by no more than ${requestTimestampMaxDeltaMin} minutes or request is stale`, ); }apps/web/app/(ee)/api/paypal/callback/route.ts (2)
69-76: Don’t surface raw error messages to end users; map to a safe, whitelisted code.Prevents leaking internal/provider errors via query string.
Apply:
- if (e instanceof Error) { - error = e.message; - } + if (e instanceof Error) { + const allowed = new Set([ + "partner_not_found", + "paypal_email_not_verified", + "invalid_state", + ]); + error = allowed.has(e.message) ? e.message : "oauth_error"; + }
41-44: Explicitly assert required fields from userinfo.Guard against schema drift (e.g., missing
email_verified).Apply:
- if (!paypalUser.email_verified) { + if (!paypalUser?.email || paypalUser.email_verified !== true) { throw new Error("paypal_email_not_verified"); }apps/web/lib/integrations/slack/oauth.ts (1)
8-11: Simplify constructor - extends functionality is sufficient.The constructor only calls
super(provider)without any additional logic. Since the baseOAuthProviderconstructor handles all the configuration, this explicit constructor is unnecessary.-class SlackOAuthProvider extends OAuthProvider<typeof slackAuthTokenSchema> { - constructor(provider: OAuthProviderConfig) { - super(provider); - } +class SlackOAuthProvider extends OAuthProvider<typeof slackAuthTokenSchema> {apps/web/app/api/integrations/uninstall/route.ts (2)
60-63: Avoid passing nulls into Promise.all.Filter falsy entries for clarity.
Apply:
- Promise.all([ + Promise.all( ...(integrationId === SLACK_INTEGRATION_ID ? [slackOAuthProvider.uninstall(installation)] : []), - - ...webhooks.map((webhook) => - isLinkLevelWebhook(webhook) ? webhookCache.delete(webhook.id) : null, - ), - ]), + ...webhooks + .filter((w) => isLinkLevelWebhook(w)) + .map((w) => webhookCache.delete(w.id)), + ),
14-21: ValidateinstallationIdinput early.Fail fast on missing/invalid
installationIdto reduce unnecessary DB calls.async ({ searchParams, session, workspace }) => { - const { installationId } = searchParams; + const { installationId } = searchParams; + if (!installationId || typeof installationId !== "string") { + throw new DubApiError({ code: "bad_request", message: "installationId is required." }); + }apps/web/lib/integrations/slack/commands.ts (2)
148-156: Use correct Slack Web API invocation for users.profile.getThis method is typically GET with Bearer token; sending POST with x-www-form-urlencoded but no body is brittle.
Apply:
- const response = await fetch( - `https://slack.com/api/users.profile.get?user=${userId}`, - { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Authorization: `Bearer ${credentials.accessToken}`, - }, - }, - ); + const response = await fetch( + `https://slack.com/api/users.profile.get?user=${userId}`, + { + headers: { + Authorization: `Bearer ${credentials.accessToken}`, + }, + }, + );
203-210: Time fields ignored by toLocaleDateStringtoLocaleDateString drops hour/minute. Use toLocaleString to show time.
Apply:
- const createdAtDate = new Date(createdAt).toLocaleDateString("en-us", { + const createdAtDate = new Date(createdAt).toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "numeric", hour12: true, });apps/web/app/(ee)/api/hubspot/callback/route.ts (3)
114-115: Avoid possible null deref on workspace in redirectworkspace is typed as possibly null; redirect after the try can trip TS and risks future regressions.
Apply:
- redirect(`/${workspace.slug}/settings/integrations/hubspot`); + // Move inside try{} after successful install: + // redirect(`/${workspace.slug}/settings/integrations/hubspot`);And inside the try block right after the waitUntil block:
+ redirect(`/${workspace.slug}/settings/integrations/hubspot`);
55-56: Remove unused select fielddefaultFolderId isn’t used; drop to reduce query cost and noise.
- role: true, - defaultFolderId: true, + role: true,
86-90: Consider persisting token expiryIf token includes expires_in, store expires_at to enable proactive refresh.
Example:
const credentials = { ...token, created_at: Date.now(), + // Optional if `token.expires_in` exists: + // expires_at: Date.now() + token.expires_in * 1000, };apps/web/lib/paypal/oauth.ts (1)
18-21: Remove Content-Type on GETGET requests don’t need x-www-form-urlencoded; some servers treat it suspiciously. Optionally set Accept: application/json.
- Authorization: `Bearer ${token}`, - "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Bearer ${token}`, + Accept: "application/json",apps/web/lib/integrations/oauth-provider.ts (2)
6-18: Tie the generic parameter to the schema for proper type inference.
tokenSchemais typed asz.ZodSchemabut the class is generic overT. As written,z.infer<T>isn’t guaranteed to matchprovider.tokenSchema. MakeOAuthProviderConfiggeneric and usez.ZodTypeAnyto preserve schema inference.Apply this diff:
-import { z } from "zod"; +import { z } from "zod"; @@ -export interface OAuthProviderConfig { +export interface OAuthProviderConfig<T extends z.ZodTypeAny> { name: string; clientId: string; clientSecret: string; authUrl: string; tokenUrl: string; redirectUri: string; scopes: string; redisStatePrefix: string; - tokenSchema: z.ZodSchema; + tokenSchema: T; bodyFormat: "form" | "json"; authorizationMethod: "header" | "body"; } @@ -export class OAuthProvider<T extends z.ZodSchema> { - constructor(private provider: OAuthProviderConfig) {} +export class OAuthProvider<T extends z.ZodTypeAny> { + constructor(private provider: OAuthProviderConfig<T>) {}This ensures
tokenis correctly inferred asz.infer<T>throughout.Also applies to: 25-27
141-166: Make refresh flow honor provider auth method/body format; add timeout and safe logging.Refresh currently hardcodes form + client creds in body. Align with
authorizationMethod/bodyFormat, add a timeout, and sanitize logs.Apply this diff:
// Refresh the token async refreshToken(refreshToken: string): Promise<z.infer<T>> { - const response = await fetch(this.provider.tokenUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: this.provider.clientId, - client_secret: this.provider.clientSecret, - }), - }); - - const result = await response.json(); - - if (!response.ok) { - console.error(`[${this.provider.name}] refreshToken`, result); - - throw new Error( - `[${this.provider.name}] Failed to refresh the access token. Please try again.`, - ); - } - - return this.provider.tokenSchema.parse(result); + let headers: Record<string, string> = {}; + let body: BodyInit; + + if (this.provider.authorizationMethod === "header") { + const credentials = Buffer.from( + `${this.provider.clientId}:${this.provider.clientSecret}`, + "utf8", + ).toString("base64"); + headers["Authorization"] = `Basic ${credentials}`; + } + + switch (this.provider.bodyFormat) { + case "json": { + headers["Content-Type"] = "application/json"; + const payload: Record<string, string> = { + grant_type: "refresh_token", + refresh_token: refreshToken, + }; + if (this.provider.authorizationMethod === "body") { + payload.client_id = this.provider.clientId; + payload.client_secret = this.provider.clientSecret; + } + body = JSON.stringify(payload); + break; + } + case "form": + default: { + headers["Content-Type"] = "application/x-www-form-urlencoded"; + const params = new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + }); + if (this.provider.authorizationMethod === "body") { + params.append("client_id", this.provider.clientId); + params.append("client_secret", this.provider.clientSecret); + } + body = params.toString(); + break; + } + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 15000); + try { + const response = await fetch(this.provider.tokenUrl, { + method: "POST", + headers: { Accept: "application/json", ...headers }, + body, + signal: controller.signal, + }); + const result = await response.json().catch(() => ({})); + if (!response.ok) { + console.error(`[${this.provider.name}] refreshToken`, { + status: response.status, + error: result?.error, + error_description: result?.error_description, + }); + throw new Error( + `[${this.provider.name}] Failed to refresh the access token. Please try again.`, + ); + } + return this.provider.tokenSchema.parse(result); + } finally { + clearTimeout(timeout); + } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (33)
apps/web/.env.example(0 hunks)apps/web/app/(ee)/api/hubspot/callback/route.ts(3 hunks)apps/web/app/(ee)/api/hubspot/webhook/route.ts(2 hunks)apps/web/app/(ee)/api/paypal/callback/route.ts(2 hunks)apps/web/app/api/integrations/uninstall/route.ts(2 hunks)apps/web/app/api/slack/callback/route.ts(3 hunks)apps/web/lib/actions/get-integration-install-url.ts(2 hunks)apps/web/lib/actions/partners/generate-paypal-oauth-url.ts(2 hunks)apps/web/lib/integrations/hubspot/api.ts(1 hunks)apps/web/lib/integrations/hubspot/constants.ts(0 hunks)apps/web/lib/integrations/hubspot/install.ts(0 hunks)apps/web/lib/integrations/hubspot/oauth.ts(1 hunks)apps/web/lib/integrations/hubspot/refresh-token.ts(0 hunks)apps/web/lib/integrations/hubspot/schema.ts(1 hunks)apps/web/lib/integrations/hubspot/track-lead.ts(1 hunks)apps/web/lib/integrations/hubspot/track-sale.ts(1 hunks)apps/web/lib/integrations/hubspot/types.ts(0 hunks)apps/web/lib/integrations/hubspot/ui/settings.tsx(1 hunks)apps/web/lib/integrations/oauth-provider.ts(1 hunks)apps/web/lib/integrations/slack/commands.ts(6 hunks)apps/web/lib/integrations/slack/env.ts(0 hunks)apps/web/lib/integrations/slack/install.ts(0 hunks)apps/web/lib/integrations/slack/oauth.ts(1 hunks)apps/web/lib/integrations/slack/schema.ts(1 hunks)apps/web/lib/integrations/slack/type.ts(0 hunks)apps/web/lib/integrations/slack/uninstall.ts(0 hunks)apps/web/lib/integrations/slack/verify-request.ts(1 hunks)apps/web/lib/integrations/types.ts(1 hunks)apps/web/lib/paypal/env.ts(1 hunks)apps/web/lib/paypal/oauth.ts(2 hunks)apps/web/lib/paypal/schema.ts(1 hunks)apps/web/scripts/create-integration.ts(1 hunks)apps/web/scripts/workflow.ts(0 hunks)
💤 Files with no reviewable changes (10)
- apps/web/.env.example
- apps/web/lib/integrations/slack/type.ts
- apps/web/lib/integrations/hubspot/install.ts
- apps/web/lib/integrations/slack/install.ts
- apps/web/lib/integrations/slack/uninstall.ts
- apps/web/lib/integrations/slack/env.ts
- apps/web/lib/integrations/hubspot/types.ts
- apps/web/lib/integrations/hubspot/constants.ts
- apps/web/lib/integrations/hubspot/refresh-token.ts
- apps/web/scripts/workflow.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T01:46:45.723Z
Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-19T01:46:45.723Z
Learning: PayPal webhook verification in the Dub codebase is handled at the route level in `apps/web/app/(ee)/api/paypal/webhook/route.ts` using the `verifySignature` function. Individual webhook handlers like `payoutsItemFailed` don't need to re-verify signatures since they're only called after successful verification.
Applied to files:
apps/web/app/(ee)/api/paypal/callback/route.ts
🧬 Code graph analysis (14)
apps/web/lib/integrations/types.ts (1)
apps/web/lib/integrations/hubspot/schema.ts (2)
hubSpotAuthTokenSchema(5-12)hubSpotContactSchema(23-33)
apps/web/lib/integrations/slack/oauth.ts (5)
apps/web/lib/integrations/oauth-provider.ts (2)
OAuthProvider(25-167)OAuthProviderConfig(6-18)apps/web/lib/integrations/slack/schema.ts (1)
slackAuthTokenSchema(3-21)apps/web/lib/integrations/types.ts (1)
SlackAuthToken(8-25)apps/web/lib/api/errors.ts (1)
DubApiError(75-92)packages/utils/src/constants/main.ts (1)
APP_DOMAIN_WITH_NGROK(20-25)
apps/web/lib/paypal/oauth.ts (4)
apps/web/lib/integrations/oauth-provider.ts (2)
OAuthProvider(25-167)OAuthProviderConfig(6-18)apps/web/lib/paypal/schema.ts (2)
paypalAuthTokenSchema(3-5)paypalUserInfoSchema(7-10)apps/web/lib/paypal/env.ts (1)
paypalEnv(3-13)packages/utils/src/constants/main.ts (1)
APP_DOMAIN_WITH_NGROK(20-25)
apps/web/lib/actions/get-integration-install-url.ts (2)
apps/web/lib/integrations/slack/oauth.ts (1)
slackOAuthProvider(41-64)apps/web/lib/integrations/hubspot/oauth.ts (1)
hubSpotOAuthProvider(55-73)
apps/web/lib/integrations/slack/commands.ts (3)
apps/web/lib/integrations/slack/schema.ts (1)
slackSlashCommandSchema(23-29)apps/web/lib/integrations/types.ts (1)
SlackAuthToken(8-25)packages/utils/src/constants/main.ts (1)
APP_DOMAIN(13-18)
apps/web/lib/integrations/hubspot/oauth.ts (4)
apps/web/lib/integrations/oauth-provider.ts (2)
OAuthProvider(25-167)OAuthProviderConfig(6-18)apps/web/lib/integrations/hubspot/schema.ts (1)
hubSpotAuthTokenSchema(5-12)apps/web/lib/integrations/types.ts (1)
HubSpotAuthToken(4-4)packages/utils/src/constants/main.ts (1)
APP_DOMAIN_WITH_NGROK(20-25)
apps/web/app/(ee)/api/hubspot/webhook/route.ts (1)
apps/web/lib/integrations/hubspot/oauth.ts (1)
hubSpotOAuthProvider(55-73)
apps/web/app/api/slack/callback/route.ts (4)
apps/web/lib/integrations/slack/oauth.ts (1)
slackOAuthProvider(41-64)apps/web/lib/integrations/types.ts (1)
SlackAuthToken(8-25)apps/web/lib/integrations/install.ts (1)
installIntegration(14-102)apps/web/lib/webhook/create-webhook.ts (1)
createWebhook(13-106)
apps/web/lib/integrations/oauth-provider.ts (2)
apps/web/lib/upstash/redis.ts (1)
redis(4-7)packages/utils/src/functions/urls.ts (1)
getSearchParams(40-49)
apps/web/app/(ee)/api/hubspot/callback/route.ts (4)
apps/web/lib/integrations/hubspot/oauth.ts (1)
hubSpotOAuthProvider(55-73)apps/web/lib/integrations/install.ts (1)
installIntegration(14-102)apps/web/lib/integrations/hubspot/api.ts (1)
HubSpotApi(7-134)apps/web/lib/integrations/hubspot/constants.ts (1)
HUBSPOT_DUB_CONTACT_PROPERTIES(8-31)
apps/web/lib/actions/partners/generate-paypal-oauth-url.ts (1)
apps/web/lib/paypal/oauth.ts (1)
paypalOAuthProvider(37-49)
apps/web/lib/integrations/hubspot/ui/settings.tsx (1)
apps/web/lib/integrations/hubspot/constants.ts (1)
HUBSPOT_DEFAULT_CLOSED_WON_DEAL_STAGE_ID(6-6)
apps/web/app/(ee)/api/paypal/callback/route.ts (1)
apps/web/lib/paypal/oauth.ts (1)
paypalOAuthProvider(37-49)
apps/web/app/api/integrations/uninstall/route.ts (1)
apps/web/lib/integrations/slack/oauth.ts (1)
slackOAuthProvider(41-64)
🔇 Additional comments (26)
apps/web/lib/integrations/hubspot/track-sale.ts (1)
3-3: Import path change looks good.
Centralizing HubSpot types is the right direction.apps/web/lib/integrations/hubspot/ui/settings.tsx (1)
18-20: State default fallback LGTM.
Good UX: default to closedwon when settings are empty.apps/web/lib/integrations/hubspot/track-lead.ts (2)
5-5: Import path change looks good.
Consistent with centralized ../types.
93-95: EmptyclickIdfallback intrackLeadvalidated
trackLeadexplicitly handles a falsyclickIdby looking up the existing customer’sclickId(and assigning it) or throwing a clearDubApiErrorif none is found—there’s no silent failure.apps/web/lib/integrations/hubspot/schema.ts (1)
11-12: Optionalcreated_atis safe; HubSpot OAuth logic covers undefined cases
isTokenValid()returns false whencreated_atis missing, causingrefreshTokenForInstallation()to fetch a new token and setcreated_at. No other HubSpot workflows assume a definedcreated_at.apps/web/app/(ee)/api/paypal/callback/route.ts (1)
35-39: exchangeCodeForToken atomically consumes and deletes the OAuthstateviaredis.getdel, preventing replay.apps/web/lib/actions/partners/generate-paypal-oauth-url.ts (1)
3-5: No change needed: the PayPal OAuth provider’sredirectUriusesAPP_DOMAIN_WITH_NGROKconsistently for both authorization and token exchange.apps/web/lib/integrations/slack/verify-request.ts (1)
14-23: Disable default parsing on your Slack webhook route and pass raw body
Slack signs the raw payload, so you must disable the JSON/body parser (e.g.export const config = { api: { bodyParser: false } }in Next.js) and feedawait req.text()directly intoverifySlackSignature. Otherwise any JSON‐parse/re-stringify will break the signature check.apps/web/lib/actions/get-integration-install-url.ts (2)
3-4: LGTM! Refactored to use OAuth providers.The refactoring successfully replaces direct imports of installation URL functions with OAuth provider instances. The method calls have been updated from
getSlackInstallationUrl(workspace.id)toslackOAuthProvider.generateAuthUrl(workspace.id)and similarly for HubSpot.
23-23: LGTM! Method calls updated to use provider pattern.The function calls have been correctly updated to use the new OAuth provider pattern. Both Slack and HubSpot now use their respective OAuth providers'
generateAuthUrlmethods instead of direct function calls.Also applies to: 25-25
apps/web/lib/paypal/env.ts (1)
6-8: LGTM! Refactored authorization URL configuration.The change from
PAYPAL_AUTHORIZE_HOSTtoPAYPAL_AUTHORIZE_URLprovides complete URLs rather than just hostnames, which is more convenient and consistent with other OAuth provider implementations in this refactoring.apps/web/lib/paypal/schema.ts (1)
1-10: LGTM! Well-defined PayPal schemas.The schemas are well-structured and follow established patterns:
paypalAuthTokenSchemacorrectly defines the OAuth token response structurepaypalUserInfoSchemaincludes the essential user information fields with a helpful comment explaining theemail_verifiedfieldapps/web/lib/integrations/slack/schema.ts (1)
1-29: LGTM! Comprehensive Slack schemas.Both schemas are well-structured and comprehensive:
slackAuthTokenSchemaproperly captures all the necessary fields from Slack's OAuth response, including nested objects forauthed_user,team, andincoming_webhookslackSlashCommandSchemacorrectly validates slash command payloads with proper transformation of thetextfield into a string array and validates the command enumThe schemas align well with Slack's API specifications and provide strong type safety.
apps/web/lib/integrations/slack/oauth.ts (2)
41-64: LGTM! Well-configured OAuth provider instance.The OAuth provider configuration is comprehensive and correct:
- All required Slack OAuth endpoints are properly configured
- Scopes include all necessary permissions for the integration functionality
- Redis state prefix follows a clear naming convention
- Configuration matches Slack's OAuth v2 flow requirements
13-38: Verify Slack apps.uninstall usage and error handling.
- Code posts token, client_id and client_secret as x-www-form-urlencoded and OAuth provider is configured with authorizationMethod: "body" (apps/web/lib/integrations/slack/oauth.ts). Confirm this matches Slack docs and that the token type stored in installation.credentials (bot vs user vs app token) is permitted for apps.uninstall.
- Improve failure handling: log response.text/body on errors, treat non-2xx HTTP statuses as failures, handle 429 Retry-After (rate limiting), and consider adding an Authorization: Bearer fallback.
apps/web/app/(ee)/api/hubspot/webhook/route.ts (1)
92-97: Good: token refresh moved behind provider.Using
hubSpotOAuthProvider.refreshTokenForInstallationcentralizes refresh and storage; keeps route lean.Please confirm that this method throws on failure so the surrounding
Promise.allSettledhandler can surface errors consistently.apps/web/lib/integrations/hubspot/oauth.ts (1)
43-52: Token validity buffer looks good.1‑minute early refresh is a sensible default.
apps/web/app/api/integrations/uninstall/route.ts (1)
56-58: Add idempotent handling and catch errors in Slack uninstall
In apps/web/lib/integrations/slack/oauth.ts, update uninstall() to treat “not_found” or expired-token responses as success instead of throwing, e.g.:
if (!data.ok && data.error !== "not_authed" && data.error !== "not_found") { // existing error handling }In apps/web/app/api/integrations/uninstall/route.ts, wrap the call under waitUntil in a try/catch so failures are logged and don’t silently drop:
waitUntil( (async () => { try { if (integrationId === SLACK_INTEGRATION_ID) { await slackOAuthProvider.uninstall(installation); } // … } catch (err) { console.error("Slack uninstall failed:", err); } })() );apps/web/lib/integrations/slack/commands.ts (1)
8-10: Good refactor to centralized schema + typesUsing slackSlashCommandSchema and SlackAuthToken improves consistency and validation across Slack flows.
Also applies to: 20-21, 76-82, 118-124, 168-177, 201-249
apps/web/app/(ee)/api/hubspot/callback/route.ts (1)
98-109: Non-blocking post‑install setup looks goodUsing waitUntil for HubSpot property creation is the right call to keep the callback snappy.
apps/web/lib/paypal/oauth.ts (2)
37-49: Providerization LGTMSwitching to OAuthProvider and parsing with paypalUserInfoSchema improves reliability and reuse.
14-34: Update PayPal host and scopes
- Use
https://api-m.paypal.com(prod) /https://api-m.sandbox.paypal.com(sandbox) for both/v1/oauth2/tokenand/v1/identity/openidconnect/userinfoper PayPal’s 2025 Log in with PayPal guide.- Ensure token requests include the
openidandemail_verifiedare returned by the userinfo endpoint.apps/web/app/api/slack/callback/route.ts (2)
40-47: Integration lookup by slug looks good.Using
findUniqueOrThrowwithslug: "slack"keeps the flow deterministic and avoids extra conditionals. LGTM.
26-27: Good use of provider exchange for state/CSRF handling.Delegating code/state verification to the reusable provider keeps callback logic lean and consistent. LGTM.
apps/web/lib/integrations/oauth-provider.ts (2)
51-53: Discard suggestion:getSearchParamscorrectly usesURLSearchParams.forEach; replacing it is unnecessary.Likely an incorrect or invalid review comment.
4-4: Keep import from../upstash—the barrel re-exportsredis. Theapps/web/lib/upstash/index.tsfile includesexport * from "./redis", soimport { redis } from "../upstash"resolves correctly.Likely an incorrect or invalid review comment.
✅ Actions performedFull review triggered. |
|
@coderabbitai addressed some of your feedback. Final review, please |
|
@devkiran I'll conduct a final review of your changes after you've addressed the previous feedback. Let me take a comprehensive look at the OAuth provider refactoring. ✅ Actions performedFull review triggered. |
Summary by CodeRabbit
New Features
Refactor
Security
Bug Fixes