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
- 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.
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
- 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.
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
- State token has expired
- Linear ${label} returned no access token.
- Linear capabilities require an OAuth connection.
- Linear capabilities require an OAuth connection.
- Invalid state token format
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/a0c72e6a8ba1ed6f.
Report an issue: GitHub.