T7: fixture recorder + fixes from live verification #16

Merged
dzenda merged 59 commits from t7-fixtures into t17-gitlab-connect 2026-11-14 17:50:05 +01:00
Owner

T7: fixture recorder + fixes from live verification

Stacked on #15 (t17-gitlab-connect) → … → #1. T7 was blocked until the first browser login, which happened today. It sits on top of the stack so it can fix the providers built since.

First real login: results

  • ✅ FIT OAuth via Better Auth works end to end in the browser ("Signed in as …").
  • ✅ We get a refresh token, and the access token lifetime is 120 minutes. All 5 requested scopes are granted, without course-restricted.
  • ✅ accounts has 0 rows with tokens. Tokens exist only encrypted in connections, and users.username is set.
  • ⚠️ Scopes ≠ resources. The token aud lists the services the OAuth app is authorized for (usermap-api-…, sirius-…, marast-…). Grades is missing, so it returns 403 access_denied: token does not contain resource id (grades-…). Grades has to be enabled for the app in auth.fit.cvut.cz/manager. That's now in "Needs you" and documented in docs/fit-auth.md.

bun run fixtures:record (scripts/record-fixtures.ts, scripts/scrub.ts)

  • Reads the dev user's encrypted FIT token from local D1 and decrypts it with .dev.vars. It makes GET requests only and never refreshes: refresh tokens may rotate and the app owns them, so it aborts if the token has expired.
  • Records Usermap, Sirius (events, exams, person, plus an iCal check), Grades and Marast (users/me, courses, quizzes, open and future), plus check_token.
  • Scrubs PII: usernames, names, emails, phones, rooms, personal number, kosPersonId, notes and descriptions, teacher/student lists, role org units, and every secret, including the Sirius calendar token. Aliases stay consistent, and structure, types, dates and course codes are kept.
  • Output goes to test/fixtures/live/, which is git-ignored. Even scrubbed recordings of real grades shouldn't be committed without human review. The script prints only statuses and shapes.
  • Audit after recording: 0 occurrences of the token, username, email or surname, and no real e-mail addresses.

Fixes found by live verification

Area Finding Fix
Marast Bearer → 401. The FIT token goes in the Api-Key header (marked as API key in the Swagger doc). fitGetJson(..., { auth: "api-key" }). Redirects still not followed.
Marast only_open/only_future work with Api-Key (26 + 9 of 52, matching our computed statuses). Kept server-side filtering for upcoming. Unfiltered lists report truncated, which appears as a marast:truncated warning in fit.today/fit.deadlines. Allowlisted the real is_important/is_tracked/name_en fields.
Usermap Fields can be null (preferredEmail: null), so name search failed with "unexpected response". .nullish() schemas (provider and login profile), plus a regression test with the live shape.
Sirius linked.teachers is a list of strings. Course names are { cs } only. Already tolerated by the defensive parser. iCal URL format confirmed (200 text/calendar).
Grades 403 because the app lacks the resource. Error message now names the real cause and the fix.

Live run of our registry functions (counts only)

fit.me ✔ · fit.people.lookup ✔ (10) · fit.sirius.events ✔ (10 events, course and teacher names resolved) · fit.sirius.icalUrl ✔ · fit.marast.courses ✔ (3) · fit.marast.assignments ✔ (35 upcoming / 52 total) · fit.today ✔ · fit.deadlines ✔ (13 deadlines: Sirius assessments + Marast quizzes, no warnings) · fit.grades.* ✘ until the resource is enabled.

Verified

  • bun run lint && bun run typecheck && bun run test: 212 tests.
  • docs/apis.md and docs/fit-auth.md record everything above.

Astra review (gpt-6-astra), 2 rounds

Fixed:

  1. (P1) The recorder's check_token request followed redirects, and the token is in its body → redirect: "manual".
  2. (P2) kosPersonId wasn't scrubbed → added.
  3. (P2) The only_open probe used Bearer, so my "filter returns 401" conclusion was wrong → re-probed with Api-Key, the filters work, and server-side filtering is restored.
  4. (P2) Local filtering of an unfiltered list could silently lose quizzes past the page cap → server filters plus a truncated flag and warnings.

Round 2: "No remaining concrete defects found."

🤖 Generated with Claude Code

## T7: fixture recorder + fixes from live verification **Stacked on #15 (`t17-gitlab-connect`) → … → #1.** T7 was blocked until the first browser login, which happened today. It sits on top of the stack so it can fix the providers built since. ### First real login: results - ✅ FIT OAuth via Better Auth works end to end in the browser ("Signed in as …"). - ✅ We get a **refresh token**, and the access token lifetime is **120 minutes**. All 5 requested scopes are granted, without `course-restricted`. - ✅ `accounts` has 0 rows with tokens. Tokens exist only encrypted in `connections`, and `users.username` is set. - ⚠️ **Scopes ≠ resources.** The token `aud` lists the services the OAuth app is authorized for (`usermap-api-…`, `sirius-…`, `marast-…`). **Grades is missing**, so it returns `403 access_denied: token does not contain resource id (grades-…)`. Grades has to be enabled for the app in `auth.fit.cvut.cz/manager`. That's now in "Needs you" and documented in `docs/fit-auth.md`. ### `bun run fixtures:record` (`scripts/record-fixtures.ts`, `scripts/scrub.ts`) - Reads the dev user's encrypted FIT token from **local** D1 and decrypts it with `.dev.vars`. It makes **GET requests only** and **never refreshes**: refresh tokens may rotate and the app owns them, so it aborts if the token has expired. - Records Usermap, Sirius (events, exams, person, plus an iCal check), Grades and Marast (users/me, courses, quizzes, open and future), plus `check_token`. - **Scrubs PII:** usernames, names, emails, phones, rooms, personal number, `kosPersonId`, notes and descriptions, teacher/student lists, role org units, and every secret, including the Sirius calendar token. Aliases stay consistent, and structure, types, dates and course codes are kept. - Output goes to **`test/fixtures/live/`, which is git-ignored**. Even scrubbed recordings of real grades shouldn't be committed without human review. The script prints only statuses and shapes. - Audit after recording: 0 occurrences of the token, username, email or surname, and no real e-mail addresses. ### Fixes found by live verification | Area | Finding | Fix | |---|---|---| | **Marast** | Bearer → 401. The FIT token goes in the **`Api-Key` header** (marked as API key in the Swagger doc). | `fitGetJson(..., { auth: "api-key" })`. Redirects still not followed. | | Marast | `only_open`/`only_future` work with Api-Key (26 + 9 of 52, matching our computed statuses). | Kept server-side filtering for `upcoming`. Unfiltered lists report **`truncated`**, which appears as a `marast:truncated` warning in `fit.today`/`fit.deadlines`. Allowlisted the real `is_important`/`is_tracked`/`name_en` fields. | | **Usermap** | Fields can be `null` (`preferredEmail: null`), so name search failed with "unexpected response". | `.nullish()` schemas (provider and login profile), plus a regression test with the live shape. | | Sirius | `linked.teachers` is a list of strings. Course names are `{ cs }` only. | Already tolerated by the defensive parser. **iCal URL format confirmed** (`200 text/calendar`). | | Grades | 403 because the app lacks the resource. | Error message now names the real cause and the fix. | ### Live run of our registry functions (counts only) `fit.me` ✔ · `fit.people.lookup` ✔ (10) · `fit.sirius.events` ✔ (10 events, course and teacher names resolved) · `fit.sirius.icalUrl` ✔ · `fit.marast.courses` ✔ (3) · `fit.marast.assignments` ✔ (35 upcoming / 52 total) · `fit.today` ✔ · **`fit.deadlines` ✔ (13 deadlines: Sirius assessments + Marast quizzes, no warnings)** · `fit.grades.*` ✘ until the resource is enabled. ### Verified - `bun run lint && bun run typecheck && bun run test`: 212 tests. - `docs/apis.md` and `docs/fit-auth.md` record everything above. ### Astra review (gpt-6-astra), 2 rounds **Fixed:** 1. *(P1)* The recorder's `check_token` request followed redirects, and the token is in its body → `redirect: "manual"`. 2. *(P2)* `kosPersonId` wasn't scrubbed → added. 3. *(P2)* The `only_open` probe used Bearer, so my "filter returns 401" conclusion was **wrong** → re-probed with Api-Key, the filters work, and server-side filtering is restored. 4. *(P2)* Local filtering of an unfiltered list could silently lose quizzes past the page cap → server filters plus a `truncated` flag and warnings. Round 2: "No remaining concrete defects found." 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- fixtures:record: reads the encrypted token from local D1, GET-only, never refreshes,
  scrubs PII into git-ignored test/fixtures/live/, prints shapes only
- Marast: FIT token goes in Api-Key (Bearer is 401); only_open is 401 → filter locally
- Usermap: fields may be null (preferredEmail) → nullish schemas
- Grades: precise error for missing OAuth app resource; docs on scopes vs. resources

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also fixes a production bug found while smoke-testing: createFnContext stored the global
fetch on the context object, so ctx.fetch(...) threw "Illegal invocation" in workerd. That
broke every registry call in the Worker (including all MCP tool calls); Node tests could
not see it. Regression test emulates workerd's this-check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ALLOWED_USERS enforced for MCP too (shared policy); excluded users' grants revoked on use
- telemetry: streaming 512-byte cap before parsing
- POST /oauth/register rate-limited per IP and instance-wide (Workers rate-limit bindings)
- FIT API bodies capped at 2 MB (streaming); Grades tree walk iterative and bounded
- readBounded moved to src/lib/bounded.ts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Logos: FIT service marks vendored in public/services (see README there); assistant marks
from @lobehub/icons (MIT), simple-icons (CC0) and devicon (MIT).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- service_settings table (migration 0002); MCP calls to a switched-off service fail with
  service_disabled, aggregates skip it with a warning, fit.status lists it
- setServiceEnabledFn: same-origin + session only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- fit.sirius.courseEvents: every parallel of a course (room, teachers, capacity/occupied),
  shared paging helper with fit.sirius.events; respects the Sirius MCP switch
- getCourseWeek server fn for the calendar overlay; upcoming deadlines (60 days) beside the week

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- layoutLanes/pragueMinutes in src/lib/calendar.ts (tested)
- shared EventDetails popover; agenda list stays on mobile
- agentSetupPrompt in src/mcp/setup.ts (tested)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Referrer-Policy: same-origin (no-referrer made browsers send Origin: null on our form POSTs,
  so every management form was rejected with 403); shared isSameOriginRequest accepts
  Origin: null only with Sec-Fetch-Site: same-origin
- No COOP on OAuth/consent/login routes (popup-based web MCP clients keep window.opener)
- fit.sirius.icalUrl is websiteOnly: excluded from the MCP registry (URL embeds a credential)
- docs: telemetry wording (operator-enabled, consent advice), CSRF/COOP/credential notes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed-on: #25
Reviewed-on: #24
Reviewed-on: #23
Reviewed-on: #22
Reviewed-on: #21
Reviewed-on: #20
Reviewed-on: #19
Reviewed-on: #18
Reviewed-on: #17
dzenda merged commit 67b1b9b07b into t17-gitlab-connect 2026-11-14 17:50:05 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dzenda/os!16
No description provided.