buster/slack-oauth-integration-prd.md

1080 lines
30 KiB
Markdown

# Slack OAuth Integration - Product Requirements Document
## Executive Summary
This document outlines the requirements for implementing Slack OAuth integration in the Buster application, enabling users to connect their Slack workspaces and utilize Slack messaging capabilities within the platform.
## Overview
### Purpose
Enable Buster users to securely authenticate with Slack workspaces, allowing the application to:
- Send messages to Slack channels
- List available channels
- Manage message threads
- Track message delivery
### Scope
- OAuth 2.0 authentication flow
- Token management and storage
- Database schema for Slack integrations
- API endpoints for OAuth flow
- Security and compliance considerations
## Technical Architecture
### Components
#### 1. **@buster/slack Package** (Existing)
- Standalone Slack integration with no database dependencies
- Provides OAuth flow management, channel operations, and messaging
- Interface-based design requiring implementation of storage interfaces
#### 2. **@apps/server** (To be implemented)
- OAuth initiation endpoint
- OAuth callback endpoint
- Integration management endpoints
#### 3. **@packages/database** (Schema additions required)
- Slack integration records
- Token storage (via Supabase Vault)
- OAuth state management
## Detailed Requirements
### OAuth Flow
#### 1. OAuth Initiation
**Endpoint:** `POST /api/v2/slack/auth/init`
**Request Body (Optional):**
```json
{
"metadata": {
"returnUrl": "/settings/integrations", // Where to redirect after OAuth completes
"source": "settings_page", // Analytics tracking
"projectId": "uuid" // Optional project context
}
}
```
**Response:**
```json
{
"authUrl": "https://slack.com/oauth/v2/authorize?...",
"state": "secure-random-state"
}
```
**Metadata Explained:**
The `metadata` object is completely optional and serves to maintain context through the OAuth flow:
1. **returnUrl** - Where in your app to redirect after OAuth completes
- Default: `/settings/integrations`
- Example: `/projects/123/settings` to return to specific project
2. **source** - Track where user initiated OAuth from
- Examples: `"onboarding"`, `"settings_page"`, `"project_view"`
- Useful for analytics and user flow optimization
3. **projectId** - Associate integration with specific context
- Optional: Since one org = one Slack integration
- Useful if you want to track which project triggered the integration
4. **Custom fields** - Any additional context your app needs
**How it works:**
```typescript
// The metadata gets stored in the pending integration record
{
id: "integration-id",
oauthState: "secure-random-state",
oauthMetadata: {
returnUrl: "/settings/integrations",
source: "settings_page",
projectId: "project-123",
// System adds:
initiatedAt: "2024-01-01T00:00:00Z",
ipAddress: "192.168.1.1"
},
status: "pending"
}
// After successful OAuth, use metadata to redirect:
const returnUrl = integration.oauthMetadata.returnUrl || '/settings';
return c.redirect(`${returnUrl}?integration=success`);
```
**Requirements:**
- Metadata is entirely optional (endpoint works without it)
- Stored in `oauth_metadata` JSONB column
- Available during callback to restore user context
- Cleared after successful OAuth
- Size limit: 1KB to prevent abuse
#### 2. OAuth Callback
**Endpoint:** `GET /api/v2/slack/auth/callback`
**Authentication:** This endpoint should be **unauthenticated** because:
- Slack redirects here directly after OAuth approval
- The user's session might have expired during the OAuth flow
- We use the `state` parameter to map back to the user/organization
**Query Parameters:**
- `code`: Authorization code from Slack
- `state`: State parameter that maps to our pending integration
**Flow:**
1. Look up pending integration by `state` parameter
2. Verify state hasn't expired (15-minute window)
3. Exchange code for access token with Slack
4. Update integration record with Slack workspace info
5. Store token in Supabase Vault
6. Redirect to success page (from metadata.returnUrl)
**Success Response (Redirect):**
```
302 Redirect to: /settings/integrations?status=success&workspace=Acme%20Corp
```
**Error Response (Redirect):**
```
302 Redirect to: /settings/integrations?status=error&error=access_denied
```
### Database Schema (Drizzle)
Add the following to `@packages/database/src/schema.ts`:
```typescript
// Enum for Slack integration status
export const slackIntegrationStatusEnum = pgEnum('slack_integration_status_enum', [
'pending',
'active',
'failed',
'revoked'
]);
// Slack integrations table
export const slackIntegrations = pgTable(
'slack_integrations',
{
id: uuid().defaultRandom().primaryKey().notNull(),
organizationId: uuid('organization_id').notNull(),
userId: uuid('user_id').notNull(),
// OAuth state fields (for pending integrations)
oauthState: varchar('oauth_state', { length: 255 }).unique(),
oauthExpiresAt: timestamp('oauth_expires_at', { withTimezone: true, mode: 'string' }),
oauthMetadata: jsonb('oauth_metadata').default({}),
// Slack workspace info (populated after successful OAuth)
teamId: varchar('team_id', { length: 255 }),
teamName: varchar('team_name', { length: 255 }),
teamDomain: varchar('team_domain', { length: 255 }),
enterpriseId: varchar('enterprise_id', { length: 255 }),
// Bot info
botUserId: varchar('bot_user_id', { length: 255 }),
scope: text(),
// Token reference (actual token in Supabase Vault)
tokenVaultKey: varchar('token_vault_key', { length: 255 }).unique(),
// Metadata
installedBySlackUserId: varchar('installed_by_slack_user_id', { length: 255 }),
installedAt: timestamp('installed_at', { withTimezone: true, mode: 'string' }),
lastUsedAt: timestamp('last_used_at', { withTimezone: true, mode: 'string' }),
status: slackIntegrationStatusEnum().default('pending').notNull(),
// Timestamps
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.defaultNow()
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' })
.defaultNow()
.notNull(),
deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }),
},
(table) => [
foreignKey({
columns: [table.organizationId],
foreignColumns: [organizations.id],
name: 'slack_integrations_organization_id_fkey',
}).onDelete('cascade'),
foreignKey({
columns: [table.userId],
foreignColumns: [users.id],
name: 'slack_integrations_user_id_fkey',
}),
unique('slack_integrations_org_team_key').on(table.organizationId, table.teamId),
index('idx_slack_integrations_org_id').using(
'btree',
table.organizationId.asc().nullsLast().op('uuid_ops')
),
index('idx_slack_integrations_team_id').using(
'btree',
table.teamId.asc().nullsLast().op('text_ops')
),
index('idx_slack_integrations_oauth_state').using(
'btree',
table.oauthState.asc().nullsLast().op('text_ops')
),
index('idx_slack_integrations_oauth_expires').using(
'btree',
table.oauthExpiresAt.asc().nullsLast().op('timestamptz_ops')
),
check(
'slack_integrations_status_check',
sql`(status = 'pending' AND oauth_state IS NOT NULL) OR (status != 'pending' AND team_id IS NOT NULL)`
),
]
);
// Slack message tracking table (optional)
export const slackMessageTracking = pgTable(
'slack_message_tracking',
{
id: uuid().defaultRandom().primaryKey().notNull(),
integrationId: uuid('integration_id').notNull(),
// Internal reference
internalMessageId: uuid('internal_message_id').notNull().unique(),
// Slack references
slackChannelId: varchar('slack_channel_id', { length: 255 }).notNull(),
slackMessageTs: varchar('slack_message_ts', { length: 255 }).notNull(),
slackThreadTs: varchar('slack_thread_ts', { length: 255 }),
// Metadata
messageType: varchar('message_type', { length: 50 }).notNull(), // 'message', 'reply', 'update'
content: text(),
senderInfo: jsonb('sender_info'),
// Timestamps
sentAt: timestamp('sent_at', { withTimezone: true, mode: 'string' }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.defaultNow()
.notNull(),
},
(table) => [
foreignKey({
columns: [table.integrationId],
foreignColumns: [slackIntegrations.id],
name: 'slack_message_tracking_integration_id_fkey',
}).onDelete('cascade'),
index('idx_message_tracking_integration').using(
'btree',
table.integrationId.asc().nullsLast().op('uuid_ops')
),
index('idx_message_tracking_channel').using(
'btree',
table.slackChannelId.asc().nullsLast().op('text_ops')
),
index('idx_message_tracking_thread').using(
'btree',
table.slackThreadTs.asc().nullsLast().op('text_ops')
),
]
);
```
### API Implementation Structure
#### File Organization
```
@apps/server/src/api/v2/slack/
├── index.ts # Route definitions
├── index.test.ts # Route tests
├── handler.ts # Request handlers
├── handler.test.ts # Unit tests
├── handler.integration.test.ts # Integration tests
└── services/ # Business logic
├── slack-oauth-service.ts # OAuth flow logic
├── slack-oauth-service.test.ts # Service unit tests
├── slack-helpers.ts # Utility functions
└── slack-helpers.test.ts # Helper tests
```
#### API Endpoints
Since each organization will only have one Slack integration, we need just these endpoints:
**1. Initiate OAuth Flow**
`POST /api/v2/slack/auth`
- Creates pending integration record
- Generates OAuth URL with state
**2. OAuth Callback**
`GET /api/v2/slack/auth/callback`
- Handles redirect from Slack
- Exchanges code for token
- Updates integration to active
**3. Get Current Integration**
`GET /api/v2/slack/integration`
- Returns current integration status
- Returns null if no integration exists
**4. Remove Integration**
`DELETE /api/v2/slack/integration`
- Revokes token from Supabase Vault
- Soft deletes integration record
#### File Responsibilities & Separation of Concerns
**index.ts** - Pure routing, no business logic
```typescript
// ONLY route definitions and middleware
const app = new Hono()
.post('/auth', authMiddleware, handler.initiateOAuth)
.get('/auth/callback', handler.handleOAuthCallback)
.get('/integration', authMiddleware, handler.getIntegration)
.delete('/integration', authMiddleware, handler.removeIntegration);
export default app;
```
**handler.ts** - Thin HTTP layer, delegates to services
```typescript
// Each handler is a thin wrapper that:
// 1. Extracts and validates input
// 2. Calls the appropriate service function
// 3. Formats the response
export const initiateOAuth = async (c: Context) => {
const { organizationId, userId } = c.get('auth');
// Delegate to service - handler doesn't contain business logic
const result = await slackOAuthService.initiateOAuth({
organizationId,
userId,
});
return c.json(result);
};
```
**services/slack-oauth-service.ts** - Business logic in pure, testable functions
```typescript
// Pure functions that can be tested independently
export async function initiateOAuth(params: {
organizationId: string;
userId: string;
}): Promise<{ authUrl: string; state: string }> {
// Check for existing integration
const existing = await slackHelpers.getActiveIntegration(params.organizationId);
if (existing) {
throw new Error('Integration already exists');
}
// Generate OAuth URL
const { authUrl, state } = await generateOAuthUrl(params);
// Store pending integration
await slackHelpers.createPendingIntegration({
...params,
oauthState: state,
});
return { authUrl, state };
}
// Each function does ONE thing and is independently testable
export async function generateOAuthUrl(params: {
organizationId: string;
userId: string;
}): Promise<{ authUrl: string; state: string }> {
// Pure OAuth URL generation logic
}
export async function exchangeCodeForToken(params: {
code: string;
state: string;
}): Promise<SlackTokenResponse> {
// Pure token exchange logic
}
```
**services/slack-helpers.ts** - Database operations as pure functions
```typescript
// Each helper is a focused, testable function
export async function getActiveIntegration(
organizationId: string
): Promise<SlackIntegration | null> {
return db
.select()
.from(slackIntegrations)
.where(and(
eq(slackIntegrations.organizationId, organizationId),
eq(slackIntegrations.status, 'active'),
isNull(slackIntegrations.deletedAt)
))
.limit(1)
.then(rows => rows[0] || null);
}
export async function createPendingIntegration(params: {
organizationId: string;
userId: string;
oauthState: string;
}): Promise<string> {
const [integration] = await db
.insert(slackIntegrations)
.values({
...params,
status: 'pending',
oauthExpiresAt: new Date(Date.now() + 15 * 60 * 1000),
})
.returning({ id: slackIntegrations.id });
return integration.id;
}
export async function storeTokenInVault(
integrationId: string,
token: string
): Promise<string> {
// Pure function for vault storage
const vaultKey = `slack-token-${integrationId}`;
await supabaseVault.store(vaultKey, token);
return vaultKey;
}
```
#### Key Design Principles
1. **Single Responsibility** - Each function does ONE thing
2. **Pure Functions** - Functions return values based on inputs, minimal side effects
3. **Dependency Injection** - Pass dependencies as parameters for easy mocking
4. **No Business Logic in Handlers** - Handlers only handle HTTP concerns
5. **Testable in Isolation** - Each function can be unit tested independently
#### Testing Structure
**Unit Tests (*.test.ts)**
- Mock external dependencies
- Test individual functions in isolation
- Focus on business logic correctness
- Mock database and Slack API calls
**Integration Tests (*.integration.test.ts)**
- Use test database
- Test full request/response cycle
- Mock only external APIs (Slack)
- Verify database state changes
**Test Utilities**
- Shared test factories for creating test data
- Mock implementations of interfaces
- Test database setup/teardown helpers
### Security Requirements
#### Token Storage
- Access tokens stored in Supabase Vault
- Vault key stored in database, not the token itself
- Tokens encrypted at rest
- Support for token rotation
#### Access Control
- Integration scoped to organization
- User must have appropriate permissions
- Audit logging for all OAuth operations
- Rate limiting on OAuth endpoints
#### OAuth Security
- State parameter validation with 15-minute expiry
- HTTPS required for all OAuth endpoints
- Redirect URI whitelist validation
- PKCE support for enhanced security (future)
### Implementation Interfaces
#### ISlackTokenStorage Implementation
```typescript
class DatabaseTokenStorage implements ISlackTokenStorage {
async storeToken(key: string, token: string): Promise<void> {
// Store in Supabase Vault
// Update slack_integrations.token_vault_key
}
async getToken(key: string): Promise<string | null> {
// Retrieve from Supabase Vault
}
async deleteToken(key: string): Promise<void> {
// Remove from Vault
// Clear slack_integrations.token_vault_key
}
async hasToken(key: string): Promise<boolean> {
// Check if token exists in Vault
}
}
```
#### ISlackOAuthStateStorage Implementation
```typescript
class DatabaseOAuthStateStorage implements ISlackOAuthStateStorage {
async storeState(state: string, data: SlackOAuthStateData): Promise<void> {
// Create a pending integration record with OAuth state
await db.insert(slackIntegrations).values({
userId: data.metadata.userId,
organizationId: data.metadata.organizationId,
oauth_state: state,
oauth_expires_at: new Date(data.expiresAt),
oauth_metadata: data.metadata,
status: 'pending'
});
}
async getState(state: string): Promise<SlackOAuthStateData | null> {
// Query slack_integrations for pending OAuth
const integration = await db
.select()
.from(slackIntegrations)
.where(and(
eq(slackIntegrations.oauth_state, state),
eq(slackIntegrations.status, 'pending'),
gt(slackIntegrations.oauth_expires_at, new Date())
))
.limit(1);
if (!integration[0]) return null;
return {
expiresAt: integration[0].oauth_expires_at.getTime(),
metadata: integration[0].oauth_metadata
};
}
async deleteState(state: string): Promise<void> {
// Clean up failed/expired OAuth attempts
await db
.delete(slackIntegrations)
.where(and(
eq(slackIntegrations.oauth_state, state),
eq(slackIntegrations.status, 'pending')
));
}
}
```
### Slack App Configuration
#### Required OAuth Scopes
- `channels:read` - List channels in workspace
- `chat:write` - Send messages as bot
- `chat:write.public` - Send to channels bot hasn't joined
- `channels:join` - Join public channels
- `users:read` - Read user information
#### OAuth Redirect URLs
- Development: `http://localhost:3000/api/v2/slack/auth/callback`
- Staging: `https://staging.buster.so/api/v2/slack/auth/callback`
- Production: `https://app.buster.so/api/v2/slack/auth/callback`
### Error Handling
#### OAuth Errors
- `OAUTH_ACCESS_DENIED` - User denied authorization
- `OAUTH_INVALID_STATE` - Invalid or expired state
- `OAUTH_TOKEN_EXCHANGE_FAILED` - Failed to exchange code
- `INVALID_SCOPE` - Required scopes not granted
- `WORKSPACE_LIMIT_REACHED` - Organization workspace limit
#### Integration Errors
- `INTEGRATION_NOT_FOUND` - Integration doesn't exist
- `INTEGRATION_INACTIVE` - Integration has been deactivated
- `INVALID_CHANNEL` - Channel not accessible
- `RATE_LIMITED` - Slack API rate limit
### Monitoring & Analytics
#### Metrics to Track
- OAuth flow completion rate
- Time to complete OAuth flow
- Integration usage by organization
- Message send success rate
- Channel access patterns
- Error rates by type
#### Audit Events
- OAuth flow initiated
- OAuth flow completed/failed
- Integration created/deleted
- Token refreshed
- Message sent/failed
- Permission changes
### User Experience
#### Integration Flow
1. User navigates to Integrations page
2. Clicks "Connect Slack"
3. Redirected to Slack OAuth page
4. Approves permissions
5. Redirected back to Buster
6. See success message with workspace info
7. Can immediately start using integration
#### Management Interface
- List all connected workspaces
- Show connection status
- Last used timestamp
- Quick actions (test, disconnect)
- Channel browser
- Message history viewer
### Future Enhancements
#### Phase 2
- Incoming webhooks support
- Event subscriptions (message reactions, new messages)
- Slash commands
- Interactive components (buttons, modals)
- Multiple workspace per organization
#### Phase 3
- Scheduled messages
- Message formatting builder
- Template library
- Bulk operations
- Analytics dashboard
### Testing Requirements
#### Test File Coverage
**index.test.ts**
- Route registration verification
- Middleware application checks
- Route parameter validation
**handler.test.ts** - Test HTTP layer only
```typescript
describe('SlackHandler', () => {
// Mock the service layer
const mockSlackOAuthService = {
initiateOAuth: vi.fn(),
handleCallback: vi.fn(),
getIntegration: vi.fn(),
removeIntegration: vi.fn(),
};
describe('initiateOAuth', () => {
it('should extract auth context and call service');
it('should return 200 with auth URL');
it('should handle service errors appropriately');
});
describe('handleOAuthCallback', () => {
it('should validate query parameters');
it('should call service with code and state');
it('should redirect on success');
it('should handle errors with proper status codes');
});
});
```
**services/slack-oauth-service.test.ts** - Test business logic
```typescript
describe('SlackOAuthService', () => {
// Mock only external dependencies
const mockSlackHelpers = {
getActiveIntegration: vi.fn(),
createPendingIntegration: vi.fn(),
updateIntegrationStatus: vi.fn(),
};
describe('initiateOAuth', () => {
it('should throw if integration already exists');
it('should generate valid OAuth URL');
it('should create pending integration with state');
it('should set 15-minute expiry on state');
});
describe('exchangeCodeForToken', () => {
it('should validate state exists and not expired');
it('should call Slack API with correct parameters');
it('should store token in vault');
it('should update integration to active status');
it('should clear OAuth state after success');
});
});
```
**services/slack-helpers.test.ts** - Test data layer
```typescript
describe('SlackHelpers', () => {
describe('getActiveIntegration', () => {
it('should return active integration for org');
it('should exclude deleted integrations');
it('should return null if no integration');
});
describe('createPendingIntegration', () => {
it('should create with pending status');
it('should set OAuth expiry timestamp');
it('should return integration ID');
});
describe('storeTokenInVault', () => {
it('should generate correct vault key');
it('should store encrypted token');
it('should return vault key');
});
});
```
**handler.integration.test.ts**
- Full OAuth flow with database
- State expiration handling
- Organization isolation
- Concurrent request handling
**services/slack-oauth-service.test.ts**
- OAuth URL generation
- State management
- Token exchange logic
- Vault integration mocking
**services/slack-helpers.test.ts**
- Database query functions
- Token encryption/decryption
- Status checking logic
- Data transformation utilities
#### Testing Best Practices
1. **Test Data Isolation**
- Each test creates its own organization/user
- Clean up after each test
- No shared state between tests
2. **Mock Strategy**
- Mock Slack API responses
- Mock Supabase Vault operations
- Use real database in integration tests
- Mock time for expiration tests
3. **Error Scenarios**
- Network failures
- Invalid OAuth codes
- Expired states
- Rate limiting
- Database conflicts
4. **Security Testing**
- CSRF protection validation
- Authorization checks
- Token exposure prevention
- Cross-organization access
### Development & Testing Workflow
#### Local Development Setup
1. **Environment Configuration**
```bash
# .env.local
SLACK_INTEGRATION_ENABLED=false # Set to true when ready to test
SLACK_CLIENT_ID=your-dev-client-id
SLACK_CLIENT_SECRET=your-dev-client-secret
SLACK_REDIRECT_URI=http://localhost:3000/api/v2/slack/auth/callback
SUPABASE_VAULT_KEY=your-vault-key
```
**Feature Flag Implementation:**
```typescript
// services/slack-oauth-service.ts
export async function initiateOAuth(params: {
organizationId: string;
userId: string;
}): Promise<{ authUrl: string; state: string }> {
// Check if Slack integration is enabled
if (!process.env.SLACK_INTEGRATION_ENABLED || process.env.SLACK_INTEGRATION_ENABLED === 'false') {
throw new Error('Slack integration is not enabled');
}
// Regular OAuth flow continues...
}
```
2. **Testing with Real Slack**
For local development, you'll need to set up a real Slack app and use ngrok to expose your localhost:
**Slack App Setup:**
1. Go to https://api.slack.com/apps and create a new app
2. Choose "From scratch" and select your workspace
3. Add OAuth scopes under "OAuth & Permissions":
- `channels:read`
- `chat:write`
- `chat:write.public`
4. Add redirect URL (will update with ngrok URL)
**Local Development with ngrok:**
```bash
# Start your local server
pnpm run dev --filter=@buster-app/server
# In another terminal, expose your localhost
ngrok http 3000
# Copy the HTTPS URL from ngrok (e.g., https://abc123.ngrok.io)
# Update your Slack app's redirect URL to:
# https://abc123.ngrok.io/api/v2/slack/auth/callback
# Update your .env.local
SLACK_CLIENT_ID=your-slack-client-id
SLACK_CLIENT_SECRET=your-slack-client-secret
SLACK_REDIRECT_URI=https://abc123.ngrok.io/api/v2/slack/auth/callback
```
**Testing Flow:**
1. Start your server and ngrok
2. Navigate to your OAuth initiation endpoint
3. You'll be redirected to real Slack OAuth
4. Approve access in your test workspace
5. Slack redirects back to your ngrok URL
6. Your callback handler processes the real OAuth code
3. **Database Setup**
```bash
# Run migrations
pnpm run db:migrate
# Seed test data
pnpm run db:seed:dev
```
#### Testing Workflow
**1. Unit Tests (Fast Feedback)**
```bash
# Run all unit tests (works without Slack credentials)
pnpm run test:unit --filter=@buster-app/server
# Run specific test file
pnpm run test apps/server/src/api/v2/slack/handler.test.ts
# Watch mode for development
pnpm run test:watch apps/server/src/api/v2/slack
```
**Unit Test Mocking Strategy:**
```typescript
// handler.test.ts - Tests work without real Slack
describe('SlackHandler', () => {
beforeEach(() => {
// Mock environment check
vi.stubEnv('SLACK_INTEGRATION_ENABLED', 'true');
// Mock Slack API calls
vi.mock('@buster/slack', () => ({
SlackAuthService: vi.fn(() => ({
generateAuthUrl: vi.fn().mockResolvedValue({
authUrl: 'https://mock-url',
state: 'mock-state'
}),
handleCallback: vi.fn().mockResolvedValue({
teamId: 'T-MOCK',
teamName: 'Mock Team'
})
}))
}));
});
it('should handle disabled integration gracefully', async () => {
vi.stubEnv('SLACK_INTEGRATION_ENABLED', 'false');
const response = await app.request('/api/v2/slack/auth', {
method: 'POST'
});
expect(response.status).toBe(503); // Service Unavailable
expect(await response.json()).toEqual({
error: 'Slack integration is not enabled'
});
});
});
```
**2. Integration Tests (Conditional Execution)**
```bash
# Integration tests skip Slack tests when disabled
pnpm run test:integration --filter=@buster-app/server
```
```typescript
// handler.integration.test.ts
describe('Slack OAuth Integration', () => {
const slackEnabled = process.env.SLACK_INTEGRATION_ENABLED === 'true';
describe.skipIf(!slackEnabled)('with real Slack', () => {
// These tests only run when SLACK_INTEGRATION_ENABLED=true
it('should complete OAuth flow', async () => {
// Real Slack OAuth test
});
});
describe('with mocked Slack', () => {
// These tests always run
it('should handle database operations', async () => {
// Test database without real Slack
});
});
});
```
**3. Manual Testing Flow**
```bash
# Start server
pnpm run dev --filter=@buster-app/server
# Test OAuth flow
curl -X POST http://localhost:3000/api/v2/slack/auth \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json"
# Response includes auth URL
# Visit URL in browser to complete OAuth
# Check integration status
curl http://localhost:3000/api/v2/slack/integration \
-H "Authorization: Bearer $TOKEN"
# Remove integration
curl -X DELETE http://localhost:3000/api/v2/slack/integration \
-H "Authorization: Bearer $TOKEN"
```
#### Verification Checklist
**OAuth Flow Verification**
- [ ] Auth URL includes all required parameters
- [ ] State is stored in database with expiry
- [ ] Callback validates state correctly
- [ ] Token exchange succeeds
- [ ] Token stored in Supabase Vault
- [ ] Integration record updated to 'active'
- [ ] OAuth metadata cleared after success
**Security Verification**
- [ ] State expires after 15 minutes
- [ ] Invalid state returns error
- [ ] Cross-organization access blocked
- [ ] Tokens never exposed in API responses
- [ ] Deleted integrations can't be accessed
**Error Handling Verification**
- [ ] User denial handled gracefully
- [ ] Network errors return appropriate status
- [ ] Duplicate integration attempts blocked
- [ ] Missing integration returns 404
### CI/CD Considerations
#### Environment-Based Configuration
**Local Development (default):**
```env
SLACK_INTEGRATION_ENABLED=false
# Slack credentials optional
```
**CI/CD Pipeline:**
```env
SLACK_INTEGRATION_ENABLED=false
# Tests run with mocked Slack
```
**Staging:**
```env
SLACK_INTEGRATION_ENABLED=true
SLACK_CLIENT_ID=staging-client-id
SLACK_CLIENT_SECRET=xxx (from secrets)
SLACK_REDIRECT_URI=https://staging.buster.so/api/v2/slack/auth/callback
```
**Production:**
```env
SLACK_INTEGRATION_ENABLED=true
SLACK_CLIENT_ID=prod-client-id
SLACK_CLIENT_SECRET=xxx (from Supabase Vault)
SLACK_REDIRECT_URI=https://app.buster.so/api/v2/slack/auth/callback
```
#### API Response When Disabled
When `SLACK_INTEGRATION_ENABLED=false`, endpoints return:
```json
{
"error": "Slack integration is not enabled",
"code": "INTEGRATION_DISABLED",
"status": 503
}
```
This allows:
- Frontend to conditionally show/hide Slack features
- Tests to run without Slack credentials
- Gradual rollout with feature flags
### Deployment Considerations
#### Environment Variables
```env
SLACK_INTEGRATION_ENABLED=true/false
SLACK_CLIENT_ID=xxx
SLACK_CLIENT_SECRET=xxx (store in Supabase Vault)
SLACK_REDIRECT_URI=https://app.buster.so/api/v2/slack/auth/callback
SLACK_OAUTH_SCOPES=channels:read,chat:write,chat:write.public
```
#### Migration Strategy
1. Deploy database schema
2. Configure Supabase Vault
3. Deploy API endpoints
4. Update Slack app configuration
5. Enable feature flag
6. Gradual rollout
### Success Criteria
1. **Security**: Zero OAuth-related security incidents
2. **Reliability**: 99.9% uptime for OAuth endpoints
3. **Performance**: OAuth flow completes in <3 seconds
4. **Adoption**: 50% of organizations connect Slack within 30 days
5. **Retention**: 90% of integrations remain active after 60 days
### Dependencies
- **@buster/slack** package (existing)
- **Supabase Vault** for token storage
- **Slack API** availability
- **Database** migrations completed
- **Frontend** integration UI
### Timeline
- **Week 1-2**: Database schema and migrations
- **Week 2-3**: OAuth endpoints implementation
- **Week 3-4**: Integration management APIs
- **Week 4-5**: Frontend integration
- **Week 5-6**: Testing and security review
- **Week 6-7**: Documentation and deployment
### Appendix
#### Slack API Reference
- [OAuth 2.0 Flow](https://api.slack.com/authentication/oauth-v2)
- [Web API Methods](https://api.slack.com/web)
- [Bot Tokens & Scopes](https://api.slack.com/authentication/token-types)
#### Internal References
- @buster/slack package documentation
- Supabase Vault documentation
- Organization permissions model