{"record":{"id":"ab64a02dddded95f","repo":"ToolJet/ToolJet","slug":"hubspot-oauth-scope-config-is-missing","errorCode":null,"errorMessage":"HubSpot OAuth \"scope\" config is missing","messagePattern":"HubSpot OAuth \"scope\" config is missing","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"marketplace/plugins/hubspot/lib/index.ts","lineNumber":122,"sourceCode":"\n  authUrl(source_options: SourceOptions): string {\n    const { clientId, clientSecret, scopes, redirectUri } = this.getOAuthCredentials(source_options);\n    const oauth_type = source_options.oauth_type.value;\n\n    if (!clientId) {\n      throw new Error(\n        `HubSpot OAuth \"clientId\" ${oauth_type === 'tooljet_app' ? 'environment variable' : 'config'} is missing`\n      );\n    }\n\n    if (!clientSecret) {\n      throw new Error(\n        `HubSpot OAuth \"clientSecret\" ${oauth_type === 'tooljet_app' ? 'environment variable' : 'config'} is missing`\n      );\n    }\n\n    if (!scopes) {\n      throw new Error(`HubSpot OAuth \"scope\" config is missing`);\n    }\n\n    const baseUrl = 'https://app.hubspot.com/oauth/authorize';\n\n    const params = new URLSearchParams({\n      response_type: 'code',\n      client_id: clientId,\n      redirect_uri: redirectUri,\n      scope: scopes,\n    });\n\n    const authUrl = `${baseUrl}?${params.toString()}`;\n    return authUrl;\n  }\n\n  async accessDetailsFrom(authCode: string, source_options: any, resetSecureData = false): Promise<object> {\n    if (resetSecureData) {\n      return [","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/ToolJet/ToolJet/blob/20602a8e101f2e59686c9afde0d1402aac2c8871/marketplace/plugins/hubspot/lib/index.ts#L104-L140","documentation":"HubSpot plugin's authUrl() throws this plain Error when getOAuthCredentials() returns falsy scopes. Unlike clientId/clientSecret (which can come from env), scopes are config-only — they must be present in source_options. This is the third guard in the authUrl pre-flight sequence and runs after clientId and clientSecret checks pass.","triggerScenarios":"Datasource config has clientId and clientSecret filled but the scopes field is empty or undefined. Happens when the form field is optional in the UI but the OAuth flow requires at least one scope (HubSpot requires explicit scopes; empty scope is rejected).","commonSituations":"New datasource setup where the operator skipped the scopes input; migration that preserved credentials but dropped scopes; scopes were set as an array but getOAuthCredentials expected a space- or comma-delimited string (or vice versa) and returned falsy.","solutions":["Open the datasource config and enter at least one HubSpot OAuth scope, e.g. 'contacts' (space-separated for multiple: 'contacts companies deals').","Confirm the value matches the shape getOAuthCredentials expects (string truthy). If it expects a string and you supplied an array, serialize it.","Match the scopes against what is registered on the HubSpot app (HubSpot will reject scopes not declared on the app).","Re-trigger authUrl() after saving to confirm the redirect URL builds."],"exampleFix":"// before — datasource config with empty scopes\nsource_options = {\n  oauth_type: { value: 'custom' },\n  client_id: '...',\n  client_secret: '...',\n  scopes: '' // or undefined\n};\n\n// after\nsource_options = {\n  oauth_type: { value: 'custom' },\n  client_id: '...',\n  client_secret: '...',\n  scopes: 'contacts companies deals'\n};","handlingStrategy":"validation","validationCode":"function ensureHubSpotScopes(source_options) {\n  const scopes = source_options?.scopes?.value ?? source_options?.scopes;\n  if (!scopes || (typeof scopes === 'string' && scopes.trim() === '')) {\n    throw new Error('HubSpot OAuth scopes are required (e.g. \"contacts companies\")');\n  }\n  return scopes;\n}\nensureHubSpotScopes(source_options);","typeGuard":"function hasHubSpotScopes(source_options): boolean {\n  const s = source_options?.scopes?.value ?? source_options?.scopes;\n  return typeof s === 'string' && s.trim().length > 0;\n}","tryCatchPattern":"try {\n  const url = plugin.authUrl(source_options);\n} catch (e) {\n  if (/scope.*missing/.test(e.message)) {\n    return { action: 'configure_scopes' };\n  }\n  throw e;\n}","preventionTips":["Make scopes a required field in the datasource UI when clientId/clientSecret are filled.","Only request scopes registered on the HubSpot app — otherwise HubSpot rejects the consent.","Document scope strings as space-separated in the field's help text."],"tags":["oauth","hubspot","configuration","scopes","tooljet"],"backgroundTag":null,"analyzedSha":"20602a8e101f2e59686c9afde0d1402aac2c8871","analyzedAt":"2026-08-13T05:58:54.221Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}