{"record":{"id":"9e3df151db418b62","repo":"different-ai/openwork","slug":"google-workspace-refresh-token-is-missing-reconne","errorCode":null,"errorMessage":"Google Workspace refresh token is missing. Reconnect Google Workspace.","messagePattern":"Google Workspace refresh token is missing\\. Reconnect Google Workspace\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/server/src/extensions/google-workspace.ts","lineNumber":600,"sourceCode":"    headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n    body: new URLSearchParams({\n      client_id: clientId,\n      client_secret: clientSecret,\n      code: input.code,\n      code_verifier: input.verifier,\n      grant_type: \"authorization_code\",\n      redirect_uri: input.redirectUri,\n    }),\n  });\n}\n\nasync function refreshGoogleWorkspaceVault(record: Record<string, unknown>) {\n  const token = isRecord(record.token) ? record.token : null;\n  const expiresAt = Number(token?.expiresAt ?? 0);\n  const accessToken = typeof token?.accessToken === \"string\" ? token.accessToken : \"\";\n  const refreshToken = typeof token?.refreshToken === \"string\" ? token.refreshToken : \"\";\n  if (accessToken && expiresAt > Date.now() + 60_000) return record;\n  if (!refreshToken) throw new Error(\"Google Workspace refresh token is missing. Reconnect Google Workspace.\");\n  const { clientId, clientSecret, tokenBrokerUrl, missing } = googleWorkspaceCredentials();\n  if (missing.length > 0) throw new Error(`Missing Google OAuth configuration: ${missing.join(\", \")}`);\n  const refreshed = tokenBrokerUrl\n    ? await fetchGoogleWorkspaceTokenBrokerJson(tokenBrokerUrl, { grantType: \"refresh_token\", provider: GOOGLE_WORKSPACE_EXTENSION_ID, clientId, refreshToken })\n    : await fetchGoogleJson(\"https://oauth2.googleapis.com/token\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n      body: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, grant_type: \"refresh_token\", refresh_token: refreshToken }),\n    });\n  if (!isRecord(refreshed) || typeof refreshed.access_token !== \"string\") throw new Error(\"Google OAuth refresh did not return an access token.\");\n  const next = {\n    ...record,\n    scopes: typeof refreshed.scope === \"string\" ? refreshed.scope.split(/\\s+/).filter(Boolean) : record.scopes,\n    token: {\n      accessToken: refreshed.access_token,\n      refreshToken: typeof refreshed.refresh_token === \"string\" ? refreshed.refresh_token : refreshToken,\n      expiresAt: Date.now() + Number(refreshed.expires_in ?? 3600) * 1000,\n    },","sourceCodeStart":582,"sourceCodeEnd":618,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/extensions/google-workspace.ts#L582-L618","documentation":"refreshGoogleWorkspaceVault() refreshes the stored Google OAuth tokens when the cached access token is expired (or expiring within 60s). If the stored vault record has no refreshToken string, it cannot get a new access token, so it throws immediately. This happens when the original OAuth grant did not return a refresh token (e.g. offline access was not granted or the user re-consented without it) or the vault record is corrupt/partial.","triggerScenarios":"Any Google Workspace tool call routed through googleWorkspaceAccessToken() after the stored access token has expired (expiresAt <= Date.now()+60s) while record.token.refreshToken is missing or not a string in the oauth vault.","commonSituations":"Vault file written by an older extension version without a refresh token; token object corrupted by a partial write; initial grant omitted the refresh token because consent/refresh scope settings changed; user manually edited or truncated the vault file.","solutions":["Reconnect Google Workspace via the Settings connect flow so a fresh grant stores a new refresh token","Delete the stale oauth.vault record for the account and redo the OAuth connect from scratch","Verify the vault JSON has token.refreshToken as a string before relying on long-lived sessions","Ensure the connect flow requests offline access / refresh tokens so Google returns one on first consent"],"exampleFix":"// before (corrupt record in oauth.vault)\n{ \"token\": { \"accessToken\": \"ya29...\", \"expiresAt\": 1700000000 } }\n\n// after: reconnect via Settings; record now contains\n{ \"token\": { \"accessToken\": \"ya29...\", \"refreshToken\": \"1//0g...\", \"expiresAt\": 1700000000 } }","handlingStrategy":"try-catch","validationCode":"const vault = JSON.parse(await readFile(vaultPath, \"utf8\"));\nconst token = vault?.token;\nif (!token || typeof token.refreshToken !== \"string\" || token.refreshToken.length === 0) {\n  await promptReconnect();\n}","typeGuard":"function hasRefreshToken(record: unknown): record is { token: { refreshToken: string } } {\n  return typeof record === \"object\" && record !== null &&\n    typeof (record as { token?: unknown }).token === \"object\" && (record as { token?: unknown }).token !== null &&\n    typeof ((record as { token?: { refreshToken?: unknown } }).token).refreshToken === \"string\";\n}","tryCatchPattern":"try {\n  const { accessToken } = await googleWorkspaceAccessToken(config);\n  await callGoogleTool(accessToken);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"refresh token is missing\")) {\n    await triggerReconnectFlow(); // surface \"Reconnect Google Workspace\" to the user\n    return;\n  }\n  throw err;\n}","preventionTips":["Ensure the initial OAuth grant requests offline access so Google always returns a refresh token","After connect, assert the stored vault record contains token.refreshToken before proceeding","Back up the oauth.vault and vault-key files; partial writes leave records without refresh tokens","Surface reconnect prompts proactively via the extension status payload instead of failing tool calls"],"tags":["oauth","refresh-token","google-workspace","reauth"],"backgroundTag":"missing-refresh-token","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}