Skip to content

feat(errors): translate infra-api blocked-team 403s into friendly text#325

Open
drankou wants to merge 1 commit into
mainfrom
feat/dashboard-team-blocked-error-ux
Open

feat(errors): translate infra-api blocked-team 403s into friendly text#325
drankou wants to merge 1 commit into
mainfrom
feat/dashboard-team-blocked-error-ux

Conversation

@drankou
Copy link
Copy Markdown
Contributor

@drankou drankou commented May 14, 2026

Pairs with the infra-api ActionIntent PR. infra-api returns 403 "team is blocked[: ]" for mutations a blocked team can't perform. The dashboard previously collapsed every 403 into "You are not authorized to access this resource", hiding the actual reason from the user.

This recognizes the wire format inside the central error adapter and translates it into the existing user-facing messages.

infra-api returns 403 with a 'team is blocked[: <reason>]' wire
format for mutations a blocked team isn't allowed to perform. The
dashboard previously collapsed every 403 into the generic 'You are not
authorized to access this resource' string, so users never saw the
actual reason (billing limit, verification required, missing payment
method).

This change recognizes the wire format inside the central server error
adapter and translates it into the user-facing strings the dashboard
already maintains via getBlockedMessage. Because TRPCError.message and
ActionError.message both flow through getPublicRepoErrorMessage, every
existing toast call site automatically picks up the friendly text — no
mass refactor needed.

What's added:
- TEAM_BLOCKED_MESSAGE_PREFIX constant + isTeamBlockedError and
  extractBlockedReason helpers in core/shared/errors.ts
- getPublicErrorMessage gains an optional 'message' field and branches
  on isTeamBlockedError before its existing code/status logic
- getPublicRepoErrorMessage forwards error.message so the new branch
  fires for repo-level 403s
- getBlockedReasonText slug-less variant in team-blocked-message; the
  existing getBlockedMessage delegates to it for the 'text' field
- 'billingLimit' added to TEAM_BLOCKED_REASONS for typeable contract
- 22 new unit tests covering detection, extraction, translation,
  status filtering, and the not-found/validation/conflict pass-through

Non-blocked-team 403s keep their existing obfuscated message.

Pairs with infra/packages/api per-endpoint enforcement work in
feat/blocked-team-intent-enforcement.
@cla-bot cla-bot Bot added the cla-signed label May 14, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented May 14, 2026

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

Project Deployment Actions Updated (UTC)
web Ready Ready Preview, Comment May 14, 2026 0:53am
web-juliett Ready Ready Preview, Comment May 14, 2026 0:53am

Request Review

@drankou drankou marked this pull request as ready for review May 14, 2026 15:18
@drankou drankou requested a review from ben-fornefeld as a code owner May 14, 2026 15:18
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0689634514

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/core/shared/errors.ts
message?: string | null
}): boolean {
if (input.status !== 403 || !input.message) return false
return input.message.toLowerCase().startsWith(TEAM_BLOCKED_MESSAGE_PREFIX)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require a delimiter before treating 403s as blocked-team

When a 403 message starts with the same words but is not the documented team is blocked[: <reason>] wire format, such as team is blocked from accessing this resource, this predicate still returns true. getPublicErrorMessage then calls extractBlockedReason and returns the raw suffix instead of the generic forbidden message, widening the blocked-team exception and potentially exposing backend 403 details; this should only match the exact bare prefix or a colon-delimited reason.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — focused error-translation change with comprehensive unit tests; existing callers that don't pass message continue to fall through to the generic forbidden message.

Extended reasoning...

Overview

This PR translates infra-api 403 team is blocked[: <reason>] responses into the existing friendly user-facing messages. Touches src/core/shared/errors.ts (adds isTeamBlockedError, extractBlockedReason, and threads message through getPublicErrorMessage/getPublicRepoErrorMessage), src/features/dashboard/team-blocked/team-blocked-message.ts (extracts getBlockedReasonText and refactors getBlockedMessage to use it), and the teams constants (adds billingLimit). Two test files cover the new behavior with ~25 cases total.

Security risks

None. The translation only exposes user-facing strings that infra-api already intended to be shown (billing/verification/payment reasons). The pattern-matching is anchored to a 403 + literal prefix, so it cannot accidentally surface other 403 bodies — non-matching 403s fall through to the generic forbidden message as before. No injection vector since the message is used as a returned string, not interpolated into HTML/SQL.

Level of scrutiny

Low. This is a narrow user-experience improvement in the central error adapter with explicit unit-test coverage of fall-through paths (non-403, missing message, unknown reason, etc.). The refactor of getBlockedMessage is behavior-preserving — it now delegates the text computation to getBlockedReasonText but produces identical {text, cta, href} outputs as before.

Other factors

The new dependency from core/shared/errors.ts into features/dashboard/team-blocked/team-blocked-message.ts reverses the usual features → core layering direction, but it's safe (no import cycle since the team-blocked module doesn't import errors). Callers like src/core/server/actions/utils.ts and src/core/server/functions/sandboxes/get-team-metrics.ts that don't pass message are unaffected because the parameter is optional.

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.

1 participant