Skip to content

Embedded Stripe checkout with redirect mode#1231

Open
manager-hub wants to merge 4 commits intodevelopfrom
sina-oracles-feature/embedded-stripe-checkout
Open

Embedded Stripe checkout with redirect mode#1231
manager-hub wants to merge 4 commits intodevelopfrom
sina-oracles-feature/embedded-stripe-checkout

Conversation

@manager-hub
Copy link
Copy Markdown
Collaborator

@manager-hub manager-hub commented Feb 16, 2026

Summary

  • Add embedded Stripe checkout mode with feature flag support
  • Fix checkout to use session URL redirect instead of client-side embedding (more reliable across regions)
  • Use trusted frontend URL for Stripe return/cancel URLs instead of req.headers.origin
  • Add python3 and build-essential to Docker for sqlite3 native build

Test plan

  • Verify Stripe checkout flow redirects correctly in staging
  • Confirm checkout works for verified countries (Nigeria, India, Thailand)
  • Test fallback behavior when checkout URL is missing

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Enhanced Stripe subscription checkout with support for embedded and redirect payment modes, providing more flexible checkout experiences.
  • Chores

    • Updated Docker build environments with additional dependencies to support expanded system requirements.

Ubuntu and others added 4 commits December 23, 2025 20:36
- Add embedded mode parameter to checkout session creation
- Return client_secret for embedded mode instead of sessionId
- Use return_url instead of success_url/cancel_url for embedded mode
- Look up promotion codes properly when coupon param is provided

Backend supports both modes - controlled by frontend request param.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Security improvements:
- Add TRUSTED_FRONTEND_URL based on SERVER_URL environment mapping
- Replace req.headers.origin with trusted URL to prevent URL spoofing
- Add null check for session.client_secret in embedded mode
- Return 500 error with log if client_secret is missing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add session.url to response for redirect mode (stripe.redirectToCheckout removed in v8)
- Add null check for session.url with error logging
- Keep sessionId in response for backwards compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
sqlite3 requires node-gyp to compile native bindings, which needs
Python and build tools. This fixes the CI build failure:
"gyp ERR! find Python - python3 is not in PATH or produced an error"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

The pull request updates system dependencies in Docker build environments by adding Python 3 and build-essential packages, and enhances the Stripe subscription controller to support embedded checkout mode with a configurable trusted frontend URL mechanism for redirect handling.

Changes

Cohort / File(s) Summary
Docker Dependencies
desci-server/Dockerfile, desci-server/Dockerfile.ci
Added python3 and build-essential packages to the apt-get install steps, expanding the base build environment to include Python tooling and compilation utilities.
Stripe Subscription Controller
desci-server/src/controllers/stripe/subscription.ts
Introduced TRUSTED_FRONTEND_URL configuration for redirect/return URLs, extended createSubscriptionSchema with returnUrl and embedded fields, implemented embedded vs. redirect mode switching for checkout sessions, and updated createCustomerPortal to use trusted URL instead of request headers origin.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/Frontend
    participant Server as Server
    participant Stripe as Stripe API

    alt Embedded Mode
        Client->>Server: POST /checkout (embedded: true)
        Server->>Server: Validate schema, set ui_mode: 'embedded'
        Server->>Stripe: Create checkout session (embedded)
        Stripe-->>Server: Return clientSecret
        Server-->>Client: Return clientSecret
        Client->>Client: Initialize Stripe embedded form
    else Redirect Mode
        Client->>Server: POST /checkout (embedded: false)
        Server->>Server: Validate schema, set success/cancel URLs
        Server->>Stripe: Create checkout session (redirect)
        Stripe-->>Server: Return sessionId & url
        Server-->>Client: Return session url
        Client->>Stripe: Redirect to checkout
        Stripe->>Client: Redirect to success/cancel URL
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • desci-labs/nodes#1190: Modifies the same createCustomerPortal function in the Stripe subscription controller, suggesting coordinated changes to portal functionality.
  • desci-labs/nodes#1170: Updates the same file with logging and import changes, indicating parallel refactoring efforts in the Stripe subscription module.

Poem

🐰 ✨ Hop hop! The Docker container grows,
With Python and build tools in tidy rows,
While Stripe's checkout dances both ways—
Embedded or redirect, the customer plays,
Trusted URLs guard each precious return! 🛒

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (78 files):

⚔️ .env.example (content)
⚔️ .env.test (content)
⚔️ .gitignore (content)
⚔️ .nvmrc (content)
⚔️ ceramic-k8s/dev/recon_dev.yaml (content)
⚔️ ceramic-k8s/migration.md (content)
⚔️ ceramic-k8s/prod/recon_prod.yaml (content)
⚔️ desci-server/Dockerfile (content)
⚔️ desci-server/Dockerfile.ci (content)
⚔️ desci-server/kubernetes/cronjob_email_reminders_dev.yaml (content)
⚔️ desci-server/kubernetes/cronjob_email_reminders_prod.yaml (content)
⚔️ desci-server/kubernetes/deployment_dev.yaml (content)
⚔️ desci-server/kubernetes/deployment_prod.yaml (content)
⚔️ desci-server/kubernetes/deployment_staging.yaml (content)
⚔️ desci-server/package.json (content)
⚔️ desci-server/prisma/schema.prisma (content)
⚔️ desci-server/src/config.ts (content)
⚔️ desci-server/src/config/stripe.ts (content)
⚔️ desci-server/src/controllers/admin/users.ts (content)
⚔️ desci-server/src/controllers/auth/google.ts (content)
⚔️ desci-server/src/controllers/externalApi/ResearchAssistant/getUsageStatus.ts (content)
⚔️ desci-server/src/controllers/journals/featured.ts (content)
⚔️ desci-server/src/controllers/journals/management/create.ts (content)
⚔️ desci-server/src/controllers/journals/submissions/index.ts (content)
⚔️ desci-server/src/controllers/nodes/publish.ts (content)
⚔️ desci-server/src/controllers/sendgrid/webhook.ts (content)
⚔️ desci-server/src/controllers/stripe/subscription.ts (content)
⚔️ desci-server/src/controllers/stripe/webhook.ts (content)
⚔️ desci-server/src/docs/journals.ts (content)
⚔️ desci-server/src/routes/v1/admin/users/index.ts (content)
⚔️ desci-server/src/routes/v1/auth.ts (content)
⚔️ desci-server/src/routes/v1/journals/submissions.ts (content)
⚔️ desci-server/src/schemas/auth.schema.ts (content)
⚔️ desci-server/src/schemas/journals.schema.ts (content)
⚔️ desci-server/src/services/ElasticNodesService.ts (content)
⚔️ desci-server/src/services/FeatureLimits/FeatureLimitsService.ts (content)
⚔️ desci-server/src/services/FeatureLimits/constants.ts (content)
⚔️ desci-server/src/services/Notifications/NotificationService.ts (content)
⚔️ desci-server/src/services/Notifications/notificationPayloadTypes.ts (content)
⚔️ desci-server/src/services/OpenAlexService.ts (content)
⚔️ desci-server/src/services/PublishServices.ts (content)
⚔️ desci-server/src/services/StripeCouponService.ts (content)
⚔️ desci-server/src/services/SubscriptionService.ts (content)
⚔️ desci-server/src/services/auth.ts (content)
⚔️ desci-server/src/services/email/email.ts (content)
⚔️ desci-server/src/services/email/sciweaveEmailTypes.ts (content)
⚔️ desci-server/src/services/email/sciweaveEmails.ts (content)
⚔️ desci-server/src/services/journals/JournalManagementService.ts (content)
⚔️ desci-server/src/services/journals/JournalSubmissionService.ts (content)
⚔️ desci-server/src/services/user.ts (content)
⚔️ desci-server/src/services/user/Marketing.ts (content)
⚔️ desci-server/src/templates/emails/journals/DeskRejection.tsx (content)
⚔️ desci-server/src/templates/emails/journals/ExternalRefereeInvite.tsx (content)
⚔️ desci-server/src/templates/emails/journals/FinalRejectionDecision.tsx (content)
⚔️ desci-server/src/templates/emails/journals/InviteEditor.tsx (content)
⚔️ desci-server/src/templates/emails/journals/MajorRevisionRequest.tsx (content)
⚔️ desci-server/src/templates/emails/journals/MinorRevisionRequest.tsx (content)
⚔️ desci-server/src/templates/emails/journals/OverdueAlertEditor.tsx (content)
⚔️ desci-server/src/templates/emails/journals/RefereeAccepted.tsx (content)
⚔️ desci-server/src/templates/emails/journals/RefereeDeclinedEmail.tsx (content)
⚔️ desci-server/src/templates/emails/journals/RefereeInvite.tsx (content)
⚔️ desci-server/src/templates/emails/journals/RefereeReassigned.tsx (content)
⚔️ desci-server/src/templates/emails/journals/RefereeReviewReminder.tsx (content)
⚔️ desci-server/src/templates/emails/journals/RevisionSubmittedConfirmation.tsx (content)
⚔️ desci-server/src/templates/emails/journals/SubmissionAcceped.tsx (content)
⚔️ desci-server/src/templates/emails/journals/SubmissionAssigned.tsx (content)
⚔️ desci-server/src/templates/emails/journals/SubmissionReassigned.tsx (content)
⚔️ desci-server/src/utils/manifest.ts (content)
⚔️ desci-server/src/workers/emailReminderConfig.ts (content)
⚔️ desci-server/test/integration/emailReminders.test.ts (content)
⚔️ desci-server/test/integration/journals/journalManagement.test.ts (content)
⚔️ desci-server/test/integration/journals/journalSettings.test.ts (content)
⚔️ desci-server/test/integration/journals/journalSubmission.test.ts (content)
⚔️ desci-server/test/integration/research-assistant/researchAssistantMetering.test.ts (content)
⚔️ desci-server/yarn.lock (content)
⚔️ docker-compose.media.yml (content)
⚔️ nodes-lib/src/shared/config/index.ts (content)
⚔️ nodes-media/Dockerfile (content)

These conflicts must be resolved before merging into develop.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: introducing embedded Stripe checkout mode alongside redirect mode functionality, which is the primary focus of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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 sina-oracles-feature/embedded-stripe-checkout
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch sina-oracles-feature/embedded-stripe-checkout
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai 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: 2

🤖 Fix all issues with AI agents
In `@desci-server/src/controllers/stripe/subscription.ts`:
- Around line 232-234: The portal return URL uses the user-supplied returnUrl
directly when creating portalSession (stripe.billingPortal.sessions.create) —
validate the origin the same way as for the other URL: parse returnUrl, ensure
its origin equals TRUSTED_FRONTEND_URL (or matches allowed trusted origins), and
only pass the user returnUrl to stripe.billingPortal.sessions.create if it
passes validation; otherwise fall back to the trusted default
(`${TRUSTED_FRONTEND_URL}/settings/subscription`). Update the logic around
portalSession creation in subscription.ts to perform this origin check before
constructing the request.
- Around line 71-79: The code accepts user-supplied
returnUrl/successUrl/cancelUrl and uses them directly, bypassing
TRUSTED_FRONTEND_URL; update the session URL assignment in the block that sets
sessionConfig (the embedded branch and the else branch) to either (1) always use
TRUSTED_FRONTEND_URL and ignore returnUrl/successUrl/cancelUrl, or (2) validate
any provided returnUrl/successUrl/cancelUrl against an allowlist rule (e.g.,
must startWith TRUSTED_FRONTEND_URL) and only use them when they pass
validation, otherwise fall back to the TRUSTED_FRONTEND_URL defaults; ensure you
reference the variables returnUrl, successUrl, cancelUrl, embedded,
sessionConfig and TRUSTED_FRONTEND_URL when implementing the change so no
user-provided URL can bypass the trusted origin.
🧹 Nitpick comments (3)
desci-server/Dockerfile.ci (1)

4-4: Consider adding --no-install-recommends to reduce image size.

build-essential in particular pulls a large set of recommended packages. Adding --no-install-recommends can significantly reduce the final image size and attack surface. Also consider cleaning up the apt cache in the same layer.

Proposed fix
-RUN apt-get -qy update && apt-get -qy install openssl curl socat jq dumb-init python3 build-essential
+RUN apt-get -qy update && apt-get -qy --no-install-recommends install openssl curl socat jq dumb-init python3 build-essential \
+    && rm -rf /var/lib/apt/lists/*
desci-server/Dockerfile (1)

5-5: Same --no-install-recommends suggestion as in Dockerfile.ci.

Proposed fix
-RUN apt-get -qy update && apt-get -qy install openssl curl socat jq python3 build-essential
+RUN apt-get -qy update && apt-get -qy --no-install-recommends install openssl curl socat jq python3 build-essential \
+    && rm -rf /var/lib/apt/lists/*
desci-server/src/controllers/stripe/subscription.ts (1)

16-24: TRUSTED_FRONTEND_URL is evaluated once at module load — env changes require a restart.

This is fine for typical deployments but worth noting. The nested ternary is a bit hard to scan; a lookup map would be clearer.

Optional: use a map for readability
-const TRUSTED_FRONTEND_URL =
-  process.env.FRONTEND_URL ||
-  (process.env.SERVER_URL === 'https://nodes-api.desci.com'
-    ? 'https://sciweave.com'
-    : process.env.SERVER_URL === 'https://nodes-api-dev.desci.com'
-      ? 'https://dev.sciweave.com'
-      : 'http://localhost:3000');
+const SERVER_TO_FRONTEND: Record<string, string> = {
+  'https://nodes-api.desci.com': 'https://sciweave.com',
+  'https://nodes-api-dev.desci.com': 'https://dev.sciweave.com',
+};
+
+const TRUSTED_FRONTEND_URL =
+  process.env.FRONTEND_URL ||
+  SERVER_TO_FRONTEND[process.env.SERVER_URL ?? ''] ||
+  'http://localhost:3000';

Comment on lines +71 to +79
// Use embedded mode or redirect mode based on request
// Use trusted frontend URL instead of req.headers.origin to prevent URL spoofing
if (embedded) {
sessionConfig.ui_mode = 'embedded';
sessionConfig.return_url = returnUrl || `${TRUSTED_FRONTEND_URL}/settings/subscription/complete?session_id={CHECKOUT_SESSION_ID}`;
} else {
sessionConfig.success_url = successUrl || `${TRUSTED_FRONTEND_URL}/settings/subscription?success=true`;
sessionConfig.cancel_url = cancelUrl || `${TRUSTED_FRONTEND_URL}/settings/subscription?canceled=true`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

User-supplied URLs bypass the trusted-URL protection.

The comment on Line 72 states the intent is to "prevent URL spoofing," but successUrl, cancelUrl, and returnUrl are accepted from the request body and used as-is when provided. An attacker can supply arbitrary redirect targets, which is the exact vector TRUSTED_FRONTEND_URL was introduced to mitigate.

Either remove the user-supplied URL overrides entirely (always use TRUSTED_FRONTEND_URL), or validate that any provided URLs match an allowlist (e.g., must start with TRUSTED_FRONTEND_URL).

Example: validate URLs against trusted origin
+    const isUrlTrusted = (url: string) => url.startsWith(TRUSTED_FRONTEND_URL);
+
     if (embedded) {
       sessionConfig.ui_mode = 'embedded';
-      sessionConfig.return_url = returnUrl || `${TRUSTED_FRONTEND_URL}/settings/subscription/complete?session_id={CHECKOUT_SESSION_ID}`;
+      const resolvedReturnUrl = returnUrl && isUrlTrusted(returnUrl) ? returnUrl : `${TRUSTED_FRONTEND_URL}/settings/subscription/complete?session_id={CHECKOUT_SESSION_ID}`;
+      sessionConfig.return_url = resolvedReturnUrl;
     } else {
-      sessionConfig.success_url = successUrl || `${TRUSTED_FRONTEND_URL}/settings/subscription?success=true`;
-      sessionConfig.cancel_url = cancelUrl || `${TRUSTED_FRONTEND_URL}/settings/subscription?canceled=true`;
+      sessionConfig.success_url = successUrl && isUrlTrusted(successUrl) ? successUrl : `${TRUSTED_FRONTEND_URL}/settings/subscription?success=true`;
+      sessionConfig.cancel_url = cancelUrl && isUrlTrusted(cancelUrl) ? cancelUrl : `${TRUSTED_FRONTEND_URL}/settings/subscription?canceled=true`;
     }
🤖 Prompt for AI Agents
In `@desci-server/src/controllers/stripe/subscription.ts` around lines 71 - 79,
The code accepts user-supplied returnUrl/successUrl/cancelUrl and uses them
directly, bypassing TRUSTED_FRONTEND_URL; update the session URL assignment in
the block that sets sessionConfig (the embedded branch and the else branch) to
either (1) always use TRUSTED_FRONTEND_URL and ignore
returnUrl/successUrl/cancelUrl, or (2) validate any provided
returnUrl/successUrl/cancelUrl against an allowlist rule (e.g., must startWith
TRUSTED_FRONTEND_URL) and only use them when they pass validation, otherwise
fall back to the TRUSTED_FRONTEND_URL defaults; ensure you reference the
variables returnUrl, successUrl, cancelUrl, embedded, sessionConfig and
TRUSTED_FRONTEND_URL when implementing the change so no user-provided URL can
bypass the trusted origin.

Comment on lines 232 to +234
const portalSession = await stripe.billingPortal.sessions.create({
customer: subscription.stripeCustomerId,
return_url: returnUrl || `${req.headers.origin}/settings/subscription`,
return_url: returnUrl || `${TRUSTED_FRONTEND_URL}/settings/subscription`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same URL validation concern applies to the portal returnUrl.

The user-supplied returnUrl is used directly here too. Apply the same trusted-origin validation as suggested above.

🤖 Prompt for AI Agents
In `@desci-server/src/controllers/stripe/subscription.ts` around lines 232 - 234,
The portal return URL uses the user-supplied returnUrl directly when creating
portalSession (stripe.billingPortal.sessions.create) — validate the origin the
same way as for the other URL: parse returnUrl, ensure its origin equals
TRUSTED_FRONTEND_URL (or matches allowed trusted origins), and only pass the
user returnUrl to stripe.billingPortal.sessions.create if it passes validation;
otherwise fall back to the trusted default
(`${TRUSTED_FRONTEND_URL}/settings/subscription`). Update the logic around
portalSession creation in subscription.ts to perform this origin check before
constructing the request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant