canvas-lms-mcp 2.8.2 → 2.8.4
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.
- package/dist/canvas/index.cjs +3 -3
- package/dist/canvas/index.js +2 -2
- package/dist/{chunk-DEFB5PL2.cjs → chunk-4TAI5SSR.cjs} +5 -5
- package/dist/{chunk-DEFB5PL2.cjs.map → chunk-4TAI5SSR.cjs.map} +1 -1
- package/dist/{chunk-AFN5BB4O.js → chunk-HF7IV5SM.js} +2 -2
- package/dist/{chunk-HJWLGW54.cjs → chunk-HMC476LT.cjs} +358 -358
- package/dist/chunk-HMC476LT.cjs.map +1 -0
- package/dist/{chunk-YNNREXP7.js → chunk-R5BMHZIZ.js} +16 -16
- package/dist/chunk-R5BMHZIZ.js.map +1 -0
- package/dist/{chunk-F34MMZIS.js → chunk-SYO65DQF.js} +2 -2
- package/dist/{chunk-F34MMZIS.js.map → chunk-SYO65DQF.js.map} +1 -1
- package/dist/{chunk-ZT2BWA2J.cjs → chunk-XDAWAHU7.cjs} +2 -2
- package/dist/{chunk-ZT2BWA2J.cjs.map → chunk-XDAWAHU7.cjs.map} +1 -1
- package/dist/http.cjs +6 -6
- package/dist/http.js +3 -3
- package/dist/init.cjs +3 -3
- package/dist/init.js +1 -1
- package/dist/server.cjs +4 -4
- package/dist/server.js +3 -3
- package/dist/stdio.cjs +4 -4
- package/dist/stdio.js +3 -3
- package/dist/tools/discussions.d.ts.map +1 -1
- package/dist/tools/modules.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-HJWLGW54.cjs.map +0 -1
- package/dist/chunk-YNNREXP7.js.map +0 -1
- /package/dist/{chunk-AFN5BB4O.js.map → chunk-HF7IV5SM.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-DEFB5PL2.cjs","../src/canvas/courses.ts","../src/canvas/assignments.ts","../src/canvas/submissions.ts","../src/canvas/rubrics.ts","../src/canvas/quizzes.ts","../src/canvas/files.ts","../src/canvas/users.ts","../src/canvas/groups.ts","../src/canvas/enrollments.ts","../src/canvas/discussions.ts","../src/canvas/modules.ts","../src/canvas/concurrency.ts","../src/canvas/pages.ts","../src/canvas/calendar.ts","../src/canvas/conversations.ts","../src/canvas/peer-reviews.ts","../src/canvas/accounts.ts","../src/canvas/analytics.ts","../src/canvas/dashboard.ts","../src/canvas/outcomes.ts","../src/canvas/gradebook-history.ts","../src/canvas/new-quizzes.ts","../src/canvas/content-exports.ts","../src/canvas/content-migrations.ts","../src/canvas/grading-standards.ts","../src/canvas/late-policy.ts","../src/canvas/appointment-groups.ts","../src/canvas/activity-stream.ts","../src/canvas/planner.ts","../src/canvas/id.ts","../src/canvas/index.ts"],"names":[],"mappings":"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","file":"/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-DEFB5PL2.cjs","sourcesContent":[null,"import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasCourse, CreateCourseParams, UpdateCourseParams } from './types'\nimport type { CanvasId } from './id'\n\nexport type CourseEnrollmentState = 'active' | 'invited_or_pending' | 'completed'\n\nexport type CourseWorkflowState = 'unpublished' | 'available' | 'completed' | 'deleted'\n\nexport type CourseListInclude =\n | 'needs_grading_count'\n | 'syllabus_body'\n | 'public_description'\n | 'total_scores'\n | 'current_grading_period_scores'\n | 'grading_periods'\n | 'term'\n | 'account'\n | 'course_progress'\n | 'sections'\n | 'storage_quota_used_mb'\n | 'total_students'\n | 'passback_status'\n | 'favorites'\n | 'teachers'\n | 'observed_users'\n | 'tabs'\n | 'course_image'\n | 'banner_image'\n | 'concluded'\n | 'lti_context_id'\n | 'post_manually'\n\nexport type CourseGetInclude =\n CourseListInclude | 'all_courses' | 'permissions' | 'public_description'\n\nexport interface ListCoursesOptions {\n enrollment_state?: CourseEnrollmentState\n state?: ReadonlyArray<CourseWorkflowState>\n enrollment_role_id?: CanvasId\n include?: ReadonlyArray<CourseListInclude>\n exclude_blueprint_courses?: boolean\n}\n\nexport interface GetCourseOptions {\n include?: ReadonlyArray<CourseGetInclude>\n teacher_limit?: number\n}\n\nconst DEFAULT_LIST_INCLUDE: ReadonlyArray<CourseListInclude> = ['term']\nconst DEFAULT_GET_INCLUDE: ReadonlyArray<CourseGetInclude> = ['term', 'total_students']\n\nexport class CoursesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(opts: ListCoursesOptions = {}): Promise<CanvasCourse[]> {\n const params: CanvasQueryParams = {}\n params.include = opts.include && opts.include.length > 0 ? opts.include : DEFAULT_LIST_INCLUDE\n if (opts.enrollment_state) params.enrollment_state = opts.enrollment_state\n if (opts.state && opts.state.length > 0) params.state = opts.state\n if (opts.enrollment_role_id !== undefined) params.enrollment_role_id = opts.enrollment_role_id\n if (opts.exclude_blueprint_courses !== undefined) {\n params.exclude_blueprint_courses = opts.exclude_blueprint_courses\n }\n return this.client.paginate<CanvasCourse>('/api/v1/courses', params)\n }\n\n async get(courseId: CanvasId, opts: GetCourseOptions = {}): Promise<CanvasCourse> {\n const include = opts.include && opts.include.length > 0 ? opts.include : DEFAULT_GET_INCLUDE\n const query: CanvasQueryParams = { include }\n if (opts.teacher_limit !== undefined) query.teacher_limit = opts.teacher_limit\n return this.client.request<CanvasCourse>(`/api/v1/courses/${courseId}`, { query })\n }\n\n async getSyllabus(courseId: CanvasId): Promise<string | null> {\n const course = await this.client.request<CanvasCourse>(`/api/v1/courses/${courseId}`, {\n query: { include: ['syllabus_body'] },\n })\n return course.syllabus_body ?? null\n }\n\n async create(params: CreateCourseParams): Promise<CanvasCourse> {\n const { account_id, ...courseFields } = params\n return this.client.request<CanvasCourse>(`/api/v1/accounts/${account_id}/courses`, {\n method: 'POST',\n body: JSON.stringify({ course: courseFields }),\n })\n }\n\n async update(courseId: CanvasId, params: UpdateCourseParams): Promise<CanvasCourse> {\n return this.client.request<CanvasCourse>(`/api/v1/courses/${courseId}`, {\n method: 'PUT',\n body: JSON.stringify({ course: params }),\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type {\n CanvasAssignment,\n CanvasAssignmentGroup,\n CanvasAssignmentOverride,\n CreateAssignmentOverrideParams,\n CreateAssignmentParams,\n UpdateAssignmentParams,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport type AssignmentListInclude =\n | 'submission'\n | 'assignment_visibility'\n | 'all_dates'\n | 'overrides'\n | 'observed_users'\n | 'can_edit'\n | 'score_statistics'\n\nexport type AssignmentGetInclude =\n | 'submission'\n | 'assignment_visibility'\n | 'overrides'\n | 'observed_users'\n | 'can_edit'\n | 'score_statistics'\n | 'all_dates'\n\nexport type AssignmentBucket =\n 'past' | 'overdue' | 'undated' | 'ungraded' | 'unsubmitted' | 'upcoming' | 'future'\n\nexport type AssignmentOrderBy = 'position' | 'name' | 'due_at'\n\nexport type AssignmentGroupInclude =\n | 'assignments'\n | 'discussion_topic'\n | 'all_dates'\n | 'assignment_visibility'\n | 'overrides'\n | 'submission'\n | 'observed_users'\n | 'can_edit'\n | 'score_statistics'\n\nexport interface ListAssignmentsOptions {\n include?: ReadonlyArray<AssignmentListInclude>\n search_term?: string\n override_assignment_dates?: boolean\n needs_grading_count_by_section?: boolean\n bucket?: AssignmentBucket\n assignment_ids?: ReadonlyArray<CanvasId>\n order_by?: AssignmentOrderBy\n post_to_sis?: boolean\n}\n\nexport interface GetAssignmentOptions {\n include?: ReadonlyArray<AssignmentGetInclude>\n override_assignment_dates?: boolean\n needs_grading_count_by_section?: boolean\n all_dates?: boolean\n}\n\nexport interface ListAssignmentGroupsOptions {\n include?: ReadonlyArray<AssignmentGroupInclude>\n assignment_ids?: ReadonlyArray<CanvasId>\n exclude_assignment_submission_types?: ReadonlyArray<string>\n override_assignment_dates?: boolean\n grading_period_id?: CanvasId\n scope_assignments_to_student?: boolean\n}\n\nfunction toQuery(opts: object): CanvasQueryParams {\n const params: CanvasQueryParams = {}\n for (const [key, value] of Object.entries(opts)) {\n if (value === undefined || value === null) continue\n if (Array.isArray(value)) {\n if (value.length === 0) continue\n params[key] = value as ReadonlyArray<string | number | boolean>\n } else if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n params[key] = value\n }\n }\n return params\n}\n\nexport class AssignmentsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId, opts: ListAssignmentsOptions = {}): Promise<CanvasAssignment[]> {\n return this.client.paginate<CanvasAssignment>(\n `/api/v1/courses/${courseId}/assignments`,\n toQuery(opts),\n )\n }\n\n async get(\n courseId: CanvasId,\n assignmentId: CanvasId,\n opts: GetAssignmentOptions = {},\n ): Promise<CanvasAssignment> {\n return this.client.request<CanvasAssignment>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}`,\n { query: toQuery(opts) },\n )\n }\n\n async listGroups(\n courseId: CanvasId,\n opts: ListAssignmentGroupsOptions = {},\n ): Promise<CanvasAssignmentGroup[]> {\n return this.client.paginate<CanvasAssignmentGroup>(\n `/api/v1/courses/${courseId}/assignment_groups`,\n toQuery(opts),\n )\n }\n\n async create(courseId: CanvasId, params: CreateAssignmentParams): Promise<CanvasAssignment> {\n return this.client.request<CanvasAssignment>(`/api/v1/courses/${courseId}/assignments`, {\n method: 'POST',\n body: JSON.stringify({ assignment: params }),\n })\n }\n\n async update(\n courseId: CanvasId,\n assignmentId: CanvasId,\n params: UpdateAssignmentParams,\n ): Promise<CanvasAssignment> {\n return this.client.request<CanvasAssignment>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}`,\n {\n method: 'PUT',\n body: JSON.stringify({ assignment: params }),\n },\n )\n }\n\n async delete(courseId: CanvasId, assignmentId: CanvasId): Promise<void> {\n await this.client.request<void>(`/api/v1/courses/${courseId}/assignments/${assignmentId}`, {\n method: 'DELETE',\n })\n }\n\n async listOverrides(\n courseId: CanvasId,\n assignmentId: CanvasId,\n ): Promise<CanvasAssignmentOverride[]> {\n return this.client.paginate<CanvasAssignmentOverride>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/overrides`,\n )\n }\n\n async createOverride(\n courseId: CanvasId,\n assignmentId: CanvasId,\n params: CreateAssignmentOverrideParams,\n ): Promise<CanvasAssignmentOverride> {\n return this.client.request<CanvasAssignmentOverride>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/overrides`,\n {\n method: 'POST',\n body: JSON.stringify({ assignment_override: params }),\n },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasSubmission } from './types'\nimport type { CanvasId } from './id'\n\nexport type SubmissionListInclude =\n | 'submission_history'\n | 'submission_comments'\n | 'rubric_assessment'\n | 'assignment'\n | 'visibility'\n | 'course'\n | 'user'\n | 'group'\n | 'read_status'\n | 'sub_assignment_submissions'\n\nexport type SubmissionGetInclude =\n | 'submission_history'\n | 'submission_comments'\n | 'rubric_assessment'\n | 'visibility'\n | 'course'\n | 'user'\n | 'read_status'\n\nexport type SubmissionWorkflowState = 'submitted' | 'unsubmitted' | 'graded' | 'pending_review'\n\nexport interface ListSubmissionsOptions {\n include?: ReadonlyArray<SubmissionListInclude>\n student_ids?: ReadonlyArray<CanvasId>\n assignment_ids?: ReadonlyArray<CanvasId>\n section_ids?: ReadonlyArray<CanvasId>\n grouped?: boolean\n workflow_state?: SubmissionWorkflowState\n grading_period_id?: CanvasId\n post_to_sis?: boolean\n submitted_since?: string\n graded_since?: string\n}\n\nexport interface GetSubmissionOptions {\n include?: ReadonlyArray<SubmissionGetInclude>\n}\n\nexport interface ListStudentSubmissionsOptions {\n student_ids?: ReadonlyArray<CanvasId>\n assignment_ids?: ReadonlyArray<CanvasId>\n include?: ReadonlyArray<SubmissionListInclude>\n workflow_state?: SubmissionWorkflowState\n}\n\nexport interface ListMySubmissionsOptions {\n include?: ReadonlyArray<SubmissionListInclude>\n}\n\nexport interface SubmitAssignmentParams {\n submission_type: 'online_text_entry' | 'online_url' | 'online_upload'\n body?: string\n url?: string\n file_ids?: ReadonlyArray<CanvasId>\n comment?: string\n}\n\nconst DEFAULT_LIST_INCLUDE: ReadonlyArray<SubmissionListInclude> = ['submission_comments']\nconst DEFAULT_GET_INCLUDE: ReadonlyArray<SubmissionGetInclude> = ['submission_comments']\n\nfunction buildListParams(opts: ListSubmissionsOptions): CanvasQueryParams {\n const params: CanvasQueryParams = {}\n params.include = opts.include && opts.include.length > 0 ? opts.include : DEFAULT_LIST_INCLUDE\n if (opts.student_ids && opts.student_ids.length > 0) params.student_ids = opts.student_ids\n if (opts.assignment_ids && opts.assignment_ids.length > 0)\n params.assignment_ids = opts.assignment_ids\n if (opts.section_ids && opts.section_ids.length > 0) params.section_ids = opts.section_ids\n if (opts.grouped !== undefined) params.grouped = opts.grouped\n if (opts.workflow_state) params.workflow_state = opts.workflow_state\n if (opts.grading_period_id !== undefined) params.grading_period_id = opts.grading_period_id\n if (opts.post_to_sis !== undefined) params.post_to_sis = opts.post_to_sis\n if (opts.submitted_since) params.submitted_since = opts.submitted_since\n if (opts.graded_since) params.graded_since = opts.graded_since\n return params\n}\n\nexport class SubmissionsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(\n courseId: CanvasId,\n assignmentId: CanvasId,\n opts: ListSubmissionsOptions = {},\n ): Promise<CanvasSubmission[]> {\n return this.client.paginate<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions`,\n buildListParams(opts),\n )\n }\n\n async get(\n courseId: CanvasId,\n assignmentId: CanvasId,\n userId: CanvasId,\n opts: GetSubmissionOptions = {},\n ): Promise<CanvasSubmission> {\n const include = opts.include && opts.include.length > 0 ? opts.include : DEFAULT_GET_INCLUDE\n return this.client.request<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`,\n { query: { include } },\n )\n }\n\n async grade(\n courseId: CanvasId,\n assignmentId: CanvasId,\n userId: CanvasId,\n grade: string,\n ): Promise<CanvasSubmission> {\n return this.client.request<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`,\n {\n method: 'PUT',\n body: JSON.stringify({ submission: { posted_grade: grade } }),\n },\n )\n }\n\n async comment(\n courseId: CanvasId,\n assignmentId: CanvasId,\n userId: CanvasId,\n comment: string,\n ): Promise<CanvasSubmission> {\n return this.client.request<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`,\n {\n method: 'PUT',\n body: JSON.stringify({\n comment: { text_comment: comment },\n }),\n },\n )\n }\n\n async listMy(\n courseId: CanvasId,\n opts: ListMySubmissionsOptions = {},\n ): Promise<CanvasSubmission[]> {\n const params: CanvasQueryParams = { student_ids: ['self'] }\n if (opts.include && opts.include.length > 0) params.include = opts.include\n return this.client.paginate<CanvasSubmission>(\n `/api/v1/courses/${courseId}/students/submissions`,\n params,\n )\n }\n\n async submit(\n courseId: CanvasId,\n assignmentId: CanvasId,\n params: SubmitAssignmentParams,\n ): Promise<CanvasSubmission> {\n const submission: Record<string, unknown> = { submission_type: params.submission_type }\n if (params.body !== undefined) submission.body = params.body\n if (params.url !== undefined) submission.url = params.url\n if (params.file_ids !== undefined) submission.file_ids = params.file_ids\n const payload: Record<string, unknown> = { submission }\n if (params.comment !== undefined) payload.comment = { text_comment: params.comment }\n return this.client.request<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions`,\n { method: 'POST', body: JSON.stringify(payload) },\n )\n }\n\n async listForStudents(\n courseId: CanvasId,\n opts: ListStudentSubmissionsOptions = {},\n ): Promise<CanvasSubmission[]> {\n const params: CanvasQueryParams = {}\n params.student_ids =\n opts.student_ids && opts.student_ids.length > 0 ? opts.student_ids : ['all']\n if (opts.assignment_ids && opts.assignment_ids.length > 0)\n params.assignment_ids = opts.assignment_ids\n if (opts.include && opts.include.length > 0) params.include = opts.include\n if (opts.workflow_state) params.workflow_state = opts.workflow_state\n return this.client.paginate<CanvasSubmission>(\n `/api/v1/courses/${courseId}/students/submissions`,\n params,\n )\n }\n}\n","import { CanvasApiError, type CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type {\n CanvasRubric,\n CanvasRubricAssessment,\n CanvasRubricAssociation,\n CanvasRubricCreateResult,\n CanvasSubmission,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport type RubricInclude =\n | 'assessments'\n | 'graded_assessments'\n | 'peer_assessments'\n | 'associations'\n | 'assignment_associations'\n | 'course_associations'\n | 'account_associations'\n\nexport interface GetRubricOptions {\n include?: ReadonlyArray<RubricInclude>\n}\n\nexport interface RubricRatingInput {\n description: string\n points: number\n}\n\nexport interface RubricCriterionInput {\n description: string\n points: number\n ratings: RubricRatingInput[]\n}\n\nexport interface RubricCreateInput {\n title: string\n criteria: RubricCriterionInput[]\n}\n\nexport interface RubricAssociationInput {\n assignment_id: CanvasId\n use_for_grading?: boolean\n purpose?: string\n}\n\nexport interface RubricAttachOptions {\n use_for_grading?: boolean\n hide_score_total?: boolean\n /** Defaults to `grading`. */\n purpose?: string\n}\n\nexport class RubricsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasRubric[]> {\n return this.client.paginate<CanvasRubric>(`/api/v1/courses/${courseId}/rubrics`)\n }\n\n async get(\n courseId: CanvasId,\n rubricId: CanvasId,\n options: GetRubricOptions = {},\n ): Promise<CanvasRubric> {\n if (!options.include || options.include.length === 0) {\n return this.client.request<CanvasRubric>(`/api/v1/courses/${courseId}/rubrics/${rubricId}`)\n }\n const query: CanvasQueryParams = {}\n query.include = options.include\n return this.client.request<CanvasRubric>(`/api/v1/courses/${courseId}/rubrics/${rubricId}`, {\n query,\n })\n }\n\n /** Canvas answers the DELETE with the deleted rubric inside a `{ rubric }` envelope, which this unwraps. */\n async delete(courseId: CanvasId, rubricId: CanvasId): Promise<CanvasRubric> {\n const envelope = await this.client.request<{ rubric: CanvasRubric }>(\n `/api/v1/courses/${courseId}/rubrics/${rubricId}`,\n { method: 'DELETE' },\n )\n return envelope.rubric\n }\n\n async getAssessment(\n courseId: CanvasId,\n assignmentId: CanvasId,\n userId: CanvasId,\n ): Promise<CanvasSubmission> {\n return this.client.request<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}?include[]=rubric_assessment`,\n )\n }\n\n async submitAssessment(\n courseId: CanvasId,\n associationId: CanvasId,\n data: CanvasRubricAssessment['data'],\n ): Promise<CanvasRubricAssessment> {\n return this.client.request<CanvasRubricAssessment>(\n `/api/v1/courses/${courseId}/rubric_associations/${associationId}/rubric_assessments`,\n {\n method: 'POST',\n body: JSON.stringify({ rubric_assessment: { data } }),\n },\n )\n }\n\n /** Canvas returns an envelope and reports association validation failures with HTTP 200. */\n async create(\n courseId: CanvasId,\n rubric: RubricCreateInput,\n association?: RubricAssociationInput,\n ): Promise<CanvasRubricCreateResult> {\n const params = new URLSearchParams()\n params.append('rubric[title]', rubric.title)\n\n rubric.criteria.forEach((criterion, ci) => {\n params.append(`rubric[criteria][${ci}][description]`, criterion.description)\n params.append(`rubric[criteria][${ci}][points]`, String(criterion.points))\n\n // Sort ratings highest → lowest before encoding\n const sortedRatings = [...criterion.ratings].sort((a, b) => b.points - a.points)\n sortedRatings.forEach((rating, ri) => {\n params.append(`rubric[criteria][${ci}][ratings][${ri}][description]`, rating.description)\n params.append(`rubric[criteria][${ci}][ratings][${ri}][points]`, String(rating.points))\n })\n })\n\n if (association) {\n params.append('rubric_association[association_id]', String(association.assignment_id))\n params.append('rubric_association[association_type]', 'Assignment')\n if (association.use_for_grading !== undefined) {\n params.append('rubric_association[use_for_grading]', String(association.use_for_grading))\n }\n if (association.purpose) {\n params.append('rubric_association[purpose]', association.purpose)\n }\n }\n\n const endpoint = `/api/v1/courses/${courseId}/rubrics`\n const result = await this.client.request<\n (CanvasRubricCreateResult & { error?: false }) | { error: true; messages?: string[] }\n >(endpoint, {\n method: 'POST',\n body: params.toString(),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n })\n if (result.error === true) {\n throw new CanvasApiError(\n result.messages?.join('; ') || 'Canvas rejected the rubric',\n 422,\n endpoint,\n )\n }\n return result\n }\n\n /**\n * Attach an existing rubric to an assignment via\n * `POST /courses/:id/rubric_associations`, so one rubric can be shared across\n * many assignments instead of `create` minting a duplicate per assignment.\n * Same bracket-notation form encoding as `create`.\n * Canvas returns the full rubric beside the association; only the association is\n * returned, so looping over many assignments does not repeat the criteria.\n */\n async associate(\n courseId: CanvasId,\n rubricId: CanvasId,\n assignmentId: CanvasId,\n options: RubricAttachOptions = {},\n ): Promise<CanvasRubricAssociation> {\n const params = new URLSearchParams()\n params.append('rubric_association[rubric_id]', String(rubricId))\n params.append('rubric_association[association_id]', String(assignmentId))\n params.append('rubric_association[association_type]', 'Assignment')\n params.append('rubric_association[purpose]', options.purpose ?? 'grading')\n if (options.use_for_grading !== undefined) {\n params.append('rubric_association[use_for_grading]', String(options.use_for_grading))\n }\n if (options.hide_score_total !== undefined) {\n params.append('rubric_association[hide_score_total]', String(options.hide_score_total))\n }\n const envelope = await this.client.request<{\n rubric: CanvasRubric\n rubric_association: CanvasRubricAssociation\n }>(`/api/v1/courses/${courseId}/rubric_associations`, {\n method: 'POST',\n body: params.toString(),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n })\n return envelope.rubric_association\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport { type CanvasQueryParams } from './query'\nimport type {\n CanvasQuiz,\n CanvasQuizSubmission,\n CanvasQuizQuestion,\n CanvasQuizSubmissionQuestion,\n CanvasQuizSubmissionEvent,\n CanvasQuizSubmissionEventsResponse,\n CanvasQuizExtension,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport class QuizzesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasQuiz[]> {\n return this.client.paginate<CanvasQuiz>(`/api/v1/courses/${courseId}/quizzes`)\n }\n\n async get(courseId: CanvasId, quizId: CanvasId): Promise<CanvasQuiz> {\n return this.client.request<CanvasQuiz>(`/api/v1/courses/${courseId}/quizzes/${quizId}`)\n }\n\n /**\n * Delete a Classic quiz. Per the Canvas Quizzes API docs, this \"deletes a\n * quiz and returns the deleted quiz object\".\n * @see https://canvas.instructure.com/doc/api/quizzes.html#method.quizzes/quizzes_api.destroy\n */\n async delete(courseId: CanvasId, quizId: CanvasId): Promise<CanvasQuiz> {\n return this.client.request<CanvasQuiz>(`/api/v1/courses/${courseId}/quizzes/${quizId}`, {\n method: 'DELETE',\n })\n }\n\n /**\n * Delete a question from a Classic quiz. Per the Canvas Quiz Questions API\n * docs, a successful deletion returns 204 No Content, so there is no body.\n * @see https://canvas.instructure.com/doc/api/quiz_questions.html\n */\n async deleteQuestion(courseId: CanvasId, quizId: CanvasId, questionId: CanvasId): Promise<void> {\n await this.client.request<void>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/questions/${questionId}`,\n { method: 'DELETE' },\n )\n }\n\n async listSubmissions(courseId: CanvasId, quizId: CanvasId): Promise<CanvasQuizSubmission[]> {\n return this.client.paginateEnvelope<CanvasQuizSubmission>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/submissions`,\n 'quiz_submissions',\n )\n }\n\n async listQuestions(courseId: CanvasId, quizId: CanvasId): Promise<CanvasQuizQuestion[]> {\n return this.client.paginate<CanvasQuizQuestion>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/questions`,\n )\n }\n\n /**\n * The questions one quiz-submission attempt was actually served, via\n * `GET /courses/:id/quizzes/:id/questions?quiz_submission_id=&quiz_submission_attempt=`.\n * Unlike `listQuestions` (the quiz's active questions only), this includes the\n * `generated` QuizQuestion rows Canvas creates for bank draws, with the text,\n * type and points_possible that attempt saw. Canvas needs BOTH parameters: with\n * either missing it silently returns the active list instead.\n */\n async listSubmissionQuestions(\n courseId: CanvasId,\n quizId: CanvasId,\n quizSubmissionId: CanvasId,\n attempt: number,\n ): Promise<CanvasQuizQuestion[]> {\n return this.client.paginate<CanvasQuizQuestion>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/questions`,\n { quiz_submission_id: quizSubmissionId, quiz_submission_attempt: attempt },\n )\n }\n\n /**\n * `GET /quiz_submissions/:id/questions` — per Canvas's own API doc header,\n * \"answering and flagging questions in a quiz-taking session\". It serves\n * two unrelated shapes depending on attempt state (`lib/api/v1/quiz_submission_question.rb:89-110`,\n * pinned SHA 1c9f0bb8013ed69c4f2efe11fd483025469b7e6c):\n *\n * - `untaken` (in progress): `submission_data` is a Hash; the response\n * includes the student's live `answer` and a real `flagged` state.\n * - `complete` / `pending_review` (submitted): `submission_data` is an\n * Array; `answer` is NEVER assigned, `flagged` is hardcoded `false` (or\n * omitted if Canvas has no graded entry for the question at all), and\n * `correct` carries the real {@link CanvasQuizCorrectness} domain.\n *\n * For a submitted attempt's actual answer text, use the Submissions API's\n * `include[]=submission_history` instead (see\n * `src/tools/quiz-question-responses.ts` and BRU-3080) — this method alone\n * cannot surface it.\n */\n async getSubmissionAnswers(quizSubmissionId: CanvasId): Promise<CanvasQuizSubmissionQuestion[]> {\n return this.client.paginateEnvelope<CanvasQuizSubmissionQuestion>(\n `/api/v1/quiz_submissions/${quizSubmissionId}/questions`,\n 'quiz_submission_questions',\n { 'include[]': 'quiz_question' },\n )\n }\n\n async scoreQuestion(\n courseId: CanvasId,\n quizId: CanvasId,\n submissionId: CanvasId,\n questionId: CanvasId,\n score: number,\n comment?: string,\n attempt?: number,\n ): Promise<void> {\n const submission: Record<string, unknown> = {\n questions: {\n [questionId]: { score, comment },\n },\n }\n if (attempt !== undefined) {\n submission.attempt = attempt\n }\n const body: Record<string, unknown> = {\n quiz_submissions: [submission],\n }\n await this.client.request(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/submissions/${submissionId}`,\n {\n method: 'PUT',\n body: JSON.stringify(body),\n },\n )\n }\n\n async getSubmissionEvents(\n courseId: CanvasId,\n quizId: CanvasId,\n submissionId: CanvasId,\n attempt?: number,\n ): Promise<CanvasQuizSubmissionEvent[]> {\n const query: CanvasQueryParams = {}\n if (attempt !== undefined) {\n query.attempt = attempt\n }\n const response = await this.client.request<CanvasQuizSubmissionEventsResponse>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/submissions/${submissionId}/events`,\n { query },\n )\n // Defensive: Canvas returns [] for an empty log; the guard also tolerates a\n // null field without throwing. Real errors surface as CanvasApiError above.\n return response.quiz_submission_events ?? []\n }\n\n /**\n * Apply a quiz extension (extra time / extra attempts) for one student on one\n * Classic Quiz via `POST /courses/:id/quizzes/:id/extensions`. Canvas accepts a\n * batch (`quiz_extensions` array), but callers operate per-student-per-quiz, so\n * the array always holds a single element. Omitted fields are left out of the\n * body — never pass `extra_time: 0` (Canvas rejects zero/negative extensions).\n */\n async setExtension(\n courseId: CanvasId,\n quizId: CanvasId,\n userId: CanvasId,\n extra_time?: number,\n extra_attempts?: number,\n ): Promise<CanvasQuizExtension[]> {\n // Not `Record<string, number>` any more (BRU-2730 §8 PR 1b): `user_id` is\n // an identifier and `extra_time` / `extra_attempts` are quantities, so the\n // two cannot share one value type. Spelling the three keys out is what\n // makes that split checkable instead of a comment. Emitting `user_id` as a\n // JSON string is also the correct form — a numeric `user_id` in a request\n // body reaches Canvas's `Api::ID_REGEX.match?(42)` and raises there (§2.4).\n const extension: { user_id: CanvasId; extra_time?: number; extra_attempts?: number } = {\n user_id: userId,\n }\n if (extra_time !== undefined) extension.extra_time = extra_time\n if (extra_attempts !== undefined) extension.extra_attempts = extra_attempts\n const response = await this.client.request<{ quiz_extensions: CanvasQuizExtension[] }>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/extensions`,\n {\n method: 'POST',\n body: JSON.stringify({ quiz_extensions: [extension] }),\n },\n )\n return response.quiz_extensions\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasFile, CanvasFileUploadInfo, CanvasFolder, DownloadedFile } from './types'\nimport type { CanvasId } from './id'\n\nconst MAX_DOWNLOAD_BYTES = 10 * 1024 * 1024 // 10 MB\n\nconst TEXT_CONTENT_TYPES = new Set([\n 'application/json',\n 'application/xml',\n 'application/javascript',\n 'application/x-javascript',\n])\n\nfunction isTextContentType(contentType: string): boolean {\n const base = (contentType.split(';')[0] ?? '').trim().toLowerCase()\n return base.startsWith('text/') || TEXT_CONTENT_TYPES.has(base)\n}\n\nexport class FilesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasFile[]> {\n return this.client.paginate<CanvasFile>(`/api/v1/courses/${courseId}/files`)\n }\n\n async listFolders(courseId: CanvasId): Promise<CanvasFolder[]> {\n return this.client.paginate<CanvasFolder>(`/api/v1/courses/${courseId}/folders`)\n }\n\n async get(courseId: CanvasId, fileId: CanvasId): Promise<CanvasFile> {\n return this.client.request<CanvasFile>(`/api/v1/courses/${courseId}/files/${fileId}`)\n }\n\n async upload(\n courseId: CanvasId,\n name: string,\n contentBase64: string,\n contentType: string,\n parentFolderPath?: string,\n ): Promise<CanvasFile> {\n const content = Buffer.from(contentBase64, 'base64')\n if (content.toString('base64') !== contentBase64) {\n throw new Error('Invalid base64 content: string is not valid base64 encoding')\n }\n\n // Step 1: Notify Canvas of the pending upload\n const uploadBody: Record<string, string> = {\n name,\n content_type: contentType,\n size: String(content.length),\n }\n if (parentFolderPath) {\n uploadBody.parent_folder_path = parentFolderPath\n }\n\n const uploadInfo = await this.client.request<CanvasFileUploadInfo>(\n `/api/v1/courses/${courseId}/files`,\n { method: 'POST', body: JSON.stringify(uploadBody) },\n )\n\n return this.completeUpload(content, name, contentType, uploadInfo)\n }\n\n async uploadToSubmission(\n courseId: CanvasId,\n assignmentId: CanvasId,\n name: string,\n contentBase64: string,\n contentType: string,\n ): Promise<CanvasFile> {\n const content = Buffer.from(contentBase64, 'base64')\n if (content.toString('base64') !== contentBase64) {\n throw new Error('Invalid base64 content: string is not valid base64 encoding')\n }\n\n const uploadInfo = await this.client.request<CanvasFileUploadInfo>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/self/files`,\n {\n method: 'POST',\n body: JSON.stringify({ name, content_type: contentType, size: String(content.length) }),\n },\n )\n\n return this.completeUpload(content, name, contentType, uploadInfo)\n }\n\n private async completeUpload(\n content: Uint8Array<ArrayBuffer>,\n name: string,\n contentType: string,\n uploadInfo: CanvasFileUploadInfo,\n ): Promise<CanvasFile> {\n // Step 2: POST file to upload_url (may be S3 — must NOT add Authorization header)\n const form = new FormData()\n for (const [key, value] of Object.entries(uploadInfo.upload_params)) {\n form.append(key, value)\n }\n form.append('file', new Blob([content], { type: contentType }), name)\n\n // Self-hosted Canvas instances (local/instfs storage) return a relative upload_url.\n // Resolve it against baseUrl so Node's fetch receives a valid absolute URL.\n const uploadUrl = uploadInfo.upload_url.startsWith('http')\n ? uploadInfo.upload_url\n : new URL(uploadInfo.upload_url, `${this.client.baseUrl}/`).toString()\n\n // Use redirect: 'manual' so we can handle the 303 → Canvas confirm step ourselves\n let s3Response: Response\n try {\n s3Response = await fetch(uploadUrl, {\n method: 'POST',\n body: form,\n redirect: 'manual',\n })\n } catch (networkErr) {\n let hostname: string\n try {\n hostname = new URL(uploadUrl).hostname\n } catch {\n hostname = uploadUrl\n }\n throw new Error(`Unable to reach file storage (${hostname})`, { cause: networkErr })\n }\n\n // Step 3: If S3 returned a redirect, POST to Canvas confirm URL\n if (s3Response.status >= 300 && s3Response.status < 400) {\n const confirmUrl = s3Response.headers.get('location')\n if (!confirmUrl) {\n throw new Error(`File upload redirect missing Location header (HTTP ${s3Response.status})`)\n }\n let confirmHostname: string\n try {\n confirmHostname = new URL(confirmUrl).hostname\n } catch {\n confirmHostname = ''\n }\n const canvasHostname = new URL(this.client.baseUrl).hostname\n if (confirmHostname !== canvasHostname) {\n throw new Error(\n `File upload redirect points to unexpected host (${confirmHostname}); expected Canvas host (${canvasHostname})`,\n )\n }\n return this.client.request<CanvasFile>(confirmUrl, { method: 'POST' })\n }\n\n if (!s3Response.ok) {\n const body = await s3Response.text()\n throw new Error(\n `File upload to storage failed (HTTP ${s3Response.status}): ${body.slice(0, 500)}`,\n )\n }\n\n const responseText = await s3Response.text()\n let parsed: unknown\n try {\n parsed = JSON.parse(responseText)\n } catch (parseErr) {\n throw new Error(\n `File upload response is not valid JSON (${parseErr instanceof Error ? parseErr.message : String(parseErr)}): ${responseText.slice(0, 500)}`,\n { cause: parseErr },\n )\n }\n if (typeof parsed !== 'object' || parsed === null || !('id' in parsed)) {\n throw new Error(\n `File upload returned unexpected response (no file ID): ${responseText.slice(0, 500)}`,\n )\n }\n return parsed as CanvasFile\n }\n\n async download(fileId: CanvasId, courseId?: CanvasId): Promise<DownloadedFile> {\n const endpoint =\n courseId != null ? `/api/v1/courses/${courseId}/files/${fileId}` : `/api/v1/files/${fileId}`\n const meta = await this.client.request<CanvasFile>(endpoint)\n\n if (meta.size > MAX_DOWNLOAD_BYTES) {\n throw new Error(\n `File too large to download (${meta.size.toLocaleString()} bytes; limit is 10 MB)`,\n )\n }\n\n const response = await fetch(meta.url)\n if (!response.ok) {\n throw new Error(`Failed to download file content (HTTP ${response.status})`)\n }\n\n const contentLengthHeader = response.headers.get('content-length')\n if (contentLengthHeader != null) {\n const declared = parseInt(contentLengthHeader, 10)\n if (!isNaN(declared) && declared > MAX_DOWNLOAD_BYTES) {\n throw new Error(\n `File too large to download (${declared.toLocaleString()} bytes declared; limit is 10 MB)`,\n )\n }\n }\n\n const contentType = (\n (response.headers.get('content-type') ?? meta.content_type).split(';')[0] ?? ''\n ).trim()\n const filename = meta.display_name ?? meta.filename ?? `file-${fileId}`\n const buffer = await response.arrayBuffer()\n\n if (buffer.byteLength > MAX_DOWNLOAD_BYTES) {\n throw new Error(\n `File too large to download (${buffer.byteLength.toLocaleString()} bytes; limit is 10 MB)`,\n )\n }\n\n if (isTextContentType(contentType)) {\n return {\n type: 'text',\n filename,\n contentType,\n size: buffer.byteLength,\n text: Buffer.from(buffer).toString('utf-8'),\n }\n }\n\n return {\n type: 'resource',\n filename,\n contentType,\n size: buffer.byteLength,\n base64: Buffer.from(buffer).toString('base64'),\n }\n }\n\n async delete(fileId: CanvasId): Promise<CanvasFile> {\n return this.client.request<CanvasFile>(`/api/v1/files/${fileId}`, { method: 'DELETE' })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasUpcomingEvent, CanvasUser, CanvasUserProfile } from './types'\nimport type { CanvasId } from './id'\n\nexport type CourseUserEnrollmentType = 'student' | 'teacher' | 'ta' | 'observer' | 'designer'\n\nexport type CourseUserEnrollmentState = 'active' | 'invited' | 'rejected' | 'completed' | 'inactive'\n\nexport type CourseUserInclude =\n | 'email'\n | 'enrollments'\n | 'locked'\n | 'avatar_url'\n | 'test_student'\n | 'bio'\n | 'custom_links'\n | 'current_grading_period_scores'\n | 'uuid'\n\nexport type UserSort = 'username' | 'email' | 'sis_id' | 'integration_id' | 'last_login'\n\nexport type SearchUserInclude = 'email' | 'last_login' | 'avatar_url' | 'time_zone' | 'uuid'\n\nexport interface ListCourseUsersOptions {\n enrollment_type?: CourseUserEnrollmentType | ReadonlyArray<CourseUserEnrollmentType>\n enrollment_state?: ReadonlyArray<CourseUserEnrollmentState>\n enrollment_role_id?: CanvasId\n include?: ReadonlyArray<CourseUserInclude>\n user_ids?: ReadonlyArray<CanvasId>\n user_id?: CanvasId\n search_term?: string\n sort?: UserSort\n order?: 'asc' | 'desc'\n}\n\nexport interface SearchUsersOptions {\n sort?: UserSort\n order?: 'asc' | 'desc'\n include?: ReadonlyArray<SearchUserInclude>\n}\n\nexport class UsersModule {\n constructor(private client: CanvasHttpClient) {}\n\n async listStudents(courseId: CanvasId): Promise<CanvasUser[]> {\n return this.client.paginate<CanvasUser>(`/api/v1/courses/${courseId}/users`, {\n enrollment_type: ['student'],\n })\n }\n\n async get(userId: CanvasId): Promise<CanvasUser> {\n return this.client.request<CanvasUser>(`/api/v1/users/${userId}`)\n }\n\n async getSelf(): Promise<CanvasUser> {\n return this.client.request<CanvasUser>('/api/v1/users/self')\n }\n\n async getProfile(): Promise<CanvasUserProfile> {\n return this.client.request<CanvasUserProfile>('/api/v1/users/self/profile')\n }\n\n async searchUsers(\n accountId: CanvasId,\n searchTerm: string,\n opts: SearchUsersOptions = {},\n ): Promise<CanvasUser[]> {\n const params: CanvasQueryParams = { search_term: searchTerm }\n if (opts.sort) params.sort = opts.sort\n if (opts.order) params.order = opts.order\n if (opts.include && opts.include.length > 0) params.include = opts.include\n return this.client.paginate<CanvasUser>(`/api/v1/accounts/${accountId}/users`, params)\n }\n\n async listCourseUsers(\n courseId: CanvasId,\n opts: ListCourseUsersOptions = {},\n ): Promise<CanvasUser[]> {\n const params: CanvasQueryParams = {}\n if (opts.enrollment_type !== undefined) {\n params.enrollment_type = Array.isArray(opts.enrollment_type)\n ? opts.enrollment_type\n : [opts.enrollment_type]\n }\n if (opts.enrollment_state && opts.enrollment_state.length > 0) {\n params.enrollment_state = opts.enrollment_state\n }\n if (opts.enrollment_role_id !== undefined) params.enrollment_role_id = opts.enrollment_role_id\n if (opts.include && opts.include.length > 0) params.include = opts.include\n if (opts.user_ids && opts.user_ids.length > 0) params.user_ids = opts.user_ids\n if (opts.user_id !== undefined) params.user_id = opts.user_id\n if (opts.search_term) params.search_term = opts.search_term\n if (opts.sort) params.sort = opts.sort\n if (opts.order) params.order = opts.order\n return this.client.paginate<CanvasUser>(`/api/v1/courses/${courseId}/users`, params)\n }\n\n async getUpcomingAssignments(): Promise<CanvasUpcomingEvent[]> {\n // Canvas's upcoming_events endpoint ignores a `type` query param entirely (it calls\n // user.upcoming_events with only context_codes) and always returns a mix of calendar\n // events and assignments. Each item's own `type` field (\"event\" | \"assignment\" |\n // \"sub_assignment\", set server-side in assignment_event_json/event_json) is the only\n // reliable discriminator, so filter client-side instead.\n const events = await this.client.request<CanvasUpcomingEvent[]>(\n '/api/v1/users/self/upcoming_events',\n )\n return events.filter((event) => event.type === 'assignment')\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasGroup, CanvasGroupCategory, CanvasGroupMembership, CanvasUser } from './types'\nimport type { CanvasId } from './id'\n\nexport interface CreateGroupCategoryParams {\n name: string\n}\n\nexport interface CreateGroupParams {\n name: string\n description?: string\n join_level?: 'parent_context_auto_join' | 'parent_context_request' | 'invitation_only'\n}\n\nexport class GroupsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasGroup[]> {\n return this.client.paginate<CanvasGroup>(`/api/v1/courses/${courseId}/groups`)\n }\n\n async listMembers(groupId: CanvasId): Promise<CanvasUser[]> {\n return this.client.paginate<CanvasUser>(`/api/v1/groups/${groupId}/users`)\n }\n\n async listCategories(courseId: CanvasId): Promise<CanvasGroupCategory[]> {\n return this.client.paginate<CanvasGroupCategory>(`/api/v1/courses/${courseId}/group_categories`)\n }\n\n /** Canvas renders the created category with no root key. */\n async createCategory(\n courseId: CanvasId,\n params: CreateGroupCategoryParams,\n ): Promise<CanvasGroupCategory> {\n return this.client.request<CanvasGroupCategory>(\n `/api/v1/courses/${courseId}/group_categories`,\n { method: 'POST', body: JSON.stringify(params) },\n )\n }\n\n /**\n * Groups are created inside a group category, so this is addressed by the\n * category ID, not the course ID. Canvas renders the new group with no root\n * key (its `users` list is empty for a new group).\n */\n async create(groupCategoryId: CanvasId, params: CreateGroupParams): Promise<CanvasGroup> {\n return this.client.request<CanvasGroup>(`/api/v1/group_categories/${groupCategoryId}/groups`, {\n method: 'POST',\n body: JSON.stringify(params),\n })\n }\n\n /** Canvas deletes the group, removes its members, and answers with the deleted group. */\n async delete(groupId: CanvasId): Promise<CanvasGroup> {\n return this.client.request<CanvasGroup>(`/api/v1/groups/${groupId}`, { method: 'DELETE' })\n }\n\n /** Returns the membership Canvas creates (or the existing one, if the user is already a member). */\n async addMember(groupId: CanvasId, userId: CanvasId): Promise<CanvasGroupMembership> {\n return this.client.request<CanvasGroupMembership>(`/api/v1/groups/${groupId}/memberships`, {\n method: 'POST',\n body: JSON.stringify({ user_id: userId }),\n })\n }\n\n /** Canvas answers a membership removal with `{ \"ok\": true }`. */\n async removeMember(groupId: CanvasId, userId: CanvasId): Promise<{ ok: boolean }> {\n return this.client.request<{ ok: boolean }>(`/api/v1/groups/${groupId}/users/${userId}`, {\n method: 'DELETE',\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasEnrollment } from './types'\nimport type { CanvasId } from './id'\n\nexport type EnrollmentType =\n | 'StudentEnrollment'\n | 'TeacherEnrollment'\n | 'TaEnrollment'\n | 'DesignerEnrollment'\n | 'ObserverEnrollment'\n\nexport type EnrollmentState =\n | 'active'\n | 'invited'\n | 'creation_pending'\n | 'deleted'\n | 'rejected'\n | 'completed'\n | 'inactive'\n | 'current_and_invited'\n | 'current_and_future'\n | 'current_and_concluded'\n\nexport type EnrollmentInclude =\n | 'avatar_url'\n | 'group_ids'\n | 'locked'\n | 'observed_users'\n | 'can_be_removed'\n | 'uuid'\n | 'current_points'\n | 'grades'\n\nexport interface ListCourseEnrollmentsOptions {\n type?: ReadonlyArray<EnrollmentType>\n role?: ReadonlyArray<string>\n state?: ReadonlyArray<EnrollmentState>\n include?: ReadonlyArray<EnrollmentInclude>\n user_id?: CanvasId\n grading_period_id?: CanvasId\n enrollment_term_id?: CanvasId\n sis_account_id?: ReadonlyArray<string>\n sis_course_id?: ReadonlyArray<string>\n sis_section_id?: ReadonlyArray<string>\n sis_user_id?: ReadonlyArray<string>\n created_for_sis_id?: ReadonlyArray<boolean>\n}\n\nexport interface ListUserEnrollmentsOptions {\n type?: ReadonlyArray<EnrollmentType>\n role?: ReadonlyArray<string>\n state?: ReadonlyArray<EnrollmentState>\n include?: ReadonlyArray<EnrollmentInclude>\n grading_period_id?: CanvasId\n enrollment_term_id?: CanvasId\n}\n\nfunction buildParams(opts: object): CanvasQueryParams {\n const params: CanvasQueryParams = {}\n for (const [key, value] of Object.entries(opts)) {\n if (value === undefined || value === null) continue\n if (Array.isArray(value)) {\n if (value.length === 0) continue\n params[key] = value as ReadonlyArray<string | number | boolean>\n } else if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n params[key] = value\n }\n }\n return params\n}\n\nexport class EnrollmentsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(opts: ListUserEnrollmentsOptions = {}): Promise<CanvasEnrollment[]> {\n return this.client.paginate<CanvasEnrollment>(\n '/api/v1/users/self/enrollments',\n buildParams(opts),\n )\n }\n\n async listForCourse(\n courseId: CanvasId,\n opts: ListCourseEnrollmentsOptions = {},\n ): Promise<CanvasEnrollment[]> {\n return this.client.paginate<CanvasEnrollment>(\n `/api/v1/courses/${courseId}/enrollments`,\n buildParams(opts),\n )\n }\n\n async enroll(\n courseId: CanvasId,\n userId: CanvasId,\n type: string,\n enrollmentState?: string,\n ): Promise<CanvasEnrollment> {\n const body: Record<string, unknown> = {\n enrollment: {\n user_id: userId,\n type,\n ...(enrollmentState ? { enrollment_state: enrollmentState } : {}),\n },\n }\n return this.client.request<CanvasEnrollment>(`/api/v1/courses/${courseId}/enrollments`, {\n method: 'POST',\n body: JSON.stringify(body),\n })\n }\n\n async remove(\n courseId: CanvasId,\n enrollmentId: CanvasId,\n task: string,\n ): Promise<CanvasEnrollment> {\n return this.client.request<CanvasEnrollment>(\n `/api/v1/courses/${courseId}/enrollments/${enrollmentId}?task=${encodeURIComponent(task)}`,\n { method: 'DELETE' },\n )\n }\n\n async listMyGrades(courseId?: CanvasId): Promise<CanvasEnrollment[]> {\n if (courseId !== undefined) {\n return this.client.paginate<CanvasEnrollment>(`/api/v1/courses/${courseId}/enrollments`, {\n user_id: 'self',\n include: ['grades'],\n })\n }\n return this.client.paginate<CanvasEnrollment>('/api/v1/users/self/enrollments', {\n include: ['grades'],\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasDiscussionTopic,\n CanvasDiscussionEntry,\n CanvasAnnouncement,\n CreateDiscussionParams,\n UpdateDiscussionParams,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport class DiscussionsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasDiscussionTopic[]> {\n return this.client.paginate<CanvasDiscussionTopic>(\n `/api/v1/courses/${courseId}/discussion_topics`,\n )\n }\n\n async get(courseId: CanvasId, topicId: CanvasId): Promise<CanvasDiscussionTopic> {\n return this.client.request<CanvasDiscussionTopic>(\n `/api/v1/courses/${courseId}/discussion_topics/${topicId}?include[]=all_dates`,\n )\n }\n\n async listAnnouncements(courseId: CanvasId): Promise<CanvasAnnouncement[]> {\n return this.client.paginate<CanvasAnnouncement>(\n `/api/v1/courses/${courseId}/discussion_topics`,\n { only_announcements: 'true' },\n )\n }\n\n async listForGroup(groupId: CanvasId): Promise<CanvasDiscussionTopic[]> {\n return this.client.paginate<CanvasDiscussionTopic>(\n `/api/v1/groups/${groupId}/discussion_topics`,\n )\n }\n\n async getForGroup(groupId: CanvasId, topicId: CanvasId): Promise<CanvasDiscussionTopic> {\n return this.client.request<CanvasDiscussionTopic>(\n `/api/v1/groups/${groupId}/discussion_topics/${topicId}?include[]=all_dates`,\n )\n }\n\n async postEntry(\n courseId: CanvasId,\n topicId: CanvasId,\n message: string,\n ): Promise<CanvasDiscussionEntry> {\n return this.client.request<CanvasDiscussionEntry>(\n `/api/v1/courses/${courseId}/discussion_topics/${topicId}/entries`,\n {\n method: 'POST',\n body: JSON.stringify({ message }),\n },\n )\n }\n\n async create(courseId: CanvasId, params: CreateDiscussionParams): Promise<CanvasDiscussionTopic> {\n return this.client.request<CanvasDiscussionTopic>(\n `/api/v1/courses/${courseId}/discussion_topics`,\n {\n method: 'POST',\n body: JSON.stringify(params),\n },\n )\n }\n\n async update(\n courseId: CanvasId,\n topicId: CanvasId,\n params: UpdateDiscussionParams,\n ): Promise<CanvasDiscussionTopic> {\n return this.client.request<CanvasDiscussionTopic>(\n `/api/v1/courses/${courseId}/discussion_topics/${topicId}`,\n {\n method: 'PUT',\n body: JSON.stringify(params),\n },\n )\n }\n\n async delete(courseId: CanvasId, topicId: CanvasId): Promise<void> {\n await this.client.request<void>(`/api/v1/courses/${courseId}/discussion_topics/${topicId}`, {\n method: 'DELETE',\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasModule, CanvasModuleItem, CanvasCourseStructure } from './types'\nimport type { CanvasId } from './id'\n\nexport interface CreateModuleItemParams {\n title: string\n type: string\n content_id?: CanvasId\n /** Page slug — Page items are addressed by URL slug, not by `content_id`. */\n page_url?: string\n external_url?: string\n position?: number\n indent?: number\n new_tab?: boolean\n}\n\nexport interface UpdateModuleItemParams {\n title?: string\n external_url?: string\n position?: number\n indent?: number\n new_tab?: boolean\n published?: boolean\n /** Move the item to a different module. */\n module_id?: CanvasId\n}\n\nexport class ModulesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasModule[]> {\n return this.client.paginate<CanvasModule>(`/api/v1/courses/${courseId}/modules`)\n }\n\n /**\n * List a course's modules with each module's items inlined via `include[]=items`.\n * Unlike `getCourseStructure`, this returns the raw Canvas shapes (including the\n * `published` state on every module and item, even unpublished ones), which the\n * course-setup health check needs to detect unpublished content.\n */\n async listWithItems(\n courseId: CanvasId,\n ): Promise<(CanvasModule & { items?: CanvasModuleItem[] })[]> {\n return this.client.paginate<CanvasModule & { items?: CanvasModuleItem[] }>(\n `/api/v1/courses/${courseId}/modules`,\n { include: ['items'] },\n )\n }\n\n async get(courseId: CanvasId, moduleId: CanvasId): Promise<CanvasModule> {\n return this.client.request<CanvasModule>(`/api/v1/courses/${courseId}/modules/${moduleId}`)\n }\n\n async listItems(courseId: CanvasId, moduleId: CanvasId): Promise<CanvasModuleItem[]> {\n return this.client.paginate<CanvasModuleItem>(\n `/api/v1/courses/${courseId}/modules/${moduleId}/items`,\n )\n }\n\n async create(\n courseId: CanvasId,\n params: {\n name: string\n position?: number\n unlock_at?: string\n prerequisite_module_ids?: CanvasId[]\n },\n ): Promise<CanvasModule> {\n return this.client.request<CanvasModule>(`/api/v1/courses/${courseId}/modules`, {\n method: 'POST',\n body: JSON.stringify({ module: params }),\n })\n }\n\n async update(\n courseId: CanvasId,\n moduleId: CanvasId,\n params: { name?: string; position?: number; published?: boolean },\n ): Promise<CanvasModule> {\n return this.client.request<CanvasModule>(`/api/v1/courses/${courseId}/modules/${moduleId}`, {\n method: 'PUT',\n body: JSON.stringify({ module: params }),\n })\n }\n\n /**\n * Canvas answers a module DELETE with the deleted module's body (the\n * controller renders `module_json` with no root key). The module's items go\n * with it; the content they link to is stored separately in the course.\n */\n async delete(courseId: CanvasId, moduleId: CanvasId): Promise<CanvasModule> {\n return this.client.request<CanvasModule>(`/api/v1/courses/${courseId}/modules/${moduleId}`, {\n method: 'DELETE',\n })\n }\n\n async createItem(\n courseId: CanvasId,\n moduleId: CanvasId,\n params: CreateModuleItemParams,\n ): Promise<CanvasModuleItem> {\n return this.client.request<CanvasModuleItem>(\n `/api/v1/courses/${courseId}/modules/${moduleId}/items`,\n {\n method: 'POST',\n body: JSON.stringify({ module_item: params }),\n },\n )\n }\n\n async updateItem(\n courseId: CanvasId,\n moduleId: CanvasId,\n itemId: CanvasId,\n params: UpdateModuleItemParams,\n ): Promise<CanvasModuleItem> {\n return this.client.request<CanvasModuleItem>(\n `/api/v1/courses/${courseId}/modules/${moduleId}/items/${itemId}`,\n {\n method: 'PUT',\n body: JSON.stringify({ module_item: params }),\n },\n )\n }\n\n /** Canvas answers a module-item DELETE with the deleted item's body. */\n async deleteItem(\n courseId: CanvasId,\n moduleId: CanvasId,\n itemId: CanvasId,\n ): Promise<CanvasModuleItem> {\n return this.client.request<CanvasModuleItem>(\n `/api/v1/courses/${courseId}/modules/${moduleId}/items/${itemId}`,\n { method: 'DELETE' },\n )\n }\n\n async getCourseStructure(\n courseId: CanvasId,\n opts: { includePublishedOnly?: boolean; includeContentDetails?: boolean } = {},\n ): Promise<CanvasCourseStructure> {\n const include: string[] = ['items']\n if (opts.includeContentDetails) include.push('content_details')\n\n const modules = await this.client.paginate<CanvasModule & { items?: CanvasModuleItem[] }>(\n `/api/v1/courses/${courseId}/modules`,\n { include },\n )\n\n const itemsByType: Record<string, number> = {}\n let totalItems = 0\n\n const filteredModules = modules.map((mod) => {\n let items = mod.items ?? []\n if (opts.includePublishedOnly) {\n items = items.filter((item) => item.published)\n }\n for (const item of items) {\n itemsByType[item.type] = (itemsByType[item.type] ?? 0) + 1\n totalItems++\n }\n return {\n id: mod.id,\n name: mod.name,\n position: mod.position,\n state: mod.state ?? (mod.published ? 'active' : 'unpublished'),\n unlock_at: mod.unlock_at,\n items: items.map((item) => ({\n id: item.id,\n title: item.title,\n type: item.type,\n position: item.position,\n published: item.published,\n html_url: item.html_url,\n page_url: item.page_url,\n content_id: item.content_id,\n content_details: item.content_details,\n })),\n }\n })\n\n return {\n modules: filteredModules,\n summary: {\n total_modules: filteredModules.length,\n total_items: totalItems,\n items_by_type: itemsByType,\n },\n }\n }\n}\n","/**\n * Run `fn` across `items` with at most `limit` invocations in flight at once,\n * returning the results in the SAME order as `items` regardless of the order in\n * which the individual calls settle (`results[i]` always corresponds to\n * `items[i]`).\n *\n * This is the single shared implementation of the batched fan-out first written\n * inline for `find_student_across_courses` (PR #252). Whole-course scans such as\n * {@link PagesModule.listWithBodies} would otherwise issue one Canvas request per\n * item all at once — a 300-page course means 300 simultaneous requests with no\n * backstop. Bounding the fan-out keeps a large course from hammering Canvas.\n *\n * Error semantics match a plain `Promise.all`: the first rejection propagates\n * unchanged and no further batches are started. Call sites that want to tolerate\n * per-item failures (like `find_student_across_courses`) do so by catching inside\n * `fn` and resolving with a discriminated result, never by letting `fn` throw.\n */\nexport async function mapWithConcurrency<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n if (limit < 1) {\n throw new RangeError(`mapWithConcurrency: limit must be >= 1, got ${limit}`)\n }\n\n const results = new Array<R>(items.length)\n for (let start = 0; start < items.length; start += limit) {\n const batch = items.slice(start, start + limit)\n const settled = await Promise.all(batch.map((item, offset) => fn(item, start + offset)))\n settled.forEach((result, offset) => {\n results[start + offset] = result\n })\n }\n return results\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasPage } from './types'\nimport { mapWithConcurrency } from './concurrency'\nimport type { CanvasId } from './id'\n\n/**\n * Max concurrent page-body fetches issued by {@link PagesModule.listWithBodies}.\n * Matches the `CONCURRENT_COURSE_LIMIT` precedent in `student-search.ts`; kept\n * as a named export so the cap is greppable and adjustable in one place.\n */\nexport const PAGE_BODY_CONCURRENCY_LIMIT = 10\n\nexport class PagesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasPage[]> {\n return this.client.paginate<CanvasPage>(`/api/v1/courses/${courseId}/pages`)\n }\n\n async get(courseId: CanvasId, pageUrl: string): Promise<CanvasPage> {\n return this.client.request<CanvasPage>(\n `/api/v1/courses/${courseId}/pages/${encodeURIComponent(pageUrl)}`,\n )\n }\n\n /**\n * List every page in a course with its full HTML `body`. The paginated list\n * endpoint returns page stubs without `body`, so this fans out a `get()` per\n * page, bounded to at most {@link PAGE_BODY_CONCURRENCY_LIMIT} concurrent\n * fetches so a large course does not open one Canvas request per page all at\n * once. Result order matches the listed page order. Used by the link-audit and\n * accessibility-audit tools to scan page content. `CanvasApiError` from either\n * the list or any individual fetch propagates unchanged.\n */\n async listWithBodies(courseId: CanvasId): Promise<CanvasPage[]> {\n const stubs = await this.client.paginate<CanvasPage>(`/api/v1/courses/${courseId}/pages`)\n return mapWithConcurrency(stubs, PAGE_BODY_CONCURRENCY_LIMIT, (stub) =>\n this.get(courseId, stub.url),\n )\n }\n\n async create(\n courseId: CanvasId,\n params: { title: string; body?: string; published?: boolean; editing_roles?: string },\n ): Promise<CanvasPage> {\n return this.client.request<CanvasPage>(`/api/v1/courses/${courseId}/pages`, {\n method: 'POST',\n body: JSON.stringify({ wiki_page: params }),\n })\n }\n\n async update(\n courseId: CanvasId,\n pageUrl: string,\n params: { title?: string; body?: string; published?: boolean; editing_roles?: string },\n ): Promise<CanvasPage> {\n return this.client.request<CanvasPage>(\n `/api/v1/courses/${courseId}/pages/${encodeURIComponent(pageUrl)}`,\n {\n method: 'PUT',\n body: JSON.stringify({ wiki_page: params }),\n },\n )\n }\n\n async delete(courseId: CanvasId, pageUrl: string): Promise<void> {\n await this.client.request<void>(\n `/api/v1/courses/${courseId}/pages/${encodeURIComponent(pageUrl)}`,\n { method: 'DELETE' },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasCalendarEvent } from './types'\nimport type { CanvasId } from './id'\n\nexport type CalendarEventType = 'event' | 'assignment' | 'sub_assignment'\n\nexport interface ListCalendarEventsOptions {\n type?: CalendarEventType\n start_date?: string\n end_date?: string\n}\n\nexport class CalendarModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(\n courseId: CanvasId,\n opts: ListCalendarEventsOptions = {},\n ): Promise<CanvasCalendarEvent[]> {\n const params: CanvasQueryParams = {\n 'context_codes[]': `course_${courseId}`,\n }\n if (opts.type) params.type = opts.type\n if (opts.start_date) params.start_date = opts.start_date\n if (opts.end_date) params.end_date = opts.end_date\n return this.client.paginate<CanvasCalendarEvent>('/api/v1/calendar_events', params)\n }\n\n async createEvent(params: {\n context_code: string\n title: string\n start_at: string\n end_at?: string\n description?: string\n location_name?: string\n }): Promise<CanvasCalendarEvent> {\n return this.client.request<CanvasCalendarEvent>('/api/v1/calendar_events', {\n method: 'POST',\n body: JSON.stringify({ calendar_event: params }),\n })\n }\n\n async updateEvent(\n eventId: CanvasId,\n params: {\n title?: string\n start_at?: string\n end_at?: string\n description?: string\n location_name?: string\n },\n ): Promise<CanvasCalendarEvent> {\n return this.client.request<CanvasCalendarEvent>(`/api/v1/calendar_events/${eventId}`, {\n method: 'PUT',\n body: JSON.stringify({ calendar_event: params }),\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasConversation,\n CanvasConversationDetail,\n CanvasConversationUnreadCount,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport class ConversationsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(): Promise<CanvasConversation[]> {\n return this.client.paginate<CanvasConversation>('/api/v1/conversations')\n }\n\n async get(conversationId: CanvasId): Promise<CanvasConversationDetail> {\n return this.client.request<CanvasConversationDetail>(`/api/v1/conversations/${conversationId}`)\n }\n\n async getUnreadCount(): Promise<CanvasConversationUnreadCount> {\n const raw = await this.client.request<{ unread_count: string }>(\n '/api/v1/conversations/unread_count',\n )\n const count = parseInt(raw.unread_count, 10)\n if (Number.isNaN(count)) {\n throw new Error(\n `Canvas returned unexpected unread_count value: ${JSON.stringify(raw.unread_count)}`,\n )\n }\n return { unread_count: count }\n }\n\n async send(recipients: string[], subject: string, body: string): Promise<CanvasConversation[]> {\n return this.client.request<CanvasConversation[]>('/api/v1/conversations', {\n method: 'POST',\n body: JSON.stringify({\n recipients,\n subject,\n body,\n }),\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasPeerReview } from './types'\nimport type { CanvasId } from './id'\n\nexport class PeerReviewsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async listForAssignment(courseId: CanvasId, assignmentId: CanvasId): Promise<CanvasPeerReview[]> {\n return this.client.paginate<CanvasPeerReview>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/peer_reviews`,\n )\n }\n\n async listForSubmission(\n courseId: CanvasId,\n assignmentId: CanvasId,\n submissionId: CanvasId,\n ): Promise<CanvasPeerReview[]> {\n return this.client.paginate<CanvasPeerReview>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${submissionId}/peer_reviews`,\n )\n }\n\n async create(\n courseId: CanvasId,\n assignmentId: CanvasId,\n submissionId: CanvasId,\n userId: CanvasId,\n ): Promise<CanvasPeerReview> {\n return this.client.request<CanvasPeerReview>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${submissionId}/peer_reviews`,\n {\n method: 'POST',\n body: JSON.stringify({ user_id: userId }),\n },\n )\n }\n\n async delete(\n courseId: CanvasId,\n assignmentId: CanvasId,\n submissionId: CanvasId,\n userId: CanvasId,\n ): Promise<void> {\n await this.client.request<void>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${submissionId}/peer_reviews?user_id=${userId}`,\n { method: 'DELETE' },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasAccount,\n CanvasAccountNotification,\n CanvasAccountReport,\n CanvasCourse,\n CanvasUser,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport class AccountsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async get(accountId: CanvasId): Promise<CanvasAccount> {\n return this.client.request<CanvasAccount>(`/api/v1/accounts/${accountId}`)\n }\n\n async list(): Promise<CanvasAccount[]> {\n return this.client.paginate<CanvasAccount>('/api/v1/accounts')\n }\n\n async listSubAccounts(accountId: CanvasId): Promise<CanvasAccount[]> {\n return this.client.paginate<CanvasAccount>(`/api/v1/accounts/${accountId}/sub_accounts`)\n }\n\n async listCourses(\n accountId: CanvasId,\n params?: { search_term?: string },\n ): Promise<CanvasCourse[]> {\n const query: Record<string, string> = {}\n if (params?.search_term) query.search_term = params.search_term\n return this.client.paginate<CanvasCourse>(\n `/api/v1/accounts/${accountId}/courses`,\n Object.keys(query).length ? query : undefined,\n )\n }\n\n async listUsers(accountId: CanvasId, params?: { search_term?: string }): Promise<CanvasUser[]> {\n const query: Record<string, string> = {}\n if (params?.search_term) query.search_term = params.search_term\n return this.client.paginate<CanvasUser>(\n `/api/v1/accounts/${accountId}/users`,\n Object.keys(query).length ? query : undefined,\n )\n }\n\n async getReports(accountId: CanvasId): Promise<CanvasAccountReport[]> {\n return this.client.request<CanvasAccountReport[]>(`/api/v1/accounts/${accountId}/reports`)\n }\n\n async listNotifications(accountId: string): Promise<CanvasAccountNotification[]> {\n return this.client.paginate<CanvasAccountNotification>(\n `/api/v1/accounts/${accountId}/account_notifications`,\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasAssignmentAnalytics,\n CanvasCourseActivitySummary,\n CanvasStudentActivitySummary,\n CanvasActivityStreamItem,\n CanvasStudentSummary,\n CourseSearchResult,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport const SEARCH_CONTENT_TYPES = [\n 'pages',\n 'discussions',\n 'assignments',\n 'announcements',\n] as const\nexport type SearchContentType = (typeof SEARCH_CONTENT_TYPES)[number]\n\nexport class AnalyticsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async searchContentType(\n courseId: CanvasId,\n searchTerm: string,\n type: SearchContentType,\n ): Promise<CourseSearchResult[]> {\n switch (type) {\n case 'pages': {\n const pages = await this.client.paginate<{ page_id: CanvasId; title: string; url: string }>(\n `/api/v1/courses/${courseId}/pages`,\n { search_term: searchTerm },\n )\n return pages.map((p) => ({\n id: p.page_id,\n title: p.title,\n type: 'page' as const,\n url: p.url,\n course_id: courseId,\n }))\n }\n case 'assignments': {\n const assignments = await this.client.paginate<{ id: CanvasId; name: string }>(\n `/api/v1/courses/${courseId}/assignments`,\n { search_term: searchTerm },\n )\n return assignments.map((a) => ({\n id: a.id,\n title: a.name,\n type: 'assignment' as const,\n course_id: courseId,\n }))\n }\n case 'discussions': {\n const discussions = await this.client.paginate<{ id: CanvasId; title: string }>(\n `/api/v1/courses/${courseId}/discussion_topics`,\n { search_term: searchTerm },\n )\n return discussions.map((d) => ({\n id: d.id,\n title: d.title,\n type: 'discussion' as const,\n course_id: courseId,\n }))\n }\n case 'announcements': {\n const announcements = await this.client.paginate<{ id: CanvasId; title: string }>(\n `/api/v1/courses/${courseId}/discussion_topics`,\n { search_term: searchTerm, only_announcements: 'true' },\n )\n return announcements.map((a) => ({\n id: a.id,\n title: a.title,\n type: 'announcement' as const,\n course_id: courseId,\n }))\n }\n default: {\n const _exhaustive: never = type\n throw new Error(`Unknown content type: ${_exhaustive}`)\n }\n }\n }\n\n async getCourseActivity(courseId: CanvasId): Promise<CanvasCourseActivitySummary[]> {\n return this.client.request<CanvasCourseActivitySummary[]>(\n `/api/v1/courses/${courseId}/analytics/activity`,\n )\n }\n\n async getStudentActivity(\n courseId: CanvasId,\n studentId: CanvasId,\n ): Promise<CanvasStudentActivitySummary> {\n return this.client.request<CanvasStudentActivitySummary>(\n `/api/v1/courses/${courseId}/analytics/users/${studentId}/activity`,\n )\n }\n\n async getCourseActivityStream(courseId: CanvasId): Promise<CanvasActivityStreamItem[]> {\n return this.client.request<CanvasActivityStreamItem[]>(\n `/api/v1/courses/${courseId}/activity_stream/summary`,\n )\n }\n\n async getStudentSummaries(courseId: CanvasId): Promise<CanvasStudentSummary[]> {\n return this.client.paginate<CanvasStudentSummary>(\n `/api/v1/courses/${courseId}/analytics/student_summaries`,\n )\n }\n\n async getAssignmentAnalytics(courseId: CanvasId): Promise<CanvasAssignmentAnalytics[]> {\n return this.client.paginate<CanvasAssignmentAnalytics>(\n `/api/v1/courses/${courseId}/analytics/assignments`,\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasDashboardCard,\n CanvasTodoItem,\n CanvasUpcomingEvent,\n CanvasMissingSubmission,\n} from './types'\n\nexport class DashboardModule {\n constructor(private client: CanvasHttpClient) {}\n\n async getDashboardCards(): Promise<CanvasDashboardCard[]> {\n return this.client.request<CanvasDashboardCard[]>('/api/v1/dashboard/dashboard_cards')\n }\n\n async getTodoItems(): Promise<CanvasTodoItem[]> {\n return this.client.paginate<CanvasTodoItem>('/api/v1/users/self/todo')\n }\n\n async getUpcomingEvents(): Promise<CanvasUpcomingEvent[]> {\n return this.client.request<CanvasUpcomingEvent[]>('/api/v1/users/self/upcoming_events')\n }\n\n async getMissingSubmissions(): Promise<CanvasMissingSubmission[]> {\n return this.client.paginate<CanvasMissingSubmission>('/api/v1/users/self/missing_submissions')\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasOutcomeContextType,\n CanvasOutcome,\n CanvasOutcomeAlignment,\n CanvasOutcomeContributingScoresResponse,\n CanvasOutcomeGroup,\n CanvasOutcomeLink,\n CanvasOutcomeMasteryDistributionResponse,\n CanvasOutcomeResultsResponse,\n CanvasOutcomeRollupsResponse,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport const OUTCOME_CONTEXT_TYPES = ['account', 'course'] as const\nexport const OUTCOME_DETAIL_LEVELS = ['abbrev', 'full'] as const\nexport const OUTCOME_ROLLUP_AGGREGATE_STATS = ['mean', 'median'] as const\nexport const OUTCOME_ROLLUP_SORT_BY = ['student', 'outcome'] as const\nexport const OUTCOME_SORT_ORDER = ['asc', 'desc'] as const\nexport const OUTCOME_EXCLUDE_OPTIONS = ['missing_user_rollups', 'missing_outcome_results'] as const\n\ntype ContextPathSuffix = 'outcome_groups' | 'outcome_group_links' | 'root_outcome_group'\n\nexport class OutcomesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async getRootOutcomeGroup(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n ): Promise<CanvasOutcomeGroup> {\n return this.client.request<CanvasOutcomeGroup>(this.buildContextPath(contextType, contextId))\n }\n\n async listOutcomeGroups(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n ): Promise<CanvasOutcomeGroup[]> {\n return this.client.paginate<CanvasOutcomeGroup>(\n this.buildContextPath(contextType, contextId, 'outcome_groups'),\n )\n }\n\n async listOutcomeGroupLinks(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n options?: { outcome_style?: 'abbrev' | 'full'; outcome_group_style?: 'abbrev' | 'full' },\n ): Promise<CanvasOutcomeLink[]> {\n return this.client.paginate<CanvasOutcomeLink>(\n this.withQuery(this.buildContextPath(contextType, contextId, 'outcome_group_links'), {\n outcome_style: options?.outcome_style,\n outcome_group_style: options?.outcome_group_style,\n }),\n )\n }\n\n async getOutcomeGroup(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n groupId: CanvasId,\n ): Promise<CanvasOutcomeGroup> {\n return this.client.request<CanvasOutcomeGroup>(\n `${this.buildContextPath(contextType, contextId, 'outcome_groups')}/${groupId}`,\n )\n }\n\n async listGroupOutcomes(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n groupId: CanvasId,\n options?: { outcome_style?: 'abbrev' | 'full' },\n ): Promise<CanvasOutcomeLink[]> {\n return this.client.paginate<CanvasOutcomeLink>(\n this.withQuery(\n `${this.buildContextPath(contextType, contextId, 'outcome_groups')}/${groupId}/outcomes`,\n { outcome_style: options?.outcome_style },\n ),\n )\n }\n\n async listGroupSubgroups(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n groupId: CanvasId,\n ): Promise<CanvasOutcomeGroup[]> {\n return this.client.paginate<CanvasOutcomeGroup>(\n `${this.buildContextPath(contextType, contextId, 'outcome_groups')}/${groupId}/subgroups`,\n )\n }\n\n async getOutcome(\n outcomeId: CanvasId,\n options?: { add_defaults?: boolean },\n ): Promise<CanvasOutcome> {\n return this.client.request<CanvasOutcome>(\n this.withQuery(`/api/v1/outcomes/${outcomeId}`, {\n add_defaults: options?.add_defaults,\n }),\n )\n }\n\n async getOutcomeAlignments(\n courseId: CanvasId,\n options?: { student_id?: CanvasId; assignment_id?: CanvasId },\n ): Promise<CanvasOutcomeAlignment[]> {\n return this.client.request<CanvasOutcomeAlignment[]>(\n this.withQuery(`/api/v1/courses/${courseId}/outcome_alignments`, {\n student_id: options?.student_id,\n assignment_id: options?.assignment_id,\n }),\n )\n }\n\n async getOutcomeResults(\n courseId: CanvasId,\n options?: {\n user_ids?: Array<CanvasId>\n outcome_ids?: CanvasId[]\n include_alignments?: boolean\n include_hidden?: boolean\n },\n ): Promise<CanvasOutcomeResultsResponse> {\n return this.client.request<CanvasOutcomeResultsResponse>(\n this.withQuery(`/api/v1/courses/${courseId}/outcome_results`, {\n 'user_ids[]': options?.user_ids,\n 'outcome_ids[]': options?.outcome_ids,\n 'include[]': options?.include_alignments ? ['alignments'] : undefined,\n include_hidden: options?.include_hidden,\n }),\n )\n }\n\n async getOutcomeRollups(\n courseId: CanvasId,\n options?: {\n aggregate?: 'course'\n aggregate_stat?: 'mean' | 'median'\n user_ids?: Array<CanvasId>\n outcome_ids?: CanvasId[]\n include_courses?: boolean\n exclude?: Array<'missing_user_rollups' | 'missing_outcome_results'>\n sort_by?: 'student' | 'outcome'\n sort_outcome_id?: CanvasId\n sort_order?: 'asc' | 'desc'\n add_defaults?: boolean\n },\n ): Promise<CanvasOutcomeRollupsResponse> {\n return this.client.request<CanvasOutcomeRollupsResponse>(\n this.withQuery(`/api/v1/courses/${courseId}/outcome_rollups`, {\n aggregate: options?.aggregate,\n aggregate_stat: options?.aggregate_stat,\n 'user_ids[]': options?.user_ids,\n 'outcome_ids[]': options?.outcome_ids,\n 'include[]': options?.include_courses ? ['courses'] : undefined,\n 'exclude[]': options?.exclude,\n sort_by: options?.sort_by,\n sort_outcome_id: options?.sort_outcome_id,\n sort_order: options?.sort_order,\n add_defaults: options?.add_defaults,\n }),\n )\n }\n\n async getOutcomeContributingScores(\n courseId: CanvasId,\n outcomeId: CanvasId,\n options?: {\n user_ids?: Array<CanvasId>\n only_assignment_alignments?: boolean\n show_unpublished_assignments?: boolean\n },\n ): Promise<CanvasOutcomeContributingScoresResponse> {\n return this.client.request<CanvasOutcomeContributingScoresResponse>(\n this.withQuery(`/api/v1/courses/${courseId}/outcomes/${outcomeId}/contributing_scores`, {\n 'user_ids[]': options?.user_ids,\n only_assignment_alignments: options?.only_assignment_alignments,\n show_unpublished_assignments: options?.show_unpublished_assignments,\n }),\n )\n }\n\n async getOutcomeMasteryDistribution(\n courseId: CanvasId,\n options?: {\n exclude?: Array<'missing_user_rollups' | 'missing_outcome_results'>\n outcome_ids?: CanvasId[]\n student_ids?: Array<CanvasId>\n include_alignment_distributions?: boolean\n only_assignment_alignments?: boolean\n show_unpublished_assignments?: boolean\n add_defaults?: boolean\n },\n ): Promise<CanvasOutcomeMasteryDistributionResponse> {\n return this.client.request<CanvasOutcomeMasteryDistributionResponse>(\n this.withQuery(`/api/v1/courses/${courseId}/outcome_mastery_distribution`, {\n 'exclude[]': options?.exclude,\n 'outcome_ids[]': options?.outcome_ids,\n 'student_ids[]': options?.student_ids,\n 'include[]': options?.include_alignment_distributions\n ? ['alignment_distributions']\n : undefined,\n only_assignment_alignments: options?.only_assignment_alignments,\n show_unpublished_assignments: options?.show_unpublished_assignments,\n add_defaults: options?.add_defaults,\n }),\n )\n }\n\n private buildContextPath(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n suffix: ContextPathSuffix = 'root_outcome_group',\n ): string {\n const segment = contextType === 'account' ? 'accounts' : 'courses'\n return `/api/v1/${segment}/${contextId}/${suffix}`\n }\n\n private withQuery(\n endpoint: string,\n params: Record<string, string | number | boolean | Array<string | number> | undefined>,\n ): string {\n const searchParams = new URLSearchParams()\n\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined) continue\n if (Array.isArray(value)) {\n for (const item of value) searchParams.append(key, String(item))\n continue\n }\n searchParams.set(key, String(value))\n }\n\n const query = searchParams.toString()\n return query ? `${endpoint}?${query}` : endpoint\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasGradebookHistoryDay,\n CanvasGradebookHistoryGrader,\n CanvasGradebookHistorySubmission,\n CanvasGradebookHistorySubmissionVersion,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport interface GradebookHistoryFeedParams {\n assignment_id?: CanvasId\n user_id?: CanvasId\n ascending?: boolean\n}\n\nexport class GradebookHistoryModule {\n constructor(private client: CanvasHttpClient) {}\n\n async listDays(courseId: CanvasId): Promise<CanvasGradebookHistoryDay[]> {\n return this.client.request<CanvasGradebookHistoryDay[]>(\n `/api/v1/courses/${courseId}/gradebook_history/days`,\n )\n }\n\n async getDay(courseId: CanvasId, date: string): Promise<CanvasGradebookHistoryGrader[]> {\n return this.client.request<CanvasGradebookHistoryGrader[]>(\n `/api/v1/courses/${courseId}/gradebook_history/${encodeURIComponent(date)}`,\n )\n }\n\n async listSubmissions(\n courseId: CanvasId,\n date: string,\n graderId: CanvasId,\n assignmentId: CanvasId,\n ): Promise<CanvasGradebookHistorySubmission[]> {\n return this.client.request<CanvasGradebookHistorySubmission[]>(\n `/api/v1/courses/${courseId}/gradebook_history/${encodeURIComponent(date)}/graders/${graderId}/assignments/${assignmentId}/submissions`,\n )\n }\n\n async getFeed(\n courseId: CanvasId,\n params: GradebookHistoryFeedParams = {},\n ): Promise<CanvasGradebookHistorySubmissionVersion[]> {\n const searchParams = new URLSearchParams()\n\n if (params.assignment_id !== undefined) {\n searchParams.set('assignment_id', String(params.assignment_id))\n }\n if (params.user_id !== undefined) {\n searchParams.set('user_id', String(params.user_id))\n }\n if (params.ascending !== undefined) {\n searchParams.set('ascending', String(params.ascending))\n }\n\n const query = searchParams.toString()\n\n return this.client.paginate<CanvasGradebookHistorySubmissionVersion>(\n `/api/v1/courses/${courseId}/gradebook_history/feed${query ? `?${query}` : ''}`,\n )\n }\n}\n","import { CanvasApiError, type CanvasHttpClient } from './client'\nimport type { CanvasNewQuiz, CanvasNewQuizAccommodation, CanvasNewQuizItem } from './types'\nimport type { CanvasId } from './id'\n\nexport interface NewQuizPayload {\n title?: string\n instructions?: string | null\n points_possible?: number\n due_at?: string | null\n unlock_at?: string | null\n lock_at?: string | null\n published?: boolean\n}\n\ntype ChoiceItem = {\n interaction_type_slug: 'choice'\n title?: string\n item_body: string\n choices: Array<{ id: string; item_body: string }>\n correct_choice_id: string\n}\n\ntype TrueFalseItem = {\n interaction_type_slug: 'true-false'\n item_body: string\n correct_answer: boolean\n}\n\ntype EssayItem = {\n interaction_type_slug: 'essay'\n item_body: string\n rich_text?: boolean\n word_count_min?: number\n word_count_max?: number\n}\n\ntype MatchingItem = {\n interaction_type_slug: 'matching'\n item_body: string\n matches: Array<{ question: string; answer: string }>\n distractors?: string[]\n}\n\ntype NumericAnswer =\n | { kind: 'exact'; value: number; margin?: number }\n | { kind: 'range'; min: number; max: number }\n | { kind: 'precision'; value: number; precision: number }\n\ntype NumericItem = {\n interaction_type_slug: 'numeric'\n item_body: string\n answers: NumericAnswer[]\n}\n\nexport type NewQuizItemInput = ChoiceItem | TrueFalseItem | EssayItem | MatchingItem | NumericItem\n\nexport interface NewQuizItemCreatePayload {\n position?: number\n points_possible: number\n item: NewQuizItemInput\n}\n\nexport interface NewQuizItemUpdatePayload {\n position?: number\n points_possible?: number\n item?: NewQuizItemInput\n}\n\nexport class NewQuizzesModule {\n constructor(private client: CanvasHttpClient) {}\n\n /**\n * The New Quizzes API expects quiz fields nested under a `quiz` key\n * (`quiz[title]`, `quiz[published]`, …), the same shape the Classic quizzes\n * endpoint uses. A flat body is rejected with `400 quiz is missing`.\n */\n async create(courseId: CanvasId, payload: NewQuizPayload): Promise<CanvasNewQuiz> {\n return this.client.request<CanvasNewQuiz>(`/api/quiz/v1/courses/${courseId}/quizzes`, {\n method: 'POST',\n body: JSON.stringify({ quiz: payload }),\n })\n }\n\n async update(\n courseId: CanvasId,\n assignmentId: CanvasId,\n patch: NewQuizPayload,\n ): Promise<CanvasNewQuiz> {\n return this.client.request<CanvasNewQuiz>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}`,\n { method: 'PATCH', body: JSON.stringify({ quiz: patch }) },\n )\n }\n\n async delete(courseId: CanvasId, assignmentId: CanvasId): Promise<void> {\n await this.client.request<void>(`/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}`, {\n method: 'DELETE',\n })\n }\n\n async listItems(courseId: CanvasId, assignmentId: CanvasId): Promise<CanvasNewQuizItem[]> {\n return this.client.paginate<CanvasNewQuizItem>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/items`,\n )\n }\n\n async getItem(\n courseId: CanvasId,\n assignmentId: CanvasId,\n itemId: string,\n ): Promise<CanvasNewQuizItem> {\n return this.client.request<CanvasNewQuizItem>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/items/${itemId}`,\n )\n }\n\n /**\n * The New Quizzes Items API expects item fields nested under an `item` key\n * (`item[entry_type]`, `item[points_possible]`, `item[entry][...]`). Flat\n * fields would not be bound to the `item` parameter.\n */\n async createItem(\n courseId: CanvasId,\n assignmentId: CanvasId,\n input: NewQuizItemCreatePayload,\n ): Promise<CanvasNewQuizItem> {\n const wireItem: Record<string, unknown> = {\n entry_type: 'Item',\n points_possible: input.points_possible,\n entry: this.toWireItem(input.item),\n }\n if (input.position !== undefined) wireItem.position = input.position\n return this.client.request<CanvasNewQuizItem>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/items`,\n { method: 'POST', body: JSON.stringify({ item: wireItem }) },\n )\n }\n\n async updateItem(\n courseId: CanvasId,\n assignmentId: CanvasId,\n itemId: string,\n patch: NewQuizItemUpdatePayload,\n ): Promise<CanvasNewQuizItem> {\n const wireItem: Record<string, unknown> = {}\n if (patch.position !== undefined) wireItem.position = patch.position\n if (patch.points_possible !== undefined) wireItem.points_possible = patch.points_possible\n if (patch.item !== undefined) wireItem.entry = this.toWireItem(patch.item)\n return this.client.request<CanvasNewQuizItem>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/items/${itemId}`,\n { method: 'PATCH', body: JSON.stringify({ item: wireItem }) },\n )\n }\n\n async deleteItem(courseId: CanvasId, assignmentId: CanvasId, itemId: string): Promise<void> {\n await this.client.request<void>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/items/${itemId}`,\n { method: 'DELETE' },\n )\n }\n\n async setAccommodation(\n courseId: CanvasId,\n userId: CanvasId,\n timeMultiplier?: number,\n extraAttempts?: number,\n ): Promise<CanvasNewQuizAccommodation> {\n const body: Record<string, unknown> = { user_id: userId }\n if (timeMultiplier !== undefined) body.time_multiplier = timeMultiplier\n if (extraAttempts !== undefined) body.extra_attempts = extraAttempts\n return this.client.request<CanvasNewQuizAccommodation>(\n `/api/quiz/v1/courses/${courseId}/accommodations`,\n { method: 'POST', body: JSON.stringify(body) },\n )\n }\n\n async setQuizAccommodation(\n courseId: CanvasId,\n assignmentId: CanvasId,\n userId: CanvasId,\n timeMultiplier?: number,\n extraAttempts?: number,\n ): Promise<CanvasNewQuizAccommodation> {\n const body: Record<string, unknown> = { user_id: userId }\n if (timeMultiplier !== undefined) body.time_multiplier = timeMultiplier\n if (extraAttempts !== undefined) body.extra_attempts = extraAttempts\n return this.client.request<CanvasNewQuizAccommodation>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/accommodations`,\n { method: 'POST', body: JSON.stringify(body) },\n )\n }\n\n async getAccommodation(\n courseId: CanvasId,\n userId: CanvasId,\n ): Promise<CanvasNewQuizAccommodation | null> {\n try {\n return await this.client.request<CanvasNewQuizAccommodation>(\n `/api/quiz/v1/courses/${courseId}/accommodations/${userId}`,\n )\n } catch (err) {\n if (err instanceof CanvasApiError && err.status === 404) return null\n throw err\n }\n }\n\n private toWireItem(item: NewQuizItemInput): Record<string, unknown> {\n switch (item.interaction_type_slug) {\n case 'choice':\n return this.choiceToWire(item)\n case 'true-false':\n return this.trueFalseToWire(item)\n case 'essay':\n return this.essayToWire(item)\n case 'matching':\n return this.matchingToWire(item)\n case 'numeric':\n return this.numericToWire(item)\n }\n }\n\n private choiceToWire(item: ChoiceItem): Record<string, unknown> {\n return {\n interaction_type_slug: 'choice',\n item_body: item.item_body,\n interaction_data: {\n choices: item.choices.map((c, i) => ({\n id: c.id,\n position: i + 1,\n item_body: c.item_body,\n })),\n },\n properties: {\n shuffle_rules: { choices: { shuffled: true } },\n },\n scoring_data: {\n value: item.correct_choice_id,\n },\n // Equivalence hard-coded per CTO direction; not exposed to callers\n scoring_algorithm: 'Equivalence',\n }\n }\n\n private trueFalseToWire(item: TrueFalseItem): Record<string, unknown> {\n return {\n interaction_type_slug: 'true-false',\n item_body: item.item_body,\n interaction_data: {\n choices: [\n { id: 'true', position: 1, item_body: 'True' },\n { id: 'false', position: 2, item_body: 'False' },\n ],\n },\n properties: {},\n scoring_data: {\n value: item.correct_answer ? 'true' : 'false',\n },\n scoring_algorithm: 'Equivalence',\n }\n }\n\n private essayToWire(item: EssayItem): Record<string, unknown> {\n const properties: Record<string, unknown> = {}\n if (item.word_count_min !== undefined || item.word_count_max !== undefined) {\n const wc: Record<string, unknown> = {}\n if (item.word_count_min !== undefined) wc.min = item.word_count_min\n if (item.word_count_max !== undefined) wc.max = item.word_count_max\n properties.word_count = wc\n }\n return {\n interaction_type_slug: 'essay',\n item_body: item.item_body,\n interaction_data: {\n rce_enabled: item.rich_text !== false,\n },\n properties,\n scoring_data: {},\n scoring_algorithm: 'None',\n }\n }\n\n private matchingToWire(item: MatchingItem): Record<string, unknown> {\n const matches = item.matches.map((m, i) => ({\n id: `match_${i}`,\n item_body: m.question,\n }))\n const correctResponses = item.matches.map((m, i) => ({\n id: `resp_${i}`,\n item_body: m.answer,\n }))\n const distractorResponses = (item.distractors ?? []).map((d, i) => ({\n id: `dist_${i}`,\n item_body: d,\n }))\n const scoringValue = item.matches.map((_, i) => ({\n id: `match_${i}`,\n match_id: `resp_${i}`,\n }))\n return {\n interaction_type_slug: 'matching',\n item_body: item.item_body,\n interaction_data: {\n matches,\n responses: [...correctResponses, ...distractorResponses],\n },\n properties: {},\n scoring_data: {\n value: scoringValue,\n },\n scoring_algorithm: 'Equivalence',\n }\n }\n\n private numericToWire(item: NumericItem): Record<string, unknown> {\n const answers = item.answers\n const first = answers[0]\n if (first === undefined) {\n throw new Error('numeric item must have at least one answer')\n }\n let scoringData: Record<string, unknown>\n let scoringAlgorithm: string\n if (first.kind === 'exact') {\n scoringData = { exact_answer: first.value, margin: first.margin ?? 0 }\n scoringAlgorithm = 'Exact'\n } else if (first.kind === 'range') {\n scoringData = { lower_bound: first.min, upper_bound: first.max }\n scoringAlgorithm = 'Range'\n } else {\n scoringData = { exact_answer: first.value, precision: first.precision }\n scoringAlgorithm = 'Precision'\n }\n return {\n interaction_type_slug: 'numeric',\n item_body: item.item_body,\n interaction_data: {},\n properties: {},\n scoring_data: scoringData,\n scoring_algorithm: scoringAlgorithm,\n }\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasContentExport, ContentExportType } from './types'\nimport type { CanvasId } from './id'\n\n/**\n * Canvas `content_exports` API — exports a course (or its assessments) as a\n * Common Cartridge / QTI / zip package. Exports are asynchronous: `create`\n * starts the job and returns immediately; `get` polls until the package is ready\n * and surfaces a time-limited `attachment.url` download link.\n */\nexport class ContentExportsModule {\n constructor(private client: CanvasHttpClient) {}\n\n /** Start a content export. Returns the export in its initial `created` state. */\n async create(courseId: CanvasId, exportType: ContentExportType): Promise<CanvasContentExport> {\n // Canvas accepts `export_type` as a top-level param here (no `content_export:` envelope).\n return this.client.request<CanvasContentExport>(`/api/v1/courses/${courseId}/content_exports`, {\n method: 'POST',\n body: JSON.stringify({ export_type: exportType }),\n })\n }\n\n /** Poll a single export's status; `attachment` is populated only once `exported`. */\n async get(courseId: CanvasId, exportId: CanvasId): Promise<CanvasContentExport> {\n return this.client.request<CanvasContentExport>(\n `/api/v1/courses/${courseId}/content_exports/${exportId}`,\n )\n }\n\n /** List all content exports for a course (most recent first, per Canvas). */\n async list(courseId: CanvasId): Promise<CanvasContentExport[]> {\n return this.client.paginate<CanvasContentExport>(`/api/v1/courses/${courseId}/content_exports`)\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasContentMigration, CanvasContentMigrator, CanvasMigrationIssue } from './types'\nimport type { CanvasId } from './id'\n\nexport class ContentMigrationsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasContentMigration[]> {\n return this.client.paginate<CanvasContentMigration>(\n `/api/v1/courses/${courseId}/content_migrations`,\n )\n }\n\n async get(courseId: CanvasId, migrationId: CanvasId): Promise<CanvasContentMigration> {\n return this.client.request<CanvasContentMigration>(\n `/api/v1/courses/${courseId}/content_migrations/${migrationId}`,\n )\n }\n\n async listMigrators(courseId: CanvasId): Promise<CanvasContentMigrator[]> {\n return this.client.request<CanvasContentMigrator[]>(\n `/api/v1/courses/${courseId}/content_migrations/migrators`,\n )\n }\n\n async getSelectiveData(\n courseId: CanvasId,\n migrationId: CanvasId,\n type?: string,\n ): Promise<unknown[]> {\n const url = `/api/v1/courses/${courseId}/content_migrations/${migrationId}/selective_data`\n return this.client.paginate<unknown>(url, type ? { type } : undefined)\n }\n\n async getAssetIdMapping(\n courseId: CanvasId,\n migrationId: CanvasId,\n ): Promise<Record<string, unknown>> {\n return this.client.request<Record<string, unknown>>(\n `/api/v1/courses/${courseId}/content_migrations/${migrationId}/asset_id_mapping`,\n )\n }\n\n async listMigrationIssues(\n courseId: CanvasId,\n migrationId: CanvasId,\n ): Promise<CanvasMigrationIssue[]> {\n return this.client.paginate<CanvasMigrationIssue>(\n `/api/v1/courses/${courseId}/content_migrations/${migrationId}/migration_issues`,\n )\n }\n\n async create(\n courseId: CanvasId,\n params: {\n migration_type: string\n settings?: Record<string, unknown>\n date_shift_options?: Record<string, unknown>\n selective_import?: boolean\n },\n ): Promise<CanvasContentMigration> {\n return this.client.request<CanvasContentMigration>(\n `/api/v1/courses/${courseId}/content_migrations`,\n {\n method: 'POST',\n body: JSON.stringify(params),\n },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasGradingSchemeEntry, CanvasGradingStandard } from './types'\nimport type { CanvasId } from './id'\n\n/**\n * Canvas grading standards (letter-to-percentage grading schemes).\n *\n * Canvas API asymmetry: the GET/response key is `grading_scheme` (plural),\n * while the POST body key is `grading_scheme_entry` (singular). The POST body\n * is flat — there is no `grading_standard:` wrapper. List endpoints return a\n * plain JSON array, so we use `client.paginate` (not `paginateEnvelope`).\n */\nexport class GradingStandardsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async listForCourse(courseId: CanvasId): Promise<CanvasGradingStandard[]> {\n return this.client.paginate<CanvasGradingStandard>(\n `/api/v1/courses/${courseId}/grading_standards`,\n )\n }\n\n async listForAccount(accountId: CanvasId): Promise<CanvasGradingStandard[]> {\n return this.client.paginate<CanvasGradingStandard>(\n `/api/v1/accounts/${accountId}/grading_standards`,\n )\n }\n\n async createForCourse(\n courseId: CanvasId,\n title: string,\n schemeEntries: CanvasGradingSchemeEntry[],\n ): Promise<CanvasGradingStandard> {\n const sorted = [...schemeEntries].sort((a, b) => b.value - a.value)\n return this.client.request<CanvasGradingStandard>(\n `/api/v1/courses/${courseId}/grading_standards`,\n {\n method: 'POST',\n body: JSON.stringify({ title, grading_scheme_entry: sorted }),\n },\n )\n }\n\n async createForAccount(\n accountId: CanvasId,\n title: string,\n schemeEntries: CanvasGradingSchemeEntry[],\n ): Promise<CanvasGradingStandard> {\n const sorted = [...schemeEntries].sort((a, b) => b.value - a.value)\n return this.client.request<CanvasGradingStandard>(\n `/api/v1/accounts/${accountId}/grading_standards`,\n {\n method: 'POST',\n body: JSON.stringify({ title, grading_scheme_entry: sorted }),\n },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasLatePolicy } from './types'\nimport type { CanvasId } from './id'\n\n/**\n * Canvas late/missing submission policy for a course.\n *\n * The response is an envelope — `{ \"late_policy\": { ... } }` — so `get` unwraps\n * the inner object before returning it. Throws `CanvasApiError` on failure\n * (notably 403 for student tokens and 404 when no policy row exists yet); the\n * caller decides how to handle those statuses.\n */\nexport class LatePolicyModule {\n constructor(private client: CanvasHttpClient) {}\n\n async get(courseId: CanvasId): Promise<CanvasLatePolicy> {\n const envelope = await this.client.request<{ late_policy: CanvasLatePolicy }>(\n `/api/v1/courses/${courseId}/late_policy`,\n )\n return envelope.late_policy\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasAppointmentGroup, CanvasCalendarEvent, CanvasGroup, CanvasUser } from './types'\nimport type { CanvasId } from './id'\n\nexport type AppointmentGroupInclude = 'appointments' | 'child_events' | 'participant_count'\n\nexport class AppointmentGroupsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(params?: {\n scope?: 'reservable' | 'manageable'\n include?: ReadonlyArray<AppointmentGroupInclude>\n context_codes?: string[]\n }): Promise<CanvasAppointmentGroup[]> {\n const query: CanvasQueryParams = {}\n if (params?.scope) query.scope = params.scope\n if (params?.include?.length) query.include = [...params.include]\n if (params?.context_codes?.length) query.context_codes = params.context_codes\n return this.client.paginate<CanvasAppointmentGroup>('/api/v1/appointment_groups', query)\n }\n\n async get(\n id: CanvasId,\n include?: ReadonlyArray<AppointmentGroupInclude>,\n ): Promise<CanvasAppointmentGroup> {\n const query: CanvasQueryParams = {}\n if (include?.length) query.include = [...include]\n return this.client.request<CanvasAppointmentGroup>(`/api/v1/appointment_groups/${id}`, {\n query,\n })\n }\n\n async create(params: {\n context_codes: string[]\n title: string\n description?: string\n location_name?: string\n location_address?: string\n publish?: boolean\n participant_type?: 'User' | 'Group'\n max_appointments_per_participant?: number\n min_appointments_per_participant?: number\n new_appointments?: Array<[string, string]>\n participant_visibility?: 'private' | 'protected'\n }): Promise<CanvasAppointmentGroup> {\n return this.client.request<CanvasAppointmentGroup>('/api/v1/appointment_groups', {\n method: 'POST',\n body: JSON.stringify({ appointment_group: params }),\n })\n }\n\n async update(\n id: CanvasId,\n params: {\n context_codes?: string[]\n title?: string\n description?: string\n location_name?: string\n location_address?: string\n publish?: boolean\n new_appointments?: Array<[string, string]>\n },\n ): Promise<CanvasAppointmentGroup> {\n return this.client.request<CanvasAppointmentGroup>(`/api/v1/appointment_groups/${id}`, {\n method: 'PUT',\n body: JSON.stringify({ appointment_group: params }),\n })\n }\n\n async delete(id: CanvasId, cancelReason?: string): Promise<void> {\n const body = cancelReason ? JSON.stringify({ cancel_reason: cancelReason }) : undefined\n return this.client.request<void>(`/api/v1/appointment_groups/${id}`, {\n method: 'DELETE',\n body,\n })\n }\n\n async listUsers(id: CanvasId): Promise<CanvasUser[]> {\n return this.client.paginate<CanvasUser>(`/api/v1/appointment_groups/${id}/users`)\n }\n\n async listGroups(id: CanvasId): Promise<CanvasGroup[]> {\n return this.client.paginate<CanvasGroup>(`/api/v1/appointment_groups/${id}/groups`)\n }\n\n async nextAppointment(appointmentGroupIds?: CanvasId[]): Promise<CanvasCalendarEvent[]> {\n const query: CanvasQueryParams = {}\n if (appointmentGroupIds?.length) query.appointment_group_ids = appointmentGroupIds\n return this.client.request<CanvasCalendarEvent[]>(\n '/api/v1/appointment_groups/next_appointment',\n { query },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasActivityStreamEntry, CanvasActivityStreamSummaryEntry } from './types'\n\nexport interface ActivityStreamQuery {\n onlyActiveCourses?: boolean\n /**\n * Stop following `Link: rel=\"next\"` once this many items have accumulated.\n * Bounds the WORK, not just the result: `paginate()` otherwise follows every\n * page to exhaustion (up to `maxPaginationPages`, default 1000) before\n * returning, so slicing afterwards would still have fetched up to 100,000\n * items.\n */\n maxItems?: number\n}\n\nexport class ActivityStreamModule {\n constructor(private client: CanvasHttpClient) {}\n\n /**\n * `GET /users/self/activity_stream/summary` is the one stream endpoint Canvas\n * does not paginate (`api_render_stream_summary` has no `Api.paginate` call),\n * so this uses `request()` rather than `paginate()` — the inverse of every\n * other activity-stream endpoint.\n */\n async getSummary(onlyActiveCourses?: boolean): Promise<CanvasActivityStreamSummaryEntry[]> {\n return this.client.request<CanvasActivityStreamSummaryEntry[]>(\n '/api/v1/users/self/activity_stream/summary',\n { query: { only_active_courses: onlyActiveCourses } },\n )\n }\n\n /**\n * `GET /users/self/activity_stream` — the cross-course stream itself.\n *\n * Uses `paginate()`, which is not interchangeable with `request()` here:\n * Canvas renders this endpoint through `Api.paginate(scope, self, …,\n * default_per_page: 21)`, so a `request()` call would silently return 21\n * items with no error and no `Link` header followed. `paginate()` sets\n * `per_page=100` (Canvas's `MAX_PER_PAGE`) and follows the links.\n */\n async getStream(query: ActivityStreamQuery = {}): Promise<CanvasActivityStreamEntry[]> {\n return this.client.paginate<CanvasActivityStreamEntry>(\n '/api/v1/users/self/activity_stream',\n { only_active_courses: query.onlyActiveCourses },\n query.maxItems === undefined ? undefined : { maxItems: query.maxItems },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasPlannerItem } from './types'\n\nexport interface PlannerItemsQuery {\n startDate?: string\n endDate?: string\n contextCodes?: string[]\n filter?: 'new_activity' | 'incomplete_items' | 'complete_items'\n /**\n * Stop following `Link: rel=\"next\"` once this many items have accumulated.\n * Same rationale as `ActivityStreamQuery.maxItems`: bounds the WORK, not\n * just the result.\n */\n maxItems?: number\n}\n\nexport class PlannerModule {\n constructor(private client: CanvasHttpClient) {}\n\n /**\n * `GET /planner/items` — unions nine Canvas collections (assignments,\n * ungraded quizzes, planner notes, wiki pages, ungraded discussions,\n * calendar events, peer reviews, sub-assignments, peer-review\n * sub-assignments) into one cross-course/cross-group feed.\n *\n * Uses `paginate()`: `Api.paginate(items, self, …)` sets no\n * `default_per_page`, so Canvas falls back to `Api::PER_PAGE = 10` absent an\n * explicit `per_page` (BRU-2797 §3.2) — the same `request()`-vs-`paginate()`\n * hazard as the activity stream.\n */\n async listItems(query: PlannerItemsQuery = {}): Promise<CanvasPlannerItem[]> {\n return this.client.paginate<CanvasPlannerItem>(\n '/api/v1/planner/items',\n {\n start_date: query.startDate,\n end_date: query.endDate,\n context_codes: query.contextCodes,\n filter: query.filter,\n },\n query.maxItems === undefined ? undefined : { maxItems: query.maxItems },\n )\n }\n}\n","/**\n * The canonical Canvas identifier input type.\n *\n * Phase 1 of the 64-bit identifier design\n * (`docs/superpowers/specs/2026-10-05-bru-2730-canvas-64bit-identifiers.md`,\n * BRU-2730). PR 1a (BRU-2816) added this module as the type alone, with no\n * call site adopting it; PR 1b (BRU-2827) then adopted it across every\n * Canvas-module input, signature, and handler.\n *\n * The defect it exists to fix is that a Canvas object whose global ID exceeds\n * `Number.MAX_SAFE_INTEGER` — every object on shard >= 901 — cannot be\n * addressed at all today. `JSON.parse('{\"course_id\":9007199254740993}')`,\n * which is what every MCP transport does to wire bytes, silently yields\n * `…992` and the request succeeds against the wrong object; the string form\n * that *would* carry the value exactly is rejected, because the schema is\n * `z.number()`.\n *\n * ## Why a string and not `string | number`\n *\n * Per §4.1: with `type CanvasId = string | number` on **both** sides of a\n * comparison, `a === b` is legal TypeScript and `false` at runtime whenever\n * the two representations differ. The compiler catches only the asymmetric\n * cases, and this codebase has 14 ID-to-ID comparisons in `src/tools/**`\n * plus 18 `Map<number, …>` / `Set<number>` occurrences — exactly where the\n * union would go blind. One canonical representation makes those correct by\n * construction and makes the arithmetic sites fail to compile, which is\n * where a human decision is wanted.\n *\n * ## Why the checks are composed the way they are\n *\n * Every departure from the obvious composition is load-bearing and has a\n * measurement behind it (§4.2.1): the `{ abort: true }` on the pattern check\n * (N1), the per-member error messages rather than one union-level message\n * (N3), and the fact that the `.transform()` makes the emitted JSON Schema\n * available only under `io: 'input'` (N2). `tests/canvas/id-input-wire.test.ts`\n * holds the wire-level assertion for each; reverting any one of them fails a\n * named, disjoint set of tests.\n */\nimport { z } from 'zod'\n\n/**\n * Canvas's own ceiling, not a value chosen here: `MAX_ID = 2**63 - 1` in\n * `lib/api.rb`, alongside `MAX_ID_LENGTH = 19` and\n * `ID_REGEX = /\\A\\d{1,19}\\z/`.\n */\nexport const CANVAS_MAX_ID = 9223372036854775807n\n\n/**\n * The largest ID a JSON **number** can carry without ambiguity. Above this,\n * the value and its neighbour are the same double, so a number is no longer\n * a faithful representation and the string form is the only one that works.\n */\nexport const MAX_SAFE_CANVAS_NUMBER_ID = Number.MAX_SAFE_INTEGER\n\n/**\n * Canvas's `ID_REGEX` minus leading zeros. Canvas tolerates `\"007\"`\n * (`\\d{1,19}` then `.to_i`); we do not, because one object must have exactly\n * one canonical string or `Map` keys fork and §4.1's whole argument is\n * defeated.\n */\nconst CANONICAL_DECIMAL_ID = /^[1-9][0-9]{0,18}$/\n\n/** A Canvas identifier in canonical form: a decimal string, no sign, no leading zeros. */\nexport type CanvasId = string\n\n/**\n * What the **wire** may carry: `JSON.parse` of a Canvas body yields a `number`\n * for an identifier unless Canvas stringified it. Nothing past the HTTP client\n * boundary sees this type, because `normalizeCanvasIds()` in\n * `src/canvas/normalize-ids.ts` rewrites every identifier to a `CanvasId`\n * before the body is returned (§4.1, §4.3). Kept exported as the name for that\n * boundary.\n */\nexport type CanvasWireId = string | number\n\nexport interface CanvasIdInputOptions {\n /**\n * Non-ID literals this call site accepts in an ID position — Canvas's\n * `\"self\"` and `\"all\"`. Only the literals a call site explicitly declares\n * are accepted; declaring one does not open the field to arbitrary strings.\n */\n readonly sentinels?: readonly string[]\n /**\n * Canvas SIS-style alternate identifier prefixes this call site accepts,\n * written **without** the trailing colon: `['sis_user_id']` accepts\n * `\"sis_user_id:A1234\"` and nothing else that is not already a Canvas ID.\n *\n * This option is PR 1b's resolution of PR #395 correction C4. Four call\n * sites in `src/tools/outcomes.ts` document and accept\n * `\"sis_user_id:<sis id>\"`, which `sentinels` cannot express: a sentinel is\n * an exact literal, and the SIS identifier after the colon is free-form.\n * Dropping the form would break tools that work today, and accepting any\n * string would reintroduce exactly what §4.1 exists to remove, so the\n * prefix is **declared per call site** on the same terms as a sentinel.\n *\n * The remainder after the colon is deliberately unconstrained beyond being\n * non-empty: a Canvas SIS ID is an arbitrary institution-assigned string,\n * and Canvas itself splits on the first colon and passes the rest through\n * (`Api.sis_parse_id`). A prefixed value is returned unchanged, so it is\n * never confused with a canonical numeric ID.\n */\n readonly prefixes?: readonly string[]\n}\n\n/** The subset of a Zod issue the messages below read. */\ninterface IdIssueContext {\n readonly input: unknown\n readonly path?: ReadonlyArray<PropertyKey> | undefined\n}\n\n/** Longest rendering of a received value admitted into a message. */\nconst MAX_RECEIVED_CHARS = 42\n\nfunction describeReceived(value: unknown): string {\n if (typeof value === 'string') {\n const quoted = JSON.stringify(value)\n return quoted.length > MAX_RECEIVED_CHARS ? `${quoted.slice(0, MAX_RECEIVED_CHARS)}…` : quoted\n }\n if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean') {\n return String(value)\n }\n if (value === null) return 'null'\n if (value === undefined) return 'undefined'\n return Array.isArray(value) ? 'an array' : `a ${typeof value}`\n}\n\n/**\n * The field name to lead the message with. The **last string** segment, not\n * `path.join('.')`: inside a `canvasIdList()` the path is\n * `['student_ids', 3]`, and `student_ids` is the name the caller wrote. The\n * MCP SDK appends the full path to the message separately, so no information\n * is lost.\n */\nfunction issueLabel(path: ReadonlyArray<PropertyKey> | undefined): string {\n for (let index = (path?.length ?? 0) - 1; index >= 0; index -= 1) {\n const segment = path?.[index]\n if (typeof segment === 'string') return segment\n }\n return 'value'\n}\n\n/**\n * The message a caller hitting the precision bug sees. Set on each union\n * **member** as well as on the union, because a union-level `error` does not\n * reach a member's own issues: without this, `9007199254740992` yields\n * `Too big: expected int to be <=9007199254740991`, which names neither the\n * received value nor the remedy (§4.2.1 N3).\n */\nfunction contractMessage(issue: IdIssueContext): string {\n return (\n `${issueLabel(issue.path)} must be a Canvas ID: a positive integer at or below ` +\n `${MAX_SAFE_CANVAS_NUMBER_ID}, or a decimal string for larger IDs ` +\n `(received ${describeReceived(issue.input)}). ` +\n `Pass large IDs as strings, e.g. \"9010000000000001\".`\n )\n}\n\n/**\n * The string arm's range failure. A separate message because the \"pass it as\n * a string\" remedy does not apply here — the value is above Canvas's own\n * `MAX_ID`, so no representation can address it. Without a message of its\n * own the `.refine` reports a bare `custom` / `Invalid input` (§4.2.1 N3).\n */\nfunction ceilingMessage(issue: IdIssueContext): string {\n return (\n `${issueLabel(issue.path)} must be a Canvas ID at or below ${CANVAS_MAX_ID}, ` +\n `the largest value Canvas can store (received ${describeReceived(issue.input)}).`\n )\n}\n\n/**\n * Exactly `z.number().int().positive()`, which in Zod 4 is a **safe-integer**\n * check and therefore already rejects `2**53` with `too_big` — not\n * hand-rolled, per §4.2 rule 1.\n */\nfunction numberArm(): z.ZodType<number, unknown> {\n return z.number().int({ error: contractMessage }).positive({ error: contractMessage })\n}\n\n/**\n * `{ abort: true }` is not a style preference. Zod 4 runs *all* checks on a\n * string schema and collects issues rather than short-circuiting, so without\n * it the `.refine` below calls `BigInt()` on values the pattern has already\n * rejected — and `BigInt('7.0')` throws a `SyntaxError` that escapes\n * `safeParse` *and* the enclosing union, surfacing to the caller as an\n * internal message instead of an `-32602` (§4.2.1 N1).\n *\n * The `.refine` deliberately calls `BigInt` unguarded, so that removing the\n * abort flag is observable rather than silently compensated for.\n */\nfunction stringArm(): z.ZodType<string, unknown> {\n return z\n .string()\n .regex(CANONICAL_DECIMAL_ID, { abort: true, error: contractMessage })\n .refine((value) => BigInt(value) <= CANVAS_MAX_ID, {\n error: ceilingMessage,\n })\n}\n\n/**\n * The message a declared SIS prefix publishes when the value starts with the\n * prefix but carries nothing after the colon. Its own message for the same\n * reason `ceilingMessage` has one: a union-level `error` does not reach a\n * member's issues (§4.2.1 N3).\n */\nfunction prefixMessage(prefix: string): (issue: IdIssueContext) => string {\n return (issue) =>\n `${issueLabel(issue.path)} may be a Canvas ID, or an SIS identifier written ` +\n `\"${prefix}:<sis id>\" with a non-empty SIS id (received ${describeReceived(issue.input)}).`\n}\n\n/** Escapes every ECMAScript regex metacharacter, so a prefix matches literally. */\nfunction escapeForRegExp(literal: string): string {\n return literal.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * One declared SIS-prefix arm. A `.regex` rather than a `.startsWith` +\n * `.refine` so that the accepted form is **published** in the emitted JSON\n * Schema as a `pattern` — §9's argument for a self-documenting schema applies\n * to this arm as much as to the canonical one. `[\\s\\S]` rather than `.` so a\n * literal newline inside an institution-assigned SIS id is not silently a\n * rejection the pattern does not explain.\n */\nfunction prefixedArm(prefix: string): z.ZodType<string, unknown> {\n return z.string().regex(new RegExp(`^${escapeForRegExp(prefix)}:[\\\\s\\\\S]+$`), {\n error: prefixMessage(prefix),\n })\n}\n\n/**\n * A required Canvas identifier input. Accepts a safe positive integer or a\n * canonical decimal string, and always yields the canonical decimal string.\n *\n * ```ts\n * course_id: canvasIdInput()\n * user_id: canvasIdInput({ sentinels: ['self'] }).optional()\n * user_ids: canvasIdList({ prefixes: ['sis_user_id'] }).optional()\n * ```\n */\nexport function canvasIdInput(options?: CanvasIdInputOptions): z.ZodType<CanvasId, unknown> {\n const sentinels = options?.sentinels ?? []\n const members = [\n numberArm(),\n stringArm(),\n ...(options?.prefixes ?? []).map(prefixedArm),\n ...(sentinels.length > 0 ? [z.literal([...sentinels])] : []),\n ] as [z.ZodType<unknown, unknown>, z.ZodType<unknown, unknown>, ...z.ZodType<unknown, unknown>[]]\n\n return z.union(members, { error: contractMessage }).transform((value) => String(value))\n}\n\n/**\n * A list of Canvas identifiers. Sentinels are **per element**, which is how\n * Canvas takes them (`student_ids[]=all`), so a declared sentinel may appear\n * alongside real IDs in the same list.\n */\nexport function canvasIdList(options?: CanvasIdInputOptions): z.ZodType<CanvasId[], unknown> {\n return z.array(canvasIdInput(options))\n}\n\n/**\n * Numeric ordering of two canonical Canvas IDs, without `Number()`.\n *\n * `a - b` is the obvious thing and is exactly what §4.4 forbids: it rounds both\n * operands above `2**53` and can order two distinct IDs as equal. Because a\n * `CanvasId` is a decimal string with no sign and no leading zeros, \"shorter is\n * smaller, same length compares lexicographically\" is total and exact at any\n * magnitude — no `BigInt` allocation per comparison.\n *\n * The contract is exactly \"shorter string sorts first, equal-length strings\n * sort lexicographically\" — nothing more. That is a total, exact numeric\n * order over two canonical `CanvasId` decimal strings, but it is not a\n * numeric order at all once one operand is non-decimal (a sentinel like\n * `\"self\"`, or a SIS-prefixed string): such a value sorts purely by its own\n * length and characters, which can place it before, after, or between\n * numeric IDs depending on how long those IDs happen to be. No call site\n * mixes sentinel/prefixed values into a sort today; this function is not\n * safe to use if one starts.\n */\nexport function compareCanvasIds(a: CanvasId, b: CanvasId): number {\n if (a.length !== b.length) return a.length - b.length\n return a < b ? -1 : a > b ? 1 : 0\n}\n\n/**\n * The same §4.2 rules, callable outside a Zod parse — for code that receives\n * an identifier from somewhere other than a tool input schema.\n *\n * Returns the canonical decimal string, or throws a `TypeError` carrying the\n * same message the schema would publish. Throws rather than returning a\n * result object so that a caller cannot accidentally thread a non-canonical\n * value onward, which is the failure §4.1 exists to prevent.\n */\nexport function normalizeCanvasIdInput(value: unknown, options?: CanvasIdInputOptions): CanvasId {\n const issue: IdIssueContext = { input: value }\n\n if (typeof value === 'string') {\n if (options?.sentinels?.includes(value)) return value\n // A declared SIS prefix is matched by `startsWith`, not by the published\n // pattern, so this path needs no escaping to be correct — and a prefix\n // containing a regex metacharacter cannot diverge between the two.\n for (const prefix of options?.prefixes ?? []) {\n if (!value.startsWith(`${prefix}:`)) continue\n if (value.length === prefix.length + 1) throw new TypeError(prefixMessage(prefix)(issue))\n return value\n }\n // Order matters, for the same reason `{ abort: true }` does above: a\n // `BigInt` call on a non-numeric string throws a SyntaxError, so the\n // pattern check has to come first (§4.2.1 N1, outside Zod).\n if (!CANONICAL_DECIMAL_ID.test(value)) throw new TypeError(contractMessage(issue))\n if (BigInt(value) > CANVAS_MAX_ID) throw new TypeError(ceilingMessage(issue))\n return value\n }\n\n if (typeof value === 'number') {\n if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(contractMessage(issue))\n return String(value)\n }\n\n throw new TypeError(contractMessage(issue))\n}\n","import { CanvasHttpClient } from './client'\nimport type { CanvasClientConfig } from './types'\nimport { CoursesModule } from './courses'\nimport { AssignmentsModule } from './assignments'\nimport { SubmissionsModule } from './submissions'\nimport { RubricsModule } from './rubrics'\nimport { QuizzesModule } from './quizzes'\nimport { FilesModule } from './files'\nimport { UsersModule } from './users'\nimport { GroupsModule } from './groups'\nimport { EnrollmentsModule } from './enrollments'\nimport { DiscussionsModule } from './discussions'\nimport { ModulesModule } from './modules'\nimport { PagesModule } from './pages'\nimport { CalendarModule } from './calendar'\nimport { ConversationsModule } from './conversations'\nimport { PeerReviewsModule } from './peer-reviews'\nimport { AccountsModule } from './accounts'\nimport { AnalyticsModule } from './analytics'\nimport { DashboardModule } from './dashboard'\nimport { OutcomesModule } from './outcomes'\nimport { GradebookHistoryModule } from './gradebook-history'\nimport { NewQuizzesModule } from './new-quizzes'\nimport { ContentExportsModule } from './content-exports'\nimport { ContentMigrationsModule } from './content-migrations'\nimport { GradingStandardsModule } from './grading-standards'\nimport { LatePolicyModule } from './late-policy'\nimport { AppointmentGroupsModule } from './appointment-groups'\nimport { ActivityStreamModule } from './activity-stream'\nimport { PlannerModule } from './planner'\n\n/**\n * Standalone Canvas REST API client. Composed of modular per-domain classes\n * sharing a `CanvasHttpClient`. Throws `CanvasApiError` on Canvas-side failures.\n *\n * NOTE on FERPA mode: pseudonymization is a tool-layer concern provided by the\n * `Pseudonymizer` in `src/pseudonym/`. Library consumers that import this\n * client directly (`canvas-lms-mcp/canvas`) bypass the MCP tool wrappers and\n * therefore receive raw Canvas responses with real student names and contact\n * info. If you need pseudonymized output, either use the MCP server entry\n * points (`canvas-lms-mcp` stdio / `canvas-lms-mcp serve` HTTP) or wrap your\n * own calls with a `Pseudonymizer` instance.\n */\nexport class CanvasClient {\n private client: CanvasHttpClient\n courses: CoursesModule\n assignments: AssignmentsModule\n submissions: SubmissionsModule\n rubrics: RubricsModule\n quizzes: QuizzesModule\n files: FilesModule\n users: UsersModule\n groups: GroupsModule\n enrollments: EnrollmentsModule\n discussions: DiscussionsModule\n modules: ModulesModule\n pages: PagesModule\n calendar: CalendarModule\n conversations: ConversationsModule\n peerReviews: PeerReviewsModule\n accounts: AccountsModule\n analytics: AnalyticsModule\n dashboard: DashboardModule\n outcomes: OutcomesModule\n gradebookHistory: GradebookHistoryModule\n newQuizzes: NewQuizzesModule\n contentExports: ContentExportsModule\n contentMigrations: ContentMigrationsModule\n gradingStandards: GradingStandardsModule\n latePolicy: LatePolicyModule\n appointmentGroups: AppointmentGroupsModule\n activityStream: ActivityStreamModule\n planner: PlannerModule\n\n constructor(config: CanvasClientConfig) {\n this.client = new CanvasHttpClient(config)\n this.courses = new CoursesModule(this.client)\n this.assignments = new AssignmentsModule(this.client)\n this.submissions = new SubmissionsModule(this.client)\n this.rubrics = new RubricsModule(this.client)\n this.quizzes = new QuizzesModule(this.client)\n this.files = new FilesModule(this.client)\n this.users = new UsersModule(this.client)\n this.groups = new GroupsModule(this.client)\n this.enrollments = new EnrollmentsModule(this.client)\n this.discussions = new DiscussionsModule(this.client)\n this.modules = new ModulesModule(this.client)\n this.pages = new PagesModule(this.client)\n this.calendar = new CalendarModule(this.client)\n this.conversations = new ConversationsModule(this.client)\n this.peerReviews = new PeerReviewsModule(this.client)\n this.accounts = new AccountsModule(this.client)\n this.analytics = new AnalyticsModule(this.client)\n this.dashboard = new DashboardModule(this.client)\n this.outcomes = new OutcomesModule(this.client)\n this.gradebookHistory = new GradebookHistoryModule(this.client)\n this.newQuizzes = new NewQuizzesModule(this.client)\n this.contentExports = new ContentExportsModule(this.client)\n this.contentMigrations = new ContentMigrationsModule(this.client)\n this.gradingStandards = new GradingStandardsModule(this.client)\n this.latePolicy = new LatePolicyModule(this.client)\n this.appointmentGroups = new AppointmentGroupsModule(this.client)\n this.activityStream = new ActivityStreamModule(this.client)\n this.planner = new PlannerModule(this.client)\n }\n}\n\nexport { CanvasHttpClient, CanvasApiError } from './client'\nexport type { CanvasRequestOptions } from './client'\nexport { appendCanvasQuery, toCanvasQuery } from './query'\nexport type { CanvasQueryParams, CanvasQueryPrimitive, CanvasQueryValue } from './query'\n/**\n * The identifier contract, re-exported because `canvas-lms-mcp/canvas` is a\n * published entry point and every ID-taking method on `CanvasClient` now names\n * `CanvasId` in its signature (BRU-2730 §3.6). Without this a consumer can see\n * the type in the emitted `.d.ts` and still not import it — `TS2305` from the\n * subpath, which is what `tests/canvas/id-package-consumer.test.ts` caught.\n * `CanvasIdInputOptions` is included because `normalizeCanvasIdInput`'s\n * signature references it.\n */\nexport {\n canvasIdInput,\n canvasIdList,\n normalizeCanvasIdInput,\n compareCanvasIds,\n CANVAS_MAX_ID,\n MAX_SAFE_CANVAS_NUMBER_ID,\n} from './id'\nexport type { CanvasId, CanvasWireId, CanvasIdInputOptions } from './id'\nexport type * from './types'\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-4TAI5SSR.cjs","../src/canvas/courses.ts","../src/canvas/assignments.ts","../src/canvas/submissions.ts","../src/canvas/rubrics.ts","../src/canvas/quizzes.ts","../src/canvas/files.ts","../src/canvas/users.ts","../src/canvas/groups.ts","../src/canvas/enrollments.ts","../src/canvas/discussions.ts","../src/canvas/modules.ts","../src/canvas/concurrency.ts","../src/canvas/pages.ts","../src/canvas/calendar.ts","../src/canvas/conversations.ts","../src/canvas/peer-reviews.ts","../src/canvas/accounts.ts","../src/canvas/analytics.ts","../src/canvas/dashboard.ts","../src/canvas/outcomes.ts","../src/canvas/gradebook-history.ts","../src/canvas/new-quizzes.ts","../src/canvas/content-exports.ts","../src/canvas/content-migrations.ts","../src/canvas/grading-standards.ts","../src/canvas/late-policy.ts","../src/canvas/appointment-groups.ts","../src/canvas/activity-stream.ts","../src/canvas/planner.ts","../src/canvas/id.ts","../src/canvas/index.ts"],"names":[],"mappings":"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","file":"/home/runner/work/canvas-lms-mcp/canvas-lms-mcp/dist/chunk-4TAI5SSR.cjs","sourcesContent":[null,"import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasCourse, CreateCourseParams, UpdateCourseParams } from './types'\nimport type { CanvasId } from './id'\n\nexport type CourseEnrollmentState = 'active' | 'invited_or_pending' | 'completed'\n\nexport type CourseWorkflowState = 'unpublished' | 'available' | 'completed' | 'deleted'\n\nexport type CourseListInclude =\n | 'needs_grading_count'\n | 'syllabus_body'\n | 'public_description'\n | 'total_scores'\n | 'current_grading_period_scores'\n | 'grading_periods'\n | 'term'\n | 'account'\n | 'course_progress'\n | 'sections'\n | 'storage_quota_used_mb'\n | 'total_students'\n | 'passback_status'\n | 'favorites'\n | 'teachers'\n | 'observed_users'\n | 'tabs'\n | 'course_image'\n | 'banner_image'\n | 'concluded'\n | 'lti_context_id'\n | 'post_manually'\n\nexport type CourseGetInclude =\n CourseListInclude | 'all_courses' | 'permissions' | 'public_description'\n\nexport interface ListCoursesOptions {\n enrollment_state?: CourseEnrollmentState\n state?: ReadonlyArray<CourseWorkflowState>\n enrollment_role_id?: CanvasId\n include?: ReadonlyArray<CourseListInclude>\n exclude_blueprint_courses?: boolean\n}\n\nexport interface GetCourseOptions {\n include?: ReadonlyArray<CourseGetInclude>\n teacher_limit?: number\n}\n\nconst DEFAULT_LIST_INCLUDE: ReadonlyArray<CourseListInclude> = ['term']\nconst DEFAULT_GET_INCLUDE: ReadonlyArray<CourseGetInclude> = ['term', 'total_students']\n\nexport class CoursesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(opts: ListCoursesOptions = {}): Promise<CanvasCourse[]> {\n const params: CanvasQueryParams = {}\n params.include = opts.include && opts.include.length > 0 ? opts.include : DEFAULT_LIST_INCLUDE\n if (opts.enrollment_state) params.enrollment_state = opts.enrollment_state\n if (opts.state && opts.state.length > 0) params.state = opts.state\n if (opts.enrollment_role_id !== undefined) params.enrollment_role_id = opts.enrollment_role_id\n if (opts.exclude_blueprint_courses !== undefined) {\n params.exclude_blueprint_courses = opts.exclude_blueprint_courses\n }\n return this.client.paginate<CanvasCourse>('/api/v1/courses', params)\n }\n\n async get(courseId: CanvasId, opts: GetCourseOptions = {}): Promise<CanvasCourse> {\n const include = opts.include && opts.include.length > 0 ? opts.include : DEFAULT_GET_INCLUDE\n const query: CanvasQueryParams = { include }\n if (opts.teacher_limit !== undefined) query.teacher_limit = opts.teacher_limit\n return this.client.request<CanvasCourse>(`/api/v1/courses/${courseId}`, { query })\n }\n\n async getSyllabus(courseId: CanvasId): Promise<string | null> {\n const course = await this.client.request<CanvasCourse>(`/api/v1/courses/${courseId}`, {\n query: { include: ['syllabus_body'] },\n })\n return course.syllabus_body ?? null\n }\n\n async create(params: CreateCourseParams): Promise<CanvasCourse> {\n const { account_id, ...courseFields } = params\n return this.client.request<CanvasCourse>(`/api/v1/accounts/${account_id}/courses`, {\n method: 'POST',\n body: JSON.stringify({ course: courseFields }),\n })\n }\n\n async update(courseId: CanvasId, params: UpdateCourseParams): Promise<CanvasCourse> {\n return this.client.request<CanvasCourse>(`/api/v1/courses/${courseId}`, {\n method: 'PUT',\n body: JSON.stringify({ course: params }),\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type {\n CanvasAssignment,\n CanvasAssignmentGroup,\n CanvasAssignmentOverride,\n CreateAssignmentOverrideParams,\n CreateAssignmentParams,\n UpdateAssignmentParams,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport type AssignmentListInclude =\n | 'submission'\n | 'assignment_visibility'\n | 'all_dates'\n | 'overrides'\n | 'observed_users'\n | 'can_edit'\n | 'score_statistics'\n\nexport type AssignmentGetInclude =\n | 'submission'\n | 'assignment_visibility'\n | 'overrides'\n | 'observed_users'\n | 'can_edit'\n | 'score_statistics'\n | 'all_dates'\n\nexport type AssignmentBucket =\n 'past' | 'overdue' | 'undated' | 'ungraded' | 'unsubmitted' | 'upcoming' | 'future'\n\nexport type AssignmentOrderBy = 'position' | 'name' | 'due_at'\n\nexport type AssignmentGroupInclude =\n | 'assignments'\n | 'discussion_topic'\n | 'all_dates'\n | 'assignment_visibility'\n | 'overrides'\n | 'submission'\n | 'observed_users'\n | 'can_edit'\n | 'score_statistics'\n\nexport interface ListAssignmentsOptions {\n include?: ReadonlyArray<AssignmentListInclude>\n search_term?: string\n override_assignment_dates?: boolean\n needs_grading_count_by_section?: boolean\n bucket?: AssignmentBucket\n assignment_ids?: ReadonlyArray<CanvasId>\n order_by?: AssignmentOrderBy\n post_to_sis?: boolean\n}\n\nexport interface GetAssignmentOptions {\n include?: ReadonlyArray<AssignmentGetInclude>\n override_assignment_dates?: boolean\n needs_grading_count_by_section?: boolean\n all_dates?: boolean\n}\n\nexport interface ListAssignmentGroupsOptions {\n include?: ReadonlyArray<AssignmentGroupInclude>\n assignment_ids?: ReadonlyArray<CanvasId>\n exclude_assignment_submission_types?: ReadonlyArray<string>\n override_assignment_dates?: boolean\n grading_period_id?: CanvasId\n scope_assignments_to_student?: boolean\n}\n\nfunction toQuery(opts: object): CanvasQueryParams {\n const params: CanvasQueryParams = {}\n for (const [key, value] of Object.entries(opts)) {\n if (value === undefined || value === null) continue\n if (Array.isArray(value)) {\n if (value.length === 0) continue\n params[key] = value as ReadonlyArray<string | number | boolean>\n } else if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n params[key] = value\n }\n }\n return params\n}\n\nexport class AssignmentsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId, opts: ListAssignmentsOptions = {}): Promise<CanvasAssignment[]> {\n return this.client.paginate<CanvasAssignment>(\n `/api/v1/courses/${courseId}/assignments`,\n toQuery(opts),\n )\n }\n\n async get(\n courseId: CanvasId,\n assignmentId: CanvasId,\n opts: GetAssignmentOptions = {},\n ): Promise<CanvasAssignment> {\n return this.client.request<CanvasAssignment>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}`,\n { query: toQuery(opts) },\n )\n }\n\n async listGroups(\n courseId: CanvasId,\n opts: ListAssignmentGroupsOptions = {},\n ): Promise<CanvasAssignmentGroup[]> {\n return this.client.paginate<CanvasAssignmentGroup>(\n `/api/v1/courses/${courseId}/assignment_groups`,\n toQuery(opts),\n )\n }\n\n async create(courseId: CanvasId, params: CreateAssignmentParams): Promise<CanvasAssignment> {\n return this.client.request<CanvasAssignment>(`/api/v1/courses/${courseId}/assignments`, {\n method: 'POST',\n body: JSON.stringify({ assignment: params }),\n })\n }\n\n async update(\n courseId: CanvasId,\n assignmentId: CanvasId,\n params: UpdateAssignmentParams,\n ): Promise<CanvasAssignment> {\n return this.client.request<CanvasAssignment>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}`,\n {\n method: 'PUT',\n body: JSON.stringify({ assignment: params }),\n },\n )\n }\n\n async delete(courseId: CanvasId, assignmentId: CanvasId): Promise<void> {\n await this.client.request<void>(`/api/v1/courses/${courseId}/assignments/${assignmentId}`, {\n method: 'DELETE',\n })\n }\n\n async listOverrides(\n courseId: CanvasId,\n assignmentId: CanvasId,\n ): Promise<CanvasAssignmentOverride[]> {\n return this.client.paginate<CanvasAssignmentOverride>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/overrides`,\n )\n }\n\n async createOverride(\n courseId: CanvasId,\n assignmentId: CanvasId,\n params: CreateAssignmentOverrideParams,\n ): Promise<CanvasAssignmentOverride> {\n return this.client.request<CanvasAssignmentOverride>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/overrides`,\n {\n method: 'POST',\n body: JSON.stringify({ assignment_override: params }),\n },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasSubmission } from './types'\nimport type { CanvasId } from './id'\n\nexport type SubmissionListInclude =\n | 'submission_history'\n | 'submission_comments'\n | 'rubric_assessment'\n | 'assignment'\n | 'visibility'\n | 'course'\n | 'user'\n | 'group'\n | 'read_status'\n | 'sub_assignment_submissions'\n\nexport type SubmissionGetInclude =\n | 'submission_history'\n | 'submission_comments'\n | 'rubric_assessment'\n | 'visibility'\n | 'course'\n | 'user'\n | 'read_status'\n\nexport type SubmissionWorkflowState = 'submitted' | 'unsubmitted' | 'graded' | 'pending_review'\n\nexport interface ListSubmissionsOptions {\n include?: ReadonlyArray<SubmissionListInclude>\n student_ids?: ReadonlyArray<CanvasId>\n assignment_ids?: ReadonlyArray<CanvasId>\n section_ids?: ReadonlyArray<CanvasId>\n grouped?: boolean\n workflow_state?: SubmissionWorkflowState\n grading_period_id?: CanvasId\n post_to_sis?: boolean\n submitted_since?: string\n graded_since?: string\n}\n\nexport interface GetSubmissionOptions {\n include?: ReadonlyArray<SubmissionGetInclude>\n}\n\nexport interface ListStudentSubmissionsOptions {\n student_ids?: ReadonlyArray<CanvasId>\n assignment_ids?: ReadonlyArray<CanvasId>\n include?: ReadonlyArray<SubmissionListInclude>\n workflow_state?: SubmissionWorkflowState\n}\n\nexport interface ListMySubmissionsOptions {\n include?: ReadonlyArray<SubmissionListInclude>\n}\n\nexport interface SubmitAssignmentParams {\n submission_type: 'online_text_entry' | 'online_url' | 'online_upload'\n body?: string\n url?: string\n file_ids?: ReadonlyArray<CanvasId>\n comment?: string\n}\n\nconst DEFAULT_LIST_INCLUDE: ReadonlyArray<SubmissionListInclude> = ['submission_comments']\nconst DEFAULT_GET_INCLUDE: ReadonlyArray<SubmissionGetInclude> = ['submission_comments']\n\nfunction buildListParams(opts: ListSubmissionsOptions): CanvasQueryParams {\n const params: CanvasQueryParams = {}\n params.include = opts.include && opts.include.length > 0 ? opts.include : DEFAULT_LIST_INCLUDE\n if (opts.student_ids && opts.student_ids.length > 0) params.student_ids = opts.student_ids\n if (opts.assignment_ids && opts.assignment_ids.length > 0)\n params.assignment_ids = opts.assignment_ids\n if (opts.section_ids && opts.section_ids.length > 0) params.section_ids = opts.section_ids\n if (opts.grouped !== undefined) params.grouped = opts.grouped\n if (opts.workflow_state) params.workflow_state = opts.workflow_state\n if (opts.grading_period_id !== undefined) params.grading_period_id = opts.grading_period_id\n if (opts.post_to_sis !== undefined) params.post_to_sis = opts.post_to_sis\n if (opts.submitted_since) params.submitted_since = opts.submitted_since\n if (opts.graded_since) params.graded_since = opts.graded_since\n return params\n}\n\nexport class SubmissionsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(\n courseId: CanvasId,\n assignmentId: CanvasId,\n opts: ListSubmissionsOptions = {},\n ): Promise<CanvasSubmission[]> {\n return this.client.paginate<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions`,\n buildListParams(opts),\n )\n }\n\n async get(\n courseId: CanvasId,\n assignmentId: CanvasId,\n userId: CanvasId,\n opts: GetSubmissionOptions = {},\n ): Promise<CanvasSubmission> {\n const include = opts.include && opts.include.length > 0 ? opts.include : DEFAULT_GET_INCLUDE\n return this.client.request<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`,\n { query: { include } },\n )\n }\n\n async grade(\n courseId: CanvasId,\n assignmentId: CanvasId,\n userId: CanvasId,\n grade: string,\n ): Promise<CanvasSubmission> {\n return this.client.request<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`,\n {\n method: 'PUT',\n body: JSON.stringify({ submission: { posted_grade: grade } }),\n },\n )\n }\n\n async comment(\n courseId: CanvasId,\n assignmentId: CanvasId,\n userId: CanvasId,\n comment: string,\n ): Promise<CanvasSubmission> {\n return this.client.request<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}`,\n {\n method: 'PUT',\n body: JSON.stringify({\n comment: { text_comment: comment },\n }),\n },\n )\n }\n\n async listMy(\n courseId: CanvasId,\n opts: ListMySubmissionsOptions = {},\n ): Promise<CanvasSubmission[]> {\n const params: CanvasQueryParams = { student_ids: ['self'] }\n if (opts.include && opts.include.length > 0) params.include = opts.include\n return this.client.paginate<CanvasSubmission>(\n `/api/v1/courses/${courseId}/students/submissions`,\n params,\n )\n }\n\n async submit(\n courseId: CanvasId,\n assignmentId: CanvasId,\n params: SubmitAssignmentParams,\n ): Promise<CanvasSubmission> {\n const submission: Record<string, unknown> = { submission_type: params.submission_type }\n if (params.body !== undefined) submission.body = params.body\n if (params.url !== undefined) submission.url = params.url\n if (params.file_ids !== undefined) submission.file_ids = params.file_ids\n const payload: Record<string, unknown> = { submission }\n if (params.comment !== undefined) payload.comment = { text_comment: params.comment }\n return this.client.request<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions`,\n { method: 'POST', body: JSON.stringify(payload) },\n )\n }\n\n async listForStudents(\n courseId: CanvasId,\n opts: ListStudentSubmissionsOptions = {},\n ): Promise<CanvasSubmission[]> {\n const params: CanvasQueryParams = {}\n params.student_ids =\n opts.student_ids && opts.student_ids.length > 0 ? opts.student_ids : ['all']\n if (opts.assignment_ids && opts.assignment_ids.length > 0)\n params.assignment_ids = opts.assignment_ids\n if (opts.include && opts.include.length > 0) params.include = opts.include\n if (opts.workflow_state) params.workflow_state = opts.workflow_state\n return this.client.paginate<CanvasSubmission>(\n `/api/v1/courses/${courseId}/students/submissions`,\n params,\n )\n }\n}\n","import { CanvasApiError, type CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type {\n CanvasRubric,\n CanvasRubricAssessment,\n CanvasRubricAssociation,\n CanvasRubricCreateResult,\n CanvasSubmission,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport type RubricInclude =\n | 'assessments'\n | 'graded_assessments'\n | 'peer_assessments'\n | 'associations'\n | 'assignment_associations'\n | 'course_associations'\n | 'account_associations'\n\nexport interface GetRubricOptions {\n include?: ReadonlyArray<RubricInclude>\n}\n\nexport interface RubricRatingInput {\n description: string\n points: number\n}\n\nexport interface RubricCriterionInput {\n description: string\n points: number\n ratings: RubricRatingInput[]\n}\n\nexport interface RubricCreateInput {\n title: string\n criteria: RubricCriterionInput[]\n}\n\nexport interface RubricAssociationInput {\n assignment_id: CanvasId\n use_for_grading?: boolean\n purpose?: string\n}\n\nexport interface RubricAttachOptions {\n use_for_grading?: boolean\n hide_score_total?: boolean\n /** Defaults to `grading`. */\n purpose?: string\n}\n\nexport class RubricsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasRubric[]> {\n return this.client.paginate<CanvasRubric>(`/api/v1/courses/${courseId}/rubrics`)\n }\n\n async get(\n courseId: CanvasId,\n rubricId: CanvasId,\n options: GetRubricOptions = {},\n ): Promise<CanvasRubric> {\n if (!options.include || options.include.length === 0) {\n return this.client.request<CanvasRubric>(`/api/v1/courses/${courseId}/rubrics/${rubricId}`)\n }\n const query: CanvasQueryParams = {}\n query.include = options.include\n return this.client.request<CanvasRubric>(`/api/v1/courses/${courseId}/rubrics/${rubricId}`, {\n query,\n })\n }\n\n /** Canvas answers the DELETE with the deleted rubric inside a `{ rubric }` envelope, which this unwraps. */\n async delete(courseId: CanvasId, rubricId: CanvasId): Promise<CanvasRubric> {\n const envelope = await this.client.request<{ rubric: CanvasRubric }>(\n `/api/v1/courses/${courseId}/rubrics/${rubricId}`,\n { method: 'DELETE' },\n )\n return envelope.rubric\n }\n\n async getAssessment(\n courseId: CanvasId,\n assignmentId: CanvasId,\n userId: CanvasId,\n ): Promise<CanvasSubmission> {\n return this.client.request<CanvasSubmission>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${userId}?include[]=rubric_assessment`,\n )\n }\n\n async submitAssessment(\n courseId: CanvasId,\n associationId: CanvasId,\n data: CanvasRubricAssessment['data'],\n ): Promise<CanvasRubricAssessment> {\n return this.client.request<CanvasRubricAssessment>(\n `/api/v1/courses/${courseId}/rubric_associations/${associationId}/rubric_assessments`,\n {\n method: 'POST',\n body: JSON.stringify({ rubric_assessment: { data } }),\n },\n )\n }\n\n /** Canvas returns an envelope and reports association validation failures with HTTP 200. */\n async create(\n courseId: CanvasId,\n rubric: RubricCreateInput,\n association?: RubricAssociationInput,\n ): Promise<CanvasRubricCreateResult> {\n const params = new URLSearchParams()\n params.append('rubric[title]', rubric.title)\n\n rubric.criteria.forEach((criterion, ci) => {\n params.append(`rubric[criteria][${ci}][description]`, criterion.description)\n params.append(`rubric[criteria][${ci}][points]`, String(criterion.points))\n\n // Sort ratings highest → lowest before encoding\n const sortedRatings = [...criterion.ratings].sort((a, b) => b.points - a.points)\n sortedRatings.forEach((rating, ri) => {\n params.append(`rubric[criteria][${ci}][ratings][${ri}][description]`, rating.description)\n params.append(`rubric[criteria][${ci}][ratings][${ri}][points]`, String(rating.points))\n })\n })\n\n if (association) {\n params.append('rubric_association[association_id]', String(association.assignment_id))\n params.append('rubric_association[association_type]', 'Assignment')\n if (association.use_for_grading !== undefined) {\n params.append('rubric_association[use_for_grading]', String(association.use_for_grading))\n }\n if (association.purpose) {\n params.append('rubric_association[purpose]', association.purpose)\n }\n }\n\n const endpoint = `/api/v1/courses/${courseId}/rubrics`\n const result = await this.client.request<\n (CanvasRubricCreateResult & { error?: false }) | { error: true; messages?: string[] }\n >(endpoint, {\n method: 'POST',\n body: params.toString(),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n })\n if (result.error === true) {\n throw new CanvasApiError(\n result.messages?.join('; ') || 'Canvas rejected the rubric',\n 422,\n endpoint,\n )\n }\n return result\n }\n\n /**\n * Attach an existing rubric to an assignment via\n * `POST /courses/:id/rubric_associations`, so one rubric can be shared across\n * many assignments instead of `create` minting a duplicate per assignment.\n * Same bracket-notation form encoding as `create`.\n * Canvas returns the full rubric beside the association; only the association is\n * returned, so looping over many assignments does not repeat the criteria.\n */\n async associate(\n courseId: CanvasId,\n rubricId: CanvasId,\n assignmentId: CanvasId,\n options: RubricAttachOptions = {},\n ): Promise<CanvasRubricAssociation> {\n const params = new URLSearchParams()\n params.append('rubric_association[rubric_id]', String(rubricId))\n params.append('rubric_association[association_id]', String(assignmentId))\n params.append('rubric_association[association_type]', 'Assignment')\n params.append('rubric_association[purpose]', options.purpose ?? 'grading')\n if (options.use_for_grading !== undefined) {\n params.append('rubric_association[use_for_grading]', String(options.use_for_grading))\n }\n if (options.hide_score_total !== undefined) {\n params.append('rubric_association[hide_score_total]', String(options.hide_score_total))\n }\n const envelope = await this.client.request<{\n rubric: CanvasRubric\n rubric_association: CanvasRubricAssociation\n }>(`/api/v1/courses/${courseId}/rubric_associations`, {\n method: 'POST',\n body: params.toString(),\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n })\n return envelope.rubric_association\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport { type CanvasQueryParams } from './query'\nimport type {\n CanvasQuiz,\n CanvasQuizSubmission,\n CanvasQuizQuestion,\n CanvasQuizSubmissionQuestion,\n CanvasQuizSubmissionEvent,\n CanvasQuizSubmissionEventsResponse,\n CanvasQuizExtension,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport class QuizzesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasQuiz[]> {\n return this.client.paginate<CanvasQuiz>(`/api/v1/courses/${courseId}/quizzes`)\n }\n\n async get(courseId: CanvasId, quizId: CanvasId): Promise<CanvasQuiz> {\n return this.client.request<CanvasQuiz>(`/api/v1/courses/${courseId}/quizzes/${quizId}`)\n }\n\n /**\n * Delete a Classic quiz. Per the Canvas Quizzes API docs, this \"deletes a\n * quiz and returns the deleted quiz object\".\n * @see https://canvas.instructure.com/doc/api/quizzes.html#method.quizzes/quizzes_api.destroy\n */\n async delete(courseId: CanvasId, quizId: CanvasId): Promise<CanvasQuiz> {\n return this.client.request<CanvasQuiz>(`/api/v1/courses/${courseId}/quizzes/${quizId}`, {\n method: 'DELETE',\n })\n }\n\n /**\n * Delete a question from a Classic quiz. Per the Canvas Quiz Questions API\n * docs, a successful deletion returns 204 No Content, so there is no body.\n * @see https://canvas.instructure.com/doc/api/quiz_questions.html\n */\n async deleteQuestion(courseId: CanvasId, quizId: CanvasId, questionId: CanvasId): Promise<void> {\n await this.client.request<void>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/questions/${questionId}`,\n { method: 'DELETE' },\n )\n }\n\n async listSubmissions(courseId: CanvasId, quizId: CanvasId): Promise<CanvasQuizSubmission[]> {\n return this.client.paginateEnvelope<CanvasQuizSubmission>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/submissions`,\n 'quiz_submissions',\n )\n }\n\n async listQuestions(courseId: CanvasId, quizId: CanvasId): Promise<CanvasQuizQuestion[]> {\n return this.client.paginate<CanvasQuizQuestion>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/questions`,\n )\n }\n\n /**\n * The questions one quiz-submission attempt was actually served, via\n * `GET /courses/:id/quizzes/:id/questions?quiz_submission_id=&quiz_submission_attempt=`.\n * Unlike `listQuestions` (the quiz's active questions only), this includes the\n * `generated` QuizQuestion rows Canvas creates for bank draws, with the text,\n * type and points_possible that attempt saw. Canvas needs BOTH parameters: with\n * either missing it silently returns the active list instead.\n */\n async listSubmissionQuestions(\n courseId: CanvasId,\n quizId: CanvasId,\n quizSubmissionId: CanvasId,\n attempt: number,\n ): Promise<CanvasQuizQuestion[]> {\n return this.client.paginate<CanvasQuizQuestion>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/questions`,\n { quiz_submission_id: quizSubmissionId, quiz_submission_attempt: attempt },\n )\n }\n\n /**\n * `GET /quiz_submissions/:id/questions` — per Canvas's own API doc header,\n * \"answering and flagging questions in a quiz-taking session\". It serves\n * two unrelated shapes depending on attempt state (`lib/api/v1/quiz_submission_question.rb:89-110`,\n * pinned SHA 1c9f0bb8013ed69c4f2efe11fd483025469b7e6c):\n *\n * - `untaken` (in progress): `submission_data` is a Hash; the response\n * includes the student's live `answer` and a real `flagged` state.\n * - `complete` / `pending_review` (submitted): `submission_data` is an\n * Array; `answer` is NEVER assigned, `flagged` is hardcoded `false` (or\n * omitted if Canvas has no graded entry for the question at all), and\n * `correct` carries the real {@link CanvasQuizCorrectness} domain.\n *\n * For a submitted attempt's actual answer text, use the Submissions API's\n * `include[]=submission_history` instead (see\n * `src/tools/quiz-question-responses.ts` and BRU-3080) — this method alone\n * cannot surface it.\n */\n async getSubmissionAnswers(quizSubmissionId: CanvasId): Promise<CanvasQuizSubmissionQuestion[]> {\n return this.client.paginateEnvelope<CanvasQuizSubmissionQuestion>(\n `/api/v1/quiz_submissions/${quizSubmissionId}/questions`,\n 'quiz_submission_questions',\n { 'include[]': 'quiz_question' },\n )\n }\n\n async scoreQuestion(\n courseId: CanvasId,\n quizId: CanvasId,\n submissionId: CanvasId,\n questionId: CanvasId,\n score: number,\n comment?: string,\n attempt?: number,\n ): Promise<void> {\n const submission: Record<string, unknown> = {\n questions: {\n [questionId]: { score, comment },\n },\n }\n if (attempt !== undefined) {\n submission.attempt = attempt\n }\n const body: Record<string, unknown> = {\n quiz_submissions: [submission],\n }\n await this.client.request(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/submissions/${submissionId}`,\n {\n method: 'PUT',\n body: JSON.stringify(body),\n },\n )\n }\n\n async getSubmissionEvents(\n courseId: CanvasId,\n quizId: CanvasId,\n submissionId: CanvasId,\n attempt?: number,\n ): Promise<CanvasQuizSubmissionEvent[]> {\n const query: CanvasQueryParams = {}\n if (attempt !== undefined) {\n query.attempt = attempt\n }\n const response = await this.client.request<CanvasQuizSubmissionEventsResponse>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/submissions/${submissionId}/events`,\n { query },\n )\n // Defensive: Canvas returns [] for an empty log; the guard also tolerates a\n // null field without throwing. Real errors surface as CanvasApiError above.\n return response.quiz_submission_events ?? []\n }\n\n /**\n * Apply a quiz extension (extra time / extra attempts) for one student on one\n * Classic Quiz via `POST /courses/:id/quizzes/:id/extensions`. Canvas accepts a\n * batch (`quiz_extensions` array), but callers operate per-student-per-quiz, so\n * the array always holds a single element. Omitted fields are left out of the\n * body — never pass `extra_time: 0` (Canvas rejects zero/negative extensions).\n */\n async setExtension(\n courseId: CanvasId,\n quizId: CanvasId,\n userId: CanvasId,\n extra_time?: number,\n extra_attempts?: number,\n ): Promise<CanvasQuizExtension[]> {\n // Not `Record<string, number>` any more (BRU-2730 §8 PR 1b): `user_id` is\n // an identifier and `extra_time` / `extra_attempts` are quantities, so the\n // two cannot share one value type. Spelling the three keys out is what\n // makes that split checkable instead of a comment. Emitting `user_id` as a\n // JSON string is also the correct form — a numeric `user_id` in a request\n // body reaches Canvas's `Api::ID_REGEX.match?(42)` and raises there (§2.4).\n const extension: { user_id: CanvasId; extra_time?: number; extra_attempts?: number } = {\n user_id: userId,\n }\n if (extra_time !== undefined) extension.extra_time = extra_time\n if (extra_attempts !== undefined) extension.extra_attempts = extra_attempts\n const response = await this.client.request<{ quiz_extensions: CanvasQuizExtension[] }>(\n `/api/v1/courses/${courseId}/quizzes/${quizId}/extensions`,\n {\n method: 'POST',\n body: JSON.stringify({ quiz_extensions: [extension] }),\n },\n )\n return response.quiz_extensions\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasFile, CanvasFileUploadInfo, CanvasFolder, DownloadedFile } from './types'\nimport type { CanvasId } from './id'\n\nconst MAX_DOWNLOAD_BYTES = 10 * 1024 * 1024 // 10 MB\n\nconst TEXT_CONTENT_TYPES = new Set([\n 'application/json',\n 'application/xml',\n 'application/javascript',\n 'application/x-javascript',\n])\n\nfunction isTextContentType(contentType: string): boolean {\n const base = (contentType.split(';')[0] ?? '').trim().toLowerCase()\n return base.startsWith('text/') || TEXT_CONTENT_TYPES.has(base)\n}\n\nexport class FilesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasFile[]> {\n return this.client.paginate<CanvasFile>(`/api/v1/courses/${courseId}/files`)\n }\n\n async listFolders(courseId: CanvasId): Promise<CanvasFolder[]> {\n return this.client.paginate<CanvasFolder>(`/api/v1/courses/${courseId}/folders`)\n }\n\n async get(courseId: CanvasId, fileId: CanvasId): Promise<CanvasFile> {\n return this.client.request<CanvasFile>(`/api/v1/courses/${courseId}/files/${fileId}`)\n }\n\n async upload(\n courseId: CanvasId,\n name: string,\n contentBase64: string,\n contentType: string,\n parentFolderPath?: string,\n ): Promise<CanvasFile> {\n const content = Buffer.from(contentBase64, 'base64')\n if (content.toString('base64') !== contentBase64) {\n throw new Error('Invalid base64 content: string is not valid base64 encoding')\n }\n\n // Step 1: Notify Canvas of the pending upload\n const uploadBody: Record<string, string> = {\n name,\n content_type: contentType,\n size: String(content.length),\n }\n if (parentFolderPath) {\n uploadBody.parent_folder_path = parentFolderPath\n }\n\n const uploadInfo = await this.client.request<CanvasFileUploadInfo>(\n `/api/v1/courses/${courseId}/files`,\n { method: 'POST', body: JSON.stringify(uploadBody) },\n )\n\n return this.completeUpload(content, name, contentType, uploadInfo)\n }\n\n async uploadToSubmission(\n courseId: CanvasId,\n assignmentId: CanvasId,\n name: string,\n contentBase64: string,\n contentType: string,\n ): Promise<CanvasFile> {\n const content = Buffer.from(contentBase64, 'base64')\n if (content.toString('base64') !== contentBase64) {\n throw new Error('Invalid base64 content: string is not valid base64 encoding')\n }\n\n const uploadInfo = await this.client.request<CanvasFileUploadInfo>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/self/files`,\n {\n method: 'POST',\n body: JSON.stringify({ name, content_type: contentType, size: String(content.length) }),\n },\n )\n\n return this.completeUpload(content, name, contentType, uploadInfo)\n }\n\n private async completeUpload(\n content: Uint8Array<ArrayBuffer>,\n name: string,\n contentType: string,\n uploadInfo: CanvasFileUploadInfo,\n ): Promise<CanvasFile> {\n // Step 2: POST file to upload_url (may be S3 — must NOT add Authorization header)\n const form = new FormData()\n for (const [key, value] of Object.entries(uploadInfo.upload_params)) {\n form.append(key, value)\n }\n form.append('file', new Blob([content], { type: contentType }), name)\n\n // Self-hosted Canvas instances (local/instfs storage) return a relative upload_url.\n // Resolve it against baseUrl so Node's fetch receives a valid absolute URL.\n const uploadUrl = uploadInfo.upload_url.startsWith('http')\n ? uploadInfo.upload_url\n : new URL(uploadInfo.upload_url, `${this.client.baseUrl}/`).toString()\n\n // Use redirect: 'manual' so we can handle the 303 → Canvas confirm step ourselves\n let s3Response: Response\n try {\n s3Response = await fetch(uploadUrl, {\n method: 'POST',\n body: form,\n redirect: 'manual',\n })\n } catch (networkErr) {\n let hostname: string\n try {\n hostname = new URL(uploadUrl).hostname\n } catch {\n hostname = uploadUrl\n }\n throw new Error(`Unable to reach file storage (${hostname})`, { cause: networkErr })\n }\n\n // Step 3: If S3 returned a redirect, POST to Canvas confirm URL\n if (s3Response.status >= 300 && s3Response.status < 400) {\n const confirmUrl = s3Response.headers.get('location')\n if (!confirmUrl) {\n throw new Error(`File upload redirect missing Location header (HTTP ${s3Response.status})`)\n }\n let confirmHostname: string\n try {\n confirmHostname = new URL(confirmUrl).hostname\n } catch {\n confirmHostname = ''\n }\n const canvasHostname = new URL(this.client.baseUrl).hostname\n if (confirmHostname !== canvasHostname) {\n throw new Error(\n `File upload redirect points to unexpected host (${confirmHostname}); expected Canvas host (${canvasHostname})`,\n )\n }\n return this.client.request<CanvasFile>(confirmUrl, { method: 'POST' })\n }\n\n if (!s3Response.ok) {\n const body = await s3Response.text()\n throw new Error(\n `File upload to storage failed (HTTP ${s3Response.status}): ${body.slice(0, 500)}`,\n )\n }\n\n const responseText = await s3Response.text()\n let parsed: unknown\n try {\n parsed = JSON.parse(responseText)\n } catch (parseErr) {\n throw new Error(\n `File upload response is not valid JSON (${parseErr instanceof Error ? parseErr.message : String(parseErr)}): ${responseText.slice(0, 500)}`,\n { cause: parseErr },\n )\n }\n if (typeof parsed !== 'object' || parsed === null || !('id' in parsed)) {\n throw new Error(\n `File upload returned unexpected response (no file ID): ${responseText.slice(0, 500)}`,\n )\n }\n return parsed as CanvasFile\n }\n\n async download(fileId: CanvasId, courseId?: CanvasId): Promise<DownloadedFile> {\n const endpoint =\n courseId != null ? `/api/v1/courses/${courseId}/files/${fileId}` : `/api/v1/files/${fileId}`\n const meta = await this.client.request<CanvasFile>(endpoint)\n\n if (meta.size > MAX_DOWNLOAD_BYTES) {\n throw new Error(\n `File too large to download (${meta.size.toLocaleString()} bytes; limit is 10 MB)`,\n )\n }\n\n const response = await fetch(meta.url)\n if (!response.ok) {\n throw new Error(`Failed to download file content (HTTP ${response.status})`)\n }\n\n const contentLengthHeader = response.headers.get('content-length')\n if (contentLengthHeader != null) {\n const declared = parseInt(contentLengthHeader, 10)\n if (!isNaN(declared) && declared > MAX_DOWNLOAD_BYTES) {\n throw new Error(\n `File too large to download (${declared.toLocaleString()} bytes declared; limit is 10 MB)`,\n )\n }\n }\n\n const contentType = (\n (response.headers.get('content-type') ?? meta.content_type).split(';')[0] ?? ''\n ).trim()\n const filename = meta.display_name ?? meta.filename ?? `file-${fileId}`\n const buffer = await response.arrayBuffer()\n\n if (buffer.byteLength > MAX_DOWNLOAD_BYTES) {\n throw new Error(\n `File too large to download (${buffer.byteLength.toLocaleString()} bytes; limit is 10 MB)`,\n )\n }\n\n if (isTextContentType(contentType)) {\n return {\n type: 'text',\n filename,\n contentType,\n size: buffer.byteLength,\n text: Buffer.from(buffer).toString('utf-8'),\n }\n }\n\n return {\n type: 'resource',\n filename,\n contentType,\n size: buffer.byteLength,\n base64: Buffer.from(buffer).toString('base64'),\n }\n }\n\n async delete(fileId: CanvasId): Promise<CanvasFile> {\n return this.client.request<CanvasFile>(`/api/v1/files/${fileId}`, { method: 'DELETE' })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasUpcomingEvent, CanvasUser, CanvasUserProfile } from './types'\nimport type { CanvasId } from './id'\n\nexport type CourseUserEnrollmentType = 'student' | 'teacher' | 'ta' | 'observer' | 'designer'\n\nexport type CourseUserEnrollmentState = 'active' | 'invited' | 'rejected' | 'completed' | 'inactive'\n\nexport type CourseUserInclude =\n | 'email'\n | 'enrollments'\n | 'locked'\n | 'avatar_url'\n | 'test_student'\n | 'bio'\n | 'custom_links'\n | 'current_grading_period_scores'\n | 'uuid'\n\nexport type UserSort = 'username' | 'email' | 'sis_id' | 'integration_id' | 'last_login'\n\nexport type SearchUserInclude = 'email' | 'last_login' | 'avatar_url' | 'time_zone' | 'uuid'\n\nexport interface ListCourseUsersOptions {\n enrollment_type?: CourseUserEnrollmentType | ReadonlyArray<CourseUserEnrollmentType>\n enrollment_state?: ReadonlyArray<CourseUserEnrollmentState>\n enrollment_role_id?: CanvasId\n include?: ReadonlyArray<CourseUserInclude>\n user_ids?: ReadonlyArray<CanvasId>\n user_id?: CanvasId\n search_term?: string\n sort?: UserSort\n order?: 'asc' | 'desc'\n}\n\nexport interface SearchUsersOptions {\n sort?: UserSort\n order?: 'asc' | 'desc'\n include?: ReadonlyArray<SearchUserInclude>\n}\n\nexport class UsersModule {\n constructor(private client: CanvasHttpClient) {}\n\n async listStudents(courseId: CanvasId): Promise<CanvasUser[]> {\n return this.client.paginate<CanvasUser>(`/api/v1/courses/${courseId}/users`, {\n enrollment_type: ['student'],\n })\n }\n\n async get(userId: CanvasId): Promise<CanvasUser> {\n return this.client.request<CanvasUser>(`/api/v1/users/${userId}`)\n }\n\n async getSelf(): Promise<CanvasUser> {\n return this.client.request<CanvasUser>('/api/v1/users/self')\n }\n\n async getProfile(): Promise<CanvasUserProfile> {\n return this.client.request<CanvasUserProfile>('/api/v1/users/self/profile')\n }\n\n async searchUsers(\n accountId: CanvasId,\n searchTerm: string,\n opts: SearchUsersOptions = {},\n ): Promise<CanvasUser[]> {\n const params: CanvasQueryParams = { search_term: searchTerm }\n if (opts.sort) params.sort = opts.sort\n if (opts.order) params.order = opts.order\n if (opts.include && opts.include.length > 0) params.include = opts.include\n return this.client.paginate<CanvasUser>(`/api/v1/accounts/${accountId}/users`, params)\n }\n\n async listCourseUsers(\n courseId: CanvasId,\n opts: ListCourseUsersOptions = {},\n ): Promise<CanvasUser[]> {\n const params: CanvasQueryParams = {}\n if (opts.enrollment_type !== undefined) {\n params.enrollment_type = Array.isArray(opts.enrollment_type)\n ? opts.enrollment_type\n : [opts.enrollment_type]\n }\n if (opts.enrollment_state && opts.enrollment_state.length > 0) {\n params.enrollment_state = opts.enrollment_state\n }\n if (opts.enrollment_role_id !== undefined) params.enrollment_role_id = opts.enrollment_role_id\n if (opts.include && opts.include.length > 0) params.include = opts.include\n if (opts.user_ids && opts.user_ids.length > 0) params.user_ids = opts.user_ids\n if (opts.user_id !== undefined) params.user_id = opts.user_id\n if (opts.search_term) params.search_term = opts.search_term\n if (opts.sort) params.sort = opts.sort\n if (opts.order) params.order = opts.order\n return this.client.paginate<CanvasUser>(`/api/v1/courses/${courseId}/users`, params)\n }\n\n async getUpcomingAssignments(): Promise<CanvasUpcomingEvent[]> {\n // Canvas's upcoming_events endpoint ignores a `type` query param entirely (it calls\n // user.upcoming_events with only context_codes) and always returns a mix of calendar\n // events and assignments. Each item's own `type` field (\"event\" | \"assignment\" |\n // \"sub_assignment\", set server-side in assignment_event_json/event_json) is the only\n // reliable discriminator, so filter client-side instead.\n const events = await this.client.request<CanvasUpcomingEvent[]>(\n '/api/v1/users/self/upcoming_events',\n )\n return events.filter((event) => event.type === 'assignment')\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasGroup, CanvasGroupCategory, CanvasGroupMembership, CanvasUser } from './types'\nimport type { CanvasId } from './id'\n\nexport interface CreateGroupCategoryParams {\n name: string\n}\n\nexport interface CreateGroupParams {\n name: string\n description?: string\n join_level?: 'parent_context_auto_join' | 'parent_context_request' | 'invitation_only'\n}\n\nexport class GroupsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasGroup[]> {\n return this.client.paginate<CanvasGroup>(`/api/v1/courses/${courseId}/groups`)\n }\n\n async listMembers(groupId: CanvasId): Promise<CanvasUser[]> {\n return this.client.paginate<CanvasUser>(`/api/v1/groups/${groupId}/users`)\n }\n\n async listCategories(courseId: CanvasId): Promise<CanvasGroupCategory[]> {\n return this.client.paginate<CanvasGroupCategory>(`/api/v1/courses/${courseId}/group_categories`)\n }\n\n /** Canvas renders the created category with no root key. */\n async createCategory(\n courseId: CanvasId,\n params: CreateGroupCategoryParams,\n ): Promise<CanvasGroupCategory> {\n return this.client.request<CanvasGroupCategory>(\n `/api/v1/courses/${courseId}/group_categories`,\n { method: 'POST', body: JSON.stringify(params) },\n )\n }\n\n /**\n * Groups are created inside a group category, so this is addressed by the\n * category ID, not the course ID. Canvas renders the new group with no root\n * key (its `users` list is empty for a new group).\n */\n async create(groupCategoryId: CanvasId, params: CreateGroupParams): Promise<CanvasGroup> {\n return this.client.request<CanvasGroup>(`/api/v1/group_categories/${groupCategoryId}/groups`, {\n method: 'POST',\n body: JSON.stringify(params),\n })\n }\n\n /** Canvas deletes the group, removes its members, and answers with the deleted group. */\n async delete(groupId: CanvasId): Promise<CanvasGroup> {\n return this.client.request<CanvasGroup>(`/api/v1/groups/${groupId}`, { method: 'DELETE' })\n }\n\n /** Returns the membership Canvas creates (or the existing one, if the user is already a member). */\n async addMember(groupId: CanvasId, userId: CanvasId): Promise<CanvasGroupMembership> {\n return this.client.request<CanvasGroupMembership>(`/api/v1/groups/${groupId}/memberships`, {\n method: 'POST',\n body: JSON.stringify({ user_id: userId }),\n })\n }\n\n /** Canvas answers a membership removal with `{ \"ok\": true }`. */\n async removeMember(groupId: CanvasId, userId: CanvasId): Promise<{ ok: boolean }> {\n return this.client.request<{ ok: boolean }>(`/api/v1/groups/${groupId}/users/${userId}`, {\n method: 'DELETE',\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasEnrollment } from './types'\nimport type { CanvasId } from './id'\n\nexport type EnrollmentType =\n | 'StudentEnrollment'\n | 'TeacherEnrollment'\n | 'TaEnrollment'\n | 'DesignerEnrollment'\n | 'ObserverEnrollment'\n\nexport type EnrollmentState =\n | 'active'\n | 'invited'\n | 'creation_pending'\n | 'deleted'\n | 'rejected'\n | 'completed'\n | 'inactive'\n | 'current_and_invited'\n | 'current_and_future'\n | 'current_and_concluded'\n\nexport type EnrollmentInclude =\n | 'avatar_url'\n | 'group_ids'\n | 'locked'\n | 'observed_users'\n | 'can_be_removed'\n | 'uuid'\n | 'current_points'\n | 'grades'\n\nexport interface ListCourseEnrollmentsOptions {\n type?: ReadonlyArray<EnrollmentType>\n role?: ReadonlyArray<string>\n state?: ReadonlyArray<EnrollmentState>\n include?: ReadonlyArray<EnrollmentInclude>\n user_id?: CanvasId\n grading_period_id?: CanvasId\n enrollment_term_id?: CanvasId\n sis_account_id?: ReadonlyArray<string>\n sis_course_id?: ReadonlyArray<string>\n sis_section_id?: ReadonlyArray<string>\n sis_user_id?: ReadonlyArray<string>\n created_for_sis_id?: ReadonlyArray<boolean>\n}\n\nexport interface ListUserEnrollmentsOptions {\n type?: ReadonlyArray<EnrollmentType>\n role?: ReadonlyArray<string>\n state?: ReadonlyArray<EnrollmentState>\n include?: ReadonlyArray<EnrollmentInclude>\n grading_period_id?: CanvasId\n enrollment_term_id?: CanvasId\n}\n\nfunction buildParams(opts: object): CanvasQueryParams {\n const params: CanvasQueryParams = {}\n for (const [key, value] of Object.entries(opts)) {\n if (value === undefined || value === null) continue\n if (Array.isArray(value)) {\n if (value.length === 0) continue\n params[key] = value as ReadonlyArray<string | number | boolean>\n } else if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n params[key] = value\n }\n }\n return params\n}\n\nexport class EnrollmentsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(opts: ListUserEnrollmentsOptions = {}): Promise<CanvasEnrollment[]> {\n return this.client.paginate<CanvasEnrollment>(\n '/api/v1/users/self/enrollments',\n buildParams(opts),\n )\n }\n\n async listForCourse(\n courseId: CanvasId,\n opts: ListCourseEnrollmentsOptions = {},\n ): Promise<CanvasEnrollment[]> {\n return this.client.paginate<CanvasEnrollment>(\n `/api/v1/courses/${courseId}/enrollments`,\n buildParams(opts),\n )\n }\n\n async enroll(\n courseId: CanvasId,\n userId: CanvasId,\n type: string,\n enrollmentState?: string,\n ): Promise<CanvasEnrollment> {\n const body: Record<string, unknown> = {\n enrollment: {\n user_id: userId,\n type,\n ...(enrollmentState ? { enrollment_state: enrollmentState } : {}),\n },\n }\n return this.client.request<CanvasEnrollment>(`/api/v1/courses/${courseId}/enrollments`, {\n method: 'POST',\n body: JSON.stringify(body),\n })\n }\n\n async remove(\n courseId: CanvasId,\n enrollmentId: CanvasId,\n task: string,\n ): Promise<CanvasEnrollment> {\n return this.client.request<CanvasEnrollment>(\n `/api/v1/courses/${courseId}/enrollments/${enrollmentId}?task=${encodeURIComponent(task)}`,\n { method: 'DELETE' },\n )\n }\n\n async listMyGrades(courseId?: CanvasId): Promise<CanvasEnrollment[]> {\n if (courseId !== undefined) {\n return this.client.paginate<CanvasEnrollment>(`/api/v1/courses/${courseId}/enrollments`, {\n user_id: 'self',\n include: ['grades'],\n })\n }\n return this.client.paginate<CanvasEnrollment>('/api/v1/users/self/enrollments', {\n include: ['grades'],\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasDiscussionTopic,\n CanvasDiscussionEntry,\n CanvasAnnouncement,\n CreateDiscussionParams,\n UpdateDiscussionParams,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport class DiscussionsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasDiscussionTopic[]> {\n return this.client.paginate<CanvasDiscussionTopic>(\n `/api/v1/courses/${courseId}/discussion_topics`,\n )\n }\n\n async get(courseId: CanvasId, topicId: CanvasId): Promise<CanvasDiscussionTopic> {\n return this.client.request<CanvasDiscussionTopic>(\n `/api/v1/courses/${courseId}/discussion_topics/${topicId}?include[]=all_dates`,\n )\n }\n\n async listAnnouncements(courseId: CanvasId): Promise<CanvasAnnouncement[]> {\n return this.client.paginate<CanvasAnnouncement>(\n `/api/v1/courses/${courseId}/discussion_topics`,\n { only_announcements: 'true' },\n )\n }\n\n async listForGroup(groupId: CanvasId): Promise<CanvasDiscussionTopic[]> {\n return this.client.paginate<CanvasDiscussionTopic>(\n `/api/v1/groups/${groupId}/discussion_topics`,\n )\n }\n\n async getForGroup(groupId: CanvasId, topicId: CanvasId): Promise<CanvasDiscussionTopic> {\n return this.client.request<CanvasDiscussionTopic>(\n `/api/v1/groups/${groupId}/discussion_topics/${topicId}?include[]=all_dates`,\n )\n }\n\n async postEntry(\n courseId: CanvasId,\n topicId: CanvasId,\n message: string,\n ): Promise<CanvasDiscussionEntry> {\n return this.client.request<CanvasDiscussionEntry>(\n `/api/v1/courses/${courseId}/discussion_topics/${topicId}/entries`,\n {\n method: 'POST',\n body: JSON.stringify({ message }),\n },\n )\n }\n\n async create(courseId: CanvasId, params: CreateDiscussionParams): Promise<CanvasDiscussionTopic> {\n return this.client.request<CanvasDiscussionTopic>(\n `/api/v1/courses/${courseId}/discussion_topics`,\n {\n method: 'POST',\n body: JSON.stringify(params),\n },\n )\n }\n\n async update(\n courseId: CanvasId,\n topicId: CanvasId,\n params: UpdateDiscussionParams,\n ): Promise<CanvasDiscussionTopic> {\n return this.client.request<CanvasDiscussionTopic>(\n `/api/v1/courses/${courseId}/discussion_topics/${topicId}`,\n {\n method: 'PUT',\n body: JSON.stringify(params),\n },\n )\n }\n\n async delete(courseId: CanvasId, topicId: CanvasId): Promise<void> {\n await this.client.request<void>(`/api/v1/courses/${courseId}/discussion_topics/${topicId}`, {\n method: 'DELETE',\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasModule, CanvasModuleItem, CanvasCourseStructure } from './types'\nimport type { CanvasId } from './id'\n\nexport interface CreateModuleItemParams {\n title: string\n type: string\n content_id?: CanvasId\n /** Page slug — Page items are addressed by URL slug, not by `content_id`. */\n page_url?: string\n external_url?: string\n position?: number\n indent?: number\n new_tab?: boolean\n}\n\nexport interface UpdateModuleItemParams {\n title?: string\n external_url?: string\n position?: number\n indent?: number\n new_tab?: boolean\n published?: boolean\n /** Move the item to a different module. */\n module_id?: CanvasId\n}\n\nexport class ModulesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasModule[]> {\n return this.client.paginate<CanvasModule>(`/api/v1/courses/${courseId}/modules`)\n }\n\n /**\n * List a course's modules with each module's items inlined via `include[]=items`.\n * Unlike `getCourseStructure`, this returns the raw Canvas shapes (including the\n * `published` state on every module and item, even unpublished ones), which the\n * course-setup health check needs to detect unpublished content.\n */\n async listWithItems(\n courseId: CanvasId,\n ): Promise<(CanvasModule & { items?: CanvasModuleItem[] })[]> {\n return this.client.paginate<CanvasModule & { items?: CanvasModuleItem[] }>(\n `/api/v1/courses/${courseId}/modules`,\n { include: ['items'] },\n )\n }\n\n async get(courseId: CanvasId, moduleId: CanvasId): Promise<CanvasModule> {\n return this.client.request<CanvasModule>(`/api/v1/courses/${courseId}/modules/${moduleId}`)\n }\n\n async listItems(courseId: CanvasId, moduleId: CanvasId): Promise<CanvasModuleItem[]> {\n return this.client.paginate<CanvasModuleItem>(\n `/api/v1/courses/${courseId}/modules/${moduleId}/items`,\n )\n }\n\n async create(\n courseId: CanvasId,\n params: {\n name: string\n position?: number\n unlock_at?: string\n prerequisite_module_ids?: CanvasId[]\n },\n ): Promise<CanvasModule> {\n return this.client.request<CanvasModule>(`/api/v1/courses/${courseId}/modules`, {\n method: 'POST',\n body: JSON.stringify({ module: params }),\n })\n }\n\n async update(\n courseId: CanvasId,\n moduleId: CanvasId,\n params: { name?: string; position?: number; published?: boolean },\n ): Promise<CanvasModule> {\n return this.client.request<CanvasModule>(`/api/v1/courses/${courseId}/modules/${moduleId}`, {\n method: 'PUT',\n body: JSON.stringify({ module: params }),\n })\n }\n\n /**\n * Canvas answers a module DELETE with the deleted module's body (the\n * controller renders `module_json` with no root key). The module's items go\n * with it; the content they link to is stored separately in the course.\n */\n async delete(courseId: CanvasId, moduleId: CanvasId): Promise<CanvasModule> {\n return this.client.request<CanvasModule>(`/api/v1/courses/${courseId}/modules/${moduleId}`, {\n method: 'DELETE',\n })\n }\n\n async createItem(\n courseId: CanvasId,\n moduleId: CanvasId,\n params: CreateModuleItemParams,\n ): Promise<CanvasModuleItem> {\n return this.client.request<CanvasModuleItem>(\n `/api/v1/courses/${courseId}/modules/${moduleId}/items`,\n {\n method: 'POST',\n body: JSON.stringify({ module_item: params }),\n },\n )\n }\n\n async updateItem(\n courseId: CanvasId,\n moduleId: CanvasId,\n itemId: CanvasId,\n params: UpdateModuleItemParams,\n ): Promise<CanvasModuleItem> {\n return this.client.request<CanvasModuleItem>(\n `/api/v1/courses/${courseId}/modules/${moduleId}/items/${itemId}`,\n {\n method: 'PUT',\n body: JSON.stringify({ module_item: params }),\n },\n )\n }\n\n /** Canvas answers a module-item DELETE with the deleted item's body. */\n async deleteItem(\n courseId: CanvasId,\n moduleId: CanvasId,\n itemId: CanvasId,\n ): Promise<CanvasModuleItem> {\n return this.client.request<CanvasModuleItem>(\n `/api/v1/courses/${courseId}/modules/${moduleId}/items/${itemId}`,\n { method: 'DELETE' },\n )\n }\n\n async getCourseStructure(\n courseId: CanvasId,\n opts: { includePublishedOnly?: boolean; includeContentDetails?: boolean } = {},\n ): Promise<CanvasCourseStructure> {\n const include: string[] = ['items']\n if (opts.includeContentDetails) include.push('content_details')\n\n const modules = await this.client.paginate<CanvasModule & { items?: CanvasModuleItem[] }>(\n `/api/v1/courses/${courseId}/modules`,\n { include },\n )\n\n const itemsByType: Record<string, number> = {}\n let totalItems = 0\n\n const filteredModules = modules.map((mod) => {\n let items = mod.items ?? []\n if (opts.includePublishedOnly) {\n items = items.filter((item) => item.published)\n }\n for (const item of items) {\n itemsByType[item.type] = (itemsByType[item.type] ?? 0) + 1\n totalItems++\n }\n return {\n id: mod.id,\n name: mod.name,\n position: mod.position,\n state: mod.state ?? (mod.published ? 'active' : 'unpublished'),\n unlock_at: mod.unlock_at,\n items: items.map((item) => ({\n id: item.id,\n title: item.title,\n type: item.type,\n position: item.position,\n published: item.published,\n html_url: item.html_url,\n page_url: item.page_url,\n content_id: item.content_id,\n content_details: item.content_details,\n })),\n }\n })\n\n return {\n modules: filteredModules,\n summary: {\n total_modules: filteredModules.length,\n total_items: totalItems,\n items_by_type: itemsByType,\n },\n }\n }\n}\n","/**\n * Run `fn` across `items` with at most `limit` invocations in flight at once,\n * returning the results in the SAME order as `items` regardless of the order in\n * which the individual calls settle (`results[i]` always corresponds to\n * `items[i]`).\n *\n * This is the single shared implementation of the batched fan-out first written\n * inline for `find_student_across_courses` (PR #252). Whole-course scans such as\n * {@link PagesModule.listWithBodies} would otherwise issue one Canvas request per\n * item all at once — a 300-page course means 300 simultaneous requests with no\n * backstop. Bounding the fan-out keeps a large course from hammering Canvas.\n *\n * Error semantics match a plain `Promise.all`: the first rejection propagates\n * unchanged and no further batches are started. Call sites that want to tolerate\n * per-item failures (like `find_student_across_courses`) do so by catching inside\n * `fn` and resolving with a discriminated result, never by letting `fn` throw.\n */\nexport async function mapWithConcurrency<T, R>(\n items: readonly T[],\n limit: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n if (limit < 1) {\n throw new RangeError(`mapWithConcurrency: limit must be >= 1, got ${limit}`)\n }\n\n const results = new Array<R>(items.length)\n for (let start = 0; start < items.length; start += limit) {\n const batch = items.slice(start, start + limit)\n const settled = await Promise.all(batch.map((item, offset) => fn(item, start + offset)))\n settled.forEach((result, offset) => {\n results[start + offset] = result\n })\n }\n return results\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasPage } from './types'\nimport { mapWithConcurrency } from './concurrency'\nimport type { CanvasId } from './id'\n\n/**\n * Max concurrent page-body fetches issued by {@link PagesModule.listWithBodies}.\n * Matches the `CONCURRENT_COURSE_LIMIT` precedent in `student-search.ts`; kept\n * as a named export so the cap is greppable and adjustable in one place.\n */\nexport const PAGE_BODY_CONCURRENCY_LIMIT = 10\n\nexport class PagesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasPage[]> {\n return this.client.paginate<CanvasPage>(`/api/v1/courses/${courseId}/pages`)\n }\n\n async get(courseId: CanvasId, pageUrl: string): Promise<CanvasPage> {\n return this.client.request<CanvasPage>(\n `/api/v1/courses/${courseId}/pages/${encodeURIComponent(pageUrl)}`,\n )\n }\n\n /**\n * List every page in a course with its full HTML `body`. The paginated list\n * endpoint returns page stubs without `body`, so this fans out a `get()` per\n * page, bounded to at most {@link PAGE_BODY_CONCURRENCY_LIMIT} concurrent\n * fetches so a large course does not open one Canvas request per page all at\n * once. Result order matches the listed page order. Used by the link-audit and\n * accessibility-audit tools to scan page content. `CanvasApiError` from either\n * the list or any individual fetch propagates unchanged.\n */\n async listWithBodies(courseId: CanvasId): Promise<CanvasPage[]> {\n const stubs = await this.client.paginate<CanvasPage>(`/api/v1/courses/${courseId}/pages`)\n return mapWithConcurrency(stubs, PAGE_BODY_CONCURRENCY_LIMIT, (stub) =>\n this.get(courseId, stub.url),\n )\n }\n\n async create(\n courseId: CanvasId,\n params: { title: string; body?: string; published?: boolean; editing_roles?: string },\n ): Promise<CanvasPage> {\n return this.client.request<CanvasPage>(`/api/v1/courses/${courseId}/pages`, {\n method: 'POST',\n body: JSON.stringify({ wiki_page: params }),\n })\n }\n\n async update(\n courseId: CanvasId,\n pageUrl: string,\n params: { title?: string; body?: string; published?: boolean; editing_roles?: string },\n ): Promise<CanvasPage> {\n return this.client.request<CanvasPage>(\n `/api/v1/courses/${courseId}/pages/${encodeURIComponent(pageUrl)}`,\n {\n method: 'PUT',\n body: JSON.stringify({ wiki_page: params }),\n },\n )\n }\n\n async delete(courseId: CanvasId, pageUrl: string): Promise<void> {\n await this.client.request<void>(\n `/api/v1/courses/${courseId}/pages/${encodeURIComponent(pageUrl)}`,\n { method: 'DELETE' },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasCalendarEvent } from './types'\nimport type { CanvasId } from './id'\n\nexport type CalendarEventType = 'event' | 'assignment' | 'sub_assignment'\n\nexport interface ListCalendarEventsOptions {\n type?: CalendarEventType\n start_date?: string\n end_date?: string\n}\n\nexport class CalendarModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(\n courseId: CanvasId,\n opts: ListCalendarEventsOptions = {},\n ): Promise<CanvasCalendarEvent[]> {\n const params: CanvasQueryParams = {\n 'context_codes[]': `course_${courseId}`,\n }\n if (opts.type) params.type = opts.type\n if (opts.start_date) params.start_date = opts.start_date\n if (opts.end_date) params.end_date = opts.end_date\n return this.client.paginate<CanvasCalendarEvent>('/api/v1/calendar_events', params)\n }\n\n async createEvent(params: {\n context_code: string\n title: string\n start_at: string\n end_at?: string\n description?: string\n location_name?: string\n }): Promise<CanvasCalendarEvent> {\n return this.client.request<CanvasCalendarEvent>('/api/v1/calendar_events', {\n method: 'POST',\n body: JSON.stringify({ calendar_event: params }),\n })\n }\n\n async updateEvent(\n eventId: CanvasId,\n params: {\n title?: string\n start_at?: string\n end_at?: string\n description?: string\n location_name?: string\n },\n ): Promise<CanvasCalendarEvent> {\n return this.client.request<CanvasCalendarEvent>(`/api/v1/calendar_events/${eventId}`, {\n method: 'PUT',\n body: JSON.stringify({ calendar_event: params }),\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasConversation,\n CanvasConversationDetail,\n CanvasConversationUnreadCount,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport class ConversationsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(): Promise<CanvasConversation[]> {\n return this.client.paginate<CanvasConversation>('/api/v1/conversations')\n }\n\n async get(conversationId: CanvasId): Promise<CanvasConversationDetail> {\n return this.client.request<CanvasConversationDetail>(`/api/v1/conversations/${conversationId}`)\n }\n\n async getUnreadCount(): Promise<CanvasConversationUnreadCount> {\n const raw = await this.client.request<{ unread_count: string }>(\n '/api/v1/conversations/unread_count',\n )\n const count = parseInt(raw.unread_count, 10)\n if (Number.isNaN(count)) {\n throw new Error(\n `Canvas returned unexpected unread_count value: ${JSON.stringify(raw.unread_count)}`,\n )\n }\n return { unread_count: count }\n }\n\n async send(recipients: string[], subject: string, body: string): Promise<CanvasConversation[]> {\n return this.client.request<CanvasConversation[]>('/api/v1/conversations', {\n method: 'POST',\n body: JSON.stringify({\n recipients,\n subject,\n body,\n }),\n })\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasPeerReview } from './types'\nimport type { CanvasId } from './id'\n\nexport class PeerReviewsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async listForAssignment(courseId: CanvasId, assignmentId: CanvasId): Promise<CanvasPeerReview[]> {\n return this.client.paginate<CanvasPeerReview>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/peer_reviews`,\n )\n }\n\n async listForSubmission(\n courseId: CanvasId,\n assignmentId: CanvasId,\n submissionId: CanvasId,\n ): Promise<CanvasPeerReview[]> {\n return this.client.paginate<CanvasPeerReview>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${submissionId}/peer_reviews`,\n )\n }\n\n async create(\n courseId: CanvasId,\n assignmentId: CanvasId,\n submissionId: CanvasId,\n userId: CanvasId,\n ): Promise<CanvasPeerReview> {\n return this.client.request<CanvasPeerReview>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${submissionId}/peer_reviews`,\n {\n method: 'POST',\n body: JSON.stringify({ user_id: userId }),\n },\n )\n }\n\n async delete(\n courseId: CanvasId,\n assignmentId: CanvasId,\n submissionId: CanvasId,\n userId: CanvasId,\n ): Promise<void> {\n await this.client.request<void>(\n `/api/v1/courses/${courseId}/assignments/${assignmentId}/submissions/${submissionId}/peer_reviews?user_id=${userId}`,\n { method: 'DELETE' },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasAccount,\n CanvasAccountNotification,\n CanvasAccountReport,\n CanvasCourse,\n CanvasUser,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport class AccountsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async get(accountId: CanvasId): Promise<CanvasAccount> {\n return this.client.request<CanvasAccount>(`/api/v1/accounts/${accountId}`)\n }\n\n async list(): Promise<CanvasAccount[]> {\n return this.client.paginate<CanvasAccount>('/api/v1/accounts')\n }\n\n async listSubAccounts(accountId: CanvasId): Promise<CanvasAccount[]> {\n return this.client.paginate<CanvasAccount>(`/api/v1/accounts/${accountId}/sub_accounts`)\n }\n\n async listCourses(\n accountId: CanvasId,\n params?: { search_term?: string },\n ): Promise<CanvasCourse[]> {\n const query: Record<string, string> = {}\n if (params?.search_term) query.search_term = params.search_term\n return this.client.paginate<CanvasCourse>(\n `/api/v1/accounts/${accountId}/courses`,\n Object.keys(query).length ? query : undefined,\n )\n }\n\n async listUsers(accountId: CanvasId, params?: { search_term?: string }): Promise<CanvasUser[]> {\n const query: Record<string, string> = {}\n if (params?.search_term) query.search_term = params.search_term\n return this.client.paginate<CanvasUser>(\n `/api/v1/accounts/${accountId}/users`,\n Object.keys(query).length ? query : undefined,\n )\n }\n\n async getReports(accountId: CanvasId): Promise<CanvasAccountReport[]> {\n return this.client.request<CanvasAccountReport[]>(`/api/v1/accounts/${accountId}/reports`)\n }\n\n async listNotifications(accountId: string): Promise<CanvasAccountNotification[]> {\n return this.client.paginate<CanvasAccountNotification>(\n `/api/v1/accounts/${accountId}/account_notifications`,\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasAssignmentAnalytics,\n CanvasCourseActivitySummary,\n CanvasStudentActivitySummary,\n CanvasActivityStreamItem,\n CanvasStudentSummary,\n CourseSearchResult,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport const SEARCH_CONTENT_TYPES = [\n 'pages',\n 'discussions',\n 'assignments',\n 'announcements',\n] as const\nexport type SearchContentType = (typeof SEARCH_CONTENT_TYPES)[number]\n\nexport class AnalyticsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async searchContentType(\n courseId: CanvasId,\n searchTerm: string,\n type: SearchContentType,\n ): Promise<CourseSearchResult[]> {\n switch (type) {\n case 'pages': {\n const pages = await this.client.paginate<{ page_id: CanvasId; title: string; url: string }>(\n `/api/v1/courses/${courseId}/pages`,\n { search_term: searchTerm },\n )\n return pages.map((p) => ({\n id: p.page_id,\n title: p.title,\n type: 'page' as const,\n url: p.url,\n course_id: courseId,\n }))\n }\n case 'assignments': {\n const assignments = await this.client.paginate<{ id: CanvasId; name: string }>(\n `/api/v1/courses/${courseId}/assignments`,\n { search_term: searchTerm },\n )\n return assignments.map((a) => ({\n id: a.id,\n title: a.name,\n type: 'assignment' as const,\n course_id: courseId,\n }))\n }\n case 'discussions': {\n const discussions = await this.client.paginate<{ id: CanvasId; title: string }>(\n `/api/v1/courses/${courseId}/discussion_topics`,\n { search_term: searchTerm },\n )\n return discussions.map((d) => ({\n id: d.id,\n title: d.title,\n type: 'discussion' as const,\n course_id: courseId,\n }))\n }\n case 'announcements': {\n const announcements = await this.client.paginate<{ id: CanvasId; title: string }>(\n `/api/v1/courses/${courseId}/discussion_topics`,\n { search_term: searchTerm, only_announcements: 'true' },\n )\n return announcements.map((a) => ({\n id: a.id,\n title: a.title,\n type: 'announcement' as const,\n course_id: courseId,\n }))\n }\n default: {\n const _exhaustive: never = type\n throw new Error(`Unknown content type: ${_exhaustive}`)\n }\n }\n }\n\n async getCourseActivity(courseId: CanvasId): Promise<CanvasCourseActivitySummary[]> {\n return this.client.request<CanvasCourseActivitySummary[]>(\n `/api/v1/courses/${courseId}/analytics/activity`,\n )\n }\n\n async getStudentActivity(\n courseId: CanvasId,\n studentId: CanvasId,\n ): Promise<CanvasStudentActivitySummary> {\n return this.client.request<CanvasStudentActivitySummary>(\n `/api/v1/courses/${courseId}/analytics/users/${studentId}/activity`,\n )\n }\n\n async getCourseActivityStream(courseId: CanvasId): Promise<CanvasActivityStreamItem[]> {\n return this.client.request<CanvasActivityStreamItem[]>(\n `/api/v1/courses/${courseId}/activity_stream/summary`,\n )\n }\n\n async getStudentSummaries(courseId: CanvasId): Promise<CanvasStudentSummary[]> {\n return this.client.paginate<CanvasStudentSummary>(\n `/api/v1/courses/${courseId}/analytics/student_summaries`,\n )\n }\n\n async getAssignmentAnalytics(courseId: CanvasId): Promise<CanvasAssignmentAnalytics[]> {\n return this.client.paginate<CanvasAssignmentAnalytics>(\n `/api/v1/courses/${courseId}/analytics/assignments`,\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasDashboardCard,\n CanvasTodoItem,\n CanvasUpcomingEvent,\n CanvasMissingSubmission,\n} from './types'\n\nexport class DashboardModule {\n constructor(private client: CanvasHttpClient) {}\n\n async getDashboardCards(): Promise<CanvasDashboardCard[]> {\n return this.client.request<CanvasDashboardCard[]>('/api/v1/dashboard/dashboard_cards')\n }\n\n async getTodoItems(): Promise<CanvasTodoItem[]> {\n return this.client.paginate<CanvasTodoItem>('/api/v1/users/self/todo')\n }\n\n async getUpcomingEvents(): Promise<CanvasUpcomingEvent[]> {\n return this.client.request<CanvasUpcomingEvent[]>('/api/v1/users/self/upcoming_events')\n }\n\n async getMissingSubmissions(): Promise<CanvasMissingSubmission[]> {\n return this.client.paginate<CanvasMissingSubmission>('/api/v1/users/self/missing_submissions')\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasOutcomeContextType,\n CanvasOutcome,\n CanvasOutcomeAlignment,\n CanvasOutcomeContributingScoresResponse,\n CanvasOutcomeGroup,\n CanvasOutcomeLink,\n CanvasOutcomeMasteryDistributionResponse,\n CanvasOutcomeResultsResponse,\n CanvasOutcomeRollupsResponse,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport const OUTCOME_CONTEXT_TYPES = ['account', 'course'] as const\nexport const OUTCOME_DETAIL_LEVELS = ['abbrev', 'full'] as const\nexport const OUTCOME_ROLLUP_AGGREGATE_STATS = ['mean', 'median'] as const\nexport const OUTCOME_ROLLUP_SORT_BY = ['student', 'outcome'] as const\nexport const OUTCOME_SORT_ORDER = ['asc', 'desc'] as const\nexport const OUTCOME_EXCLUDE_OPTIONS = ['missing_user_rollups', 'missing_outcome_results'] as const\n\ntype ContextPathSuffix = 'outcome_groups' | 'outcome_group_links' | 'root_outcome_group'\n\nexport class OutcomesModule {\n constructor(private client: CanvasHttpClient) {}\n\n async getRootOutcomeGroup(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n ): Promise<CanvasOutcomeGroup> {\n return this.client.request<CanvasOutcomeGroup>(this.buildContextPath(contextType, contextId))\n }\n\n async listOutcomeGroups(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n ): Promise<CanvasOutcomeGroup[]> {\n return this.client.paginate<CanvasOutcomeGroup>(\n this.buildContextPath(contextType, contextId, 'outcome_groups'),\n )\n }\n\n async listOutcomeGroupLinks(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n options?: { outcome_style?: 'abbrev' | 'full'; outcome_group_style?: 'abbrev' | 'full' },\n ): Promise<CanvasOutcomeLink[]> {\n return this.client.paginate<CanvasOutcomeLink>(\n this.withQuery(this.buildContextPath(contextType, contextId, 'outcome_group_links'), {\n outcome_style: options?.outcome_style,\n outcome_group_style: options?.outcome_group_style,\n }),\n )\n }\n\n async getOutcomeGroup(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n groupId: CanvasId,\n ): Promise<CanvasOutcomeGroup> {\n return this.client.request<CanvasOutcomeGroup>(\n `${this.buildContextPath(contextType, contextId, 'outcome_groups')}/${groupId}`,\n )\n }\n\n async listGroupOutcomes(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n groupId: CanvasId,\n options?: { outcome_style?: 'abbrev' | 'full' },\n ): Promise<CanvasOutcomeLink[]> {\n return this.client.paginate<CanvasOutcomeLink>(\n this.withQuery(\n `${this.buildContextPath(contextType, contextId, 'outcome_groups')}/${groupId}/outcomes`,\n { outcome_style: options?.outcome_style },\n ),\n )\n }\n\n async listGroupSubgroups(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n groupId: CanvasId,\n ): Promise<CanvasOutcomeGroup[]> {\n return this.client.paginate<CanvasOutcomeGroup>(\n `${this.buildContextPath(contextType, contextId, 'outcome_groups')}/${groupId}/subgroups`,\n )\n }\n\n async getOutcome(\n outcomeId: CanvasId,\n options?: { add_defaults?: boolean },\n ): Promise<CanvasOutcome> {\n return this.client.request<CanvasOutcome>(\n this.withQuery(`/api/v1/outcomes/${outcomeId}`, {\n add_defaults: options?.add_defaults,\n }),\n )\n }\n\n async getOutcomeAlignments(\n courseId: CanvasId,\n options?: { student_id?: CanvasId; assignment_id?: CanvasId },\n ): Promise<CanvasOutcomeAlignment[]> {\n return this.client.request<CanvasOutcomeAlignment[]>(\n this.withQuery(`/api/v1/courses/${courseId}/outcome_alignments`, {\n student_id: options?.student_id,\n assignment_id: options?.assignment_id,\n }),\n )\n }\n\n async getOutcomeResults(\n courseId: CanvasId,\n options?: {\n user_ids?: Array<CanvasId>\n outcome_ids?: CanvasId[]\n include_alignments?: boolean\n include_hidden?: boolean\n },\n ): Promise<CanvasOutcomeResultsResponse> {\n return this.client.request<CanvasOutcomeResultsResponse>(\n this.withQuery(`/api/v1/courses/${courseId}/outcome_results`, {\n 'user_ids[]': options?.user_ids,\n 'outcome_ids[]': options?.outcome_ids,\n 'include[]': options?.include_alignments ? ['alignments'] : undefined,\n include_hidden: options?.include_hidden,\n }),\n )\n }\n\n async getOutcomeRollups(\n courseId: CanvasId,\n options?: {\n aggregate?: 'course'\n aggregate_stat?: 'mean' | 'median'\n user_ids?: Array<CanvasId>\n outcome_ids?: CanvasId[]\n include_courses?: boolean\n exclude?: Array<'missing_user_rollups' | 'missing_outcome_results'>\n sort_by?: 'student' | 'outcome'\n sort_outcome_id?: CanvasId\n sort_order?: 'asc' | 'desc'\n add_defaults?: boolean\n },\n ): Promise<CanvasOutcomeRollupsResponse> {\n return this.client.request<CanvasOutcomeRollupsResponse>(\n this.withQuery(`/api/v1/courses/${courseId}/outcome_rollups`, {\n aggregate: options?.aggregate,\n aggregate_stat: options?.aggregate_stat,\n 'user_ids[]': options?.user_ids,\n 'outcome_ids[]': options?.outcome_ids,\n 'include[]': options?.include_courses ? ['courses'] : undefined,\n 'exclude[]': options?.exclude,\n sort_by: options?.sort_by,\n sort_outcome_id: options?.sort_outcome_id,\n sort_order: options?.sort_order,\n add_defaults: options?.add_defaults,\n }),\n )\n }\n\n async getOutcomeContributingScores(\n courseId: CanvasId,\n outcomeId: CanvasId,\n options?: {\n user_ids?: Array<CanvasId>\n only_assignment_alignments?: boolean\n show_unpublished_assignments?: boolean\n },\n ): Promise<CanvasOutcomeContributingScoresResponse> {\n return this.client.request<CanvasOutcomeContributingScoresResponse>(\n this.withQuery(`/api/v1/courses/${courseId}/outcomes/${outcomeId}/contributing_scores`, {\n 'user_ids[]': options?.user_ids,\n only_assignment_alignments: options?.only_assignment_alignments,\n show_unpublished_assignments: options?.show_unpublished_assignments,\n }),\n )\n }\n\n async getOutcomeMasteryDistribution(\n courseId: CanvasId,\n options?: {\n exclude?: Array<'missing_user_rollups' | 'missing_outcome_results'>\n outcome_ids?: CanvasId[]\n student_ids?: Array<CanvasId>\n include_alignment_distributions?: boolean\n only_assignment_alignments?: boolean\n show_unpublished_assignments?: boolean\n add_defaults?: boolean\n },\n ): Promise<CanvasOutcomeMasteryDistributionResponse> {\n return this.client.request<CanvasOutcomeMasteryDistributionResponse>(\n this.withQuery(`/api/v1/courses/${courseId}/outcome_mastery_distribution`, {\n 'exclude[]': options?.exclude,\n 'outcome_ids[]': options?.outcome_ids,\n 'student_ids[]': options?.student_ids,\n 'include[]': options?.include_alignment_distributions\n ? ['alignment_distributions']\n : undefined,\n only_assignment_alignments: options?.only_assignment_alignments,\n show_unpublished_assignments: options?.show_unpublished_assignments,\n add_defaults: options?.add_defaults,\n }),\n )\n }\n\n private buildContextPath(\n contextType: CanvasOutcomeContextType,\n contextId: CanvasId,\n suffix: ContextPathSuffix = 'root_outcome_group',\n ): string {\n const segment = contextType === 'account' ? 'accounts' : 'courses'\n return `/api/v1/${segment}/${contextId}/${suffix}`\n }\n\n private withQuery(\n endpoint: string,\n params: Record<string, string | number | boolean | Array<string | number> | undefined>,\n ): string {\n const searchParams = new URLSearchParams()\n\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined) continue\n if (Array.isArray(value)) {\n for (const item of value) searchParams.append(key, String(item))\n continue\n }\n searchParams.set(key, String(value))\n }\n\n const query = searchParams.toString()\n return query ? `${endpoint}?${query}` : endpoint\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type {\n CanvasGradebookHistoryDay,\n CanvasGradebookHistoryGrader,\n CanvasGradebookHistorySubmission,\n CanvasGradebookHistorySubmissionVersion,\n} from './types'\nimport type { CanvasId } from './id'\n\nexport interface GradebookHistoryFeedParams {\n assignment_id?: CanvasId\n user_id?: CanvasId\n ascending?: boolean\n}\n\nexport class GradebookHistoryModule {\n constructor(private client: CanvasHttpClient) {}\n\n async listDays(courseId: CanvasId): Promise<CanvasGradebookHistoryDay[]> {\n return this.client.request<CanvasGradebookHistoryDay[]>(\n `/api/v1/courses/${courseId}/gradebook_history/days`,\n )\n }\n\n async getDay(courseId: CanvasId, date: string): Promise<CanvasGradebookHistoryGrader[]> {\n return this.client.request<CanvasGradebookHistoryGrader[]>(\n `/api/v1/courses/${courseId}/gradebook_history/${encodeURIComponent(date)}`,\n )\n }\n\n async listSubmissions(\n courseId: CanvasId,\n date: string,\n graderId: CanvasId,\n assignmentId: CanvasId,\n ): Promise<CanvasGradebookHistorySubmission[]> {\n return this.client.request<CanvasGradebookHistorySubmission[]>(\n `/api/v1/courses/${courseId}/gradebook_history/${encodeURIComponent(date)}/graders/${graderId}/assignments/${assignmentId}/submissions`,\n )\n }\n\n async getFeed(\n courseId: CanvasId,\n params: GradebookHistoryFeedParams = {},\n ): Promise<CanvasGradebookHistorySubmissionVersion[]> {\n const searchParams = new URLSearchParams()\n\n if (params.assignment_id !== undefined) {\n searchParams.set('assignment_id', String(params.assignment_id))\n }\n if (params.user_id !== undefined) {\n searchParams.set('user_id', String(params.user_id))\n }\n if (params.ascending !== undefined) {\n searchParams.set('ascending', String(params.ascending))\n }\n\n const query = searchParams.toString()\n\n return this.client.paginate<CanvasGradebookHistorySubmissionVersion>(\n `/api/v1/courses/${courseId}/gradebook_history/feed${query ? `?${query}` : ''}`,\n )\n }\n}\n","import { CanvasApiError, type CanvasHttpClient } from './client'\nimport type { CanvasNewQuiz, CanvasNewQuizAccommodation, CanvasNewQuizItem } from './types'\nimport type { CanvasId } from './id'\n\nexport interface NewQuizPayload {\n title?: string\n instructions?: string | null\n points_possible?: number\n due_at?: string | null\n unlock_at?: string | null\n lock_at?: string | null\n published?: boolean\n}\n\ntype ChoiceItem = {\n interaction_type_slug: 'choice'\n title?: string\n item_body: string\n choices: Array<{ id: string; item_body: string }>\n correct_choice_id: string\n}\n\ntype TrueFalseItem = {\n interaction_type_slug: 'true-false'\n item_body: string\n correct_answer: boolean\n}\n\ntype EssayItem = {\n interaction_type_slug: 'essay'\n item_body: string\n rich_text?: boolean\n word_count_min?: number\n word_count_max?: number\n}\n\ntype MatchingItem = {\n interaction_type_slug: 'matching'\n item_body: string\n matches: Array<{ question: string; answer: string }>\n distractors?: string[]\n}\n\ntype NumericAnswer =\n | { kind: 'exact'; value: number; margin?: number }\n | { kind: 'range'; min: number; max: number }\n | { kind: 'precision'; value: number; precision: number }\n\ntype NumericItem = {\n interaction_type_slug: 'numeric'\n item_body: string\n answers: NumericAnswer[]\n}\n\nexport type NewQuizItemInput = ChoiceItem | TrueFalseItem | EssayItem | MatchingItem | NumericItem\n\nexport interface NewQuizItemCreatePayload {\n position?: number\n points_possible: number\n item: NewQuizItemInput\n}\n\nexport interface NewQuizItemUpdatePayload {\n position?: number\n points_possible?: number\n item?: NewQuizItemInput\n}\n\nexport class NewQuizzesModule {\n constructor(private client: CanvasHttpClient) {}\n\n /**\n * The New Quizzes API expects quiz fields nested under a `quiz` key\n * (`quiz[title]`, `quiz[published]`, …), the same shape the Classic quizzes\n * endpoint uses. A flat body is rejected with `400 quiz is missing`.\n */\n async create(courseId: CanvasId, payload: NewQuizPayload): Promise<CanvasNewQuiz> {\n return this.client.request<CanvasNewQuiz>(`/api/quiz/v1/courses/${courseId}/quizzes`, {\n method: 'POST',\n body: JSON.stringify({ quiz: payload }),\n })\n }\n\n async update(\n courseId: CanvasId,\n assignmentId: CanvasId,\n patch: NewQuizPayload,\n ): Promise<CanvasNewQuiz> {\n return this.client.request<CanvasNewQuiz>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}`,\n { method: 'PATCH', body: JSON.stringify({ quiz: patch }) },\n )\n }\n\n async delete(courseId: CanvasId, assignmentId: CanvasId): Promise<void> {\n await this.client.request<void>(`/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}`, {\n method: 'DELETE',\n })\n }\n\n async listItems(courseId: CanvasId, assignmentId: CanvasId): Promise<CanvasNewQuizItem[]> {\n return this.client.paginate<CanvasNewQuizItem>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/items`,\n )\n }\n\n async getItem(\n courseId: CanvasId,\n assignmentId: CanvasId,\n itemId: string,\n ): Promise<CanvasNewQuizItem> {\n return this.client.request<CanvasNewQuizItem>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/items/${itemId}`,\n )\n }\n\n /**\n * The New Quizzes Items API expects item fields nested under an `item` key\n * (`item[entry_type]`, `item[points_possible]`, `item[entry][...]`). Flat\n * fields would not be bound to the `item` parameter.\n */\n async createItem(\n courseId: CanvasId,\n assignmentId: CanvasId,\n input: NewQuizItemCreatePayload,\n ): Promise<CanvasNewQuizItem> {\n const wireItem: Record<string, unknown> = {\n entry_type: 'Item',\n points_possible: input.points_possible,\n entry: this.toWireItem(input.item),\n }\n if (input.position !== undefined) wireItem.position = input.position\n return this.client.request<CanvasNewQuizItem>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/items`,\n { method: 'POST', body: JSON.stringify({ item: wireItem }) },\n )\n }\n\n async updateItem(\n courseId: CanvasId,\n assignmentId: CanvasId,\n itemId: string,\n patch: NewQuizItemUpdatePayload,\n ): Promise<CanvasNewQuizItem> {\n const wireItem: Record<string, unknown> = {}\n if (patch.position !== undefined) wireItem.position = patch.position\n if (patch.points_possible !== undefined) wireItem.points_possible = patch.points_possible\n if (patch.item !== undefined) wireItem.entry = this.toWireItem(patch.item)\n return this.client.request<CanvasNewQuizItem>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/items/${itemId}`,\n { method: 'PATCH', body: JSON.stringify({ item: wireItem }) },\n )\n }\n\n async deleteItem(courseId: CanvasId, assignmentId: CanvasId, itemId: string): Promise<void> {\n await this.client.request<void>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/items/${itemId}`,\n { method: 'DELETE' },\n )\n }\n\n async setAccommodation(\n courseId: CanvasId,\n userId: CanvasId,\n timeMultiplier?: number,\n extraAttempts?: number,\n ): Promise<CanvasNewQuizAccommodation> {\n const body: Record<string, unknown> = { user_id: userId }\n if (timeMultiplier !== undefined) body.time_multiplier = timeMultiplier\n if (extraAttempts !== undefined) body.extra_attempts = extraAttempts\n return this.client.request<CanvasNewQuizAccommodation>(\n `/api/quiz/v1/courses/${courseId}/accommodations`,\n { method: 'POST', body: JSON.stringify(body) },\n )\n }\n\n async setQuizAccommodation(\n courseId: CanvasId,\n assignmentId: CanvasId,\n userId: CanvasId,\n timeMultiplier?: number,\n extraAttempts?: number,\n ): Promise<CanvasNewQuizAccommodation> {\n const body: Record<string, unknown> = { user_id: userId }\n if (timeMultiplier !== undefined) body.time_multiplier = timeMultiplier\n if (extraAttempts !== undefined) body.extra_attempts = extraAttempts\n return this.client.request<CanvasNewQuizAccommodation>(\n `/api/quiz/v1/courses/${courseId}/quizzes/${assignmentId}/accommodations`,\n { method: 'POST', body: JSON.stringify(body) },\n )\n }\n\n async getAccommodation(\n courseId: CanvasId,\n userId: CanvasId,\n ): Promise<CanvasNewQuizAccommodation | null> {\n try {\n return await this.client.request<CanvasNewQuizAccommodation>(\n `/api/quiz/v1/courses/${courseId}/accommodations/${userId}`,\n )\n } catch (err) {\n if (err instanceof CanvasApiError && err.status === 404) return null\n throw err\n }\n }\n\n private toWireItem(item: NewQuizItemInput): Record<string, unknown> {\n switch (item.interaction_type_slug) {\n case 'choice':\n return this.choiceToWire(item)\n case 'true-false':\n return this.trueFalseToWire(item)\n case 'essay':\n return this.essayToWire(item)\n case 'matching':\n return this.matchingToWire(item)\n case 'numeric':\n return this.numericToWire(item)\n }\n }\n\n private choiceToWire(item: ChoiceItem): Record<string, unknown> {\n return {\n interaction_type_slug: 'choice',\n item_body: item.item_body,\n interaction_data: {\n choices: item.choices.map((c, i) => ({\n id: c.id,\n position: i + 1,\n item_body: c.item_body,\n })),\n },\n properties: {\n shuffle_rules: { choices: { shuffled: true } },\n },\n scoring_data: {\n value: item.correct_choice_id,\n },\n // Equivalence hard-coded per CTO direction; not exposed to callers\n scoring_algorithm: 'Equivalence',\n }\n }\n\n private trueFalseToWire(item: TrueFalseItem): Record<string, unknown> {\n return {\n interaction_type_slug: 'true-false',\n item_body: item.item_body,\n interaction_data: {\n choices: [\n { id: 'true', position: 1, item_body: 'True' },\n { id: 'false', position: 2, item_body: 'False' },\n ],\n },\n properties: {},\n scoring_data: {\n value: item.correct_answer ? 'true' : 'false',\n },\n scoring_algorithm: 'Equivalence',\n }\n }\n\n private essayToWire(item: EssayItem): Record<string, unknown> {\n const properties: Record<string, unknown> = {}\n if (item.word_count_min !== undefined || item.word_count_max !== undefined) {\n const wc: Record<string, unknown> = {}\n if (item.word_count_min !== undefined) wc.min = item.word_count_min\n if (item.word_count_max !== undefined) wc.max = item.word_count_max\n properties.word_count = wc\n }\n return {\n interaction_type_slug: 'essay',\n item_body: item.item_body,\n interaction_data: {\n rce_enabled: item.rich_text !== false,\n },\n properties,\n scoring_data: {},\n scoring_algorithm: 'None',\n }\n }\n\n private matchingToWire(item: MatchingItem): Record<string, unknown> {\n const matches = item.matches.map((m, i) => ({\n id: `match_${i}`,\n item_body: m.question,\n }))\n const correctResponses = item.matches.map((m, i) => ({\n id: `resp_${i}`,\n item_body: m.answer,\n }))\n const distractorResponses = (item.distractors ?? []).map((d, i) => ({\n id: `dist_${i}`,\n item_body: d,\n }))\n const scoringValue = item.matches.map((_, i) => ({\n id: `match_${i}`,\n match_id: `resp_${i}`,\n }))\n return {\n interaction_type_slug: 'matching',\n item_body: item.item_body,\n interaction_data: {\n matches,\n responses: [...correctResponses, ...distractorResponses],\n },\n properties: {},\n scoring_data: {\n value: scoringValue,\n },\n scoring_algorithm: 'Equivalence',\n }\n }\n\n private numericToWire(item: NumericItem): Record<string, unknown> {\n const answers = item.answers\n const first = answers[0]\n if (first === undefined) {\n throw new Error('numeric item must have at least one answer')\n }\n let scoringData: Record<string, unknown>\n let scoringAlgorithm: string\n if (first.kind === 'exact') {\n scoringData = { exact_answer: first.value, margin: first.margin ?? 0 }\n scoringAlgorithm = 'Exact'\n } else if (first.kind === 'range') {\n scoringData = { lower_bound: first.min, upper_bound: first.max }\n scoringAlgorithm = 'Range'\n } else {\n scoringData = { exact_answer: first.value, precision: first.precision }\n scoringAlgorithm = 'Precision'\n }\n return {\n interaction_type_slug: 'numeric',\n item_body: item.item_body,\n interaction_data: {},\n properties: {},\n scoring_data: scoringData,\n scoring_algorithm: scoringAlgorithm,\n }\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasContentExport, ContentExportType } from './types'\nimport type { CanvasId } from './id'\n\n/**\n * Canvas `content_exports` API — exports a course (or its assessments) as a\n * Common Cartridge / QTI / zip package. Exports are asynchronous: `create`\n * starts the job and returns immediately; `get` polls until the package is ready\n * and surfaces a time-limited `attachment.url` download link.\n */\nexport class ContentExportsModule {\n constructor(private client: CanvasHttpClient) {}\n\n /** Start a content export. Returns the export in its initial `created` state. */\n async create(courseId: CanvasId, exportType: ContentExportType): Promise<CanvasContentExport> {\n // Canvas accepts `export_type` as a top-level param here (no `content_export:` envelope).\n return this.client.request<CanvasContentExport>(`/api/v1/courses/${courseId}/content_exports`, {\n method: 'POST',\n body: JSON.stringify({ export_type: exportType }),\n })\n }\n\n /** Poll a single export's status; `attachment` is populated only once `exported`. */\n async get(courseId: CanvasId, exportId: CanvasId): Promise<CanvasContentExport> {\n return this.client.request<CanvasContentExport>(\n `/api/v1/courses/${courseId}/content_exports/${exportId}`,\n )\n }\n\n /** List all content exports for a course (most recent first, per Canvas). */\n async list(courseId: CanvasId): Promise<CanvasContentExport[]> {\n return this.client.paginate<CanvasContentExport>(`/api/v1/courses/${courseId}/content_exports`)\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasContentMigration, CanvasContentMigrator, CanvasMigrationIssue } from './types'\nimport type { CanvasId } from './id'\n\nexport class ContentMigrationsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(courseId: CanvasId): Promise<CanvasContentMigration[]> {\n return this.client.paginate<CanvasContentMigration>(\n `/api/v1/courses/${courseId}/content_migrations`,\n )\n }\n\n async get(courseId: CanvasId, migrationId: CanvasId): Promise<CanvasContentMigration> {\n return this.client.request<CanvasContentMigration>(\n `/api/v1/courses/${courseId}/content_migrations/${migrationId}`,\n )\n }\n\n async listMigrators(courseId: CanvasId): Promise<CanvasContentMigrator[]> {\n return this.client.request<CanvasContentMigrator[]>(\n `/api/v1/courses/${courseId}/content_migrations/migrators`,\n )\n }\n\n async getSelectiveData(\n courseId: CanvasId,\n migrationId: CanvasId,\n type?: string,\n ): Promise<unknown[]> {\n const url = `/api/v1/courses/${courseId}/content_migrations/${migrationId}/selective_data`\n return this.client.paginate<unknown>(url, type ? { type } : undefined)\n }\n\n async getAssetIdMapping(\n courseId: CanvasId,\n migrationId: CanvasId,\n ): Promise<Record<string, unknown>> {\n return this.client.request<Record<string, unknown>>(\n `/api/v1/courses/${courseId}/content_migrations/${migrationId}/asset_id_mapping`,\n )\n }\n\n async listMigrationIssues(\n courseId: CanvasId,\n migrationId: CanvasId,\n ): Promise<CanvasMigrationIssue[]> {\n return this.client.paginate<CanvasMigrationIssue>(\n `/api/v1/courses/${courseId}/content_migrations/${migrationId}/migration_issues`,\n )\n }\n\n async create(\n courseId: CanvasId,\n params: {\n migration_type: string\n settings?: Record<string, unknown>\n date_shift_options?: Record<string, unknown>\n selective_import?: boolean\n },\n ): Promise<CanvasContentMigration> {\n return this.client.request<CanvasContentMigration>(\n `/api/v1/courses/${courseId}/content_migrations`,\n {\n method: 'POST',\n body: JSON.stringify(params),\n },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasGradingSchemeEntry, CanvasGradingStandard } from './types'\nimport type { CanvasId } from './id'\n\n/**\n * Canvas grading standards (letter-to-percentage grading schemes).\n *\n * Canvas API asymmetry: the GET/response key is `grading_scheme` (plural),\n * while the POST body key is `grading_scheme_entry` (singular). The POST body\n * is flat — there is no `grading_standard:` wrapper. List endpoints return a\n * plain JSON array, so we use `client.paginate` (not `paginateEnvelope`).\n */\nexport class GradingStandardsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async listForCourse(courseId: CanvasId): Promise<CanvasGradingStandard[]> {\n return this.client.paginate<CanvasGradingStandard>(\n `/api/v1/courses/${courseId}/grading_standards`,\n )\n }\n\n async listForAccount(accountId: CanvasId): Promise<CanvasGradingStandard[]> {\n return this.client.paginate<CanvasGradingStandard>(\n `/api/v1/accounts/${accountId}/grading_standards`,\n )\n }\n\n async createForCourse(\n courseId: CanvasId,\n title: string,\n schemeEntries: CanvasGradingSchemeEntry[],\n ): Promise<CanvasGradingStandard> {\n const sorted = [...schemeEntries].sort((a, b) => b.value - a.value)\n return this.client.request<CanvasGradingStandard>(\n `/api/v1/courses/${courseId}/grading_standards`,\n {\n method: 'POST',\n body: JSON.stringify({ title, grading_scheme_entry: sorted }),\n },\n )\n }\n\n async createForAccount(\n accountId: CanvasId,\n title: string,\n schemeEntries: CanvasGradingSchemeEntry[],\n ): Promise<CanvasGradingStandard> {\n const sorted = [...schemeEntries].sort((a, b) => b.value - a.value)\n return this.client.request<CanvasGradingStandard>(\n `/api/v1/accounts/${accountId}/grading_standards`,\n {\n method: 'POST',\n body: JSON.stringify({ title, grading_scheme_entry: sorted }),\n },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasLatePolicy } from './types'\nimport type { CanvasId } from './id'\n\n/**\n * Canvas late/missing submission policy for a course.\n *\n * The response is an envelope — `{ \"late_policy\": { ... } }` — so `get` unwraps\n * the inner object before returning it. Throws `CanvasApiError` on failure\n * (notably 403 for student tokens and 404 when no policy row exists yet); the\n * caller decides how to handle those statuses.\n */\nexport class LatePolicyModule {\n constructor(private client: CanvasHttpClient) {}\n\n async get(courseId: CanvasId): Promise<CanvasLatePolicy> {\n const envelope = await this.client.request<{ late_policy: CanvasLatePolicy }>(\n `/api/v1/courses/${courseId}/late_policy`,\n )\n return envelope.late_policy\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasQueryParams } from './query'\nimport type { CanvasAppointmentGroup, CanvasCalendarEvent, CanvasGroup, CanvasUser } from './types'\nimport type { CanvasId } from './id'\n\nexport type AppointmentGroupInclude = 'appointments' | 'child_events' | 'participant_count'\n\nexport class AppointmentGroupsModule {\n constructor(private client: CanvasHttpClient) {}\n\n async list(params?: {\n scope?: 'reservable' | 'manageable'\n include?: ReadonlyArray<AppointmentGroupInclude>\n context_codes?: string[]\n }): Promise<CanvasAppointmentGroup[]> {\n const query: CanvasQueryParams = {}\n if (params?.scope) query.scope = params.scope\n if (params?.include?.length) query.include = [...params.include]\n if (params?.context_codes?.length) query.context_codes = params.context_codes\n return this.client.paginate<CanvasAppointmentGroup>('/api/v1/appointment_groups', query)\n }\n\n async get(\n id: CanvasId,\n include?: ReadonlyArray<AppointmentGroupInclude>,\n ): Promise<CanvasAppointmentGroup> {\n const query: CanvasQueryParams = {}\n if (include?.length) query.include = [...include]\n return this.client.request<CanvasAppointmentGroup>(`/api/v1/appointment_groups/${id}`, {\n query,\n })\n }\n\n async create(params: {\n context_codes: string[]\n title: string\n description?: string\n location_name?: string\n location_address?: string\n publish?: boolean\n participant_type?: 'User' | 'Group'\n max_appointments_per_participant?: number\n min_appointments_per_participant?: number\n new_appointments?: Array<[string, string]>\n participant_visibility?: 'private' | 'protected'\n }): Promise<CanvasAppointmentGroup> {\n return this.client.request<CanvasAppointmentGroup>('/api/v1/appointment_groups', {\n method: 'POST',\n body: JSON.stringify({ appointment_group: params }),\n })\n }\n\n async update(\n id: CanvasId,\n params: {\n context_codes?: string[]\n title?: string\n description?: string\n location_name?: string\n location_address?: string\n publish?: boolean\n new_appointments?: Array<[string, string]>\n },\n ): Promise<CanvasAppointmentGroup> {\n return this.client.request<CanvasAppointmentGroup>(`/api/v1/appointment_groups/${id}`, {\n method: 'PUT',\n body: JSON.stringify({ appointment_group: params }),\n })\n }\n\n async delete(id: CanvasId, cancelReason?: string): Promise<void> {\n const body = cancelReason ? JSON.stringify({ cancel_reason: cancelReason }) : undefined\n return this.client.request<void>(`/api/v1/appointment_groups/${id}`, {\n method: 'DELETE',\n body,\n })\n }\n\n async listUsers(id: CanvasId): Promise<CanvasUser[]> {\n return this.client.paginate<CanvasUser>(`/api/v1/appointment_groups/${id}/users`)\n }\n\n async listGroups(id: CanvasId): Promise<CanvasGroup[]> {\n return this.client.paginate<CanvasGroup>(`/api/v1/appointment_groups/${id}/groups`)\n }\n\n async nextAppointment(appointmentGroupIds?: CanvasId[]): Promise<CanvasCalendarEvent[]> {\n const query: CanvasQueryParams = {}\n if (appointmentGroupIds?.length) query.appointment_group_ids = appointmentGroupIds\n return this.client.request<CanvasCalendarEvent[]>(\n '/api/v1/appointment_groups/next_appointment',\n { query },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasActivityStreamEntry, CanvasActivityStreamSummaryEntry } from './types'\n\nexport interface ActivityStreamQuery {\n onlyActiveCourses?: boolean\n /**\n * Stop following `Link: rel=\"next\"` once this many items have accumulated.\n * Bounds the WORK, not just the result: `paginate()` otherwise follows every\n * page to exhaustion (up to `maxPaginationPages`, default 1000) before\n * returning, so slicing afterwards would still have fetched up to 100,000\n * items.\n */\n maxItems?: number\n}\n\nexport class ActivityStreamModule {\n constructor(private client: CanvasHttpClient) {}\n\n /**\n * `GET /users/self/activity_stream/summary` is the one stream endpoint Canvas\n * does not paginate (`api_render_stream_summary` has no `Api.paginate` call),\n * so this uses `request()` rather than `paginate()` — the inverse of every\n * other activity-stream endpoint.\n */\n async getSummary(onlyActiveCourses?: boolean): Promise<CanvasActivityStreamSummaryEntry[]> {\n return this.client.request<CanvasActivityStreamSummaryEntry[]>(\n '/api/v1/users/self/activity_stream/summary',\n { query: { only_active_courses: onlyActiveCourses } },\n )\n }\n\n /**\n * `GET /users/self/activity_stream` — the cross-course stream itself.\n *\n * Uses `paginate()`, which is not interchangeable with `request()` here:\n * Canvas renders this endpoint through `Api.paginate(scope, self, …,\n * default_per_page: 21)`, so a `request()` call would silently return 21\n * items with no error and no `Link` header followed. `paginate()` sets\n * `per_page=100` (Canvas's `MAX_PER_PAGE`) and follows the links.\n */\n async getStream(query: ActivityStreamQuery = {}): Promise<CanvasActivityStreamEntry[]> {\n return this.client.paginate<CanvasActivityStreamEntry>(\n '/api/v1/users/self/activity_stream',\n { only_active_courses: query.onlyActiveCourses },\n query.maxItems === undefined ? undefined : { maxItems: query.maxItems },\n )\n }\n}\n","import type { CanvasHttpClient } from './client'\nimport type { CanvasPlannerItem } from './types'\n\nexport interface PlannerItemsQuery {\n startDate?: string\n endDate?: string\n contextCodes?: string[]\n filter?: 'new_activity' | 'incomplete_items' | 'complete_items'\n /**\n * Stop following `Link: rel=\"next\"` once this many items have accumulated.\n * Same rationale as `ActivityStreamQuery.maxItems`: bounds the WORK, not\n * just the result.\n */\n maxItems?: number\n}\n\nexport class PlannerModule {\n constructor(private client: CanvasHttpClient) {}\n\n /**\n * `GET /planner/items` — unions nine Canvas collections (assignments,\n * ungraded quizzes, planner notes, wiki pages, ungraded discussions,\n * calendar events, peer reviews, sub-assignments, peer-review\n * sub-assignments) into one cross-course/cross-group feed.\n *\n * Uses `paginate()`: `Api.paginate(items, self, …)` sets no\n * `default_per_page`, so Canvas falls back to `Api::PER_PAGE = 10` absent an\n * explicit `per_page` (BRU-2797 §3.2) — the same `request()`-vs-`paginate()`\n * hazard as the activity stream.\n */\n async listItems(query: PlannerItemsQuery = {}): Promise<CanvasPlannerItem[]> {\n return this.client.paginate<CanvasPlannerItem>(\n '/api/v1/planner/items',\n {\n start_date: query.startDate,\n end_date: query.endDate,\n context_codes: query.contextCodes,\n filter: query.filter,\n },\n query.maxItems === undefined ? undefined : { maxItems: query.maxItems },\n )\n }\n}\n","/**\n * The canonical Canvas identifier input type.\n *\n * Phase 1 of the 64-bit identifier design\n * (`docs/superpowers/specs/2026-10-05-bru-2730-canvas-64bit-identifiers.md`,\n * BRU-2730). PR 1a (BRU-2816) added this module as the type alone, with no\n * call site adopting it; PR 1b (BRU-2827) then adopted it across every\n * Canvas-module input, signature, and handler.\n *\n * The defect it exists to fix is that a Canvas object whose global ID exceeds\n * `Number.MAX_SAFE_INTEGER` — every object on shard >= 901 — cannot be\n * addressed at all today. `JSON.parse('{\"course_id\":9007199254740993}')`,\n * which is what every MCP transport does to wire bytes, silently yields\n * `…992` and the request succeeds against the wrong object; the string form\n * that *would* carry the value exactly is rejected, because the schema is\n * `z.number()`.\n *\n * ## Why a string and not `string | number`\n *\n * Per §4.1: with `type CanvasId = string | number` on **both** sides of a\n * comparison, `a === b` is legal TypeScript and `false` at runtime whenever\n * the two representations differ. The compiler catches only the asymmetric\n * cases, and this codebase has 14 ID-to-ID comparisons in `src/tools/**`\n * plus 18 `Map<number, …>` / `Set<number>` occurrences — exactly where the\n * union would go blind. One canonical representation makes those correct by\n * construction and makes the arithmetic sites fail to compile, which is\n * where a human decision is wanted.\n *\n * ## Why the checks are composed the way they are\n *\n * Every departure from the obvious composition is load-bearing and has a\n * measurement behind it (§4.2.1): the `{ abort: true }` on the pattern check\n * (N1), the per-member error messages rather than one union-level message\n * (N3), and the fact that the `.transform()` makes the emitted JSON Schema\n * available only under `io: 'input'` (N2). `tests/canvas/id-input-wire.test.ts`\n * holds the wire-level assertion for each; reverting any one of them fails a\n * named, disjoint set of tests.\n */\nimport { z } from 'zod'\n\n/**\n * Canvas's own ceiling, not a value chosen here: `MAX_ID = 2**63 - 1` in\n * `lib/api.rb`, alongside `MAX_ID_LENGTH = 19` and\n * `ID_REGEX = /\\A\\d{1,19}\\z/`.\n */\nexport const CANVAS_MAX_ID = 9223372036854775807n\n\n/**\n * The largest ID a JSON **number** can carry without ambiguity. Above this,\n * the value and its neighbour are the same double, so a number is no longer\n * a faithful representation and the string form is the only one that works.\n */\nexport const MAX_SAFE_CANVAS_NUMBER_ID = Number.MAX_SAFE_INTEGER\n\n/**\n * Canvas's `ID_REGEX` minus leading zeros. Canvas tolerates `\"007\"`\n * (`\\d{1,19}` then `.to_i`); we do not, because one object must have exactly\n * one canonical string or `Map` keys fork and §4.1's whole argument is\n * defeated.\n */\nconst CANONICAL_DECIMAL_ID = /^[1-9][0-9]{0,18}$/\n\n/** A Canvas identifier in canonical form: a decimal string, no sign, no leading zeros. */\nexport type CanvasId = string\n\n/**\n * What the **wire** may carry: `JSON.parse` of a Canvas body yields a `number`\n * for an identifier unless Canvas stringified it. Nothing past the HTTP client\n * boundary sees this type, because `normalizeCanvasIds()` in\n * `src/canvas/normalize-ids.ts` rewrites every identifier to a `CanvasId`\n * before the body is returned (§4.1, §4.3). Kept exported as the name for that\n * boundary.\n */\nexport type CanvasWireId = string | number\n\nexport interface CanvasIdInputOptions {\n /**\n * Non-ID literals this call site accepts in an ID position — Canvas's\n * `\"self\"` and `\"all\"`. Only the literals a call site explicitly declares\n * are accepted; declaring one does not open the field to arbitrary strings.\n */\n readonly sentinels?: readonly string[]\n /**\n * Canvas SIS-style alternate identifier prefixes this call site accepts,\n * written **without** the trailing colon: `['sis_user_id']` accepts\n * `\"sis_user_id:A1234\"` and nothing else that is not already a Canvas ID.\n *\n * This option is PR 1b's resolution of PR #395 correction C4. Four call\n * sites in `src/tools/outcomes.ts` document and accept\n * `\"sis_user_id:<sis id>\"`, which `sentinels` cannot express: a sentinel is\n * an exact literal, and the SIS identifier after the colon is free-form.\n * Dropping the form would break tools that work today, and accepting any\n * string would reintroduce exactly what §4.1 exists to remove, so the\n * prefix is **declared per call site** on the same terms as a sentinel.\n *\n * The remainder after the colon is deliberately unconstrained beyond being\n * non-empty: a Canvas SIS ID is an arbitrary institution-assigned string,\n * and Canvas itself splits on the first colon and passes the rest through\n * (`Api.sis_parse_id`). A prefixed value is returned unchanged, so it is\n * never confused with a canonical numeric ID.\n */\n readonly prefixes?: readonly string[]\n}\n\n/** The subset of a Zod issue the messages below read. */\ninterface IdIssueContext {\n readonly input: unknown\n readonly path?: ReadonlyArray<PropertyKey> | undefined\n}\n\n/** Longest rendering of a received value admitted into a message. */\nconst MAX_RECEIVED_CHARS = 42\n\nfunction describeReceived(value: unknown): string {\n if (typeof value === 'string') {\n const quoted = JSON.stringify(value)\n return quoted.length > MAX_RECEIVED_CHARS ? `${quoted.slice(0, MAX_RECEIVED_CHARS)}…` : quoted\n }\n if (typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean') {\n return String(value)\n }\n if (value === null) return 'null'\n if (value === undefined) return 'undefined'\n return Array.isArray(value) ? 'an array' : `a ${typeof value}`\n}\n\n/**\n * The field name to lead the message with. The **last string** segment, not\n * `path.join('.')`: inside a `canvasIdList()` the path is\n * `['student_ids', 3]`, and `student_ids` is the name the caller wrote. The\n * MCP SDK appends the full path to the message separately, so no information\n * is lost.\n */\nfunction issueLabel(path: ReadonlyArray<PropertyKey> | undefined): string {\n for (let index = (path?.length ?? 0) - 1; index >= 0; index -= 1) {\n const segment = path?.[index]\n if (typeof segment === 'string') return segment\n }\n return 'value'\n}\n\n/**\n * The message a caller hitting the precision bug sees. Set on each union\n * **member** as well as on the union, because a union-level `error` does not\n * reach a member's own issues: without this, `9007199254740992` yields\n * `Too big: expected int to be <=9007199254740991`, which names neither the\n * received value nor the remedy (§4.2.1 N3).\n */\nfunction contractMessage(issue: IdIssueContext): string {\n return (\n `${issueLabel(issue.path)} must be a Canvas ID: a positive integer at or below ` +\n `${MAX_SAFE_CANVAS_NUMBER_ID}, or a decimal string for larger IDs ` +\n `(received ${describeReceived(issue.input)}). ` +\n `Pass large IDs as strings, e.g. \"9010000000000001\".`\n )\n}\n\n/**\n * The string arm's range failure. A separate message because the \"pass it as\n * a string\" remedy does not apply here — the value is above Canvas's own\n * `MAX_ID`, so no representation can address it. Without a message of its\n * own the `.refine` reports a bare `custom` / `Invalid input` (§4.2.1 N3).\n */\nfunction ceilingMessage(issue: IdIssueContext): string {\n return (\n `${issueLabel(issue.path)} must be a Canvas ID at or below ${CANVAS_MAX_ID}, ` +\n `the largest value Canvas can store (received ${describeReceived(issue.input)}).`\n )\n}\n\n/**\n * Exactly `z.number().int().positive()`, which in Zod 4 is a **safe-integer**\n * check and therefore already rejects `2**53` with `too_big` — not\n * hand-rolled, per §4.2 rule 1.\n */\nfunction numberArm(): z.ZodType<number, unknown> {\n return z.number().int({ error: contractMessage }).positive({ error: contractMessage })\n}\n\n/**\n * `{ abort: true }` is not a style preference. Zod 4 runs *all* checks on a\n * string schema and collects issues rather than short-circuiting, so without\n * it the `.refine` below calls `BigInt()` on values the pattern has already\n * rejected — and `BigInt('7.0')` throws a `SyntaxError` that escapes\n * `safeParse` *and* the enclosing union, surfacing to the caller as an\n * internal message instead of an `-32602` (§4.2.1 N1).\n *\n * The `.refine` deliberately calls `BigInt` unguarded, so that removing the\n * abort flag is observable rather than silently compensated for.\n */\nfunction stringArm(): z.ZodType<string, unknown> {\n return z\n .string()\n .regex(CANONICAL_DECIMAL_ID, { abort: true, error: contractMessage })\n .refine((value) => BigInt(value) <= CANVAS_MAX_ID, {\n error: ceilingMessage,\n })\n}\n\n/**\n * The message a declared SIS prefix publishes when the value starts with the\n * prefix but carries nothing after the colon. Its own message for the same\n * reason `ceilingMessage` has one: a union-level `error` does not reach a\n * member's issues (§4.2.1 N3).\n */\nfunction prefixMessage(prefix: string): (issue: IdIssueContext) => string {\n return (issue) =>\n `${issueLabel(issue.path)} may be a Canvas ID, or an SIS identifier written ` +\n `\"${prefix}:<sis id>\" with a non-empty SIS id (received ${describeReceived(issue.input)}).`\n}\n\n/** Escapes every ECMAScript regex metacharacter, so a prefix matches literally. */\nfunction escapeForRegExp(literal: string): string {\n return literal.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * One declared SIS-prefix arm. A `.regex` rather than a `.startsWith` +\n * `.refine` so that the accepted form is **published** in the emitted JSON\n * Schema as a `pattern` — §9's argument for a self-documenting schema applies\n * to this arm as much as to the canonical one. `[\\s\\S]` rather than `.` so a\n * literal newline inside an institution-assigned SIS id is not silently a\n * rejection the pattern does not explain.\n */\nfunction prefixedArm(prefix: string): z.ZodType<string, unknown> {\n return z.string().regex(new RegExp(`^${escapeForRegExp(prefix)}:[\\\\s\\\\S]+$`), {\n error: prefixMessage(prefix),\n })\n}\n\n/**\n * A required Canvas identifier input. Accepts a safe positive integer or a\n * canonical decimal string, and always yields the canonical decimal string.\n *\n * ```ts\n * course_id: canvasIdInput()\n * user_id: canvasIdInput({ sentinels: ['self'] }).optional()\n * user_ids: canvasIdList({ prefixes: ['sis_user_id'] }).optional()\n * ```\n */\nexport function canvasIdInput(options?: CanvasIdInputOptions): z.ZodType<CanvasId, unknown> {\n const sentinels = options?.sentinels ?? []\n const members = [\n numberArm(),\n stringArm(),\n ...(options?.prefixes ?? []).map(prefixedArm),\n ...(sentinels.length > 0 ? [z.literal([...sentinels])] : []),\n ] as [z.ZodType<unknown, unknown>, z.ZodType<unknown, unknown>, ...z.ZodType<unknown, unknown>[]]\n\n return z.union(members, { error: contractMessage }).transform((value) => String(value))\n}\n\n/**\n * A list of Canvas identifiers. Sentinels are **per element**, which is how\n * Canvas takes them (`student_ids[]=all`), so a declared sentinel may appear\n * alongside real IDs in the same list.\n */\nexport function canvasIdList(options?: CanvasIdInputOptions): z.ZodType<CanvasId[], unknown> {\n return z.array(canvasIdInput(options))\n}\n\n/**\n * Numeric ordering of two canonical Canvas IDs, without `Number()`.\n *\n * `a - b` is the obvious thing and is exactly what §4.4 forbids: it rounds both\n * operands above `2**53` and can order two distinct IDs as equal. Because a\n * `CanvasId` is a decimal string with no sign and no leading zeros, \"shorter is\n * smaller, same length compares lexicographically\" is total and exact at any\n * magnitude — no `BigInt` allocation per comparison.\n *\n * The contract is exactly \"shorter string sorts first, equal-length strings\n * sort lexicographically\" — nothing more. That is a total, exact numeric\n * order over two canonical `CanvasId` decimal strings, but it is not a\n * numeric order at all once one operand is non-decimal (a sentinel like\n * `\"self\"`, or a SIS-prefixed string): such a value sorts purely by its own\n * length and characters, which can place it before, after, or between\n * numeric IDs depending on how long those IDs happen to be. No call site\n * mixes sentinel/prefixed values into a sort today; this function is not\n * safe to use if one starts.\n */\nexport function compareCanvasIds(a: CanvasId, b: CanvasId): number {\n if (a.length !== b.length) return a.length - b.length\n return a < b ? -1 : a > b ? 1 : 0\n}\n\n/**\n * The same §4.2 rules, callable outside a Zod parse — for code that receives\n * an identifier from somewhere other than a tool input schema.\n *\n * Returns the canonical decimal string, or throws a `TypeError` carrying the\n * same message the schema would publish. Throws rather than returning a\n * result object so that a caller cannot accidentally thread a non-canonical\n * value onward, which is the failure §4.1 exists to prevent.\n */\nexport function normalizeCanvasIdInput(value: unknown, options?: CanvasIdInputOptions): CanvasId {\n const issue: IdIssueContext = { input: value }\n\n if (typeof value === 'string') {\n if (options?.sentinels?.includes(value)) return value\n // A declared SIS prefix is matched by `startsWith`, not by the published\n // pattern, so this path needs no escaping to be correct — and a prefix\n // containing a regex metacharacter cannot diverge between the two.\n for (const prefix of options?.prefixes ?? []) {\n if (!value.startsWith(`${prefix}:`)) continue\n if (value.length === prefix.length + 1) throw new TypeError(prefixMessage(prefix)(issue))\n return value\n }\n // Order matters, for the same reason `{ abort: true }` does above: a\n // `BigInt` call on a non-numeric string throws a SyntaxError, so the\n // pattern check has to come first (§4.2.1 N1, outside Zod).\n if (!CANONICAL_DECIMAL_ID.test(value)) throw new TypeError(contractMessage(issue))\n if (BigInt(value) > CANVAS_MAX_ID) throw new TypeError(ceilingMessage(issue))\n return value\n }\n\n if (typeof value === 'number') {\n if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(contractMessage(issue))\n return String(value)\n }\n\n throw new TypeError(contractMessage(issue))\n}\n","import { CanvasHttpClient } from './client'\nimport type { CanvasClientConfig } from './types'\nimport { CoursesModule } from './courses'\nimport { AssignmentsModule } from './assignments'\nimport { SubmissionsModule } from './submissions'\nimport { RubricsModule } from './rubrics'\nimport { QuizzesModule } from './quizzes'\nimport { FilesModule } from './files'\nimport { UsersModule } from './users'\nimport { GroupsModule } from './groups'\nimport { EnrollmentsModule } from './enrollments'\nimport { DiscussionsModule } from './discussions'\nimport { ModulesModule } from './modules'\nimport { PagesModule } from './pages'\nimport { CalendarModule } from './calendar'\nimport { ConversationsModule } from './conversations'\nimport { PeerReviewsModule } from './peer-reviews'\nimport { AccountsModule } from './accounts'\nimport { AnalyticsModule } from './analytics'\nimport { DashboardModule } from './dashboard'\nimport { OutcomesModule } from './outcomes'\nimport { GradebookHistoryModule } from './gradebook-history'\nimport { NewQuizzesModule } from './new-quizzes'\nimport { ContentExportsModule } from './content-exports'\nimport { ContentMigrationsModule } from './content-migrations'\nimport { GradingStandardsModule } from './grading-standards'\nimport { LatePolicyModule } from './late-policy'\nimport { AppointmentGroupsModule } from './appointment-groups'\nimport { ActivityStreamModule } from './activity-stream'\nimport { PlannerModule } from './planner'\n\n/**\n * Standalone Canvas REST API client. Composed of modular per-domain classes\n * sharing a `CanvasHttpClient`. Throws `CanvasApiError` on Canvas-side failures.\n *\n * NOTE on FERPA mode: pseudonymization is a tool-layer concern provided by the\n * `Pseudonymizer` in `src/pseudonym/`. Library consumers that import this\n * client directly (`canvas-lms-mcp/canvas`) bypass the MCP tool wrappers and\n * therefore receive raw Canvas responses with real student names and contact\n * info. If you need pseudonymized output, either use the MCP server entry\n * points (`canvas-lms-mcp` stdio / `canvas-lms-mcp serve` HTTP) or wrap your\n * own calls with a `Pseudonymizer` instance.\n */\nexport class CanvasClient {\n private client: CanvasHttpClient\n courses: CoursesModule\n assignments: AssignmentsModule\n submissions: SubmissionsModule\n rubrics: RubricsModule\n quizzes: QuizzesModule\n files: FilesModule\n users: UsersModule\n groups: GroupsModule\n enrollments: EnrollmentsModule\n discussions: DiscussionsModule\n modules: ModulesModule\n pages: PagesModule\n calendar: CalendarModule\n conversations: ConversationsModule\n peerReviews: PeerReviewsModule\n accounts: AccountsModule\n analytics: AnalyticsModule\n dashboard: DashboardModule\n outcomes: OutcomesModule\n gradebookHistory: GradebookHistoryModule\n newQuizzes: NewQuizzesModule\n contentExports: ContentExportsModule\n contentMigrations: ContentMigrationsModule\n gradingStandards: GradingStandardsModule\n latePolicy: LatePolicyModule\n appointmentGroups: AppointmentGroupsModule\n activityStream: ActivityStreamModule\n planner: PlannerModule\n\n constructor(config: CanvasClientConfig) {\n this.client = new CanvasHttpClient(config)\n this.courses = new CoursesModule(this.client)\n this.assignments = new AssignmentsModule(this.client)\n this.submissions = new SubmissionsModule(this.client)\n this.rubrics = new RubricsModule(this.client)\n this.quizzes = new QuizzesModule(this.client)\n this.files = new FilesModule(this.client)\n this.users = new UsersModule(this.client)\n this.groups = new GroupsModule(this.client)\n this.enrollments = new EnrollmentsModule(this.client)\n this.discussions = new DiscussionsModule(this.client)\n this.modules = new ModulesModule(this.client)\n this.pages = new PagesModule(this.client)\n this.calendar = new CalendarModule(this.client)\n this.conversations = new ConversationsModule(this.client)\n this.peerReviews = new PeerReviewsModule(this.client)\n this.accounts = new AccountsModule(this.client)\n this.analytics = new AnalyticsModule(this.client)\n this.dashboard = new DashboardModule(this.client)\n this.outcomes = new OutcomesModule(this.client)\n this.gradebookHistory = new GradebookHistoryModule(this.client)\n this.newQuizzes = new NewQuizzesModule(this.client)\n this.contentExports = new ContentExportsModule(this.client)\n this.contentMigrations = new ContentMigrationsModule(this.client)\n this.gradingStandards = new GradingStandardsModule(this.client)\n this.latePolicy = new LatePolicyModule(this.client)\n this.appointmentGroups = new AppointmentGroupsModule(this.client)\n this.activityStream = new ActivityStreamModule(this.client)\n this.planner = new PlannerModule(this.client)\n }\n}\n\nexport { CanvasHttpClient, CanvasApiError } from './client'\nexport type { CanvasRequestOptions } from './client'\nexport { appendCanvasQuery, toCanvasQuery } from './query'\nexport type { CanvasQueryParams, CanvasQueryPrimitive, CanvasQueryValue } from './query'\n/**\n * The identifier contract, re-exported because `canvas-lms-mcp/canvas` is a\n * published entry point and every ID-taking method on `CanvasClient` now names\n * `CanvasId` in its signature (BRU-2730 §3.6). Without this a consumer can see\n * the type in the emitted `.d.ts` and still not import it — `TS2305` from the\n * subpath, which is what `tests/canvas/id-package-consumer.test.ts` caught.\n * `CanvasIdInputOptions` is included because `normalizeCanvasIdInput`'s\n * signature references it.\n */\nexport {\n canvasIdInput,\n canvasIdList,\n normalizeCanvasIdInput,\n compareCanvasIds,\n CANVAS_MAX_ID,\n MAX_SAFE_CANVAS_NUMBER_ID,\n} from './id'\nexport type { CanvasId, CanvasWireId, CanvasIdInputOptions } from './id'\nexport type * from './types'\n"]}
|