{"record":{"id":"4bac3f312a715bd1","repo":"paperclipai/paperclip","slug":"sign-in-again-to-connect-your-own-account","errorCode":null,"errorMessage":"Sign in again to connect your own account.","messagePattern":"Sign in again to connect your own account\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ui/src/pages/apps/AppDetail.tsx","lineNumber":327,"sourceCode":"        body: error instanceof Error ? error.message : \"Please try again.\",\n        tone: \"error\",\n      }),\n  });\n\n  const invalidateGrants = () => {\n    queryClient.invalidateQueries({ queryKey: queryKeys.tools.connectionGrants(connectionId) });\n    queryClient.invalidateQueries({ queryKey: queryKeys.tools.connection(connectionId) });\n  };\n\n  /**\n   * \"Connect as me\" and \"Reconnect\" for the signed-in user's own identity. The\n   * subject is always the caller — the server refuses any other subject — so\n   * there is no path here to start consent on a coworker's behalf.\n   */\n  const startPersonalAuth = useMutation({\n    mutationFn: () => {\n      const subjectUserId = grantsQuery.data?.currentUserId;\n      if (!subjectUserId) throw new Error(\"Sign in again to connect your own account.\");\n      return toolsApi.startPersonalAuthorization(selectedCompanyId!, connectionId, {\n        subjectUserId,\n        returnTo: appTabHref(connectionId, \"permissions\"),\n      });\n    },\n    onSuccess: async ({ url, handoff }) => {\n      try {\n        const target = await prepareOAuthNavigation({ authorizationUrl: url, handoff });\n        if (target.kind === \"reauthentication\" && handoff) {\n          savePendingCloudHandoff(handoff.session);\n        }\n        navigateTopLevel(target.url);\n      } catch (error) {\n        pushToast({\n          title: \"Couldn't start sign-in\",\n          body: error instanceof Error ? error.message : \"Please try again.\",\n          tone: \"error\",\n        });","sourceCodeStart":309,"sourceCodeEnd":345,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/ui/src/pages/apps/AppDetail.tsx#L309-L345","documentation":"This client-side guard in AppDetail.tsx throws when the user attempts to start a personal OAuth authorization for an Apps catalog connection before the grants query has resolved a currentUserId. The server requires the subject of a personal connection to be the calling user, so without a known user id there is no valid consent request to start. The message tells the user their session identity is missing and they should sign in again.","triggerScenarios":"Clicking the 'connect your own account' action while grantsQuery.data is undefined (query still loading) or its currentUserId field is absent/null — typically when the session expired, the grants endpoint returned an empty/error payload, or the mutation fires before the grants query finishes fetching.","commonSituations":"A stale or expired session token where the /grants endpoint no longer returns the current user; a slow network where the user clicks connect before grantsQuery resolves; an API error (401/403) swallowed into an undefined query result; switching companies where grants are refetched and momentarily empty.","solutions":["Ensure the user re-authenticates so the session is valid, then retry connecting — the error message's own remedy ('Sign in again').","Gate the connect button on grantsQuery.isSuccess && !!grantsQuery.data?.currentUserId so the mutation can only run once the subject id exists.","Inspect why the grants query returned no currentUserId (check network response, auth token, and company scoping) — fix the session/endpoint rather than just retrying.","If the query is merely in-flight, use its isPending state to disable or defer the action instead of letting the mutation throw.","Add an onError handler on the mutation to surface a friendly re-sign-in prompt/toast instead of an unhandled throw."],"exampleFix":"// before\nconst startPersonalAuth = useMutation({\n  mutationFn: () => {\n    const subjectUserId = grantsQuery.data?.currentUserId;\n    if (!subjectUserId) throw new Error(\"Sign in again to connect your own account.\");\n    return toolsApi.startPersonalAuthorization(selectedCompanyId!, connectionId, {\n      subjectUserId,\n      returnTo: appTabHref(connectionId, \"permissions\"),\n    });\n  },\n// after\nconst subjectUserId = grantsQuery.data?.currentUserId;\nconst canStartAuth = grantsQuery.isSuccess && !!subjectUserId;\n\nconst startPersonalAuth = useMutation({\n  mutationFn: () => {\n    return toolsApi.startPersonalAuthorization(selectedCompanyId!, connectionId, {\n      subjectUserId: subjectUserId!,\n      returnTo: appTabHref(connectionId, \"permissions\"),\n    });\n  },\n  onError: () => showToast(\"Your session expired — sign in again to connect your account.\"),\n// (button: disabled={!canStartAuth})","handlingStrategy":"validation","validationCode":"if (grantsQuery.isPending) return; // wait for grants to load\nconst subjectUserId = grantsQuery.data?.currentUserId;\nif (!subjectUserId) {\n  redirectToSignIn({ returnTo: appTabHref(connectionId, \"permissions\") });\n  return;\n}\nstartPersonalAuth.mutate();","typeGuard":"function hasSubjectUser(\n  g: { currentUserId?: string | null } | undefined\n): g is { currentUserId: string } {\n  return typeof g?.currentUserId === \"string\" && g.currentUserId.length > 0;\n}\n// usage: if (!hasSubjectUser(grantsQuery.data)) redirect to sign-in;","tryCatchPattern":"try {\n  const { url, handoff } = await startPersonalAuth.mutateAsync();\n  window.location.href = handoff ? url : url; // proceed with handoff\n} catch (e) {\n  if (e instanceof Error && e.message.includes(\"Sign in again\")) {\n    showToast(\"Your session expired. Please sign in and try connecting again.\");\n  } else {\n    showToast(\"Could not start authorization. Please try again.\");\n  }\n}","preventionTips":["Disable the connect action until grantsQuery.isSuccess and currentUserId is present.","Handle 401 responses from the grants endpoint globally by routing the user to re-authentication.","Show a loading state instead of leaving the connect button clickable while queries are in flight.","On company switch, clear in-flight mutation state and re-check the subject before allowing consent flows.","Write an activity/toast on auth-guard failures so users understand why the action did not proceed."],"tags":["auth","session-expired","oauth","react-query","client-side-guard"],"backgroundTag":"authentication-required","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}