{"record":{"id":"95fbb4125bc8427f","repo":"paperclipai/paperclip","slug":"oauth-callback-connection-belongs-to-a-missing-com","errorCode":null,"errorMessage":"OAuth callback connection belongs to a missing company","messagePattern":"OAuth callback connection belongs to a missing company","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/routes/tool-access.ts","lineNumber":445,"sourceCode":"    if (!host) return null;\n    try {\n      const parsed = new URL(`${req.protocol}://${host}`);\n      return isLoopbackHost(parsed.hostname) ? parsed.origin : null;\n    } catch {\n      return null;\n    }\n  }\n\n  async function oauthAppPath(\n    companyId: string,\n    connectionId: string,\n  ) {\n    const [company] = await db\n      .select({ issuePrefix: companies.issuePrefix })\n      .from(companies)\n      .where(eq(companies.id, companyId))\n      .limit(1);\n    if (!company) throw new Error(\"OAuth callback connection belongs to a missing company\");\n    return `/${company.issuePrefix}/apps/${connectionId}/permissions`;\n  }\n\nfunction connectorEnrollmentPrincipal(req: Request): string {\n  return req.actor.userId ? `user:${req.actor.userId}` : `source:${req.actor.source ?? \"board\"}`;\n}\n\n/**\n   * A failed first authorization is still an incomplete setup, not an app\n   * configuration task. Send it back to the same exact draft so the operator\n   * can retry the missing checkpoint. Reauthorization of an already-active\n   * connection keeps the established detail-page recovery route.\n   */\n  async function oauthRecoveryPath(\n    connection: ToolConnection,\n    outcome: \"failed\" | \"denied\",\n    code?: string | null,\n    providerRecovery?: { installationUrl?: unknown; managementUrl?: unknown },","sourceCodeStart":427,"sourceCodeEnd":463,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/routes/tool-access.ts#L427-L463","documentation":"oauthAppPath builds a callback redirect path of the form /:issuePrefix/apps/:connectionId/permissions and requires the company row to exist to read its issuePrefix. When the companyId referenced by the OAuth connection has no matching row in the companies table, it throws this Error, which surfaces as a 500 on the OAuth callback/permissions routes.","triggerScenarios":"OAuth callback or permissions resolution (via detailPermissionsPath/permissionsPath) for a connection whose companies FK points at a deleted or never-created company — the SELECT on companies by id returns empty and the function throws.","commonSituations":"Company hard-deleted while OAuth connections remained (missing cascade); cross-environment DB restore where companies rows were dropped; seeded connection rows with bogus companyIds; environments synced partially.","solutions":["Delete or reassign the orphaned OAuth connection so its companyId references a live company row.","Restore/re-insert the missing companies row (with correct issuePrefix) that the connection references.","Add an ON DELETE cascade or cleanup job for connections when companies are removed to prevent orphans.","Harden oauthAppPath to return a 404/redirect instead of throwing for missing companies."],"exampleFix":"// before\nif (!company) throw new Error(\"OAuth callback connection belongs to a missing company\");\n// after\nif (!company) return null; // caller redirects to an app-error page instead of a 500","handlingStrategy":"try-catch","validationCode":"const [company] = await db.select().from(companies).where(eq(companies.id, connection.companyId));\nif (!company) throw new Error(`connection ${connection.id} references deleted company ${connection.companyId}`);","typeGuard":"function hasLiveCompany(connection, company) { return Boolean(company && company.id === connection.companyId && company.issuePrefix); }","tryCatchPattern":"try {\n  return buildOauthAppPath(connection.companyId, connection.id);\n} catch (e) {\n  logger.error({ connectionId: connection.id, companyId: connection.companyId }, e.message);\n  return null; // render generic OAuth error page\n}","preventionTips":["Delete OAuth connections when their company is deleted (cascade).","Add a periodic orphaned-connection cleanup job.","Avoid hard-deleting companies; soft-delete and keep issuePrefix resolvable.","Validate connection.companyId on connection creation."],"tags":["oauth","orphaned-record","database","callback"],"backgroundTag":"entity-not-found","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"}