Skip to content

fix(api): require token to view workspace invitation (GHSA-gf48-p6jp-cwc4)#9015

Open
sriramveeraghanta wants to merge 1 commit intopreviewfrom
fix/ghsa-gf48-p6jp-cwc4-invitation-token-leak
Open

fix(api): require token to view workspace invitation (GHSA-gf48-p6jp-cwc4)#9015
sriramveeraghanta wants to merge 1 commit intopreviewfrom
fix/ghsa-gf48-p6jp-cwc4-invitation-token-leak

Conversation

@sriramveeraghanta
Copy link
Copy Markdown
Member

@sriramveeraghanta sriramveeraghanta commented May 4, 2026

Summary

Closes the unauthenticated invitation token leak reported in GHSA-gf48-p6jp-cwc4.

GET /api/workspaces/<slug>/invitations/<pk>/join/ was registered with permission_classes = [AllowAny] and serialized WorkspaceMemberInvite with fields = "__all__", exposing the secret invitation token (plus an invite_link containing it) to any caller who could guess a workspace slug + invitation id. An attacker could iterate through invitation IDs and accept invites intended for other users.

Backend (apps/api/plane/app/views/workspace/invite.py)

  • WorkspaceJoinEndpoint.get now requires the ?token= from the email link, compared in constant time. Missing/mismatched/non-existent invites all return a uniform 403 so existence cannot be probed.
  • token and invite_link are stripped from the response as defense in depth, even for verified callers — they already have the token, so we don't echo it back.
  • POST behavior is unchanged.

Frontend

  • WorkspaceService.getWorkspaceInvitation(slug, id, token) now requires a token and forwards it as a query param.
  • /workspace-invitations page passes the token it already extracts from the URL.
  • AuthRoot reads token from the search params and threads it into AuthHeader as invitationToken. When absent, AuthHeader simply skips the invitation-context SWR call — pages without proof of access shouldn't render invitation data anyway.
  • A few pre-existing lint warnings in the touched files (no-shadow, always-return) were tidied up so the security commit could pass lint-staged without bypass.

Test plan

  • Unauthenticated GET /api/workspaces/<slug>/invitations/<pk>/join/ without ?token= returns 403 and no invitation data.
  • Same endpoint with a wrong token returns 403.
  • Same endpoint with the correct token returns the invitation without token or invite_link in the body.
  • Clicking an emailed invite link still loads the /workspace-invitations page and lets the invitee accept/reject.
  • Sign-in/sign-up via an invite link still shows the "Join {workspace}" header (token is now propagated through AuthRootAuthHeader).
  • Visiting /sign-in without invite params still works (no SWR call fires).

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Enhanced workspace invitation security with required token verification for all invitation access requests
    • Prevented accidental exposure of sensitive invitation data in API responses
    • Strengthened protection against timing-based validation attacks

…cwc4)

The unauthenticated GET /api/workspaces/<slug>/invitations/<pk>/join/
endpoint serialized the invitation with `fields = "__all__"`, exposing
the secret invitation `token` (and an `invite_link` containing it) to
any caller who could guess a workspace slug + invitation id.

Reject GET requests that don't supply the matching `?token=` from the
emailed invite link, comparing in constant time, and additionally strip
`token` and `invite_link` from the response as defense in depth.

Frontend callers (`/workspace-invitations` page, auth header) now thread
the token from the URL through `getWorkspaceInvitation`. Auth pages
visited without an invite token simply skip the invitation banner SWR
call — they have no proof of access and shouldn't see invitation data.
Copilot AI review requested due to automatic review settings May 4, 2026 19:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses GHSA-gf48-p6jp-cwc4 by preventing unauthenticated callers from retrieving workspace invitation secrets and metadata via the workspace invitation “join” GET endpoint, and updates the web client to supply the invitation token where needed.

Changes:

  • Backend: require ?token= for GET /api/workspaces/<slug>/invitations/<pk>/join/, compare in constant time, and return uniform 403 responses; remove token and invite_link from the successful response payload.
  • Frontend: update invitation fetch flows to require/forward the token (workspace invitation page + auth header invitation context).
  • Minor frontend lint-related refactors in touched components.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
apps/web/core/services/workspace.service.ts Requires a token when fetching workspace invitation details and forwards it as a query param.
apps/web/core/components/account/auth-forms/auth-root.tsx Reads token from URL params and passes it into AuthHeader for invite-context rendering.
apps/web/core/components/account/auth-forms/auth-header.tsx Gates SWR invitation fetch on presence of invitationToken and uses the token in the fetcher call.
apps/web/app/(all)/workspace-invitations/page.tsx Requires token to fetch invitation details and keeps accept/reject flows working with token present.
apps/api/plane/app/views/workspace/invite.py Enforces token validation for unauthenticated invitation detail retrieval and strips secret fields from the response.

Comment on lines 66 to +72
const { data: invitation, isLoading } = useSWR(
workspaceSlug && invitationId ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null,
async () => workspaceSlug && invitationId && workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId),
workspaceSlug && invitationId && invitationToken ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null,
async () =>
workspaceSlug &&
invitationId &&
invitationToken &&
workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId, invitationToken),
Comment on lines +42 to +43
invitation_id && slug && token && WORKSPACE_INVITATION(invitation_id.toString()),
invitation_id && slug && token
Comment thread apps/api/plane/app/views/workspace/invite.py
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

A security enhancement to workspace invitations that adds token-based validation. The backend now requires a token query parameter, validates it against stored invite tokens using constant-time comparison, and filters sensitive fields from responses. Frontend code is updated throughout the auth flow to extract and pass the token.

Changes

Workspace Invitation Token Validation

Layer / File(s) Summary
API Service Signature
apps/web/core/services/workspace.service.ts
getWorkspaceInvitation method extended to accept token: string and pass it as a query parameter in the invitation request.
Backend Validation
apps/api/plane/app/views/workspace/invite.py
WorkspaceJoinEndpoint.get now requires token query parameter, validates it against stored token using constant_time_compare, returns uniform 403 on missing/invalid token, and filters token and invite_link from response serializer data.
Component & Header Wiring
apps/web/core/components/account/auth-forms/auth-header.tsx, apps/web/core/components/account/auth-forms/auth-root.tsx
AuthHeader type extended with invitationToken prop; AuthRoot extracts invitation_token from query parameters and passes it down to AuthHeader for API requests.
Page Integration
apps/web/app/(all)/workspace-invitations/page.tsx
SWR fetcher condition updated to require token alongside invitation_id and slug; explicit return statements added after accept/reject navigation to prevent further promise handling.

Sequence Diagram

sequenceDiagram
    participant Page as Workspace Invitations Page
    participant AuthRoot as AuthRoot Component
    participant AuthHeader as AuthHeader Component
    participant Service as WorkspaceService
    participant API as Backend API Endpoint
    
    Page->>AuthRoot: Extract token from URL query params
    AuthRoot->>AuthHeader: Pass invitationToken prop
    AuthHeader->>AuthHeader: Build SWR key with token
    AuthHeader->>Service: getWorkspaceInvitation(slug, id, token)
    Service->>API: GET /invite with token query param
    API->>API: Fetch invitation record
    API->>API: Constant-time compare provided vs stored token
    alt Token Valid
        API->>API: Remove token & invite_link from response
        API->>Service: Return filtered invitation data
        Service->>AuthHeader: Invitation details
        AuthHeader->>Page: Render invitation UI
    else Token Invalid/Missing
        API->>Service: Return 403 Forbidden
        Service->>AuthHeader: Error state
        AuthHeader->>Page: Deny access
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A hop through tokens, secure and true,
Constant-time checking, we validate you,
Through auth flows they bounce, from page to API,
Guard the invites with cryptographic spree!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and specifically summarizes the main security fix: requiring token authentication for workspace invitation access to address GHSA-gf48-p6jp-cwc4.
Description check ✅ Passed The pull request description covers all required sections: a detailed summary of the security vulnerability, backend and frontend changes, and a comprehensive test plan with checkboxes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ghsa-gf48-p6jp-cwc4-invitation-token-leak

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
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

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

Copy link
Copy Markdown
Contributor

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/`(all)/workspace-invitations/page.tsx:
- Around line 41-45: The SWR cache key currently only includes invitation_id
(via WORKSPACE_INVITATION) but not token, so changing token won't cause a
refetch; update the useSWR key to include token (and slug if desired) so each
distinct token has its own cache entry — e.g., when calling useSWR ensure the
first arg contains token (for example an array or a key string that includes
token) and keep the same fetcher
workspaceService.getWorkspaceInvitation(slug.toString(),
invitation_id.toString(), token.toString()) so SWR will create separate entries
per token.

In `@apps/web/core/components/account/auth-forms/auth-header.tsx`:
- Around line 66-72: The SWR cache key used in useSWR (currently
`WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}`) omits invitationToken,
allowing stale/unauthorized responses to be reused; update the key to include
invitationToken (e.g.,
`WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}_${invitationToken}`) and
ensure the same workspaceSlug && invitationId && invitationToken condition
guards the hook so the cache differentiates requests by token for
workSpaceService.getWorkspaceInvitation.

In `@apps/web/core/services/workspace.service.ts`:
- Around line 172-180: getWorkspaceInvitation currently declares it returns
IWorkspaceMemberInvitation but the API response omits token and invite_link;
change the method's return type to reflect that by using Omit to remove those
keys (e.g. Promise<Omit<IWorkspaceMemberInvitation, 'token' | 'invite_link'>>),
update any related imports/signatures if needed, and ensure callers are adjusted
to the narrower shape so TypeScript strict mode is satisfied; keep the
getWorkspaceInvitation implementation unchanged aside from the updated return
type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34cee557-ff0c-4259-86c3-fcbddc0349f1

📥 Commits

Reviewing files that changed from the base of the PR and between 9491bdb and fbb96fc.

📒 Files selected for processing (5)
  • apps/api/plane/app/views/workspace/invite.py
  • apps/web/app/(all)/workspace-invitations/page.tsx
  • apps/web/core/components/account/auth-forms/auth-header.tsx
  • apps/web/core/components/account/auth-forms/auth-root.tsx
  • apps/web/core/services/workspace.service.ts

Comment on lines 41 to 45
const { data: invitationDetail, error } = useSWR(
invitation_id && slug && WORKSPACE_INVITATION(invitation_id.toString()),
invitation_id && slug
? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString())
invitation_id && slug && token && WORKSPACE_INVITATION(invitation_id.toString()),
invitation_id && slug && token
? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString(), token.toString())
: null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and read the relevant section
cat -n apps/web/app/\(all\)/workspace-invitations/page.tsx | sed -n '35,55p'

Repository: makeplane/plane

Length of output: 956


🏁 Script executed:

# Also check the structure of WORKSPACE_INVITATION function to understand what it returns
grep -n "WORKSPACE_INVITATION" apps/web/app/\(all\)/workspace-invitations/page.tsx -B 2 -A 2

Repository: makeplane/plane

Length of output: 604


🏁 Script executed:

# Search for WORKSPACE_INVITATION definition across the codebase
rg "WORKSPACE_INVITATION\s*=" --type ts --type tsx -A 3

Repository: makeplane/plane

Length of output: 87


🏁 Script executed:

# Search for WORKSPACE_INVITATION definition
rg "WORKSPACE_INVITATION" -A 3 | head -50

Repository: makeplane/plane

Length of output: 3581


🏁 Script executed:

# Check what the function returns
fd "fetch-keys" -type f | head -5

Repository: makeplane/plane

Length of output: 230


🏁 Script executed:

# Let's read the fetch-keys file to see WORKSPACE_INVITATION definition
find . -name "*fetch-keys*" -type f

Repository: makeplane/plane

Length of output: 99


🏁 Script executed:

# Let's check the fetch-keys file to see the full context
cat -n apps/web/core/constants/fetch-keys.ts | head -50

Repository: makeplane/plane

Length of output: 2168


🏁 Script executed:

# Check if there are other usages of SWR with token in cache keys
rg "useSWR.*token" -B 2 -A 3 | head -100

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Look at the auth-header.tsx example more closely
cat -n apps/web/core/components/account/auth-forms/auth-header.tsx | grep -A 10 -B 5 "WORKSPACE_INVITATION"

Repository: makeplane/plane

Length of output: 765


🏁 Script executed:

# Check if there are other invitation-related SWR calls with token parameters
rg "getWorkspaceInvitation|joinWorkspace" -B 5 -A 5 | head -80

Repository: makeplane/plane

Length of output: 5954


🏁 Script executed:

# Let's check if invitation_id is globally unique or workspace-specific by looking at the API
rg "invitations.*invitation_id" --type ts -B 5 | head -60

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Check SWR caching behavior and if there are comments about token-based caching
cat -n apps/web/app/\(all\)/workspace-invitations/page.tsx | sed -n '1,80p'

Repository: makeplane/plane

Length of output: 3194


🏁 Script executed:

# Let's also verify the WORKSPACE_INVITATION constant more thoroughly
rg "export const WORKSPACE_INVITATION" -A 2

Repository: makeplane/plane

Length of output: 350


🏁 Script executed:

# Check the workspace service getWorkspaceInvitation method signature
cat -n apps/web/core/services/workspace.service.ts | grep -A 15 "async getWorkspaceInvitation"

Repository: makeplane/plane

Length of output: 615


🏁 Script executed:

# Look at API endpoint to understand if token is user-specific or invitation-specific
rg "invitations.*join" -B 3 -A 8 | head -40

Repository: makeplane/plane

Length of output: 2927


🏁 Script executed:

# Check if there are other places in the codebase where token is included in SWR keys
rg "useSWR.*token.*\[" -B 1 -A 5 | head -60

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Check if there are any comments or documentation about SWR key design in the project
rg "SWR.*key|cache.*key" --type ts -B 2 -A 2 | head -50

Repository: makeplane/plane

Length of output: 1143


🏁 Script executed:

# Let's verify the exact behavior difference - check if slug is used in other similar patterns
rg "WORKSPACE_INVITATION\(" -B 3 -A 3

Repository: makeplane/plane

Length of output: 754


🏁 Script executed:

# Verify the diff format is correct for SWR array keys
rg "useSWR.*\[" -B 1 -A 3 | head -50

Repository: makeplane/plane

Length of output: 41


Include token in the SWR cache key.

The fetcher requires token as a parameter (sent as a query parameter to the API), but the cache key only includes invitation_id. If the page loads with a valid token, then the token is changed in the URL, SWR will still return the cached response from the first token instead of fetching with the new token. Each unique token should have a separate cache entry since it represents a different authorization context.

Suggested cache-key fix
   const { data: invitationDetail, error } = useSWR(
-    invitation_id && slug && token && WORKSPACE_INVITATION(invitation_id.toString()),
+    invitation_id && slug && token
+      ? [WORKSPACE_INVITATION(invitation_id.toString()), slug.toString(), token.toString()]
+      : null,
     invitation_id && slug && token
       ? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString(), token.toString())
       : null
   );
📝 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 { data: invitationDetail, error } = useSWR(
invitation_id && slug && WORKSPACE_INVITATION(invitation_id.toString()),
invitation_id && slug
? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString())
invitation_id && slug && token && WORKSPACE_INVITATION(invitation_id.toString()),
invitation_id && slug && token
? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString(), token.toString())
: null
const { data: invitationDetail, error } = useSWR(
invitation_id && slug && token
? [WORKSPACE_INVITATION(invitation_id.toString()), slug.toString(), token.toString()]
: null,
invitation_id && slug && token
? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString(), token.toString())
: null
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(all)/workspace-invitations/page.tsx around lines 41 - 45, The
SWR cache key currently only includes invitation_id (via WORKSPACE_INVITATION)
but not token, so changing token won't cause a refetch; update the useSWR key to
include token (and slug if desired) so each distinct token has its own cache
entry — e.g., when calling useSWR ensure the first arg contains token (for
example an array or a key string that includes token) and keep the same fetcher
workspaceService.getWorkspaceInvitation(slug.toString(),
invitation_id.toString(), token.toString()) so SWR will create separate entries
per token.

Comment on lines 66 to +72
const { data: invitation, isLoading } = useSWR(
workspaceSlug && invitationId ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null,
async () => workspaceSlug && invitationId && workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId),
workspaceSlug && invitationId && invitationToken ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null,
async () =>
workspaceSlug &&
invitationId &&
invitationToken &&
workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId, invitationToken),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/core/components/account/auth-forms/auth-header.tsx | head -80

Repository: makeplane/plane

Length of output: 3073


🏁 Script executed:

# Get the full extent of the useSWR call and understand the context
sed -n '60,80p' apps/web/core/components/account/auth-forms/auth-header.tsx

Repository: makeplane/plane

Length of output: 785


🏁 Script executed:

# Check if invitationToken is a query parameter in the URL
rg "invitationToken" apps/web/core/components/account/auth-forms/auth-header.tsx -A 2 -B 2

Repository: makeplane/plane

Length of output: 865


Include invitationToken in the SWR cache key.

The cache key only uses workspaceSlug and invitationId, but the API authorization requires all three parameters including invitationToken. This allows responses from one token to be reused when the same invitation is accessed with a different token within the same session, bypassing re-authorization.

Suggested cache-key fix
   const { data: invitation, isLoading } = useSWR(
-    workspaceSlug && invitationId && invitationToken ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null,
+    workspaceSlug && invitationId && invitationToken
+      ? ["WORKSPACE_INVITATION", workspaceSlug, invitationId, invitationToken]
+      : null,
     async () =>
       workspaceSlug &&
       invitationId &&
       invitationToken &&
       workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId, invitationToken),
📝 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 { data: invitation, isLoading } = useSWR(
workspaceSlug && invitationId ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null,
async () => workspaceSlug && invitationId && workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId),
workspaceSlug && invitationId && invitationToken ? `WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}` : null,
async () =>
workspaceSlug &&
invitationId &&
invitationToken &&
workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId, invitationToken),
const { data: invitation, isLoading } = useSWR(
workspaceSlug && invitationId && invitationToken
? ["WORKSPACE_INVITATION", workspaceSlug, invitationId, invitationToken]
: null,
async () =>
workspaceSlug &&
invitationId &&
invitationToken &&
workSpaceService.getWorkspaceInvitation(workspaceSlug, invitationId, invitationToken),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/components/account/auth-forms/auth-header.tsx` around lines 66
- 72, The SWR cache key used in useSWR (currently
`WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}`) omits invitationToken,
allowing stale/unauthorized responses to be reused; update the key to include
invitationToken (e.g.,
`WORKSPACE_INVITATION_${workspaceSlug}_${invitationId}_${invitationToken}`) and
ensure the same workspaceSlug && invitationId && invitationToken condition
guards the hook so the cache differentiates requests by token for
workSpaceService.getWorkspaceInvitation.

Comment on lines +172 to +180
async getWorkspaceInvitation(
workspaceSlug: string,
invitationId: string,
token: string
): Promise<IWorkspaceMemberInvitation> {
return this.get(`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/join/`, {
headers: {},
params: { token },
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Verify the method exists at the stated lines and get context
cat -n apps/web/core/services/workspace.service.ts | sed -n '165,185p'

Repository: makeplane/plane

Length of output: 803


🏁 Script executed:

# Check the type definition of IWorkspaceMemberInvitation
cat packages/types/src/workspace.ts | grep -A 20 "IWorkspaceMemberInvitation"

Repository: makeplane/plane

Length of output: 467


🏁 Script executed:

# Verify the backend endpoint mentioned in the review
cat apps/api/plane/app/views/workspace/invite.py | sed -n '255,275p'

Repository: makeplane/plane

Length of output: 757


🏁 Script executed:

# Check all imports in workspace.service.ts to see if types are properly imported
head -50 apps/web/core/services/workspace.service.ts

Repository: makeplane/plane

Length of output: 1396


🏁 Script executed:

# Find all usages of getWorkspaceInvitation
rg "getWorkspaceInvitation" --type ts --type tsx -B 2 -A 2

Repository: makeplane/plane

Length of output: 87


🏁 Script executed:

# Check if there's an existing public invitation type or similar pattern in workspace.ts
rg "Invitation|Invite" packages/types/src/workspace.ts -i

Repository: makeplane/plane

Length of output: 213


🏁 Script executed:

# Check the entire WorkspaceService to see if there are similar type mismatches
rg "Promise<I" apps/web/core/services/workspace.service.ts

Repository: makeplane/plane

Length of output: 1756


🏁 Script executed:

# Fix the file type search - use ts for TypeScript files
rg "getWorkspaceInvitation" -t ts -B 2 -A 5

Repository: makeplane/plane

Length of output: 6372


🏁 Script executed:

# Check if userWorkspaceInvitations and workspaceInvitations also have similar issues
rg "userWorkspaceInvitations|workspaceInvitations" apps/web/core/services/workspace.service.ts -B 2 -A 10

Repository: makeplane/plane

Length of output: 896


🏁 Script executed:

# Find where getWorkspaceInvitation is called to understand how the return value is used
rg "getWorkspaceInvitation" --type-list | head -5

Repository: makeplane/plane

Length of output: 449


🏁 Script executed:

# Search for files that call getWorkspaceInvitation
find apps/web -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "getWorkspaceInvitation" 2>/dev/null

Repository: makeplane/plane

Length of output: 346


🏁 Script executed:

# Check how the invitation data is used in workspace-invitations/page.tsx
sed -n '1,100p' apps/web/app/\(all\)/workspace-invitations/page.tsx | head -80

Repository: makeplane/plane

Length of output: 2634


🏁 Script executed:

# Check the full usage of invitationDetail in workspace-invitations/page.tsx
grep -A 20 "invitationDetail" apps/web/app/\(all\)/workspace-invitations/page.tsx | head -50

Repository: makeplane/plane

Length of output: 1832


🏁 Script executed:

# Check how the invitation data is used in auth-header.tsx
grep -B 5 -A 20 "getWorkspaceInvitation" apps/web/core/components/account/auth-forms/auth-header.tsx

Repository: makeplane/plane

Length of output: 1026


🏁 Script executed:

# Check the backend endpoints for userWorkspaceInvitations and workspaceInvitations
# to see if they also strip token and invite_link
rg "token.*invite_link|invite_link.*token" apps/api/plane/app/views/workspace/invite.py -B 5 -A 5

Repository: makeplane/plane

Length of output: 401


🏁 Script executed:

# Check other invitation-related endpoints to understand the pattern
grep -n "def.*invitation" apps/api/plane/app/views/workspace/invite.py -i

Repository: makeplane/plane

Length of output: 41


Narrow this method's return type to match the public invite shape.

getWorkspaceInvitation returns IWorkspaceMemberInvitation, but the backend endpoint strips token and invite_link from the response. This breaks the type contract—callers compile against fields that are no longer returned. Update the return type using Omit to reflect the actual response shape:

Suggested fix
+type TPublicWorkspaceMemberInvitation = Omit<IWorkspaceMemberInvitation, "token" | "invite_link">;
+
   async getWorkspaceInvitation(
     workspaceSlug: string,
     invitationId: string,
     token: string
-  ): Promise<IWorkspaceMemberInvitation> {
+  ): Promise<TPublicWorkspaceMemberInvitation> {
     return this.get(`/api/workspaces/${workspaceSlug}/invitations/${invitationId}/join/`, {
       headers: {},
       params: { token },
     })

Per coding guidelines: "Enable TypeScript strict mode and ensure all files must be typed."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/core/services/workspace.service.ts` around lines 172 - 180,
getWorkspaceInvitation currently declares it returns IWorkspaceMemberInvitation
but the API response omits token and invite_link; change the method's return
type to reflect that by using Omit to remove those keys (e.g.
Promise<Omit<IWorkspaceMemberInvitation, 'token' | 'invite_link'>>), update any
related imports/signatures if needed, and ensure callers are adjusted to the
narrower shape so TypeScript strict mode is satisfied; keep the
getWorkspaceInvitation implementation unchanged aside from the updated return
type.

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.

2 participants