#!/usr/bin/env node /** * Verifies this server's assumptions against the live instance and prints what * it finds. Run it after a Schulcloud release, or when a tool starts failing, * to tell "the token expired" apart from "the API moved". * * Read-only. Usage: `node --env-file=.env scripts/probe.mjs` */ import { loadConfig } from '../dist/config.js'; import { SchulcloudClient, SchulcloudApiError } from '../dist/schulcloud/client.js'; const config = loadConfig(); const client = new SchulcloudClient(config); console.log(`instance: ${config.baseUrl}\n`); // --- token --------------------------------------------------------------- const payload = decodeJwt(config.jwt); if (payload) { const expires = new Date(payload.exp * 1000); const daysLeft = (payload.exp * 1000 - Date.now()) / 86_400_000; console.log(`token: issued ${new Date(payload.iat * 1000).toISOString().slice(0, 10)}, ` + `expires ${expires.toISOString().slice(0, 10)} (${daysLeft.toFixed(1)} days left)`); if (daysLeft < 0) console.log(' *** EXPIRED — copy a fresh jwt cookie, see docs/AUTH.md'); else if (daysLeft < 5) console.log(' *** expiring soon — plan to copy a fresh jwt cookie'); } else { console.log('token: could not decode (not a JWT?)'); } // --- endpoints this server depends on ------------------------------------ const checks = [ ['GET /api/v3/me', () => client.me()], ['GET /api/v3/courses', () => client.listCourses({ limit: 1 })], ['GET /api/v3/dashboard', () => client.getDashboard()], ['GET /api/v3/tasks', () => client.listTasks({ limit: 1 })], ['GET /api/v3/tasks/finished', () => client.listFinishedTasks({ limit: 1 })], ['GET /api/v3/news', () => client.listNews({ limit: 1 })], ['GET /api/v3/docs-json', () => client.getJson('/api/v3/docs-json')], ['GET /api/v3/file/docs-json', () => client.getJson('/api/v3/file/docs-json')], ]; console.log('\ncore endpoints:'); let me; for (const [label, run] of checks) { try { const result = await run(); if (label.endsWith('/me')) me = result; console.log(` ok ${label}${summarize(result)}`); } catch (error) { const status = error instanceof SchulcloudApiError ? error.status : '—'; console.log(` FAIL ${label} → ${status} ${error.message.slice(0, 120)}`); } } // --- the chains that make the content tools work ------------------------- if (me) { console.log('\ncontent chain:'); const courses = await client.listAllCourses(); console.log(` ${courses.length} course(s) visible`); let boardId, lessonId; for (const course of courses) { const page = await client.getCourseBoard(course.id).catch(() => null); if (!page) continue; boardId ??= page.elements.find((e) => e.type === 'column-board')?.content.id; lessonId ??= page.elements.find((e) => e.type === 'lesson')?.content.id; if (boardId && lessonId) break; } if (boardId) { const skeleton = await client.getBoardSkeleton(boardId); const cardIds = skeleton.columns.flatMap((c) => c.cards.map((x) => x.cardId)); const cards = await client.getCards(cardIds.slice(0, 5)); console.log(` ok board → columns → cards (board ${boardId}: ${skeleton.columns.length} col, ${cardIds.length} cards)`); const fileElement = cards.flatMap((c) => c.elements).find((e) => e.type === 'file'); if (fileElement) { const files = await client.listFiles({ storageLocationId: me.school.id, parentType: 'boardnodes', parentId: fileElement.id, }); console.log(` ok file element → files-storage (${files.total} file(s) on element ${fileElement.id})`); } else { console.log(' — no file element among the sampled cards'); } } else { console.log(' — no column board found to test with'); } if (lessonId) { const lesson = await client.getLesson(lessonId); console.log(` ok lesson ${lessonId} ("${lesson.name}", ${lesson.contents?.length ?? 0} section(s))`); } } function summarize(result) { if (result && typeof result === 'object') { if ('total' in result) return ` (total ${result.total})`; if ('paths' in result) return ` (${Object.keys(result.paths).length} paths)`; if ('school' in result) return ` (${result.school.name})`; if ('gridElements' in result) return ` (${result.gridElements.length} tiles)`; } return ''; } function decodeJwt(token) { try { const part = token.split('.')[1]; return JSON.parse(Buffer.from(part, 'base64url').toString('utf8')); } catch { return null; } }