mastra-ai/mastra · error · LinearReauthRequiredError

Linear authorization expired. Reconnect Linear to keep synci

Error message

Linear authorization expired. Reconnect Linear to keep syncing intake issues.

What it means

`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.

Source

Thrown at mastracode/factory/src/integrations/linear/integration.ts:397

   * concurrent refreshes with the same token would invalidate each other —
   * single-flight ensures one exchange per org and shares the result.
   */
  readonly #inflightRefreshes = new Map<string, Promise<string>>();

  /**
   * Return a usable access token for the connection, proactively refreshing
   * it when the recorded expiry is past (or imminent). Throws
   * `LinearReauthRequiredError` when the token is expired and cannot be
   * refreshed — the org has to go through the OAuth flow again.
   */
  async getFreshAccessToken(connection: LinearConnectionRow): Promise<string> {
    const expired =
      connection.expiresAt !== null && connection.expiresAt.getTime() - TOKEN_REFRESH_SKEW_MS <= Date.now();
    if (!expired) return connection.accessToken;

    if (!connection.refreshToken) {
      // Legacy row from before refresh-token support: nothing to renew with.
      throw new LinearReauthRequiredError();
    }

    const existing = this.#inflightRefreshes.get(connection.orgId);
    if (existing) return existing;

    // The caller may hold a stale row: another request could have refreshed
    // and rotated the refresh token since this row was loaded. Reload before
    // refreshing so we don't burn the rotated token and force a false reauth.
    const latest = await this.loadConnection(connection.orgId);
    if (!latest) throw new LinearReauthRequiredError();

    const concurrent = this.#inflightRefreshes.get(connection.orgId);
    if (concurrent) return concurrent;

    const latestExpired = latest.expiresAt !== null && latest.expiresAt.getTime() - TOKEN_REFRESH_SKEW_MS <= Date.now();
    if (!latestExpired) return latest.accessToken;
    if (!latest.refreshToken) throw new LinearReauthRequiredError();

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Have the org reconnect Linear via the integration's OAuth connect URL (buildAuthorizeUrl flow) to mint a fresh token set including a refresh token.
  2. Catch `LinearReauthRequiredError` and surface a 'Reconnect Linear' prompt in your UI.
  3. If you hold connection rows out-of-band, re-upsert them with `refreshToken` set via `upsertConnection` after a fresh OAuth exchange.
  4. Mark the connection read-only/stale in your app until reconnection completes.

Example fix

// before: assume token always valid
const token = await integration.accessToken(orgId);
// after
try {
  const token = await integration.accessToken(orgId);
} catch (err) {
  if (isLinearReauthRequiredError(err)) redirectUserToConnectUrl(integration.buildAuthorizeUrl(state, redirectUri));
  throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const conn = await integration.loadConnection(orgId);
if (!conn) throw new Error('Linear not connected');
const needsReauth =
  conn.expiresAt !== null && conn.expiresAt.getTime() - 60_000 <= Date.now() && !conn.refreshToken;
if (needsReauth) redirectUserToConnectUrl(integration.buildAuthorizeUrl(state, redirectUri));

Type guard

function isLinearReauthRequiredError(err: unknown): boolean {
  return err instanceof Error && err.message.startsWith('Linear authorization expired.');
}

Try / catch

try {
  const token = await integration.accessToken(orgId);
} catch (err) {
  if (isLinearReauthRequiredError(err)) {
    markOrgNeedsReconnect(orgId);
    return; // skip Linear work this run
  }
  throw err;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/a0c72e6a8ba1ed6f. Report an issue: GitHub.