@programinglive/dev-workflow-mcp-server 1.7.13 → 1.7.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ import fetch from 'node-fetch';
2
+
3
+ const API_BASE_URL = 'https://hollywood-andrew-cycle-genre.trycloudflare.com';
4
+ const USERNAME = 'programinglive';
5
+ const PASSWORD = 'mcpdevserver';
6
+
7
+ async function testAuthFlow() {
8
+ console.log('šŸš€ Starting Auth Flow Test...');
9
+ console.log(`Target: ${API_BASE_URL}\n`);
10
+
11
+ try {
12
+ // 1. Login
13
+ console.log('1ļøāƒ£ Attempting Login...');
14
+ const loginRes = await fetch(`${API_BASE_URL}/api/auth/login`, {
15
+ method: 'POST',
16
+ headers: { 'Content-Type': 'application/json' },
17
+ body: JSON.stringify({ username: USERNAME, password: PASSWORD }),
18
+ });
19
+
20
+ console.log(` Status: ${loginRes.status}`);
21
+ if (!loginRes.ok) {
22
+ throw new Error(`Login failed: ${loginRes.statusText}`);
23
+ }
24
+
25
+ const loginData = await loginRes.json();
26
+ const token = loginData.token;
27
+
28
+ if (token) {
29
+ console.log(' āœ… Login Successful! JWT Token received.');
30
+ console.log(` Token: ${token.substring(0, 20)}...`);
31
+ } else {
32
+ console.log(' āš ļø Login successful but NO TOKEN returned (Legacy Session mode?)');
33
+ }
34
+
35
+ // 2. Access Protected Route (GET /me)
36
+ console.log('\n2ļøāƒ£ Verifying Session/Token (GET /api/auth/me)...');
37
+ const headers = {};
38
+ if (token) {
39
+ headers['Authorization'] = `Bearer ${token}`;
40
+ console.log(' Using Authorization: Bearer header');
41
+ } else {
42
+ console.log(' Using Cookies (if any)');
43
+ }
44
+
45
+ const meRes = await fetch(`${API_BASE_URL}/api/auth/me`, {
46
+ headers,
47
+ });
48
+
49
+ console.log(` Status: ${meRes.status}`);
50
+ if (meRes.ok) {
51
+ const meData = await meRes.json();
52
+ console.log(` āœ… Authorized! User: ${meData.username} (${meData.email})`);
53
+ } else {
54
+ console.error(' āŒ Authorization Failed!');
55
+ console.error(await meRes.text());
56
+ }
57
+
58
+ // 3. Access History (GET /api/history)
59
+ console.log('\n3ļøāƒ£ Fetching History...');
60
+ const historyRes = await fetch(`${API_BASE_URL}/api/history`, { headers });
61
+ console.log(` Status: ${historyRes.status}`);
62
+
63
+ if (historyRes.ok) {
64
+ const historyData = await historyRes.json();
65
+ console.log(` āœ… History fetched! Count: ${historyData.history ? historyData.history.length : 0}`);
66
+ } else {
67
+ console.log(' āŒ History fetch failed.');
68
+ }
69
+
70
+ } catch (error) {
71
+ console.error('\nāŒ Test Failed:', error.message);
72
+ }
73
+ }
74
+
75
+ testAuthFlow();
@@ -8,7 +8,7 @@ import path from 'path';
8
8
  const exec = promisify(execCallback);
9
9
 
10
10
  test('build generates dist/index.mjs file', async () => {
11
- const { stderr } = await exec('npm run build:server');
11
+ const { stderr } = await exec('NODE_ENV=development npm run build:server');
12
12
  // Allow the known vite warning about node:process
13
13
  const knownWarning = '[plugin vite:resolve] Module "node:process" has been externalized for browser compatibility';
14
14
  const tailwindOutput = 'ā‰ˆ tailwindcss';
@@ -0,0 +1,73 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { spawn } from 'node:child_process';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const ROOT = path.join(__dirname, '..');
10
+
11
+ test('Server startup performance and resilience', async (t) => {
12
+ await t.test('Server starts in under 2 seconds (Studio timeout safety)', async () => {
13
+ const start = Date.now();
14
+ const server = spawn('node', [path.join(ROOT, 'index.js')], {
15
+ env: { ...process.env, DEV_WORKFLOW_DB_TYPE: 'sqlite' },
16
+ stdio: ['pipe', 'pipe', 'pipe']
17
+ });
18
+
19
+ return new Promise((resolve, reject) => {
20
+ let output = '';
21
+ const timeout = setTimeout(() => {
22
+ server.kill();
23
+ reject(new Error('Server timed out starting'));
24
+ }, 5000);
25
+
26
+ server.stderr.on('data', (data) => {
27
+ output += data.toString();
28
+ if (output.includes('running on stdio')) {
29
+ const duration = Date.now() - start;
30
+ console.log(`Verified startup duration: ${duration}ms`);
31
+ assert.ok(duration < 2000, `Startup should be fast (took ${duration}ms)`);
32
+ clearTimeout(timeout);
33
+ server.kill();
34
+ resolve();
35
+ }
36
+ });
37
+
38
+ server.on('error', (err) => {
39
+ clearTimeout(timeout);
40
+ reject(err);
41
+ });
42
+ });
43
+ });
44
+
45
+ await t.test('Resilience: Server starts even with unreachable database', async () => {
46
+ const server = spawn('node', [path.join(ROOT, 'index.js')], {
47
+ env: {
48
+ ...process.env,
49
+ DEV_WORKFLOW_DB_TYPE: 'postgres',
50
+ DEV_WORKFLOW_DB_URL: 'postgres://user:pass@1.2.3.4:5432/invalid'
51
+ },
52
+ stdio: ['pipe', 'pipe', 'pipe']
53
+ });
54
+
55
+ return new Promise((resolve, reject) => {
56
+ let output = '';
57
+ const timeout = setTimeout(() => {
58
+ server.kill();
59
+ reject(new Error('Server hung on unreachable DB'));
60
+ }, 15000); // Allow time for the 5s DB timeout + startup
61
+
62
+ server.stderr.on('data', (data) => {
63
+ output += data.toString();
64
+ if (output.includes('running on stdio')) {
65
+ assert.ok(true, 'Server started despite invalid DB');
66
+ clearTimeout(timeout);
67
+ server.kill();
68
+ resolve();
69
+ }
70
+ });
71
+ });
72
+ });
73
+ });
@@ -0,0 +1,25 @@
1
+ FROM node:20-alpine
2
+
3
+ # Install dependencies for native modules
4
+ RUN apk add --no-cache python3 make g++ postgresql-client
5
+
6
+ WORKDIR /app
7
+
8
+ # Copy package files
9
+ COPY package*.json ./
10
+
11
+ # Install dependencies
12
+ RUN npm install --only=production
13
+
14
+ # Copy application code
15
+ COPY . .
16
+
17
+ # Expose API port
18
+ EXPOSE 8080
19
+
20
+ # Set environment
21
+ ENV NODE_ENV=production
22
+ ENV API_PORT=8080
23
+
24
+ # Run API server
25
+ CMD ["node", "api-server.js"]
@@ -1,11 +1,13 @@
1
- const request = require('supertest');
2
- const express = require('express');
1
+ import { test, describe } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import request from 'supertest';
4
+ import express from 'express';
3
5
 
4
6
  describe('Workflow History API', () => {
5
7
  let app;
6
8
  let server;
7
9
 
8
- beforeAll(() => {
10
+ test('setup app', () => {
9
11
  // Mock Express app for testing
10
12
  app = express();
11
13
  app.use(express.json());
@@ -40,9 +42,9 @@ describe('Workflow History API', () => {
40
42
  .get('/api/workflow/history')
41
43
  .expect(200);
42
44
 
43
- expect(response.body).toHaveProperty('history');
44
- expect(Array.isArray(response.body.history)).toBe(true);
45
- expect(response.body.history.length).toBeGreaterThan(0);
45
+ assert(response.body.history, 'History property should exist');
46
+ assert(Array.isArray(response.body.history), 'History should be an array');
47
+ assert(response.body.history.length > 0, 'History should not be empty');
46
48
  });
47
49
 
48
50
  test('History items have required fields', async () => {
@@ -51,11 +53,11 @@ describe('Workflow History API', () => {
51
53
  .expect(200);
52
54
 
53
55
  const item = response.body.history[0];
54
- expect(item).toHaveProperty('id');
55
- expect(item).toHaveProperty('task_type');
56
- expect(item).toHaveProperty('description');
57
- expect(item).toHaveProperty('commit_message');
58
- expect(item).toHaveProperty('completed_at');
59
- expect(item).toHaveProperty('tests_passed');
56
+ assert(item.id, 'Item should have id');
57
+ assert(item.task_type, 'Item should have task_type');
58
+ assert(item.description, 'Item should have description');
59
+ assert(item.commit_message, 'Item should have commit_message');
60
+ assert(item.completed_at, 'Item should have completed_at');
61
+ assert(item.tests_passed !== undefined, 'Item should have tests_passed');
60
62
  });
61
63
  });
@@ -0,0 +1,211 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import pg from 'pg';
4
+ import dotenv from 'dotenv';
5
+ import bcrypt from 'bcrypt';
6
+ import jwt from 'jsonwebtoken';
7
+
8
+ dotenv.config({ path: '../.env' });
9
+
10
+ const app = express();
11
+ const { Pool } = pg;
12
+
13
+ const PORT = process.env.API_PORT || 8080;
14
+ const dbUrl = process.env.DEV_WORKFLOW_DB_URL;
15
+ const JWT_SECRET = process.env.JWT_SECRET || 'dev-workflow-jwt-secret-change-me';
16
+
17
+ if (!dbUrl) {
18
+ console.error('āŒ DEV_WORKFLOW_DB_URL is not set');
19
+ process.exit(1);
20
+ }
21
+
22
+ const pool = new Pool({ connectionString: dbUrl });
23
+
24
+ // Authentication helper functions
25
+ async function authenticateUser(pool, username, password) {
26
+ const result = await pool.query(
27
+ 'SELECT * FROM users WHERE username = $1',
28
+ [username]
29
+ );
30
+
31
+ if (result.rows.length === 0) {
32
+ return null;
33
+ }
34
+
35
+ const user = result.rows[0];
36
+ const isValid = await bcrypt.compare(password, user.password_hash);
37
+
38
+ if (!isValid) {
39
+ return null;
40
+ }
41
+
42
+ return {
43
+ id: user.id,
44
+ username: user.username,
45
+ email: user.email,
46
+ };
47
+ }
48
+
49
+ async function getUserById(pool, userId) {
50
+ const result = await pool.query(
51
+ 'SELECT id, username, email FROM users WHERE id = $1',
52
+ [userId]
53
+ );
54
+
55
+ return result.rows[0] || null;
56
+ }
57
+
58
+ // CORS configuration - allow Netlify domain
59
+ const corsOptions = {
60
+ origin: [
61
+ 'https://devworkflow.programinglive.com',
62
+ 'http://localhost:3000',
63
+ ],
64
+ credentials: true,
65
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
66
+ allowedHeaders: ['Content-Type', 'Authorization'],
67
+ };
68
+
69
+ app.use(cors(corsOptions));
70
+ app.use(express.json());
71
+ app.use(express.urlencoded({ extended: true }));
72
+ app.set('trust proxy', 1);
73
+
74
+ // Middleware to verify JWT
75
+ const requireAuth = (req, res, next) => {
76
+ const authHeader = req.headers.authorization;
77
+
78
+ if (authHeader && authHeader.startsWith('Bearer ')) {
79
+ const token = authHeader.split(' ')[1];
80
+ try {
81
+ const decoded = jwt.verify(token, JWT_SECRET);
82
+ req.user = decoded;
83
+ return next();
84
+ } catch (err) {
85
+ return res.status(401).json({ error: 'Invalid token' });
86
+ }
87
+ }
88
+
89
+ return res.status(401).json({ error: 'Authentication required' });
90
+ };
91
+
92
+ // Health check endpoint
93
+ app.get('/api/health', (req, res) => {
94
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
95
+ });
96
+
97
+ // Authentication endpoints
98
+ app.post('/api/auth/login', async (req, res) => {
99
+ try {
100
+ const { username, password } = req.body;
101
+
102
+ if (!username || !password) {
103
+ return res.status(400).json({ error: 'Username and password are required' });
104
+ }
105
+
106
+ const user = await authenticateUser(pool, username, password);
107
+
108
+ if (!user) {
109
+ return res.status(401).json({ error: 'Invalid credentials' });
110
+ }
111
+
112
+ // Generate JWT Token
113
+ const token = jwt.sign(
114
+ { userId: user.id, username: user.username },
115
+ JWT_SECRET,
116
+ { expiresIn: '30d' }
117
+ );
118
+
119
+ res.json({
120
+ success: true,
121
+ token,
122
+ user: {
123
+ id: user.id,
124
+ username: user.username,
125
+ email: user.email,
126
+ },
127
+ });
128
+ } catch (error) {
129
+ console.error('Login error:', error);
130
+ res.status(500).json({ error: 'Internal server error' });
131
+ }
132
+ });
133
+
134
+ app.post('/api/auth/logout', (req, res) => {
135
+ // Client-side only action for JWT
136
+ res.json({ success: true });
137
+ });
138
+
139
+ app.get('/api/auth/me', requireAuth, async (req, res) => {
140
+ try {
141
+ const user = await getUserById(pool, req.user.userId);
142
+
143
+ if (!user) {
144
+ return res.status(404).json({ error: 'User not found' });
145
+ }
146
+
147
+ res.json({
148
+ id: user.id,
149
+ username: user.username,
150
+ email: user.email,
151
+ });
152
+ } catch (error) {
153
+ console.error('Get user error:', error);
154
+ res.status(500).json({ error: 'Internal server error' });
155
+ }
156
+ });
157
+
158
+ // Workflow history endpoints
159
+ app.get('/api/history', requireAuth, async (req, res) => {
160
+ try {
161
+ const result = await pool.query(
162
+ `SELECT id, task_type, description, commit_message, completed_at, tests_passed, documentation_type
163
+ FROM workflow_history
164
+ WHERE user_id = $1
165
+ ORDER BY completed_at DESC
166
+ LIMIT 100`,
167
+ [req.user.userId]
168
+ );
169
+
170
+ res.json({
171
+ history: result.rows,
172
+ });
173
+ } catch (error) {
174
+ console.error('Get history error:', error);
175
+ res.status(500).json({ error: 'Failed to fetch history' });
176
+ }
177
+ });
178
+
179
+ app.post('/api/history', requireAuth, async (req, res) => {
180
+ try {
181
+ const { task_type, description, commit_message, tests_passed, documentation_type } = req.body;
182
+
183
+ const result = await pool.query(
184
+ `INSERT INTO workflow_history (user_id, task_type, description, commit_message, tests_passed, documentation_type, completed_at)
185
+ VALUES ($1, $2, $3, $4, $5, $6, NOW())
186
+ RETURNING *`,
187
+ [req.user.userId, task_type, description, commit_message, tests_passed, documentation_type]
188
+ );
189
+
190
+ res.json({
191
+ success: true,
192
+ history: result.rows[0],
193
+ });
194
+ } catch (error) {
195
+ console.error('Create history error:', error);
196
+ res.status(500).json({ error: 'Failed to create history entry' });
197
+ }
198
+ });
199
+
200
+ // Error handling middleware
201
+ app.use((err, req, res, next) => {
202
+ console.error('Unhandled error:', err);
203
+ res.status(500).json({ error: 'Internal server error' });
204
+ });
205
+
206
+ // Start server
207
+ app.listen(PORT, '0.0.0.0', () => {
208
+ console.log(`āœ… API server running on port ${PORT}`);
209
+ console.log(`šŸ“Š Database: ${dbUrl.replace(/:[^:]*@/, ':****@')}`);
210
+ console.log(`🌐 CORS enabled for: ${corsOptions.origin.join(', ')}`);
211
+ });
@@ -22,7 +22,7 @@ export default function DocsPage() {
22
22
  </p>
23
23
 
24
24
  <div className="bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-8">
25
- <p className="text-emerald-700 dark:text-emerald-400 font-semibold">Version: 1.7.11</p>
25
+ <p className="text-emerald-700 dark:text-emerald-400 font-semibold">Version: 1.7.15</p>
26
26
  </div>
27
27
 
28
28
  <h2 className="text-3xl font-bold mt-12 mb-6 text-gray-900 dark:text-white">Features</h2>
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useState, useMemo } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
+ import { apiClient } from '@/lib/api-client';
5
6
  import {
6
7
  CheckCircle,
7
8
  XCircle,
@@ -34,27 +35,33 @@ export default function HistoryPage() {
34
35
  const router = useRouter();
35
36
 
36
37
  useEffect(() => {
37
- fetch('/api/auth/me')
38
- .then(res => {
39
- if (res.ok) {
40
- setIsAuthenticated(true);
41
- // Sync auth state with Navbar
42
- localStorage.setItem('isLoggedIn', 'true');
43
- window.dispatchEvent(new Event('authUpdate'));
44
- return fetch('/api/history-fallback');
45
- } else {
46
- router.push('/login');
38
+ const loadData = async () => {
39
+ try {
40
+ // Verify auth
41
+ const user = await apiClient.getCurrentUser();
42
+ if (!user) {
47
43
  throw new Error('Not authenticated');
48
44
  }
49
- })
50
- .then(res => res?.json())
51
- .then(data => {
52
- if (data?.history) {
53
- setHistory(data.history);
45
+
46
+ setIsAuthenticated(true);
47
+ // Sync auth state
48
+ localStorage.setItem('isLoggedIn', 'true');
49
+ window.dispatchEvent(new Event('authUpdate'));
50
+
51
+ // Fetch history
52
+ const result = await apiClient.getHistory();
53
+ if (result?.history) {
54
+ setHistory(result.history);
54
55
  }
55
- })
56
- .catch(() => { })
57
- .finally(() => setLoading(false));
56
+ } catch (error) {
57
+ console.error('History load error:', error);
58
+ router.push('/login');
59
+ } finally {
60
+ setLoading(false);
61
+ }
62
+ };
63
+
64
+ loadData();
58
65
  }, [router]);
59
66
 
60
67
  const stats = useMemo(() => {
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
5
  import Image from 'next/image';
6
6
  import { Lock, User } from 'lucide-react';
7
+ import { apiClient } from '@/lib/api-client';
7
8
 
8
9
  export default function LoginPage() {
9
10
  const [username, setUsername] = useState('');
@@ -21,9 +22,9 @@ export default function LoginPage() {
21
22
  return;
22
23
  }
23
24
 
24
- fetch('/api/auth/me')
25
- .then(res => {
26
- if (res.ok) {
25
+ apiClient.getCurrentUser()
26
+ .then(user => {
27
+ if (user) {
27
28
  router.push('/history');
28
29
  }
29
30
  })
@@ -37,23 +38,17 @@ export default function LoginPage() {
37
38
  setLoading(true);
38
39
 
39
40
  try {
40
- const response = await fetch('/api/auth/login', {
41
- method: 'POST',
42
- headers: { 'Content-Type': 'application/json' },
43
- body: JSON.stringify({ username, password }),
44
- });
41
+ const data = await apiClient.login(username, password);
45
42
 
46
- const data = await response.json();
47
-
48
- if (response.ok) {
43
+ if (data.success) {
49
44
  localStorage.setItem('isLoggedIn', 'true');
50
45
  window.dispatchEvent(new Event('authUpdate'));
51
46
  router.push('/history');
52
47
  } else {
53
- setError(data.error || 'Login failed');
48
+ setError('Login failed');
54
49
  }
55
- } catch (err) {
56
- setError('Network error. Please try again.');
50
+ } catch (err: any) {
51
+ setError(err.message || 'Network error. Please try again.');
57
52
  } finally {
58
53
  setLoading(false);
59
54
  }
@@ -77,6 +72,9 @@ export default function LoginPage() {
77
72
  <p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
78
73
  Sign in to access your workflow history
79
74
  </p>
75
+ <p className="mt-1 text-xs text-gray-400 font-mono">
76
+ v1.7.15
77
+ </p>
80
78
  </div>
81
79
 
82
80
  <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
@@ -17,7 +17,7 @@ export default function Hero() {
17
17
  .catch(() => setDownloads(null));
18
18
 
19
19
  // Version is hardcoded for static export
20
- setVersion("1.7.11");
20
+ setVersion("1.7.15");
21
21
  }, []);
22
22
 
23
23
  const formatDownloads = (num: number) => {
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useEffect } from "react";
4
+ import { apiClient } from "@/lib/api-client";
4
5
 
5
6
  type HistoryItem = {
6
7
  id: number;
@@ -31,11 +32,10 @@ export default function History() {
31
32
 
32
33
  const loadData = async () => {
33
34
  try {
34
- // Fetch workflow history from API
35
- const response = await fetch('/api/workflow/history');
35
+ // Fetch workflow history from API via apiClient
36
+ const data = await apiClient.getHistory();
36
37
 
37
- if (response.ok) {
38
- const data = await response.json();
38
+ if (data?.history) {
39
39
  setHistory(data.history || []);
40
40
  setTotalPages(Math.ceil((data.history?.length || 0) / 10));
41
41
  setSummary({
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
4
4
  import Link from "next/link";
5
5
  import { Github, Menu, X, Moon, Sun, LogOut } from "lucide-react";
6
6
  import Image from "next/image";
7
+ import { apiClient } from "@/lib/api-client";
7
8
 
8
9
  export default function Navbar() {
9
10
  const [isOpen, setIsOpen] = useState(false);
@@ -29,18 +30,16 @@ export default function Navbar() {
29
30
  const mightBeLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
30
31
 
31
32
  if (mightBeLoggedIn) {
32
- fetch('/api/auth/me')
33
- .then(res => {
34
- if (res.ok) {
35
- return res.json();
36
- }
37
- throw new Error('Not authenticated');
38
- })
33
+ apiClient.getCurrentUser()
39
34
  .then(data => {
40
- setIsAuthenticated(true);
41
- setUsername(data.user?.username || 'User');
42
- // Ensure local storage is in sync
43
- localStorage.setItem('isLoggedIn', 'true');
35
+ if (data) {
36
+ setIsAuthenticated(true);
37
+ setUsername(data.username || 'User');
38
+ // Ensure local storage is in sync
39
+ localStorage.setItem('isLoggedIn', 'true');
40
+ } else {
41
+ throw new Error('Not authenticated');
42
+ }
44
43
  })
45
44
  .catch(() => {
46
45
  // Session invalid/expired
@@ -74,7 +73,7 @@ export default function Navbar() {
74
73
 
75
74
  const handleLogout = async () => {
76
75
  try {
77
- await fetch('/api/auth/logout', { method: 'POST' });
76
+ await apiClient.logout();
78
77
  localStorage.removeItem('isLoggedIn');
79
78
  setIsAuthenticated(false);
80
79
  setUsername(null);