Files
vercel__chat/INSTALLATION_PLAN.md
2026-01-06 14:46:51 -08:00

16 KiB

Native App Installation Plan

This document outlines how to make Chat SDK bots installable as native apps on each platform, with the goal of minimizing setup friction and supporting multi-tenant deployments.

Overview

Platform Current State Target State
Slack Single workspace, manual token "Add to Slack" button, multi-workspace
Teams Manual manifest + sideload Manifest generator, org deployment
Google Chat Service account + impersonation Setup wizard, Marketplace ready
Discord Manual token + interactions only Invite link generator, full Gateway

Slack: "Add to Slack" Flow

Current Limitations

  • Requires manually copying xoxb- token from Slack app settings
  • Single workspace only
  • No OAuth callback handling

Target Experience

1. Developer configures Client ID + Client Secret
2. SDK generates "Add to Slack" button/URL
3. User clicks → authorizes → redirected back
4. SDK stores workspace token automatically
5. Bot works in that workspace immediately

Implementation

New Config Options:

interface SlackAdapterConfig {
  // Existing (single workspace)
  botToken?: string;
  signingSecret: string;

  // New (multi-workspace distribution)
  oauth?: {
    clientId: string;
    clientSecret: string;
    scopes: string[];           // Bot scopes
    redirectUri: string;
    installationStore: InstallationStore;
  };
}

Installation Store Interface:

interface InstallationStore {
  // Store installation when user authorizes
  storeInstallation(installation: SlackInstallation): Promise<void>;

  // Fetch installation by team ID (from incoming webhook)
  fetchInstallation(teamId: string): Promise<SlackInstallation | null>;

  // Remove installation (bot removed from workspace)
  deleteInstallation(teamId: string): Promise<void>;
}

interface SlackInstallation {
  teamId: string;
  teamName: string;
  botToken: string;        // xoxb-...
  botUserId: string;
  botId: string;
  installedAt: Date;
  installerUserId?: string;
}

New Adapter Methods:

interface SlackAdapter {
  // Generate "Add to Slack" URL
  getInstallUrl(state?: string): string;

  // Handle OAuth callback (exchange code for token)
  handleInstallCallback(request: Request): Promise<SlackInstallation>;

  // Webhook handler now looks up token per workspace
  handleWebhook(request: Request): Promise<Response>;
}

Route Setup:

// app/api/slack/install/route.ts
export async function GET() {
  const url = slack.getInstallUrl();
  return Response.redirect(url);
}

// app/api/slack/oauth/callback/route.ts
export async function GET(request: Request) {
  const installation = await slack.handleInstallCallback(request);
  return Response.redirect('/installed?team=' + installation.teamId);
}

Required Scopes

app_mentions:read    - Receive @mentions
channels:history     - Read channel messages (for subscribed threads)
channels:read        - List channels
chat:write           - Send messages
groups:history       - Read private channel messages
groups:read          - List private channels
im:history           - Read DM messages
im:read              - List DMs
reactions:read       - Receive reaction events
reactions:write      - Add reactions
users:read           - Get user info

Multi-Workspace Webhook Handling

async handleWebhook(request: Request): Promise<Response> {
  // 1. Verify signature (uses signing secret, same for all workspaces)
  await this.verifySignature(request);

  // 2. Parse payload to get team_id
  const payload = await this.parsePayload(request);
  const teamId = payload.team_id;

  // 3. Fetch installation (gets workspace-specific bot token)
  const installation = await this.oauth.installationStore.fetchInstallation(teamId);
  if (!installation) {
    return new Response('App not installed in this workspace', { status: 404 });
  }

  // 4. Use installation's bot token for API calls
  this.currentBotToken = installation.botToken;

  // 5. Process message as normal
  return this.processWebhook(payload);
}

Microsoft Teams: Manifest Generator

Current Limitations

  • Manual manifest.json creation
  • Must manually zip with icons
  • Complex Azure setup

Target Experience

1. Developer provides Azure credentials
2. SDK generates manifest.json
3. SDK provides icons or uses defaults
4. Developer uploads zip to Teams

Implementation

Manifest Generator:

interface TeamsManifestConfig {
  // Required
  name: string;
  description: {
    short: string;   // Max 80 chars
    full: string;    // Max 4000 chars
  };

  // Azure credentials
  appId: string;     // Microsoft App ID

  // Optional
  accentColor?: string;
  developerName?: string;
  developerUrl?: string;
  privacyUrl?: string;
  termsOfUseUrl?: string;

  // Bot configuration
  botEndpoint: string;  // e.g., https://yourapp.com/api/webhooks/teams

  // Scopes: where can bot be used?
  scopes?: ('personal' | 'team' | 'groupchat')[];
}

function generateTeamsManifest(config: TeamsManifestConfig): TeamsManifest {
  return {
    "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
    "manifestVersion": "1.17",
    "version": "1.0.0",
    "id": config.appId,
    "developer": {
      "name": config.developerName ?? "Developer",
      "websiteUrl": config.developerUrl ?? "https://example.com",
      "privacyUrl": config.privacyUrl ?? "https://example.com/privacy",
      "termsOfUseUrl": config.termsOfUseUrl ?? "https://example.com/terms"
    },
    "name": {
      "short": config.name,
      "full": config.name
    },
    "description": config.description,
    "accentColor": config.accentColor ?? "#5558AF",
    "bots": [{
      "botId": config.appId,
      "scopes": config.scopes ?? ["personal", "team", "groupchat"],
      "supportsFiles": false,
      "isNotificationOnly": false,
      "commandLists": []
    }],
    "permissions": ["identity", "messageTeamMembers"],
    "validDomains": [new URL(config.botEndpoint).hostname]
  };
}

Package Generator:

async function createTeamsPackage(
  manifest: TeamsManifest,
  icons?: { outline: Buffer; color: Buffer }
): Promise<Buffer> {
  const zip = new JSZip();

  zip.file('manifest.json', JSON.stringify(manifest, null, 2));
  zip.file('outline.png', icons?.outline ?? DEFAULT_OUTLINE_ICON);
  zip.file('color.png', icons?.color ?? DEFAULT_COLOR_ICON);

  return zip.generateAsync({ type: 'nodebuffer' });
}

CLI or Export:

// In adapter
class TeamsAdapter {
  generateManifest(config: TeamsManifestConfig): TeamsManifest;

  async createPackage(
    config: TeamsManifestConfig,
    icons?: { outline: Buffer; color: Buffer }
  ): Promise<Buffer>;

  // Writes manifest.json and package.zip to disk
  async exportPackage(
    config: TeamsManifestConfig,
    outputDir: string
  ): Promise<void>;
}

Multi-Tenant Support (Already Implemented)

Teams adapter already supports multi-tenant via appTenantId:

  • undefined or "common" = works across all organizations
  • Specific tenant ID = single organization only

Google Chat: Setup Wizard

Current Limitations

  • Complex GCP project setup
  • Service account key management
  • Domain-wide delegation requires admin
  • Pub/Sub setup is manual

Target Experience

1. SDK provides step-by-step GCP setup guide
2. SDK validates credentials
3. SDK auto-detects Pub/Sub configuration
4. SDK provides health check endpoint

Implementation

Validation Helpers:

class GoogleChatAdapter {
  // Validate credentials can authenticate
  async validateCredentials(): Promise<{
    valid: boolean;
    projectId?: string;
    serviceAccountEmail?: string;
    errors?: string[];
  }>;

  // Check API access
  async checkApiAccess(): Promise<{
    chatApi: boolean;
    pubsubApi: boolean;
    workspaceEventsApi: boolean;
    errors?: string[];
  }>;

  // Validate Pub/Sub subscription
  async validatePubSubSetup(): Promise<{
    topicExists: boolean;
    subscriptionExists: boolean;
    pushEndpointConfigured: boolean;
    errors?: string[];
  }>;

  // Health check endpoint
  async healthCheck(): Promise<HealthCheckResult>;
}

Setup Guide Generator:

function generateSetupGuide(config: {
  projectId: string;
  webhookUrl: string;
  appName: string;
}): SetupGuide {
  return {
    steps: [
      {
        title: "Create GCP Project",
        url: `https://console.cloud.google.com/projectcreate`,
        instructions: "Create a new project or select existing"
      },
      {
        title: "Enable APIs",
        url: `https://console.cloud.google.com/apis/library?project=${config.projectId}`,
        apis: ["Google Chat API", "Cloud Pub/Sub API", "Google Workspace Events API"]
      },
      {
        title: "Create Service Account",
        url: `https://console.cloud.google.com/iam-admin/serviceaccounts/create?project=${config.projectId}`,
        instructions: "Create service account with no additional permissions"
      },
      {
        title: "Download Key",
        instructions: "Create JSON key and set as GOOGLE_CHAT_CREDENTIALS env var"
      },
      {
        title: "Configure Chat App",
        url: `https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat?project=${config.projectId}`,
        settings: {
          name: config.appName,
          avatarUrl: "optional",
          description: "optional",
          interactiveFeatures: true,
          httpEndpoint: config.webhookUrl
        }
      },
      // Pub/Sub steps if needed...
    ]
  };
}

Workspace Marketplace (Future)

For public distribution:

  • Requires Google Workspace Marketplace listing
  • App must pass security review
  • Can enable admin-controlled deployment

Current Limitations

  • Manual bot token setup
  • No invite URL generation
  • Interactions only (no message receiving without Gateway)

Target Experience

1. Developer provides Application ID + Bot Token
2. SDK generates invite URL with correct permissions
3. User clicks → adds to server
4. Bot receives messages via Gateway (serverless-compatible)

Implementation

Invite URL Generator:

interface DiscordInviteOptions {
  applicationId: string;

  // Permissions (SDK provides sensible defaults)
  permissions?: DiscordPermissions;

  // Scopes
  scopes?: ('bot' | 'applications.commands')[];

  // Pre-select guild (optional)
  guildId?: string;

  // Disable guild select (for specific guild only)
  disableGuildSelect?: boolean;
}

interface DiscordPermissions {
  sendMessages?: boolean;        // 2048
  sendMessagesInThreads?: boolean; // 274877906944
  createPublicThreads?: boolean;   // 34359738368
  readMessageHistory?: boolean;    // 65536
  addReactions?: boolean;          // 64
  useExternalEmojis?: boolean;     // 262144
  mentionEveryone?: boolean;       // 131072
  manageMessages?: boolean;        // 8192
  embedLinks?: boolean;            // 16384
  attachFiles?: boolean;           // 32768
  viewChannel?: boolean;           // 1024
  // ... more as needed
}

function generateInviteUrl(options: DiscordInviteOptions): string {
  const permissions = calculatePermissionsBitfield(options.permissions ?? {
    sendMessages: true,
    sendMessagesInThreads: true,
    readMessageHistory: true,
    addReactions: true,
    viewChannel: true,
  });

  const scopes = options.scopes ?? ['bot', 'applications.commands'];

  const params = new URLSearchParams({
    client_id: options.applicationId,
    permissions: permissions.toString(),
    scope: scopes.join(' '),
  });

  if (options.guildId) {
    params.set('guild_id', options.guildId);
  }
  if (options.disableGuildSelect) {
    params.set('disable_guild_select', 'true');
  }

  return `https://discord.com/api/oauth2/authorize?${params}`;
}

Adapter Method:

class DiscordAdapter {
  // Generate invite URL
  getInviteUrl(options?: Partial<DiscordInviteOptions>): string {
    return generateInviteUrl({
      applicationId: this.applicationId,
      ...options
    });
  }

  // Get required permissions for full functionality
  getRequiredPermissions(): DiscordPermissions {
    return {
      sendMessages: true,
      sendMessagesInThreads: true,
      createPublicThreads: true,
      readMessageHistory: true,
      addReactions: true,
      viewChannel: true,
    };
  }
}

Gateway for Serverless (Already Implemented)

The Gateway listener with cron job is already implemented:

  • startGatewayListener() connects to Discord Gateway
  • Cron job keeps it running in serverless
  • Redis pub/sub coordinates across instances

Shared Infrastructure

Installation Store Interface

All platforms need similar storage for multi-tenant:

interface InstallationStore<T> {
  store(id: string, installation: T): Promise<void>;
  fetch(id: string): Promise<T | null>;
  delete(id: string): Promise<void>;
  list(): AsyncIterable<T>;
}

// Platform-specific implementations
type SlackInstallationStore = InstallationStore<SlackInstallation>;
type TeamsInstallationStore = InstallationStore<TeamsInstallation>;
type DiscordInstallationStore = InstallationStore<DiscordInstallation>;
// Google Chat uses service account per org, different model

Redis Installation Store

function createRedisInstallationStore<T>(
  redis: RedisClient,
  prefix: string
): InstallationStore<T> {
  return {
    async store(id, installation) {
      await redis.set(`${prefix}:${id}`, JSON.stringify(installation));
    },
    async fetch(id) {
      const data = await redis.get(`${prefix}:${id}`);
      return data ? JSON.parse(data) : null;
    },
    async delete(id) {
      await redis.del(`${prefix}:${id}`);
    },
    async *list() {
      for await (const key of redis.scanIterator({ match: `${prefix}:*` })) {
        const data = await redis.get(key);
        if (data) yield JSON.parse(data);
      }
    }
  };
}

Implementation Phases

Phase 1: Discord Invite URL (Simplest)

  • Add getInviteUrl() method
  • Document required permissions
  • Already has Gateway support

Phase 2: Teams Manifest Generator

  • Add manifest generation utility
  • Add package creation (zip)
  • Add default icons
  • Document upload process

Phase 3: Slack OAuth Install Flow

  • Add OAuth configuration
  • Implement getInstallUrl() and handleInstallCallback()
  • Add InstallationStore interface
  • Multi-workspace webhook routing

Phase 4: Google Chat Setup Helpers

  • Add credential validation
  • Add API access checks
  • Generate setup guide
  • Health check endpoint

File Structure

packages/
├── adapter-slack/
│   ├── src/
│   │   ├── index.ts          # Add OAuth methods
│   │   ├── oauth.ts          # OAuth flow implementation
│   │   └── installation.ts   # InstallationStore types
│   └── README.md             # Update with install flow
├── adapter-teams/
│   ├── src/
│   │   ├── index.ts          # Add manifest methods
│   │   └── manifest.ts       # Manifest generator
│   ├── assets/
│   │   ├── outline.png       # Default icon
│   │   └── color.png         # Default icon
│   └── README.md             # Update with manifest guide
├── adapter-gchat/
│   ├── src/
│   │   ├── index.ts          # Add validation methods
│   │   └── setup.ts          # Setup helpers
│   └── README.md             # Update with setup guide
└── adapter-discord/
    ├── src/
    │   ├── index.ts          # Add getInviteUrl
    │   └── permissions.ts    # Permission helpers
    └── README.md             # Update with invite guide

Success Criteria

Platform Metric
Slack User can install bot to workspace with 2 clicks
Teams Developer can generate valid manifest in 1 command
Google Chat Validation catches 90% of setup issues
Discord Invite URL generated with correct permissions

Open Questions

  1. Slack App Directory: Should SDK help with directory listing requirements?

  2. Teams App Store: Should SDK support publishing to Teams App Store?

  3. Default Icons: Should SDK include default bot icons for all platforms?

  4. Installation Events: How to notify app when bot is added/removed from workspace?

  5. Credential Rotation: How to handle token refresh and credential rotation?