different-ai/openwork · warning · PluginArchRouteFailure
invalid_github_install_state
invalid_github_install_state
Error message
GitHub install state is invalid or expired.
What it means
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.
Source
Thrown at ee/apps/den-api/src/routes/org/plugin-system/store.ts:4342
return { enqueuedCount }
}
function githubConnectorAppConfig() {
try {
return getGithubConnectorAppConfig(env.githubConnectorApp)
} catch (error) {
if (error instanceof GithubConnectorConfigError) {
throw new PluginArchRouteFailure(409, "github_connector_app_not_configured", error.message)
}
throw error
}
}
export function consumeGithubInstallState(state: string) {
const parsed = verifyGithubInstallStateToken({ secret: env.betterAuthSecret, token: state })
if (!parsed) {
throw new PluginArchRouteFailure(400, "invalid_github_install_state", "GitHub install state is invalid or expired.")
}
return parsed
}
function wrapGithubConnectorError(error: unknown): never {
if (error instanceof PluginArchRouteFailure) {
throw error
}
if (error instanceof GithubConnectorConfigError) {
throw new PluginArchRouteFailure(409, "github_connector_app_not_configured", error.message)
}
if (error instanceof GithubConnectorRequestError) {
throw new PluginArchRouteFailure(409, "github_connector_request_failed", error.message, { cause: error })
}
throw errorView on GitHub (pinned to 2b7df46e8a)
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
Example fix
// before // secret rotated between authorize start and callback -> old state unverifiable // after // keep betterAuthSecret stable across instances, or invalidate in-flight installs on rotation: // rotate secret, then re-initiate: window.location = installStartUrl (new signed state issued)
Defensive patterns
Strategy: try-catch
Validate before calling
// client-side: detect stale/expired state before it matters
const startedAt = Number(localStorage.getItem("ghInstallStartedAt") ?? 0)
if (Date.now() - startedAt > STATE_TTL_MS) {
// state token likely expired — restart the install flow
location.assign(installStartUrl)
} Type guard
function isInvalidInstallState(e: unknown): boolean {
return e instanceof PluginArchRouteFailure && e.code === "invalid_github_install_state"
} Try / catch
try {
return await handleGithubInstallCallback(searchParams)
} catch (e) {
if (isInvalidInstallState(e)) {
return redirectTo(installStartUrl) // fresh signed state, re-run flow
}
throw e
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- MCP_OAUTH_CREDENTIAL_EXPIRED
- `${t("providers.no_oauth_prefix")} ${resolved}. ${t("provide
- `${t("providers.not_oauth_flow_prefix")} ${resolved}.`
- t("providers.oauth_method_required")
- OpenWork-managed MCP OAuth is currently available for local
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/09c63bfcaf06b7a3.
Report an issue: GitHub.