{"record":{"id":"a0c72e6a8ba1ed6f","repo":"mastra-ai/mastra","slug":"linear-authorization-expired-reconnect-linear-to","errorCode":null,"errorMessage":"Linear authorization expired. Reconnect Linear to keep syncing intake issues.","messagePattern":"Linear authorization expired\\. Reconnect Linear to keep syncing intake issues\\.","errorType":"exception","errorClass":"LinearReauthRequiredError","httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/integrations/linear/integration.ts","lineNumber":397,"sourceCode":"   * concurrent refreshes with the same token would invalidate each other —\n   * single-flight ensures one exchange per org and shares the result.\n   */\n  readonly #inflightRefreshes = new Map<string, Promise<string>>();\n\n  /**\n   * Return a usable access token for the connection, proactively refreshing\n   * it when the recorded expiry is past (or imminent). Throws\n   * `LinearReauthRequiredError` when the token is expired and cannot be\n   * refreshed — the org has to go through the OAuth flow again.\n   */\n  async getFreshAccessToken(connection: LinearConnectionRow): Promise<string> {\n    const expired =\n      connection.expiresAt !== null && connection.expiresAt.getTime() - TOKEN_REFRESH_SKEW_MS <= Date.now();\n    if (!expired) return connection.accessToken;\n\n    if (!connection.refreshToken) {\n      // Legacy row from before refresh-token support: nothing to renew with.\n      throw new LinearReauthRequiredError();\n    }\n\n    const existing = this.#inflightRefreshes.get(connection.orgId);\n    if (existing) return existing;\n\n    // The caller may hold a stale row: another request could have refreshed\n    // and rotated the refresh token since this row was loaded. Reload before\n    // refreshing so we don't burn the rotated token and force a false reauth.\n    const latest = await this.loadConnection(connection.orgId);\n    if (!latest) throw new LinearReauthRequiredError();\n\n    const concurrent = this.#inflightRefreshes.get(connection.orgId);\n    if (concurrent) return concurrent;\n\n    const latestExpired = latest.expiresAt !== null && latest.expiresAt.getTime() - TOKEN_REFRESH_SKEW_MS <= Date.now();\n    if (!latestExpired) return latest.accessToken;\n    if (!latest.refreshToken) throw new LinearReauthRequiredError();\n","sourceCodeStart":379,"sourceCodeEnd":415,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/integrations/linear/integration.ts#L379-L415","documentation":"`getFreshAccessToken` throws `LinearReauthRequiredError` (message: 'Linear authorization expired...') when the stored access token's `expiresAt` is past the refresh-skew window and the connection row has no `refreshToken` — a legacy row saved before refresh-token support existed. There is nothing to renew the token with, so the org must complete the OAuth flow again.","triggerScenarios":"Calling any token-consuming surface (`accessToken`, intake listing, `#resolveIntakeDispatch`) for an org whose connection row predates refresh-token support and whose access token has expired (expiresAt minus TOKEN_REFRESH_SKEW_MS is in the past).","commonSituations":"Environments connected to Linear before refresh tokens were added, left idle until the access token expired; restored databases containing old connection rows; orgs syncing rarely (e.g. batch jobs) so the short-lived access token always ages out.","solutions":["Have the org reconnect Linear via the integration's OAuth connect URL (buildAuthorizeUrl flow) to mint a fresh token set including a refresh token.","Catch `LinearReauthRequiredError` and surface a 'Reconnect Linear' prompt in your UI.","If you hold connection rows out-of-band, re-upsert them with `refreshToken` set via `upsertConnection` after a fresh OAuth exchange.","Mark the connection read-only/stale in your app until reconnection completes."],"exampleFix":"// before: assume token always valid\nconst token = await integration.accessToken(orgId);\n// after\ntry {\n  const token = await integration.accessToken(orgId);\n} catch (err) {\n  if (isLinearReauthRequiredError(err)) redirectUserToConnectUrl(integration.buildAuthorizeUrl(state, redirectUri));\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"const conn = await integration.loadConnection(orgId);\nif (!conn) throw new Error('Linear not connected');\nconst needsReauth =\n  conn.expiresAt !== null && conn.expiresAt.getTime() - 60_000 <= Date.now() && !conn.refreshToken;\nif (needsReauth) redirectUserToConnectUrl(integration.buildAuthorizeUrl(state, redirectUri));","typeGuard":"function isLinearReauthRequiredError(err: unknown): boolean {\n  return err instanceof Error && err.message.startsWith('Linear authorization expired.');\n}","tryCatchPattern":"try {\n  const token = await integration.accessToken(orgId);\n} catch (err) {\n  if (isLinearReauthRequiredError(err)) {\n    markOrgNeedsReconnect(orgId);\n    return; // skip Linear work this run\n  }\n  throw err;\n}","preventionTips":["Proactively reconnect orgs whose connections predate refresh-token support.","Surface a 'Reconnect Linear' UI state instead of failing jobs silently.","Persist full token sets (including refreshToken) from every OAuth exchange.","Schedule background syncs more frequently than token expiry so proactive refresh runs."],"tags":["oauth","token-expired","linear","reauth-required"],"backgroundTag":"oauth-token-expired-reauth-required","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}