Skip to content

Latest commit

 

History

History
352 lines (278 loc) · 14.4 KB

File metadata and controls

352 lines (278 loc) · 14.4 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Orbitant Calendar Sync is a multi-tenant calendar aggregation service that synchronizes events from multiple sources (Google Calendar, Microsoft Outlook, iCal feeds) and provides:

  • Unified iCal feeds per user
  • Slack bot integration (/ajustes, /calendario)
  • OAuth 2.0 authentication per Slack user
  • Automatic periodic synchronization via cron

Key Architecture Concept: Each Slack user authenticates independently with calendar providers. The system stores encrypted OAuth tokens per user and aggregates their calendar sources into a unified feed.

Common Development Commands

Development

# Start with auto-reload (Node 22 watch mode)
npm run dev

# Start in production
npm start

# Linting
npm run lint
npm run lint:fix

# Run legacy token generation script (deprecated — tokens managed via Slack OAuth flow)
npm run auth

Docker

# Build and run with Docker Compose
docker-compose up -d

# View logs
docker-compose logs -f

# Stop
docker-compose down

Database

The SQLite database is automatically initialized on startup from src/database/schema.sql. Migrations run automatically in src/config/database.js.

Architecture

Core Components

Express Server (src/index.js)

  • OAuth callbacks (/auth/google/callback, /auth/azure/callback)
  • iCal feed endpoint (/feed/:token/orbitando.ics)
  • Health check (/health)
  • Graceful shutdown handlers (SIGINT/SIGTERM)

Slack Bot (Socket Mode)

  • Commands: /ajustes (settings), /calendario (view events)
  • Actions: Source management, OAuth flow initiation
  • Runs on Socket Mode (no HTTP webhooks needed)

SyncService (src/services/SyncService.js)

  • Singleton coordinating all synchronization
  • Methods: syncAll(), syncSource(id), syncUserSources(slackUserId)
  • Handles incremental sync with sync tokens and ETags

SyncScheduler (src/jobs/SyncScheduler.js)

  • Cron-based automatic sync (default: every 15 minutes)
  • Configurable via SYNC_CRON environment variable
  • Skips during maintenance mode

CalendarAggregator (src/services/CalendarAggregator.js)

  • Factory for provider instances
  • Caches providers per source (keyed by ${type}-${id})
  • Delegates to provider-specific implementations

GoogleCalendarService (src/services/google-calendar.js)

  • Google Calendar API wrapper with OAuth token management
  • Initializes OAuth2 client from stored tokens per Slack user (initOAuthFromDB())
  • Automatically refreshes expired access tokens and persists new tokens to DB

MicrosoftCalendarService (src/services/microsoft-calendar.js)

  • Microsoft Graph API wrapper with MSAL token management
  • Initializes Graph client from stored tokens per Slack user (initOAuthFromDB())
  • Delegates token refresh to MSAL via refreshMicrosoftTokens()

ICalGenerator (src/services/ICalGenerator.js)

  • Generates iCalendar (.ics) output from stored events
  • Uses ical.js to build VCALENDAR with VEVENT components
  • Supports per-user feed generation via generateForUser(slackUserId)

Provider Pattern

All calendar sources implement BaseProvider (src/providers/BaseProvider.js):

  • initialize(): Authenticate and connect to calendar API
  • fetchEvents(options): Full fetch (returns all events)
  • sync(syncState): Incremental sync (returns { events, deleted, newSyncState })
  • normalizeEvent(rawEvent): Convert to unified format
  • supportsIncrementalSync(): Whether provider can do incremental updates

Provider Implementations:

  • GoogleCalendarProvider - Uses Google Calendar API v3, supports sync tokens
  • MicrosoftCalendarProvider - Uses Microsoft Graph API, supports delta queries
  • ICalRemoteProvider - Fetches remote .ics URLs, uses ETags for caching
  • ICalLocalProvider - Reads local .ics files, uses mtime for change detection

Important: Google and Microsoft providers require OAuth tokens from the oauth_tokens table. These are automatically created during the OAuth callback flow and associated with the Slack user.

Data Models

All models use better-sqlite3 directly (no ORM).

  • Source (src/models/Source.js) - Calendar sources (Google, Microsoft, iCal)

    • User-scoped queries: findBySlackUserId(), createForUser(), deleteForUser()
    • Config stored as JSON string in database
  • Event (src/models/Event.js) - Calendar events from all sources

    • Unified schema with source_id foreign key
    • bulkUpsert() for efficient batch updates
    • Unique constraint on (source_id, external_id)
  • OAuthToken (src/models/OAuthToken.js) - Encrypted OAuth tokens per user

    • Provider field: 'google' or 'microsoft'
    • Tokens encrypted using src/utils/crypto.js (AES-256-GCM)
    • Includes refresh tokens and expiry tracking
  • SyncState (src/models/SyncState.js) - Sync status per source

    • Stores sync tokens, ETags, error states
    • Used for incremental sync
  • FeedToken (src/models/FeedToken.js) - Unique tokens for iCal feed URLs

    • One per Slack user
    • Used in /feed/:token/orbitando.ics endpoint

OAuth Flow

Google Calendar:

  1. User clicks "Connect Google" in Slack
  2. OAuth state includes { slackUserId, slackTeamId, responseUrl }
  3. User redirects to Google OAuth consent screen
  4. Google redirects to /auth/google/callback
  5. Server exchanges code for tokens, stores in oauth_tokens
  6. Auto-creates Google Calendar source if not exists
  7. Triggers immediate sync for user

Microsoft Outlook:

  • Same flow but uses /auth/azure/callback and Microsoft Graph

Key Functions:

  • src/slack/actions/oauth.js - Google OAuth helpers
  • src/slack/actions/microsoft-oauth.js - Microsoft OAuth helpers

Token Encryption

All OAuth tokens are encrypted before storage using src/utils/crypto.js:

  • Algorithm: AES-256-GCM (authenticated encryption)
  • Key derivation: PBKDF2 with SHA-512, 100,000 iterations, unique 64-byte salt per encryption
  • IV: 16 random bytes per encryption
  • Storage format: salt:iv:authTag:ciphertext (all hex-encoded, colon-separated)
  • Master key: 32-byte hex string from TOKEN_ENCRYPTION_KEY env var
  • Methods: encrypt(plaintext, masterKey), decrypt(encryptedData, masterKey), generateEncryptionKey()
  • Generate key: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Slack Commands

/ajustes (src/slack/commands/ajustes.js)

  • Shows connected accounts, calendar sources, and feed URL
  • Buttons to connect Google/Microsoft, manage sources
  • Uses Slack Block Kit interactive messages

/calendario (src/slack/commands/calendario.js)

  • Displays today's and tomorrow's events
  • Fetches from local database (not real-time API calls)
  • Supports timezone per user (stored in oauth_tokens.timezone)

Slack Actions and Modals

Source Management (src/slack/actions/sources.js)

  • Handles source CRUD actions: add, edit, toggle enable/disable, delete
  • Filters ICS sources (Google/Microsoft sources managed via OAuth separately)
  • Validates user ownership before modifications

Source Modal (src/slack/modals/sourceModal.js)

  • Slack modal for adding/editing iCal calendar sources
  • Fields: name, type (ical_remote/ical_local), URL, color
  • Predefined color palette for calendar visual identification

Event Normalization

Calendar providers return diverse formats. Events are normalized to:

{
  source_id: number,
  external_id: string,
  summary: string,
  description: string | null,
  location: string | null,
  start_datetime: string,  // ISO 8601
  end_datetime: string | null,
  all_day: 0 | 1,
  status: string,
  recurrence: string | null,  // JSON
  raw_data: object  // Original event
}

This happens in each provider's normalizeEvent() method. Shared date normalization utilities are in src/utils/eventNormalizer.js.

Timezone Handling

  • Users can set their timezone via Slack modals
  • Stored in oauth_tokens.timezone (defaults to 'UTC')
  • Used when displaying events in Slack and generating iCal feeds
  • Timezone utilities in src/utils/timezone.js

Environment Variables

See .env.example for full list. Critical variables:

  • TOKEN_ENCRYPTION_KEY (required) - 64-char hex for encrypting OAuth tokens
  • GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI
  • GOOGLE_SCOPES - Google OAuth scopes (calendar.readonly, userinfo.email)
  • GOOGLE_CALENDAR_ID - Calendar to sync (default: primary)
  • AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, AZURE_REDIRECT_URI
  • AZURE_SCOPES - Microsoft Graph scopes (Calendars.Read, User.Read, offline_access)
  • SLACK_APP_TOKEN (xapp-...), SLACK_BOT_TOKEN (xoxb-...)
  • SLACK_ADMINS - Comma-separated user IDs with admin privileges
  • SLACK_CHANNEL_ID - Channel ID for notifications (optional)
  • SYNC_CRON - Cron expression (default: 0 */15 * * * *)
  • SYNC_ON_STARTUP - Whether to sync on server start (default: true)
  • MAINTENANCE_MODE - Set to true to pause sync and gate Slack commands
  • BASE_URL - Used to generate iCal feed URLs
  • DATABASE_PATH - SQLite database location (default: ./data/calendar.db)

Testing Sync Manually

To test synchronization without waiting for cron:

  1. Use /ajustes in Slack, connect your accounts
  2. Check logs for sync activity
  3. Or trigger via health endpoint: curl http://localhost:3000/health
  4. Or manually call syncService.syncAll() in code

Database Schema

SQLite schema in src/database/schema.sql:

  • sources - Calendar sources (type: google | microsoft | ical_remote | ical_local)
  • events - Unified events from all sources
  • sync_state - Sync status per source (tokens, errors, counts)
  • oauth_tokens - Encrypted OAuth credentials per Slack user + provider
  • feed_tokens - Unique tokens for iCal feed URLs

Migrations handled in src/config/database.js runMigrations().

Maintenance Mode

The system supports maintenance mode to pause syncing:

  • Check: isMaintenanceMode() in src/utils/maintenance.js
  • When active, SyncScheduler skips sync jobs
  • Slack commands can also be gated (see src/slack/middleware/maintenanceMiddleware.js)

Slack User Context

Many operations are scoped to Slack users:

  • Each Slack user has independent OAuth tokens for Google/Microsoft
  • Sources are linked to slack_user_id
  • Events are queried via source → Slack user relationship
  • iCal feeds are user-specific via unique tokens

When implementing features:

  • Always validate user ownership of sources before operations
  • Use Source.findByIdAndUser(id, slackUserId) for user-scoped queries
  • Use Event.findBySlackUserId(slackUserId) to get a user's events

Code Style

  • ES modules (import/export, "type": "module" in package.json)
  • ESLint config in eslint.config.js
  • Async/await throughout (no callbacks)
  • Singleton pattern for services (SyncService, SyncScheduler)
  • Factory pattern for providers (CalendarAggregator)
  • Active Record-style models (static methods + instance data)

Project Structure

src/
├── index.js                          # Express server, routes, startup
├── config/
│   └── database.js                   # SQLite initialization and migrations
├── database/
│   └── schema.sql                    # Database schema
├── jobs/
│   └── SyncScheduler.js             # Cron-based sync scheduling
├── models/
│   ├── Event.js                      # Calendar events
│   ├── FeedToken.js                  # iCal feed tokens per user
│   ├── OAuthToken.js                 # Encrypted OAuth tokens
│   ├── Source.js                     # Calendar sources
│   └── SyncState.js                  # Sync status per source
├── providers/
│   ├── BaseProvider.js               # Abstract base class
│   ├── GoogleCalendarProvider.js     # Google Calendar API v3
│   ├── MicrosoftCalendarProvider.js  # Microsoft Graph API
│   ├── ICalRemoteProvider.js         # Remote .ics URL fetching
│   └── ICalLocalProvider.js          # Local .ics file reading
├── scripts/
│   └── generate-tokens.js           # Legacy token script (deprecated)
├── services/
│   ├── CalendarAggregator.js         # Provider factory and cache
│   ├── google-calendar.js            # Google Calendar API service
│   ├── microsoft-calendar.js         # Microsoft Graph API service
│   ├── ICalGenerator.js              # iCal feed generation
│   └── SyncService.js                # Sync orchestration (singleton)
├── slack/
│   ├── app.js                        # Slack Bolt app setup
│   ├── actions/
│   │   ├── oauth.js                  # Google OAuth flow helpers
│   │   ├── microsoft-oauth.js        # Microsoft OAuth flow helpers
│   │   └── sources.js                # Source management actions
│   ├── commands/
│   │   ├── ajustes.js                # /ajustes command
│   │   └── calendario.js             # /calendario command
│   ├── middleware/
│   │   └── maintenanceMiddleware.js  # Maintenance mode gating
│   └── modals/
│       └── sourceModal.js            # Add/edit iCal source modal
└── utils/
    ├── crypto.js                     # AES-256-GCM encryption with PBKDF2
    ├── eventNormalizer.js            # Date/event normalization utilities
    ├── maintenance.js                # Maintenance mode management
    └── timezone.js                   # Timezone utilities

Error Handling Patterns

  • Consistent [Tag] prefix logging throughout (e.g., [GoogleCalendar], [SyncService], [Maintenance])
  • Try-catch blocks in all async operations with descriptive error messages
  • Sync errors are recorded in sync_state table (last_sync_status, last_error)
  • OAuth token refresh failures are logged and propagated to the sync layer

Deployment Notes

  • Uses Node 22 watch mode in development (--watch)
  • Docker image based on node:22 (full Debian image, not Alpine) with libsqlite3-dev
  • Requires persistent volume for SQLite database (./data)
  • Docker Compose maps port 3030 (host) → 3000 (container)
  • Slack Socket Mode means no inbound webhooks needed (firewall-friendly)
  • OAuth callbacks require public URLs (use ngrok for local dev)
  • .nvmrc specifies Node 22 for version management