Skip to content

feat(canvas): add Canvas LMS - #487

Open
Dhirenderchoudhary wants to merge 9 commits into
corsairdev:mainfrom
Dhirenderchoudhary:feat/canvas-plugin
Open

feat(canvas): add Canvas LMS#487
Dhirenderchoudhary wants to merge 9 commits into
corsairdev:mainfrom
Dhirenderchoudhary:feat/canvas-plugin

Conversation

@Dhirenderchoudhary

@Dhirenderchoudhary Dhirenderchoudhary commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR introduces a comprehensive Canvas LMS integration to Corsair.

Key Features:

  • Added canvas plugin with over 200+ REST and GraphQL endpoints.
  • Implemented operations registry covering courses, assignments, quizzes, modules, discussions, conversations, groups, enrollments, and more.
  • Built on the data-driven factory pattern with autogenerated input/output Zod schemas, endpoint metadata, and risk levels.
  • Full support for both Bearer token (API key) and OAuth 2.0 authentication flows.
  • Added dynamic baseUrl resolution to accommodate both cloud and self-hosted Canvas instances.
  • 100% type safety with zero TypeScript errors and passing test suites.

Closes #486

Checklist

Before submitting your PR, please verify the following:

  • I have run pnpm lint and all checks pass (Note: passed for canvas package)
  • I have run pnpm typecheck and there are no TypeScript errors
  • I have run pnpm build and all packages build successfully
  • I have run pnpm test and all tests pass (Canvas tests passing 5/5)
  • I have added or updated tests where applicable
  • I have added or updated necessary documentation

Screenshots / Demos (if applicable)

Screenshot 2026-07-22 at 1 21 23 PM

Additional Notes

  • Dependencies: Uses standard Corsair core utilities. No external dependencies were added to the monorepo root.
  • Webhooks: Webhook tenant matchers and signatures are configured using standard Canvas header specifications (x-canvas-signature).

Summary by CodeRabbit

  • New Features
    • Added Canvas LMS integration with typed API access across courses, users, assignments, grading, files, discussions, reports, and more.
    • Added support for API key, OAuth, and webhook authentication.
    • Added verified webhook triggers for assignments, submissions, discussions, file uploads, and course creation.
    • Added request validation, response schemas, rate-limit retries, and authentication error handling.
    • Added Canvas tenant matching and OAuth tenant linking.
    • Added route metadata and risk classification for supported operations.
  • Tests
    • Added comprehensive coverage for API requests, schemas, authentication, tenant linking, and webhook verification.

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

@Dhirenderchoudhary is attempting to deploy a commit to the corsair Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the core Changes in packages/corsair label Jul 22, 2026
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Canvas LMS integration now includes:

  • A data-driven endpoint registry with runtime input and output validation.
  • Per-instance HTTPS base URL resolution and bearer-authenticated HTTP requests.
  • API-key and OAuth authentication flows.
  • Signed webhook handling with numeric and UUID tenant-link namespaces.
  • Behavioral coverage for endpoint dispatch, schemas, authentication, tenant resolution, and webhook verification.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/canvas/client.ts Implements HTTPS instance normalization, path interpolation, Canvas query serialization, bearer authentication, and transport-level rate-limit retries.
packages/canvas/endpoints/factory.ts Resolves the configured Canvas instance and applies operation-specific input and output schemas around each request.
packages/canvas/endpoints/types.ts Requires declared path parameters and mutation bodies while preserving bodyless Canvas actions.
packages/canvas/endpoints/response-schemas.ts Defines operation-aware response schemas for REST collections, resource objects, GraphQL responses, deletions, and text downloads.
packages/canvas/webhooks/types.ts Parses Canvas event names and authenticates webhook payloads with a non-empty secret, raw body, and timing-safe HMAC comparison.
packages/canvas/webhooks/tenant-matcher.ts Routes webhook tenants through distinct numeric account-ID and root-account UUID namespaces.
packages/canvas/webhooks/oauth-tenant-link.ts Resolves OAuth tenant links from token metadata or an unambiguous Canvas account lookup.
packages/canvas/api.test.ts Exercises all registered operations and validates URL resolution, serialization, schemas, authentication, webhook verification, and tenant linking.
packages/canvas/index.ts Registers Canvas authentication, endpoints, schemas, metadata, webhooks, tenant matching, and instance-specific OAuth configuration.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Caller["Canvas endpoint caller"] --> Input["Validate operation input"]
  Input --> BaseURL["Resolve plugin or account base URL"]
  BaseURL --> HTTP["Canvas REST / GraphQL request"]
  HTTP --> Output["Validate operation response"]
  Output --> Result["Return typed result"]
  Canvas["Canvas Live Event"] --> Match["Match event and tenant"]
  Match --> Verify["Verify HMAC signature"]
  Verify --> Trigger["Dispatch webhook trigger"]
Loading

Reviews (13): Last reviewed commit: "feat(canvas): add Canvas API response sc..." | Re-trigger Greptile

Comment thread packages/canvas/webhooks/types.ts Outdated
Comment thread packages/canvas/endpoints/factory.ts Outdated
Comment thread packages/canvas/client.ts Outdated
Comment thread packages/canvas/endpoints/factory.ts Outdated
Comment thread packages/canvas/endpoints/types.ts Outdated
Comment thread packages/canvas/webhooks/tenant-matcher.ts Outdated
Comment thread packages/canvas/schema.test.ts
Comment thread packages/canvas/endpoints/example.ts Outdated
Comment thread packages/canvas/schema/database.ts Outdated
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Plugin PR scorecard — packages/canvas

Check Status Notes
R1 — Scope: plugin files only
R2 — Tests with assertions
R3 — Description complete
R3 — Linked issue / claim
R4 — Demo video / recording

Rules: PLUGIN_PR_RULES.md · re-runs on every push

@github-actions github-actions Bot added the gate:failed Plugin PR gate checks failing label Jul 22, 2026
@github-actions

Copy link
Copy Markdown

Hey @Dhirenderchoudhary, thanks for the contribution! 🏴‍☠️ Before a maintainer reviews, please fix the items below — the review re-runs automatically on your next push.

Must fix

  • P1 packages/canvas/webhooks/types.ts:61Webhook Verification Always Succeeds
    Any request whose payload has type: "example" and whose headers pass the plugin matcher is accepted without checking its signature, raw body, or secret. An attacker can forge an event that the handler records and returns as authentic.

Rule Used: Flag boilerplate residue from the plugin generator... (source)

  • P1 packages/canvas/endpoints/factory.ts:28Configured Instance URL Is Dropped
    The factory does not pass the plugin's baseUrl to makeCanvasRequest. Calling an endpoint after configuring a self-hosted Canvas instance therefore sends the request and bearer token to the default canvas.instructure.com host instead of the configured institution.

Rule Used: Verify the implementation matches the PR descripti... (source)

  • P1 packages/canvas/client.ts:83Error Wrapping Removes Rate Limits
    A Canvas 429 is converted from ApiError to CanvasAPIError, discarding its status and retryAfter. When the message does not contain the literal text 429 or rate_limited, the registered rate-limit matcher misses it and the request receives no retries.

Rule Used: Every endpoint must validate inputs and outputs wi... (source)

  • P1 packages/canvas/endpoints/factory.ts:29Responses Bypass Zod Validation
    The generic type argument only affects TypeScript and does not parse the response with CanvasEndpointOutputSchemas[name]. A malformed response, including an array where the public schema promises a record, is returned to callers without the required output validation.

Rule Used: Every endpoint must validate inputs and outputs wi... (source)

  • P1 packages/canvas/endpoints/types.ts:44Mutation Bodies Remain Optional
    Both sides of the needsBody condition use bodySchema.optional(). Create and update endpoints therefore accept missing bodies and send undefined to Canvas, even when the operation requires request data, shifting a known input error into a remote API failure.

Rule Used: Every endpoint must validate inputs and outputs wi... (source)

  • P1 packages/canvas/webhooks/tenant-matcher.ts:24Placeholder Tenant Field Blocks Routing
    The matcher only recognizes the scaffold field tenant_external_id. A Canvas event without that invented field returns null, while the OAuth resolver also returns null unless the token response contains the same placeholder, so connected accounts cannot establish or resolve a tenant routing key.

Rule Used: Flag boilerplate residue from the plugin generator... (source)

  • P1 packages/canvas/schema.test.ts:46Endpoints Have No Behavioral Tests
    These tests only inspect registry metadata and schema presence; none of the implemented endpoints is called or checked for its URL, serialized input, response validation, or error handling. This leaves every real endpoint without the corresponding test required for a plugin implementation.

Rule Used: Flag any types on exported or public surfaces as... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  • P1 packages/canvas/endpoints/example.ts:3Generator Placeholder Remains
    This is the generator's leftover endpoints/example.ts placeholder rather than part of the factory implementation. The plugin rules explicitly reject this residue in a completed integration.

Rule Used: Flag boilerplate residue from the plugin generator... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  • P1 packages/canvas/schema/database.ts:9Database Schema Is Still Scaffolded
    The file retains the generator TODO and commented example entity while CanvasSchema.entities remains empty. This is prohibited boilerplate residue; either implement the intended persisted entities or remove the unused scaffold.

Rule Used: Flag boilerplate residue from the plugin generator... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

PR requirements (rules)

  • R4 — Required in "Screenshots / Demos" before a maintainer reviews

If anything remains after your next push, a bot commit will clean it up; a maintainer always does the final review and merge.

@github-actions github-actions Bot added the bot:round-1 Review bot posted consolidated findings label Jul 22, 2026
Comment thread packages/canvas/endpoints/types.ts Outdated
@github-actions github-actions Bot added bot:round-2 Review bot pushed an automated fix and removed gate:failed Plugin PR gate checks failing labels Jul 22, 2026
@github-actions

Copy link
Copy Markdown

Remaining findings are being fixed by a bot commit — it will be re-reviewed automatically.

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

Maintainer review needed

Automated rounds are exhausted. Remaining findings:

  • P1 packages/canvas/webhooks/tenant-matcher.tsTenant identifier namespaces conflict
    When a Canvas Live Event identifies its tenant through metadata.root_account_uuid without an earlier numeric account ID, this matcher returns the UUID as a canvas_account_id, while the OAuth resolver persists a numeric account ID under that link type. Exact tenant-link lookup therefore cannot match the valid webhook to its connected account.

Knowledge Base Used: The provider-plugin package pattern

@github-actions github-actions Bot added the needs-maintainer Automated rounds exhausted - human review needed label Jul 22, 2026
Comment thread packages/canvas/webhooks/types.ts
@Dhirenderchoudhary Dhirenderchoudhary removed the needs-maintainer Automated rounds exhausted - human review needed label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Canvas LMS provider with typed REST and GraphQL operations, authenticated requests, endpoint schemas, API-key and OAuth support, webhook verification and triggers, tenant resolution, packaging, tests, and provider registration.

Changes

Canvas operation contracts

Layer / File(s) Summary
Operation catalog, routes, and schemas
packages/canvas/endpoints/operations.ts, packages/canvas/endpoints/routes.ts, packages/canvas/endpoints/types.ts, packages/canvas/endpoints/response-schemas.ts
Defines Canvas REST and GraphQL operations, route metadata, typed request inputs, response outputs, and generated Zod schemas.

Canvas endpoint execution

Layer / File(s) Summary
Request client and endpoint factory
packages/canvas/client.ts, packages/canvas/endpoints/factory.ts, packages/canvas/endpoints/index.ts, packages/canvas/error-handlers.ts
Normalizes base URLs, encodes paths, sends authenticated requests, applies retry handling, validates inputs and responses, and exports grouped endpoints.

Canvas plugin wiring and packaging

Layer / File(s) Summary
Plugin registration and package setup
packages/canvas/index.ts, packages/canvas/schema/index.ts, packages/canvas/package.json, packages/canvas/tsconfig.json, packages/canvas/tsup.config.ts, packages/canvas/jest.config.cjs
Registers endpoint and webhook catalogs, configures authentication and metadata, resolves credentials, exports types, and defines package tooling.

Canvas webhook support

Layer / File(s) Summary
Webhook schemas, verification, triggers, and tenant resolution
packages/canvas/webhooks/*
Adds payload schemas, event matching, HMAC-SHA256 verification, tenant matching, OAuth tenant links, and six webhook triggers.

Validation and provider registration

Layer / File(s) Summary
Schema and integration tests
packages/canvas/schema.test.ts, packages/canvas/api.test.ts, packages/corsair/core/constants.ts
Tests operation coverage, endpoint requests, base URL resolution, mutation bodies, response arrays, webhook verification, event matching, rate-limit handling, tenant resolution, and Canvas provider registration.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • corsairdev/corsair#344: Adds a similarly structured provider plugin with typed endpoints, schemas, authentication, webhooks, and rate-limit handling.
  • corsairdev/corsair#375: Adds a provider integration with typed API registries, schemas, authentication, error handling, webhooks, and provider registration.
  • corsairdev/corsair#552: Adds a provider plugin with an authenticated HTTP client, typed endpoint registries, Zod schemas, retry handling, and provider registration.

Suggested reviewers: devjain32

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers Canvas APIs, authentication, tenant URLs, webhooks, and rate-limit retries, but it lacks Canvas Link-header pagination and 403/X-Rate-Limit backoff. Add Link-header pagination and handle Canvas rate limits signaled by 403 responses or X-Rate-Limit-Remaining before merging.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding Canvas LMS support.
Out of Scope Changes check ✅ Passed The changes support the Canvas integration objective, including package setup, endpoint metadata, schemas, authentication, and webhook handling.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.6)
packages/canvas/endpoints/types.ts

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added ci CI / GitHub Actions app App / Hub-facing app code docs Docs / Mintlify / markdown changes plugin Changes inside a plugin package cli CLI package changes labels Aug 3, 2026
Comment thread packages/canvas/client.ts Fixed
@Dhirenderchoudhary

Copy link
Copy Markdown
Collaborator Author

@greptile review

@Dhirenderchoudhary Dhirenderchoudhary removed ci CI / GitHub Actions app App / Hub-facing app code docs Docs / Mintlify / markdown changes labels Aug 3, 2026
@Dhirenderchoudhary Dhirenderchoudhary removed the cli CLI package changes label Aug 3, 2026
Comment thread packages/canvas/endpoints/types.ts Outdated
@Dhirenderchoudhary

Copy link
Copy Markdown
Collaborator Author

@greptile review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (9)
packages/corsair/core/constants.ts (1)

220-316: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add 'canvas' to AllProviders for autocomplete consistency.

No exhaustive AllProviders handling exists, and (string & {}) keeps the type open. The omission does not affect assignability or exhaustive switches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/corsair/core/constants.ts` around lines 220 - 316, Update the
AllProviders type to include the literal provider value 'canvas' alongside the
existing provider names, preserving the open (string & {}) fallback and current
type behavior.
packages/canvas/index.ts (1)

578-579: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the canvasOperations import to the top of the file.

The import sits between declarations at Line 579. ES module imports hoist, so the behavior is correct. The placement still breaks the convention used by the other imports in this file and can confuse readers and tooling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/index.ts` around lines 578 - 579, Move the canvasOperations
import from its current position between declarations to the file’s top import
section, alongside the other module imports. Do not change how canvasOperations
is used.
packages/canvas/endpoints/operations.ts (2)

619-628: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicate deleteAMessage operation.

deleteAMessage declares the same method and the same path as deleteConversationMessages. Two operation names for one Canvas endpoint increase the public surface without adding capability.

♻️ Proposed removal
 	deleteConversationMessages: {
 		method: 'POST',
 		path: '/api/v1/conversations/{conversation_id}/remove_messages',
 		description: 'Delete specific messages from a Canvas conversation',
 	},
-	deleteAMessage: {
-		method: 'POST',
-		path: '/api/v1/conversations/{conversation_id}/remove_messages',
-		description: 'Delete messages from a Canvas conversation',
-	},

Also remove deleteAMessage from the Conversations group in packages/canvas/endpoints/index.ts and from canvasEndpointsNested.conversations in packages/canvas/index.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/operations.ts` around lines 619 - 628, Remove the
duplicate deleteAMessage operation from the endpoint definitions, and remove its
references from the Conversations group and canvasEndpointsNested.conversations.
Retain deleteConversationMessages as the sole operation for this method and
path.

63-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document that GraphQL operations require a caller-supplied query document.

Many entries map to POST /api/graphql with the same method and path. The factory sends only input.body to that path. Therefore getAccountGraphQl, getAssignment2, createAssignmentGraphQl, and getLegacyNode are behaviorally identical passthroughs. The descriptions state that each operation retrieves or creates a specific resource. That is only true when the caller supplies the correct GraphQL query and variables in the body.

Two options improve this:

  • Add the GraphQL document to the operation metadata and merge it in the factory.
  • Update the descriptions to state that the caller must supply query and variables.

Also applies to: 223-228, 244-248, 1210-1215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/operations.ts` around lines 63 - 68, Update the
descriptions for getAccountGraphQl, getAssignment2, createAssignmentGraphQl, and
getLegacyNode to state that callers must supply the appropriate GraphQL query
document and variables in input.body. Preserve their existing method and path
metadata; do not claim resource-specific behavior is built in unless the factory
also supplies the GraphQL document.
packages/canvas/endpoints/factory.ts (1)

12-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type the base_url key getter instead of casting ctx.keys.

canvasAuthConfig declares account: ['base_url'] for both auth types in packages/canvas/index.ts (Lines 105-112). The generated key context should therefore expose the base_url getter. The cast to { get_base_url?: () => Promise<string | null | undefined> } hides that contract. If the generated getter name changes, the cast keeps compiling and the fallback silently returns undefined.

Use the typed KeyBuilderContext shape for Canvas here, or export a small typed helper from packages/canvas/index.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/factory.ts` around lines 12 - 28, Update
resolveCanvasBaseUrl to use the generated KeyBuilderContext type for ctx.keys,
preserving the base_url getter contract declared by canvasAuthConfig instead of
casting to an ad hoc getter shape. Reuse the existing Canvas typing from
canvas/index.ts or export a focused typed helper there, and keep the current
option/account fallback behavior unchanged.
packages/canvas/jest.config.cjs (1)

5-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the copied patterns that do not apply to this package.

collectCoverageFrom excludes jest.config.ts, but this file is jest.config.cjs, so the exclusion never applies. The **/plugins/** and **/setup/** entries in testMatch target directories that this package does not contain. The **/*.test.ts pattern already covers schema.test.ts.

♻️ Proposed cleanup
 	testMatch: [
 		'**/*.test.ts',
 		'**/tests/**/*.test.ts',
-		'**/plugins/**/*.test.ts',
-		'**/setup/**/*.test.ts',
 	],
 	collectCoverageFrom: [
 		'**/*.ts',
 		'!**/*.d.ts',
 		'!**/node_modules/**',
 		'!**/dist/**',
-		'!jest.config.ts',
+		'!tsup.config.ts',
 		'!tests/**',
 	],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/jest.config.cjs` around lines 5 - 18, Clean up the Jest
configuration by removing the redundant `**/plugins/**/*.test.ts` and
`**/setup/**/*.test.ts` entries from `testMatch`, and remove the ineffective
`!jest.config.ts` exclusion from `collectCoverageFrom` because the config is
CJS. Keep the broad `**/*.test.ts` matching and all applicable coverage
exclusions unchanged.
packages/canvas/endpoints/types.ts (1)

35-56: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Unresolved path placeholders reach the Canvas API. No layer enforces that a value exists for each {placeholder} in an operation path: the generated input schema keeps pathParams fully optional, and resolvePath substitutes silently. A call such as getSingleCourse({}) therefore sends a request whose URL still contains the literal placeholder, and Canvas answers with a confusing error instead of a local validation failure.

  • packages/canvas/endpoints/types.ts#L35-L56: extract the placeholder names from operation.path and require each one as a non-empty string in the generated input schema.
  • packages/canvas/client.ts#L37-L47: replace all placeholders with a global regex and throw when a value is missing, so an unresolved placeholder cannot reach the request URL.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/types.ts` around lines 35 - 56, The generated
schema in createRequestInputSchema must extract every placeholder from
operation.path and require each corresponding pathParams value to be a non-empty
string, while keeping unrelated parameters optional. In
packages/canvas/endpoints/types.ts lines 35-56, add this per-operation
validation. In packages/canvas/client.ts lines 37-47, update resolvePath to
replace all occurrences using a global placeholder match and throw when any
required value is missing, preventing unresolved placeholders from reaching the
request URL.
packages/canvas/tsconfig.json (1)

17-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Exclude non-library files from the declaration project. include: ["./**/*"] includes schema.test.ts and tsup.config.ts; exclude these files to prevent declaration output for them. Keep references: []; workspace package resolution handles the corsair imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/tsconfig.json` around lines 17 - 19, Update the declaration
project configuration in packages/canvas/tsconfig.json to exclude schema.test.ts
and tsup.config.ts alongside dist and node_modules, while preserving the
existing include pattern and references: [] setting.
packages/canvas/endpoints/index.ts (1)

4-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an exhaustiveness check for endpoint groups.

defineGroup permits partial registration. Add a type-level or test-level assertion that every CanvasOperationName appears in exactly one endpoint group.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/index.ts` around lines 4 - 14, Add an
exhaustiveness assertion alongside defineGroup and the endpoint-group
declarations so every CanvasOperationName is registered exactly once: reject
missing names and duplicate registrations at the type or test level. Keep
defineGroup’s generated endpoint mapping behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/canvas/client.ts`:
- Around line 12-22: Update normalizeCanvasBaseUrl to parse the trimmed URL with
new URL(), require the protocol to be exactly https:, and reject malformed or
non-HTTPS values before returning the normalized base URL. Preserve
trailing-slash removal and the existing required-value validation, using the
parsed URL result to ensure the base URL includes a valid host.
- Around line 65-86: Update the request preparation in the Canvas client around
the config and requestOptions objects to transform array-valued query parameters
into Canvas bracketed keys such as include[] before passing them to the shared
executor. Preserve the existing bearer-token configuration and ensure non-array
query parameters remain unchanged.

In `@packages/canvas/endpoints/factory.ts`:
- Around line 54-58: Update the completion logging around logEventFromContext in
the endpoint handler to avoid passing the full input object, which may contain
personal or access-token data. Construct and pass only non-sensitive metadata,
or redact input.body, pathParams, and query fields known to contain sensitive
values, while preserving the parsed response and completed-event behavior.

In `@packages/canvas/endpoints/types.ts`:
- Around line 65-75: Widen canvasResponseSchema in CanvasEndpointOutputSchemas
to accept successful responses with undefined, empty-string, and other string
bodies in addition to JSON objects and arrays. Preserve the existing object and
array validation so factory.ts parsing continues to validate structured
responses without turning 204 DELETE results or getRubricsUploadTemplate into
errors.

In `@packages/canvas/error-handlers.ts`:
- Around line 5-18: Restrict the fallback matching in RATE_LIMIT_ERROR.match so
“429” is recognized only when the error message clearly indicates rate limiting,
while preserving the existing ApiError status check and rate_limited matching.
Remove the broad substring behavior that treats unrelated identifiers containing
429 as retryable.

In `@packages/canvas/index.ts`:
- Around line 630-644: Update the OAuth configuration created by canvas() so
authUrl and tokenUrl use each tenant’s account base_url, matching
resolveCanvasBaseUrl in the endpoint factory, instead of resolving
options.baseUrl only once. If OAuth URLs cannot be resolved per tenant by the
framework, require options.baseUrl for oauth_2 and reject configuration without
it rather than falling back to the public Canvas host.
- Around line 581-601: Update CanvasOperation in operations.ts to support an
optional riskLevel, then modify buildEndpointMeta to prefer an operation’s
explicit riskLevel over HTTP-method inference. Mark deleteDiscussionEntry,
deleteDiscussionTopicGraphQl, deleteSubmissionDraft, and deleteOutcomeLinks as
destructive so PluginPermissionsConfig enforces the correct permission.

In `@packages/canvas/webhooks/types.ts`:
- Around line 91-99: Update verifyCanvasWebhookSignature to normalize
request.headers['x-canvas-signature'] by selecting the first element when it is
an array, while preserving the existing string and missing-header behavior. Use
the normalized string for subsequent signature verification instead of directly
casting the header value.

---

Nitpick comments:
In `@packages/canvas/endpoints/factory.ts`:
- Around line 12-28: Update resolveCanvasBaseUrl to use the generated
KeyBuilderContext type for ctx.keys, preserving the base_url getter contract
declared by canvasAuthConfig instead of casting to an ad hoc getter shape. Reuse
the existing Canvas typing from canvas/index.ts or export a focused typed helper
there, and keep the current option/account fallback behavior unchanged.

In `@packages/canvas/endpoints/index.ts`:
- Around line 4-14: Add an exhaustiveness assertion alongside defineGroup and
the endpoint-group declarations so every CanvasOperationName is registered
exactly once: reject missing names and duplicate registrations at the type or
test level. Keep defineGroup’s generated endpoint mapping behavior unchanged.

In `@packages/canvas/endpoints/operations.ts`:
- Around line 619-628: Remove the duplicate deleteAMessage operation from the
endpoint definitions, and remove its references from the Conversations group and
canvasEndpointsNested.conversations. Retain deleteConversationMessages as the
sole operation for this method and path.
- Around line 63-68: Update the descriptions for getAccountGraphQl,
getAssignment2, createAssignmentGraphQl, and getLegacyNode to state that callers
must supply the appropriate GraphQL query document and variables in input.body.
Preserve their existing method and path metadata; do not claim resource-specific
behavior is built in unless the factory also supplies the GraphQL document.

In `@packages/canvas/endpoints/types.ts`:
- Around line 35-56: The generated schema in createRequestInputSchema must
extract every placeholder from operation.path and require each corresponding
pathParams value to be a non-empty string, while keeping unrelated parameters
optional. In packages/canvas/endpoints/types.ts lines 35-56, add this
per-operation validation. In packages/canvas/client.ts lines 37-47, update
resolvePath to replace all occurrences using a global placeholder match and
throw when any required value is missing, preventing unresolved placeholders
from reaching the request URL.

In `@packages/canvas/index.ts`:
- Around line 578-579: Move the canvasOperations import from its current
position between declarations to the file’s top import section, alongside the
other module imports. Do not change how canvasOperations is used.

In `@packages/canvas/jest.config.cjs`:
- Around line 5-18: Clean up the Jest configuration by removing the redundant
`**/plugins/**/*.test.ts` and `**/setup/**/*.test.ts` entries from `testMatch`,
and remove the ineffective `!jest.config.ts` exclusion from
`collectCoverageFrom` because the config is CJS. Keep the broad `**/*.test.ts`
matching and all applicable coverage exclusions unchanged.

In `@packages/canvas/tsconfig.json`:
- Around line 17-19: Update the declaration project configuration in
packages/canvas/tsconfig.json to exclude schema.test.ts and tsup.config.ts
alongside dist and node_modules, while preserving the existing include pattern
and references: [] setting.

In `@packages/corsair/core/constants.ts`:
- Around line 220-316: Update the AllProviders type to include the literal
provider value 'canvas' alongside the existing provider names, preserving the
open (string & {}) fallback and current type behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a6d72a4b-7ce9-4b84-ad90-8ffbe5279db5

📥 Commits

Reviewing files that changed from the base of the PR and between 839b807 and 092957c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • packages/canvas/client.ts
  • packages/canvas/endpoints/factory.ts
  • packages/canvas/endpoints/index.ts
  • packages/canvas/endpoints/operations.ts
  • packages/canvas/endpoints/types.ts
  • packages/canvas/error-handlers.ts
  • packages/canvas/index.ts
  • packages/canvas/jest.config.cjs
  • packages/canvas/package.json
  • packages/canvas/schema.test.ts
  • packages/canvas/schema/index.ts
  • packages/canvas/tsconfig.json
  • packages/canvas/tsup.config.ts
  • packages/canvas/webhooks/index.ts
  • packages/canvas/webhooks/oauth-tenant-link.ts
  • packages/canvas/webhooks/tenant-matcher.ts
  • packages/canvas/webhooks/triggers.ts
  • packages/canvas/webhooks/types.ts
  • packages/corsair/core/constants.ts

Comment thread packages/canvas/client.ts
Comment thread packages/canvas/client.ts
Comment thread packages/canvas/endpoints/factory.ts
Comment thread packages/canvas/endpoints/types.ts Outdated
Comment thread packages/canvas/error-handlers.ts
Comment thread packages/canvas/index.ts
Comment thread packages/canvas/index.ts Outdated
Comment thread packages/canvas/webhooks/types.ts
@Dhirenderchoudhary

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread packages/canvas/endpoints/types.ts Outdated
Comment thread packages/canvas/endpoints/types.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/canvas/endpoints/operations.ts (2)

207-210: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use user_id in the last-attended route.

Canvas defines this endpoint as /api/v1/courses/{course_id}/users/{user_id}/last_attended, not an enrollment-scoped route.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/operations.ts` around lines 207 - 210, Update the
addLastAttendedDate operation’s path to use the Canvas user-scoped route with
the {user_id} placeholder instead of the enrollment-scoped {enrollment_id}
route, while preserving the existing method and operation metadata.

492-495: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mark both no-body POST operations as bodyless.

duplicateGroupDiscussionTopic needs no body. assignUnassignedMembersToGroupCategory only has optional sync query input. Without bodyless: true, valid path-only calls fail local validation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/operations.ts` around lines 492 - 495, Mark both
duplicateGroupDiscussionTopic and assignUnassignedMembersToGroupCategory
operation definitions as bodyless: true, while preserving the existing method,
paths, and optional sync query input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/canvas/webhooks/oauth-tenant-link.ts`:
- Around line 59-64: Replace the accounts[0] fallback in the OAuth tenant-link
resolution with an explicit account identifier from the OAuth contract; when no
identifier exists or multiple accounts remain ambiguous, return null instead of
selecting an arbitrary account. Update the surrounding account-resolution flow
and add coverage for reordered multi-account responses and paginated results.

---

Outside diff comments:
In `@packages/canvas/endpoints/operations.ts`:
- Around line 207-210: Update the addLastAttendedDate operation’s path to use
the Canvas user-scoped route with the {user_id} placeholder instead of the
enrollment-scoped {enrollment_id} route, while preserving the existing method
and operation metadata.
- Around line 492-495: Mark both duplicateGroupDiscussionTopic and
assignUnassignedMembersToGroupCategory operation definitions as bodyless: true,
while preserving the existing method, paths, and optional sync query input.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5be6ea9b-b88e-4427-89c4-2c986f7af8a6

📥 Commits

Reviewing files that changed from the base of the PR and between 092957c and 92e07ec.

📒 Files selected for processing (6)
  • packages/canvas/api.test.ts
  • packages/canvas/client.ts
  • packages/canvas/endpoints/operations.ts
  • packages/canvas/endpoints/types.ts
  • packages/canvas/schema.test.ts
  • packages/canvas/webhooks/oauth-tenant-link.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/canvas/client.ts
  • packages/canvas/schema.test.ts

Comment thread packages/canvas/webhooks/oauth-tenant-link.ts Outdated
@Dhirenderchoudhary

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread packages/canvas/webhooks/tenant-matcher.ts Outdated
@Dhirenderchoudhary

Copy link
Copy Markdown
Collaborator Author

@greptile review

@Dhirenderchoudhary

Copy link
Copy Markdown
Collaborator Author

@greptile review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
packages/canvas/endpoints/routes.ts (2)

53-59: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Use a Map for route lookup.

canvasRoutes holds over 200 entries. getCanvasRoute performs a linear scan on every call. Build a Map<CanvasOperationName, CanvasRoute> once at module load and read from it.

♻️ Proposed refactor
+const canvasRouteByKey = new Map<CanvasOperationName, CanvasRoute>(
+	canvasRoutes.map((entry) => [entry.key, entry]),
+);
+
 export function getCanvasRoute(key: CanvasOperationName): CanvasRoute {
-	const route = canvasRoutes.find((entry) => entry.key === key);
+	const route = canvasRouteByKey.get(key);
 	if (!route) {
 		throw new Error(`[canvas] Unknown operation: ${key}`);
 	}
 	return route;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/routes.ts` around lines 53 - 59, Replace the linear
canvasRoutes.find lookup in getCanvasRoute with a module-level
Map<CanvasOperationName, CanvasRoute> initialized once from canvasRoutes.
Retrieve routes by key from the Map, preserve the existing unknown-operation
error and return behavior, and avoid rebuilding the Map per call.

24-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

pathParamsOf duplicates pathParamNames in packages/canvas/endpoints/types.ts.

Both functions extract {param} placeholders with the same regular expression. Export one helper and import it in the other module. This keeps the path-parameter contract in a single place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/routes.ts` around lines 24 - 26, Remove the
duplicate placeholder extraction from pathParamsOf and reuse the exported
pathParamNames helper from the endpoints types module instead. Update the
relevant import/export declarations so both call sites share the single
implementation and preserve the existing readonly string-array behavior.
packages/canvas/endpoints/response-schemas.ts (2)

449-468: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive expectsListResponse from the schema instead of repeating the branches.

expectsListResponse duplicates every branch of createResponseSchema. If one function changes, the two can disagree, and packages/canvas/api.test.ts builds mock responses from expectsListResponse. A test would then pass while the runtime schema rejects the real response.

Compute the answer from the schema that createResponseSchema returns.

♻️ Proposed refactor
 export function expectsListResponse(
 	name: CanvasOperationName,
 	operation: CanvasOperation = canvasOperations[name],
 ): boolean {
-	if (operation.path === '/api/graphql') return false;
-	if (operation.method === 'DELETE') return false;
-	if (operation.path.includes('/upload')) return false;
-	const resource = resourceSchemaFor(name, operation);
-	if (
-		resource === CanvasPermissionsSchema ||
-		resource === CanvasUnreadCountSchema ||
-		resource === CanvasQuotaSchema ||
-		resource === CanvasSubmissionSummarySchema ||
-		resource === CanvasJsonObjectSchema ||
-		resource === CanvasJsonArraySchema
-	) {
-		return resource === CanvasJsonArraySchema;
-	}
-	return isListOperation(name, operation);
+	return createResponseSchema(name, operation) instanceof z.ZodArray;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/response-schemas.ts` around lines 449 - 468,
Refactor expectsListResponse to derive its result from the response schema
produced by createResponseSchema, rather than duplicating path, method, and
resource-specific branches. Reuse the existing schema-generation symbols and
determine whether the returned schema represents a list, keeping the boolean
consistent with runtime validation.

13-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace .passthrough() with z.looseObject() across this file. Zod 4 deprecates the method, and z.looseObject() preserves unknown keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/response-schemas.ts` around lines 13 - 17, Replace
the deprecated .passthrough() usage on CanvasEntitySchema and any other schemas
in this file with z.looseObject(), preserving each schema’s existing fields and
unknown-key behavior.
packages/canvas/endpoints/operations.ts (1)

68-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider marking read-only GraphQL operations as read.

riskFor in packages/canvas/endpoints/routes.ts maps any POST to write. All GraphQL operations use POST, so query-only operations such as getAccountGraphQl, getLegacyNode, getAuditLogs, getInternalSettings, getModuleItem, and getAssignmentGroup are reported as write risk. The new riskLevel field can correct this metadata for query-only GraphQL operations.

Note that the GraphQL document is supplied by the caller in input.body, so a read label is only accurate if callers are constrained to queries. Choose the label that matches the intended guarantee.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/operations.ts` around lines 68 - 73, Update the
GraphQL endpoint metadata for getAccountGraphQl and the other query-only
operations (getLegacyNode, getAuditLogs, getInternalSettings, getModuleItem, and
getAssignmentGroup) to use the riskLevel field with a read value only if callers
are constrained to query documents; otherwise retain write to reflect the
caller-supplied GraphQL operation. Ensure each label matches the intended
guarantee and riskFor consumes this metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/canvas/endpoints/response-schemas.ts`:
- Around line 415-446: Update createResponseSchema to return operation-specific
wrapper schemas for getQuizStatistics, getOutcomeResults,
createBatchOverridesInACourse, and polling create operations returning polls,
poll_choices, poll_sessions, or poll_submissions. Define or reuse strict Zod
schemas matching each documented response shape, including the
assignment-overrides array, and select them by operation name before generic
resource/list handling; do not use a permissive fallback.

In `@packages/canvas/endpoints/types.ts`:
- Around line 62-67: Update CanvasEndpointInputs to derive body optionality from
each operation’s mutation and bodyless status, matching
createRequestInputSchema: require a non-empty body for mutations that are not
bodyless, while preserving optional body for bodyless or non-mutation
operations. Ensure the existing pathParams mapping remains unchanged.

In `@packages/canvas/webhooks/oauth-tenant-link.ts`:
- Around line 44-46: Update the singleton-row branch in the tenant-link
resolution function so it does not use the child row’s id as the tenant identity
when parent_account_id is set. Prefer a valid root_account_id from the row or
related account data, and return null when no valid root identity is available;
preserve the existing behavior for root accounts.

In `@packages/canvas/webhooks/tenant-matcher.ts`:
- Around line 5-8: The numericAccountId function in
packages/canvas/webhooks/tenant-matcher.ts:5-8 must iterate through all values
and return the first normalized string matching /^\d+$/, rather than validating
only firstString(values). In
packages/canvas/webhooks/oauth-tenant-link.ts:74-76, scan canvas_account_id,
account_id, and root_account_id for the first normalized numeric value before
checking UUID fields.

---

Nitpick comments:
In `@packages/canvas/endpoints/operations.ts`:
- Around line 68-73: Update the GraphQL endpoint metadata for getAccountGraphQl
and the other query-only operations (getLegacyNode, getAuditLogs,
getInternalSettings, getModuleItem, and getAssignmentGroup) to use the riskLevel
field with a read value only if callers are constrained to query documents;
otherwise retain write to reflect the caller-supplied GraphQL operation. Ensure
each label matches the intended guarantee and riskFor consumes this metadata.

In `@packages/canvas/endpoints/response-schemas.ts`:
- Around line 449-468: Refactor expectsListResponse to derive its result from
the response schema produced by createResponseSchema, rather than duplicating
path, method, and resource-specific branches. Reuse the existing
schema-generation symbols and determine whether the returned schema represents a
list, keeping the boolean consistent with runtime validation.
- Around line 13-17: Replace the deprecated .passthrough() usage on
CanvasEntitySchema and any other schemas in this file with z.looseObject(),
preserving each schema’s existing fields and unknown-key behavior.

In `@packages/canvas/endpoints/routes.ts`:
- Around line 53-59: Replace the linear canvasRoutes.find lookup in
getCanvasRoute with a module-level Map<CanvasOperationName, CanvasRoute>
initialized once from canvasRoutes. Retrieve routes by key from the Map,
preserve the existing unknown-operation error and return behavior, and avoid
rebuilding the Map per call.
- Around line 24-26: Remove the duplicate placeholder extraction from
pathParamsOf and reuse the exported pathParamNames helper from the endpoints
types module instead. Update the relevant import/export declarations so both
call sites share the single implementation and preserve the existing readonly
string-array behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ef056dd4-115d-4e3a-adf9-d4531366220e

📥 Commits

Reviewing files that changed from the base of the PR and between 92e07ec and 382007f.

📒 Files selected for processing (17)
  • packages/canvas/api.test.ts
  • packages/canvas/client.ts
  • packages/canvas/endpoints/factory.ts
  • packages/canvas/endpoints/index.ts
  • packages/canvas/endpoints/operations.ts
  • packages/canvas/endpoints/response-schemas.ts
  • packages/canvas/endpoints/routes.ts
  • packages/canvas/endpoints/types.ts
  • packages/canvas/error-handlers.ts
  • packages/canvas/index.ts
  • packages/canvas/jest.config.cjs
  • packages/canvas/schema.test.ts
  • packages/canvas/tsconfig.json
  • packages/canvas/webhooks/oauth-tenant-link.ts
  • packages/canvas/webhooks/tenant-matcher.ts
  • packages/canvas/webhooks/types.ts
  • packages/corsair/core/constants.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/canvas/tsconfig.json
  • packages/canvas/endpoints/factory.ts
  • packages/canvas/error-handlers.ts
  • packages/canvas/webhooks/types.ts
  • packages/canvas/client.ts
  • packages/canvas/schema.test.ts

Comment on lines +415 to +446
export function createResponseSchema(
name: CanvasOperationName,
operation: CanvasOperation,
): z.ZodTypeAny {
if (operation.path === '/api/graphql') {
return CanvasGraphqlResponseSchema;
}
if (operation.method === 'DELETE') {
return CanvasDeleteResponseSchema;
}
if (operation.path.includes('/upload')) {
return CanvasTextResponseSchema;
}

const resource = resourceSchemaFor(name, operation);
if (
resource === CanvasPermissionsSchema ||
resource === CanvasUnreadCountSchema ||
resource === CanvasQuotaSchema ||
resource === CanvasSubmissionSummarySchema ||
resource === CanvasJsonObjectSchema ||
resource === CanvasJsonArraySchema
) {
return resource;
}

if (isListOperation(name, operation)) {
return z.array(resource);
}

return resource;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Canvas LMS API GET quizzes statistics response quiz_statistics wrapper object

💡 Result:

In the Canvas LMS REST API, the GET /api/v1/courses/:course_id/quizzes/:quiz_id/statistics endpoint returns a response that uses the quiz_statistics key as the root wrapper object [1][2][3]. The API returns this object as an array containing the quiz statistics [1][2][3]. The structure of the response is as follows: { "quiz_statistics": [ { "id": 1, "quiz_id": 2, "multiple_attempts_exist": true, "includes_all_versions": true, "generated_at": "2013-01-23T23:59:00-07:00", "url": "http://canvas.example.edu/api/v1/courses/1/quizzes/2/statistics", "html_url": "http://canvas.example.edu/courses/1/quizzes/2/statistics", "question_statistics": null, "submission_statistics": null, "links": null } ] } According to the official Canvas LMS API documentation, the QuizStatistics object itself contains various fields, such as generated_at, multiple_attempts_exist, and URLs for both API and HTML access [1][3]. Note that certain fields, such as quiz_id, are specifically noted as being available only in non-JSON-API requests [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- target file outline ---'
ast-grep outline packages/canvas/endpoints/response-schemas.ts --match createResponseSchema --view expanded || true
printf '%s\n' '--- target implementation ---'
sed -n '1,180p' packages/canvas/endpoints/response-schemas.ts
sed -n '380,470p' packages/canvas/endpoints/response-schemas.ts
printf '%s\n' '--- factory ---'
sed -n '1,100p' packages/canvas/endpoints/factory.ts
printf '%s\n' '--- Canvas operation/schema references ---'
rg -n "getQuizStatistics|getOutcomeResults|createBatchOverridesInACourse|Poll|poll|QuizSubmission|quiz_statistics|outcome_results|quiz_submissions|poll_choices" packages/canvas

Repository: corsairdev/corsair

Length of output: 16166


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- response schema selection and definitions ---'
sed -n '180,430p' packages/canvas/endpoints/response-schemas.ts
sed -n '430,510p' packages/canvas/endpoints/response-schemas.ts
printf '%s\n' '--- cited operation definitions ---'
sed -n '280,315p' packages/canvas/endpoints/operations.ts
sed -n '525,565p' packages/canvas/endpoints/operations.ts
sed -n '995,1025p' packages/canvas/endpoints/operations.ts
sed -n '1075,1140p' packages/canvas/endpoints/operations.ts
printf '%s\n' '--- operation name and type declarations ---'
rg -n "type CanvasOperationName|export type CanvasOperation|function isListOperation|const isListOperation|resourceSchemaFor|CanvasEndpointOutputSchemas" packages/canvas/endpoints

Repository: corsairdev/corsair

Length of output: 16607


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re

ops = Path("packages/canvas/endpoints/operations.ts").read_text()
src = Path("packages/canvas/endpoints/response-schemas.ts").read_text()

names = [
    "getQuizStatistics",
    "getOutcomeResults",
    "createBatchOverridesInACourse",
    "createSinglePoll",
    "getSinglePoll",
    "createSinglePollChoice",
    "getSinglePollChoice",
    "createSinglePollSession",
    "createSinglePollSubmission",
]

for name in names:
    m = re.search(
        rf"^\s*{re.escape(name)}:\s*\{{\s*"
        rf"method:\s*'([^']+)'\s*,\s*"
        rf"path:\s*'([^']+)'",
        ops,
        re.MULTILINE,
    )
    if not m:
        print(f"{name}: operation not found")
        continue
    method, path = m.groups()
    key = name.lower()
    if "quizstatistics" in key:
        resource = "CanvasJsonArraySchema"
    elif "course" in key or "/courses" in path.lower():
        resource = "CanvasCourseSchema"
    elif "quiz" in key or "/quizzes" in path.lower():
        resource = "CanvasQuizSchema"
    elif "poll" in key:
        resource = "CanvasEntitySchema"
    else:
        resource = "CanvasEntitySchema"

    last = path.rstrip("/").split("/")[-1]
    is_list = (
        method == "GET"
        and (
            re.match(r"^(getAll|find|getAccountsThat|getAccountNotifications"
                     r"|getEnrollmentInvitations|getCourses12|getAUsersMostRecently"
                     r"|getAllPeer|getAllOutcome|getAllEPortfolios|getAligned"
                     r"|getCoursesBlueprint)", name, re.I)
            or (not last.endswith("}") and (last.endswith("s") or last == "recipients"
                                             or last == "overrides"))
        )
    )
    schema = resource if resource in {
        "CanvasJsonArraySchema", "CanvasJsonObjectSchema"
    } else f"z.array({resource})" if is_list else resource
    print(f"{name}: {method} {path} -> {schema}")

print("\nMinimal Zod-shape checks:")
responses = {
    "getQuizStatistics": {"quiz_statistics": [{"id": 1}]},
    "getOutcomeResults": {"outcome_results": [{"id": 1}]},
    "createBatchOverridesInACourse": [{"id": 1}],
}
expected = {
    "getQuizStatistics": "array",
    "getOutcomeResults": "array",
    "createBatchOverridesInACourse": "object",
}
for name, value in responses.items():
    actual = "array" if isinstance(value, list) else "object"
    print(f"{name}: expected {expected[name]}, documented-shape sample {actual}, mismatch={actual != expected[name]}")
PY

Repository: corsairdev/corsair

Length of output: 1280


🌐 Web query:

Canvas LMS API polls poll choices poll sessions poll submissions response wrapper object create poll get poll official documentation

💡 Result:

The Canvas LMS API provides several endpoints for managing polls, poll sessions, choices, and submissions. These are structured under the Polling namespace [1][2][3][4]. Polls The Poll object includes a unique ID, question, description, creation timestamp, user ID of the creator, and an aggregate of results from all associated poll sessions (where keys are poll choice IDs and values are submission counts) [1][5]. You can list, retrieve, create, update, and delete polls using the /api/v1/polls endpoints [1][6]. Poll Sessions A PollSession object tracks a session associated with a specific poll, course, and course section [3]. It includes information on whether the session is published (is_published) and if results are public (has_public_results) [3]. It also contains an aggregate results object [3]. New poll sessions can be created via POST /api/v1/polls/:poll_id/poll_sessions [3]. Poll Choices PollChoice objects define the options available for a poll [4][7]. Each object contains an ID, the associated poll_id, the text of the choice, whether it is considered a correct answer (is_correct), and its position order [4][8]. You can manage these via /api/v1/polls/:poll_id/poll_choices [4][7]. Poll Submissions PollSubmission objects manage individual responses to poll sessions [2][9]. A submission is created by specifying the poll_choice_id for a given poll session via POST /api/v1/polls/:poll_id/poll_sessions/:poll_session_id/poll_submissions [2][10][9]. Response Wrapper Objects API responses for these endpoints typically wrap the requested object or array of objects in a root-level key matching the resource name, such as {"poll_submissions": [PollSubmission]} or {"poll_choices": [PollChoice]} [2][3][11].

Citations:


🌐 Web query:

Canvas LMS API batch create assignment overrides response list official documentation

💡 Result:

The official Canvas LMS REST API endpoint for batch creating assignment overrides is POST /api/v1/courses/:course_id/assignments/overrides [1]. According to the official documentation and source code, this endpoint accepts an array of override objects and returns a list of the successfully created AssignmentOverride objects [2][1]. Key details regarding the response and error handling: 1. Response Format: Upon success, the API returns a JSON array containing the created AssignmentOverride objects [1]. 2. Error Handling: The API processes creation within a database transaction, meaning all records are created or none are [2][1]. If errors occur, the response includes an errors attribute, which is an array of error messages corresponding to the inputs [2][1]. Global errors are reported as a single-element array [2][1]. When making the request, the assignment_overrides[] parameter is required and must contain the attributes for each override (such as assignment_id and either student_ids, group_id, or course_section_id) [3][2][1]. Note that if multiple target types (student_ids, group_id, or course_section_id) are provided for a single override, only the most specific one is used (student_ids, then group_id, then course_section_id) and others are ignored [2][1].

Citations:


🌐 Web query:

Canvas LMS API outcome results response outcome_results official documentation

💡 Result:

The Canvas LMS API provides the endpoint GET /api/v1/courses/:course_id/outcome_results to retrieve outcome results for users and outcomes within a specific course context [1][2][3]. ### Endpoint Overview - Endpoint: GET /api/v1/courses/:course_id/outcome_results [1][2] - Purpose: Gets the outcome results for users and outcomes in the specified context (frequently used in the Learning Mastery Gradebook) [1][2][4]. ### Request Parameters The endpoint supports several parameters to filter and include additional data: * user_ids[] (integer): Filters results to only include the specified users (SIS IDs can be used if prefixed with sis_user_id:). It is an error to specify a user who is not a student in the context [1][2][3]. * outcome_ids[] (integer): Filters results to only include the specified outcomes. It is an error to specify an outcome not linked to the context [1][2][3]. * include[] (string): Allows for side-loading additional collections. Accepted values include alignments, outcomes, outcomes.alignments, outcome_groups, outcome_links, outcome_paths, and users [2][3][4]. * include_hidden (boolean): If set to true, includes results that are typically hidden from the Learning Mastery Gradebook and student rollup scores [1][3][4]. ### Example Response Structure The standard response returns a JSON object containing an array of outcome_results [1][2][3]: json { "outcome_results": [OutcomeResult] } When specific include[] parameters are used, the response may also contain a linked object containing additional related data, such as outcomes, users, or alignments, depending on what was requested [5]. For more detailed information, you can consult the official Canvas LMS REST API documentation [2][3].

Citations:


🌐 Web query:

site:canvas.instructure.com/doc/api/polls.html "POST /api/v1/polls" response

💡 Result:

The POST /api/v1/polls endpoint is defined in the Canvas LMS REST API [1]. It is used to create a new poll for the current user [1]. The request requires parameters such as the poll title (polls[][question]) and an optional description (polls[][description]) [1]. Upon a successful request, the endpoint returns a response object containing the created poll details [1]. The structure of the response is a JSON object containing an array of polls, formatted as follows [1]: { "polls": [Poll] } This endpoint belongs to the Polling::PollsController in the Canvas LMS architecture [1].

Citations:


🌐 Web query:

site:canvas.instructure.com/doc/api/poll_choices.html "POST /api/v1/polls/:poll_id/poll_choices" response

💡 Result:

The POST /api/v1/polls/:poll_id/poll_choices endpoint is part of the Canvas LMS REST API and is used to create a new poll choice for a specific poll [1]. According to the official documentation, the response to a successful request returns an object containing a list of the poll's choices [1]. The structure of the response is as follows: { "poll_choices": [PollChoice] } In this response, the [PollChoice] element represents the newly created poll choice object [1]. The endpoint requires the poll_choices[][text] parameter for the descriptive text of the new choice, while poll_choices[][is_correct] (boolean) and poll_choices[][position] (integer) are optional parameters [1].

Citations:


🌐 Web query:

site:canvas.instructure.com/doc/api/poll_sessions.html "POST /api/v1/polls/:poll_id/poll_sessions" response

💡 Result:

The response is:

{
  "poll_sessions": [PollSession]
}

The PollSession object includes fields such as id, poll_id, course_id, course_section_id, is_published, has_public_results, created_at, results, and poll_submissions. (canvas.instructure.com)

Citations:


🌐 Web query:

site:canvas.instructure.com/doc/api/poll_submissions.html "POST /api/v1/polls/:poll_id/poll_sessions/:poll_session_id/poll_submissions" response

💡 Result:

The POST /api/v1/polls/:poll_id/poll_sessions/:poll_session_id/poll_submissions endpoint in the Canvas LMS REST API is used to create a new poll submission for a specific poll session [1]. The request requires the following parameter: | Parameter | Type | Description | | --- | --- | --- | | poll_submissions[][poll_choice_id] | integer | The chosen poll choice ID for this submission | The API returns a response containing the created poll submission object, wrapped in a poll_submissions array [1]. The structure of a PollSubmission object is as follows [1]: { "id": 1023, "poll_choice_id": 155, "user_id": 4555, "created_at": "2013-11-07T13:16:18Z" } The full response structure is [1]: { "poll_submissions": [ { "id": 1023, "poll_choice_id": 155, "user_id": 4555, "created_at": "2013-11-07T13:16:18Z" } ] }

Citations:


🌐 Web query:

site:canvas.instructure.com/doc/api/polls.html "GET /api/v1/polls/:poll_id" "Poll"

💡 Result:

The GET /api/v1/polls/:id endpoint is part of the Canvas LMS REST API [1]. It is used to retrieve the details of a specific poll by its unique identifier [1]. In this endpoint,:id represents the ID of the poll you wish to fetch [1]. The request is handled by the Polling::PollsController#show method within the Canvas backend [1].

Citations:


🌐 Web query:

site:canvas.instructure.com/doc/api/quizzes.html "POST /api/v1/courses/:course_id/quizzes/:quiz_id/submissions" response quiz_submissions

💡 Result:

The Canvas LMS REST API does not provide a standard endpoint with the path POST /api/v1/courses/:course_id/quizzes/:quiz_id/submissions [1]. Based on the official Canvas LMS REST API documentation, quiz-related operations are typically handled through the Quizzes API (e.g., creating or updating quizzes) [1]. Operations involving quiz submissions are generally managed through the Quiz Submissions API, which uses different endpoint structures, such as POST /api/v1/courses/:course_id/quizzes/:quiz_id/submissions (which is not documented as a valid path for creating submissions) or other endpoints specifically designed for interacting with submission data. Because the requested path is not present in the official documentation, there is no defined response format for it [1]. You should verify the intended API documentation for Quiz Submissions or Quiz Reports if you are looking to interact with student attempts or results.

Citations:


Add operation-specific schemas for Canvas response wrappers

CanvasEndpointOutputSchemas[name].parse(response) rejects documented responses for getQuizStatistics, getOutcomeResults, and createBatchOverridesInACourse. Canvas returns wrapper objects for the first two and an array of assignment overrides for the third.

The polling create operations also return wrappers such as { "polls": [...] }, { "poll_choices": [...] }, { "poll_sessions": [...] }, and { "poll_submissions": [...] }. Add explicit schemas for these operations and preserve strict validation instead of using a permissive fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/response-schemas.ts` around lines 415 - 446, Update
createResponseSchema to return operation-specific wrapper schemas for
getQuizStatistics, getOutcomeResults, createBatchOverridesInACourse, and polling
create operations returning polls, poll_choices, poll_sessions, or
poll_submissions. Define or reuse strict Zod schemas matching each documented
response shape, including the assignment-overrides array, and select them by
operation name before generic resource/list handling; do not use a permissive
fallback.

Comment on lines +62 to +67
export type CanvasEndpointInputs = {
[K in CanvasOperationName]: CanvasRequestFields &
(HasPathParams<(typeof canvasOperations)[K]['path']> extends true
? { pathParams: PathParamsFor<(typeof canvasOperations)[K]['path']> }
: { pathParams?: Record<string, string> });
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Type-level body optionality disagrees with the runtime schema.

CanvasEndpointInputs[K] spreads CanvasRequestFields, where body is always optional. createRequestInputSchema requires a non-empty body for every mutation that is not bodyless. A caller can therefore omit body, compile successfully, and fail at runtime inside CanvasEndpointInputSchemas[name].parse.

Mirror the runtime rule in the mapped type. Make body required for mutations that are not bodyless.

🐛 Sketch of the type change
+type RequiresBody<K extends CanvasOperationName> =
+	(typeof canvasOperations)[K]['method'] extends 'POST' | 'PUT' | 'PATCH'
+		? (typeof canvasOperations)[K] extends { bodyless: true }
+			? false
+			: true
+		: false;
+
 export type CanvasEndpointInputs = {
-	[K in CanvasOperationName]: CanvasRequestFields &
+	[K in CanvasOperationName]: Omit<CanvasRequestFields, 'body'> &
+		(RequiresBody<K> extends true
+			? { body: Record<string, unknown> }
+			: { body?: Record<string, unknown> }) &
 		(HasPathParams<(typeof canvasOperations)[K]['path']> extends true
 			? { pathParams: PathParamsFor<(typeof canvasOperations)[K]['path']> }
 			: { pathParams?: Record<string, string> });
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type CanvasEndpointInputs = {
[K in CanvasOperationName]: CanvasRequestFields &
(HasPathParams<(typeof canvasOperations)[K]['path']> extends true
? { pathParams: PathParamsFor<(typeof canvasOperations)[K]['path']> }
: { pathParams?: Record<string, string> });
};
type RequiresBody<K extends CanvasOperationName> =
(typeof canvasOperations)[K]['method'] extends 'POST' | 'PUT' | 'PATCH'
? (typeof canvasOperations)[K] extends { bodyless: true }
? false
: true
: false;
export type CanvasEndpointInputs = {
[K in CanvasOperationName]: Omit<CanvasRequestFields, 'body'> &
(RequiresBody<K> extends true
? { body: Record<string, unknown> }
: { body?: Record<string, unknown> }) &
(HasPathParams<(typeof canvasOperations)[K]['path']> extends true
? { pathParams: PathParamsFor<(typeof canvasOperations)[K]['path']> }
: { pathParams?: Record<string, string> });
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/endpoints/types.ts` around lines 62 - 67, Update
CanvasEndpointInputs to derive body optionality from each operation’s mutation
and bodyless status, matching createRequestInputSchema: require a non-empty body
for mutations that are not bodyless, while preserving optional body for bodyless
or non-mutation operations. Ensure the existing pathParams mapping remains
unchanged.

Comment on lines +44 to +46
if (rows.length === 1) {
return toExternalId(firstString(rows[0]?.id)) ?? null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not use a singleton child account as the tenant identity.

Line 45 returns a child id when the API page has one row. The resulting canvas_account_id cannot match webhook events that carry the root account ID. If parent_account_id is set, use a valid root_account_id or return null.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/webhooks/oauth-tenant-link.ts` around lines 44 - 46, Update
the singleton-row branch in the tenant-link resolution function so it does not
use the child row’s id as the tenant identity when parent_account_id is set.
Prefer a valid root_account_id from the row or related account data, and return
null when no valid root identity is available; preserve the existing behavior
for root accounts.

Comment on lines +5 to +8
function numericAccountId(values: unknown[]): string | null {
const value = firstString(values);
if (!value || !/^\d+$/.test(value)) return null;
return value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Select the first numeric account ID, not the first non-empty value.

Both paths stop when an earlier identifier is non-empty but non-numeric. A valid later account ID is then ignored.

  • packages/canvas/webhooks/tenant-matcher.ts#L5-L8: iterate through values and return the first normalized value that matches ^\d+$.
  • packages/canvas/webhooks/oauth-tenant-link.ts#L74-L76: scan canvas_account_id, account_id, and root_account_id for the first normalized numeric value before checking UUID fields.
📍 Affects 2 files
  • packages/canvas/webhooks/tenant-matcher.ts#L5-L8 (this comment)
  • packages/canvas/webhooks/oauth-tenant-link.ts#L74-L76
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/canvas/webhooks/tenant-matcher.ts` around lines 5 - 8, The
numericAccountId function in packages/canvas/webhooks/tenant-matcher.ts:5-8 must
iterate through all values and return the first normalized string matching
/^\d+$/, rather than validating only firstString(values). In
packages/canvas/webhooks/oauth-tenant-link.ts:74-76, scan canvas_account_id,
account_id, and root_account_id for the first normalized numeric value before
checking UUID fields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:round-1 Review bot posted consolidated findings bot:round-2 Review bot pushed an automated fix core Changes in packages/corsair plugin Changes inside a plugin package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integration request: Canvas LMS

2 participants