<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Akanksha Trehun</title>
    <description>The latest articles on DEV Community by Akanksha Trehun (@magic-peach).</description>
    <link>https://dev.to/magic-peach</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3909309%2F540870bc-2796-4ade-a0dd-68e2fce4e1c2.png</url>
      <title>DEV Community: Akanksha Trehun</title>
      <link>https://dev.to/magic-peach</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/magic-peach"/>
    <language>en</language>
    <item>
      <title>Week 11: Welcome to my Midterm evaluation</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sun, 16 Aug 2026 06:06:06 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-11-welcome-to-my-midterm-evaluation-2l</link>
      <guid>https://dev.to/magic-peach/week-11-welcome-to-my-midterm-evaluation-2l</guid>
      <description>&lt;p&gt;Midterm evaluations ran from the 10th to the 14th. Deep linking and roster sync both&lt;br&gt;
shipped, two more pull requests merged, and then I sat down to write an honest account of&lt;br&gt;
where the project stands — which is how I found out that the plan I have been working&lt;br&gt;
from for a month builds a machine nobody can switch on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep linking, end to end
&lt;/h2&gt;

&lt;p&gt;Two pull requests, two days apart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7772" rel="noopener noreferrer"&gt;#7772&lt;/a&gt; handles the request&lt;br&gt;
side: a launch arrives with &lt;code&gt;LtiDeepLinkingRequest&lt;/code&gt; as its message type instead of&lt;br&gt;
&lt;code&gt;LtiResourceLinkRequest&lt;/code&gt;, and everything downstream has to route differently. The&lt;br&gt;
platform sends along &lt;code&gt;deep_linking_settings&lt;/code&gt; — where to return the response, whether&lt;br&gt;
multiple items are allowed, whether the platform wants a title back — and that has to be&lt;br&gt;
stashed for the response, which happens on a completely separate request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7777" rel="noopener noreferrer"&gt;#7777&lt;/a&gt; is what the instructor&lt;br&gt;
actually sees: a picker listing their circuits and assignments, with the selection posted&lt;br&gt;
back to the LMS as a signed JWT.&lt;/p&gt;

&lt;p&gt;The satisfying part is that neither pull request needed anything new from the protocol&lt;br&gt;
layer. The JWT signing is the key manager from week 4. The validation on the way in is&lt;br&gt;
the validator from week 9. Deep linking is a different &lt;em&gt;conversation&lt;/em&gt; with the platform,&lt;br&gt;
but it is the same handshake underneath, and by this point the handshake is just there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The roster
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7779" rel="noopener noreferrer"&gt;#7779&lt;/a&gt; reads a course roster&lt;br&gt;
over the Names and Roles service — pagination via &lt;code&gt;Link&lt;/code&gt; headers, roles parsed out of the&lt;br&gt;
IMS role URIs. &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7781" rel="noopener noreferrer"&gt;#7781&lt;/a&gt; turns&lt;br&gt;
that into group members.&lt;/p&gt;

&lt;p&gt;The interesting decision in the second one is how users get matched. Not on email — that&lt;br&gt;
was week 5's lesson, and the reason for it holds here too: a platform's privacy settings&lt;br&gt;
may mean no email arrives at all, and even when one does, it might belong to a different&lt;br&gt;
CircuitVerse account than the person clicking. The match is on &lt;code&gt;sub&lt;/code&gt; plus issuer, the&lt;br&gt;
same identity pair the launch signs the user in with. A roster import that mapped on&lt;br&gt;
email could silently add a stranger to a course.&lt;/p&gt;

&lt;p&gt;Members created this way get marked as LTI-synced, which matters for the drop-handling&lt;br&gt;
pull request that follows: when someone disappears from the LMS roster, only synced&lt;br&gt;
members get deactivated. Somebody who joined the CircuitVerse group directly should not&lt;br&gt;
be removed because they were never in Canvas to begin with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two merges
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7715" rel="noopener noreferrer"&gt;#7715&lt;/a&gt;, the JWT validator,&lt;br&gt;
merged on the 11th. &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7763" rel="noopener noreferrer"&gt;#7763&lt;/a&gt;,&lt;br&gt;
the resource link model, merged on the 12th. Both had been sitting approved for a while —&lt;br&gt;
the validator since the end of July.&lt;/p&gt;

&lt;p&gt;Five merged now: the 1.1 passback fix, the deployment model, the JWKS and tool&lt;br&gt;
configuration endpoints, the validator, and the resource link. Eight more open and&lt;br&gt;
waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evaluation
&lt;/h2&gt;

&lt;p&gt;What the evaluation actually asked me to do was describe the state of the project, which&lt;br&gt;
sounds trivial and was not. Writing "here is what works" forces a different question than&lt;br&gt;
"here is what I built this week," and the two answers were not the same shape.&lt;/p&gt;

&lt;p&gt;So I went back to the three issues this project exists to close and read them line by&lt;br&gt;
line against what is in the repository. Not the proposal — the issues, as filed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pipeline nobody could feed
&lt;/h2&gt;

&lt;p&gt;The autograding phase of my plan had six pull requests: a test case model, a headless&lt;br&gt;
runner wrapper, a grading result, a job that runs on submission, a results page, and a&lt;br&gt;
push to the LMS gradebook. Student submits, autograder runs, grade lands in Canvas.&lt;/p&gt;

&lt;p&gt;Every one of those is about &lt;em&gt;executing&lt;/em&gt; test cases. Not one of them is about &lt;em&gt;creating&lt;/em&gt;&lt;br&gt;
test cases.&lt;/p&gt;

&lt;p&gt;The issue says it plainly — "a test case editor in the assignment settings UI where&lt;br&gt;
instructors can add, edit and delete test cases" — and I had somehow read that as&lt;br&gt;
satisfied by a database table. It is not. A model with no UI means the only way to define&lt;br&gt;
a test suite is the Rails console, which is not a feature an instructor can use. I had&lt;br&gt;
planned six pull requests that would ship a grading pipeline with no way to put anything&lt;br&gt;
into it.&lt;/p&gt;

&lt;p&gt;Reading further with that lens, four more gaps fell out. There was no visible/hidden flag&lt;br&gt;
on test cases, which both issues ask for. There was nothing for the autograde&lt;br&gt;
configuration — partial credit, allowed attempts, whether students see the cases before&lt;br&gt;
submitting. "Versioned with the assignment" had no plan behind it at all, and my model&lt;br&gt;
actively worked against it: one row per assignment, edited in place, so changing a suite&lt;br&gt;
would quietly invalidate every grade already produced from it.&lt;/p&gt;

&lt;p&gt;And the least visible one, which I only found by reading the &lt;code&gt;Grade&lt;/code&gt; model rather than&lt;br&gt;
remembering it: nothing in the plan mapped a score onto an assignment's grading scale.&lt;br&gt;
The autograder produces a fraction. &lt;code&gt;Grade&lt;/code&gt; validates against &lt;code&gt;letter&lt;/code&gt;, &lt;code&gt;percent&lt;/code&gt;, or&lt;br&gt;
&lt;code&gt;custom&lt;/code&gt;, and rejects &lt;code&gt;no_scale&lt;/code&gt; outright. Autograding an assignment with no grading&lt;br&gt;
scale would have failed at the last step, after the run, with the student watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twenty-four becomes twenty-seven
&lt;/h2&gt;

&lt;p&gt;Week 7's post was called "Twenty-Four Pull Requests." That plan is now twenty-seven, all&lt;br&gt;
three additions in the autograding phase: the test case editor, the autograde settings,&lt;br&gt;
and the grading-scale mapping — plus reworking the grading result to carry an attempt&lt;br&gt;
number and a snapshot, and folding hidden-case redaction into the results page.&lt;/p&gt;

&lt;p&gt;The snapshot is how "versioned with the assignment" gets solved, and it is the change I&lt;br&gt;
am happiest with. Rather than a version table with all the machinery that implies, each&lt;br&gt;
grading result stores a copy of the suite it ran against. Edit the suite afterwards and&lt;br&gt;
old results stay reproducible, because the thing that produced them is still sitting&lt;br&gt;
right there next to them.&lt;/p&gt;

&lt;p&gt;Three of the nine are written. The model and the runner are pushed; the editor is done&lt;br&gt;
and waiting locally, because it needs the model's table to exist and I would rather open&lt;br&gt;
one clean pull request after the first merges than one showing two commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusing a format instead of inventing one
&lt;/h2&gt;

&lt;p&gt;The nicest thing I wrote this week is four words long.&lt;/p&gt;

&lt;p&gt;A test case needs a name, input pin values, expected output pin values, and a hidden&lt;br&gt;
flag. The simulator's testbench format already has &lt;em&gt;groups&lt;/em&gt; — a named collection of cases&lt;br&gt;
with input and output signals. My first instinct was to add fields: a &lt;code&gt;name&lt;/code&gt; array, a&lt;br&gt;
&lt;code&gt;hidden&lt;/code&gt; array, indices lining up across them.&lt;/p&gt;

&lt;p&gt;Then: a test case is just a group with &lt;code&gt;n: 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One case, one name, one hidden boolean. It validates against the model I had already&lt;br&gt;
written without changing a line of it, it invents no schema, and when the suite reaches&lt;br&gt;
the simulator it is a format the simulator already knows how to run. The editor&lt;br&gt;
serialises its table into exactly that on submit.&lt;/p&gt;

&lt;p&gt;I have written the other version of this before — the one with parallel arrays and a&lt;br&gt;
comment explaining how the indices correspond. It works right up until someone deletes a&lt;br&gt;
row from one array and not the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not reimplementing the simulator
&lt;/h2&gt;

&lt;p&gt;The runner wrapper was the piece I was most nervous about, and the design question was&lt;br&gt;
where the simulation actually happens.&lt;/p&gt;

&lt;p&gt;The tempting answer was Ruby: run the circuit server-side, in the language the rest of&lt;br&gt;
the grading code is written in. I talked myself out of it in about ten minutes. It means&lt;br&gt;
reimplementing thousands of lines of simulation logic, and then maintaining two engines&lt;br&gt;
that will drift — and the failure mode of that drift is a student's circuit passing in&lt;br&gt;
the browser and failing at grading. For a grading tool there is no worse bug.&lt;/p&gt;

&lt;p&gt;Driving a real browser was the next candidate, and &lt;code&gt;capybara-playwright-driver&lt;/code&gt; is&lt;br&gt;
already in the Gemfile, so it looked nearly free. A browser process per submission is not&lt;br&gt;
nearly free.&lt;/p&gt;

&lt;p&gt;Shelling out to Node was the closest call, because there is precedent: the Yosys&lt;br&gt;
integration in this repository does exactly that. I went with an HTTP call to a sidecar&lt;br&gt;
service instead, mirroring the other half of that same integration — a URL from the&lt;br&gt;
environment, the same timeout-and-post shape the simulator controller already uses. It&lt;br&gt;
keeps the engine on the frontend repository's release cadence, where that code and the&lt;br&gt;
people who know it live.&lt;/p&gt;

&lt;p&gt;Every failure collapses into one error class: bad circuit data, a non-2xx, a timeout, a&lt;br&gt;
non-JSON body, an empty result. That is deliberate. The only caller is a background job,&lt;br&gt;
and there is exactly one right answer to all of them — mark the run failed, leave the&lt;br&gt;
student's grade alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project stands
&lt;/h2&gt;

&lt;p&gt;Five merged, eight open, three written and waiting. The protocol layer is done and&lt;br&gt;
merged. Grade passback, deep linking, and roster sync are all in the queue. Autograding is&lt;br&gt;
started.&lt;/p&gt;

&lt;p&gt;The plan grew by three pull requests this week, which on a burndown chart is a bad week.&lt;br&gt;
It is the most useful thing I did. I have spent eleven weeks getting good at making&lt;br&gt;
mechanisms work, and the evaluation caught me having built most of a system that no&lt;br&gt;
instructor could have used, because I kept reading a feature request as a description of&lt;br&gt;
a machine instead of a description of a person trying to get something done.&lt;/p&gt;

&lt;p&gt;Next week: the editor lands, and then the settings and the grading result behind it.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>opensource</category>
      <category>career</category>
      <category>learning</category>
    </item>
    <item>
      <title>Week 10: The Handshake Begins</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:21:38 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-10-the-handshake-begins-3j5n</link>
      <guid>https://dev.to/magic-peach/week-10-the-handshake-begins-3j5n</guid>
      <description>&lt;p&gt;This week the first leg of the LTI 1.3 handshake shipped as&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7746" rel="noopener noreferrer"&gt;#7746&lt;/a&gt;, a static analyser and I&lt;br&gt;
disagreed about CSRF, and I spent an afternoon cleaning up a database mess I had made&lt;br&gt;
for myself weeks earlier.&lt;/p&gt;
&lt;h2&gt;
  
  
  OIDC login initiation
&lt;/h2&gt;

&lt;p&gt;The endpoint is &lt;code&gt;/lti/login&lt;/code&gt;, and it is the URL that the tool configuration from&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7659" rel="noopener noreferrer"&gt;#7659&lt;/a&gt; has been advertising as&lt;br&gt;
&lt;code&gt;oidc_initiation_url&lt;/code&gt; for three weeks while returning 404.&lt;/p&gt;

&lt;p&gt;An LMS calls it with &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;login_hint&lt;/code&gt;, and optionally &lt;code&gt;client_id&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;lti_deployment_id&lt;/code&gt;. CircuitVerse resolves those to a registered deployment, mints a&lt;br&gt;
one-time nonce and a signed state, and redirects the browser to the platform's&lt;br&gt;
authorization endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;302 Found
location: http://canvas.docker/api/lti/authorize_redirect?scope=openid
  &amp;amp;response_type=id_token&amp;amp;response_mode=form_post&amp;amp;prompt=none
  &amp;amp;client_id=...&amp;amp;redirect_uri=.../lti/launch&amp;amp;login_hint=demo-student
  &amp;amp;nonce=ddbf28cc...&amp;amp;state=eyJfcmFpbHMi...--e3a4d84925a8df11c8dc6a...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;response_mode=form_post&lt;/code&gt; and &lt;code&gt;prompt=none&lt;/code&gt; are what make the platform POST the token&lt;br&gt;
straight back rather than showing a login screen. The &lt;code&gt;--&lt;/code&gt; in the state is the HMAC&lt;br&gt;
separator — that is the signed-state design from week 6 finally in production form.&lt;/p&gt;

&lt;p&gt;64 lines of controller code, about 150 lines of tests.&lt;/p&gt;
&lt;h2&gt;
  
  
  Arguing with CodeQL
&lt;/h2&gt;

&lt;p&gt;CI came back with CodeQL failing: &lt;em&gt;CSRF protection weakened or disabled.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It was pointing at this line, which I had edited to add the new action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;skip_before_action&lt;/span&gt; &lt;span class="ss"&gt;:verify_authenticity_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="sx"&gt;%i[launch oidc_login]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first reaction was that this is a false positive. Of course there is no CSRF token —&lt;br&gt;
the request originates from Canvas. There is no way for an LMS to hold a token minted by&lt;br&gt;
our forms.&lt;/p&gt;

&lt;p&gt;My second reaction, which took longer, was that the scanner was pointing at something&lt;br&gt;
real even if its conclusion did not fit. &lt;code&gt;skip_before_action&lt;/code&gt; is a blunt instrument: it&lt;br&gt;
turns the check off for the entire action, for every request shape, forever. What I&lt;br&gt;
actually wanted to say is narrower — &lt;em&gt;this particular kind of request does not need a&lt;br&gt;
token, and everything else still does.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So instead of skipping, I overrode the predicate the check uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verified_request?&lt;/span&gt;
  &lt;span class="k"&gt;super&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"oidc_login"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:iss&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:login_hint&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Token verification stays fully active for every other action and every other request&lt;br&gt;
shape. Only a well-formed LTI initiation is treated as verified. And the justification&lt;br&gt;
holds up: the action reads no session, writes nothing, and the signed state it returns is&lt;br&gt;
what actually protects the launch that follows.&lt;/p&gt;

&lt;p&gt;I checked this was real rather than incidental by removing the override and running the&lt;br&gt;
tests — the cross-site POST spec fails with &lt;code&gt;InvalidAuthenticityToken&lt;/code&gt;. Worth doing,&lt;br&gt;
because a test that passes for the wrong reason is worse than no test. The test&lt;br&gt;
environment disables forgery protection by default, so that spec has to switch it back&lt;br&gt;
on explicitly or it proves nothing at all.&lt;/p&gt;

&lt;p&gt;CodeQL went green, and the code is genuinely narrower than what I started with. The&lt;br&gt;
scanner was more right than I initially gave it credit for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What review caught
&lt;/h2&gt;

&lt;p&gt;Three findings on the pull request, all worth having:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ambiguous deployment matches.&lt;/strong&gt; My lookup ended with &lt;code&gt;scope.order(:id).first&lt;/code&gt;. Since&lt;br&gt;
&lt;code&gt;client_id&lt;/code&gt; is optional in an initiation, a platform with several registrations under&lt;br&gt;
one issuer would silently resolve to whichever row happened to have the lowest ID — and&lt;br&gt;
the launch would be sent to a different registration's client ID and authorization&lt;br&gt;
endpoint. Now an ambiguous match is refused with a 404 rather than guessed at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-HTTPS authorization endpoints.&lt;/strong&gt; The &lt;code&gt;LtiDeployment&lt;/code&gt; model only validates that&lt;br&gt;
&lt;code&gt;auth_login_url&lt;/code&gt; is present, not that it is a sane URL. Since the redirect uses&lt;br&gt;
&lt;code&gt;allow_other_host: true&lt;/code&gt;, a malformed or hostile value in that column goes straight to&lt;br&gt;
the browser. A &lt;code&gt;javascript:&lt;/code&gt; URL is now rejected before it can reach &lt;code&gt;redirect_to&lt;/code&gt;, and&lt;br&gt;
TLS is required in production while plain http still works locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Registered query parameters being discarded.&lt;/strong&gt; Some platforms register an&lt;br&gt;
authorization endpoint that already carries query parameters. I was replacing the query&lt;br&gt;
string wholesale. The suggested fix merged instead of replaced — and introduced a subtle&lt;br&gt;
bug of its own by merging symbol keys into a hash with string keys, so a platform's own&lt;br&gt;
&lt;code&gt;scope=&lt;/code&gt; would have survived alongside ours as a duplicate parameter. String keys&lt;br&gt;
throughout fixed it.&lt;/p&gt;

&lt;p&gt;That last one is a good reminder that a suggested patch is a suggestion. Both PRs this&lt;br&gt;
week had a fix applied through the GitHub web UI that did not splice cleanly — one&lt;br&gt;
duplicated the tail of a method, leaving four unreachable statements and a duplicate&lt;br&gt;
&lt;code&gt;rescue&lt;/code&gt; clause that failed lint. Convenient, but it still needs reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The database mess
&lt;/h2&gt;

&lt;p&gt;Somewhere in this week I tried to run the new endpoint against my development database&lt;br&gt;
and got a &lt;code&gt;NOT NULL&lt;/code&gt; violation on a column that does not exist in the merged schema.&lt;/p&gt;

&lt;p&gt;The cause was archaeology. My abandoned POC branch from week 2 had a migration creating&lt;br&gt;
&lt;code&gt;lti_deployments&lt;/code&gt; with a &lt;code&gt;platform_id&lt;/code&gt; column. I ran it against my dev database in June.&lt;br&gt;
The branch was closed; the migration was never rolled back. The merged version of that&lt;br&gt;
table, from a completely different pull request, has no &lt;code&gt;platform_id&lt;/code&gt; — so my dev&lt;br&gt;
database had a table that matched no branch anyone was working on.&lt;/p&gt;

&lt;p&gt;Tracing it was more interesting than expected. The physical column order gave it away:&lt;br&gt;
&lt;code&gt;platform_public_key&lt;/code&gt; sat &lt;em&gt;after&lt;/em&gt; the timestamps, which meant it had been added by a&lt;br&gt;
later &lt;code&gt;add_column&lt;/code&gt; rather than declared inline — which pinned it to the March POC&lt;br&gt;
migration rather than the June one. Twenty-six migration versions were recorded in my&lt;br&gt;
dev database with no corresponding file on any branch, along with an orphaned&lt;br&gt;
&lt;code&gt;lti_platforms&lt;/code&gt; table whose migration no longer exists anywhere.&lt;/p&gt;

&lt;p&gt;I cleaned it surgically rather than dropping the database: exported the two Canvas&lt;br&gt;
registrations my demo depends on, dropped the drifted columns and tables, cleared the&lt;br&gt;
stale migration stamps, ran the real migration, and re-imported the registrations&lt;br&gt;
against the new schema.&lt;/p&gt;

&lt;p&gt;The lesson is cheap to state and I had to learn it the expensive way: &lt;strong&gt;migrations you&lt;br&gt;
run from a branch that never merges do not clean themselves up.&lt;/strong&gt; A dev database is&lt;br&gt;
long-lived state that accumulates the residue of every experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project stands
&lt;/h2&gt;

&lt;p&gt;Merged: the 1.1 grade passback fix, and the deployment model. Approved and waiting: the&lt;br&gt;
JWKS and tool configuration endpoints, the JWT validator, and now the OIDC login.&lt;/p&gt;

&lt;p&gt;That is four of the twenty-four, with the whole of phase 1's protocol layer either&lt;br&gt;
merged or in the queue.&lt;/p&gt;

&lt;p&gt;Next is the launch itself — verify the state, validate the token through the validator&lt;br&gt;
from week 9, bind the token's deployment to the one the state was issued for, and sign&lt;br&gt;
the user in on their &lt;code&gt;sub&lt;/code&gt;. It is the piece where all the separate bricks finally do&lt;br&gt;
something a user can see: a Canvas click landing a signed-in user inside CircuitVerse.&lt;/p&gt;

&lt;p&gt;It is also the one I need to be most careful with, and I have a list of open questions&lt;br&gt;
to settle before writing it — what happens when a platform's privacy settings mean it&lt;br&gt;
sends no email address at all, what happens when that email already belongs to a&lt;br&gt;
CircuitVerse account, and where the nonce gets recorded so a state cannot be replayed&lt;br&gt;
inside its five-minute window.&lt;/p&gt;

&lt;p&gt;Ten weeks ago I would have just written it and found out.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>circuitverse</category>
      <category>lti</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Week 9: Verifying the Token</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:19:37 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-9-verifying-the-token-4mmg</link>
      <guid>https://dev.to/magic-peach/week-9-verifying-the-token-4mmg</guid>
      <description>&lt;p&gt;Two pull requests merged this week and the most security-critical piece of the whole&lt;br&gt;
project went out for review on its own. This was the week the plan from week 7 started&lt;br&gt;
visibly paying off.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two merges
&lt;/h2&gt;

&lt;p&gt;On 28 July, &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7654" rel="noopener noreferrer"&gt;#7654&lt;/a&gt; merged — the&lt;br&gt;
&lt;code&gt;LtiDeployment&lt;/code&gt; model. CircuitVerse can now store which LMS platforms it trusts: the&lt;br&gt;
issuer, the client ID, the deployment ID, and the platform's endpoint URLs, unique per&lt;br&gt;
issuer + client + deployment because that triple is how LTI 1.3 identifies a deployment.&lt;/p&gt;

&lt;p&gt;Nothing reads the table yet. That is the point of building this way, and it is the thing&lt;br&gt;
that needs explaining in every PR description.&lt;/p&gt;
&lt;h2&gt;
  
  
  The validator
&lt;/h2&gt;

&lt;p&gt;The main work was &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7715" rel="noopener noreferrer"&gt;#7715&lt;/a&gt;:&lt;br&gt;
&lt;code&gt;Lti::JwtValidator&lt;/code&gt;, a class with one public method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Lti&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;JwtValidator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deployment&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give it an &lt;code&gt;id_token&lt;/code&gt;, the deployment it should belong to, and the nonce we issued. It&lt;br&gt;
either returns the verified payload or raises. No controller, no session, no Rails&lt;br&gt;
request object. That isolation was forced by the constraint that it had to be its own&lt;br&gt;
pull request, and it is a much better boundary than I would have drawn otherwise.&lt;/p&gt;
&lt;h3&gt;
  
  
  What it has to refuse
&lt;/h3&gt;

&lt;p&gt;The tests are mostly attacks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A token signed by the wrong key.&lt;/strong&gt; The baseline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A token with &lt;code&gt;alg: none&lt;/code&gt;.&lt;/strong&gt; Early JWT libraries would accept an unsigned token if it&lt;br&gt;
declared its algorithm as &lt;code&gt;none&lt;/code&gt;. Any validator that reads the algorithm out of the&lt;br&gt;
token and trusts it is exploitable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An HS256 token where RS256 is expected.&lt;/strong&gt; This is algorithm confusion, and it is the&lt;br&gt;
subtle one. RS256 is asymmetric: the platform signs with a private key, we verify with&lt;br&gt;
their public key, which is published. HS256 is symmetric: the same secret signs and&lt;br&gt;
verifies. If a validator takes the algorithm from the token header and passes the&lt;br&gt;
platform's public key as the key, an attacker can sign a token using that &lt;em&gt;public&lt;/em&gt; key&lt;br&gt;
as an HMAC secret — and the validator verifies it, because it is doing exactly what it&lt;br&gt;
was told. The public key is public, so anyone can do this.&lt;/p&gt;

&lt;p&gt;The defence is to never read the algorithm from the token. Pass an explicit allow-list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;encoded_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify_signature!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;algorithm: &lt;/span&gt;&lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expired, wrong issuer, wrong audience, missing subject, replayed nonce&lt;/strong&gt;, and the&lt;br&gt;
multi-audience case: when &lt;code&gt;aud&lt;/code&gt; is an array with more than one entry, the &lt;code&gt;azp&lt;/code&gt; claim&lt;br&gt;
must name our client ID.&lt;/p&gt;
&lt;h3&gt;
  
  
  Resolving the platform's key
&lt;/h3&gt;

&lt;p&gt;The other half is fetching the right public key. A platform publishes a JWKS endpoint;&lt;br&gt;
the token header carries a &lt;code&gt;kid&lt;/code&gt; identifying which key signed it.&lt;/p&gt;

&lt;p&gt;Two things made this more interesting than expected:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key rotation.&lt;/strong&gt; Platforms rotate keys. A cached key set will not contain a newly&lt;br&gt;
rotated key. So a &lt;code&gt;kid&lt;/code&gt; that is missing from the cache is treated as a signal that&lt;br&gt;
rotation may have happened, and triggers a refetch rather than a wait for expiry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching.&lt;/strong&gt; Without it, every single launch makes an outbound HTTP request to the&lt;br&gt;
platform before anyone can sign in. If the platform's JWKS endpoint is slow, every login&lt;br&gt;
is slow; if it is down, every login is down. A short-lived cache keyed on the JWKS URL&lt;br&gt;
fixes that, with the rotation behaviour above as the escape hatch.&lt;/p&gt;
&lt;h3&gt;
  
  
  What review found
&lt;/h3&gt;

&lt;p&gt;The bot review flagged two failure modes I had not handled, and both were correct:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A blank &lt;code&gt;jwks_url&lt;/code&gt;.&lt;/strong&gt; A deployment configured with only a stored public key and no&lt;br&gt;
JWKS URL would still make an HTTP request, because a blank URL parses as a relative path&lt;br&gt;
rather than failing. So every launch for that deployment took a pointless network trip&lt;br&gt;
before falling back. One guard clause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Malformed stored key data.&lt;/strong&gt; &lt;code&gt;OpenSSL::PKey::RSA.new&lt;/code&gt; raises &lt;code&gt;OpenSSL::PKey::RSAError&lt;/code&gt;&lt;br&gt;
on bad PEM data, and I was only rescuing &lt;code&gt;JWT::DecodeError&lt;/code&gt;. So a misconfigured&lt;br&gt;
deployment record would propagate an OpenSSL exception out of a method whose entire&lt;br&gt;
contract is "raises &lt;code&gt;ValidationError&lt;/code&gt;". Callers written against that contract would not&lt;br&gt;
catch it.&lt;/p&gt;

&lt;p&gt;That second one is a good example of a bug that is invisible while you are writing the&lt;br&gt;
code and obvious once named. My mental model was "this method validates tokens, so it&lt;br&gt;
raises validation errors." The actual behaviour was "this method raises validation&lt;br&gt;
errors &lt;em&gt;unless the database row is malformed&lt;/em&gt;, in which case it raises something else&lt;br&gt;
entirely."&lt;/p&gt;

&lt;p&gt;I fixed both in &lt;code&gt;fix(lti): harden JwtValidator key resolution per review&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The refactor request
&lt;/h2&gt;

&lt;p&gt;Late in the week, a maintainer asked something more interesting than a bug report:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;could you suggest refactoring this class using JWT::Token or JWT::EncodedToken&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My implementation called &lt;code&gt;JWT.decode&lt;/code&gt; twice — once with verification disabled to read&lt;br&gt;
the header and pick a key by &lt;code&gt;kid&lt;/code&gt;, then again with verification on. That works, and it&lt;br&gt;
has a smell: there is a window where the code holds an unverified, parsed token, and&lt;br&gt;
nothing but my own discipline stops me from reading a claim out of it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;JWT::EncodedToken&lt;/code&gt; closes that window at the library level. &lt;code&gt;#payload&lt;/code&gt; refuses to&lt;br&gt;
decode until both the signature and the claims have been verified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;payload&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@signature_verified&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@claims_verified&lt;/span&gt;
  &lt;span class="n"&gt;decoded_payload&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header is still read unverified, which is unavoidable — you cannot pick a key&lt;br&gt;
without knowing which key was used — but it is only ever used to select a candidate key,&lt;br&gt;
and the ordering the class used to depend on by convention is now enforced by the&lt;br&gt;
library.&lt;/p&gt;

&lt;p&gt;That is a strictly better property: the difference between "the code is correct" and&lt;br&gt;
"the code cannot easily be made incorrect by the next person to edit it."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am taking from the week
&lt;/h2&gt;

&lt;p&gt;The isolation was the win. Because the validator was its own pull request, with no&lt;br&gt;
controller and no session attached, both the review comments were about &lt;em&gt;the validator&lt;/em&gt;&lt;br&gt;
— its error contract, its failure modes — rather than about whether the launch flow&lt;br&gt;
around it made sense.&lt;/p&gt;

&lt;p&gt;Compare that to my POC, where the same class was buried in 2,000 lines and nobody looked&lt;br&gt;
at it at all.&lt;/p&gt;

&lt;p&gt;Next week: the first half of the handshake ships, and a scanner disagrees with me about&lt;br&gt;
CSRF.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>circuitverse</category>
      <category>lti</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Week 8: The First Merge, and Learning to Wait</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:18:20 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-8-the-first-merge-and-learning-to-wait-1pa2</link>
      <guid>https://dev.to/magic-peach/week-8-the-first-merge-and-learning-to-wait-1pa2</guid>
      <description>&lt;p&gt;On 26 July, &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7647" rel="noopener noreferrer"&gt;#7647&lt;/a&gt; merged. It&lt;br&gt;
was the first piece of my project to land in CircuitVerse's master branch, fourteen&lt;br&gt;
days after I opened it and about seven weeks into the project.&lt;/p&gt;

&lt;p&gt;It is a small change. It makes sure an LTI 1.1 grade is written to CircuitVerse's&lt;br&gt;
database before it is pushed to the LMS gradebook, so the two cannot silently disagree&lt;br&gt;
when a local save fails. Plus the fix from last week's review — clearing stale grading&lt;br&gt;
context only after a launch has been verified, so a forged request cannot wipe a&lt;br&gt;
teacher's session on its way to being rejected.&lt;/p&gt;

&lt;p&gt;Most of this week was spent not writing code, which is a thing I want to write about&lt;br&gt;
honestly rather than skip over.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually shipped
&lt;/h2&gt;

&lt;p&gt;The merged change has two halves, and the second only exists because of review.&lt;/p&gt;

&lt;p&gt;The first is ordering: persist the grade, then push it to the LMS. Previously the&lt;br&gt;
passback ran first, so a local save that failed validation left the gradebook holding a&lt;br&gt;
score CircuitVerse had no record of. The LMS is the more visible system, which makes&lt;br&gt;
this the worse direction to be wrong in — a student and an instructor both see a grade&lt;br&gt;
that does not exist anywhere in our database.&lt;/p&gt;

&lt;p&gt;The second is the fix from &lt;code&gt;anxkhn&lt;/code&gt;'s review: stale grading context is now cleared only&lt;br&gt;
after a launch has been verified, not before. An unauthenticated request that gets&lt;br&gt;
rejected can no longer wipe a teacher's in-progress grading session on its way out.&lt;/p&gt;

&lt;p&gt;Both are small. Neither is glamorous. Together they mean the gradebook and CircuitVerse&lt;br&gt;
cannot silently disagree, in an integration that schools are using today, which is worth&lt;br&gt;
more than another thousand lines of the 1.3 work sitting unmerged on a branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the week
&lt;/h2&gt;

&lt;p&gt;My commit graph for 16–25 July is empty. Two pull requests were open and waiting&lt;br&gt;
(&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7654" rel="noopener noreferrer"&gt;#7654&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7659" rel="noopener noreferrer"&gt;#7659&lt;/a&gt;), one was awaiting the&lt;br&gt;
final look before merge, and the next thing in the plan — the JWT validator — depended&lt;br&gt;
on decisions inside them.&lt;/p&gt;

&lt;p&gt;Early in the project, a week like this would have gone one of two ways. Either I would&lt;br&gt;
have sat refreshing the PR page, or I would have started building three PRs ahead and&lt;br&gt;
piled up work that might need reworking depending on how the review went.&lt;/p&gt;

&lt;p&gt;I had done the second thing already, in weeks 2 and 3, and it is how I ended up with a&lt;br&gt;
2,000 line branch nobody could review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What review latency actually is
&lt;/h2&gt;

&lt;p&gt;The reframe that helped: maintainer response time is not an obstacle in the process. It&lt;br&gt;
&lt;em&gt;is&lt;/em&gt; the process.&lt;/p&gt;

&lt;p&gt;CircuitVerse's maintainers review contributions alongside their own jobs. A day or two&lt;br&gt;
of latency on a pull request is not neglect, it is the normal operating speed of a&lt;br&gt;
volunteer-maintained project. Every plan I make has to be built for that speed rather&lt;br&gt;
than in spite of it.&lt;/p&gt;

&lt;p&gt;Concretely, that changes what "being blocked" means. If PR N is in review and PR N+1&lt;br&gt;
depends on it, I am not blocked from working — I am blocked from &lt;em&gt;committing to a&lt;br&gt;
particular implementation&lt;/em&gt; of N+1. Those are different, and the gap between them is&lt;br&gt;
where the useful work of a waiting week lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did with it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Read the review comments properly.&lt;/strong&gt; Not just applying them, but working out the&lt;br&gt;
general rule behind each. &lt;code&gt;anxkhn&lt;/code&gt;'s note about clearing grading context before&lt;br&gt;
verification was specifically about one method, but the rule it implies — nothing that&lt;br&gt;
mutates state may happen before the security check, including deletes — is one I could&lt;br&gt;
then apply to code they had not looked at. A review comment is a sample from a&lt;br&gt;
distribution of mistakes I am prone to. Fixing the sample is the minimum; finding the&lt;br&gt;
distribution is the value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrote the next PR's tests before its code.&lt;/strong&gt; The JWT validator was next, and while I&lt;br&gt;
did not want to commit to its implementation before the deployment model merged, the&lt;br&gt;
&lt;em&gt;attacks it must survive&lt;/em&gt; did not depend on any of that. A token signed with the wrong&lt;br&gt;
key. A token with &lt;code&gt;alg: none&lt;/code&gt;. An HS256 token where the validator expects RS256 —&lt;br&gt;
algorithm confusion, where an attacker signs with the public key as an HMAC secret and a&lt;br&gt;
naive validator accepts it. Expired tokens, wrong issuer, wrong audience, missing&lt;br&gt;
subject, replayed nonce.&lt;/p&gt;

&lt;p&gt;Writing those first meant that when the validator got written, its shape was determined&lt;br&gt;
by what it had to refuse rather than by what I found convenient to implement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read other people's LTI implementations.&lt;/strong&gt; Specifically for how they handle key&lt;br&gt;
rotation — a platform can publish a new signing key at any time, and a tool that caches&lt;br&gt;
the key set has to notice. The pattern I settled on: cache the key set, but if a token&lt;br&gt;
arrives with a &lt;code&gt;kid&lt;/code&gt; that is not in the cache, refetch immediately rather than waiting&lt;br&gt;
for the cache to expire. Rotation is exactly the case where the cache is stale, so&lt;br&gt;
treating a cache miss as a rotation signal is the right default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kept the queue moving.&lt;/strong&gt; The point of the twenty-four PR plan is a steady stream of&lt;br&gt;
small reviewable things. That only works if the queue is never empty from my side, and&lt;br&gt;
never so full that it becomes a burden. Two open at a time, both small, both green,&lt;br&gt;
turned out to be about right.&lt;/p&gt;

&lt;h2&gt;
  
  
  On the size of the win
&lt;/h2&gt;

&lt;p&gt;Fourteen days from open to merge, for a change of about 150 lines that fixes a real bug&lt;br&gt;
in a feature real users have today.&lt;/p&gt;

&lt;p&gt;Measured in lines of code per day it is not much. But the previous seven weeks produced&lt;br&gt;
zero merged lines, and the thing that changed was not that I started working harder — it&lt;br&gt;
was that I started sending work in a form somebody could say yes to.&lt;/p&gt;

&lt;p&gt;That is the actual lesson of the first half of this project, and I could not have&lt;br&gt;
learned it by reading about it. I had to spend three weeks building something good that&lt;br&gt;
could not merge.&lt;/p&gt;

&lt;p&gt;There is a version of this project where I never write the POC, go straight to small&lt;br&gt;
PRs, and have six things merged by now. I do not think I would have understood &lt;em&gt;why&lt;/em&gt;&lt;br&gt;
they had to be small. The POC being closed is what made the constraint make sense rather&lt;br&gt;
than feel like bureaucracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where things stand
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Merged:&lt;/strong&gt; the 1.1 grade passback fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approved and waiting:&lt;/strong&gt; the deployment model, and the JWKS and tool configuration
endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next:&lt;/strong&gt; the JWT validator — the security-critical brick, reviewed on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next week: the token validator, and finding out my key-resolution code had two failure&lt;br&gt;
modes I had not handled.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>circuitverse</category>
      <category>lti</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Week 7: Creating phases for the tasks</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:16:10 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-7-twenty-four-pull-requests-46ff</link>
      <guid>https://dev.to/magic-peach/week-7-twenty-four-pull-requests-46ff</guid>
      <description>&lt;p&gt;This was the week the project got a plan it could actually execute, and the week a&lt;br&gt;
reviewer found a security bug in a fix I had already convinced myself was correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The review
&lt;/h2&gt;

&lt;p&gt;I had opened &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7647" rel="noopener noreferrer"&gt;#7647&lt;/a&gt; at the end&lt;br&gt;
of last week — the small fix ensuring an LTI 1.1 grade is saved locally before being&lt;br&gt;
pushed to the LMS.&lt;/p&gt;

&lt;p&gt;On 14 July, &lt;code&gt;anxkhn&lt;/code&gt; reviewed it. The first comment was housekeeping: rebase and look&lt;br&gt;
at failing CI. I rebased, tests went green, and I said so.&lt;/p&gt;

&lt;p&gt;The second comment is the one worth writing about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;storing the context is fixed, but it is still cleared before &lt;code&gt;valid_request?&lt;/code&gt;. for&lt;br&gt;
example, a fake launch could clear a teacher's valid grading session and prevent the&lt;br&gt;
next grade passback. please clear it only after a valid launch too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let me unpack what that means, because it is genuinely subtle.&lt;/p&gt;

&lt;p&gt;An LTI 1.1 launch stores some grading context in the session — the outcome service URL&lt;br&gt;
the LMS wants scores posted to, and which assignment is being graded. My change made&lt;br&gt;
sure that context is written &lt;em&gt;after&lt;/em&gt; the launch signature is verified. Good.&lt;/p&gt;

&lt;p&gt;But the &lt;em&gt;clearing&lt;/em&gt; of stale context happened in a &lt;code&gt;before_action&lt;/code&gt;, which runs before&lt;br&gt;
&lt;code&gt;valid_request?&lt;/code&gt; — before we know whether the launch is genuine at all.&lt;/p&gt;

&lt;p&gt;So: a teacher launches from Canvas, gets a valid grading session, starts grading.&lt;br&gt;
Meanwhile anyone can POST garbage at &lt;code&gt;/lti/launch&lt;/code&gt; with no valid signature. That request&lt;br&gt;
gets rejected — 401, correct — but on its way to being rejected it has already wiped&lt;br&gt;
the teacher's grading context. Their next grade passback silently fails.&lt;/p&gt;

&lt;p&gt;An unauthenticated request cannot sign in, cannot read anything, cannot write&lt;br&gt;
anything — and can still degrade a legitimate user's session as a side effect of being&lt;br&gt;
rejected.&lt;/p&gt;

&lt;p&gt;I had read that code many times. I had written the fix for the adjacent bug in the same&lt;br&gt;
method. I did not see it, because I was asking "is the write ordered correctly?" and the&lt;br&gt;
bug was in the delete.&lt;/p&gt;

&lt;p&gt;What I take from it: &lt;strong&gt;when you fix ordering around a security check, audit every&lt;br&gt;
operation on that path, not just the one you came for.&lt;/strong&gt; The invariant is not "the write&lt;br&gt;
happens after verification." It is "nothing that affects state happens before&lt;br&gt;
verification." I had fixed one instance of a general rule while leaving another in&lt;br&gt;
place.&lt;/p&gt;

&lt;p&gt;I also want to record that this is the value of review that no amount of care&lt;br&gt;
substitutes for. Not because the reviewer knew something I did not, but because they&lt;br&gt;
came to the code without my assumption about which line was interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other review comment
&lt;/h2&gt;

&lt;p&gt;The same day, &lt;code&gt;anxkhn&lt;/code&gt; reviewed&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7654" rel="noopener noreferrer"&gt;#7654&lt;/a&gt;, the &lt;code&gt;LtiDeployment&lt;/code&gt;&lt;br&gt;
model, and approved it — with a nitpick:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;could you change &lt;code&gt;Fixes #7405&lt;/code&gt; to &lt;code&gt;Part of #7405&lt;/code&gt;? this PR is the first foundation&lt;br&gt;
step, so merging it should not close the larger issue.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Small, and it corrected a real misunderstanding. I had been writing &lt;code&gt;Fixes #7405&lt;/code&gt; on&lt;br&gt;
every LTI pull request out of habit. GitHub's auto-close keywords mean the first one to&lt;br&gt;
merge would have closed the umbrella issue tracking the entire LTI 1.3 upgrade — with&lt;br&gt;
about 90% of it unbuilt.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Part of #7405&lt;/code&gt; links without closing. I have used that phrasing on every LTI PR since.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twenty-four pull requests
&lt;/h2&gt;

&lt;p&gt;The bigger piece of the week was planning. On 14 July I wrote out the remaining work as&lt;br&gt;
a sequence of pull requests, and the constraint I set was that no single one should&lt;br&gt;
exceed roughly 200 hand-written lines. Generated files like &lt;code&gt;Gemfile.lock&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;db/schema.rb&lt;/code&gt; do not count, but get flagged for reviewers.&lt;/p&gt;

&lt;p&gt;It came to twenty-four.&lt;/p&gt;

&lt;p&gt;The number was startling and also clarifying. What used to be "implement LTI 1.3" became&lt;br&gt;
a dependency-ordered queue, with phases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase 0&lt;/strong&gt; — the credibility work: fix the 1.1 bug that already affects real users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1&lt;/strong&gt; — the launch core, five PRs: gems and the deployment model; key manager
and public endpoints; the JWT validator alone; OIDC login initiation; the launch
itself; then the resource link model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2&lt;/strong&gt; — Assignment and Grade Services, so grades flow to the LMS gradebook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 3&lt;/strong&gt; — Deep Linking, so an instructor can embed specific CircuitVerse content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 4&lt;/strong&gt; — Names and Roles, so the roster syncs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 5&lt;/strong&gt; — autograding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thing that surprised me is how much the constraint improved the &lt;em&gt;design&lt;/em&gt;, not just&lt;br&gt;
the delivery. Forced to make &lt;code&gt;Lti::JwtValidator&lt;/code&gt; a standalone pull request, it has to be&lt;br&gt;
a class that takes a token and a deployment and returns a payload — no controller, no&lt;br&gt;
session, no Rails request. That is a better boundary than I would have drawn if I were&lt;br&gt;
free to let it reach into whatever it wanted.&lt;/p&gt;

&lt;p&gt;The same happened to the OIDC login. Made to stand alone, it has to hand back a state&lt;br&gt;
that the launch can verify later with no shared memory between them — which is exactly&lt;br&gt;
the signed-state design from last week. The constraint and the correct architecture&lt;br&gt;
pointed the same way.&lt;/p&gt;

&lt;p&gt;Two rules I wrote down alongside the plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If a PR crosses ~200 lines, split the spec edge cases into a follow-up — &lt;strong&gt;never&lt;/strong&gt;
thin out security validation to fit the budget.&lt;/li&gt;
&lt;li&gt;Every PR description names which future PR depends on it, so a maintainer can see why
a change with no user-visible behaviour deserves to merge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That second rule is doing a lot of work. The hardest thing about this approach is that&lt;br&gt;
most of the PRs do nothing on their own. A deployments table nothing reads. A validator&lt;br&gt;
nothing calls. Without the dependency note, each looks like dead code, and "we don't&lt;br&gt;
merge unused code" is a reasonable instinct for a maintainer to have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping against the plan
&lt;/h2&gt;

&lt;p&gt;Two PRs went out under the new scheme:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7654" rel="noopener noreferrer"&gt;#7654&lt;/a&gt; — the &lt;code&gt;LtiDeployment&lt;/code&gt;&lt;br&gt;
model: issuer, client ID, deployment ID, and the platform's endpoint URLs, unique per&lt;br&gt;
issuer + client + deployment. 97 lines. Storage only, nothing reads it yet. Approved&lt;br&gt;
the day it was reviewed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7659" rel="noopener noreferrer"&gt;#7659&lt;/a&gt; — the JWKS and tool&lt;br&gt;
configuration endpoints, plus a key manager that owns CircuitVerse's own RSA keypair.&lt;br&gt;
This is what makes CircuitVerse &lt;em&gt;registerable&lt;/em&gt;: an LMS admin fetches the tool&lt;br&gt;
configuration, and the platform can fetch our public key to verify anything we sign.&lt;br&gt;
193 lines, behind the flag.&lt;/p&gt;

&lt;p&gt;Both were reviewable in a sitting. Which, compared to a 2,000 line POC closed the next&lt;br&gt;
day, is the whole difference.&lt;/p&gt;

&lt;p&gt;Next week: waiting, and what I did with it.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>circuitverse</category>
      <category>opensource</category>
      <category>planning</category>
    </item>
    <item>
      <title>Week 6: Signing the State, and Closing My Own Pull Requests</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:13:42 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-6-signing-the-state-and-closing-my-own-pull-requests-4nkb</link>
      <guid>https://dev.to/magic-peach/week-6-signing-the-state-and-closing-my-own-pull-requests-4nkb</guid>
      <description>&lt;p&gt;Three things happened this week: I undid the cookie change from week 5 with something&lt;br&gt;
better, I put the entire LTI 1.3 feature behind a flag that defaults to off, and I&lt;br&gt;
closed two of my own pull requests.&lt;/p&gt;
&lt;h2&gt;
  
  
  Undoing the cookie
&lt;/h2&gt;

&lt;p&gt;Last week I set &lt;code&gt;SameSite=None&lt;/code&gt; on the session cookie so it would survive the&lt;br&gt;
cross-site POST that carries the &lt;code&gt;id_token&lt;/code&gt; back from the platform. It worked, and it&lt;br&gt;
weakened a CSRF defence across the whole application to serve one endpoint.&lt;/p&gt;

&lt;p&gt;This week's commit is&lt;br&gt;
&lt;code&gt;fix(lti): sign the OIDC state instead of weakening the session cookie&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The reframe that unlocked it: I had been treating "the state must survive the round&lt;br&gt;
trip" as "the state must be &lt;em&gt;stored&lt;/em&gt; somewhere that survives the round trip." Those are&lt;br&gt;
not the same requirement. The state does not need to be stored at all. It needs to be&lt;br&gt;
&lt;em&gt;recognisable&lt;/em&gt; when it comes back.&lt;/p&gt;

&lt;p&gt;So instead of keeping a value in the session and comparing, the state becomes a signed,&lt;br&gt;
self-contained token carrying its own payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;LTI_STATE_PURPOSE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lti.launch.state"&lt;/span&gt;
&lt;span class="no"&gt;LTI_STATE_TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;

&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lti_state_verifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"nonce"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"deployment_id"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="ss"&gt;purpose: &lt;/span&gt;&lt;span class="no"&gt;LTI_STATE_PURPOSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;expires_in: &lt;/span&gt;&lt;span class="no"&gt;LTI_STATE_TTL&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Rails.application.message_verifier&lt;/code&gt; signs it with the application's secret. When the&lt;br&gt;
launch returns, we verify the signature, and if it checks out we know the contents came&lt;br&gt;
from us and have not been altered. The nonce and the deployment ID ride inside the&lt;br&gt;
token itself.&lt;/p&gt;

&lt;p&gt;What this buys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No session, so no cookie problem.&lt;/strong&gt; Nothing needs to survive the cross-site POST
except the parameter the platform is already sending back to us.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cookie policy change.&lt;/strong&gt; &lt;code&gt;SameSite=Lax&lt;/code&gt; stays as it is for every other page on
the site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No server-side state.&lt;/strong&gt; No table, no cleanup job, no expiry sweeper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The signature is the CSRF defence.&lt;/strong&gt; Only CircuitVerse can mint a valid state, so a
launch carrying one is answering an initiation we started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It expires.&lt;/strong&gt; Five minutes, enforced by the verifier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;purpose&lt;/code&gt; argument is a detail worth calling out. It scopes the signature to this&lt;br&gt;
one use, so a token signed elsewhere in the application with the same secret will not&lt;br&gt;
verify here. Same secret, different purposes, no cross-use.&lt;/p&gt;

&lt;p&gt;The general lesson: when a constraint blocks you, check whether you are solving the&lt;br&gt;
constraint or the requirement behind it. "Survive a cross-site POST" sounded like a&lt;br&gt;
cookie problem. It was a state-management problem, and moving the state out of the&lt;br&gt;
session dissolved the cookie question entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flag
&lt;/h2&gt;

&lt;p&gt;The commit &lt;code&gt;feat(lti): gate LTI 1.3 behind a default-off lti_advantage flag&lt;/code&gt; does what&lt;br&gt;
it says: every 1.3 endpoint returns 404 unless an operator turns the flag on.&lt;/p&gt;

&lt;p&gt;The reason is specific. The 1.3 launch &lt;strong&gt;auto-provisions user accounts&lt;/strong&gt;. A launch from&lt;br&gt;
a registered platform creates a CircuitVerse user, confirmed, ready to sign in. That is&lt;br&gt;
correct behaviour for the feature and it is also a significant thing to have live in&lt;br&gt;
production the moment a branch merges.&lt;/p&gt;

&lt;p&gt;A default-off flag means the code can land, be reviewed, be tested, and sit there inert&lt;br&gt;
until somebody deliberately switches it on. It decouples "this code is merged" from&lt;br&gt;
"this code is running", which for an auth path is exactly the separation you want.&lt;/p&gt;

&lt;p&gt;There was also a small commit removing some stale comments about CSRF token handling —&lt;br&gt;
notes I had written to myself that had stopped being true after the state change.&lt;br&gt;
Comments that describe an older version of the code are worse than no comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing two pull requests
&lt;/h2&gt;

&lt;p&gt;On 12 July I closed &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7510" rel="noopener noreferrer"&gt;#7510&lt;/a&gt;,&lt;br&gt;
the &lt;code&gt;lti-advantage&lt;/code&gt; gem addition, and&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7632" rel="noopener noreferrer"&gt;#7632&lt;/a&gt;, the subgroups feature.&lt;/p&gt;

&lt;p&gt;The gem one was straightforward: I had already reversed that decision in week 4 and&lt;br&gt;
gone direct to &lt;code&gt;ruby-jwt&lt;/code&gt;. Leaving the PR open was just noise in the queue. Closing your&lt;br&gt;
own obsolete pull requests is a small courtesy — every open PR is something a maintainer&lt;br&gt;
has to look at at least once to decide it is not for them.&lt;/p&gt;

&lt;p&gt;Subgroups was harder, because that one was finished. Data layer, UI, controller, API&lt;br&gt;
endpoints, scoping, tests. Earlier in the week I had added the REST API v1 endpoints and&lt;br&gt;
fixed a bug where a synced user who was also a subgroup mentor got a duplicate&lt;br&gt;
membership row inserted.&lt;/p&gt;

&lt;p&gt;But it had the same problem as my original POC: it was a large change to a core model,&lt;br&gt;
arriving as one piece, at a moment when the maintainers' attention on my work was&lt;br&gt;
pointed at LTI. It was not going to get the review it needed, and holding it open would&lt;br&gt;
not change that.&lt;/p&gt;

&lt;p&gt;The work is not lost — the branch is intact and the design still holds up. It just is&lt;br&gt;
not the thing I should be spending review capital on right now.&lt;/p&gt;

&lt;p&gt;That framing — &lt;strong&gt;review capacity as a budget you spend&lt;/strong&gt; — was new to me this week.&lt;br&gt;
Maintainer attention is the scarcest resource on the project. Every PR I open spends&lt;br&gt;
some, and if I spend it on something that is not on the critical path, the thing that&lt;br&gt;
is on the critical path waits longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opening the first small one
&lt;/h2&gt;

&lt;p&gt;Right at the end of the week I opened&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7647" rel="noopener noreferrer"&gt;#7647&lt;/a&gt;, and it is deliberately&lt;br&gt;
tiny.&lt;/p&gt;

&lt;p&gt;It fixes a bug in the &lt;em&gt;existing&lt;/em&gt; LTI 1.1 code: when a teacher grades a project in an LTI&lt;br&gt;
session, the score was pushed to the LMS before the grade was saved locally. If the&lt;br&gt;
local save then failed validation, the LMS would show a grade that CircuitVerse had&lt;br&gt;
never stored. The gradebook and the source of truth silently disagree.&lt;/p&gt;

&lt;p&gt;It has nothing to do with 1.3. I picked it as the first thing to send precisely because&lt;br&gt;
it is small, self-contained, and improves the integration that already has real users —&lt;br&gt;
and because it demonstrates I can send something a maintainer can review in ten minutes.&lt;/p&gt;

&lt;p&gt;Which, after week 3, felt like the point.&lt;/p&gt;

&lt;p&gt;Next week: a reviewer finds a bug in it that I had looked straight past, and I cut the&lt;br&gt;
remaining work into twenty-four pull requests.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>lti</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Community Bonding: Before the First Line of Project Code</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:01:18 +0000</pubDate>
      <link>https://dev.to/magic-peach/community-bonding-before-the-first-line-of-project-code-5ia</link>
      <guid>https://dev.to/magic-peach/community-bonding-before-the-first-line-of-project-code-5ia</guid>
      <description>&lt;p&gt;Coding starts on 1 June. This post is about the three weeks before that — what I did&lt;br&gt;
with them, and what I thought the project was going to be, which turned out to be only&lt;br&gt;
partly right.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I got here
&lt;/h2&gt;

&lt;p&gt;By the time the accepted projects were announced, I had already sent CircuitVerse about&lt;br&gt;
forty pull requests.&lt;/p&gt;

&lt;p&gt;That number sounds more impressive than it is. A lot of them were typo fixes in&lt;br&gt;
documentation and specs — genuinely trivial changes, the kind you send while you are&lt;br&gt;
still working out where anything lives. Some were closed. Some are still open.&lt;/p&gt;

&lt;p&gt;But they did their job, which was less about the changes themselves than about learning&lt;br&gt;
the shape of the project: how the app is laid out, what the review process feels like,&lt;br&gt;
which parts of the codebase are load-bearing and which are quiet corners nobody has&lt;br&gt;
touched in three years.&lt;/p&gt;

&lt;p&gt;The ones I am actually pleased with came later and were small bug fixes rather than&lt;br&gt;
typos — scoping an assignments index to the current user's groups, removing a duplicate&lt;br&gt;
&lt;code&gt;update!&lt;/code&gt; call, adding &lt;code&gt;includes(:user)&lt;/code&gt; to some notification callbacks that were firing&lt;br&gt;
N+1 queries.&lt;/p&gt;

&lt;p&gt;One of them turned out to be a signpost: a partial unique index on&lt;br&gt;
&lt;code&gt;assignments.lti_consumer_key&lt;/code&gt;, because LTI 1.1 launches look assignments up by that&lt;br&gt;
column and there was nothing enforcing uniqueness. That was the first time I read&lt;br&gt;
CircuitVerse's LMS integration properly, months before I proposed to rewrite it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning a proposal into issues
&lt;/h2&gt;

&lt;p&gt;The most useful thing I did during bonding was on 25 May, and it produced no code at&lt;br&gt;
all. I broke the project down into four separate feature issues and filed them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/issues/7405" rel="noopener noreferrer"&gt;#7405&lt;/a&gt; — upgrade the LMS
integration from LTI 1.1 to LTI 1.3&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/issues/7406" rel="noopener noreferrer"&gt;#7406&lt;/a&gt; — roster sync, so a
class list flows from the LMS into a CircuitVerse group&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/issues/7407" rel="noopener noreferrer"&gt;#7407&lt;/a&gt; — grade passback via
Assignment and Grade Services&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/issues/7408" rel="noopener noreferrer"&gt;#7408&lt;/a&gt; — deep linking, so an
instructor can embed specific CircuitVerse content in a course page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A proposal is a document you write for a small audience during a selection process, and&lt;br&gt;
then it mostly sits still. Issues are different. They live in the repository where the&lt;br&gt;
work happens, anyone can comment on them, and they force you to state what you are&lt;br&gt;
actually going to build in terms a maintainer can disagree with &lt;em&gt;before&lt;/em&gt; you have built&lt;br&gt;
anything.&lt;/p&gt;

&lt;p&gt;Writing them also clarified something for me. In the proposal, "LTI 1.3 support" was one&lt;br&gt;
line item. Split into four issues, the dependency structure became obvious: the launch&lt;br&gt;
has to exist before grades can flow back, because grade passback needs the deployment&lt;br&gt;
and resource-link context that a launch establishes. Roster sync and deep linking both&lt;br&gt;
need the same. #7405 is not one of four features, it is the foundation the other three&lt;br&gt;
stand on.&lt;/p&gt;

&lt;p&gt;That ordering shaped everything since.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the code I was proposing to replace
&lt;/h2&gt;

&lt;p&gt;The other substantial piece of bonding was reading CircuitVerse's existing LTI 1.1&lt;br&gt;
integration line by line — the controller, the score submission service, the launch&lt;br&gt;
views, the specs.&lt;/p&gt;

&lt;p&gt;I went in expecting to find it neglected, because 1.1 is the old standard and my project&lt;br&gt;
is the new one. That is not what I found. It is compact and it works, and it is used by&lt;br&gt;
real courses today.&lt;/p&gt;

&lt;p&gt;I did find things I noted for later. Grades were being pushed to the LMS before being&lt;br&gt;
saved locally, so a failed local save could leave the gradebook holding a score&lt;br&gt;
CircuitVerse never stored. A &lt;code&gt;NameError&lt;/code&gt; in a code path that only triggers under&lt;br&gt;
specific conditions. Small gaps, mostly in error handling rather than in the happy path.&lt;/p&gt;

&lt;p&gt;Two of them became my first pull requests of the coding period.&lt;/p&gt;

&lt;p&gt;Reading it also settled a design question I had been vague about in the proposal.&lt;br&gt;
Nothing about my project requires removing 1.1, and plenty of institutions are still on&lt;br&gt;
it. So the plan became strictly additive: 1.3 arrives alongside 1.1, both work, and&lt;br&gt;
nobody's existing setup breaks. "Backward compatible" stopped being a nice phrase in a&lt;br&gt;
proposal and became a constraint I could check every change against.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mergathon
&lt;/h2&gt;

&lt;p&gt;On 26 May, CircuitVerse ran a Mergathon — a coordinated push across all the org's&lt;br&gt;
repositories to deal with the pull request backlog. I spent the day on triage.&lt;/p&gt;

&lt;p&gt;I have permissions to label, close and comment, not to merge, so the work was: read&lt;br&gt;
every open PR, work out whether it was still alive, and close the ones that were not&lt;br&gt;
with an explanation.&lt;/p&gt;

&lt;p&gt;The numbers were bracing. On the Interactive-Book repository I closed around forty-six&lt;br&gt;
stale pull requests, some open since 2019. Thirteen more on the Vue frontend, five on&lt;br&gt;
the blog. On the mobile app there were ten Dependabot PRs all failing for the same&lt;br&gt;
reason — a dependency requiring a newer Flutter SDK than CI was running — so I traced&lt;br&gt;
the root cause once and commented it on all ten. A handful of others were genuinely&lt;br&gt;
green and just needed someone to say so, which I labelled for a maintainer to merge.&lt;/p&gt;

&lt;p&gt;Reading dozens of dead pull requests in one sitting is an unusual way to spend a day and&lt;br&gt;
I learned more from it than from anything else in bonding. Almost none of them died&lt;br&gt;
because the code was wrong. They died from going quiet — the author moved on, the branch&lt;br&gt;
drifted behind master, a maintainer asked a question that was never answered, and then&lt;br&gt;
too much time passed for anyone to want to pick it up.&lt;/p&gt;

&lt;p&gt;The pattern I noticed and did not fully absorb: the bigger a pull request was, the more&lt;br&gt;
likely it was to be in that pile. Small ones either merged or got closed quickly. Large&lt;br&gt;
ones lingered until they were unmergeable.&lt;/p&gt;

&lt;p&gt;I understood that as an observation about &lt;em&gt;other people's&lt;/em&gt; pull requests. It took until&lt;br&gt;
week 3 of the coding period, and having my own large one closed, to understand it was&lt;br&gt;
about mine too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up something to test against
&lt;/h2&gt;

&lt;p&gt;The last piece was practical: getting an LMS I could actually launch from. Reading the&lt;br&gt;
LTI specification tells you what the protocol says. Watching a real Canvas instance&lt;br&gt;
refuse your launch for reasons the spec does not mention teaches you a different set of&lt;br&gt;
things.&lt;/p&gt;

&lt;p&gt;This took longer than expected, which I gather is normal. LTI is a protocol where two&lt;br&gt;
servers have to agree about URLs, keys and identifiers before anything works at all, and&lt;br&gt;
every mismatch surfaces as the same unhelpful failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I expected to be hard
&lt;/h2&gt;

&lt;p&gt;Going into 1 June, I thought the difficulty of this project was the protocol —&lt;br&gt;
signatures, key rotation, the security model, getting the cryptography right.&lt;/p&gt;

&lt;p&gt;Some of that has been hard. But the protocol is a written specification with a right&lt;br&gt;
answer; if you read it carefully you converge on it.&lt;/p&gt;

&lt;p&gt;The part I did not budget for is the one this whole first stretch has actually turned&lt;br&gt;
on: getting the work into a shape somebody else can be confident about. That is not in&lt;br&gt;
any specification, and it is not something I understood until I had done it wrong.&lt;/p&gt;

&lt;p&gt;Next: week 1, and actually writing some code.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>programming</category>
      <category>lti</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Week 5: Identity Is Not an Email Address</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Fri, 07 Aug 2026 14:15:14 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-5-identity-is-not-an-email-address-4ie9</link>
      <guid>https://dev.to/magic-peach/week-5-identity-is-not-an-email-address-4ie9</guid>
      <description>&lt;p&gt;This week produced the single most important line of the project so far, and a decision&lt;br&gt;
I would reverse four days later. It also took a detour into a completely different&lt;br&gt;
feature, which turned out to be a better use of time than it sounds.&lt;/p&gt;
&lt;h2&gt;
  
  
  The claim you must not trust
&lt;/h2&gt;

&lt;p&gt;The commit is&lt;br&gt;
&lt;code&gt;fix(lti): key 1.3 identity on validated sub, not the email claim&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the problem. When a launch arrives, the &lt;code&gt;id_token&lt;/code&gt; contains claims about the&lt;br&gt;
user — a subject identifier (&lt;code&gt;sub&lt;/code&gt;), usually a name, and often an email address. The&lt;br&gt;
natural way to sign somebody in is to take that email and look them up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is what my POC did, and it is wrong in a way that took me a while to see, because&lt;br&gt;
it &lt;em&gt;works perfectly&lt;/em&gt; in every test you would think to write.&lt;/p&gt;

&lt;p&gt;The token's signature proves the platform sent it. It does not prove the platform&lt;br&gt;
verified anything inside it. The email claim is whatever the LMS has in its user record&lt;br&gt;
— self-asserted, frequently unverified, sometimes editable by the user in their own&lt;br&gt;
profile settings.&lt;/p&gt;

&lt;p&gt;So if a CircuitVerse account exists with the email &lt;code&gt;maintainer@circuitverse.org&lt;/code&gt;, and&lt;br&gt;
somebody can get an LMS to emit a launch with that email claim, matching on email hands&lt;br&gt;
them that account. The token is valid. The signature checks out. And they are signed in&lt;br&gt;
as somebody else.&lt;/p&gt;

&lt;p&gt;The fix is to identify people by the &lt;code&gt;sub&lt;/code&gt; claim, which is the platform's own stable&lt;br&gt;
identifier for that user and is not user-editable, scoped to the deployment it came&lt;br&gt;
from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_or_create_by!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;provider: &lt;/span&gt;&lt;span class="s2"&gt;"lti"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;uid: &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sub'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scoping matters as much as the choice of claim. Two different institutions can both&lt;br&gt;
have a user with &lt;code&gt;sub&lt;/code&gt; of &lt;code&gt;12345&lt;/code&gt;. Without the deployment prefix, those two people&lt;br&gt;
collide into one CircuitVerse account.&lt;/p&gt;

&lt;p&gt;Email still gets used — when provisioning a brand new account, we have to put something&lt;br&gt;
in the email column. But it is used as an attribute of a user we have already&lt;br&gt;
identified, never as the thing that identifies them. That distinction is the whole&lt;br&gt;
lesson: &lt;strong&gt;authentication data and profile data are different, even when they arrive in&lt;br&gt;
the same signed envelope.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The companion commit, &lt;code&gt;fix(lti): return 401 for tokens missing required claims&lt;/code&gt;, comes&lt;br&gt;
from the same place. If &lt;code&gt;sub&lt;/code&gt; is missing, there is no identity to key on, and the right&lt;br&gt;
answer is to reject the launch rather than fall back to something more convenient.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cookie I weakened
&lt;/h2&gt;

&lt;p&gt;The other commit from this week is&lt;br&gt;
&lt;code&gt;Add Canvas placements to tool config and SameSite=None session cookie&lt;/code&gt;, and I want to&lt;br&gt;
write about it honestly because I undid half of it the following week.&lt;/p&gt;

&lt;p&gt;The problem is real. When the platform POSTs the &lt;code&gt;id_token&lt;/code&gt; back to CircuitVerse, that&lt;br&gt;
is a &lt;strong&gt;cross-site POST&lt;/strong&gt; — the request originates from Canvas's domain and lands on&lt;br&gt;
ours. Browsers do not send &lt;code&gt;SameSite=Lax&lt;/code&gt; cookies on cross-site POSTs. That is the&lt;br&gt;
entire point of &lt;code&gt;SameSite&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had stored the login state in the session. So when the launch came back, the session&lt;br&gt;
cookie was not sent, the session was empty, and the launch failed with no state to&lt;br&gt;
check against.&lt;/p&gt;

&lt;p&gt;The fix I reached for was to set &lt;code&gt;SameSite=None&lt;/code&gt; on the session cookie, which makes the&lt;br&gt;
browser send it on cross-site requests. It worked immediately.&lt;/p&gt;

&lt;p&gt;It is also a change to how &lt;em&gt;every&lt;/em&gt; cookie in the application behaves, made to serve one&lt;br&gt;
endpoint. Every page on CircuitVerse would have a session cookie that browsers are&lt;br&gt;
willing to attach to cross-site requests, weakening a default that exists specifically&lt;br&gt;
to blunt CSRF — for a feature that is off by default and used by a small fraction of&lt;br&gt;
users.&lt;/p&gt;

&lt;p&gt;I did not see it that way when I wrote it. I saw a blocked feature and a config flag&lt;br&gt;
that unblocked it. Seeing it required asking a different question: not "does this make&lt;br&gt;
the launch work" but "what else does this change, for people not using this feature at&lt;br&gt;
all?" More on how I fixed it next week.&lt;/p&gt;

&lt;p&gt;The other half of the commit has aged better: Canvas placements in the tool&lt;br&gt;
configuration document. Placements are how a tool declares where it wants to appear in&lt;br&gt;
the LMS — course navigation, assignment selection. It is the difference between a tool&lt;br&gt;
an admin can register and a tool an instructor can actually find.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detour: subgroups
&lt;/h2&gt;

&lt;p&gt;Midweek I switched to something unrelated: subgroups within a group, so an instructor&lt;br&gt;
can split a class into smaller teams. Commits&lt;br&gt;
&lt;code&gt;feat(groups): add self-referential subgroups data layer&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;feat(groups): subgroup management UI, controller and API scoping&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The design decision worth recording: a subgroup is just a &lt;code&gt;Group&lt;/code&gt; with a&lt;br&gt;
&lt;code&gt;parent_group_id&lt;/code&gt;. That is it. No new model, no parallel hierarchy.&lt;/p&gt;

&lt;p&gt;The alternative was a dedicated &lt;code&gt;Subgroup&lt;/code&gt; model. It looks cleaner on a diagram and it&lt;br&gt;
is much worse in practice, because assignments, membership, grades, notifications and&lt;br&gt;
authorization policies are all written against &lt;code&gt;Group&lt;/code&gt;. A new model means every one of&lt;br&gt;
those systems needs to learn about a second kind of container, and every one of them is&lt;br&gt;
a place to forget. A self-referential association means all of that machinery works&lt;br&gt;
unchanged, and the entire feature is a nullable foreign key plus scoping.&lt;/p&gt;

&lt;p&gt;Why work on this at all mid-project? Partly because it had been asked for. But the&lt;br&gt;
honest reason is that I was waiting on review for the LTI work, and I had learned the&lt;br&gt;
week before that pushing on a branch nobody has agreed to review does not move anything&lt;br&gt;
forward. Having a second track meant review latency stopped being dead time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell myself
&lt;/h2&gt;

&lt;p&gt;Two things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The signature does not vouch for the contents.&lt;/strong&gt; A signed token proves who sent it,&lt;br&gt;
not that what it says is true. Everything inside still needs to be evaluated on whether&lt;br&gt;
the sender had any business asserting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It works" is where you start asking questions, not where you stop.&lt;/strong&gt; The&lt;br&gt;
&lt;code&gt;SameSite=None&lt;/code&gt; change worked on the first try. Working was exactly what stopped me&lt;br&gt;
looking at it.&lt;/p&gt;

&lt;p&gt;Next week: undoing the cookie change properly, and two pull requests getting closed on&lt;br&gt;
the same day.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>opensource</category>
      <category>api</category>
    </item>
    <item>
      <title>Week 4: Rebuilding From the Spec (and Dropping the Gem)</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Fri, 07 Aug 2026 14:11:20 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-4-rebuilding-from-the-spec-and-dropping-the-gem-1in0</link>
      <guid>https://dev.to/magic-peach/week-4-rebuilding-from-the-spec-and-dropping-the-gem-1in0</guid>
      <description>&lt;p&gt;After a week with nothing to show, this one produced a lot: the LTI 1.3 infrastructure&lt;br&gt;
rebuilt from the specification rather than from other people's example code, a&lt;br&gt;
dependency added and then removed again, and the three security checks that make a 1.3&lt;br&gt;
launch trustworthy written as three separate, named things instead of one blob.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with the gem
&lt;/h2&gt;

&lt;p&gt;I had opened &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7510" rel="noopener noreferrer"&gt;#7510&lt;/a&gt; the&lt;br&gt;
previous week to add &lt;code&gt;lti-advantage&lt;/code&gt; 0.1.0, and I had a separate issue&lt;br&gt;
(&lt;a href="https://github.com/CircuitVerse/CircuitVerse/issues/7509" rel="noopener noreferrer"&gt;#7509&lt;/a&gt;) tracking it. My&lt;br&gt;
assumption was the ordinary one: this is a well-specified protocol, somebody has&lt;br&gt;
written the gem, I should use the gem rather than hand-rolling security code.&lt;/p&gt;

&lt;p&gt;That assumption is usually right. It was not right here, and the version number was the&lt;br&gt;
clue I ignored. &lt;code&gt;0.1.0&lt;/code&gt; is not a version that has been through much contact with&lt;br&gt;
production.&lt;/p&gt;

&lt;p&gt;When I started wiring the rebuilt launch against it, the shape of the problem became&lt;br&gt;
clear. The gem gave me some convenience wrappers around things &lt;code&gt;ruby-jwt&lt;/code&gt; already does&lt;br&gt;
well, and did not give me the parts that are actually hard: resolving a platform's&lt;br&gt;
public key by &lt;code&gt;kid&lt;/code&gt; from its JWKS endpoint, handling key rotation, binding a token to a&lt;br&gt;
specific deployment. Those I was going to write regardless.&lt;/p&gt;

&lt;p&gt;So I was adding a dependency, in the authentication path, that I would still have to&lt;br&gt;
write the security-critical code around. That is the worst of both: a supply-chain&lt;br&gt;
surface without the benefit.&lt;/p&gt;

&lt;p&gt;By the end of the week I had reversed it. Two commits tell that story —&lt;br&gt;
&lt;code&gt;build(deps): restore ims-lti for the LTI 1.1 launch path&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;build(deps): drop unused lti-advantage gem&lt;/code&gt;. CircuitVerse keeps &lt;code&gt;ims-lti&lt;/code&gt; for the&lt;br&gt;
existing 1.1 path, which still has real users, and 1.3 gets built directly on&lt;br&gt;
&lt;code&gt;ruby-jwt&lt;/code&gt;, which is mature and which I understand.&lt;/p&gt;

&lt;p&gt;The lesson I want to keep: "there's a gem for it" is a hypothesis, not a conclusion.&lt;br&gt;
Check what it actually does before you build on it, especially in an auth path. The&lt;br&gt;
cost of finding out late is that you have already designed around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three checks
&lt;/h2&gt;

&lt;p&gt;The main work of the week was the commit&lt;br&gt;
&lt;code&gt;fix(lti): enforce state, nonce, and deployment binding on 1.3 launch&lt;/code&gt;. In the POC, all&lt;br&gt;
of this had been one function that either returned a user or didn't. Pulling the three&lt;br&gt;
defences apart and naming them individually changed how I thought about each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State.&lt;/strong&gt; The &lt;code&gt;state&lt;/code&gt; parameter is a value the tool generates when it starts the login&lt;br&gt;
and checks when the launch comes back. Without it, anyone can POST a token at your&lt;br&gt;
launch endpoint and you have no idea whether you ever asked for that login. It is the&lt;br&gt;
CSRF defence for the handshake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nonce.&lt;/strong&gt; The tool generates a nonce, sends it in the authorization request, and the&lt;br&gt;
platform copies it into the &lt;code&gt;id_token&lt;/code&gt; it returns. When the token comes back, the nonce&lt;br&gt;
must match the one issued. This ties the token to &lt;em&gt;this&lt;/em&gt; login attempt. Without it a&lt;br&gt;
token captured once could be presented again later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment binding.&lt;/strong&gt; This is the one I had missed entirely in the POC, and the one&lt;br&gt;
reading the spec properly surfaced. A platform is identified by an issuer, a client ID,&lt;br&gt;
and a deployment ID. Every one of those platforms signs its own valid tokens. So a&lt;br&gt;
token can be perfectly valid — correct signature, unexpired, well-formed — and still be&lt;br&gt;
from a completely different institution's Canvas than the one that started this login.&lt;br&gt;
The token has to be bound to the deployment the login was initiated against, or a valid&lt;br&gt;
token from platform B can be presented in a session opened for platform A.&lt;/p&gt;

&lt;p&gt;It is the kind of hole you do not find by testing, because every test you write by hand&lt;br&gt;
uses one platform. You find it by reading the spec and asking what each field is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity, and a bug I created for myself
&lt;/h2&gt;

&lt;p&gt;Two smaller commits this week were about actually signing the user in, and both came&lt;br&gt;
from things breaking.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fix(lti): confirm LTI 1.3 provisioned users so sign_in succeeds&lt;/code&gt; — CircuitVerse uses&lt;br&gt;
Devise with the &lt;code&gt;:confirmable&lt;/code&gt; module, so a newly created user has to have a&lt;br&gt;
confirmation timestamp before they can be signed in. When CircuitVerse provisions an&lt;br&gt;
account from a launch, the platform has already authenticated that person; there is no&lt;br&gt;
email round-trip to wait for. So provisioning sets &lt;code&gt;confirmed_at&lt;/code&gt; at creation. Obvious&lt;br&gt;
in hindsight, and it took a while to see, because the failure looked like the sign-in&lt;br&gt;
silently doing nothing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fix(spec): stub JWKS HTTP request in LTI 1.3 launch tests&lt;/code&gt; — the launch fetches the&lt;br&gt;
platform's public keys over HTTP. My tests were really trying to make that request.&lt;br&gt;
Stubbing it made the suite fast and deterministic, and it forced me to be explicit&lt;br&gt;
about what the JWKS response actually looks like, which paid off later when I came to&lt;br&gt;
handle key rotation.&lt;/p&gt;

&lt;p&gt;There was also a housekeeping commit,&lt;br&gt;
&lt;code&gt;fix(lti): add missing schema migration and resolve all rubocop offenses&lt;/code&gt;. Not&lt;br&gt;
interesting, except as a reminder that "it works" and "it is ready for someone else to&lt;br&gt;
look at" are different states.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing tests against forgeries
&lt;/h2&gt;

&lt;p&gt;The other shift this week was in how I write tests for this. My instinct had been to&lt;br&gt;
test the happy path thoroughly and add a couple of failure cases.&lt;/p&gt;

&lt;p&gt;For authentication code that is the wrong ratio. The happy path is the case an attacker&lt;br&gt;
does not care about. What matters is every way a token can be wrong: signed with the&lt;br&gt;
wrong key, signed with no key at all, signed with a symmetric algorithm when we expect&lt;br&gt;
an asymmetric one, expired, wrong issuer, wrong audience, right issuer but wrong&lt;br&gt;
deployment, missing the subject claim, replayed nonce.&lt;/p&gt;

&lt;p&gt;Each of those is a test that fails loudly if I ever weaken the validator. That framing —&lt;br&gt;
the test suite as a set of attacks the code has to survive, not a set of features it has&lt;br&gt;
to have — is the thing I have found most useful to internalise, and it is what made the&lt;br&gt;
validator work later in the project reviewable in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves the week
&lt;/h2&gt;

&lt;p&gt;The protocol layer exists again, built from the spec, with the three defences separated&lt;br&gt;
and named, no unnecessary dependency in the auth path, and a test suite that mostly&lt;br&gt;
consists of things going wrong.&lt;/p&gt;

&lt;p&gt;What it does not have yet is a shape a reviewer can accept. It is still one branch with&lt;br&gt;
everything on it. That problem is still ahead of me, and it is the one that closed my&lt;br&gt;
POC.&lt;/p&gt;

&lt;p&gt;Next week: the email address a platform sends you is not proof of anything, and I&lt;br&gt;
weaken a cookie in a way I will have to undo.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>opensource</category>
      <category>api</category>
    </item>
    <item>
      <title>Week 3: The Week Nothing Merged</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Fri, 07 Aug 2026 14:10:25 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-3-the-week-nothing-merged-4mbj</link>
      <guid>https://dev.to/magic-peach/week-3-the-week-nothing-merged-4mbj</guid>
      <description>&lt;h2&gt;
  
  
  What I had built
&lt;/h2&gt;

&lt;p&gt;The project is upgrading CircuitVerse's LMS integration from LTI 1.1 to LTI 1.3&lt;br&gt;
(issue &lt;a href="https://github.com/CircuitVerse/CircuitVerse/issues/7405" rel="noopener noreferrer"&gt;#7405&lt;/a&gt;). LTI is the&lt;br&gt;
standard that lets a tool like CircuitVerse live inside Canvas or Moodle: a student&lt;br&gt;
clicks an assignment in their course, and they land inside CircuitVerse already signed&lt;br&gt;
in, with their grade flowing back to the gradebook.&lt;/p&gt;

&lt;p&gt;CircuitVerse currently speaks LTI 1.1, which authenticates a launch with an OAuth 1.0&lt;br&gt;
signature over a shared secret. Canvas has deprecated that for new installations. LTI&lt;br&gt;
1.3 replaces it with OpenID Connect and signed JWTs.&lt;/p&gt;

&lt;p&gt;At the start of week 2 I opened &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7488" rel="noopener noreferrer"&gt;#7488&lt;/a&gt;,&lt;br&gt;
titled &lt;code&gt;[POC] Feat/lti 1 3 upgrade&lt;/code&gt;. It was the whole thing. Migrations for a new&lt;br&gt;
deployments table, a JWT validator, an OIDC login endpoint, a rewritten launch, key&lt;br&gt;
management, a JWKS endpoint, tool configuration, user provisioning, and a rewritten&lt;br&gt;
test suite. It worked. I had a real Canvas container talking to a local CircuitVerse&lt;br&gt;
and a real user landing signed in.&lt;/p&gt;

&lt;p&gt;It was closed the next day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sitting with that
&lt;/h2&gt;

&lt;p&gt;My first instinct was that this was a communication failure — that if I had explained&lt;br&gt;
the design better, or written a longer description, it would have gone differently. I&lt;br&gt;
spent a chunk of this week drafting exactly that: a better writeup of the same pull&lt;br&gt;
request.&lt;/p&gt;

&lt;p&gt;Somewhere in the middle of writing it I opened the diff and tried to review it as if&lt;br&gt;
someone else had sent it to me. That was the moment it clicked.&lt;/p&gt;

&lt;p&gt;The change touched authentication. It auto-provisioned user accounts. It added a code&lt;br&gt;
path that signs somebody in based on a token that arrived from an external server. To&lt;br&gt;
approve it, a maintainer would have to hold all of that in their head at once and be&lt;br&gt;
confident that no part of it was wrong. Not "probably fine" — confident, because the&lt;br&gt;
failure mode is somebody signing in as somebody else.&lt;/p&gt;

&lt;p&gt;I could not have reviewed it myself, and I wrote it.&lt;/p&gt;

&lt;p&gt;That is not a communication problem. No description makes a change like that&lt;br&gt;
reviewable. The size &lt;em&gt;is&lt;/em&gt; the problem, and a better cover letter would only have made&lt;br&gt;
it more persuasive without making it more correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I had actually gotten wrong
&lt;/h2&gt;

&lt;p&gt;I had been treating the pull request as a delivery mechanism — a box you put finished&lt;br&gt;
work in. Get the feature working, put it in the box, hand it over.&lt;/p&gt;

&lt;p&gt;For a project where a maintainer has to put their name on the merge, that is backwards.&lt;br&gt;
The pull request is the unit of review, and review is the thing that has to be possible.&lt;br&gt;
Which means the size and shape of a change is not a packaging decision made at the end.&lt;br&gt;
It is a design decision made at the start, and it constrains how you build.&lt;/p&gt;

&lt;p&gt;A 2,000 line change that works is worth less than a 150 line change that a reviewer can&lt;br&gt;
be confident about, because only one of them can actually land. Code that cannot merge&lt;br&gt;
is not really finished, no matter how well it runs on my machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did with the rest of the week
&lt;/h2&gt;

&lt;p&gt;Two smaller pull requests were sitting in the queue and I left them there rather than&lt;br&gt;
poking at them:&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7502" rel="noopener noreferrer"&gt;#7502&lt;/a&gt;, a &lt;code&gt;NameError&lt;/code&gt; fix in&lt;br&gt;
the existing 1.1 code, and&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7510" rel="noopener noreferrer"&gt;#7510&lt;/a&gt;, which swapped in an&lt;br&gt;
&lt;code&gt;lti-advantage&lt;/code&gt; gem. Both had been open since the previous week.&lt;/p&gt;

&lt;p&gt;Instead I went back to the specification. Not skimming for the parts I needed to make&lt;br&gt;
Canvas work — reading it properly, in order, including the parts I had implemented by&lt;br&gt;
copying from other people's tools.&lt;/p&gt;

&lt;p&gt;That turned out to matter. There is a real difference between "I made the handshake&lt;br&gt;
succeed" and "I understand what each step of the handshake is defending against." I had&lt;br&gt;
the first. Writing the OIDC login by pattern-matching against blog posts, I could tell&lt;br&gt;
you the &lt;code&gt;state&lt;/code&gt; parameter is required. I could not have told you crisply what an&lt;br&gt;
attacker does if you get it wrong.&lt;/p&gt;

&lt;p&gt;Going through it deliberately, the shape of the protocol became much clearer. The&lt;br&gt;
handshake has two legs, and each carries a specific defence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The platform asks the tool to begin a login. The tool generates a &lt;strong&gt;nonce&lt;/strong&gt; and a
&lt;strong&gt;state&lt;/strong&gt;, and redirects back to the platform's authorization endpoint.&lt;/li&gt;
&lt;li&gt;The platform authenticates the user and POSTs an &lt;code&gt;id_token&lt;/code&gt; back to the tool. The
tool verifies the token's signature against the platform's published keys, checks the
nonce it issued, and only then signs anyone in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;state&lt;/code&gt; proves the launch answers an initiation &lt;em&gt;we&lt;/em&gt; started. The &lt;code&gt;nonce&lt;/code&gt; ties the&lt;br&gt;
token to that one initiation, so a captured token cannot be replayed. The signature&lt;br&gt;
proves the platform, and only the platform, minted the token. Three separate defences,&lt;br&gt;
and I had implemented all three as one undifferentiated blob of code.&lt;/p&gt;

&lt;p&gt;Once I saw them as separate defences, they started to look like separate pieces of&lt;br&gt;
work. Which is, I suspect, the point I was supposed to reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am carrying into next week
&lt;/h2&gt;

&lt;p&gt;Rebuilding, and rebuilding differently. Not the same code re-uploaded in smaller&lt;br&gt;
chunks, but actually reconsidered — because reading the spec properly turned up things&lt;br&gt;
my POC had glossed over, particularly around binding a token to the specific deployment&lt;br&gt;
that issued it.&lt;/p&gt;

&lt;p&gt;The plan is to start from the pieces that stand alone and can be judged on their own&lt;br&gt;
terms. A storage model for platform registrations does not need the launch to exist to&lt;br&gt;
be reviewable. A JWT validator can be unit tested against forged tokens without any&lt;br&gt;
controller at all.&lt;/p&gt;

&lt;p&gt;I will be honest that the week felt bad while I was in it. There is a particular&lt;br&gt;
discomfort in having working code and no way to ship it, and in reporting a week with&lt;br&gt;
an empty commit graph. But I would rather find out in week 3 that my approach to&lt;br&gt;
shipping was wrong than in week 10.&lt;/p&gt;

&lt;p&gt;Next week: rebuilding the protocol layer, and discovering that the gem I had been&lt;br&gt;
counting on does less than its name suggests.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>github</category>
      <category>opensource</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Week 2: Pull Requests, Rejected Code, and the Art of Not Breaking Things</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 13 Jun 2026 15:53:27 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-2-pull-requests-rejected-code-and-the-art-of-not-breaking-things-mcf</link>
      <guid>https://dev.to/magic-peach/week-2-pull-requests-rejected-code-and-the-art-of-not-breaking-things-mcf</guid>
      <description>&lt;p&gt;&lt;em&gt;GSoC 2026 | CircuitVerse × Canvas LMS LTI 1.3 Integration&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If Week 1 was about getting familiar with the codebase and understanding what needed to be built, Week 2 was about learning the hard way that writing code is only half the job. The other half — the messier, more humbling half — is getting that code accepted by the people who actually maintain the project.&lt;/p&gt;

&lt;p&gt;This week was full of detours, rejected pull requests, reviewer feedback that stung a little, and a surprisingly frustrating fight with a two-letter word in Ruby. But by the end of it, I had something real to show: a clean, reviewed, and submitted change to CircuitVerse that lays the foundation for the entire LTI 1.3 integration. Let me walk you through it.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Quick Refresher: What Are We Building?
&lt;/h2&gt;

&lt;p&gt;CircuitVerse is an open-source platform where students can build and simulate digital circuits right in their browser. The project I'm working on aims to connect CircuitVerse with Canvas, one of the most widely used Learning Management Systems (LMS) in universities around the world.&lt;/p&gt;

&lt;p&gt;The technology that makes this connection possible is called &lt;strong&gt;LTI&lt;/strong&gt; — Learning Tools Interoperability. Think of it as a universal plug that lets any educational tool (like CircuitVerse) slot into any LMS (like Canvas) so that students can log in once, get assignments, submit work, and have their grades flow back automatically — all without leaving their course page.&lt;/p&gt;

&lt;p&gt;There are two versions of this plug: &lt;strong&gt;LTI 1.1&lt;/strong&gt;, which is old and uses a simpler (but outdated) security mechanism, and &lt;strong&gt;LTI 1.3&lt;/strong&gt;, which is newer, more secure, and what Canvas actually recommends today. My job is to bring CircuitVerse fully up to LTI 1.3 standards.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monday–Tuesday: A Pull Request That Taught Me to Read Diffs
&lt;/h2&gt;

&lt;p&gt;I started the week with what I thought was a solid pull request (PR) — a fix for a bug in CircuitVerse's existing LTI 1.1 grade passback feature. "Grade passback" is the process where CircuitVerse sends a student's score back to Canvas after they complete an assignment.&lt;/p&gt;

&lt;p&gt;My PR got reviewed, and the reviewer left two comments that immediately made me wince:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comment 1:&lt;/strong&gt; &lt;em&gt;"It seems that the whole schema is sorted. This issue has been fixed upstream."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Translation: I had accidentally included a massive, unrelated change to a file called &lt;code&gt;schema.rb&lt;/code&gt; — a file that describes the structure of CircuitVerse's database. When I ran a command to regenerate this file on my laptop, a newer version of the Rails framework (the toolkit CircuitVerse is built on) quietly reordered every single database column alphabetically. This produced a diff with hundreds of changed lines — none of which were related to my actual fix. The reviewer spotted it immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comment 2:&lt;/strong&gt; &lt;em&gt;"You are downgrading gem version here."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Translation: I had accidentally rolled back a security upgrade. CircuitVerse uses a library called &lt;code&gt;jwt&lt;/code&gt; (JSON Web Tokens — a way to securely verify identity online) that had recently been upgraded from version 2.x to 3.x. My branch had unknowingly reverted it to the older version because of a chain reaction: another library I'd updated (&lt;code&gt;webpush&lt;/code&gt;, used for browser notifications) happened to be incompatible with jwt 3.x, pulling the version back down without me realising.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; I had to undo the schema changes by restoring the file to exactly what the main CircuitVerse codebase had, restore jwt to version 3.2.0, and also downgrade webpush back to the version that didn't cause the conflict. Three files. Careful surgery with git commands. Then push the corrected version, re-commit, and update the PR.&lt;/p&gt;

&lt;p&gt;This taught me something I'll carry for the rest of the summer: &lt;strong&gt;always check your diff before opening a pull request.&lt;/strong&gt; A diff is the list of everything your code changes compared to the original. If anything looks unfamiliar or unexpected, it probably shouldn't be there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wednesday: The PR I Had to Close
&lt;/h2&gt;

&lt;p&gt;After fixing those issues and updating the pull request, I got some harder news: the PR had to be closed. The grade passback code I'd written depended on an old library (&lt;code&gt;ims-lti&lt;/code&gt;) that used LTI 1.1's way of doing things — OAuth 1.0, an older form of authentication. The problem was that several other files in the project also depended on that same library, and changing it was causing things to break in places I hadn't touched.&lt;/p&gt;

&lt;p&gt;It felt frustrating. But my mentor pointed me in the right direction: before fixing the grade passback flow, I needed to first upgrade the underlying library itself. That meant replacing &lt;code&gt;ims-lti&lt;/code&gt; with something modern that could handle LTI 1.3 properly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wednesday–Thursday: Reviewing Someone Else's Pull Request
&lt;/h2&gt;

&lt;p&gt;While I regrouped, I was asked to review another PR that had been submitted — a Dependabot update (an automated tool that suggests library upgrades) proposing to bump &lt;code&gt;ims-lti&lt;/code&gt; from version 1.2.9 to 2.3.7.&lt;/p&gt;

&lt;p&gt;This was a useful exercise. Looking at what changed between the two versions, I spotted that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The 2.x version had removed support for the OAuth library entirely — the same one that &lt;code&gt;LtiScoreSubmission&lt;/code&gt; (the grade passback code) depended on&lt;/li&gt;
&lt;li&gt;A class called &lt;code&gt;IMS::LTI::ToolProvider&lt;/code&gt;, which is what the controller uses to verify a student's identity when they launch CircuitVerse from Canvas, simply doesn't exist in 2.x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Merging this PR would have silently broken every LTI launch and every grade submission. I wrote up a review explaining this and recommended the PR be held until the LTI code was properly migrated to 1.3. The PR was not merged.&lt;/p&gt;

&lt;p&gt;Reviewing other people's code with a critical eye is a skill in itself, and this was good practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Thursday: Syncing With the Main Codebase (A Surprisingly Tricky Task)
&lt;/h2&gt;

&lt;p&gt;Before writing any new code, I needed to make sure my local copy of CircuitVerse was up to date with the changes the main team had been making. This process is called "fetching upstream" — pulling in changes from the official CircuitVerse repository.&lt;/p&gt;

&lt;p&gt;It should have been simple. It wasn't.&lt;/p&gt;

&lt;p&gt;Git (the tool developers use to track code changes) threw an error about corrupted branch references — essentially, some branch names in the CircuitVerse repository contained spaces in their names, which git wasn't expecting. These bad references were blocking the entire fetch operation.&lt;/p&gt;

&lt;p&gt;The fix involved tracking down these broken references, deleting them, and then fetching again. Once that worked, I had a new problem: a &lt;strong&gt;merge conflict&lt;/strong&gt;. The main codebase had changed the jwt library version (to 3.2.0), while my local copy still had the older pin (2.10.3). Git didn't know which one to keep, so it stopped and asked me to decide.&lt;/p&gt;

&lt;p&gt;I chose the upstream version (3.2.0) — the right call, since that's the one the maintainers had intentionally upgraded to. I also removed a manually-added entry for a library called &lt;code&gt;faraday&lt;/code&gt; (used for making HTTP requests) since it was already included indirectly through other libraries and didn't need to be declared explicitly.&lt;/p&gt;

&lt;p&gt;There was also a leftover reference to a submodule (a separate mini-repository embedded inside CircuitVerse) that I'd already removed in an earlier session. The merge picked it back up; I removed it again. After all of that, the sync was complete.&lt;/p&gt;




&lt;h2&gt;
  
  
  Friday: Finding the Right Gem — A Detective Story
&lt;/h2&gt;

&lt;p&gt;With the codebase up to date, it was time to research the actual replacement for &lt;code&gt;ims-lti&lt;/code&gt;. The library I'd been asked to migrate to is called &lt;strong&gt;&lt;code&gt;lti_advantage&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's where it got interesting. There are &lt;em&gt;two&lt;/em&gt; different things with very similar names:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;lti_advantage&lt;/code&gt; (version 0.0.3)&lt;/strong&gt; — published on RubyGems (the standard place Ruby libraries are shared), authored by an individual developer, with no publicly accessible source code and almost no documentation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;lti-advantage&lt;/code&gt; (version 0.1.0)&lt;/strong&gt; — embedded directly inside Canvas LMS's own codebase, maintained by Instructure (the company that builds Canvas), with full source code, tests, and comments linking to the official LTI standards.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since the entire point of this integration is to work with Canvas, using the library that &lt;em&gt;Canvas itself uses&lt;/em&gt; was clearly the right call. The Canvas version is battle-tested, actively maintained, and built to the exact same LTI 1.3 specification we're implementing.&lt;/p&gt;

&lt;p&gt;I updated the &lt;code&gt;Gemfile&lt;/code&gt; (the file that lists all libraries a Ruby application depends on) to point to Canvas's version of the gem, directly from the Canvas GitHub repository.&lt;/p&gt;




&lt;h2&gt;
  
  
  Friday Evening: The Two-Letter Word That Cost Me an Hour
&lt;/h2&gt;

&lt;p&gt;After setting up the Gemfile, I ran a quick test to verify the library had loaded correctly. Instead of a confirmation, I got a wall of red error text.&lt;/p&gt;

&lt;p&gt;The culprit? The word &lt;strong&gt;&lt;code&gt;it&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In Ruby 3.4 (released late 2023), &lt;code&gt;it&lt;/code&gt; was introduced as a new shorthand inside code blocks — a way to refer to the current item being processed without naming it explicitly. The Canvas team had adopted this syntax in their codebase.&lt;/p&gt;

&lt;p&gt;But CircuitVerse runs on Ruby 3.2.2. In that version, &lt;code&gt;it&lt;/code&gt; isn't a special keyword — it's just an undefined method name. So when the library tried to load, Ruby 3.2.2 hit the word &lt;code&gt;it&lt;/code&gt; and said: &lt;em&gt;"I have no idea what this is."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The solution was to find the specific point in Canvas's git history just before this syntax was introduced — a commit from March 2025, before the Canvas team upgraded their Rubocop linting tool to support Ruby 3.4 style. By pinning our Gemfile to that specific historical commit, we get the full functionality of the library without the Ruby version incompatibility.&lt;/p&gt;

&lt;p&gt;After making that change, the verification check finally printed what I needed to see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.1.0
LtiAdvantage::Messages::ResourceLinkRequest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library was in, working, and ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up: 793 Tests, 0 Failures
&lt;/h2&gt;

&lt;p&gt;With the new library loaded, the last step was making sure I hadn't accidentally broken anything else in the process. I ran CircuitVerse's full test suite — 793 individual automated tests covering models, controllers, API endpoints, background jobs, and more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;793 examples, 0 failures.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every part of the existing application still worked exactly as before. The only test excluded from this run was the one specifically for LTI — which I already knew would fail, because it still references the old library. Rewriting that test is part of next week's work.&lt;/p&gt;

&lt;p&gt;I created a clean pull request with a single, focused commit describing exactly what changed and why, linked it to the issue (#7509) I'd opened for the work, and submitted it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned This Week
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read your diffs.&lt;/strong&gt; Every line in a pull request should be there intentionally. Unexpected changes are usually a sign something went wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Library upgrades are never just library upgrades.&lt;/strong&gt; Every dependency has dependencies of its own, and changing one can quietly shift others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewing code is as valuable as writing it.&lt;/strong&gt; Catching a breaking change before it merges is just as important as shipping a feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version numbers matter — down to the commit.&lt;/strong&gt; Pinning a library to a specific historical commit felt overly cautious at first, but it was the only correct solution given the Ruby version gap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A clean pull request with one clear purpose is worth far more than a large one with many.&lt;/strong&gt; My first PR of the week was rejected partly because it accidentally carried in unrelated changes. The PR at the end of the week had exactly one commit, one purpose, and zero noise.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Next week: I start the actual code migration — replacing the old LTI 1.1 authentication logic with the new &lt;code&gt;lti-advantage&lt;/code&gt; library's APIs, rewriting the tests, and wiring up the full OIDC launch flow that Canvas LTI 1.3 requires.&lt;/p&gt;

&lt;p&gt;The foundation is set. Time to build.&lt;/p&gt;

</description>
      <category>devjournal</category>
      <category>git</category>
      <category>opensource</category>
      <category>ruby</category>
    </item>
    <item>
      <title>GSoC 2026 · Week 1 — What Happens When a Student Clicks "Open Assignment"?</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 06 Jun 2026 17:46:44 +0000</pubDate>
      <link>https://dev.to/magic-peach/gsoc-2026-week-1-what-happens-when-a-student-clicks-open-assignment-1jk4</link>
      <guid>https://dev.to/magic-peach/gsoc-2026-week-1-what-happens-when-a-student-clicks-open-assignment-1jk4</guid>
      <description>&lt;h3&gt;
  
  
  Testing the LMS–LTI integration in CircuitVerse
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A behind-the-scenes look at debugging a silent integration — and what it taught me about the gap between code that exists and code that works.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; CircuitVerse · &lt;strong&gt;Week:&lt;/strong&gt; 1 · &lt;strong&gt;Focus:&lt;/strong&gt; Testing &amp;amp; fixing the LMS–LTI assignment workflow&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In my first week, I tested the full LMS–LTI flow end to end — a student opening an assignment from Canvas, building a circuit in CircuitVerse, and a teacher grading it back in Canvas. On the surface the feature looked complete. In reality, &lt;strong&gt;four interconnected bugs&lt;/strong&gt; were silently breaking the handoff. Here's how I found and fixed each one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Student hit a login page&lt;/td&gt;
&lt;td&gt;Session was never established after launch&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;sign_in(@user)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Student landed on the wrong page&lt;/td&gt;
&lt;td&gt;Redirect pointed at the group home, not the project&lt;/td&gt;
&lt;td&gt;Redirect to &lt;code&gt;user_project_path&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Teacher couldn't grade&lt;/td&gt;
&lt;td&gt;Grading form hidden until deadline passes&lt;/td&gt;
&lt;td&gt;Allow past deadlines for testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Grade showed &lt;code&gt;0&lt;/code&gt; in Canvas&lt;/td&gt;
&lt;td&gt;Feature flag off, missing LTI session, &lt;code&gt;maxPoints = 0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Enable flag, launch via Canvas, set max points&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  First, some context: what is LMS–LTI?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;LMS&lt;/strong&gt; stands for &lt;strong&gt;Learning Management System&lt;/strong&gt; — the software your university uses to manage courses, post assignments, and track grades. Canvas, Moodle, Google Classroom, and Blackboard are all examples.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LTI&lt;/strong&gt; stands for &lt;strong&gt;Learning Tools Interoperability&lt;/strong&gt; — an industry standard that lets an LMS talk to an external tool like CircuitVerse, so students don't need separate accounts or a second login. In theory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A student clicks a link inside Canvas and is transported directly into CircuitVerse, identity already known.&lt;/li&gt;
&lt;li&gt;The teacher grades the work from Canvas.&lt;/li&gt;
&lt;li&gt;The score flows back automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;In practice, making that seamless handoff actually work is where things get interesting.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Setting up the test
&lt;/h2&gt;

&lt;p&gt;Before any real testing could happen, I needed two systems running side by side and talking to each other. So I set up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A local &lt;strong&gt;Canvas&lt;/strong&gt; instance and a local &lt;strong&gt;CircuitVerse&lt;/strong&gt; instance.&lt;/li&gt;
&lt;li&gt;A test course in Canvas, with a &lt;strong&gt;test student&lt;/strong&gt; and a &lt;strong&gt;test teacher&lt;/strong&gt; enrolled.&lt;/li&gt;
&lt;li&gt;An LTI assignment configured to point at CircuitVerse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With everything in place, I logged in as the student and clicked the assignment link.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 1: the student ended up on the login page
&lt;/h2&gt;

&lt;p&gt;The first screenshot told the whole story. Instead of landing in CircuitVerse on the assignment, the student was redirected to &lt;code&gt;/users/sign_in&lt;/code&gt; — CircuitVerse's standard login page.&lt;/p&gt;

&lt;p&gt;The LTI handoff itself had worked. Canvas had sent all the right information to CircuitVerse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who&lt;/strong&gt; the student was&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which course&lt;/strong&gt; they were in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which assignment&lt;/strong&gt; they were opening&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CircuitVerse read that information, found the matching user, and even created a blank project for the student to fill in. But it never told CircuitVerse's authentication system that the student was now logged in.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Think of it like a hotel. The front desk checked your reservation, confirmed your identity, and handed you a room key — but forgot to actually activate the key. Every door you try still says "access denied."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix was a single line in the code that handles student LTI launches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;sign_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding this before showing the student their assignment established the session properly. After this fix, the student landed &lt;strong&gt;authenticated and ready&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 2: the student landed on the wrong page
&lt;/h2&gt;

&lt;p&gt;With authentication fixed, the student was now signed in — but being sent to a course overview page with no obvious path to their actual assignment. The intermediate "please wait" page CircuitVerse shows during the handoff was opening a new browser tab pointing at the &lt;strong&gt;group's home page&lt;/strong&gt;, not the student's specific work.&lt;/p&gt;

&lt;p&gt;The fix was conceptually simple: redirect the student straight to the project that was auto-created for them during the launch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before: took the student to the course home page&lt;/span&gt;
&lt;span class="n"&gt;group_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After: takes the student straight to their project&lt;/span&gt;
&lt;span class="n"&gt;user_project_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="vi"&gt;@project&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when the student's browser opens the new tab, they land on their own project page with a clear &lt;strong&gt;"Launch Simulator"&lt;/strong&gt; button waiting for them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 3: the teacher could not grade
&lt;/h2&gt;

&lt;p&gt;With the student flow working, I switched to the teacher's perspective. The teacher launches the same LTI assignment from Canvas, lands on the assignment management page, and can see the student's submission — the circuit they built — in a preview panel.&lt;/p&gt;

&lt;p&gt;But there was &lt;strong&gt;no grading form&lt;/strong&gt;. Nowhere to enter a score.&lt;/p&gt;

&lt;p&gt;This one required reading the code rather than guessing. The grading form is deliberately hidden until the assignment's deadline has passed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;can_be_graded?&lt;/span&gt;
  &lt;span class="n"&gt;mentor_access?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;assignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;graded?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;negative?&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last condition — &lt;code&gt;negative?&lt;/code&gt; — means the deadline must be in the past. My test assignment had a deadline set a week in the &lt;strong&gt;future&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The logic makes sense for real classrooms: you don't want teachers accidentally grading before students have finished submitting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There was also a secondary problem: the date picker on the assignment edit form had a &lt;code&gt;minDate&lt;/code&gt; restriction that prevented selecting any past date — so even if you knew the workaround, the UI would reset your choice.&lt;/p&gt;

&lt;p&gt;Both restrictions were removed for the editing flow, allowing a past deadline for testing. Once the deadline was in the past, the grading form &lt;strong&gt;appeared immediately&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 4: the grade did not appear in Canvas
&lt;/h2&gt;

&lt;p&gt;This was the most layered problem of the four, and the most instructive.&lt;/p&gt;

&lt;p&gt;After the teacher submitted a grade of &lt;strong&gt;68&lt;/strong&gt; in CircuitVerse, I went to the Canvas gradebook expecting to see 68. Instead, it showed &lt;strong&gt;0&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A grade of zero when you entered 68 is never a simple problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I pulled the Rails server logs to see exactly what happened on submission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST "/grades"
Parameters: {"grade"=&amp;gt;{"project_id"=&amp;gt;"15", "assignment_id"=&amp;gt;"19",
             "grade"=&amp;gt;"68", "remarks"=&amp;gt;"average"}}
...
Completed 200 OK
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The grade was saved successfully in CircuitVerse — but there was &lt;strong&gt;no outgoing HTTP request to Canvas&lt;/strong&gt; anywhere in the log. The code that should have sent the grade back was never reached.&lt;/p&gt;

&lt;p&gt;Three conditions had to be true &lt;em&gt;simultaneously&lt;/em&gt; for the grade passback to fire:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A feature flag called &lt;code&gt;lms_integration&lt;/code&gt; had to be enabled.&lt;/strong&gt; CircuitVerse uses feature flags to toggle capabilities without deploying new code. This one was off by default. Running &lt;code&gt;Flipper.enable(:lms_integration)&lt;/code&gt; in the server console switched it on.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The teacher had to have launched via Canvas LTI before grading.&lt;/strong&gt; The passback code checks &lt;code&gt;session[:is_lti]&lt;/code&gt; — a marker that says "this session started from an LTI launch." Logging into CircuitVerse directly never set it. The fix: always initiate grading from Canvas by clicking &lt;strong&gt;"Load LTI test in a new window."&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Canvas assignment had zero maximum points.&lt;/strong&gt; LTI sends scores as a decimal between 0 and 1 — so 68 out of 100 becomes &lt;code&gt;0.68&lt;/code&gt;. Canvas multiplies that by the assignment's max points:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;With max points &lt;code&gt;0&lt;/code&gt; → &lt;code&gt;0.68 × 0 = 0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;With max points &lt;code&gt;100&lt;/code&gt; → &lt;code&gt;0.68 × 100 = 68&lt;/code&gt; ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After resolving all three, the Canvas gradebook finally showed &lt;strong&gt;68 out of 100&lt;/strong&gt; for the test student.&lt;/p&gt;




&lt;h2&gt;
  
  
  What made this hard
&lt;/h2&gt;

&lt;p&gt;None of these bugs announced themselves loudly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server returned &lt;strong&gt;success codes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Pages &lt;strong&gt;loaded&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No errors&lt;/strong&gt; appeared on screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The student reached CircuitVerse — just not authenticated. The grade was saved — just not forwarded. The grading form existed in the code — just not rendered.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is the particular difficulty of integration testing: two systems each behaving correctly on their own, but the handoff between them silently failing. The only way to catch it is to walk through the entire experience from beginning to end, as an actual user would.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The lesson is simple but easy to forget:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A feature is not done when the code is written. It is done when a real person can use it from start to finish without needing to know what is happening underneath.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The moment it worked
&lt;/h2&gt;

&lt;p&gt;After all of the above, I went back to Canvas, clicked the LTI assignment as the teacher, entered a grade of 68, and reloaded the gradebook.&lt;/p&gt;

&lt;p&gt;It showed &lt;strong&gt;68 out of 100&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That number, small and unremarkable on its own, represented a complete round trip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A student identity handed from Canvas to CircuitVerse&lt;/li&gt;
&lt;li&gt;A circuit built and saved&lt;/li&gt;
&lt;li&gt;A grade entered by a teacher&lt;/li&gt;
&lt;li&gt;A score carried back to where the course lives&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The workflow works. Now the real work begins — &lt;strong&gt;upgrading it from LTI 1.1 to the more secure and capable LTI 1.3.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading my Week 1 update. More to come as the LTI 1.3 migration gets underway.&lt;/em&gt; 🌸&lt;/p&gt;

</description>
      <category>programming</category>
      <category>opensource</category>
      <category>career</category>
      <category>github</category>
    </item>
  </channel>
</rss>
