T10: Sirius provider (events, iCal URL) #9

Merged
dzenda merged 85 commits from t10-sirius into t9-usermap 2026-11-14 17:50:32 +01:00
Owner

T10: Sirius provider (fit.sirius.events, fit.sirius.icalUrl)

Stacked on #8 (t9-usermap) → … → #1. Merge those first.

What

  • src/providers/sirius.ts, built against the Sirius API v1 docs:
    • fit.sirius.events: the personal timetable via GET /people/{username}/events.
      • Input: from/to (default today + 7 days, Europe/Prague), an optional type (lecture, tutorial, laboratory, exam, assessment, course_event) and includeCancelled.
      • Output per event: course, course name, room, teachers (with names from include=courses,teachers), parallel, sequence number, original when a schedule exception moved it, and capacity/occupied.
      • Paginates 100 per page, up to 500 events, with a range of at most 120 days. truncated is accurate.
      • The upstream to semantics aren't documented, so it requests one extra day and filters by Prague-local start date. That's correct either way.
    • fit.sirius.icalUrl: the ICS subscription URL …/people/{username}/events.ical?access_token=<Sirius calendar token>. The token comes from GET /people/{username}.
  • Privacy: links.students (other students' usernames) is never parsed or returned.
  • Calendar token decision: the iCal URL contains Sirius's calendar-only token. It is not the FIT OAuth token, which never leaves the server. SPEC §6 explicitly lists fit.sirius.icalUrl. The function docs and the output note tell the assistant to treat it as a private link. Astra agrees this is intentional and acceptable.
  • src/lib/dates.ts: pragueDate, addDays, daysBetween, with DST and midnight tests.
  • Event datetimes are validated as ISO with offset. The linked section isn't documented, so it's parsed defensively: arrays or id-keyed maps are accepted, anything else is ignored.
  • Range errors return invalid_arguments before any upstream call. 404 ("no events in scope") returns an empty list.

Verified

  • bun run lint && bun run typecheck && bun run test: 143 tests, 18 of them for Sirius and dates. They cover the default range and query params, mapping, sorting, hiding cancelled events, no student data, filters, filtering out the extra day, pagination and truncation (including exactly 500, and extra-day records), 404, odd linked, range validation, username validation, the iCal URL, unexpected shapes, and Czech and English search.
  • bun run dev + curl /api/functions lists both functions.
  • Not yet run against a real token (T7). To verify then: the linked shape, to semantics and the .ical URL format.

Astra review (gpt-6-astra)

Fixed: (P2) truncated was true for exactly 500 events, and the extra-day records could inflate it. Now it fetches up to 6 pages and sets truncated only when more than 500 events matched or pages are left unfetched. Tests added for exactly 500 (with and without meta.count) and for extra-day overflow.

Confirmed: returning the Sirius calendar token in the private iCal URL is intentional and doesn't expose FIT OAuth credentials.

🤖 Generated with Claude Code

## T10: Sirius provider (`fit.sirius.events`, `fit.sirius.icalUrl`) **Stacked on #8 (`t9-usermap`) → … → #1.** Merge those first. ### What - `src/providers/sirius.ts`, built against the [Sirius API v1 docs](https://cvut.github.io/sirius/docs/api-v1.html): - **`fit.sirius.events`:** the personal timetable via `GET /people/{username}/events`. - Input: `from`/`to` (default today + 7 days, Europe/Prague), an optional `type` (lecture, tutorial, laboratory, exam, assessment, course_event) and `includeCancelled`. - Output per event: course, course name, room, teachers (with names from `include=courses,teachers`), parallel, sequence number, `original` when a schedule exception moved it, and capacity/occupied. - Paginates 100 per page, up to 500 events, with a range of at most 120 days. `truncated` is accurate. - The upstream `to` semantics aren't documented, so it requests one extra day and filters by Prague-local start date. That's correct either way. - **`fit.sirius.icalUrl`:** the ICS subscription URL `…/people/{username}/events.ical?access_token=<Sirius calendar token>`. The token comes from `GET /people/{username}`. - **Privacy:** `links.students` (other students' usernames) is never parsed or returned. - **Calendar token decision:** the iCal URL contains Sirius's calendar-only token. It is *not* the FIT OAuth token, which never leaves the server. SPEC §6 explicitly lists `fit.sirius.icalUrl`. The function docs and the output `note` tell the assistant to treat it as a private link. Astra agrees this is intentional and acceptable. - `src/lib/dates.ts`: `pragueDate`, `addDays`, `daysBetween`, with DST and midnight tests. - Event datetimes are validated as ISO with offset. The `linked` section isn't documented, so it's parsed defensively: arrays or id-keyed maps are accepted, anything else is ignored. - Range errors return `invalid_arguments` before any upstream call. 404 ("no events in scope") returns an empty list. ### Verified - `bun run lint && bun run typecheck && bun run test`: 143 tests, 18 of them for Sirius and dates. They cover the default range and query params, mapping, sorting, hiding cancelled events, no student data, filters, filtering out the extra day, pagination and truncation (including exactly 500, and extra-day records), 404, odd `linked`, range validation, username validation, the iCal URL, unexpected shapes, and Czech and English search. - `bun run dev` + `curl /api/functions` lists both functions. - Not yet run against a real token (T7). To verify then: the `linked` shape, `to` semantics and the `.ical` URL format. ### Astra review (gpt-6-astra) **Fixed:** *(P2)* `truncated` was true for exactly 500 events, and the extra-day records could inflate it. Now it fetches up to 6 pages and sets `truncated` only when more than 500 events matched or pages are left unfetched. Tests added for exactly 500 (with and without `meta.count`) and for extra-day overflow. **Confirmed:** returning the Sirius calendar token in the private iCal URL is intentional and doesn't expose FIT OAuth credentials. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
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>
- 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
Reviewed-on: #16
Reviewed-on: #15
Reviewed-on: #14
Reviewed-on: #13
Reviewed-on: #12
Reviewed-on: #11
Reviewed-on: #10
dzenda merged commit 277d23389a into t9-usermap 2026-11-14 17:50:32 +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!9
No description provided.