paperclipai/paperclip · error
Sign in again to connect your own account.
Error message
Sign in again to connect your own account.
What it means
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.
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.
Example fix
// before
const startPersonalAuth = useMutation({
mutationFn: () => {
const subjectUserId = grantsQuery.data?.currentUserId;
if (!subjectUserId) throw new Error("Sign in again to connect your own account.");
return toolsApi.startPersonalAuthorization(selectedCompanyId!, connectionId, {
subjectUserId,
returnTo: appTabHref(connectionId, "permissions"),
});
},
// after
const subjectUserId = grantsQuery.data?.currentUserId;
const canStartAuth = grantsQuery.isSuccess && !!subjectUserId;
const startPersonalAuth = useMutation({
mutationFn: () => {
return toolsApi.startPersonalAuthorization(selectedCompanyId!, connectionId, {
subjectUserId: subjectUserId!,
returnTo: appTabHref(connectionId, "permissions"),
});
},
onError: () => showToast("Your session expired — sign in again to connect your account."),
// (button: disabled={!canStartAuth}) Defensive patterns
Strategy: validation
Validate before calling
if (grantsQuery.isPending) return; // wait for grants to load
const subjectUserId = grantsQuery.data?.currentUserId;
if (!subjectUserId) {
redirectToSignIn({ returnTo: appTabHref(connectionId, "permissions") });
return;
}
startPersonalAuth.mutate(); Type guard
function hasSubjectUser(
g: { currentUserId?: string | null } | undefined
): g is { currentUserId: string } {
return typeof g?.currentUserId === "string" && g.currentUserId.length > 0;
}
// usage: if (!hasSubjectUser(grantsQuery.data)) redirect to sign-in; Try / catch
try {
const { url, handoff } = await startPersonalAuth.mutateAsync();
window.location.href = handoff ? url : url; // proceed with handoff
} catch (e) {
if (e instanceof Error && e.message.includes("Sign in again")) {
showToast("Your session expired. Please sign in and try connecting again.");
} else {
showToast("Could not start authorization. Please try again.");
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- device-login credential promotion rejected
- expired
- Agent not found
- / : oauth requires ownershipModes
- Cloud control assertion has already been used
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-10).
Data as JSON: /api/errors/4bac3f312a715bd1.
Report an issue: GitHub.
Appendix: source
Thrown at ui/src/pages/apps/AppDetail.tsx:337
body: error instanceof Error ? error.message : "Please try again.",
tone: "error",
}),
});
const invalidateGrants = () => {
queryClient.invalidateQueries({ queryKey: queryKeys.tools.connectionGrants(connectionId) });
queryClient.invalidateQueries({ queryKey: queryKeys.tools.connection(connectionId) });
};
/**
* "Connect as me" and "Reconnect" for the signed-in user's own identity. The
* subject is always the caller — the server refuses any other subject — so
* there is no path here to start consent on a coworker's behalf.
*/
const startPersonalAuth = useMutation({
mutationFn: () => {
const subjectUserId = grantsQuery.data?.currentUserId;
if (!subjectUserId) throw new Error("Sign in again to connect your own account.");
return toolsApi.startPersonalAuthorization(selectedCompanyId!, connectionId, {
subjectUserId,
returnTo: appTabHref(connectionId, "permissions"),
});
},
onSuccess: async ({ url, handoff }) => {
try {
const target = await prepareOAuthNavigation({ authorizationUrl: url, handoff });
if (target.kind === "reauthentication" && handoff) {
savePendingCloudHandoff(handoff.session);
}
navigateTopLevel(target.url);
} catch (error) {
pushToast({
title: "Couldn't start sign-in",
body: error instanceof Error ? error.message : "Please try again.",
tone: "error",
});View on GitHub (pinned to 3f1d897a7c)