Skip to content

OUT-3679: drop token prop from client tree, enforce live-token reads#1203

Open
priosshrsth wants to merge 1 commit intoanit/out-3679-update-to-latest-sdk-task-appfrom
anit/out-3679-drop-token-props
Open

OUT-3679: drop token prop from client tree, enforce live-token reads#1203
priosshrsth wants to merge 1 commit intoanit/out-3679-update-to-latest-sdk-task-appfrom
anit/out-3679-drop-token-props

Conversation

@priosshrsth
Copy link
Copy Markdown
Collaborator

Summary

  • Removes the token prop from every client-side component so the live token from useTokenRefresh is the single source of truth for client calls.
  • createUploadFn now takes a token getter and resolves it at upload time, so closure-captured uploadFn references no longer go stale across the 5-min token rotation.
  • Server fetchers (AllTasksFetcher, WorkflowStateFetcher, AssigneeFetcher, TemplatesFetcher, ValidateNotificationCountFetcher) keep token as a prop — they run at SSR with a fresh URL searchParam.
  • The three client-side _fetchers/* (TaskDataFetcher, OneTaskDataFetcher, OneTemplateDataFetcher) now use stable SWR keys; fetcher injects the live token at fetch time.

What this is built on

This stacks on the previous PR which introduced assemblyTokenStore, useTokenRefresh, and the live-token-injection in fetcher. With those primitives in place, this PR is the cleanup pass that removes prop drilling.

Files of interest

  • src/utils/createUploadFn.tstoken: stringtoken: () => string | undefined
  • src/components/cards/TaskCard.tsx — drops Redux token selector read; uses requireLiveToken() for assignee/workflow/due-date mutations
  • All *AppBridge*, Subtasks, ActivityWrapper, TaskEditor, TaskBoard, comment/reply cluster — drop the prop
  • Page-level files ((home), client, detail/[task_id], manage-templates) — stop forwarding token to client components but still pass it to server fetchers and ClientSideStateUpdate

Test plan

  • Open the app, idle past 5 min, drag a task — should succeed (no 401)
  • Post / delete a comment, post a reply — no 401
  • Toggle archive on a task — no 401
  • Upload an attachment to a comment after token has rotated — should use the live token
  • Realtime task / template update from another session — link followed in the iframe still authenticates
  • Navigate via breadcrumbs / CustomLink after rotation — destination page authenticates

Out of scope (follow-up)

The inline 'use server' action closures in page.tsx files (e.g. updateTaskDetail, deleteTask, postAttachment, editTemplate, ...) still close over the SSR-time token. They will stale at minute 6 when invoked from the client. Fixing them requires either (a) adding token as the first arg of each inline action and having callers pass requireLiveToken(), or (b) moving them to module-scope 'use server' files that read token from request headers. Recommend a separate PR.

🤖 Generated with Claude Code

…eads

Removes the token prop from every client-side component so the live
token from useTokenRefresh becomes the single source of truth. Server
fetchers still receive token via prop (they run during SSR with a fresh
URL searchParam), and ClientSideStateUpdate keeps it as the seed input.

Highlights:
- createUploadFn now takes a token getter and resolves it at upload
  time, so closure-captured uploadFn references no longer go stale
- TaskBoard, ActivityWrapper, Subtasks, TaskEditor, CommentCard,
  ReplyCard, CommentInput, ReplyInput, TaskCard, RealtimeTemplates,
  TaskBoardAppBridge, ManageTemplatesAppBridge, HeaderBreadcrumbs,
  DeletedRedirectPage, etc. all drop the token prop
- TaskDataFetcher / OneTaskDataFetcher / OneTemplateDataFetcher are
  client fetchers with stable SWR keys; the fetcher injects the live
  token at request time
- TaskCard reads workflowState change / assignee change tokens via
  requireLiveToken() instead of Redux selector

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 4, 2026

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 4, 2026

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

Project Deployment Actions Updated (UTC)
tasks-app Ready Ready Preview, Comment May 4, 2026 9:01am

Request Review

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR removes the token prop from client-side components across the tree, replacing closed-over token values with live reads via getLiveToken() / requireLiveToken(), and changes createUploadFn to accept a token getter so uploads always use the freshest token. The approach is architecturally sound — useTokenRefresh writes both to the module store and to Redux on each rotation, ensuring components re-render and pick up fresh values.

Two P1 issues need attention before merge:

  • DeletedRedirectPage replaces getLiveToken() ?? z.string().parse(token) with z.string().parse(getLiveToken()), removing the SSR-time fallback. If getLiveToken() returns undefined at render time (token store not yet seeded), Zod throws during render, crashing the page.
  • TemplateBoard's template card links call getLiveToken() at render time without a guard; a undefined return on first paint produces a navigation link with a missing token.

Confidence Score: 3/5

Not safe to merge as-is — two P1 issues can cause render crashes and broken navigation links on first page load before the token store is seeded.

Two P1 findings: a guaranteed ZodError crash in DeletedRedirectPage if the token store is not seeded at render time, and a similar first-paint issue in TemplateBoard. These are direct regressions introduced by removing the prop-based fallback without adding a null guard.

src/components/layouts/DeletedRedirectPage.tsx and src/app/manage-templates/ui/TemplateBoard.tsx

Important Files Changed

Filename Overview
src/utils/createUploadFn.ts Correctly changes token field from a string to a getter function `() => string
src/components/layouts/DeletedRedirectPage.tsx Removes token prop fallback; z.string().parse(getLiveToken()) will throw a ZodError during render if the token store is not yet seeded — regression from the previous getLiveToken() ?? z.string().parse(token) guard.
src/app/manage-templates/ui/TemplateBoard.tsx Drops token from Redux selector; getLiveToken() in the link href may produce undefined on first paint before the token store is seeded.
src/components/layouts/HeaderBreadcrumbs.tsx Drops token prop; uses getLiveToken() in link query. Safe after first Redux dispatch from useTokenRefresh, but may briefly render with undefined token on first paint.
src/app/ui/VirtualizedTasksLists.tsx Reads getLiveToken() at render time for task card href; safe because useTokenRefresh dispatches setToken to Redux on rotation, triggering re-renders that refresh the captured value.
src/app/_fetchers/TaskDataFetcher.tsx Moves to stable SWR key (no token in key); adds missing 'use client' directive; revalidateOnMount: false + fallbackData prevents eager fetch, consistent with prior behaviour.
src/components/cards/TaskCard.tsx Drops Redux token selector; correctly uses requireLiveToken() for all mutation call sites (assignee, workflow state, due date, subtask link). Clean.
src/utils/assemblyTokenStore.ts Module-level live token store with getLiveToken (nullable), requireLiveToken (throws), and setLiveToken (writer). Well-documented semantics.

Sequence Diagram

sequenceDiagram
    participant Layout as Layout (SSR)
    participant uTR as useTokenRefresh
    participant Store as assemblyTokenStore (module var)
    participant Redux as Redux taskBoardSlice
    participant Bridge as AssemblyBridge
    participant Comp as Client Component (e.g. DeletedRedirectPage)

    Layout->>uTR: mount with initialToken?
    alt initialToken provided
        uTR->>Store: setLiveToken(initialToken) [lazy useState — synchronous]
    end
    uTR->>Bridge: getCurrent() + onTokenUpdate()
    Bridge-->>uTR: token (async, via postMessage)
    uTR->>Store: setLiveToken(next)
    uTR->>Redux: dispatch(setToken(next))
    Redux-->>Comp: selector change — re-render
    Comp->>Store: getLiveToken() — fresh token

    Note over Comp,Store: If initialToken is absent AND first render fires before Bridge callback, getLiveToken() returns undefined — crash
Loading

Reviews (1): Last reviewed commit: "fix(OUT-3679): drop token prop from clie..." | Re-trigger Greptile

entity?: 'Task' | 'Template'
}) => {
const tokenstring = getLiveToken() ?? z.string().parse(token)
const tokenstring = z.string().parse(getLiveToken())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Uncaught ZodError if token store not yet seeded

z.string().parse(getLiveToken()) is called during the render body. getLiveToken() returns string | undefined; if the Assembly bridge's useEffect subscription hasn't delivered the first token yet (or if useTokenRefresh was called without an initialToken), liveToken is undefined. Zod will throw ZodError: Expected string, received undefined during render, crashing this page.

The previous code guarded against this with getLiveToken() ?? z.string().parse(token) — the SSR-time prop was the fallback. Removing both the fallback prop and the ?? guard turns a recoverable situation into an unhandled render crash.

entity?: 'Task' | 'Template'
}) => {
const tokenstring = getLiveToken() ?? z.string().parse(token)
const tokenstring = z.string().parse(getLiveToken())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 getLiveToken() can return undefined on first render if the token store hasn't been seeded yet (e.g., useTokenRefresh was mounted without an initialToken and the effect hasn't fired). Falling back to an empty string keeps the Link intact as a no-op while avoiding a ZodError crash; the token will be present in the URL once the component re-renders after setToken is dispatched to Redux.

Suggested change
const tokenstring = z.string().parse(getLiveToken())
const tokenstring = getLiveToken() ?? ''

href={{
pathname: getCardHrefTemplate(template),
query: { token },
query: { token: getLiveToken() },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 getLiveToken() computed at render may be undefined on first paint

getLiveToken() returns string | undefined. If this component renders before useTokenRefresh's useEffect has fired (the initial hydration pass when useTokenRefresh was not passed initialToken), the live token is still unset and getLiveToken() returns undefined. The resulting link query string will carry undefined as the token value, sending the user to a page that fails authentication.

useTokenRefresh dispatches setToken to Redux on each rotation (which triggers re-renders and refreshes the link), but the very first render before that dispatch is the risky window.

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