Skip to content

Add OAuth connectors as a CLI resource #184

@Paveltarno

Description

@Paveltarno

Overview

OAuth connectors allow Base44 apps to integrate with external services (Google Calendar, Slack, LinkedIn, etc.) via OAuth. This document describes the proposed CLI resource implementation.


Implementation Tasks

Backend (apper)

  • Task 0: Implement /sync endpoint — base44-dev/apper#3374

CLI

Task Details

Task Description Blocked By
Task 0 Backend /sync endpoint with exact scope matching
Task 1 Zod schemas for connector resources, JSONC file reader
Task 2 API client: syncConnector(), listConnectors(), getOAuthStatus(), removeConnector() Task 0 (for sync)
Task 3 Compare local vs upstream, determine actions (sync/delete) Task 0
Task 4 Display auth URL, poll for completion, handle edge cases Task 2
Task 5 Wire into push command, colored summary output Tasks 3, 4

Key Concepts

Term Description
AppIntegration MongoDB model storing connector state per app
Composio Third-party service handling OAuth flows, token storage, and refresh
Connected Account A user's authorized connection to an external service

Supported Integration Types

googlecalendar, googledrive, gmail, googlesheets, googledocs, googleslides,
slack, notion, salesforce, hubspot, linkedin, tiktok

Connector States

Status Description
ACTIVE Connected and functional
DISCONNECTED Soft-deleted, preserves scopes for reconnection
EXPIRED Token expired, needs re-auth

CLI Resource File Format

File Structure

connectors/
├── googlecalendar.jsonc
├── slack.jsonc
└── notion.jsonc

Resource Schema

// connectors/googlecalendar.jsonc
{
  "type": "googlecalendar",
  "scopes": [
    "https://www.googleapis.com/auth/calendar.readonly",
    "https://www.googleapis.com/auth/calendar.events"
  ]
}
  • One file per connector, named by integration type
  • Stateless - contains only type and scopes, no status field
  • Uses JSONC (JSON with comments) for consistency with other CLI resources
  • No pull support - connectors are push-only

Push Flow Logic

The /sync Endpoint

The CLI uses a dedicated /sync endpoint designed for declarative state management. Unlike the /initiate endpoint (used by Apper's AI for incremental scope expansion), /sync provides:

  1. Exact scope matching - returns already_authorized=true only when scopes match exactly
  2. Scope replacement - never merges scopes, always replaces with requested scopes
  3. Built-in scope enhancement - backend handles auto-added scopes internally

Comparison Matrix

Local File Upstream State Scopes Match? Action
Exists Not exists N/A Call /sync → auth URL + poll
Exists DISCONNECTED N/A Call /sync → auth URL + poll
Exists EXPIRED N/A Call /sync → auth URL + poll
Exists ACTIVE Yes Call /syncalready_authorized=true (no-op)
Exists ACTIVE No Call /sync → auth URL + poll (scopes replaced)
Not exists Exists (any) N/A Hard delete upstream

How /sync Works

  1. CLI sends { integration_type, scopes } to /sync
  2. Backend enhances scopes internally (adds identity scopes like email)
  3. Backend compares enhanced(requested) with existing approved scopes (exact equality)
  4. If exact match → returns { already_authorized: true }
  5. If different → replaces auth config scopes, creates new connection, returns { redirect_url, connection_id }

This is simpler than the AI flow because:

  • No client-side scope enhancement needed
  • No delete-then-create dance
  • Single atomic operation

Post-Auth Verification

After OAuth completes, verify the approved scopes match what was expected:

  1. Fetch connector from /list endpoint
  2. Compare approved_scopes with requested_scopes from JSONC
  3. If not equal → show SCOPE_MISMATCH (user gave partial consent)

Note: The /list endpoint returns the enhanced scopes (including auto-added). The CLI should compare against the full approved set, not just the user-specified scopes.

Possible outcomes:

Approved vs Expected Result
Scopes match ✓ ACTIVE
User deselected scopes during consent ⚠ SCOPE_MISMATCH

Auth Flow Behavior

  • Sequential auth: If multiple connectors need auth, prompt one at a time (interactive state machine)
  • Polling timeout: Same as device login auth timeout

Push Summary Output

Uses @clack/prompts logging with colors (consistent with existing CLI patterns):

Connectors push summary:
  - googlecalendar: active (3 scopes)
  - slack: active (4 scopes, re-authed)
  - linkedin: scope mismatch (requested 5, approved 3)
  - notion: auth not completed
  - hubspot: deleted (no local definition)

Some connectors need attention:
  - linkedin: Approved scopes differ from requested. Update connectors/linkedin.jsonc or run push again.
  - notion: Authentication not completed. Run push to retry.

Status text will be colored via chalk:

  • active → green
  • scope mismatch → yellow
  • auth not completed / failed → red
  • deleted → dim/gray

Summary Status States

Status Color Description
ACTIVE green Connector active, scopes match
SCOPE_MISMATCH yellow Active but approved scopes ≠ requested (partial consent)
PENDING_AUTH red Auth URL shown but user didn't complete
AUTH_FAILED red OAuth flow failed
DELETED dim Removed from upstream (no local file)
DIFFERENT_USER red Another user already authorized this connector

API Endpoints

Endpoint Method Description
/api/apps/{app_id}/external-auth/sync POST CLI endpoint - sync connector with exact scopes
/api/apps/{app_id}/external-auth/list GET List all connectors
/api/apps/{app_id}/external-auth/status GET Poll for OAuth completion
/api/apps/{app_id}/external-auth/integrations/{type}/remove DELETE Hard delete

Sync Endpoint (CLI-specific)

POST /api/apps/{app_id}/external-auth/sync
{
  "integration_type": "googlecalendar",
  "scopes": ["https://www.googleapis.com/auth/calendar.readonly"]
}

// Response - needs auth
{
  "redirect_url": "https://accounts.google.com/o/oauth2/...",
  "connection_id": "abc123",
  "already_authorized": false
}

// Response - already authorized with exact scopes
{
  "redirect_url": null,
  "connection_id": null,
  "already_authorized": true
}

// Response - different user error
{
  "redirect_url": null,
  "connection_id": null,
  "already_authorized": false,
  "error": "different_user",
  "error_message": "Integration already authorized by another user",
  "other_user_email": "other@example.com"
}

Key differences from /initiate:

  • Exact scope comparison (not subset)
  • Replaces scopes (not merges)
  • Handles scope enhancement server-side

Polling

GET /status?integration_type=googlecalendar&connection_id=abc123
→ {"status": "ACTIVE" | "FAILED" | "PENDING"}

Poll until ACTIVE or FAILED. Suggested interval: 2 seconds.


Edge Cases

  1. Partial Consent: OAuth provider may approve fewer scopes than requested → show SCOPE_MISMATCH

  2. Different User: Only one user can auth per connector per app. If another user already authorized, show their email and suggest they disconnect first.

  3. Scope Validation: Scopes are NOT validated by apper - the OAuth provider validates during consent. Invalid scopes cause errors on the provider's consent screen.

  4. Provider doesn't return scopes: Some OAuth providers (e.g., Facebook) don't return approved scopes in token response. Apper falls back to requested (enhanced) scopes.


Auto-Added Scopes (Reference)

These scopes are added automatically by the backend for identity extraction. Users don't need to include them in JSONC files - the /sync endpoint handles enhancement internally.

Integration Auto-added Scopes
All Google (Calendar, Drive, Gmail, Sheets, Docs, Slides) email
Slack users:read, users:read.email
Salesforce openid, profile, email
HubSpot oauth
LinkedIn openid, profile, email
TikTok user.info.basic
Notion (none)

Known Scopes Reference

Google Calendar

Scope Description
https://www.googleapis.com/auth/calendar.readonly Read-only access
https://www.googleapis.com/auth/calendar.events Read/write events
https://www.googleapis.com/auth/calendar Full access

Google Drive

Scope Description
https://www.googleapis.com/auth/drive.file App-created/selected files only (only supported)
https://www.googleapis.com/auth/drive.readonly Read all files

Gmail

Scope Description
https://www.googleapis.com/auth/gmail.send Send emails
https://www.googleapis.com/auth/gmail.readonly Read emails
https://www.googleapis.com/auth/gmail.modify Read/write/send

Google Sheets

Scope Description
https://www.googleapis.com/auth/spreadsheets Read/write
https://www.googleapis.com/auth/spreadsheets.readonly Read-only

Google Docs

Scope Description
https://www.googleapis.com/auth/documents Read/write
https://www.googleapis.com/auth/documents.readonly Read-only

Google Slides

Scope Description
https://www.googleapis.com/auth/presentations Read/write
https://www.googleapis.com/auth/presentations.readonly Read-only

Slack

Scope Description
chat:write Send messages
channels:read Read channel info
channels:history, groups:read, im:read, mpim:read Read various channels
files:read, files:write File access

Note: User token integration only. Do not request bot scopes.

Notion

Notion uses a single-scope model - users select pages/databases in the OAuth UI. No scopes needed in JSONC.

Salesforce

Scope Description
api API access
refresh_token Offline access

HubSpot

Scope Description
crm.objects.contacts.read/write Contacts access
crm.objects.deals.read/write Deals access
crm.objects.companies.read/write Companies access

LinkedIn

Scope Description
r_basicprofile, r_profile_basicinfo Profile read
w_member_social Post as member
w_organization_social, r_organization_social Org social
r_organization_admin, rw_organization_admin Org admin
r_ads, r_ads_reporting, rw_ads Ads access
r_verify, r_1st_connections_size Verification, connections

TikTok

Scope Description
user.info.profile Links, bio, verification
user.info.stats Follower/video counts
video.list Read public videos
artist.certification.read/update Artist certification

Note: Content API and video uploading not supported.


Implementation: Zod Schemas

import { z } from 'zod';

export const IntegrationTypeSchema = z.enum([
  'googlecalendar', 'googledrive', 'gmail', 'googlesheets',
  'googledocs', 'googleslides', 'slack', 'notion',
  'salesforce', 'hubspot', 'linkedin', 'tiktok',
]);

export const ConnectorResourceSchema = z.object({
  type: IntegrationTypeSchema,
  scopes: z.array(z.string()),  // No validation - OAuth provider is the ultimate validator
});

export type ConnectorResource = z.infer<typeof ConnectorResourceSchema>;

Validation Behavior

Scenario Behavior
Unknown integration type Error: reject file
Unknown scope No validation - OAuth provider validates during consent
Empty scopes array Allowed (some integrations like Notion don't need scopes)
Missing type field Error: reject file

Dependencies

  • Apper: New /sync endpoint - see base44-dev/apper#3374

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    In progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions