campus-cli
Advanced tools
+13
-0
@@ -7,2 +7,15 @@ # Changelog | ||
| ## [1.4.2] — 2026-08-08 | ||
| ### Security | ||
| - Los IDs de Blackboard (`courseId`, `contentId`, `parentId`, `columnId`, `attemptId`, `fileId`) se interpolaban sin validar en las URLs de la API REST. Un valor con `/` o `..` — por ejemplo, sugerido a un agente por contenido malicioso de un curso — podía redirigir la petición a un endpoint distinto dentro del mismo host. Ahora las 21 tools MCP que reciben alguno de estos campos exigen el formato real de Blackboard (`_529580_1`) antes de armar la petición. | ||
| ### Fixed | ||
| - 10 vulnerabilidades de dependencias resueltas, sin saltar a la última versión mayor de nada: | ||
| - `inquirer` 10 → 12 (no 14): el mínimo salto donde `@inquirer/editor` deja de arrastrar el paquete `tmp` vulnerable (escritura arbitraria de archivos vía symlink, path traversal por prefix/postfix sin sanear). | ||
| - Se eliminaron `google-auth-library`, `@modelcontextprotocol/node`, `@modelcontextprotocol/server` y `@modelcontextprotocol/client`: ninguno se importa en el código — dependencias muertas que arrastraban las vulnerabilidades de `gaxios`/`uuid` y una versión vieja de `@hono/node-server`. | ||
| - `tsx` refrescado dentro de su propio rango ya declarado, trayendo el `esbuild` con el fix de lectura arbitraria de archivos en Windows. | ||
| --- | ||
| ## [1.4.1] — 2026-08-08 | ||
@@ -9,0 +22,0 @@ |
@@ -16,2 +16,8 @@ "use strict"; | ||
| const MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50MB | ||
| // Blackboard's own IDs (course, content, column, attempt, file) always look like | ||
| // `_529580_1`. These get interpolated straight into REST path templates below — | ||
| // without this shape check, a crafted ID containing `/` or `..` could redirect | ||
| // the request to a different endpoint entirely (path injection within the same | ||
| // host), e.g. from a prompt-injected value the agent didn't scrutinize. | ||
| const blackboardId = (label) => zod_1.z.string().regex(/^_\d+_\d+$/, `${label} must look like a Blackboard ID, e.g. _529580_1`); | ||
| async function getClient() { | ||
@@ -75,3 +81,3 @@ const session = await (0, session_js_1.loadOrRefreshSession)(); | ||
| description: 'Get details of a specific course by its Blackboard ID (e.g. _529580_1)', | ||
| inputSchema: { courseId: zod_1.z.string().describe('Blackboard course ID like _529580_1') }, | ||
| inputSchema: { courseId: blackboardId('courseId').describe('Blackboard course ID like _529580_1') }, | ||
| }, async ({ courseId }) => { | ||
@@ -86,4 +92,4 @@ const { client } = await getClient(); | ||
| inputSchema: { | ||
| courseId: zod_1.z.string().describe('Blackboard course ID'), | ||
| parentId: zod_1.z.string().optional().describe('Parent folder content ID (omit for root level)'), | ||
| courseId: blackboardId('courseId').describe('Blackboard course ID'), | ||
| parentId: blackboardId('parentId').optional().describe('Parent folder content ID (omit for root level)'), | ||
| }, | ||
@@ -98,3 +104,3 @@ }, async ({ courseId, parentId }) => { | ||
| description: 'List recent announcements for a course', | ||
| inputSchema: { courseId: zod_1.z.string().describe('Blackboard course ID') }, | ||
| inputSchema: { courseId: blackboardId('courseId').describe('Blackboard course ID') }, | ||
| }, async ({ courseId }) => { | ||
@@ -115,3 +121,3 @@ const { client } = await getClient(); | ||
| inputSchema: { | ||
| courseId: zod_1.z.string().describe('Blackboard course ID'), | ||
| courseId: blackboardId('courseId').describe('Blackboard course ID'), | ||
| search: zod_1.z.string().optional().describe('Name of one person in the course'), | ||
@@ -165,3 +171,3 @@ }, | ||
| description: 'List assignments and tasks in a course with due dates, scores and submission status', | ||
| inputSchema: { courseId: zod_1.z.string().describe('Blackboard course ID') }, | ||
| inputSchema: { courseId: blackboardId('courseId').describe('Blackboard course ID') }, | ||
| }, async ({ courseId }) => { | ||
@@ -176,4 +182,4 @@ const { client } = await getClient(); | ||
| inputSchema: { | ||
| courseId: zod_1.z.string().describe('Blackboard course ID'), | ||
| columnId: zod_1.z.string().describe('Gradebook column ID (assignment ID)'), | ||
| courseId: blackboardId('courseId').describe('Blackboard course ID'), | ||
| columnId: blackboardId('columnId').describe('Gradebook column ID (assignment ID)'), | ||
| }, | ||
@@ -188,3 +194,3 @@ }, async ({ courseId, columnId }) => { | ||
| description: 'Get all grades for the current student in a course', | ||
| inputSchema: { courseId: zod_1.z.string().describe('Blackboard course ID') }, | ||
| inputSchema: { courseId: blackboardId('courseId').describe('Blackboard course ID') }, | ||
| }, async ({ courseId }) => { | ||
@@ -212,4 +218,4 @@ const { client, session } = await getClient(); | ||
| inputSchema: { | ||
| courseId: zod_1.z.string().describe('Blackboard course ID'), | ||
| contentId: zod_1.z.string().describe('Content item ID'), | ||
| courseId: blackboardId('courseId').describe('Blackboard course ID'), | ||
| contentId: blackboardId('contentId').describe('Content item ID'), | ||
| attachmentId: zod_1.z.string().describe('Attachment ID from blackboard_list_attachments, or a full bbcswebdav URL for embedded files'), | ||
@@ -248,4 +254,4 @@ filename: zod_1.z.string().optional().describe('Filename to save as (e.g. displayName from blackboard_list_attachments). Falls back to Content-Disposition header.'), | ||
| inputSchema: { | ||
| courseId: zod_1.z.string().describe('Blackboard course ID'), | ||
| contentId: zod_1.z.string().describe('Content item ID'), | ||
| courseId: blackboardId('courseId').describe('Blackboard course ID'), | ||
| contentId: blackboardId('contentId').describe('Content item ID'), | ||
| }, | ||
@@ -362,4 +368,4 @@ }, async ({ courseId, contentId }) => { | ||
| inputSchema: { | ||
| courseId: zod_1.z.string().describe('Blackboard course ID'), | ||
| columnId: zod_1.z.string().describe('Assignment (gradebook column) ID'), | ||
| courseId: blackboardId('courseId').describe('Blackboard course ID'), | ||
| columnId: blackboardId('columnId').describe('Assignment (gradebook column) ID'), | ||
| studentComments: zod_1.z.string().optional().describe('Comment to the instructor'), | ||
@@ -387,4 +393,4 @@ studentSubmission: zod_1.z.string().optional().describe('Text body of the submission'), | ||
| inputSchema: { | ||
| courseId: zod_1.z.string().describe('Blackboard course ID'), | ||
| columnId: zod_1.z.string().describe('Assignment (gradebook column) ID'), | ||
| courseId: blackboardId('courseId').describe('Blackboard course ID'), | ||
| columnId: blackboardId('columnId').describe('Assignment (gradebook column) ID'), | ||
| studentComments: zod_1.z.string().optional().describe('Comment to the instructor'), | ||
@@ -409,3 +415,3 @@ studentSubmission: zod_1.z.string().optional().describe('Text body of the submission'), | ||
| 'For each graded submission, shows score, instructor comments, and any feedback files attached by the professor.', | ||
| inputSchema: { courseId: zod_1.z.string().describe('Blackboard course ID') }, | ||
| inputSchema: { courseId: blackboardId('courseId').describe('Blackboard course ID') }, | ||
| }, async ({ courseId }) => { | ||
@@ -464,6 +470,6 @@ const { client, session } = await getClient(); | ||
| inputSchema: { | ||
| courseId: zod_1.z.string().describe('Blackboard course ID'), | ||
| columnId: zod_1.z.string().describe('Gradebook column (assignment) ID'), | ||
| attemptId: zod_1.z.string().describe('Attempt ID from blackboard_get_assignment_feedback'), | ||
| fileId: zod_1.z.string().describe('File ID from blackboard_get_assignment_feedback → attempt.feedbackFiles'), | ||
| courseId: blackboardId('courseId').describe('Blackboard course ID'), | ||
| columnId: blackboardId('columnId').describe('Gradebook column (assignment) ID'), | ||
| attemptId: blackboardId('attemptId').describe('Attempt ID from blackboard_get_assignment_feedback'), | ||
| fileId: blackboardId('fileId').describe('File ID from blackboard_get_assignment_feedback → attempt.feedbackFiles'), | ||
| filename: zod_1.z.string().optional().describe('Filename to save as (defaults to the name from feedbackFiles)'), | ||
@@ -470,0 +476,0 @@ outputDir: zod_1.z.string().optional().describe('Directory to save the file (default: current working directory)'), |
+3
-9
| { | ||
| "name": "campus-cli", | ||
| "version": "1.4.1", | ||
| "version": "1.4.2", | ||
| "description": "Conecta Blackboard UPC con ChatGPT y Claude vía MCP, o úsalo desde la terminal — CLI/MCP no oficial para el campus universitario (Blackboard, Canvas, Moodle...)", | ||
@@ -65,5 +65,3 @@ "mcpName": "io.github.alejooroncoy/campus-cli", | ||
| "@browserbasehq/sdk": "^2.16.0", | ||
| "@modelcontextprotocol/node": "^2.0.0", | ||
| "@modelcontextprotocol/sdk": "^1.28.0", | ||
| "@modelcontextprotocol/server": "^2.0.0", | ||
| "@types/inquirer": "^9.0.9", | ||
@@ -75,14 +73,10 @@ "@types/node": "^22.0.0", | ||
| "form-data": "^4.0.5", | ||
| "google-auth-library": "^9.15.1", | ||
| "inquirer": "^10.1.0", | ||
| "inquirer": "^12.0.0", | ||
| "ora": "^8.1.0", | ||
| "playwright": "^1.47.0", | ||
| "playwright-core": "^1.62.0", | ||
| "tsx": "^4.19.0", | ||
| "tsx": "^4.23.11", | ||
| "typescript": "^5.5.0", | ||
| "zod": "^4.3.6" | ||
| }, | ||
| "devDependencies": { | ||
| "@modelcontextprotocol/client": "^2.0.0" | ||
| } | ||
| } |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
234594
0.76%15
-16.67%0
-100%4113
0.15%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated