{"record":{"id":"a28d080d60b2008b","repo":"different-ai/openwork","slug":"google-workspace-not-connected","errorCode":"google_workspace_not_connected","errorMessage":"Connect Google Workspace in OpenWork Settings to use this tool.","messagePattern":"Connect Google Workspace in OpenWork Settings to use this tool\\.","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"apps/server/src/extensions/google-workspace.ts","lineNumber":627,"sourceCode":"    });\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    },\n    updatedAt: new Date().toISOString(),\n  };\n  return next;\n}\n\nasync function googleWorkspaceAccessToken(config: ServerConfig): Promise<{ record: Record<string, unknown>; accessToken: string }> {\n  const vault = await readGoogleWorkspaceVault(config);\n  const record = googleWorkspacePrimaryRecord(vault);\n  if (!record) throw new ApiError(400, \"google_workspace_not_connected\", \"Connect Google Workspace in OpenWork Settings to use this tool.\");\n  const refreshed = await refreshGoogleWorkspaceVault(record);\n  const refreshedAccountId = googleWorkspaceAccountId(refreshed);\n  if (refreshedAccountId) {\n    const nextAccounts = googleWorkspaceAccountRecords(vault).map((entry) => googleWorkspaceAccountId(entry) === refreshedAccountId ? refreshed : entry);\n    await writeGoogleWorkspaceAccountsVault(config, nextAccounts, refreshedAccountId);\n  }\n  const token = isRecord(refreshed.token) ? refreshed.token : null;\n  const accessToken = typeof token?.accessToken === \"string\" ? token.accessToken : \"\";\n  if (!accessToken) throw new Error(\"Google Workspace access token is unavailable. Reconnect Google Workspace.\");\n  return { record: refreshed, accessToken };\n}\n\nfunction multipartRelatedBody(metadata: Record<string, unknown>, content: string, boundary: string): string {\n  return [\n    `--${boundary}`,\n    \"Content-Type: application/json; charset=UTF-8\",\n    \"\",\n    JSON.stringify(metadata),","sourceCodeStart":609,"sourceCodeEnd":645,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/extensions/google-workspace.ts#L609-L645","documentation":"googleWorkspaceAccessToken() is the entry point every Google Workspace tool uses to obtain a bearer token. It reads the extension's oauth vault and, if there is no primary account record at all, throws an ApiError with status 400 and code google_workspace_not_connected instead of attempting a refresh. This is the library's signal that the user never completed (or erased) the Google OAuth connect flow.","triggerScenarios":"Invoking any Google Workspace tool/endpoint (Gmail, Drive, Calendar tools) before any OAuth connect has completed, or after the vault was cleared/failed to decrypt so googleWorkspacePrimaryRecord(vault) returns null.","commonSituations":"Fresh server install where Settings > Google Workspace connect was never run; vault directory removed or permissions broken so the record cannot be read; connecting in one environment (desktop) and calling tools against another (server) with a different configDir; vault key missing so the encrypted vault is unreadable.","solutions":["Connect Google Workspace in OpenWork Settings (complete the OAuth browser flow) — this populates the vault record","Verify the server's configDir actually contains extensions/google-workspace/oauth.vault for the environment you are calling","If a connection exists but is unreadable, check the vault-key file and file permissions on the extension directory","Check the connect status endpoint first (googleWorkspaceStatusPayload) before invoking tools"],"exampleFix":"// before\nawait callTool(\"gmail_list_messages\", {}) // ApiError 400 google_workspace_not_connected\n\n// after: complete Settings > Connect Google Workspace OAuth flow once\nawait callTool(\"gmail_list_messages\", {}) // works with stored tokens","handlingStrategy":"validation","validationCode":"const status = await getGoogleWorkspaceStatus(); // googleWorkspaceStatusPayload: { configured, connected, ... }\nif (!status.connected) {\n  return { redirectTo: \"/settings/google-workspace\" }; // ask user to connect first\n}\nconst { accessToken } = await googleWorkspaceAccessToken(config);","typeGuard":"function isConnected(vault: unknown): vault is { accounts: Record<string, unknown>[] } {\n  return typeof vault === \"object\" && vault !== null &&\n    Array.isArray((vault as { accounts?: unknown }).accounts) &&\n    (vault as { accounts: unknown[] }).accounts.length > 0;\n}","tryCatchPattern":"try {\n  const { accessToken } = await googleWorkspaceAccessToken(config);\n} catch (err) {\n  if (err instanceof ApiError && err.code === \"google_workspace_not_connected\") {\n    ui.showConnectPrompt(\"Connect Google Workspace in OpenWork Settings to use this tool.\");\n    return;\n  }\n  throw err;\n}","preventionTips":["Check the extension status payload (connected/configured) before enabling Google Workspace tools in the UI","Gate tool availability on connection state so users never hit the 400 mid-workflow","Remember configDir is per-environment: a connection made in desktop dev doesn't carry to a server deployment","Verify vault + vault-key files exist and are readable if a known-connected account suddenly reports not connected"],"tags":["oauth","google-workspace","not-connected","vault","api-error"],"backgroundTag":"oauth-not-connected","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}