Skip to content

[comp] Production Deploy#2510

Merged
Marfuen merged 19 commits intoreleasefrom
main
Apr 10, 2026
Merged

[comp] Production Deploy#2510
Marfuen merged 19 commits intoreleasefrom
main

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

This is an automated pull request to release the candidate branch into production, which will trigger a deployment.
It was created by the [Production PR] action.

tofikwest and others added 12 commits April 10, 2026 13:18
The initializeOrganization transaction runs 20+ DB operations (controls,
policies, tasks, versions, requirement maps) and was hitting Prisma's
default 5s timeout for users selecting multiple frameworks.

- Set global transaction timeout to 30s across all 5 Prisma client instances
- Clean up partially created org on failure to prevent orphans on retry
- Surface actual error messages instead of generic "Failed to create organization"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address Bugbot review: if setActiveOrganization succeeded but a later
step (revalidatePath) threw, the cleanup would delete a fully initialized
org while the session still referenced it. Now cleanup is disabled after
activation, and revalidatePath errors are caught separately since they
are non-critical.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
isOnboarding is true during the server action but wasn't used in the
disabled prop — only isSubmitting (react-hook-form) was, which resets
after the synchronous onSubmit handler. This allowed double-clicks to
create duplicate orgs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…to previous org

When users create an additional org via create-additional flow, they get
trapped in the onboarding funnel with no way to go back. This adds a
Cancel button (visible only when user has other completed orgs) to:

- Pre-payment setup form: navigates back to root (no org to delete yet)
- Upgrade page: deletes incomplete org, switches to previous org
- Post-payment onboarding: deletes incomplete org, switches to previous org

Includes confirmation step before deletion to prevent accidental cancels.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…before delete

- Reject cancel on orgs with onboardingCompleted=true
- Switch activeOrganization BEFORE deleting so session never references
  a deleted org (prevents dangling session on slow client redirect)
- Fail cancel if org switch fails rather than leaving orphaned state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Don't expose raw Prisma/DB error messages (like constraint violations or
connection details) in user-facing toasts. Log the raw error to console
for debugging, show a generic user-friendly message in the toast.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refuse to delete org if no fallback org exists server-side. Prevents
race condition where other orgs are removed between page render (which
checks hasOtherOrgs) and action execution, which would leave the
session pointing at a deleted org.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If setActiveOrganization succeeds but organization.delete fails, roll
back the active org to the original one so the session stays consistent
with what the user sees on screen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add List-Unsubscribe headers and throttle email sends

- Add List-Unsubscribe and List-Unsubscribe-Post headers to all
  outbound emails for Gmail/RFC 8058 one-click unsubscribe compliance
- Reduce email queue concurrency from 30 to 10
- Add 1s delay between sends to avoid email spikes that trigger
  reputation systems

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use setTimeout instead of wait.for for email throttling

wait.for suspends execution and frees the concurrency slot,
defeating the throttling purpose. setTimeout holds the slot
occupied for 1s, actually spacing out sends.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use real recipient email for unsubscribe URL, not test override

When RESEND_TO_TEST is set, toAddress becomes the test email.
The unsubscribe URL should always reference the real recipient
(params.to) so the token validates correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove List-Unsubscribe-Post, add mailto fallback

The one-click POST handler doesn't exist yet (unsubscribe page is
GET only). Removed List-Unsubscribe-Post to avoid claiming RFC 8058
support we don't have. Added mailto fallback for broader client
compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add RFC 8058 one-click unsubscribe POST endpoint

- New POST /v1/email/unsubscribe endpoint that accepts email+token
  via query params, verifies HMAC token, and unsubscribes the user
- No auth required (token IS the auth, Gmail needs to POST directly)
- Re-add List-Unsubscribe-Post header now that the handler exists
- List-Unsubscribe URL points to API endpoint for one-click POST

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove dead import, use timing-safe token comparison

- Remove unused getUnsubscribeUrl import from send-email.ts
- Use crypto.timingSafeEqual for HMAC token verification in
  unsubscribe endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: guard against type confusion on query/body params

CodeQL flagged that query params could be arrays. Explicitly
coerce to string before using.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: include findingNotifications in unsubscribe preferences

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cursor
Copy link
Copy Markdown

cursor bot commented Apr 10, 2026

PR Summary

Medium Risk
Touches user lifecycle and notification preferences by adding org-deletion during onboarding cancel and a public one-click unsubscribe endpoint; mistakes could lead to accidental data loss or unintended unsubscribes. Also adjusts Prisma transaction timeouts and email send throttling, which can impact production performance/throughput.

Overview
Adds RFC 8058 one-click unsubscribe support by emitting List-Unsubscribe headers on outbound emails and introducing a new POST /v1/email/unsubscribe endpoint that validates an HMAC token and disables all user email notifications.

Improves onboarding resilience and escape hatches: minimal org creation now stores both framework display names and raw frameworkIds, cleans up partially-created orgs on failure, and completeOnboarding can recover missing framework initialization + upsert the onboarding record before triggering jobs. UI adds a guarded Cancel onboarding action (only when the user has another completed org), which switches the active org then deletes the incomplete org.

Operational tweaks: sets Prisma transactionOptions.timeout to 30s across apps, and reduces email send queue concurrency with an added 1s throttle to space out sends.

Reviewed by Cursor Bugbot for commit 082501f. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app (staging) Ready Ready Preview, Comment Apr 10, 2026 8:38pm
comp-framework-editor Ready Ready Preview, Comment Apr 10, 2026 8:38pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
portal (staging) Skipped Skipped Apr 10, 2026 8:38pm

Request Review

tofikwest and others added 2 commits April 10, 2026 15:53
Deep path import @trycompai/email/lib/unsubscribe resolves to
source .ts files which Trigger's esbuild can't find in dist/.
Use barrel import from @trycompai/email instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
const expectedToken = generateUnsubscribeToken(email);
const tokensMatch =
expectedToken.length === token.length &&
timingSafeEqual(Buffer.from(expectedToken), Buffer.from(token));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

String length guard doesn't prevent timingSafeEqual buffer mismatch

Low Severity

The guard expectedToken.length === token.length compares JavaScript string lengths, but timingSafeEqual compares buffer byte lengths. If an attacker submits a token containing multi-byte UTF-8 characters whose JS string length matches the expected token's length (43 chars for SHA-256 base64url), the string-length check passes, but Buffer.from(token) produces more bytes than Buffer.from(expectedToken), causing timingSafeEqual to throw an unhandled RangeError. This results in a 500 instead of a 400.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 80db5d9. Configure here.

tofikwest and others added 2 commits April 10, 2026 15:57
…-flight

Prevents race between org deletion and completeOnboarding by hiding the
cancel button when isOnboarding or isFinalizing is true.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: use barrel import for email package (Trigger build fix)
@vercel vercel bot temporarily deployed to staging – portal April 10, 2026 20:10 Inactive
@vercel vercel bot temporarily deployed to staging – app April 10, 2026 20:10 Inactive
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0ec7eed. Configure here.

try {
await db.organization.delete({
where: { id: parsedInput.organizationId },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cancel action can delete completed org via race

Medium Severity

The onboardingCompleted check at line 45 and the db.organization.delete at line 86 are not atomic. If a concurrent request (e.g., a background job or another browser tab) sets onboardingCompleted: true between the check and the delete, a fully completed organization with all its production data gets cascade-deleted. The delete WHERE clause only uses id and does not re-verify onboardingCompleted: false.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0ec7eed. Configure here.

<CancelOnboardingButton
organizationId={organization.id}
hasOtherOrgs={hasOtherOrgs && !isOnboarding && !isFinalizing}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dynamic prop can unmount cancel button mid-action

Low Severity

Passing hasOtherOrgs={hasOtherOrgs && !isOnboarding && !isFinalizing} causes CancelOnboardingButton to unmount when isOnboarding or isFinalizing flip to true. If the cancel server action is already in-flight, the component unmounts, the onSuccess callback with window.location.assign may not fire, and the user gets stranded on a page for a now-deleted organization.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0ec7eed. Configure here.

#2512)

* fix(onboarding): add initialize-organization trigger task and recovery guard

If createOrganizationMinimal partially fails (org created but
initializeOrganization doesn't run), the org ends up with no framework
instances, controls, policies, or tasks. completeOnboarding now detects
this and runs initializeOrganization as recovery before triggering the
onboard job. A standalone Trigger.dev task allows manual re-runs from
the dashboard for orgs already in this broken state.

Also saves raw framework IDs to context for reliable recovery lookups
and upserts the onboarding record in case that was also missing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(onboarding): extract resolveFrameworkIds to shared helper

Deduplicates the resolveFrameworkIds logic that was copied in both
complete-onboarding.ts and initialize-organization.ts. Now lives in
actions/organization/lib/resolve-framework-ids.ts alongside
initialize-organization.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel vercel bot temporarily deployed to staging – portal April 10, 2026 20:36 Inactive
@Marfuen Marfuen merged commit 0958dbf into release Apr 10, 2026
14 checks passed
@claudfuen
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.21.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants