{"record":{"id":"09c63bfcaf06b7a3","repo":"different-ai/openwork","slug":"invalid-github-install-state","errorCode":"invalid_github_install_state","errorMessage":"GitHub install state is invalid or expired.","messagePattern":"GitHub install state is invalid or expired\\.","errorType":"http","errorClass":"PluginArchRouteFailure","httpStatus":400,"severity":"warning","filePath":"ee/apps/den-api/src/routes/org/plugin-system/store.ts","lineNumber":4342,"sourceCode":"\n  return { enqueuedCount }\n}\n\nfunction githubConnectorAppConfig() {\n  try {\n    return getGithubConnectorAppConfig(env.githubConnectorApp)\n  } catch (error) {\n    if (error instanceof GithubConnectorConfigError) {\n      throw new PluginArchRouteFailure(409, \"github_connector_app_not_configured\", error.message)\n    }\n    throw error\n  }\n}\n\nexport function consumeGithubInstallState(state: string) {\n  const parsed = verifyGithubInstallStateToken({ secret: env.betterAuthSecret, token: state })\n  if (!parsed) {\n    throw new PluginArchRouteFailure(400, \"invalid_github_install_state\", \"GitHub install state is invalid or expired.\")\n  }\n  return parsed\n}\n\nfunction wrapGithubConnectorError(error: unknown): never {\n  if (error instanceof PluginArchRouteFailure) {\n    throw error\n  }\n\n  if (error instanceof GithubConnectorConfigError) {\n    throw new PluginArchRouteFailure(409, \"github_connector_app_not_configured\", error.message)\n  }\n\n  if (error instanceof GithubConnectorRequestError) {\n    throw new PluginArchRouteFailure(409, \"github_connector_request_failed\", error.message, { cause: error })\n  }\n\n  throw error","sourceCodeStart":4324,"sourceCodeEnd":4360,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/ee/apps/den-api/src/routes/org/plugin-system/store.ts#L4324-L4360","documentation":"consumeGithubInstallState verifies the signed state token from a GitHub App installation callback using verifyGithubInstallStateToken with env.betterAuthSecret. If verification fails (bad signature, expired, or malformed token) it throws a 400 invalid_github_install_state. This protects the OAuth-style install flow against forged or stale state.","triggerScenarios":"GitHub redirects back to the install callback with a state param that is expired, signed with a different secret (e.g. betterAuthSecret rotated between start and callback), truncated by the client, or fabricated.","commonSituations":"User sat on the GitHub install page past token TTL and then completed it; server restarted with a rotated betterAuthSecret mid-install; load balancer routing callback to a different env; proxy mangling the query string.","solutions":["Restart the install flow: navigate to the install start endpoint again so a fresh state token is issued","Confirm betterAuthSecret has not rotated and is identical across all server instances handling the flow","Complete the GitHub installation promptly without long pauses","Inspect the callback URL to ensure the state query parameter is intact and not URL-decoded/re-encoded by a proxy"],"exampleFix":"// before\n// secret rotated between authorize start and callback -> old state unverifiable\n// after\n// keep betterAuthSecret stable across instances, or invalidate in-flight installs on rotation:\n// rotate secret, then re-initiate: window.location = installStartUrl (new signed state issued)","handlingStrategy":"try-catch","validationCode":"// client-side: detect stale/expired state before it matters\nconst startedAt = Number(localStorage.getItem(\"ghInstallStartedAt\") ?? 0)\nif (Date.now() - startedAt > STATE_TTL_MS) {\n  // state token likely expired — restart the install flow\n  location.assign(installStartUrl)\n}","typeGuard":"function isInvalidInstallState(e: unknown): boolean {\n  return e instanceof PluginArchRouteFailure && e.code === \"invalid_github_install_state\"\n}","tryCatchPattern":"try {\n  return await handleGithubInstallCallback(searchParams)\n} catch (e) {\n  if (isInvalidInstallState(e)) {\n    return redirectTo(installStartUrl) // fresh signed state, re-run flow\n  }\n  throw e\n}","preventionTips":["Keep betterAuthSecret stable and identical across all replicas","Complete the GitHub install promptly; restart the flow if it stalls","Rotate secrets only during a maintenance window and invalidate in-flight installs","Preserve the state query param verbatim through proxies/redirects"],"tags":["oauth","csrf-state","token-expired","github-app"],"backgroundTag":"oauth-state-mismatch","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}