From eb72856a96055e47c2f779dbeba64d0dcf40e759 Mon Sep 17 00:00:00 2001
From: sethwebster <sethwebster@gmail.com>
Date: Tue, 28 Oct 2025 17:01:08 -0400
Subject: [PATCH] feat: Implement GitHub bot for reduced OAuth scope and fix
 chatbot issue filing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

OAuth Scope Reduction:
- Reduced user OAuth scope from 'public_repo' to 'read:user user:email'
- Users no longer grant write access to all public repositories
- Much less intimidating OAuth permission prompt

GitHub Bot Service (src/lib/github-bot.ts):
- Created dedicated bot service for write operations
- fileIssue() - File issues on behalf of users with attribution
- addComment() - Add comments to issues with attribution
- Clear attribution format: "Filed by @username via React Foundation Store"
- Bot account handles all write operations with user attribution

API Endpoint (src/app/api/bot/file-issue/route.ts):
- POST /api/bot/file-issue for filing issues via bot
- Requires user authentication (NextAuth session)
- Comprehensive validation and error handling
- GitHub API error handling (404, 403, 410)

React Hook (src/lib/hooks/use-github-bot.ts):
- useGitHubBot() hook for easy frontend integration
- Built-in loading states and error handling
- Simple API for components to file issues

Chatbot Integration Fixes:
- Fixed: Chatbot now always uses bot to file issues (not user token)
- Fixed: Removed confusing prompt about filing as bot vs user
- Fixed: Corrected authentication status detection
- Issues now filed as @react-foundation-bot with user attribution
- Updated system prompt to clarify bot-based filing

Environment Setup:
- Added GITHUB_BOT_TOKEN to .env.example with setup instructions
- Created test script (scripts/test-bot.ts) to verify bot configuration
- Added dotenv dependency for test script

Documentation:
- Created comprehensive setup guide (docs/development/github-bot-setup.md)
- Covers bot account creation, token generation, usage examples
- Security considerations, rate limiting, troubleshooting
- Added to docs/README.md index

Benefits:
- ✅ Users feel safe - only minimal read permissions required
- ✅ Full functionality - bot performs all write operations
- ✅ Clear attribution - shows who filed each issue
- ✅ Centralized control - rate limiting, validation, moderation
- ✅ Better UX - no scary OAuth prompts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
---
 .env.example                         |   7 +
 docs/README.md                       |   1 +
 docs/development/github-bot-setup.md | 330 +++++++++++++++++++++++++++
 package-lock.json                    |  23 ++
 package.json                         |  12 +-
 scripts/test-bot.ts                  |  88 +++++++
 src/app/api/bot/file-issue/route.ts  | 162 +++++++++++++
 src/app/api/chat/route.ts            |  73 +++---
 src/lib/auth.ts                      |   4 +-
 src/lib/github-bot.ts                | 170 ++++++++++++++
 src/lib/hooks/use-github-bot.ts      | 107 +++++++++
 11 files changed, 926 insertions(+), 51 deletions(-)
 create mode 100644 docs/development/github-bot-setup.md
 create mode 100644 scripts/test-bot.ts
 create mode 100644 src/app/api/bot/file-issue/route.ts
 create mode 100644 src/lib/github-bot.ts
 create mode 100644 src/lib/hooks/use-github-bot.ts

diff --git a/.env.example b/.env.example
index 90d6f73..ab65089 100644
--- a/.env.example
+++ b/.env.example
@@ -90,6 +90,13 @@ NEXT_PUBLIC_SITE_URL=
 
 GITHUB_TOKEN=
 
+# GitHub Bot Token (for write operations on behalf of users)
+# Create a dedicated bot account at https://github.com/signup
+# Generate a PAT with public_repo scope at https://github.com/settings/tokens/new
+# Required scopes: public_repo (for creating issues/PRs)
+# This bot will file issues on behalf of users, reducing OAuth scope requirements
+GITHUB_BOT_TOKEN=
+
 # Shopify Storefront API (for fetching products/collections on the frontend)
 SHOPIFY_STORE_DOMAIN=your-store.myshopify.com
 SHOPIFY_STOREFRONT_TOKEN=
diff --git a/docs/README.md b/docs/README.md
index 538b7ec..faf2083 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -55,6 +55,7 @@ Development guides and best practices:
 - **[Theming](./development/theming.md)** - Semantic theming system
 - **[RIS Setup](./development/ris-setup.md)** - React Impact Score data collection
 - **[Ecosystem Libraries](./development/ecosystem-libraries.md)** - Tracked React libraries
+- **[GitHub Bot Setup](./development/github-bot-setup.md)** - Bot account for reduced OAuth scopes
 
 ---
 
diff --git a/docs/development/github-bot-setup.md b/docs/development/github-bot-setup.md
new file mode 100644
index 0000000..74e57bd
--- /dev/null
+++ b/docs/development/github-bot-setup.md
@@ -0,0 +1,330 @@
+# GitHub Bot Setup Guide
+
+## Overview
+
+The React Foundation Store uses a **GitHub bot account** to perform write operations (filing issues, creating PRs, etc.) on behalf of users. This approach reduces OAuth scope requirements - users only need to grant **read-only access** (`read:user user:email`), while the bot handles all write operations.
+
+## Why Use a Bot?
+
+### Before (scary OAuth):
+```
+User → Grants public_repo scope → Can write to ALL public repos ❌
+```
+
+### After (bot approach):
+```
+User → Grants read:user scope → Only read access ✅
+User files issue → Backend → Bot files issue with attribution ✅
+```
+
+## Benefits
+
+✅ **Users feel safe** - Only granting minimal read permissions
+✅ **Full functionality** - Bot performs all write operations
+✅ **Clear attribution** - Issues/PRs clearly show who filed them
+✅ **Centralized control** - Rate limiting, validation, moderation
+✅ **Audit trail** - All bot actions logged on your backend
+
+## Setup Instructions
+
+### 1. Create a Bot Account
+
+1. **Sign out** of your GitHub account
+2. Go to https://github.com/signup
+3. Create a new account (e.g., `react-foundation-bot`)
+4. Use a dedicated email (e.g., `bot@react.foundation`)
+5. Complete email verification
+6. Optional: Set a profile picture and bio identifying it as a bot
+
+**Recommended profile:**
+- **Name**: React Foundation Bot
+- **Bio**: Automated assistant for the React Foundation Store
+- **Location**: https://react.foundation
+- **Company**: @facebook/react
+
+### 2. Generate a Personal Access Token
+
+1. **Sign in** as the bot account
+2. Go to https://github.com/settings/tokens/new
+3. **Note**: "React Foundation Store Bot Token"
+4. **Expiration**: No expiration (or set to custom long duration)
+5. **Select scopes**:
+   - ✅ `public_repo` - Create issues/PRs in public repositories
+6. Click **Generate token**
+7. **Copy the token** - you won't see it again!
+
+### 3. Add Token to Environment Variables
+
+Add the token to your `.env` file:
+
+```bash
+GITHUB_BOT_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+```
+
+**Important**:
+- Never commit this token to version control
+- Rotate the token periodically for security
+- Use different tokens for dev/staging/production
+
+### 4. Verify Setup
+
+Test the bot configuration:
+
+```bash
+# Start your development server
+npm run dev
+
+# In another terminal, test the bot endpoint
+curl -X POST http://localhost:3000/api/bot/file-issue \
+  -H "Content-Type: application/json" \
+  -d '{
+    "owner": "your-username",
+    "repo": "test-repo",
+    "title": "Test issue from bot",
+    "body": "This is a test to verify the bot is working."
+  }'
+```
+
+If successful, you should see:
+```json
+{
+  "success": true,
+  "issue": {
+    "number": 1,
+    "url": "https://github.com/your-username/test-repo/issues/1"
+  }
+}
+```
+
+## API Usage
+
+### File an Issue
+
+**Endpoint**: `POST /api/bot/file-issue`
+
+**Authentication**: Requires user session (NextAuth)
+
+**Request Body**:
+```typescript
+{
+  owner: string;        // Repository owner
+  repo: string;         // Repository name
+  title: string;        // Issue title (max 256 chars)
+  body: string;         // Issue body (markdown supported)
+  labels?: string[];    // Optional labels
+  assignees?: string[]; // Optional assignees
+}
+```
+
+**Response**:
+```typescript
+{
+  success: true;
+  issue: {
+    number: number;   // Issue number
+    url: string;      // Issue HTML URL
+  }
+}
+```
+
+**Example** (from frontend):
+```typescript
+async function fileIssue() {
+  const response = await fetch('/api/bot/file-issue', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({
+      owner: 'facebook',
+      repo: 'react',
+      title: 'Bug: useEffect runs twice in development',
+      body: 'I noticed that useEffect runs twice...',
+      labels: ['bug']
+    })
+  });
+
+  const data = await response.json();
+
+  if (data.success) {
+    console.log('Issue filed:', data.issue.url);
+  }
+}
+```
+
+## Using the Bot Service Directly
+
+You can also use the bot service in your backend code:
+
+```typescript
+import { fileIssue, addComment } from '@/lib/github-bot';
+
+// File an issue
+const issue = await fileIssue({
+  owner: 'facebook',
+  repo: 'react',
+  title: 'Feature request: Add new hook',
+  body: 'It would be great if...',
+  filedBy: {
+    username: 'johndoe',
+    name: 'John Doe'
+  }
+});
+
+// Add a comment to an existing issue
+await addComment({
+  owner: 'facebook',
+  repo: 'react',
+  issue_number: 123,
+  body: 'Thanks for the update!',
+  commentBy: {
+    username: 'johndoe',
+    name: 'John Doe'
+  }
+});
+```
+
+## Attribution Format
+
+All issues and comments filed by the bot include attribution to the original user:
+
+```markdown
+> **Filed by [@johndoe](https://github.com/johndoe)** via [React Foundation Store](https://react.foundation)
+
+User's issue content here...
+```
+
+This ensures:
+- Clear transparency about who filed the issue
+- Users get GitHub notifications (if mentioned)
+- Repository maintainers can contact the real user
+- Maintains trust and accountability
+
+## Security Considerations
+
+### Rate Limiting
+
+GitHub's rate limits apply to the bot account:
+- **Public repositories**: 5,000 requests/hour
+- **Authenticated**: Per-bot account
+
+Consider implementing your own rate limiting to prevent abuse:
+
+```typescript
+// Example: Limit users to 5 issues per hour
+import { RateLimiter } from '@/lib/rate-limiter';
+
+const limiter = new RateLimiter({
+  maxRequests: 5,
+  windowMs: 60 * 60 * 1000 // 1 hour
+});
+
+export async function POST(request: NextRequest) {
+  const session = await getServerAuthSession();
+  const userId = session.user.email;
+
+  if (!limiter.checkLimit(userId)) {
+    return NextResponse.json(
+      { error: 'Rate limit exceeded' },
+      { status: 429 }
+    );
+  }
+
+  // ... file issue
+}
+```
+
+### Validation & Moderation
+
+Add validation to prevent abuse:
+
+```typescript
+// Block spam keywords
+const spamKeywords = ['viagra', 'casino', 'lottery'];
+if (spamKeywords.some(keyword => title.toLowerCase().includes(keyword))) {
+  return NextResponse.json(
+    { error: 'Content blocked' },
+    { status: 400 }
+  );
+}
+
+// Require minimum content length
+if (body.length < 20) {
+  return NextResponse.json(
+    { error: 'Issue body too short' },
+    { status: 400 }
+  );
+}
+
+// Validate repository whitelist
+const allowedRepos = ['facebook/react', 'vercel/next.js'];
+if (!allowedRepos.includes(`${owner}/${repo}`)) {
+  return NextResponse.json(
+    { error: 'Repository not allowed' },
+    { status: 403 }
+  );
+}
+```
+
+### Logging & Monitoring
+
+Log all bot actions for auditing:
+
+```typescript
+console.log(`[BOT] User ${session.user.email} filed issue in ${owner}/${repo}`);
+console.log(`[BOT] Issue #${issue.number}: ${issue.html_url}`);
+```
+
+Consider integrating with monitoring services:
+- Sentry for error tracking
+- LogRocket for user session replay
+- DataDog for metrics and alerts
+
+## Troubleshooting
+
+### "Bot is not configured" Error
+
+**Cause**: `GITHUB_BOT_TOKEN` environment variable is not set
+
+**Solution**:
+1. Verify `.env` file contains `GITHUB_BOT_TOKEN=ghp_...`
+2. Restart your dev server after adding the token
+3. Check the token hasn't expired
+
+### "Forbidden" Error (403)
+
+**Cause**: Bot doesn't have permission to access the repository
+
+**Solution**:
+- Ensure the repository is **public** (bot can't access private repos)
+- Verify the token has `public_repo` scope
+- Check if the repository owner has blocked the bot account
+
+### "Issues disabled" Error (410)
+
+**Cause**: Issues are disabled for the target repository
+
+**Solution**:
+- Repository settings → Features → Enable Issues
+- Or choose a different repository
+
+### Rate Limit Exceeded
+
+**Cause**: Bot has made too many requests (5,000/hour limit)
+
+**Solution**:
+- Implement user-level rate limiting on your backend
+- Consider using multiple bot accounts for high-traffic scenarios
+- Use GitHub's GraphQL API to reduce request count
+
+## Next Steps
+
+- **Add comment functionality**: Use `addComment()` to let users reply to issues
+- **Create PRs**: Extend the bot service to create pull requests
+- **GitHub App**: For even better scalability, consider creating a GitHub App instead of using a PAT
+- **Rate limiting**: Implement user-level rate limiting
+- **Analytics**: Track bot usage with analytics
+
+## Related Documentation
+
+- [NextAuth Configuration](./authentication.md)
+- [GitHub OAuth Setup](./github-oauth.md)
+- [API Routes](./api-routes.md)
diff --git a/package-lock.json b/package-lock.json
index ff986e2..36b722c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,6 +21,7 @@
         "@react-three/postprocessing": "^3.0.4",
         "@tailwindcss/typography": "^0.5.19",
         "@types/chart.js": "^2.9.41",
+        "@types/dotenv": "^6.1.1",
         "@types/ioredis": "^4.28.10",
         "@types/jsdom": "^27.0.0",
         "@types/mdx": "^2.0.13",
@@ -29,6 +30,7 @@
         "@types/react-leaflet": "^2.8.3",
         "babel-plugin-react-compiler": "^1.0.0",
         "chart.js": "^4.5.1",
+        "dotenv": "^17.2.3",
         "framer-motion": "^12.23.24",
         "gray-matter": "^4.0.3",
         "ioredis": "^5.8.2",
@@ -4024,6 +4026,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/dotenv": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz",
+      "integrity": "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/draco3d": {
       "version": "1.4.10",
       "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
@@ -6696,6 +6707,18 @@
         "url": "https://github.com/fb55/domutils?sponsor=1"
       }
     },
+    "node_modules/dotenv": {
+      "version": "17.2.3",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+      "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
     "node_modules/draco3d": {
       "version": "1.5.7",
       "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
diff --git a/package.json b/package.json
index 6603f79..1fd338f 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
     "@react-three/postprocessing": "^3.0.4",
     "@tailwindcss/typography": "^0.5.19",
     "@types/chart.js": "^2.9.41",
+    "@types/dotenv": "^6.1.1",
     "@types/ioredis": "^4.28.10",
     "@types/jsdom": "^27.0.0",
     "@types/mdx": "^2.0.13",
@@ -52,6 +53,7 @@
     "@types/react-leaflet": "^2.8.3",
     "babel-plugin-react-compiler": "^1.0.0",
     "chart.js": "^4.5.1",
+    "dotenv": "^17.2.3",
     "framer-motion": "^12.23.24",
     "gray-matter": "^4.0.3",
     "ioredis": "^5.8.2",
@@ -83,22 +85,22 @@
     "@storybook/addon-docs": "^10.0.0",
     "@storybook/addon-onboarding": "^10.0.0",
     "@storybook/addon-vitest": "^10.0.0",
+    "@storybook/nextjs-vite": "^10.0.0",
     "@tailwindcss/postcss": "^4",
     "@types/leaflet": "^1.9.21",
     "@types/node": "^20",
     "@types/react": "^19.2.2",
     "@types/react-dom": "^19.2.2",
+    "@vitest/browser-playwright": "^4.0.4",
+    "@vitest/coverage-v8": "^4.0.4",
     "eslint": "^9",
     "eslint-config-next": "^16.0.0",
     "eslint-plugin-storybook": "^10.0.0",
+    "playwright": "^1.56.1",
     "sharp": "^0.34.4",
     "storybook": "^10.0.0",
     "tailwindcss": "^4",
     "typescript": "^5",
-    "@storybook/nextjs-vite": "^10.0.0",
-    "vitest": "^4.0.4",
-    "playwright": "^1.56.1",
-    "@vitest/browser-playwright": "^4.0.4",
-    "@vitest/coverage-v8": "^4.0.4"
+    "vitest": "^4.0.4"
   }
 }
diff --git a/scripts/test-bot.ts b/scripts/test-bot.ts
new file mode 100644
index 0000000..683c921
--- /dev/null
+++ b/scripts/test-bot.ts
@@ -0,0 +1,88 @@
+/**
+ * Test script for GitHub bot
+ *
+ * Usage: npx tsx scripts/test-bot.ts
+ */
+
+import { Octokit } from '@octokit/rest';
+import * as dotenv from 'dotenv';
+import * as path from 'path';
+
+// Load .env file
+dotenv.config({ path: path.resolve(process.cwd(), '.env') });
+
+async function testBot() {
+  console.log('🤖 Testing GitHub Bot Configuration...\n');
+
+  // Check if bot is configured
+  const botToken = process.env.GITHUB_BOT_TOKEN;
+
+  if (!botToken) {
+    console.error('❌ Bot is not configured!');
+    console.error('   Please set GITHUB_BOT_TOKEN in your .env file');
+    console.error('\n   Steps:');
+    console.error('   1. Open .env file');
+    console.error('   2. Add: GITHUB_BOT_TOKEN=ghp_your_token_here');
+    console.error('   3. Save and run this script again\n');
+    process.exit(1);
+  }
+
+  console.log('✅ Bot token is configured\n');
+
+  // Get bot info
+  try {
+    console.log('📡 Fetching bot account info...\n');
+
+    const octokit = new Octokit({ auth: botToken });
+    const response = await octokit.rest.users.getAuthenticated();
+    const botInfo = response.data;
+
+    console.log('✅ Successfully connected to GitHub!\n');
+    console.log('Bot Account Details:');
+    console.log('─────────────────────────────────────');
+    console.log(`Username:  ${botInfo.login}`);
+    console.log(`Name:      ${botInfo.name || '(not set)'}`);
+    console.log(`Profile:   ${botInfo.html_url}`);
+    console.log(`Avatar:    ${botInfo.avatar_url}`);
+    console.log('─────────────────────────────────────\n');
+
+    console.log('🎉 Bot is ready to use!');
+    console.log('\nNext steps:');
+    console.log('1. The bot can now file issues on behalf of users');
+    console.log('2. Users only need read:user OAuth scope');
+    console.log('3. Use the useGitHubBot() hook in your components');
+    console.log('4. See docs/development/github-bot-setup.md for usage examples\n');
+  } catch (error) {
+    console.error('❌ Failed to connect to GitHub');
+
+    if (error && typeof error === 'object' && 'status' in error) {
+      const githubError = error as { status: number; message?: string };
+
+      if (githubError.status === 401) {
+        console.error('\n   The token is invalid or expired');
+        console.error('   Please regenerate the token and update .env');
+        console.error('\n   Steps:');
+        console.error('   1. Sign in as the bot account');
+        console.error('   2. Go to https://github.com/settings/tokens/new');
+        console.error('   3. Generate new token with public_repo scope');
+        console.error('   4. Update GITHUB_BOT_TOKEN in .env\n');
+      } else if (githubError.status === 403) {
+        console.error('\n   The token does not have the required permissions');
+        console.error('   Make sure the token has public_repo scope');
+        console.error('\n   Steps:');
+        console.error('   1. Sign in as the bot account');
+        console.error('   2. Go to https://github.com/settings/tokens');
+        console.error('   3. Regenerate the token with public_repo scope checked\n');
+      } else {
+        console.error(`\n   GitHub API error: ${githubError.status}`);
+        console.error(`   Message: ${githubError.message || 'Unknown error'}\n`);
+      }
+    } else {
+      console.error(`\n   Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`);
+    }
+
+    process.exit(1);
+  }
+}
+
+testBot();
diff --git a/src/app/api/bot/file-issue/route.ts b/src/app/api/bot/file-issue/route.ts
new file mode 100644
index 0000000..47ae163
--- /dev/null
+++ b/src/app/api/bot/file-issue/route.ts
@@ -0,0 +1,162 @@
+/**
+ * API endpoint for filing GitHub issues via bot
+ *
+ * POST /api/bot/file-issue
+ *
+ * This endpoint allows authenticated users to file issues on GitHub
+ * without needing write permissions. The bot account handles the actual
+ * issue creation and attributes it to the user.
+ */
+
+import { NextRequest, NextResponse } from 'next/server';
+import { getServerAuthSession } from '@/lib/auth';
+import { fileIssue, isBotConfigured } from '@/lib/github-bot';
+
+export async function POST(request: NextRequest) {
+  try {
+    // Check if bot is configured
+    if (!isBotConfigured()) {
+      return NextResponse.json(
+        {
+          error: 'Bot is not configured',
+          message: 'GITHUB_BOT_TOKEN is not set. Please configure a bot account.',
+        },
+        { status: 503 }
+      );
+    }
+
+    // Check authentication
+    const session = await getServerAuthSession();
+    if (!session?.user) {
+      return NextResponse.json(
+        { error: 'Unauthorized', message: 'You must be logged in to file issues.' },
+        { status: 401 }
+      );
+    }
+
+    // Parse and validate request body
+    const body = await request.json();
+    const { owner, repo, title, body: issueBody, labels, assignees } = body;
+
+    // Validate required fields
+    if (!owner || typeof owner !== 'string') {
+      return NextResponse.json(
+        { error: 'Invalid request', message: 'Owner is required' },
+        { status: 400 }
+      );
+    }
+
+    if (!repo || typeof repo !== 'string') {
+      return NextResponse.json(
+        { error: 'Invalid request', message: 'Repository is required' },
+        { status: 400 }
+      );
+    }
+
+    if (!title || typeof title !== 'string') {
+      return NextResponse.json(
+        { error: 'Invalid request', message: 'Title is required' },
+        { status: 400 }
+      );
+    }
+
+    if (title.length > 256) {
+      return NextResponse.json(
+        { error: 'Invalid request', message: 'Title is too long (max 256 characters)' },
+        { status: 400 }
+      );
+    }
+
+    if (!issueBody || typeof issueBody !== 'string') {
+      return NextResponse.json(
+        { error: 'Invalid request', message: 'Body is required' },
+        { status: 400 }
+      );
+    }
+
+    // Validate optional arrays
+    if (labels && !Array.isArray(labels)) {
+      return NextResponse.json(
+        { error: 'Invalid request', message: 'Labels must be an array' },
+        { status: 400 }
+      );
+    }
+
+    if (assignees && !Array.isArray(assignees)) {
+      return NextResponse.json(
+        { error: 'Invalid request', message: 'Assignees must be an array' },
+        { status: 400 }
+      );
+    }
+
+    // File the issue via bot
+    const issue = await fileIssue({
+      owner,
+      repo,
+      title,
+      body: issueBody,
+      labels,
+      assignees,
+      filedBy: {
+        username: session.user.githubLogin || session.user.name || 'Unknown',
+        name: session.user.name || undefined,
+      },
+    });
+
+    return NextResponse.json(
+      {
+        success: true,
+        issue: {
+          number: issue.number,
+          url: issue.html_url,
+        },
+      },
+      { status: 201 }
+    );
+  } catch (error) {
+    console.error('Error filing issue via bot:', error);
+
+    // Handle GitHub API errors
+    if (error && typeof error === 'object' && 'status' in error) {
+      const githubError = error as { status: number; message?: string };
+
+      if (githubError.status === 404) {
+        return NextResponse.json(
+          {
+            error: 'Repository not found',
+            message: 'The specified repository does not exist or is not accessible.',
+          },
+          { status: 404 }
+        );
+      }
+
+      if (githubError.status === 403) {
+        return NextResponse.json(
+          {
+            error: 'Forbidden',
+            message: 'The bot does not have permission to create issues in this repository.',
+          },
+          { status: 403 }
+        );
+      }
+
+      if (githubError.status === 410) {
+        return NextResponse.json(
+          {
+            error: 'Issues disabled',
+            message: 'Issues are disabled for this repository.',
+          },
+          { status: 410 }
+        );
+      }
+    }
+
+    return NextResponse.json(
+      {
+        error: 'Internal server error',
+        message: 'An unexpected error occurred while filing the issue.',
+      },
+      { status: 500 }
+    );
+  }
+}
diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts
index 36ad18b..219d5ee 100644
--- a/src/app/api/chat/route.ts
+++ b/src/app/api/chat/route.ts
@@ -11,6 +11,7 @@ import { authOptions } from '@/lib/auth';
 import { getOpenAIClient, getResponseModel, getEmbeddingModel } from '@/lib/chatbot/openai';
 import { ensureVectorIndexIfMissing, searchSimilar } from '@/lib/chatbot/vector-store';
 import { ChatbotIssueError, createIssue } from '@/lib/chatbot/github';
+import { fileIssue } from '@/lib/github-bot';
 import {
   appendMessage,
   createConversation,
@@ -23,6 +24,7 @@ import { notifyHumanHandoff } from '@/lib/chatbot/handoff';
 import { ChatRequestSchema, type ChatResponse, type RetrievalResult } from '@/lib/chatbot/types';
 import { logger } from '@/lib/logger';
 import { getRedisClient } from '@/lib/redis';
+import { getChatbotEnv } from '@/lib/chatbot/env';
 
 const NAVIGATION_TARGETS: Record<string, string> = {
   home: '/',
@@ -47,10 +49,7 @@ Respond with concise, friendly language. You can and should use Markdown formatt
 DO NOT include citation markers like [source:...] in your response text - citations are shown separately below your message.
 If you cannot find an answer in the documents, clearly say you do not know and offer to escalate.
 When a user reports a potential bug, gather steps to reproduce, expected vs actual outcomes, and context before filing an issue.
-When you have gathered enough information to create a GitHub issue:
-- If the user is authenticated with GitHub (check the context), ask them: "Would you like me to create this issue under your GitHub account (@username), or should I create it as the Foundation bot?"
-- Wait for their response before calling create_github_issue
-- Set use_user_account to true if they want it under their account, false if they want the bot to do it
+When you have gathered enough information to create a GitHub issue, call create_github_issue to file it. Issues are always filed via the Foundation bot, with attribution to the user if they are authenticated.
 If you cannot self-serve, ask for the visitor's best contact information, then call submit_handoff_request to notify our team.
 When someone asks about adding a community, collect: community name, location/region, focus areas, primary links (website/join), meeting cadence, approximate size, and contact name/email before calling submit_community_listing. Confirm all details with the visitor first.
 When a visitor explicitly wants to open a page (e.g., "take me to the impact page"), call navigate_site with the closest matching target or a safe path (anything starting with "/" except /admin).
@@ -69,7 +68,6 @@ const IssueToolSchema = z.object({
   expected_result: z.string().nullable().optional(),
   actual_result: z.string().nullable().optional(),
   severity: z.enum(['low', 'medium', 'high']).optional(),
-  use_user_account: z.boolean().optional().default(false),
 });
 
 const HandoffToolSchema = z.object({
@@ -139,7 +137,7 @@ function buildTools(): ChatCompletionTool[] {
       function: {
         name: 'create_github_issue',
         description:
-          'Create a GitHub issue when a user reports a validated bug with clear reproduction steps.',
+          'Create a GitHub issue when a user reports a validated bug with clear reproduction steps. Issues are always filed via the Foundation bot, with attribution to the authenticated user if available.',
         parameters: {
           type: 'object',
           properties: {
@@ -156,10 +154,6 @@ function buildTools(): ChatCompletionTool[] {
               enum: ['low', 'medium', 'high'],
               description: 'Impact of the reported issue.',
             },
-            use_user_account: {
-              type: 'boolean',
-              description: 'If true, create the issue under the authenticated user\'s GitHub account. If false or not specified, create it as the Foundation bot.',
-            },
           },
           required: ['title', 'description', 'reproduction_steps'],
         },
@@ -452,45 +446,36 @@ async function handleToolCalls(
 
       const body = formatIssueBody(parsed.data, metadata);
 
-      // Check if user wants to create issue under their account
-      const useUserAccount = parsed.data.use_user_account === true;
-      const hasUserToken = !!options.userGithubToken;
-
-      // If user wants to use their account but doesn't have a token, fail gracefully
-      if (useUserAccount && !hasUserToken) {
-        messages.push({
-          role: 'tool',
-          tool_call_id: toolCall.id,
-          content: JSON.stringify({
-            success: false,
-            error: 'user_not_authenticated',
-            message: 'User is not authenticated with GitHub',
-          }),
-        });
-        continue;
-      }
-
       try {
-        const result = await createIssue(
-          {
-            title: parsed.data.title,
-            body,
-            labels: ['bug', 'chatbot'],
-          },
-          useUserAccount && hasUserToken
-            ? { userToken: options.userGithubToken }
-            : undefined
-        );
+        // Always use the bot to file issues
+        const env = getChatbotEnv();
+        const result = await fileIssue({
+          owner: env.githubOwner,
+          repo: env.githubRepo,
+          title: parsed.data.title,
+          body,
+          labels: ['bug', 'from-chatbot'],
+          filedBy: options.userGithubLogin
+            ? {
+                username: options.userGithubLogin,
+                name: options.userGithubLogin,
+              }
+            : undefined,
+        });
 
-        issue = result;
+        issue = {
+          url: result.html_url,
+          number: result.number,
+        };
 
         messages.push({
           role: 'tool',
           tool_call_id: toolCall.id,
           content: JSON.stringify({
             success: true,
-            issue: result,
-            createdAs: useUserAccount && hasUserToken ? 'user' : 'bot',
+            issue,
+            createdAs: 'bot',
+            attributedTo: options.userGithubLogin || 'anonymous',
           }),
         });
       } catch (error) {
@@ -684,10 +669,10 @@ export async function POST(request: NextRequest) {
 
     // Build system prompt with user authentication context
     let systemPrompt = SYSTEM_PROMPT;
-    if (userGithubLogin && userGithubToken) {
-      systemPrompt += `\n\nCONTEXT: The user is authenticated with GitHub as @${userGithubLogin}.`;
+    if (userGithubLogin) {
+      systemPrompt += `\n\nCONTEXT: The user is authenticated with GitHub as @${userGithubLogin}. You can personalize responses and attribute GitHub issues to them.`;
     } else {
-      systemPrompt += '\n\nCONTEXT: The user is not authenticated with GitHub.';
+      systemPrompt += '\n\nCONTEXT: The user is not authenticated. GitHub issues will be filed anonymously via the Foundation bot.';
     }
 
     const openaiMessages: ChatCompletionMessageParam[] = [
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index 9085e6c..c88256a 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -12,8 +12,8 @@ export const authOptions: NextAuthOptions = {
       authorization: {
         params: {
           // read:user and user:email for profile info
-          // public_repo allows creating issues/PRs in public repos on user's behalf
-          scope: "read:user user:email public_repo",
+          // Bot account handles write operations (issues/PRs)
+          scope: "read:user user:email",
         },
       },
     }),
diff --git a/src/lib/github-bot.ts b/src/lib/github-bot.ts
new file mode 100644
index 0000000..fbcecd0
--- /dev/null
+++ b/src/lib/github-bot.ts
@@ -0,0 +1,170 @@
+/**
+ * GitHub Bot Service
+ *
+ * This service uses a dedicated bot account to perform write operations
+ * on behalf of users, reducing OAuth scope requirements.
+ *
+ * Users only need read:user and user:email scopes.
+ * Bot handles all write operations (issues, PRs, comments).
+ */
+
+import { Octokit } from '@octokit/rest';
+import { getOptionalEnvVar } from './env';
+
+/**
+ * Get Octokit instance authenticated as the bot
+ */
+function getBotOctokit(): Octokit {
+  const botToken = getOptionalEnvVar('GITHUB_BOT_TOKEN');
+
+  if (!botToken) {
+    throw new Error(
+      'GITHUB_BOT_TOKEN is not configured. Please set up a bot account and add the token to your environment variables.'
+    );
+  }
+
+  return new Octokit({ auth: botToken });
+}
+
+/**
+ * Issue filing parameters
+ */
+export interface FileIssueParams {
+  owner: string;
+  repo: string;
+  title: string;
+  body: string;
+  labels?: string[];
+  assignees?: string[];
+  filedBy?: {
+    username: string;
+    name?: string;
+  };
+}
+
+/**
+ * Filed issue response
+ */
+export interface FiledIssueResult {
+  number: number;
+  url: string;
+  html_url: string;
+}
+
+/**
+ * File an issue on behalf of a user
+ *
+ * @param params Issue parameters including optional user attribution
+ * @returns The created issue details
+ *
+ * @example
+ * ```typescript
+ * const issue = await fileIssue({
+ *   owner: 'facebook',
+ *   repo: 'react',
+ *   title: 'Bug: useEffect runs twice',
+ *   body: 'Description of the bug...',
+ *   filedBy: {
+ *     username: 'johndoe',
+ *     name: 'John Doe'
+ *   }
+ * });
+ * ```
+ */
+export async function fileIssue(params: FileIssueParams): Promise<FiledIssueResult> {
+  const octokit = getBotOctokit();
+
+  // Build the issue body with attribution
+  let issueBody = '';
+
+  if (params.filedBy) {
+    issueBody = `> **Filed by [@${params.filedBy.username}](https://github.com/${params.filedBy.username})** via [React Foundation Store](https://react.foundation)\n\n`;
+  }
+
+  issueBody += params.body;
+
+  // Create the issue
+  const response = await octokit.rest.issues.create({
+    owner: params.owner,
+    repo: params.repo,
+    title: params.title,
+    body: issueBody,
+    labels: params.labels,
+    assignees: params.assignees,
+  });
+
+  return {
+    number: response.data.number,
+    url: response.data.url,
+    html_url: response.data.html_url,
+  };
+}
+
+/**
+ * Comment parameters
+ */
+export interface AddCommentParams {
+  owner: string;
+  repo: string;
+  issue_number: number;
+  body: string;
+  commentBy?: {
+    username: string;
+    name?: string;
+  };
+}
+
+/**
+ * Add a comment to an existing issue on behalf of a user
+ *
+ * @param params Comment parameters including optional user attribution
+ * @returns The created comment details
+ */
+export async function addComment(params: AddCommentParams) {
+  const octokit = getBotOctokit();
+
+  // Build the comment body with attribution
+  let commentBody = '';
+
+  if (params.commentBy) {
+    commentBody = `> **Comment by [@${params.commentBy.username}](https://github.com/${params.commentBy.username})**\n\n`;
+  }
+
+  commentBody += params.body;
+
+  // Create the comment
+  const response = await octokit.rest.issues.createComment({
+    owner: params.owner,
+    repo: params.repo,
+    issue_number: params.issue_number,
+    body: commentBody,
+  });
+
+  return {
+    id: response.data.id,
+    url: response.data.url,
+    html_url: response.data.html_url,
+  };
+}
+
+/**
+ * Check if the bot token is configured
+ */
+export function isBotConfigured(): boolean {
+  return !!getOptionalEnvVar('GITHUB_BOT_TOKEN');
+}
+
+/**
+ * Get bot account information
+ */
+export async function getBotInfo() {
+  const octokit = getBotOctokit();
+  const response = await octokit.rest.users.getAuthenticated();
+
+  return {
+    login: response.data.login,
+    name: response.data.name,
+    avatar_url: response.data.avatar_url,
+    html_url: response.data.html_url,
+  };
+}
diff --git a/src/lib/hooks/use-github-bot.ts b/src/lib/hooks/use-github-bot.ts
new file mode 100644
index 0000000..ac6e8a7
--- /dev/null
+++ b/src/lib/hooks/use-github-bot.ts
@@ -0,0 +1,107 @@
+/**
+ * React hook for filing GitHub issues via bot
+ *
+ * Provides a simple interface for components to file issues
+ * without requiring users to grant public_repo OAuth scope.
+ */
+
+import { useState } from 'react';
+import { useSession } from 'next-auth/react';
+
+export interface FileIssueParams {
+  owner: string;
+  repo: string;
+  title: string;
+  body: string;
+  labels?: string[];
+  assignees?: string[];
+}
+
+export interface FileIssueResult {
+  number: number;
+  url: string;
+}
+
+export interface UseGitHubBotReturn {
+  fileIssue: (params: FileIssueParams) => Promise<FileIssueResult>;
+  isLoading: boolean;
+  error: string | null;
+}
+
+/**
+ * Hook for filing GitHub issues via bot
+ *
+ * @example
+ * ```tsx
+ * function IssueForm() {
+ *   const { fileIssue, isLoading, error } = useGitHubBot();
+ *
+ *   async function handleSubmit(e: FormEvent) {
+ *     e.preventDefault();
+ *     const issue = await fileIssue({
+ *       owner: 'facebook',
+ *       repo: 'react',
+ *       title: 'Bug report',
+ *       body: 'Description...'
+ *     });
+ *     console.log('Filed issue:', issue.url);
+ *   }
+ *
+ *   return (
+ *     <form onSubmit={handleSubmit}>
+ *       {error && <div>Error: {error}</div>}
+ *       <button disabled={isLoading}>File Issue</button>
+ *     </form>
+ *   );
+ * }
+ * ```
+ */
+export function useGitHubBot(): UseGitHubBotReturn {
+  const { data: session, status } = useSession();
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+
+  async function fileIssue(params: FileIssueParams): Promise<FileIssueResult> {
+    // Check authentication
+    if (status === 'unauthenticated' || !session) {
+      const authError = 'You must be logged in to file issues';
+      setError(authError);
+      throw new Error(authError);
+    }
+
+    setIsLoading(true);
+    setError(null);
+
+    try {
+      const response = await fetch('/api/bot/file-issue', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(params),
+      });
+
+      const data = await response.json();
+
+      if (!response.ok) {
+        const errorMessage = data.message || data.error || 'Failed to file issue';
+        setError(errorMessage);
+        throw new Error(errorMessage);
+      }
+
+      return data.issue;
+    } catch (err) {
+      const errorMessage = err instanceof Error ? err.message : 'Unknown error';
+      setError(errorMessage);
+      throw err;
+    } finally {
+      setIsLoading(false);
+    }
+  }
+
+  return {
+    fileIssue,
+    isLoading,
+    error,
+  };
+}
