From 4843734c1af1c6116d228276dd96f66437fd0bf3 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:38:50 -0400 Subject: [PATCH 01/33] fix: add single-function Classroom dispatcher --- api/_classroomRouter.js | 100 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 api/_classroomRouter.js diff --git a/api/_classroomRouter.js b/api/_classroomRouter.js new file mode 100644 index 0000000..1a724fb --- /dev/null +++ b/api/_classroomRouter.js @@ -0,0 +1,100 @@ +/** + * @module api/classroomRouter + * @description + * Server-only dispatcher that lets Vercel expose the complete Classroom API + * through one Serverless Function while preserving the existing public URLs + * via vercel.json rewrites. Routing is not authorization: every delegated + * handler retains its existing capability, method, and payload checks. + */ + +import { + classAdmitHandler, + classCoachingHandler, + classHandler, + classJoinHandler, + classObserveHandler, + classParticipantsHandler, + classStudentAccessHandler, + classStudentCoachingHandler, + classStudentHandler, + classWorkspacesHandler +} from './_classroom.js'; +import { + instructorCaseStudiesHandler, + studentCaseStudiesHandler +} from './_protectedCaseStudies.js'; +import { + classExerciseCheckpointHandler, + classExerciseHandler, + classStudentExerciseHandler, + classStudentExerciseReadyHandler +} from './_classroomExercise.js'; + +export const CLASSROOM_ROUTE_PARAM = '__classroomRoute'; + +export const CLASSROOM_ROUTE_IDS = Object.freeze([ + 'root', + 'admit', + 'join', + 'participants', + 'student', + 'student-access', + 'workspaces', + 'observe', + 'coaching', + 'coaching-student', + 'case-studies', + 'case-studies-student', + 'exercise', + 'exercise-checkpoint', + 'exercise-student', + 'exercise-student-ready' +]); + +function defaultHandlers() { + return { + root: classHandler(), + admit: classAdmitHandler(), + join: classJoinHandler(), + participants: classParticipantsHandler(), + student: classStudentHandler(), + 'student-access': classStudentAccessHandler(), + workspaces: classWorkspacesHandler(), + observe: classObserveHandler(), + coaching: classCoachingHandler(), + 'coaching-student': classStudentCoachingHandler(), + 'case-studies': instructorCaseStudiesHandler(), + 'case-studies-student': studentCaseStudiesHandler(), + exercise: classExerciseHandler(), + 'exercise-checkpoint': classExerciseCheckpointHandler(), + 'exercise-student': classStudentExerciseHandler(), + 'exercise-student-ready': classStudentExerciseReadyHandler() + }; +} + +function requestedRoute(req) { + const value = req?.query?.[CLASSROOM_ROUTE_PARAM]; + return typeof value === 'string' ? value.trim() : ''; +} + +function notFound(res) { + res.setHeader?.('Cache-Control', 'no-store'); + res.setHeader?.('Referrer-Policy', 'no-referrer'); + return res.status(404).json({ error: 'Classroom route not found.' }); +} + +/** + * Create the single-function Classroom route dispatcher. + * + * @param {object} [options] Dependency overrides. + * @param {Record} [options.handlers] Route handler map for deterministic tests. + * @returns {Function} Vercel request handler. + */ +export function createClassroomRouteDispatcher({ handlers = defaultHandlers() } = {}) { + return async (req, res) => { + const route = requestedRoute(req); + const handler = Object.hasOwn(handlers, route) ? handlers[route] : null; + if (typeof handler !== 'function') return notFound(res); + return handler(req, res); + }; +} From ccc83e12d9be979a0034fe5b904d30ce9bfdf3f4 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:38:53 -0400 Subject: [PATCH 02/33] fix: add consolidated Classroom Vercel entrypoint --- api/classroom.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 api/classroom.js diff --git a/api/classroom.js b/api/classroom.js new file mode 100644 index 0000000..71e5b6c --- /dev/null +++ b/api/classroom.js @@ -0,0 +1,7 @@ +/** + * @module api/classroom + * @description Single Vercel Serverless Function entrypoint for all Classroom routes. + */ +import { createClassroomRouteDispatcher } from './_classroomRouter.js'; + +export default createClassroomRouteDispatcher(); From 295dc2135beda49aef1948faca0a6896ea554f23 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:38:57 -0400 Subject: [PATCH 03/33] test: cover consolidated Classroom dispatcher --- tests/classroomRouter.unit.test.mjs | 82 +++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/classroomRouter.unit.test.mjs diff --git a/tests/classroomRouter.unit.test.mjs b/tests/classroomRouter.unit.test.mjs new file mode 100644 index 0000000..65fe2a8 --- /dev/null +++ b/tests/classroomRouter.unit.test.mjs @@ -0,0 +1,82 @@ +/** + * Classroom single-function dispatcher coverage. + */ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { + CLASSROOM_ROUTE_IDS, + CLASSROOM_ROUTE_PARAM, + createClassroomRouteDispatcher +} from '../api/_classroomRouter.js'; + +function response() { + return { + statusCode: 200, + headers: {}, + body: null, + setHeader(name, value) { + this.headers[name] = value; + }, + status(code) { + this.statusCode = code; + return this; + }, + json(body) { + this.body = body; + return this; + } + }; +} + +test('Classroom dispatcher delegates every public route marker without changing the request', async () => { + const calls = []; + const handlers = Object.fromEntries(CLASSROOM_ROUTE_IDS.map(route => [ + route, + async (req, res) => { + calls.push({ route, req }); + return res.status(200).json({ route }); + } + ])); + const dispatch = createClassroomRouteDispatcher({ handlers }); + + for (const route of CLASSROOM_ROUTE_IDS) { + const req = { + method: 'GET', + query: { + [CLASSROOM_ROUTE_PARAM]: route, + workspaceId: 'workspace-1' + }, + body: { keep: true } + }; + const res = response(); + await dispatch(req, res); + assert.equal(res.statusCode, 200); + assert.deepEqual(res.body, { route }); + assert.equal(calls.at(-1).req, req); + assert.equal(calls.at(-1).req.query.workspaceId, 'workspace-1'); + assert.deepEqual(calls.at(-1).req.body, { keep: true }); + } + + assert.deepEqual(calls.map(item => item.route), CLASSROOM_ROUTE_IDS); +}); + +test('Classroom dispatcher rejects unknown, missing, or duplicated route markers', async () => { + const dispatch = createClassroomRouteDispatcher({ + handlers: { + root: async (_req, res) => res.status(200).json({ ok: true }) + } + }); + + for (const value of [undefined, 'unknown', ['root', 'exercise']]) { + const res = response(); + await dispatch({ + method: 'GET', + query: value === undefined ? {} : { [CLASSROOM_ROUTE_PARAM]: value } + }, res); + assert.equal(res.statusCode, 404); + assert.deepEqual(res.body, { error: 'Classroom route not found.' }); + assert.equal(res.headers['Cache-Control'], 'no-store'); + assert.equal(res.headers['Referrer-Policy'], 'no-referrer'); + } +}); From b5512eadcd0eeb4e731d2fc66267f103ace55269 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:39:32 -0400 Subject: [PATCH 04/33] fix: route Classroom APIs through one Vercel function --- vercel.json | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index 5d8f4e4..de32232 100644 --- a/vercel.json +++ b/vercel.json @@ -9,5 +9,71 @@ "verify/**": true } }, - "ignoreCommand": "case \"$VERCEL_GIT_COMMIT_REF\" in main|verify/*) exit 1 ;; *) exit 0 ;; esac" + "ignoreCommand": "case \"$VERCEL_GIT_COMMIT_REF\" in main|verify/*) exit 1 ;; *) exit 0 ;; esac", + "rewrites": [ + { + "source": "/api/classes", + "destination": "/api/classroom?__classroomRoute=root" + }, + { + "source": "/api/classes/admit", + "destination": "/api/classroom?__classroomRoute=admit" + }, + { + "source": "/api/classes/join", + "destination": "/api/classroom?__classroomRoute=join" + }, + { + "source": "/api/classes/participants", + "destination": "/api/classroom?__classroomRoute=participants" + }, + { + "source": "/api/classes/student", + "destination": "/api/classroom?__classroomRoute=student" + }, + { + "source": "/api/classes/student/access", + "destination": "/api/classroom?__classroomRoute=student-access" + }, + { + "source": "/api/classes/workspaces", + "destination": "/api/classroom?__classroomRoute=workspaces" + }, + { + "source": "/api/classes/observe", + "destination": "/api/classroom?__classroomRoute=observe" + }, + { + "source": "/api/classes/coaching", + "destination": "/api/classroom?__classroomRoute=coaching" + }, + { + "source": "/api/classes/coaching/student", + "destination": "/api/classroom?__classroomRoute=coaching-student" + }, + { + "source": "/api/classes/case-studies", + "destination": "/api/classroom?__classroomRoute=case-studies" + }, + { + "source": "/api/classes/case-studies/student", + "destination": "/api/classroom?__classroomRoute=case-studies-student" + }, + { + "source": "/api/classes/exercise", + "destination": "/api/classroom?__classroomRoute=exercise" + }, + { + "source": "/api/classes/exercise/checkpoint", + "destination": "/api/classroom?__classroomRoute=exercise-checkpoint" + }, + { + "source": "/api/classes/exercise/student", + "destination": "/api/classroom?__classroomRoute=exercise-student" + }, + { + "source": "/api/classes/exercise/student/ready", + "destination": "/api/classroom?__classroomRoute=exercise-student-ready" + } + ] } From 622ed526819c35fc2258e274bc9ca4c50bb67150 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:39:41 -0400 Subject: [PATCH 05/33] fix: remove redundant Classroom route wrapper --- api/classes/index.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/index.js diff --git a/api/classes/index.js b/api/classes/index.js deleted file mode 100644 index 973e159..0000000 --- a/api/classes/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/index - * @description Vercel endpoint for class creation, instructor metadata, credential rotation, join enablement, and revocation. - */ -import { classHandler } from '../_classroom.js'; - -export default classHandler(); From d4dda3cd556b80bee504dc01d7027a8a1ec1233e Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:39:45 -0400 Subject: [PATCH 06/33] fix: remove redundant Classroom route wrapper --- api/classes/admit.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/admit.js diff --git a/api/classes/admit.js b/api/classes/admit.js deleted file mode 100644 index 478c4d4..0000000 --- a/api/classes/admit.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/admit - * @description One-code Student admission into a live class waiting/assigned session. - */ -import { classAdmitHandler } from '../_classroom.js'; - -export default classAdmitHandler(); From 04c8ebfd75e2bfe723f8b72f7c4c48e1afbcd9f1 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:39:49 -0400 Subject: [PATCH 07/33] fix: remove redundant Classroom route wrapper --- api/classes/join.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/join.js diff --git a/api/classes/join.js b/api/classes/join.js deleted file mode 100644 index 1485305..0000000 --- a/api/classes/join.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/join - * @description Student admission endpoint that resolves one authorized assignment without exposing a workspace list. - */ -import { classJoinHandler } from '../_classroom.js'; - -export default classJoinHandler(); From cf4ad8da74c3de021a6833958d3c2c14b4ad6e3d Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:39:53 -0400 Subject: [PATCH 08/33] fix: remove redundant Classroom route wrapper --- api/classes/participants.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/participants.js diff --git a/api/classes/participants.js b/api/classes/participants.js deleted file mode 100644 index 1dbb459..0000000 --- a/api/classes/participants.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/participants - * @description Instructor-only live Classroom participant roster. - */ -import { classParticipantsHandler } from '../_classroom.js'; - -export default classParticipantsHandler(); From b699e38cafa8b754e45176bc9694e9b62f6e2ec5 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:39:58 -0400 Subject: [PATCH 09/33] fix: remove redundant Classroom route wrapper --- api/classes/student.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/student.js diff --git a/api/classes/student.js b/api/classes/student.js deleted file mode 100644 index 826e591..0000000 --- a/api/classes/student.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/student - * @description Student class-session endpoint for own assignment status only. - */ -import { classStudentHandler } from '../_classroom.js'; - -export default classStudentHandler(); From 4e40925343369598d39b965581862354997183fd Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:40:02 -0400 Subject: [PATCH 10/33] fix: remove redundant Classroom route wrapper --- api/classes/student/access.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/student/access.js diff --git a/api/classes/student/access.js b/api/classes/student/access.js deleted file mode 100644 index aab6b6e..0000000 --- a/api/classes/student/access.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/student/access - * @description Exchanges one Student class-session capability for current assignment-specific workspace access. - */ -import { classStudentAccessHandler } from '../../_classroom.js'; - -export default classStudentAccessHandler(); From 55c5b88454c78c4c5311d2f4490266e7b459dffa Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:40:06 -0400 Subject: [PATCH 11/33] fix: remove redundant Classroom route wrapper --- api/classes/workspaces.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/workspaces.js diff --git a/api/classes/workspaces.js b/api/classes/workspaces.js deleted file mode 100644 index 196e8f9..0000000 --- a/api/classes/workspaces.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/workspaces - * @description Instructor-only Vercel endpoint for classroom workspace creation, listing, and assignment-capability lifecycle. - */ -import { classWorkspacesHandler } from '../_classroom.js'; - -export default classWorkspacesHandler(); From 32a9efebde838158d8e91b41347b721bc6435b22 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:40:11 -0400 Subject: [PATCH 12/33] fix: remove redundant Classroom route wrapper --- api/classes/observe.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/observe.js diff --git a/api/classes/observe.js b/api/classes/observe.js deleted file mode 100644 index 23278d8..0000000 --- a/api/classes/observe.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/observe - * @description Instructor-only, GET-only live observation of one authorized classroom workspace. - */ -import { classObserveHandler } from '../_classroom.js'; - -export default classObserveHandler(); From c3cbe5621e57c2b52154a116cbfcad5427587d2c Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:40:15 -0400 Subject: [PATCH 13/33] fix: remove redundant Classroom route wrapper --- api/classes/coaching.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/coaching.js diff --git a/api/classes/coaching.js b/api/classes/coaching.js deleted file mode 100644 index db4e56c..0000000 --- a/api/classes/coaching.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/coaching - * @description Instructor class-scoped coaching feedback CRUD. - */ -import { classCoachingHandler } from '../_classroom.js'; - -export default classCoachingHandler(); From 2174cabc32d29df0909ccdeb3c1a6dc46e533981 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:40:20 -0400 Subject: [PATCH 14/33] fix: remove redundant Classroom route wrapper --- api/classes/coaching/student.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/coaching/student.js diff --git a/api/classes/coaching/student.js b/api/classes/coaching/student.js deleted file mode 100644 index f2f613c..0000000 --- a/api/classes/coaching/student.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/coaching/student - * @description Student GET-only coaching feedback for the membership behind its workspace capability. - */ -import { classStudentCoachingHandler } from '../../_classroom.js'; - -export default classStudentCoachingHandler(); From cb3393b873ceeed2ad34ef513529aaae0ba6ef34 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:40:37 -0400 Subject: [PATCH 15/33] fix: remove redundant Classroom route wrapper --- api/classes/case-studies.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/case-studies.js diff --git a/api/classes/case-studies.js b/api/classes/case-studies.js deleted file mode 100644 index d753a76..0000000 --- a/api/classes/case-studies.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/case-studies - * @description Instructor-authorized protected Case Study catalog and payload delivery. - */ -import { instructorCaseStudiesHandler } from '../_protectedCaseStudies.js'; - -export default instructorCaseStudiesHandler(); From 48417f5d8310fdcd266da0e5033471ec5be337e9 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:40:41 -0400 Subject: [PATCH 16/33] fix: remove redundant Classroom route wrapper --- api/classes/case-studies/student.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/case-studies/student.js diff --git a/api/classes/case-studies/student.js b/api/classes/case-studies/student.js deleted file mode 100644 index 3f4b1e4..0000000 --- a/api/classes/case-studies/student.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/case-studies/student - * @description Student-authorized protected Case Study catalog and payload delivery. - */ -import { studentCaseStudiesHandler } from '../../_protectedCaseStudies.js'; - -export default studentCaseStudiesHandler(); From 44491754a90225ee1c66b28b79cd6d7ad4dbc1be Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:40:45 -0400 Subject: [PATCH 17/33] fix: remove redundant Classroom route wrapper --- api/classes/exercise.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/exercise.js diff --git a/api/classes/exercise.js b/api/classes/exercise.js deleted file mode 100644 index c8d0e1b..0000000 --- a/api/classes/exercise.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/exercise - * @description Instructor-authorized staged Classroom exercise lifecycle. - */ -import { classExerciseHandler } from '../_classroomExercise.js'; - -export default classExerciseHandler(); From ec1281ce662cab1d0931bb441fa52a91a560794c Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:40:54 -0400 Subject: [PATCH 18/33] fix: remove redundant Classroom route wrapper --- api/classes/exercise/checkpoint.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/exercise/checkpoint.js diff --git a/api/classes/exercise/checkpoint.js b/api/classes/exercise/checkpoint.js deleted file mode 100644 index 88050a1..0000000 --- a/api/classes/exercise/checkpoint.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/exercise/checkpoint - * @description Instructor-authorized immutable current-stage checkpoint inspection. - */ -import { classExerciseCheckpointHandler } from '../../_classroomExercise.js'; - -export default classExerciseCheckpointHandler(); From 624defd955201ab9f39de3c5bd5056de3393a388 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:40:58 -0400 Subject: [PATCH 19/33] fix: remove redundant Classroom route wrapper --- api/classes/exercise/student.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/exercise/student.js diff --git a/api/classes/exercise/student.js b/api/classes/exercise/student.js deleted file mode 100644 index d62e75f..0000000 --- a/api/classes/exercise/student.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/exercise/student - * @description Student class-session current staged release endpoint. - */ -import { classStudentExerciseHandler } from '../../_classroomExercise.js'; - -export default classStudentExerciseHandler(); From f8a85124765acbd9f1a24555c0388fd76b22e8a5 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:41:02 -0400 Subject: [PATCH 20/33] fix: remove redundant Classroom route wrapper --- api/classes/exercise/student/ready.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 api/classes/exercise/student/ready.js diff --git a/api/classes/exercise/student/ready.js b/api/classes/exercise/student/ready.js deleted file mode 100644 index 4b772a1..0000000 --- a/api/classes/exercise/student/ready.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @module api/classes/exercise/student/ready - * @description Student class-session current-workspace Ready endpoint. - */ -import { classStudentExerciseReadyHandler } from '../../../_classroomExercise.js'; - -export default classStudentExerciseReadyHandler(); From 64b2be3ef980c26ad00b5212668e43d3f81497f1 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:41:39 -0400 Subject: [PATCH 21/33] build: guard Vercel function budget --- scripts/verify-vercel-function-budget.mjs | 80 +++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 scripts/verify-vercel-function-budget.mjs diff --git a/scripts/verify-vercel-function-budget.mjs b/scripts/verify-vercel-function-budget.mjs new file mode 100644 index 0000000..74072e9 --- /dev/null +++ b/scripts/verify-vercel-function-budget.mjs @@ -0,0 +1,80 @@ +/** + * @fileoverview Guard the Vercel Hobby Serverless Function budget. + * + * Vercel treats JavaScript files under api/ as potential function entrypoints. + * Server-only helper modules in this repository are prefixed with "_" and are + * bundled into the public entrypoints instead. Count every other api/*.js file + * conservatively so deployment cannot exceed the Hobby plan limit unnoticed. + */ + +import { readdir } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +export const VERCEL_HOBBY_FUNCTION_LIMIT = 12; + +async function walk(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...await walk(fullPath)); + } else if (entry.isFile()) { + files.push(fullPath); + } + } + return files; +} + +/** + * List conservative Vercel function candidates below an API directory. + * + * @param {string} apiDirectory Absolute or relative api/ path. + * @returns {Promise} Sorted repo-relative candidate paths. + */ +export async function listVercelFunctionCandidates(apiDirectory = 'api') { + const root = path.resolve(apiDirectory); + const files = await walk(root); + return files + .filter(file => file.endsWith('.js')) + .filter(file => !path.basename(file).startsWith('_')) + .map(file => path.relative(process.cwd(), file).split(path.sep).join('/')) + .sort(); +} + +/** + * Verify the conservative Vercel function count stays within budget. + * + * @param {object} [options] Verification options. + * @param {string} [options.apiDirectory] API directory. + * @param {number} [options.limit] Maximum allowed functions. + * @returns {Promise} Candidate paths when valid. + */ +export async function verifyVercelFunctionBudget({ + apiDirectory = 'api', + limit = VERCEL_HOBBY_FUNCTION_LIMIT +} = {}) { + const candidates = await listVercelFunctionCandidates(apiDirectory); + if (candidates.length > limit) { + throw new Error( + `Vercel function budget exceeded: ${candidates.length}/${limit}.\n` + + candidates.map(item => `- ${item}`).join('\n') + ); + } + return candidates; +} + +const invokedPath = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : ''; +if (import.meta.url === invokedPath) { + try { + const candidates = await verifyVercelFunctionBudget(); + console.log( + `[verify:vercel-functions] ${candidates.length}/${VERCEL_HOBBY_FUNCTION_LIMIT} conservative function candidates.\n` + + candidates.map(item => `- ${item}`).join('\n') + ); + } catch (error) { + console.error(`[verify:vercel-functions] ${error.message}`); + process.exitCode = 1; + } +} From 51b1b16d379c34d162d364104c9d08f42a57d098 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:41:42 -0400 Subject: [PATCH 22/33] test: lock Classroom rewrites and function budget --- tests/vercelFunctionBudget.unit.test.mjs | 80 ++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/vercelFunctionBudget.unit.test.mjs diff --git a/tests/vercelFunctionBudget.unit.test.mjs b/tests/vercelFunctionBudget.unit.test.mjs new file mode 100644 index 0000000..8250a04 --- /dev/null +++ b/tests/vercelFunctionBudget.unit.test.mjs @@ -0,0 +1,80 @@ +/** + * Vercel Classroom routing and function-budget regression coverage. + */ +import assert from 'node:assert/strict'; +import { readFile, readdir } from 'node:fs/promises'; +import { test } from 'node:test'; + +import { + VERCEL_HOBBY_FUNCTION_LIMIT, + listVercelFunctionCandidates +} from '../scripts/verify-vercel-function-budget.mjs'; + +const EXPECTED_CLASSROOM_REWRITES = Object.freeze({ + '/api/classes': 'root', + '/api/classes/admit': 'admit', + '/api/classes/join': 'join', + '/api/classes/participants': 'participants', + '/api/classes/student': 'student', + '/api/classes/student/access': 'student-access', + '/api/classes/workspaces': 'workspaces', + '/api/classes/observe': 'observe', + '/api/classes/coaching': 'coaching', + '/api/classes/coaching/student': 'coaching-student', + '/api/classes/case-studies': 'case-studies', + '/api/classes/case-studies/student': 'case-studies-student', + '/api/classes/exercise': 'exercise', + '/api/classes/exercise/checkpoint': 'exercise-checkpoint', + '/api/classes/exercise/student': 'exercise-student', + '/api/classes/exercise/student/ready': 'exercise-student-ready' +}); + +test('Vercel preserves every Classroom public URL through the consolidated function', async () => { + const vercel = JSON.parse(await readFile('vercel.json', 'utf8')); + const actual = Object.fromEntries( + vercel.rewrites + .filter(item => item.source.startsWith('/api/classes')) + .map(item => { + const destination = new URL(item.destination, 'https://intake.test'); + return [item.source, destination.searchParams.get('__classroomRoute')]; + }) + ); + + assert.deepEqual(actual, EXPECTED_CLASSROOM_REWRITES); + for (const rewrite of vercel.rewrites.filter(item => item.source.startsWith('/api/classes'))) { + assert.equal(rewrite.destination.startsWith('/api/classroom?'), true); + } +}); + +test('legacy Classroom wrapper directory has no deployable JavaScript entrypoints', async () => { + async function jsFiles(directory) { + let entries; + try { + entries = await readdir(directory, { withFileTypes: true }); + } catch (error) { + if (error?.code === 'ENOENT') return []; + throw error; + } + const found = []; + for (const entry of entries) { + const fullPath = `${directory}/${entry.name}`; + if (entry.isDirectory()) found.push(...await jsFiles(fullPath)); + else if (entry.isFile() && entry.name.endsWith('.js')) found.push(fullPath); + } + return found; + } + + assert.deepEqual(await jsFiles('api/classes'), []); +}); + +test('repository stays below the conservative Vercel Hobby function budget', async () => { + const candidates = await listVercelFunctionCandidates('api'); + assert.equal(candidates.length <= VERCEL_HOBBY_FUNCTION_LIMIT, true); + assert.deepEqual(candidates, [ + 'api/classroom.js', + 'api/protected-case-studies.manifest.js', + 'api/workspaces/index.js', + 'api/workspaces/presence.js', + 'api/workspaces/session.js' + ]); +}); From 7546f926085eafc667599d084a6e8d293e87965f Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:41:56 -0400 Subject: [PATCH 23/33] build: enforce Vercel function budget in quality --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 74ba5cd..a4ab5ef 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,13 @@ "verify:lockfile": "node scripts/verify-lockfile.js", "check:templates": "npm run build:templates && git diff --exit-code -- src/templates.manifest.js api/protected-case-studies.manifest.js", "repo:doctor": "node scripts/repo-doctor.js", - "quality": "npm run verify:lockfile && npm run repo:doctor && npm run verify:tests && npm run verify:summary && npm run verify:persistence && npm run lint && npm run check:templates && npm run verify:protected-cases && npm run build:vercel-public && npm run check:storage-docs && npm test", + "quality": "npm run verify:lockfile && npm run repo:doctor && npm run verify:vercel-functions && npm run verify:tests && npm run verify:summary && npm run verify:persistence && npm run lint && npm run check:templates && npm run verify:protected-cases && npm run build:vercel-public && npm run check:storage-docs && npm test", "verify:protected-cases": "node scripts/verify-protected-case-boundary.mjs", "build:vercel-public": "node scripts/build-vercel-public.mjs", "serve:browser-tests": "node scripts/serve-browser-tests.mjs", "test:browser": "playwright test", - "test:browser:headed": "playwright test --headed" + "test:browser:headed": "playwright test --headed", + "verify:vercel-functions": "node scripts/verify-vercel-function-budget.mjs" }, "dependencies": { "@neondatabase/serverless": "1.1.0" From c0eb72403695d76fbc78b8493b710beec63a6401 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:41:59 -0400 Subject: [PATCH 24/33] build: verify function budget before Vercel deploy --- vercel.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index de32232..1cffece 100644 --- a/vercel.json +++ b/vercel.json @@ -1,5 +1,5 @@ { - "buildCommand": "npm run verify:protected-cases && npm run build:vercel-public", + "buildCommand": "npm run verify:vercel-functions && npm run verify:protected-cases && npm run build:vercel-public", "outputDirectory": "dist", "cleanUrls": true, "git": { From 34294de824a473ad5b4da43919249dee64c2282a Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:43:07 -0400 Subject: [PATCH 25/33] docs: document consolidated Classroom deployment --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 11a204c..95de313 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ AI contributors should run the following commands (or manual preview) whenever t | `npm run verify:persistence` | When adding or editing inputs/captions that should survive reloads. | Confirms new controls tie into `src/appState.js` and `src/storage.js`, prompting template/state updates so saves/loads remain lossless. | | `npm test` | Before committing or when adding new suites. | Runs the full test matrix (DOM + unit) so CI sees the same state you validated locally. | | `npm run verify:protected-cases` | After any template/classroom/deployment boundary change. | Proves protected Case Study IDs/names are absent from public browser runtime assets and that authored `templates/*.json` remains excluded from Vercel deployment. | +| `npm run verify:vercel-functions` | After adding or moving anything under `api/`, or changing Vercel routing. | Conservatively counts deployable `api/*.js` entrypoints and fails above the Hobby-plan budget of 12. Classroom public URLs are intentionally multiplexed through one function. | | `npm run quality` | Before marking any pull request ready. | Canonical repository gate: lockfile, repo doctor, domain guards, lint, generated-file freshness, protected-case boundary, storage docs, and the full test suite. See [`docs/REPOSITORY-OPERATIONS.md`](docs/REPOSITORY-OPERATIONS.md). | | `npm run update:storage-docs` / `npm run check:storage-docs` | Run `update` whenever you alter persisted schema, then `check` before pushing. | Keeps [`docs/storage-schema.md`](docs/storage-schema.md) and [`docs/storage-schema.appendix.md`](docs/storage-schema.appendix.md) synced with new keys or shapes. | @@ -127,6 +128,8 @@ The database stores only SHA-256 capability hashes. `CLASS_EXPIRY_DAYS` optional See `docs/classroom-api.md` for the endpoint/capability matrix and `SECURITY.md` for the security boundary. Instructor observation is implemented through the separate GET-only `/api/classes/observe` path, authorized by the Instructor class capability after class/workspace ownership is proven. It never resolves through the editable workspace-alias path. +The public `/api/classes/**` URL contract is preserved while Vercel packaging is consolidated: `vercel.json` rewrites those paths into the single `api/classroom.js` Serverless Function, which delegates through `api/_classroomRouter.js` to the same class/coaching/protected-case/exercise handlers. The rewrite marker is routing only and grants no authority. Do not restore one Vercel function file per Classroom endpoint; `npm run verify:vercel-functions` protects the deployment budget. + #313 staged simulation is implemented end-to-end on the active classroom branch. Protected Case Studies may carry an explicit server-only `simulation` definition; additive Neon tables persist class exercise lifecycle, optional releases, workspace readiness, and immutable debrief checkpoints. Running exercises pin the staged-definition version plus a definition fingerprint and inherit the owning class expiry. Instructor APIs/UI support staged selection, Start/Pause/Resume, controlled evidence release, debrief, explicit edit freeze/unfreeze, checkpoint inspection, Advance, and Complete. Student staged reads use the stable class-session capability, expose only current/cumulative Student-safe material, support Ready/Resume Working, and project frozen debrief Intake read-only while the server enforces writes with HTTP 423. These tables and clients do **not** duplicate live Intake state: collaboration workspaces remain the only live snapshot/revision source of truth, checkpoint snapshots are historical read-only evidence, and Classroom exercise/session state stays outside Intake persistence/export/summary. Production official Case Study stage content remains intentionally unauthored until authoritative material is supplied. ### Conflict recovery and two-window testing @@ -188,12 +191,13 @@ Need to know which module owns a given storage field? Jump to the [Storage-to-Mo - `npm run dev` and local `npm run build` still regenerate both manifests for maintainers. Commit the authored JSON and both generated manifests together. ### Vercel deployment -- Production Vercel builds execute `npm run verify:protected-cases && npm run build:vercel-public` (see `vercel.json`). They **do not regenerate manifests**, because authored `templates/*.json` source files are deliberately excluded from the deployment upload. +- Production Vercel builds execute `npm run verify:vercel-functions && npm run verify:protected-cases && npm run build:vercel-public` (see `vercel.json`). They **do not regenerate manifests**, because authored `templates/*.json` source files are deliberately excluded from the deployment upload. - `scripts/build-vercel-public.mjs` creates the only public static surface under `dist/`: `index.html`, `main.js`, `styles.css`, browser JavaScript under `src/` and `components/`, plus the explicitly public `docs/eula.md` linked from the footer. All other docs, tests, build scripts, authoring JSON, AGENTS files, and repository metadata are not copied. - `vercel.json` must keep `outputDirectory: "dist"`. The former repository-root output exposed internal files such as `/docs/classroom-workstream.md` and `/scripts/build-templates-manifest.mjs`. - Generated manifests are committed artifacts. GitHub CI remains responsible for running `npm run build:templates` / `check:templates` and proving they match authored JSON; `npm run quality` also builds/verifies the same minimal `dist/` bundle. - `.vercelignore` prevents raw authored Case Study JSON from entering the Vercel source upload, while the `dist/` build prevents all non-runtime repository files from becoming public static assets. - Vercel Git deployments are deny-by-default for ordinary branches. `main` deploys automatically; short-lived `verify/**` branches are the explicit preview path for security/E2E checks. `vercel.json` also owns the ignored-build decision so those two allowed branch classes proceed even if the project dashboard has an older Ignored Build Step configured. This prevents AI checkpoint commits from exhausting Vercel build capacity while preserving GitHub CI on every PR commit. +- The linked Vercel project currently runs on the Hobby plan, which permits at most 12 Serverless Functions per deployment. Classroom routes therefore share one deployment entrypoint. Server helpers stay in underscore-prefixed `api/_*.js` modules, and collaboration workspace endpoints remain separate. - When modifying the manifest workflow, public bundle, or required quality gates, update both this README and `vercel.json` so the documented steps mirror the actual hosted build. - Dependency changes must be installed through the normal npm registry so npm generates the complete lockfile. Run `npm run verify:lockfile` before committing and `npm ci` from a clean dependency tree; the offline guard catches missing resolved root-package entries before CI reaches its clean install. From 3cce1a9572082a39f6f0a4604f7c65586ff6ed10 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:43:10 -0400 Subject: [PATCH 26/33] docs: govern Vercel function budget --- docs/REPOSITORY-OPERATIONS.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/REPOSITORY-OPERATIONS.md b/docs/REPOSITORY-OPERATIONS.md index fa82373..7692c38 100644 --- a/docs/REPOSITORY-OPERATIONS.md +++ b/docs/REPOSITORY-OPERATIONS.md @@ -14,6 +14,7 @@ After changing the runtime, update `.nvmrc` and `package.json#engines` together - lockfile consistency; - repository contract checks (`repo:doctor`); +- conservative Vercel Serverless Function budget verification (`verify:vercel-functions`); - changed-runtime test coverage guard for `src/`, `components/`, and server `api/` code; - summary integration guard; - persistence integration guard; @@ -36,11 +37,27 @@ Case Study authoring source is intentionally present in Git but intentionally ab - `scripts/build-vercel-public.mjs` generates the **only** public static surface under `dist/`: `index.html`, `main.js`, `styles.css`, browser JavaScript under `src/` and `components/`, plus the explicitly public `docs/eula.md` linked from the app footer. - Vercel `outputDirectory` must remain `dist`. Serving `.` is prohibited because production previously exposed internal files such as `docs/classroom-workstream.md` and `scripts/build-templates-manifest.mjs`. - The public-bundle builder rejects all other Markdown plus JSON/MJS/AGENTS/internal files and scans all emitted text—including the EULA—for protected Case Study IDs/names. -- Production Vercel builds run `npm run verify:protected-cases && npm run build:vercel-public`; they do not regenerate manifests because authored template JSON is intentionally excluded from the upload. +- Production Vercel builds run `npm run verify:vercel-functions && npm run verify:protected-cases && npm run build:vercel-public`; they do not regenerate manifests because authored template JSON is intentionally excluded from the upload. - GitHub CI/local authoring remains responsible for `npm run build:templates` / `npm run check:templates` freshness, and `npm run quality` also builds/verifies the same minimal `dist/` surface. Any change to `vercel.json`, `.vercelignore`, template generation, static output layout, or protected-resource routing must re-run `npm run quality` and include a deployed HTTP check that raw `/templates/*.json` Case Study paths and internal `/docs/*` / `/scripts/*` paths are not served. +### Vercel Serverless Function budget + +The linked `intake` project is intentionally compatible with Vercel Hobby's **12 Serverless Function** deployment limit. #312/#313 originally exceeded that limit when each new Classroom URL received its own thin `api/classes/**` wrapper, even though GitHub tests were green. + +Repository contract after #321: + +- every public `/api/classes/**` URL remains stable; +- `vercel.json` rewrites those URLs to the single `api/classroom.js` deployment function; +- `api/_classroomRouter.js` selects the existing handler; the route marker is not authentication and no handler authorization is bypassed; +- do not add standalone JavaScript route wrappers back under `api/classes/**`; add the handler to the dispatcher + rewrite map instead; +- underscore-prefixed `api/_*.js` files are server helper modules bundled into entrypoints; +- `npm run verify:vercel-functions` conservatively counts every non-underscore JavaScript file under `api/` and fails above 12; +- the same guard runs in `npm run quality` and before every Vercel production build. + +A PR that changes API routing is not publish-complete until the exact merged `main` SHA has a **READY** production deployment, not merely green GitHub checks. + ### Vercel Git deployment policy From d470126d9fcde9b014594d9d4bd0da2aa907c274 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:43:14 -0400 Subject: [PATCH 27/33] docs: guard single-function Classroom routing --- api/AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/AGENTS.md b/api/AGENTS.md index d51d7ec..04987c3 100644 --- a/api/AGENTS.md +++ b/api/AGENTS.md @@ -8,6 +8,8 @@ These rules apply to all server-only modules below `api/`. - Never return or log database connection values, raw capability hashes, authorization headers, or production Intake snapshots. - Keep route files thin and inject repositories into handlers for deterministic tests. +- Vercel Classroom deployment uses one `api/classroom.js` entrypoint plus `api/_classroomRouter.js`; preserve public `/api/classes/**` URLs through `vercel.json` rewrites instead of creating new `api/classes/**/*.js` function wrappers. Routing never replaces handler authorization. +- Keep the conservative deployable-function count at or below 12 and preserve `npm run verify:vercel-functions` in both `quality` and the Vercel build command. - Schema changes must remain additive/idempotent and be documented in the README plus the owning architecture document. - Validate method, authorization, identifiers, names, and payload size before expensive or mutating work. - Responses containing capabilities or private Intake data use `Cache-Control: no-store` and `Referrer-Policy: no-referrer`. From 876b85ac3234f999991fa390089a73a673054e74 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:43:18 -0400 Subject: [PATCH 28/33] docs: define Classroom deployment routing invariant --- docs/classroom-api.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/classroom-api.md b/docs/classroom-api.md index ef1738d..befcae4 100644 --- a/docs/classroom-api.md +++ b/docs/classroom-api.md @@ -34,6 +34,13 @@ Raw capabilities are returned only when created or rotated and are never persist ## Endpoints +### Deployment routing invariant + +The public endpoint paths documented below are stable API contracts. To stay within the Vercel Hobby Serverless Function budget, `vercel.json` rewrites every `/api/classes/**` path to the single `api/classroom.js` deployment entrypoint. `api/_classroomRouter.js` then delegates to the existing handler factory for that route. + +The internal `__classroomRoute` marker is **routing only**. It is not a capability, does not select a class/workspace, and does not weaken the method/authentication/authorization checks inside the delegated handler. Do not recreate one physical `api/classes/**/*.js` wrapper per public route; extend the rewrite/dispatcher mapping and keep `npm run verify:vercel-functions` green instead. + + ### `POST /api/classes` Creates a class from a title and returns: From 7cd33fdbbdbc793c3cd8e0b5234c41b3bde94a35 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:43:53 -0400 Subject: [PATCH 29/33] docs: add Classroom production publish gate --- docs/classroom-roadmap.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/classroom-roadmap.md b/docs/classroom-roadmap.md index 6fbea84..898d0da 100644 --- a/docs/classroom-roadmap.md +++ b/docs/classroom-roadmap.md @@ -229,6 +229,19 @@ The eight-slice #288 program establishes the secure Classroom foundation; it doe The secure classroom/orchestration engine is complete, but a **real production KT simulation is not yet runnable from source-faithful case material**. The remaining roadmap is now product/content enablement rather than another exercise-engine tranche. +### Publish recovery — #321 / PR #322 + +#312 and #313 were both green in GitHub, but their first `main` deployments exceeded the Vercel Hobby limit of 12 Serverless Functions. Production therefore remained on the older #308 baseline even after PR #315 merged. + +#321 resolves the deployment packaging without changing public API or authorization semantics: +- keep every public `/api/classes/**` URL; +- rewrite those paths to one `api/classroom.js` Vercel function; +- delegate through `api/_classroomRouter.js` to the existing tested handlers; +- keep collaboration workspace functions separate; +- enforce the <=12 budget in `npm run quality` and in the Vercel build. + +**Roadmap gate:** do not begin #316 as production work until #321/#322 has a READY Vercel deployment from the exact merged `main` SHA. Once that publish recovery is verified, #316 is the next implementation issue. + ### P0 — production case material delivery **#316 — Protected staged case assets and rich Student case rendering** @@ -260,6 +273,8 @@ Add QR/share-link convenience around the human join code without putting bearer ```text #315 merge | + #321 / #322 restore production deployment under Vercel function budget + | #316 protected rich staged content | #317 first production staged KT Case Study From 0a19960d85e2d51dd1c81b65d64a000744b2ed2d Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:43:56 -0400 Subject: [PATCH 30/33] docs: checkpoint Vercel publish recovery --- docs/classroom-workstream.md | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/docs/classroom-workstream.md b/docs/classroom-workstream.md index 89f98d0..9780773 100644 --- a/docs/classroom-workstream.md +++ b/docs/classroom-workstream.md @@ -680,3 +680,47 @@ Cold-start priority after #315 merges: 4. do **not** add an official production `simulation` definition until #316 is complete and the source material for #317 is present/reviewed. The #288 foundation roadmap plus #312/#313 orchestration generation are complete after #315 merges. The next program phase is **production case enablement and facilitator scale**, beginning with #316. + + +## #321 publish-recovery checkpoint + +**Issue:** #321 +**PR:** #322 +**Branch:** `fix/vercel-classroom-function-budget` +**Base:** `main` at #315 merge `7c6afa4069ac5de5eae8ec0c9b0959084650c93a` + +Why this exists: +- #314 (`b141d55...`) and #315 (`7c6afa4...`) both passed GitHub quality/security/browser gates but their Vercel production deployments ended in `ERROR`; +- Vercel reported `exceeded_serverless_functions_per_deployment`: Hobby permits at most 12 Serverless Functions; +- the last READY production deployment remained #308 at `6c58336ac1677764641c09eadb201abcad8eeac1`; +- #312 added four Classroom route entrypoints and #313 added four more, taking the one-file-per-route design beyond the plan limit. + +Recovery architecture: +- all existing public `/api/classes/**` URLs remain unchanged; +- `vercel.json` rewrites those paths to one `api/classroom.js` deployment entrypoint; +- `api/_classroomRouter.js` delegates to the existing class/coaching/protected-case/exercise handler factories; +- routing is not authorization: every delegated handler retains its existing method/capability/class/workspace checks; +- standalone collaboration continues through the separate `api/workspaces/*` functions; +- the 16 redundant `api/classes/**/*.js` wrapper files are removed; +- the conservative deployable function count falls to **5**, leaving headroom below the Hobby limit of 12; +- `npm run verify:vercel-functions` now guards the budget in both `npm run quality` and the Vercel production build; +- regression coverage locks every preserved Classroom public URL to its dispatcher route. + +Security invariants unchanged: +- Instructor vs Student capability isolation; +- stable Student class-session vs assignment/workspace edit authority; +- Student non-enumeration; +- protected Case Study and staged progressive-disclosure boundaries; +- checkpoint authorization; +- server-enforced HTTP 423 freeze; +- Standalone/ad-hoc collaboration compatibility. + +Exact continuation: +1. finish #322 GitHub gates; +2. create a short-lived `verify/**` branch at the exact PR head and confirm Vercel can build/deploy it under the function budget; +3. merge #322 only after review/gates; +4. verify the exact merged `main` SHA reaches Vercel production `READY` and scan runtime errors; +5. close #321 and confirm #316 is unblocked; +6. then begin #316 — protected staged assets + rich Student rendering. + +Do not describe #312/#313 as production-published until step 4 is complete. From a8ec11273feefed673f2473d4483437e208e93ce Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:43:59 -0400 Subject: [PATCH 31/33] docs: gate production case work on publish recovery --- docs/classroom-staged-simulation.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/classroom-staged-simulation.md b/docs/classroom-staged-simulation.md index e0ac546..e56ecf3 100644 --- a/docs/classroom-staged-simulation.md +++ b/docs/classroom-staged-simulation.md @@ -1001,7 +1001,9 @@ Known remaining product gaps are now tracked separately: - **#319** — Instructor cross-team debrief comparison/progress view built on immutable checkpoints/live observation. - **#320** — share/QR join convenience and compact mobile Notes/Class/Case chrome. -After PR #315 merges, the exact next implementation action is **#316**. +PR #315 is merged, but production publication requires the #321 Vercel function-budget recovery. #321/#322 consolidates the public Classroom URLs into one deployment function without changing staged authorization semantics. + +After #321 is merged **and the exact `main` SHA is verified Vercel production READY**, the exact next implementation action is **#316**. Do not begin production official-case staging as a shortcut. Preserve: - server-side progressive disclosure; From da73f6fccefa79c6773d72d142ceff802ca8a367 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:44:57 -0400 Subject: [PATCH 32/33] fix: remove unused budget-script import --- scripts/verify-vercel-function-budget.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/verify-vercel-function-budget.mjs b/scripts/verify-vercel-function-budget.mjs index 74072e9..c57263f 100644 --- a/scripts/verify-vercel-function-budget.mjs +++ b/scripts/verify-vercel-function-budget.mjs @@ -9,7 +9,7 @@ import { readdir } from 'node:fs/promises'; import path from 'node:path'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { pathToFileURL } from 'node:url'; export const VERCEL_HOBBY_FUNCTION_LIMIT = 12; From 9094a3fa2a56c8720e9c149278ef03165ecd0b17 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:46:07 -0400 Subject: [PATCH 33/33] build: align protected boundary with function preflight --- scripts/verify-protected-case-boundary.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/verify-protected-case-boundary.mjs b/scripts/verify-protected-case-boundary.mjs index 6c1e579..0ff66f5 100644 --- a/scripts/verify-protected-case-boundary.mjs +++ b/scripts/verify-protected-case-boundary.mjs @@ -47,10 +47,10 @@ async function main() { } const vercelConfig = JSON.parse(vercelConfigRaw); - const expectedBuildCommand = 'npm run verify:protected-cases && npm run build:vercel-public'; + const expectedBuildCommand = 'npm run verify:vercel-functions && npm run verify:protected-cases && npm run build:vercel-public'; if (vercelConfig.buildCommand !== expectedBuildCommand) { throw new Error( - 'Vercel buildCommand must verify protected cases and then build the minimal public dist/ bundle.' + 'Vercel buildCommand must verify the function budget, verify protected cases, and then build the minimal public dist/ bundle.' ); } if (vercelConfig.outputDirectory !== 'dist') {