{"record":{"id":"93170802e76acbf7","repo":"paperclipai/paperclip","slug":"invalid-handoff","errorCode":"invalid_handoff","errorMessage":"Paperclip Cloud returned an invalid sign-in handoff.","messagePattern":"Paperclip Cloud returned an invalid sign-in handoff\\.","errorType":"exception","errorClass":"OAuthHandoffError","httpStatus":null,"severity":"error","filePath":"ui/src/lib/oauthHandoff.ts","lineNumber":37,"sourceCode":"\nexport class OAuthHandoffError extends Error {\n  constructor(\n    message: string,\n    readonly code:\n      | \"invalid_handoff\"\n      | \"expired\"\n      | \"forbidden\"\n      | \"unavailable\",\n  ) {\n    super(message);\n    this.name = \"OAuthHandoffError\";\n  }\n}\n\nfunction parseHandoff(value: unknown): { kind: \"paperclip_cloud\"; session: string } | null {\n  if (value === undefined) return null;\n  if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n    throw new OAuthHandoffError(\"Paperclip Cloud returned an invalid sign-in handoff.\", \"invalid_handoff\");\n  }\n  const handoff = value as Record<string, unknown>;\n  if (\n    handoff.kind !== \"paperclip_cloud\"\n    || typeof handoff.session !== \"string\"\n    || handoff.session.length < 16\n    || handoff.session.length > 512\n    || !/^[A-Za-z0-9_-]+$/.test(handoff.session)\n  ) {\n    throw new OAuthHandoffError(\"Paperclip Cloud returned an invalid sign-in handoff.\", \"invalid_handoff\");\n  }\n  return { kind: \"paperclip_cloud\", session: handoff.session };\n}\n\nfunction handoffFailure(status: number, code: unknown): OAuthHandoffError {\n  if (status === 404 || code === \"SESSION_NOT_AVAILABLE\") {\n    return new OAuthHandoffError(\"This sign-in expired. Start the connection again.\", \"expired\");\n  }","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/ui/src/lib/oauthHandoff.ts#L19-L55","documentation":"parseHandoff validates the shape of a Paperclip Cloud sign-in handoff object. If the value is present but not a plain object (null, non-object, or array), it throws OAuthHandoffError with code \"invalid_handoff\" because the handoff payload from the server (or storage) is structurally corrupt and cannot be used to resume sign-in.","triggerScenarios":"Calling parseHandoff (via prepareOAuthNavigation or readPendingCloudHandoff) with `start.handoff` or a stored pending-handoff value that is null, an array, or a primitive instead of an object.","commonSituations":"Backend bug returning handoff as a string; sessionStorage corruption or schema change leaving non-object JSON; API version mismatch where handoff shape changed.","solutions":["Log/inspect the raw handoff value returned by the start endpoint and fix the producer to return `{ kind: \"paperclip_cloud\", session }`.","Clear stale sessionStorage entries (the pending handoff key) and restart sign-in.","Wrap parseHandoff callers in try/catch for OAuthHandoffError and fall back to a fresh sign-in."],"exampleFix":"// before\nconst handoff = JSON.parse(raw); // could be anything\nawait prepareOAuthNavigation({ handoff, authorizationUrl });\n// after\nlet parsed: unknown;\ntry { parsed = JSON.parse(raw); } catch { parsed = null; }\nif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n  await prepareOAuthNavigation({ handoff: parsed, authorizationUrl });\n} else {\n  sessionStorage.removeItem(PENDING_HANDOFF_KEY); // start fresh\n}","handlingStrategy":"type-guard","validationCode":"function looksLikeHandoff(v: unknown): boolean {\n  return !!v && typeof v === \"object\" && !Array.isArray(v);\n}","typeGuard":"function isCloudHandoff(v: unknown): v is { kind: \"paperclip_cloud\"; session: string } {\n  return !!v && typeof v === \"object\" && !Array.isArray(v)\n    && (v as any).kind === \"paperclip_cloud\"\n    && typeof (v as any).session === \"string\";\n}","tryCatchPattern":"try {\n  const handoff = readPendingCloudHandoff(sessionStorage);\n} catch (e) {\n  if (e instanceof OAuthHandoffError && e.code === \"invalid_handoff\") {\n    sessionStorage.removeItem(PENDING_HANDOFF_KEY); // discard corrupt payload\n  } else throw e;\n}","preventionTips":["Validate the handoff shape at the API boundary before storing or forwarding it.","Version the sessionStorage payload and drop entries with unknown shapes.","Add contract tests asserting the start endpoint returns `{ kind, session }`."],"tags":["oauth","validation","shape-mismatch"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}