From d52a040e0b0f78e5f6ea72c008ddcb1cacf4a266 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Sat, 26 Jul 2025 20:11:31 +0900 Subject: [PATCH 01/18] feat: Add dynamic port finding for server and development Implement automatic port discovery to avoid conflicts - Add port finder utility to check and find available ports - Add port configuration manager to share ports between processes - Update server to dynamically find available port on startup - Update Vite config to read backend port and find its own port - Add .port-config.json to gitignore for runtime configuration - Remove deprecated @anthropic-ai/claude-code dependency - Bump version to 1.5.0 This ensures the application can run even when default ports are occupied --- .gitignore | 5 +- package-lock.json | 223 +------------------------------------------- server/index.js | 19 +++- utils/portConfig.js | 52 +++++++++++ utils/portFinder.js | 57 +++++++++++ vite.config.js | 16 +++- 6 files changed, 141 insertions(+), 231 deletions(-) create mode 100644 utils/portConfig.js create mode 100644 utils/portFinder.js diff --git a/.gitignore b/.gitignore index bb65f138..1e1c8cab 100755 --- a/.gitignore +++ b/.gitignore @@ -104,4 +104,7 @@ temp/ # Database files *.db *.sqlite -*.sqlite3 \ No newline at end of file +*.sqlite3 + +# Runtime configuration +.port-config.json \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 623c2d69..59490b41 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { "name": "claude-code-ui", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-ui", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { - "@anthropic-ai/claude-code": "^1.0.24", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.9", "@codemirror/lang-javascript": "^6.2.4", @@ -80,26 +79,6 @@ "node": ">=6.0.0" } }, - "node_modules/@anthropic-ai/claude-code": { - "version": "1.0.43", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.43.tgz", - "integrity": "sha512-VnuRK4s/R9ZRTkwH4gUjsp4SiBQXq7Y0B47OtgeXIZYVQYkhTW8m+E0IisFzXXFIyTQrE0SodGCpvgLhAYzGCg==", - "hasInstallScript": true, - "bin": { - "claude": "cli.js" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "^0.33.5", - "@img/sharp-darwin-x64": "^0.33.5", - "@img/sharp-linux-arm": "^0.33.5", - "@img/sharp-linux-arm64": "^0.33.5", - "@img/sharp-linux-x64": "^0.33.5", - "@img/sharp-win32-x64": "^0.33.5" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -985,108 +964,6 @@ "node": ">=18" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-libvips-linux-ppc64": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", @@ -1119,21 +996,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", @@ -1166,48 +1028,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, "node_modules/@img/sharp-linux-s390x": { "version": "0.34.2", "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz", @@ -1230,27 +1050,6 @@ "@img/sharp-libvips-linux-s390x": "1.1.0" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, "node_modules/@img/sharp-linuxmusl-arm64": { "version": "0.34.2", "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz", @@ -1352,24 +1151,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", diff --git a/server/index.js b/server/index.js index 2a7c45b3..c46ef576 100755 --- a/server/index.js +++ b/server/index.js @@ -3,6 +3,8 @@ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; +import { findAvailablePort } from '../utils/portFinder.js'; +import { savePortConfig } from '../utils/portConfig.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -23,8 +25,6 @@ try { console.log('No .env file found or error reading it:', e.message); } -console.log('PORT from env:', process.env.PORT); - import express from 'express'; import { WebSocketServer } from 'ws'; import http from 'http'; @@ -180,13 +180,13 @@ app.use(express.static(path.join(__dirname, '../dist'))); // API Routes (protected) app.get('/api/config', authenticateToken, (req, res) => { - const host = req.headers.host || `${req.hostname}:${PORT}`; + const host = req.headers.host || `${req.hostname}:${process.env.ACTUAL_PORT || DEFAULT_PORT}`; const protocol = req.protocol === 'https' || req.get('x-forwarded-proto') === 'https' ? 'wss' : 'ws'; console.log('Config API called - Returning host:', host, 'Protocol:', protocol); res.json({ - serverPort: PORT, + serverPort: process.env.ACTUAL_PORT || DEFAULT_PORT, wsUrl: `${protocol}://${host}` }); }); @@ -978,7 +978,7 @@ async function getFileTree(dirPath, maxDepth = 3, currentDepth = 0, showHidden = }); } -const PORT = process.env.PORT || 3000; +const DEFAULT_PORT = process.env.PORT || 3000; // Initialize database and start server async function startServer() { @@ -987,6 +987,15 @@ async function startServer() { await initializeDatabase(); console.log('✅ Database initialization skipped (testing)'); + // Find an available port + const PORT = await findAvailablePort(parseInt(DEFAULT_PORT)); + + // Export the port for other modules to use + process.env.ACTUAL_PORT = PORT.toString(); + + // Save port configuration for Vite to use + savePortConfig({ backend: PORT }); + server.listen(PORT, '0.0.0.0', async () => { console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`); diff --git a/utils/portConfig.js b/utils/portConfig.js new file mode 100644 index 00000000..cab435e5 --- /dev/null +++ b/utils/portConfig.js @@ -0,0 +1,52 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const PORT_CONFIG_FILE = path.join(__dirname, '../.port-config.json'); + +/** + * Save the actual ports being used to a config file + * @param {Object} ports - Object with port information + * @param {number} ports.backend - Backend server port + * @param {number} ports.frontend - Frontend dev server port + */ +export function savePortConfig(ports) { + try { + fs.writeFileSync(PORT_CONFIG_FILE, JSON.stringify(ports, null, 2)); + } catch (error) { + console.error('Failed to save port configuration:', error); + } +} + +/** + * Read the saved port configuration + * @returns {Object|null} - Port configuration or null if not found + */ +export function readPortConfig() { + try { + if (fs.existsSync(PORT_CONFIG_FILE)) { + const content = fs.readFileSync(PORT_CONFIG_FILE, 'utf8'); + return JSON.parse(content); + } + } catch (error) { + console.error('Failed to read port configuration:', error); + } + return null; +} + +/** + * Delete the port configuration file + */ +export function deletePortConfig() { + try { + if (fs.existsSync(PORT_CONFIG_FILE)) { + fs.unlinkSync(PORT_CONFIG_FILE); + } + } catch (error) { + console.error('Failed to delete port configuration:', error); + } +} \ No newline at end of file diff --git a/utils/portFinder.js b/utils/portFinder.js new file mode 100644 index 00000000..c8895019 --- /dev/null +++ b/utils/portFinder.js @@ -0,0 +1,57 @@ +import net from 'net'; + +/** + * Check if a port is available + * @param {number} port - Port to check + * @returns {Promise} - True if port is available + */ +export function isPortAvailable(port) { + return new Promise((resolve) => { + const server = net.createServer(); + + server.once('error', (err) => { + if (err.code === 'EADDRINUSE') { + resolve(false); + } else { + resolve(false); + } + }); + + server.once('listening', () => { + server.close(); + resolve(true); + }); + + server.listen(port, '0.0.0.0'); + }); +} + +/** + * Find an available port starting from a given port + * @param {number} startPort - Starting port number + * @param {number} maxAttempts - Maximum number of ports to try + * @returns {Promise} - Available port number + */ +export async function findAvailablePort(startPort = 3000, maxAttempts = 100) { + for (let i = 0; i < maxAttempts; i++) { + const port = startPort + i; + const available = await isPortAvailable(port); + if (available) { + return port; + } + } + throw new Error(`No available ports found between ${startPort} and ${startPort + maxAttempts - 1}`); +} + +/** + * Find two different available ports + * @param {number} startPort1 - Starting port for first search + * @param {number} startPort2 - Starting port for second search + * @returns {Promise<{port1: number, port2: number}>} - Two available ports + */ +export async function findTwoAvailablePorts(startPort1 = 3000, startPort2 = 3001) { + const port1 = await findAvailablePort(startPort1); + // Ensure second port is different from first + const port2 = await findAvailablePort(startPort2 === port1 ? startPort2 + 1 : startPort2); + return { port1, port2 }; +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 4d3971b7..db6aab97 100755 --- a/vite.config.js +++ b/vite.config.js @@ -1,19 +1,27 @@ import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' +import { findAvailablePort } from './utils/portFinder.js' +import { readPortConfig } from './utils/portConfig.js' -export default defineConfig(({ command, mode }) => { +export default defineConfig(async ({ command, mode }) => { // Load env file based on `mode` in the current working directory. const env = loadEnv(mode, process.cwd(), '') + // Try to read saved port configuration + const portConfig = readPortConfig() + const backendPort = portConfig?.backend || env.PORT || 3000 + + // Find an available port for Vite + const vitePort = await findAvailablePort(parseInt(env.VITE_PORT) || 3001) return { plugins: [react()], server: { - port: parseInt(env.VITE_PORT) || 3001, + port: vitePort, proxy: { - '/api': `http://localhost:${env.PORT || 3002}`, + '/api': `http://localhost:${backendPort}`, '/ws': { - target: `ws://localhost:${env.PORT || 3002}`, + target: `ws://localhost:${backendPort}`, ws: true } } From fb267a9a5bf403968882f9934ef688f331fb8c52 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:47:16 +0900 Subject: [PATCH 02/18] =?UTF-8?q?=F0=9F=94=A7=20chore:=20Add=20GitHub=20OA?= =?UTF-8?q?uth=20dependencies=20and=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add passport, passport-github2, express-session, connect-sqlite3 - Update .env.example with GitHub OAuth configuration - Add environment variables for client ID, secret, allowed users Co-Authored-By: Claude --- .env.example | 18 +- package-lock.json | 1517 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 4 + 3 files changed, 1517 insertions(+), 22 deletions(-) diff --git a/.env.example b/.env.example index 7cd2dd5b..97d3a732 100755 --- a/.env.example +++ b/.env.example @@ -9,4 +9,20 @@ #API server PORT=3008 #Frontend port -VITE_PORT=3009 \ No newline at end of file +VITE_PORT=3009 + +# ============================================================================= +# GITHUB OAUTH CONFIGURATION +# ============================================================================= + +# GitHub OAuth App credentials (create at https://github.com/settings/developers) +GITHUB_CLIENT_ID=your_github_client_id +GITHUB_CLIENT_SECRET=your_github_client_secret +GITHUB_CALLBACK_URL=http://localhost:3008/api/auth/github/callback + +# Allowed GitHub usernames (comma-separated list) +# Only these GitHub accounts will be allowed to authenticate +GITHUB_ALLOWED_USERS=username1,username2 + +# Session secret for OAuth state management +SESSION_SECRET=your-session-secret-here diff --git a/package-lock.json b/package-lock.json index 59490b41..cf1f0a59 100755 --- a/package-lock.json +++ b/package-lock.json @@ -25,14 +25,18 @@ "chokidar": "^4.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "connect-sqlite3": "^0.9.16", "cors": "^2.8.5", "express": "^4.18.2", + "express-session": "^1.18.2", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.515.0", "mime-types": "^3.0.1", "multer": "^2.0.1", "node-fetch": "^2.7.0", "node-pty": "^1.0.0", + "passport": "^0.7.0", + "passport-github2": "^0.1.12", "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", @@ -964,6 +968,13 @@ "node": ">=18" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, "node_modules/@img/sharp-libvips-linux-ppc64": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", @@ -1315,6 +1326,58 @@ "node": ">= 8" } }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1612,6 +1675,16 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1825,6 +1898,13 @@ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "peer": true }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1856,6 +1936,46 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -1904,6 +2024,28 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -1993,6 +2135,15 @@ } ] }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -2189,6 +2340,128 @@ "node": ">= 0.8" } }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2331,6 +2604,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -2342,6 +2624,16 @@ "url": "https://polar.sh/cva" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2475,6 +2767,16 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -2492,6 +2794,13 @@ "node": ">= 6" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -2534,6 +2843,24 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/connect-sqlite3": { + "version": "0.9.16", + "resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.16.tgz", + "integrity": "sha512-2gqo0QmcBBL8p8+eqpBETn7RgM/PaoKvpQGl8PfjEgwlr0VuMYNMxRJRrRCo3KR3fxMYeSsCw2tGNG0JKN9Nvg==", + "dependencies": { + "sqlite3": "^5.0.2" + }, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2684,6 +3011,13 @@ "node": ">=4.0.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2794,7 +3128,6 @@ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "optional": true, - "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -2804,7 +3137,6 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "optional": true, - "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -2820,6 +3152,23 @@ "once": "^1.4.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2973,6 +3322,55 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-session": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", + "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.1.0", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3132,6 +3530,43 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3153,31 +3588,104 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, "engines": { - "node": ">=6.9.0" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=8" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", @@ -3252,6 +3760,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3272,6 +3787,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -3330,6 +3852,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -3345,6 +3874,45 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -3375,6 +3943,45 @@ } ] }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3390,6 +3997,20 @@ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3496,6 +4117,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3552,6 +4180,13 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3731,6 +4366,67 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4420,6 +5116,207 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -4554,6 +5451,31 @@ } } }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -4564,6 +5486,65 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-pty": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", @@ -4579,6 +5560,22 @@ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4596,6 +5593,29 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4634,6 +5654,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4642,6 +5671,22 @@ "wrappy": "1" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -4678,6 +5723,73 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-github2": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/passport-github2/-/passport-github2-0.1.12.tgz", + "integrity": "sha512-3nPUCc7ttF/3HSP/k9sAXjz3SkGv5Nki84I05kSQPo01Jqq1NzJACgMblCK0fGcv9pKCG/KXU3AJRDGLqHLoIw==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4716,6 +5828,11 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4921,6 +6038,27 @@ "node": ">=10" } }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4995,6 +6133,15 @@ } ] }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5234,6 +6381,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5243,6 +6400,69 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rollup": { "version": "4.44.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.1.tgz", @@ -5412,6 +6632,13 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5841,6 +7068,47 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", + "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5864,6 +7132,76 @@ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sqlite3/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ssri/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -6167,6 +7505,23 @@ "node": ">=8.10.0" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", @@ -6198,6 +7553,33 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6371,6 +7753,24 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -6389,6 +7789,26 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, "node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -6673,6 +8093,61 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/package.json b/package.json index 14b40d8e..bc0c8124 100755 --- a/package.json +++ b/package.json @@ -38,14 +38,18 @@ "chokidar": "^4.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "connect-sqlite3": "^0.9.16", "cors": "^2.8.5", "express": "^4.18.2", + "express-session": "^1.18.2", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.515.0", "mime-types": "^3.0.1", "multer": "^2.0.1", "node-fetch": "^2.7.0", "node-pty": "^1.0.0", + "passport": "^0.7.0", + "passport-github2": "^0.1.12", "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", From 6847896da6135b99843fa022f18008c2f60e95d4 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:47:28 +0900 Subject: [PATCH 03/18] =?UTF-8?q?=E2=9C=A8=20feat:=20Create=20GitHub=20OAu?= =?UTF-8?q?th=20strategy=20with=20account=20restrictions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GitHub OAuth passport strategy - Implement allowed users check based on environment variable - Create env.js to load environment variables early - Add user serialization/deserialization for sessions Co-Authored-By: Claude --- server/auth/passport.js | 6 +++ server/auth/strategies/github.js | 73 ++++++++++++++++++++++++++++++++ server/config/env.js | 26 ++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 server/auth/passport.js create mode 100644 server/auth/strategies/github.js create mode 100644 server/config/env.js diff --git a/server/auth/passport.js b/server/auth/passport.js new file mode 100644 index 00000000..fc880935 --- /dev/null +++ b/server/auth/passport.js @@ -0,0 +1,6 @@ +import passport from 'passport'; + +// Initialize passport strategies +import './strategies/github.js'; + +export default passport; \ No newline at end of file diff --git a/server/auth/strategies/github.js b/server/auth/strategies/github.js new file mode 100644 index 00000000..e5a292fd --- /dev/null +++ b/server/auth/strategies/github.js @@ -0,0 +1,73 @@ +import passport from 'passport'; +import { Strategy as GitHubStrategy } from 'passport-github2'; +import { userDb } from '../../database/db.js'; + +// Helper function to check if user is allowed +const isAllowedUser = (githubUsername) => { + const allowedUsers = process.env.GITHUB_ALLOWED_USERS + ? process.env.GITHUB_ALLOWED_USERS.split(',').map(u => u.trim()) + : []; + + return allowedUsers.includes(githubUsername); +}; + +// Only initialize GitHub strategy if credentials are provided +if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { + console.log('🔐 Registering GitHub OAuth strategy'); + passport.use(new GitHubStrategy({ + clientID: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackURL: process.env.GITHUB_CALLBACK_URL + }, + async (accessToken, refreshToken, profile, done) => { + try { + // Check if user is allowed + if (!isAllowedUser(profile.username)) { + return done(null, false, { + message: `GitHub user @${profile.username} is not authorized to access this application` + }); + } + + // Check if user already exists + const existingUser = await userDb.getUserByGithubId(profile.id); + + if (existingUser) { + // Update last login + await userDb.updateUserLastLogin(existingUser.id); + return done(null, existingUser); + } + + // Create new user + const newUser = await userDb.createGithubUser({ + username: profile.username, + github_id: profile.id, + github_username: profile.username, + email: profile.emails && profile.emails[0] ? profile.emails[0].value : null, + avatar_url: profile.photos && profile.photos[0] ? profile.photos[0].value : null + }); + + return done(null, newUser); + } catch (error) { + return done(error); + } + } +)); + console.log('✅ GitHub strategy registered successfully'); +} else { + console.log('⚠️ GitHub OAuth not configured - missing CLIENT_ID or CLIENT_SECRET'); +} + +passport.serializeUser((user, done) => { + done(null, user.id); +}); + +passport.deserializeUser(async (id, done) => { + try { + const user = await userDb.getUserById(id); + done(null, user); + } catch (error) { + done(error); + } +}); + +export { isAllowedUser }; \ No newline at end of file diff --git a/server/config/env.js b/server/config/env.js new file mode 100644 index 00000000..887bd291 --- /dev/null +++ b/server/config/env.js @@ -0,0 +1,26 @@ +// Load environment variables before anything else +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +try { + const envPath = path.join(__dirname, '../../.env'); + const envFile = fs.readFileSync(envPath, 'utf8'); + envFile.split('\n').forEach(line => { + const trimmedLine = line.trim(); + if (trimmedLine && !trimmedLine.startsWith('#')) { + const [key, ...valueParts] = trimmedLine.split('='); + if (key && valueParts.length > 0 && !process.env[key]) { + process.env[key] = valueParts.join('=').trim(); + } + } + }); + console.log('✅ Environment variables loaded successfully'); + console.log('GitHub OAuth configured:', !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET)); +} catch (e) { + console.log('⚠️ No .env file found or error reading it:', e.message); +} \ No newline at end of file From c5ab1a21d986b7875388e38b7c6bfdee1f08d725 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:47:41 +0900 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20feat:=20Update=20?= =?UTF-8?q?database=20schema=20and=20server=20for=20GitHub=20OAuth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GitHub-specific columns to users table (github_id, avatar_url, etc) - Create sessions table for OAuth state management - Add GitHub user database operations - Configure express-session with SQLite store - Initialize passport in server middleware Co-Authored-By: Claude --- server/database/db.js | 46 ++++++++++++++++- server/database/init.sql | 21 ++++++-- .../migrations/001_add_github_auth.sql | 48 ++++++++++++++++++ server/index.js | 50 ++++++++++++------- 4 files changed, 143 insertions(+), 22 deletions(-) create mode 100644 server/database/migrations/001_add_github_auth.sql diff --git a/server/database/db.js b/server/database/db.js index 6fc13477..b83be681 100644 --- a/server/database/db.js +++ b/server/database/db.js @@ -71,11 +71,55 @@ const userDb = { // Get user by ID getUserById: (userId) => { try { - const row = db.prepare('SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1').get(userId); + const row = db.prepare('SELECT id, username, created_at, last_login, auth_provider, github_username, email, avatar_url FROM users WHERE id = ? AND is_active = 1').get(userId); return row; } catch (err) { throw err; } + }, + + // Get user by GitHub ID + getUserByGithubId: (githubId) => { + try { + const row = db.prepare('SELECT * FROM users WHERE github_id = ? AND is_active = 1').get(githubId); + return row; + } catch (err) { + throw err; + } + }, + + // Create GitHub user + createGithubUser: (userData) => { + try { + const stmt = db.prepare( + 'INSERT INTO users (username, auth_provider, github_id, github_username, email, avatar_url) VALUES (?, ?, ?, ?, ?, ?)' + ); + const result = stmt.run( + userData.username, + 'github', + userData.github_id, + userData.github_username, + userData.email, + userData.avatar_url + ); + return { + id: result.lastInsertRowid, + username: userData.username, + auth_provider: 'github', + github_username: userData.github_username + }; + } catch (err) { + throw err; + } + }, + + // Update user last login + updateUserLastLogin: (userId) => { + try { + db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(userId); + } catch (err) { + throw err; + } } }; diff --git a/server/database/init.sql b/server/database/init.sql index bf007b96..9d57dc3c 100644 --- a/server/database/init.sql +++ b/server/database/init.sql @@ -1,16 +1,31 @@ -- Initialize authentication database PRAGMA foreign_keys = ON; --- Users table (single user system) +-- Users table (supports both local and GitHub authentication) CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, + password_hash TEXT, -- Nullable for OAuth users + auth_provider TEXT DEFAULT 'local', + github_id TEXT UNIQUE, + github_username TEXT, + email TEXT, + avatar_url TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME, is_active BOOLEAN DEFAULT 1 ); +-- Sessions table for OAuth state management +CREATE TABLE IF NOT EXISTS sessions ( + sid TEXT PRIMARY KEY, + sess TEXT NOT NULL, + expire INTEGER NOT NULL +); + -- Indexes for performance CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); -CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active); +CREATE INDEX IF NOT EXISTS idx_users_github_id ON users(github_id); +CREATE INDEX IF NOT EXISTS idx_users_auth_provider ON users(auth_provider); +CREATE INDEX IF NOT EXISTS idx_sessions_expire ON sessions(expire); diff --git a/server/database/migrations/001_add_github_auth.sql b/server/database/migrations/001_add_github_auth.sql new file mode 100644 index 00000000..c659288f --- /dev/null +++ b/server/database/migrations/001_add_github_auth.sql @@ -0,0 +1,48 @@ +-- Migration to add GitHub OAuth support +-- This migration adds fields needed for GitHub authentication + +-- Add GitHub-related columns to users table +ALTER TABLE users ADD COLUMN auth_provider TEXT DEFAULT 'local'; +ALTER TABLE users ADD COLUMN github_id TEXT UNIQUE; +ALTER TABLE users ADD COLUMN github_username TEXT; +ALTER TABLE users ADD COLUMN email TEXT; +ALTER TABLE users ADD COLUMN avatar_url TEXT; + +-- Make password_hash nullable for OAuth users +-- SQLite doesn't support directly modifying column constraints, so we need to recreate the table +CREATE TABLE users_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT, -- Now nullable for OAuth users + auth_provider TEXT DEFAULT 'local', + github_id TEXT UNIQUE, + github_username TEXT, + email TEXT, + avatar_url TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + last_login DATETIME, + is_active BOOLEAN DEFAULT 1 +); + +-- Copy existing data +INSERT INTO users_new (id, username, password_hash, created_at, last_login, is_active) +SELECT id, username, password_hash, created_at, last_login, is_active FROM users; + +-- Drop old table and rename new one +DROP TABLE users; +ALTER TABLE users_new RENAME TO users; + +-- Recreate indexes +CREATE INDEX idx_users_username ON users(username); +CREATE INDEX idx_users_active ON users(is_active); +CREATE INDEX idx_users_github_id ON users(github_id); +CREATE INDEX idx_users_auth_provider ON users(auth_provider); + +-- Add sessions table for OAuth state management +CREATE TABLE IF NOT EXISTS sessions ( + sid TEXT PRIMARY KEY, + sess TEXT NOT NULL, + expire INTEGER NOT NULL +); + +CREATE INDEX idx_sessions_expire ON sessions(expire); \ No newline at end of file diff --git a/server/index.js b/server/index.js index c46ef576..278c7a64 100755 --- a/server/index.js +++ b/server/index.js @@ -1,4 +1,6 @@ -// Load environment variables from .env file +// Load environment variables FIRST before any other imports +import './config/env.js'; + import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -9,22 +11,6 @@ import { savePortConfig } from '../utils/portConfig.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -try { - const envPath = path.join(__dirname, '../.env'); - const envFile = fs.readFileSync(envPath, 'utf8'); - envFile.split('\n').forEach(line => { - const trimmedLine = line.trim(); - if (trimmedLine && !trimmedLine.startsWith('#')) { - const [key, ...valueParts] = trimmedLine.split('='); - if (key && valueParts.length > 0 && !process.env[key]) { - process.env[key] = valueParts.join('=').trim(); - } - } - }); -} catch (e) { - console.log('No .env file found or error reading it:', e.message); -} - import express from 'express'; import { WebSocketServer } from 'ws'; import http from 'http'; @@ -160,9 +146,37 @@ const wss = new WebSocketServer({ } }); -app.use(cors()); +app.use(cors({ + origin: process.env.CLIENT_URL || 'http://localhost:3009', + credentials: true +})); app.use(express.json()); +// Session configuration for OAuth +import session from 'express-session'; +import SQLiteStore from 'connect-sqlite3'; +const SQLiteStoreSession = SQLiteStore(session); + +app.use(session({ + store: new SQLiteStoreSession({ + db: 'sessions.db', + dir: './server/database' + }), + secret: process.env.SESSION_SECRET || 'default-session-secret-change-in-production', + resave: false, + saveUninitialized: false, + cookie: { + secure: process.env.NODE_ENV === 'production', + httpOnly: true, + maxAge: 24 * 60 * 60 * 1000 // 24 hours + } +})); + +// Initialize Passport +import passport from './auth/passport.js'; +app.use(passport.initialize()); +app.use(passport.session()); + // Optional API key validation (if configured) app.use('/api', validateApiKey); From 986df8b6d181f589f0c0519277ec6b63755f2277 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:47:54 +0900 Subject: [PATCH 05/18] =?UTF-8?q?=E2=9C=A8=20feat:=20Implement=20GitHub=20?= =?UTF-8?q?OAuth=20endpoints=20and=20middleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add /api/auth/github and callback endpoints - Create requireGithubAuth middleware - Update auth status to include GitHub configuration info - Add GitHub user check endpoint - Disable local login/register endpoints Co-Authored-By: Claude --- server/middleware/auth.js | 32 ++++++++ server/routes/auth.js | 159 ++++++++++++++------------------------ 2 files changed, 92 insertions(+), 99 deletions(-) diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 433c3293..0428c59b 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -71,10 +71,42 @@ const authenticateWebSocket = (token) => { } }; +// Middleware to require GitHub authentication +const requireGithubAuth = async (req, res, next) => { + // First check if user is authenticated + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: 'GitHub authentication required' }); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET); + + // Verify user exists and is GitHub authenticated + const user = userDb.getUserById(decoded.userId); + if (!user) { + return res.status(401).json({ error: 'Invalid token. User not found.' }); + } + + if (user.auth_provider !== 'github') { + return res.status(403).json({ error: 'GitHub authentication required. Please sign in with GitHub.' }); + } + + req.user = user; + next(); + } catch (error) { + console.error('Token verification error:', error); + return res.status(403).json({ error: 'Invalid token' }); + } +}; + export { validateApiKey, authenticateToken, generateToken, authenticateWebSocket, + requireGithubAuth, JWT_SECRET }; \ No newline at end of file diff --git a/server/routes/auth.js b/server/routes/auth.js index 82a7c0d8..9c5dada7 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -2,6 +2,8 @@ import express from 'express'; import bcrypt from 'bcrypt'; import { userDb, db } from '../database/db.js'; import { generateToken, authenticateToken } from '../middleware/auth.js'; +import passport from '../auth/passport.js'; +import { isAllowedUser } from '../auth/strategies/github.js'; const router = express.Router(); @@ -9,9 +11,16 @@ const router = express.Router(); router.get('/status', async (req, res) => { try { const hasUsers = await userDb.hasUsers(); + const githubConfigured = !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET); + const allowedUsers = process.env.GITHUB_ALLOWED_USERS + ? process.env.GITHUB_ALLOWED_USERS.split(',').map(u => u.trim()) + : []; + res.json({ needsSetup: !hasUsers, - isAuthenticated: false // Will be overridden by frontend if token exists + isAuthenticated: false, // Will be overridden by frontend if token exists + githubConfigured, + githubAllowedUsers: allowedUsers }); } catch (error) { console.error('Auth status error:', error); @@ -19,104 +28,9 @@ router.get('/status', async (req, res) => { } }); -// User registration (setup) - only allowed if no users exist -router.post('/register', async (req, res) => { - try { - const { username, password } = req.body; - - // Validate input - if (!username || !password) { - return res.status(400).json({ error: 'Username and password are required' }); - } - - if (username.length < 3 || password.length < 6) { - return res.status(400).json({ error: 'Username must be at least 3 characters, password at least 6 characters' }); - } - - // Use a transaction to prevent race conditions - db.prepare('BEGIN').run(); - try { - // Check if users already exist (only allow one user) - const hasUsers = userDb.hasUsers(); - if (hasUsers) { - db.prepare('ROLLBACK').run(); - return res.status(403).json({ error: 'User already exists. This is a single-user system.' }); - } - - // Hash password - const saltRounds = 12; - const passwordHash = await bcrypt.hash(password, saltRounds); - - // Create user - const user = userDb.createUser(username, passwordHash); - - // Generate token - const token = generateToken(user); - - // Update last login - userDb.updateLastLogin(user.id); - - db.prepare('COMMIT').run(); - - res.json({ - success: true, - user: { id: user.id, username: user.username }, - token - }); - } catch (error) { - db.prepare('ROLLBACK').run(); - throw error; - } - - } catch (error) { - console.error('Registration error:', error); - if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { - res.status(409).json({ error: 'Username already exists' }); - } else { - res.status(500).json({ error: 'Internal server error' }); - } - } -}); - -// User login -router.post('/login', async (req, res) => { - try { - const { username, password } = req.body; - - // Validate input - if (!username || !password) { - return res.status(400).json({ error: 'Username and password are required' }); - } - - // Get user from database - const user = userDb.getUserByUsername(username); - if (!user) { - return res.status(401).json({ error: 'Invalid username or password' }); - } - - // Verify password - const isValidPassword = await bcrypt.compare(password, user.password_hash); - if (!isValidPassword) { - return res.status(401).json({ error: 'Invalid username or password' }); - } - - // Generate token - const token = generateToken(user); - - // Update last login - userDb.updateLastLogin(user.id); - - res.json({ - success: true, - user: { id: user.id, username: user.username }, - token - }); - - } catch (error) { - console.error('Login error:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); +// Local registration and login are disabled - use GitHub OAuth instead +// router.post('/register', ...) - removed +// router.post('/login', ...) - removed // Get current user (protected route) router.get('/user', authenticateToken, (req, res) => { @@ -132,4 +46,51 @@ router.post('/logout', authenticateToken, (req, res) => { res.json({ success: true, message: 'Logged out successfully' }); }); +// GitHub OAuth routes +router.get('/github', (req, res, next) => { + // Store the original URL if provided + if (req.query.returnUrl) { + req.session.returnUrl = req.query.returnUrl; + } + passport.authenticate('github', { scope: ['user:email'] })(req, res, next); +}); + +router.get('/github/callback', + passport.authenticate('github', { failureRedirect: '/login?error=github_auth_failed' }), + async (req, res) => { + try { + // Generate JWT token for the authenticated user + const token = generateToken(req.user); + + // Update last login + userDb.updateUserLastLogin(req.user.id); + + // Redirect to client with token + const returnUrl = req.session.returnUrl || '/'; + delete req.session.returnUrl; + + // Redirect with token in query parameter (client will handle storing it) + res.redirect(`${process.env.CLIENT_URL || 'http://localhost:3009'}${returnUrl}?token=${token}`); + } catch (error) { + console.error('GitHub callback error:', error); + res.redirect(`${process.env.CLIENT_URL || 'http://localhost:3009'}/login?error=auth_failed`); + } + } +); + +// Check if a GitHub username is allowed +router.get('/github/check/:username', (req, res) => { + const { username } = req.params; + const allowed = isAllowedUser(username); + res.json({ allowed }); +}); + +// Get GitHub authentication status +router.get('/github/status', authenticateToken, (req, res) => { + res.json({ + isGithubAuthenticated: req.user.auth_provider === 'github', + githubUsername: req.user.github_username || null + }); +}); + export default router; \ No newline at end of file From 3a00ccabeb984ee16afa4ad3c7543680321ff057 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:48:06 +0900 Subject: [PATCH 06/18] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20RequireGithubAu?= =?UTF-8?q?th=20component=20and=20API=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create RequireGithubAuth wrapper component - Add GitHub status and user check API methods - Enable GitHub authentication flow in frontend Co-Authored-By: Claude --- src/components/RequireGithubAuth.jsx | 81 ++++++++++++++++++++++++++++ src/utils/api.js | 2 + 2 files changed, 83 insertions(+) create mode 100644 src/components/RequireGithubAuth.jsx diff --git a/src/components/RequireGithubAuth.jsx b/src/components/RequireGithubAuth.jsx new file mode 100644 index 00000000..4d090946 --- /dev/null +++ b/src/components/RequireGithubAuth.jsx @@ -0,0 +1,81 @@ +import React, { useEffect, useState } from 'react'; +import { Github } from 'lucide-react'; +import { api } from '../utils/api'; +import { useAuth } from '../contexts/AuthContext'; + +const RequireGithubAuth = ({ children }) => { + const [isGithubAuthenticated, setIsGithubAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const { token } = useAuth(); + + useEffect(() => { + const checkGithubAuth = async () => { + if (!token) { + setIsLoading(false); + return; + } + + try { + const response = await api.auth.githubStatus(); + if (response.ok) { + const data = await response.json(); + setIsGithubAuthenticated(data.isGithubAuthenticated); + } + } catch (error) { + console.error('Failed to check GitHub auth status:', error); + } finally { + setIsLoading(false); + } + }; + + checkGithubAuth(); + }, [token]); + + const handleGithubLogin = () => { + // Store current URL to return after auth + const returnUrl = window.location.pathname + window.location.search; + window.location.href = `/api/auth/github?returnUrl=${encodeURIComponent(returnUrl)}`; + }; + + if (isLoading) { + return ( +
+
Checking authentication...
+
+ ); + } + + if (!isGithubAuthenticated) { + return ( +
+
+
+
+
+ +
+
+ +

GitHub Authentication Required

+ +

+ This feature requires GitHub authentication. Please sign in with your GitHub account to continue. +

+ + +
+
+
+ ); + } + + return children; +}; + +export default RequireGithubAuth; \ No newline at end of file diff --git a/src/utils/api.js b/src/utils/api.js index 49ae915c..c16274bc 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -36,6 +36,8 @@ export const api = { }), user: () => authenticatedFetch('/api/auth/user'), logout: () => authenticatedFetch('/api/auth/logout', { method: 'POST' }), + githubStatus: () => authenticatedFetch('/api/auth/github/status'), + checkGithubUser: (username) => fetch(`/api/auth/github/check/${username}`), }, // Protected endpoints From bcc1830d5888f98c696f8f8149fb89551afc74e5 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:48:19 +0900 Subject: [PATCH 07/18] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20Replace?= =?UTF-8?q?=20local=20auth=20with=20GitHub=20OAuth=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove username/password fields from LoginForm and SetupForm - Remove login/register functions from AuthContext - Update UI to show only GitHub sign-in button - Add GitHub configuration status display - Show allowed users when configured Co-Authored-By: Claude --- src/components/LoginForm.jsx | 123 +++++++++++++---------------- src/components/SetupForm.jsx | 146 ++++++++++++----------------------- src/contexts/AuthContext.jsx | 91 +++++++++------------- 3 files changed, 140 insertions(+), 220 deletions(-) diff --git a/src/components/LoginForm.jsx b/src/components/LoginForm.jsx index f2a490a1..ce47f527 100644 --- a/src/components/LoginForm.jsx +++ b/src/components/LoginForm.jsx @@ -1,33 +1,32 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useAuth } from '../contexts/AuthContext'; -import { MessageSquare } from 'lucide-react'; +import { MessageSquare, Github } from 'lucide-react'; +import { api } from '../utils/api'; const LoginForm = () => { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); + const [authStatus, setAuthStatus] = useState({}); + const { error: authError } = useAuth(); - const { login } = useAuth(); + useEffect(() => { + // Get auth status including GitHub configuration + const checkAuthStatus = async () => { + try { + const response = await api.auth.status(); + const data = await response.json(); + setAuthStatus(data); + } catch (err) { + console.error('Failed to check auth status:', err); + } + }; + checkAuthStatus(); + }, []); - const handleSubmit = async (e) => { - e.preventDefault(); - setError(''); - - if (!username || !password) { - setError('Please enter both username and password'); - return; - } - + const handleGithubLogin = () => { setIsLoading(true); - - const result = await login(username, password); - - if (!result.success) { - setError(result.error); - } - - setIsLoading(false); + // Redirect to GitHub OAuth endpoint + window.location.href = `${window.location.origin}/api/auth/github`; }; return ( @@ -41,64 +40,48 @@ const LoginForm = () => { -

Welcome Back

+

Welcome to Claude Code UI

- Sign in to your Claude Code UI account + {authStatus.githubConfigured ? 'Sign in with GitHub to continue' : 'GitHub authentication not configured'}

- {/* Login Form */} -
-
- - setUsername(e.target.value)} - className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Enter your username" - required + {/* GitHub Login Button */} + {authStatus.githubConfigured ? ( +
+ + + {authStatus.githubAllowedUsers && authStatus.githubAllowedUsers.length > 0 && ( +
+ Allowed users: {authStatus.githubAllowedUsers.join(', ')} +
+ )} + + {(error || authError) && ( +
+

{error || authError}

+
+ )}
- -
- - setPassword(e.target.value)} - className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Enter your password" - required - disabled={isLoading} - /> + ) : ( +
+

+ GitHub authentication is not configured. Please check your environment variables. +

- - {error && ( -
-

{error}

-
- )} - - - + )}

- Enter your credentials to access Claude Code UI + Authentication is managed through GitHub OAuth

diff --git a/src/components/SetupForm.jsx b/src/components/SetupForm.jsx index f1aa497e..bcec2a72 100644 --- a/src/components/SetupForm.jsx +++ b/src/components/SetupForm.jsx @@ -1,44 +1,32 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { useAuth } from '../contexts/AuthContext'; import ClaudeLogo from './ClaudeLogo'; +import { Github } from 'lucide-react'; +import { api } from '../utils/api'; const SetupForm = () => { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); + const [authStatus, setAuthStatus] = useState({}); - const { register } = useAuth(); + useEffect(() => { + // Get auth status including GitHub configuration + const checkAuthStatus = async () => { + try { + const response = await api.auth.status(); + const data = await response.json(); + setAuthStatus(data); + } catch (err) { + console.error('Failed to check auth status:', err); + } + }; + checkAuthStatus(); + }, []); - const handleSubmit = async (e) => { - e.preventDefault(); - setError(''); - - if (password !== confirmPassword) { - setError('Passwords do not match'); - return; - } - - if (username.length < 3) { - setError('Username must be at least 3 characters long'); - return; - } - - if (password.length < 6) { - setError('Password must be at least 6 characters long'); - return; - } - + const handleGithubLogin = () => { setIsLoading(true); - - const result = await register(username, password); - - if (!result.success) { - setError(result.error); - } - - setIsLoading(false); + // Redirect to GitHub OAuth endpoint + window.location.href = `${window.location.origin}/api/auth/github`; }; return ( @@ -52,78 +40,46 @@ const SetupForm = () => {

Welcome to Claude Code UI

- Set up your account to get started + {authStatus.githubConfigured ? 'Sign in with GitHub to get started' : 'GitHub authentication not configured'}

- {/* Setup Form */} -
-
- - setUsername(e.target.value)} - className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Enter your username" - required + {/* GitHub Login Button */} + {authStatus.githubConfigured ? ( +
+ + + {authStatus.githubAllowedUsers && authStatus.githubAllowedUsers.length > 0 && ( +
+ Allowed users: {authStatus.githubAllowedUsers.join(', ')} +
+ )} + + {error && ( +
+

{error}

+
+ )}
- -
- - setPassword(e.target.value)} - className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Enter your password" - required - disabled={isLoading} - /> + ) : ( +
+

+ GitHub authentication is not configured. Please check your environment variables. +

- -
- - setConfirmPassword(e.target.value)} - className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Confirm your password" - required - disabled={isLoading} - /> -
- - {error && ( -
-

{error}

-
- )} - - - + )}

- This is a single-user system. Only one account can be created. + Authentication is managed through GitHub OAuth.

diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx index 77acb6c6..e27e69dc 100644 --- a/src/contexts/AuthContext.jsx +++ b/src/contexts/AuthContext.jsx @@ -4,8 +4,6 @@ import { api } from '../utils/api'; const AuthContext = createContext({ user: null, token: null, - login: () => {}, - register: () => {}, logout: () => {}, isLoading: true, needsSetup: false, @@ -29,10 +27,40 @@ export const AuthProvider = ({ children }) => { // Check authentication status on mount useEffect(() => { - checkAuthStatus(); + // Check for token in URL (OAuth callback) + const urlParams = new URLSearchParams(window.location.search); + const urlToken = urlParams.get('token'); + const urlError = urlParams.get('error'); + + if (urlError) { + // Handle OAuth error + setError(urlError === 'github_auth_failed' ? 'GitHub authentication failed. Please try again.' : 'Authentication failed.'); + window.history.replaceState({}, document.title, window.location.pathname); + setIsLoading(false); + return; + } + + if (urlToken) { + // Store token and clean URL + localStorage.setItem('auth-token', urlToken); + setToken(urlToken); + + // Remove token from URL + window.history.replaceState({}, document.title, window.location.pathname); + + // Pass token directly to avoid state timing issues + checkAuthStatus(urlToken); + + // Force reload after a short delay to ensure proper state update + setTimeout(() => { + window.location.reload(); + }, 100); + } else { + checkAuthStatus(); + } }, []); - const checkAuthStatus = async () => { + const checkAuthStatus = async (providedToken = null) => { try { setIsLoading(true); setError(null); @@ -47,8 +75,11 @@ export const AuthProvider = ({ children }) => { return; } + // Use provided token or state token + const authToken = providedToken || token; + // If we have a token, verify it - if (token) { + if (authToken) { try { const userResponse = await api.auth.user(); @@ -77,54 +108,6 @@ export const AuthProvider = ({ children }) => { } }; - const login = async (username, password) => { - try { - setError(null); - const response = await api.auth.login(username, password); - - const data = await response.json(); - - if (response.ok) { - setToken(data.token); - setUser(data.user); - localStorage.setItem('auth-token', data.token); - return { success: true }; - } else { - setError(data.error || 'Login failed'); - return { success: false, error: data.error || 'Login failed' }; - } - } catch (error) { - console.error('Login error:', error); - const errorMessage = 'Network error. Please try again.'; - setError(errorMessage); - return { success: false, error: errorMessage }; - } - }; - - const register = async (username, password) => { - try { - setError(null); - const response = await api.auth.register(username, password); - - const data = await response.json(); - - if (response.ok) { - setToken(data.token); - setUser(data.user); - setNeedsSetup(false); - localStorage.setItem('auth-token', data.token); - return { success: true }; - } else { - setError(data.error || 'Registration failed'); - return { success: false, error: data.error || 'Registration failed' }; - } - } catch (error) { - console.error('Registration error:', error); - const errorMessage = 'Network error. Please try again.'; - setError(errorMessage); - return { success: false, error: errorMessage }; - } - }; const logout = () => { setToken(null); @@ -142,8 +125,6 @@ export const AuthProvider = ({ children }) => { const value = { user, token, - login, - register, logout, isLoading, needsSetup, From 3db22d58b47a19c072541ac400679cc0e7eb1ea5 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:48:32 +0900 Subject: [PATCH 08/18] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20logout=20functi?= =?UTF-8?q?onality=20to=20Quick=20Settings=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Display user info with GitHub avatar and username - Add logout button with icon - Show email if available - Create account section in settings Co-Authored-By: Claude --- src/components/QuickSettingsPanel.jsx | 47 ++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/components/QuickSettingsPanel.jsx b/src/components/QuickSettingsPanel.jsx index 76e95141..58057856 100755 --- a/src/components/QuickSettingsPanel.jsx +++ b/src/components/QuickSettingsPanel.jsx @@ -12,10 +12,13 @@ import { Brain, Sparkles, FileText, - Languages + Languages, + LogOut, + Github } from 'lucide-react'; import DarkModeToggle from './DarkModeToggle'; import { useTheme } from '../contexts/ThemeContext'; +import { useAuth } from '../contexts/AuthContext'; const QuickSettingsPanel = ({ isOpen, @@ -35,6 +38,7 @@ const QuickSettingsPanel = ({ return localStorage.getItem('whisperMode') || 'default'; }); const { isDarkMode } = useTheme(); + const { user, logout } = useAuth(); useEffect(() => { setLocalIsOpen(isOpen); @@ -84,6 +88,47 @@ const QuickSettingsPanel = ({ {/* Settings Content */}
+ {/* User Info */} + {user && ( +
+

Account

+
+
+
+ {user.avatar_url ? ( + {user.username} + ) : ( +
+ +
+ )} +
+
+ {user.github_username || user.username} +
+ {user.email && ( +
+ {user.email} +
+ )} +
+
+ +
+
+
+ )} + {/* Appearance Settings */}

Appearance

From a413cf44a6b488883a2816a026cf0c2323034755 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Sun, 27 Jul 2025 20:48:44 +0900 Subject: [PATCH 09/18] =?UTF-8?q?=F0=9F=93=9D=20docs:=20Add=20comprehensiv?= =?UTF-8?q?e=20GitHub=20OAuth=20setup=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Document OAuth app creation steps - Provide environment variable configuration - Include testing procedures - Add troubleshooting section - Cover security considerations Co-Authored-By: Claude --- GITHUB_OAUTH_SETUP.md | 138 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 GITHUB_OAUTH_SETUP.md diff --git a/GITHUB_OAUTH_SETUP.md b/GITHUB_OAUTH_SETUP.md new file mode 100644 index 00000000..02a47eef --- /dev/null +++ b/GITHUB_OAUTH_SETUP.md @@ -0,0 +1,138 @@ +# GitHub OAuth Setup Guide + +This guide explains how to set up and test the GitHub OAuth integration for Claude Code UI. + +## Prerequisites + +1. A GitHub account +2. Access to create OAuth Apps in GitHub + +## Setup Instructions + +### 1. Create a GitHub OAuth App + +1. Go to https://github.com/settings/developers +2. Click "New OAuth App" +3. Fill in the application details: + - **Application name**: Claude Code UI (or your preferred name) + - **Homepage URL**: `http://localhost:3009` + - **Authorization callback URL**: `http://localhost:3008/api/auth/github/callback` +4. Click "Register application" +5. Copy the Client ID and generate a new Client Secret + +### 2. Configure Environment Variables + +Create a `.env` file in the project root (copy from `.env.example`): + +```bash +cp .env.example .env +``` + +Update the `.env` file with your GitHub OAuth credentials: + +```env +# Server ports +PORT=3008 +VITE_PORT=3009 + +# GitHub OAuth configuration +GITHUB_CLIENT_ID=your_actual_client_id +GITHUB_CLIENT_SECRET=your_actual_client_secret +GITHUB_CALLBACK_URL=http://localhost:3008/api/auth/github/callback + +# Allowed GitHub usernames (comma-separated) +GITHUB_ALLOWED_USERS=your-github-username,other-allowed-username + +# Session secret (generate a random string) +SESSION_SECRET=your-random-session-secret-here +``` + +### 3. Clear Existing Database (if needed) + +If you have an existing database, you may need to delete it to test the new schema: + +```bash +rm server/database/auth.db +rm server/database/sessions.db +``` + +### 4. Start the Application + +```bash +npm run dev +``` + +## Testing the OAuth Flow + +### Test 1: Initial Setup with GitHub + +1. Open http://localhost:3009 +2. You should see the setup screen with a "Sign in with GitHub" button +3. Click the GitHub button +4. You'll be redirected to GitHub for authorization +5. After authorizing, you'll be redirected back and logged in + +### Test 2: Allowed Users Only + +1. Try logging in with a GitHub account that's in the `GITHUB_ALLOWED_USERS` list + - Should succeed +2. Try logging in with a GitHub account that's NOT in the list + - Should be rejected with an error message + +### Test 3: Mixed Authentication + +1. First, sign in with GitHub +2. Log out +3. Create a local account (if no users exist) +4. Verify both authentication methods work + +### Test 4: Token Persistence + +1. Log in with GitHub +2. Refresh the page +3. You should remain logged in +4. Check that the token is stored in localStorage + +## Troubleshooting + +### Common Issues + +1. **"GitHub authentication required" error** + - Ensure your GitHub OAuth app is properly configured + - Check that environment variables are set correctly + +2. **Redirect URI mismatch** + - Make sure the callback URL in your GitHub app matches exactly: `http://localhost:3008/api/auth/github/callback` + +3. **User not authorized** + - Verify the GitHub username is in the `GITHUB_ALLOWED_USERS` environment variable + - Usernames are case-sensitive + +4. **Session errors** + - Check that `SESSION_SECRET` is set in your environment + - Ensure the `server/database` directory exists and is writable + +### Debug Mode + +To see more detailed logs: + +1. Check server console for authentication logs +2. Check browser console for client-side errors +3. Monitor network tab for OAuth flow requests + +## Security Considerations + +1. **Never commit `.env` file** - It contains sensitive credentials +2. **Use HTTPS in production** - OAuth requires secure connections +3. **Restrict allowed users** - Only add trusted GitHub usernames +4. **Rotate secrets regularly** - Change CLIENT_SECRET and SESSION_SECRET periodically + +## Production Deployment + +For production deployment: + +1. Update OAuth App URLs to your production domain +2. Set `NODE_ENV=production` +3. Use proper HTTPS certificates +4. Store secrets in environment variables, not in code +5. Consider using a proper session store (Redis, etc.) instead of SQLite \ No newline at end of file From 408beeb659ab068988b0a976287e03bf924c68de Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:00:28 +0900 Subject: [PATCH 10/18] =?UTF-8?q?=F0=9F=92=84=20style:=20Remove=20redundan?= =?UTF-8?q?t=20authentication=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove "Authentication is managed through GitHub OAuth" message from both LoginForm and SetupForm components to simplify UI Co-Authored-By: Claude --- src/components/LoginForm.jsx | 5 ----- src/components/SetupForm.jsx | 5 ----- 2 files changed, 10 deletions(-) diff --git a/src/components/LoginForm.jsx b/src/components/LoginForm.jsx index ce47f527..bdae3fb6 100644 --- a/src/components/LoginForm.jsx +++ b/src/components/LoginForm.jsx @@ -79,11 +79,6 @@ const LoginForm = () => {
)} -
-

- Authentication is managed through GitHub OAuth -

-
diff --git a/src/components/SetupForm.jsx b/src/components/SetupForm.jsx index bcec2a72..54de7003 100644 --- a/src/components/SetupForm.jsx +++ b/src/components/SetupForm.jsx @@ -77,11 +77,6 @@ const SetupForm = () => { )} -
-

- Authentication is managed through GitHub OAuth. -

-
From 7d2a8a5a3001c604026f7bcfae8120e424d82e7e Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Mon, 28 Jul 2025 19:19:17 +0900 Subject: [PATCH 11/18] =?UTF-8?q?=F0=9F=8E=A8=20style:=20Clean=20up=20GitH?= =?UTF-8?q?ub=20auth=20implementation=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove unnecessary comments, trailing spaces, and ensure final newlines - Remove obvious implementation comments - Fix trailing whitespace issues - Add missing final newlines to all modified files - Maintain code functionality while improving readability --- server/auth/strategies/github.js | 16 +++++----------- server/config/env.js | 3 +-- server/routes/auth.js | 33 ++++++++------------------------ src/components/LoginForm.jsx | 12 ++++-------- src/contexts/AuthContext.jsx | 32 ++++++++++--------------------- 5 files changed, 28 insertions(+), 68 deletions(-) diff --git a/server/auth/strategies/github.js b/server/auth/strategies/github.js index e5a292fd..06fb7476 100644 --- a/server/auth/strategies/github.js +++ b/server/auth/strategies/github.js @@ -2,16 +2,14 @@ import passport from 'passport'; import { Strategy as GitHubStrategy } from 'passport-github2'; import { userDb } from '../../database/db.js'; -// Helper function to check if user is allowed const isAllowedUser = (githubUsername) => { - const allowedUsers = process.env.GITHUB_ALLOWED_USERS + const allowedUsers = process.env.GITHUB_ALLOWED_USERS ? process.env.GITHUB_ALLOWED_USERS.split(',').map(u => u.trim()) : []; - + return allowedUsers.includes(githubUsername); }; -// Only initialize GitHub strategy if credentials are provided if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { console.log('🔐 Registering GitHub OAuth strategy'); passport.use(new GitHubStrategy({ @@ -21,23 +19,19 @@ if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { }, async (accessToken, refreshToken, profile, done) => { try { - // Check if user is allowed if (!isAllowedUser(profile.username)) { - return done(null, false, { - message: `GitHub user @${profile.username} is not authorized to access this application` + return done(null, false, { + message: `GitHub user @${profile.username} is not authorized to access this application` }); } - // Check if user already exists const existingUser = await userDb.getUserByGithubId(profile.id); if (existingUser) { - // Update last login await userDb.updateUserLastLogin(existingUser.id); return done(null, existingUser); } - // Create new user const newUser = await userDb.createGithubUser({ username: profile.username, github_id: profile.id, @@ -70,4 +64,4 @@ passport.deserializeUser(async (id, done) => { } }); -export { isAllowedUser }; \ No newline at end of file +export { isAllowedUser }; diff --git a/server/config/env.js b/server/config/env.js index 887bd291..f39d5c63 100644 --- a/server/config/env.js +++ b/server/config/env.js @@ -1,4 +1,3 @@ -// Load environment variables before anything else import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -23,4 +22,4 @@ try { console.log('GitHub OAuth configured:', !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET)); } catch (e) { console.log('⚠️ No .env file found or error reading it:', e.message); -} \ No newline at end of file +} diff --git a/server/routes/auth.js b/server/routes/auth.js index 9c5dada7..8b13b356 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -7,18 +7,17 @@ import { isAllowedUser } from '../auth/strategies/github.js'; const router = express.Router(); -// Check auth status and setup requirements router.get('/status', async (req, res) => { try { const hasUsers = await userDb.hasUsers(); const githubConfigured = !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET); - const allowedUsers = process.env.GITHUB_ALLOWED_USERS + const allowedUsers = process.env.GITHUB_ALLOWED_USERS ? process.env.GITHUB_ALLOWED_USERS.split(',').map(u => u.trim()) : []; - + res.json({ needsSetup: !hasUsers, - isAuthenticated: false, // Will be overridden by frontend if token exists + isAuthenticated: false, githubConfigured, githubAllowedUsers: allowedUsers }); @@ -28,48 +27,34 @@ router.get('/status', async (req, res) => { } }); -// Local registration and login are disabled - use GitHub OAuth instead -// router.post('/register', ...) - removed -// router.post('/login', ...) - removed - -// Get current user (protected route) router.get('/user', authenticateToken, (req, res) => { res.json({ user: req.user }); }); -// Logout (client-side token removal, but this endpoint can be used for logging) router.post('/logout', authenticateToken, (req, res) => { - // In a simple JWT system, logout is mainly client-side - // This endpoint exists for consistency and potential future logging res.json({ success: true, message: 'Logged out successfully' }); }); -// GitHub OAuth routes router.get('/github', (req, res, next) => { - // Store the original URL if provided if (req.query.returnUrl) { req.session.returnUrl = req.query.returnUrl; } passport.authenticate('github', { scope: ['user:email'] })(req, res, next); }); -router.get('/github/callback', +router.get('/github/callback', passport.authenticate('github', { failureRedirect: '/login?error=github_auth_failed' }), async (req, res) => { try { - // Generate JWT token for the authenticated user const token = generateToken(req.user); - - // Update last login + userDb.updateUserLastLogin(req.user.id); - - // Redirect to client with token + const returnUrl = req.session.returnUrl || '/'; delete req.session.returnUrl; - - // Redirect with token in query parameter (client will handle storing it) + res.redirect(`${process.env.CLIENT_URL || 'http://localhost:3009'}${returnUrl}?token=${token}`); } catch (error) { console.error('GitHub callback error:', error); @@ -78,14 +63,12 @@ router.get('/github/callback', } ); -// Check if a GitHub username is allowed router.get('/github/check/:username', (req, res) => { const { username } = req.params; const allowed = isAllowedUser(username); res.json({ allowed }); }); -// Get GitHub authentication status router.get('/github/status', authenticateToken, (req, res) => { res.json({ isGithubAuthenticated: req.user.auth_provider === 'github', @@ -93,4 +76,4 @@ router.get('/github/status', authenticateToken, (req, res) => { }); }); -export default router; \ No newline at end of file +export default router; diff --git a/src/components/LoginForm.jsx b/src/components/LoginForm.jsx index bdae3fb6..2c192128 100644 --- a/src/components/LoginForm.jsx +++ b/src/components/LoginForm.jsx @@ -8,9 +8,8 @@ const LoginForm = () => { const [error, setError] = useState(''); const [authStatus, setAuthStatus] = useState({}); const { error: authError } = useAuth(); - + useEffect(() => { - // Get auth status including GitHub configuration const checkAuthStatus = async () => { try { const response = await api.auth.status(); @@ -25,7 +24,6 @@ const LoginForm = () => { const handleGithubLogin = () => { setIsLoading(true); - // Redirect to GitHub OAuth endpoint window.location.href = `${window.location.origin}/api/auth/github`; }; @@ -33,7 +31,6 @@ const LoginForm = () => {
- {/* Logo and Title */}
@@ -46,7 +43,6 @@ const LoginForm = () => {

- {/* GitHub Login Button */} {authStatus.githubConfigured ? (
- + {authStatus.githubAllowedUsers && authStatus.githubAllowedUsers.length > 0 && (
Allowed users: {authStatus.githubAllowedUsers.join(', ')}
)} - + {(error || authError) && (

{error || authError}

@@ -85,4 +81,4 @@ const LoginForm = () => { ); }; -export default LoginForm; \ No newline at end of file +export default LoginForm; diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx index e27e69dc..bec1dd87 100644 --- a/src/contexts/AuthContext.jsx +++ b/src/contexts/AuthContext.jsx @@ -25,33 +25,26 @@ export const AuthProvider = ({ children }) => { const [needsSetup, setNeedsSetup] = useState(false); const [error, setError] = useState(null); - // Check authentication status on mount useEffect(() => { - // Check for token in URL (OAuth callback) const urlParams = new URLSearchParams(window.location.search); const urlToken = urlParams.get('token'); const urlError = urlParams.get('error'); - + if (urlError) { - // Handle OAuth error setError(urlError === 'github_auth_failed' ? 'GitHub authentication failed. Please try again.' : 'Authentication failed.'); window.history.replaceState({}, document.title, window.location.pathname); setIsLoading(false); return; } - + if (urlToken) { - // Store token and clean URL localStorage.setItem('auth-token', urlToken); setToken(urlToken); - - // Remove token from URL + window.history.replaceState({}, document.title, window.location.pathname); - - // Pass token directly to avoid state timing issues + checkAuthStatus(urlToken); - - // Force reload after a short delay to ensure proper state update + setTimeout(() => { window.location.reload(); }, 100); @@ -65,20 +58,17 @@ export const AuthProvider = ({ children }) => { setIsLoading(true); setError(null); - // Check if system needs setup const statusResponse = await api.auth.status(); const statusData = await statusResponse.json(); - + if (statusData.needsSetup) { setNeedsSetup(true); setIsLoading(false); return; } - - // Use provided token or state token + const authToken = providedToken || token; - - // If we have a token, verify it + if (authToken) { try { const userResponse = await api.auth.user(); @@ -88,7 +78,6 @@ export const AuthProvider = ({ children }) => { setUser(userData.user); setNeedsSetup(false); } else { - // Token is invalid localStorage.removeItem('auth-token'); setToken(null); setUser(null); @@ -113,8 +102,7 @@ export const AuthProvider = ({ children }) => { setToken(null); setUser(null); localStorage.removeItem('auth-token'); - - // Optional: Call logout endpoint for logging + if (token) { api.auth.logout().catch(error => { console.error('Logout endpoint error:', error); @@ -136,4 +124,4 @@ export const AuthProvider = ({ children }) => { {children} ); -}; \ No newline at end of file +}; From c38759126338b5bfeaf8b4c60ce0f018a073a950 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Mon, 28 Jul 2025 19:28:21 +0900 Subject: [PATCH 12/18] =?UTF-8?q?=F0=9F=94=A7=20chore:=20Add=20final=20new?= =?UTF-8?q?lines=20to=20GitHub=20auth-related=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensure auth-related files end with proper newline character - GITHUB_OAUTH_SETUP.md - server/auth/passport.js - server/database/db.js - server/middleware/auth.js - src/components/ProtectedRoute.jsx - src/components/RequireGithubAuth.jsx - src/components/SetupForm.jsx - src/utils/api.js --- GITHUB_OAUTH_SETUP.md | 2 +- server/auth/passport.js | 2 +- server/database/db.js | 2 +- server/middleware/auth.js | 2 +- src/components/ProtectedRoute.jsx | 2 +- src/components/RequireGithubAuth.jsx | 2 +- src/components/SetupForm.jsx | 2 +- src/utils/api.js | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/GITHUB_OAUTH_SETUP.md b/GITHUB_OAUTH_SETUP.md index 02a47eef..4ad2d7a7 100644 --- a/GITHUB_OAUTH_SETUP.md +++ b/GITHUB_OAUTH_SETUP.md @@ -135,4 +135,4 @@ For production deployment: 2. Set `NODE_ENV=production` 3. Use proper HTTPS certificates 4. Store secrets in environment variables, not in code -5. Consider using a proper session store (Redis, etc.) instead of SQLite \ No newline at end of file +5. Consider using a proper session store (Redis, etc.) instead of SQLite diff --git a/server/auth/passport.js b/server/auth/passport.js index fc880935..735b4e9b 100644 --- a/server/auth/passport.js +++ b/server/auth/passport.js @@ -3,4 +3,4 @@ import passport from 'passport'; // Initialize passport strategies import './strategies/github.js'; -export default passport; \ No newline at end of file +export default passport; diff --git a/server/database/db.js b/server/database/db.js index b83be681..4c7cd80c 100644 --- a/server/database/db.js +++ b/server/database/db.js @@ -127,4 +127,4 @@ export { db, initializeDatabase, userDb -}; \ No newline at end of file +}; diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 0428c59b..ca0b89ae 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -109,4 +109,4 @@ export { authenticateWebSocket, requireGithubAuth, JWT_SECRET -}; \ No newline at end of file +}; diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx index 88b404bb..04462798 100644 --- a/src/components/ProtectedRoute.jsx +++ b/src/components/ProtectedRoute.jsx @@ -41,4 +41,4 @@ const ProtectedRoute = ({ children }) => { return children; }; -export default ProtectedRoute; \ No newline at end of file +export default ProtectedRoute; diff --git a/src/components/RequireGithubAuth.jsx b/src/components/RequireGithubAuth.jsx index 4d090946..839ca05e 100644 --- a/src/components/RequireGithubAuth.jsx +++ b/src/components/RequireGithubAuth.jsx @@ -78,4 +78,4 @@ const RequireGithubAuth = ({ children }) => { return children; }; -export default RequireGithubAuth; \ No newline at end of file +export default RequireGithubAuth; diff --git a/src/components/SetupForm.jsx b/src/components/SetupForm.jsx index 54de7003..827b2437 100644 --- a/src/components/SetupForm.jsx +++ b/src/components/SetupForm.jsx @@ -83,4 +83,4 @@ const SetupForm = () => { ); }; -export default SetupForm; \ No newline at end of file +export default SetupForm; diff --git a/src/utils/api.js b/src/utils/api.js index c16274bc..065b60b1 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -80,4 +80,4 @@ export const api = { body: formData, headers: {}, // Let browser set Content-Type for FormData }), -}; \ No newline at end of file +}; From 370ed152b332935744ff40ea343016b6a85e3a22 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:14:06 +0900 Subject: [PATCH 13/18] build: add react-syntax-highlighter dependency --- package-lock.json | 268 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 269 insertions(+) diff --git a/package-lock.json b/package-lock.json index cf1f0a59..856f5699 100755 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "react-dropzone": "^14.2.3", "react-markdown": "^10.1.0", "react-router-dom": "^6.8.1", + "react-syntax-highlighter": "^15.6.1", "tailwind-merge": "^3.3.1", "ws": "^8.14.2", "xterm": "^5.3.0", @@ -3423,6 +3424,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/file-selector": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", @@ -3496,6 +3510,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3805,6 +3827,16 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -3843,6 +3875,86 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/hastscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -4349,6 +4461,20 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -6038,6 +6164,15 @@ "node": ">=10" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -6289,6 +6424,23 @@ "react-dom": ">=16.8" } }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -6322,6 +6474,122 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", diff --git a/package.json b/package.json index bc0c8124..6ebc1d65 100755 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "react-dropzone": "^14.2.3", "react-markdown": "^10.1.0", "react-router-dom": "^6.8.1", + "react-syntax-highlighter": "^15.6.1", "tailwind-merge": "^3.3.1", "ws": "^8.14.2", "xterm": "^5.3.0", From 501b9be90ace907460cb91b8614caf98027a9bce Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:14:20 +0900 Subject: [PATCH 14/18] feat: create markdown utilities for content extraction --- src/utils/markdownUtils.js | 300 +++++++++++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 src/utils/markdownUtils.js diff --git a/src/utils/markdownUtils.js b/src/utils/markdownUtils.js new file mode 100644 index 00000000..3e1e1297 --- /dev/null +++ b/src/utils/markdownUtils.js @@ -0,0 +1,300 @@ +/** + * Utilities for extracting and processing markdown content from tool results + */ + +/** + * Detects the content type of a given text + * @param {string} content - The content to analyze + * @returns {'markdown' | 'json' | 'text' | 'mixed'} The detected content type + */ +export function detectContentType(content) { + if (!content || typeof content !== 'string') { + return 'text'; + } + + const trimmed = content.trim(); + + // Check for JSON + if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || + (trimmed.startsWith('[') && trimmed.endsWith(']'))) { + try { + JSON.parse(trimmed); + return 'json'; + } catch { + // Not valid JSON, continue checking + } + } + + // Check for markdown indicators + const markdownPatterns = [ + /^#{1,6}\s/m, // Headers + /^\*\s|^-\s|^\d+\.\s/m, // Lists + /```[\s\S]*```/, // Code blocks + /`[^`]+`/, // Inline code + /\*\*[^*]+\*\*/, // Bold + /\*[^*]+\*/, // Italic + /\[.+\]\(.+\)/, // Links + /^>/m, // Blockquotes + ]; + + const hasMarkdown = markdownPatterns.some(pattern => pattern.test(content)); + return hasMarkdown ? 'markdown' : 'text'; +} + +/** + * Extracts meaningful content from tool results + * @param {string} toolName - The name of the tool + * @param {string} toolInput - The tool input parameters + * @param {string} toolResult - The tool result/output + * @returns {Object} Extracted content with type and metadata + */ +export function extractToolContent(toolName, toolInput, toolResult) { + // Priority: toolResult > meaningful toolInput content > raw parameters + + // First, try to extract from tool result + if (toolResult && typeof toolResult === 'string' && toolResult.trim()) { + const contentType = detectContentType(toolResult); + + // Special handling for specific tools + if (toolName === 'exit_plan_mode' || toolName === 'ExitPlanMode') { + try { + // Try to parse JSON response + const parsed = JSON.parse(toolResult); + if (parsed.plan) { + return { + contentType: 'markdown', + primaryContent: parsed.plan.replace(/\\n/g, '\n'), + metadata: { title: 'Implementation Plan' } + }; + } + } catch { + // If not JSON, use the raw content + return { + contentType, + primaryContent: toolResult, + metadata: { title: 'Plan Result' } + }; + } + } + + // For Result tool, extract the content + if (toolName === 'Result') { + return { + contentType, + primaryContent: toolResult, + metadata: { title: 'Result' } + }; + } + + // Generic tool result handling + return { + contentType, + primaryContent: toolResult, + metadata: { toolName } + }; + } + + // Next, try to extract from tool input + if (toolInput && typeof toolInput === 'string') { + try { + const input = JSON.parse(toolInput); + + // ExitPlanMode tool input handling + if ((toolName === 'exit_plan_mode' || toolName === 'ExitPlanMode') && input.plan) { + return { + contentType: 'markdown', + primaryContent: input.plan.replace(/\\n/g, '\n'), + metadata: { title: 'Implementation Plan' } + }; + } + + // Result tool input handling + if (toolName === 'Result' && input.result) { + const contentType = detectContentType(input.result); + return { + contentType, + primaryContent: input.result, + metadata: { title: 'Result' } + }; + } + + // Write/Edit tool content + if ((toolName === 'Write' || toolName === 'Edit') && input.content) { + return { + contentType: 'text', + primaryContent: input.content, + metadata: { + language: detectLanguageFromPath(input.file_path || input.path), + filePath: input.file_path || input.path + } + }; + } + + // MultiEdit tool + if (toolName === 'MultiEdit' && input.edits) { + return { + contentType: 'mixed', + primaryContent: JSON.stringify(input.edits, null, 2), + metadata: { + filePath: input.file_path || input.path, + editCount: input.edits.length + } + }; + } + + // Read tool + if (toolName === 'Read' && input.file_path) { + return { + contentType: 'text', + primaryContent: toolInput, // Will be replaced by actual content in result + metadata: { + filePath: input.file_path, + language: detectLanguageFromPath(input.file_path) + } + }; + } + + } catch { + // Failed to parse input, fall back to raw display + } + } + + // Fallback: return raw parameters + return { + contentType: 'json', + primaryContent: toolInput || '', + metadata: { toolName }, + fallback: true + }; +} + +/** + * Detects programming language from file path + * @param {string} filePath - The file path to analyze + * @returns {string} The detected language or empty string + */ +export function detectLanguageFromPath(filePath) { + if (!filePath) return ''; + + const ext = filePath.split('.').pop()?.toLowerCase(); + const languageMap = { + 'js': 'javascript', + 'jsx': 'javascript', + 'ts': 'typescript', + 'tsx': 'typescript', + 'py': 'python', + 'rb': 'ruby', + 'java': 'java', + 'cpp': 'cpp', + 'c': 'c', + 'cs': 'csharp', + 'php': 'php', + 'go': 'go', + 'rs': 'rust', + 'swift': 'swift', + 'kt': 'kotlin', + 'scala': 'scala', + 'sh': 'bash', + 'bash': 'bash', + 'zsh': 'bash', + 'ps1': 'powershell', + 'sql': 'sql', + 'html': 'html', + 'css': 'css', + 'scss': 'scss', + 'sass': 'sass', + 'less': 'less', + 'xml': 'xml', + 'json': 'json', + 'yaml': 'yaml', + 'yml': 'yaml', + 'md': 'markdown', + 'markdown': 'markdown', + 'rst': 'rst', + 'tex': 'latex', + 'r': 'r', + 'matlab': 'matlab', + 'lua': 'lua', + 'vim': 'vim', + 'dockerfile': 'dockerfile', + 'makefile': 'makefile', + }; + + return languageMap[ext] || ext || ''; +} + +/** + * Sanitizes content for safe rendering + * @param {string} content - The content to sanitize + * @returns {string} Sanitized content + */ +export function sanitizeContent(content) { + if (!content || typeof content !== 'string') { + return ''; + } + + // Remove any potential script tags (additional safety layer) + return content + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/javascript:/gi, '') + .replace(/on\w+\s*=/gi, ''); +} + +/** + * Truncates content with ellipsis if too long + * @param {string} content - The content to truncate + * @param {number} maxLength - Maximum length before truncation + * @returns {Object} Object with truncated content and isTruncated flag + */ +export function truncateContent(content, maxLength = 5000) { + if (!content || content.length <= maxLength) { + return { content, isTruncated: false }; + } + + const truncated = content.substring(0, maxLength); + return { + content: truncated + '...', + isTruncated: true, + fullLength: content.length + }; +} + +/** + * Preprocesses markdown content to convert single-quoted text to inline code + * @param {string} content - The markdown content to preprocess + * @returns {string} Preprocessed content with single quotes converted to backticks + */ +export function preprocessMarkdown(content) { + if (!content || typeof content !== 'string') { + return content; + } + + // Split content by code blocks to preserve them + const codeBlockPattern = /```[\s\S]*?```/g; + const codeBlocks = content.match(codeBlockPattern) || []; + + // Replace code blocks with placeholders + let processed = content; + codeBlocks.forEach((block, i) => { + processed = processed.replace(block, `__CODE_BLOCK_${i}__`); + }); + + // Convert 'text' to `text` (but not contractions or possessives) + // Matches 'word' or 'multiple words' but not don't, won't, it's, etc. + // Also avoids matching quotes at the start/end of lines to prevent issues with quoted speech + processed = processed.replace(/(^|[^a-zA-Z])'([^']+?)'([^a-zA-Z]|$)/g, (match, before, content, after) => { + // Ensure the match is not part of a contraction or possessive + if (/\w'/.test(match)) { + return match; // Skip replacement for contractions/possessives + } + return `${before}\`${content}\`${after}`; + }); + + // Restore code blocks + codeBlocks.forEach((block, i) => { + processed = processed.replace(`__CODE_BLOCK_${i}__`, block); + }); + + return processed; +} + From 78ba4a711a0c28c1c505c2a32cf82072c29bdd49 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:14:36 +0900 Subject: [PATCH 15/18] feat: create CodeBlock component with syntax highlighting --- src/components/CodeBlock.jsx | 135 +++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/components/CodeBlock.jsx diff --git a/src/components/CodeBlock.jsx b/src/components/CodeBlock.jsx new file mode 100644 index 00000000..4ac424ba --- /dev/null +++ b/src/components/CodeBlock.jsx @@ -0,0 +1,135 @@ +import React, { useState, memo, useCallback } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { useTheme } from '../contexts/ThemeContext'; + +/** + * Enhanced code block component with syntax highlighting and copy functionality + */ +const CodeBlock = memo(({ + language = '', + children, + inline = false, + showLineNumbers = false, + className = '' +}) => { + const [copied, setCopied] = useState(false); + const { isDarkMode } = useTheme(); + + const code = String(children).replace(/\n$/, ''); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy code:', err); + // Fallback for older browsers + try { + const textArea = document.createElement('textarea'); + textArea.value = code; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (fallbackErr) { + console.error('Fallback copy also failed:', fallbackErr); + } + } + }, [code]); + + // Inline code rendering + if (inline) { + return ( + + {children} + + ); + } + + // Full code block rendering - use a span wrapper to avoid div in p issue + const codeBlock = ( +
+ + + + + {language && ( + + {language} + + )} + + + {code} + +
+ ); + + return codeBlock; +}); + +CodeBlock.displayName = 'CodeBlock'; + +/** + * Wrapper component for use with ReactMarkdown + */ +export const MarkdownCodeBlock = ({ node, inline, className, children, ...props }) => { + const match = /language-(\w+)/.exec(className || ''); + const language = match ? match[1] : ''; + + return ( + + {children} + + ); +}; + +export default CodeBlock; From d284ac1c95965250b88ce177ece23ce67b57d7b2 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:14:52 +0900 Subject: [PATCH 16/18] feat: create lazy loading wrapper for CodeBlock --- src/components/CodeBlockLazy.jsx | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/components/CodeBlockLazy.jsx diff --git a/src/components/CodeBlockLazy.jsx b/src/components/CodeBlockLazy.jsx new file mode 100644 index 00000000..e1660a24 --- /dev/null +++ b/src/components/CodeBlockLazy.jsx @@ -0,0 +1,53 @@ +import React, { lazy, Suspense } from 'react'; + +// Lazy load with error boundary +const CodeBlock = lazy(() => + import('./CodeBlock').catch(() => { + console.error('Failed to load CodeBlock component'); + // Return a fallback component on error + return { default: ({ children, className = '' }) => ( + + {children} + + )}; + }) +); + +const CodeBlockFallback = ({ children, className = '' }) => ( + + {children} + +); + +export const LazyCodeBlock = (props) => ( + }> + + +); + +export const MarkdownCodeBlock = ({ node, inline, className, children, ...props }) => { + const match = /language-(\w+)/.exec(className || ''); + const language = match ? match[1] : ''; + + const isInline = inline !== undefined ? inline : !match; + + if (isInline) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +}; + +export default LazyCodeBlock; From 09350a8702718f0e68c4b40be489beb1aa4fbf66 Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:15:06 +0900 Subject: [PATCH 17/18] feat: create ToolResultRenderer for enhanced tool output --- src/components/ToolResultRenderer.jsx | 234 ++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 src/components/ToolResultRenderer.jsx diff --git a/src/components/ToolResultRenderer.jsx b/src/components/ToolResultRenderer.jsx new file mode 100644 index 00000000..dda1ff37 --- /dev/null +++ b/src/components/ToolResultRenderer.jsx @@ -0,0 +1,234 @@ +import React, { memo, useMemo } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { extractToolContent, sanitizeContent, truncateContent, preprocessMarkdown } from '../utils/markdownUtils'; +import { MarkdownCodeBlock } from './CodeBlockLazy'; +import TodoList from './TodoList'; + +/** + * Component for rendering tool results with enhanced markdown support + */ +const ToolResultRenderer = memo(({ + toolName, + toolInput, + toolResult, + showRawParameters = false, + autoExpandTools = true, + truncationLimit = 5000, + className = '' +}) => { + // Extract meaningful content from tool result with error handling + const extractedContent = useMemo(() => { + try { + return extractToolContent(toolName, toolInput, toolResult); + } catch (error) { + console.error('Error extracting tool content:', error); + return { + contentType: 'text', + primaryContent: toolInput || '', + metadata: { toolName }, + fallback: true + }; + } + }, [toolName, toolInput, toolResult]); + + // Sanitize and prepare content for rendering + const { primaryContent, contentType, metadata, fallback } = extractedContent; + const sanitizedContent = sanitizeContent(primaryContent); + const { content: displayContent, isTruncated, fullLength } = truncateContent(sanitizedContent, truncationLimit); + + // Special handling for TodoWrite tool + if (toolName === 'TodoWrite') { + try { + const input = JSON.parse(toolInput); + if (input.todos && Array.isArray(input.todos)) { + return ( +
+ + + + + Updating Todo List + +
+ + {showRawParameters && ( + + )} +
+
+ ); + } + } catch (e) { + // Fall through to default rendering + } + } + + // Special handling for exit_plan_mode with enhanced markdown + if (toolName === 'exit_plan_mode' || toolName === 'ExitPlanMode') { + return ( +
+ + + + + 📋 {metadata?.title || 'View implementation plan'} + +
+ + {isTruncated && ( +

+ Content truncated ({fullLength} characters total) +

+ )} + {showRawParameters && ( + + )} +
+
+ ); + } + + // Result tool with enhanced display + if (toolName === 'Result' && !fallback) { + return ( +
+
+ + + + Result +
+
+ + {isTruncated && ( +

+ Content truncated ({fullLength} characters total) +

+ )} +
+
+ ); + } + + // Generic tool result rendering + if (!fallback && primaryContent && primaryContent !== toolInput) { + return ( +
+ + + + + {metadata?.title || `View ${toolName} result`} + +
+ + {isTruncated && ( +

+ Content truncated ({fullLength} characters total) +

+ )} + {showRawParameters && ( + + )} +
+
+ ); + } + + // Fallback to raw parameters display + return showRawParameters ? ( +
+ + View {toolName} parameters + + +
+ ) : null; +}); + +/** + * Component for rendering markdown content with enhanced features + */ +const MarkdownContent = memo(({ content, contentType = 'markdown', metadata = {} }) => { + // Custom components for ReactMarkdown + const components = useMemo(() => ({ + code: MarkdownCodeBlock, + pre: ({ children }) => <>{children}, // Remove default pre wrapper + table: ({ children }) => ( +
+ + {children} +
+
+ ), + thead: ({ children }) => ( + {children} + ), + th: ({ children }) => ( + + {children} + + ), + td: ({ children }) => ( + + {children} + + ), + a: ({ href, children }) => ( + + {children} + + ), + }), []); + + if (contentType === 'json') { + return ( +
+        
+          {content}
+        
+      
+ ); + } + + if (contentType === 'text' && metadata.language) { + return ( + + {content} + + ); + } + + return ( +
+ + {preprocessMarkdown(content)} + +
+ ); +}); + +/** + * Component for displaying raw parameters + */ +const RawParametersView = memo(({ content }) => ( +
+ + View raw parameters + +
+      {content}
+    
+
+)); + +ToolResultRenderer.displayName = 'ToolResultRenderer'; +MarkdownContent.displayName = 'MarkdownContent'; +RawParametersView.displayName = 'RawParametersView'; + +export default ToolResultRenderer; From a39d9ba43d3a3c0d550994743d601de4eb96137a Mon Sep 17 00:00:00 2001 From: Keigo Yamauchi <40446332+Ovaw@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:15:21 +0900 Subject: [PATCH 18/18] refactor: integrate enhanced markdown rendering in ChatInterface --- src/components/ChatInterface.jsx | 671 +++++++++---------------------- 1 file changed, 194 insertions(+), 477 deletions(-) diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index d61ddd31..1ad5bc0d 100755 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -1,18 +1,18 @@ /* * ChatInterface.jsx - Chat Component with Session Protection Integration - * + * * SESSION PROTECTION INTEGRATION: * =============================== - * + * * This component integrates with the Session Protection System to prevent project updates * from interrupting active conversations: - * + * * Key Integration Points: * 1. handleSubmit() - Marks session as active when user sends message (including temp ID for new sessions) - * 2. session-created handler - Replaces temporary session ID with real WebSocket session ID + * 2. session-created handler - Replaces temporary session ID with real WebSocket session ID * 3. claude-complete handler - Marks session as inactive when conversation finishes * 4. session-aborted handler - Marks session as inactive when conversation is aborted - * + * * This ensures uninterrupted chat experience by coordinating with App.jsx to pause sidebar updates. */ @@ -21,6 +21,8 @@ import ReactMarkdown from 'react-markdown'; import { useDropzone } from 'react-dropzone'; import TodoList from './TodoList'; import ClaudeLogo from './ClaudeLogo.jsx'; +import ToolResultRenderer from './ToolResultRenderer'; +import { MarkdownCodeBlock } from './CodeBlockLazy'; import ClaudeStatus from './ClaudeStatus'; import { MicButton } from './MicButton.jsx'; @@ -44,7 +46,7 @@ const safeLocalStorage = { console.warn('Could not parse chat messages for truncation:', parseError); } } - + localStorage.setItem(key, value); } catch (error) { if (error.name === 'QuotaExceededError') { @@ -52,7 +54,7 @@ const safeLocalStorage = { // Clear old chat messages to free up space const keys = Object.keys(localStorage); const chatKeys = keys.filter(k => k.startsWith('chat_messages_')).sort(); - + // Remove oldest chat data first, keeping only the 3 most recent projects if (chatKeys.length > 3) { chatKeys.slice(0, chatKeys.length - 3).forEach(k => { @@ -60,13 +62,13 @@ const safeLocalStorage = { console.log(`Removed old chat data: ${k}`); }); } - + // If still failing, clear draft inputs too const draftKeys = keys.filter(k => k.startsWith('draft_input_')); draftKeys.forEach(k => { localStorage.removeItem(k); }); - + // Try again with reduced data try { localStorage.setItem(key, value); @@ -110,14 +112,14 @@ const safeLocalStorage = { // Memoized message component to prevent unnecessary re-renders const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters }) => { - const isGrouped = prevMessage && prevMessage.type === message.type && - prevMessage.type === 'assistant' && + const isGrouped = prevMessage && prevMessage.type === message.type && + prevMessage.type === 'assistant' && !prevMessage.isToolUse && !message.isToolUse; const messageRef = React.useRef(null); const [isExpanded, setIsExpanded] = React.useState(false); React.useEffect(() => { if (!autoExpandTools || !messageRef.current || !message.isToolUse) return; - + const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { @@ -133,9 +135,9 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile }, { threshold: 0.1 } ); - + observer.observe(messageRef.current); - + return () => { if (messageRef.current) { observer.unobserve(messageRef.current); @@ -197,9 +199,9 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
)} - +
- + {message.isToolUse && !['Read', 'TodoWrite', 'TodoRead'].includes(message.toolName) ? (
@@ -243,8 +245,8 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile - 📝 View edit diff for - - ))} -
- - {selectedOption && ( -
-

- ✓ Claude selected option {selectedOption} -

-

- In the CLI, you would select this option interactively using arrow keys or by typing the number. -

-
- )} -
-
-
-
- ); - } - - const fileEditMatch = content.match(/The file (.+?) has been updated\./); - if (fileEditMatch) { - return ( -
-
- File updated successfully -
- -
- ); - } - - // Handle Write tool output for file creation - const fileCreateMatch = content.match(/(?:The file|File) (.+?) has been (?:created|written)(?: successfully)?\.?/); - if (fileCreateMatch) { - return ( -
-
- File created successfully -
- -
- ); - } - - // Special handling for Write tool - hide content if it's just the file content - if (message.toolName === 'Write' && !message.toolResult.isError) { - // For Write tool, the diff is already shown in the tool input section - // So we just show a success message here - return ( -
-
- - - - File written successfully -
-

- The file content is displayed in the diff view above -

-
- ); - } - - if (content.includes('cat -n') && content.includes('→')) { - return ( -
- - - - - View file content - -
-
- {content} -
-
-
- ); - } - - if (content.length > 300) { - return ( -
- - - - - View full output ({content.length} chars) - -
- {content} -
-
- ); - } - - return ( -
- {content} -
- ); - })()} -
-
+ )}
) : message.isInteractivePrompt ? ( @@ -875,7 +603,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile const lines = message.content.split('\n').filter(line => line.trim()); const questionLine = lines.find(line => line.includes('?')) || lines[0] || ''; const options = []; - + // Parse the menu options lines.forEach(line => { // Match lines like "❯ 1. Yes" or " 2. No" @@ -889,13 +617,13 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile }); } }); - + return ( <>

{questionLine}

- + {/* Option buttons */}
{options.map((option) => ( @@ -926,7 +654,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile ))}
- +

⏳ Waiting for your response in the CLI @@ -951,7 +679,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile return (

📖 Read{' '} -
)} - + {visibleMessages.map((message, index) => { const prevMessage = index > 0 ? visibleMessages[index - 1] : null; - + return ( )} - + {isLoading && (
@@ -2320,7 +2037,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
)} - +
@@ -2330,12 +2047,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess isInputFocused ? 'pb-2 sm:pb-4 md:pb-6' : 'pb-16 sm:pb-4 md:pb-6' }`}> {/* Claude Working Status - positioned above the input form */} - - + {/* Permission Mode Selector with scroll to bottom button - Above input, clickable for mobile */}
@@ -2343,7 +2060,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess type="button" onClick={handleModeSwitch} className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-all duration-200 ${ - permissionMode === 'default' + permissionMode === 'default' ? 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600' : permissionMode === 'acceptEdits' ? 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-300 dark:border-green-600 hover:bg-green-100 dark:hover:bg-green-900/30' @@ -2355,7 +2072,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess >
- + {/* Scroll to bottom button - positioned next to mode indicator */} {isUserScrolledUp && chatMessages.length > 0 && (
- +
{/* Drag overlay */} {isDragActive && ( @@ -2399,7 +2116,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
)} - + {/* Image attachments preview */} {attachedImages.length > 0 && (
@@ -2418,7 +2135,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
)} - + {/* File dropdown - positioned outside dropzone to avoid conflicts */} {showFileDropdown && filteredFiles.length > 0 && (
@@ -2449,7 +2166,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess ))}
)} - +