Code and technology abstract background
2026-07-20·Jordan

v2.8.10: Share Link Revocation, Per-Assessment Upload Limits & Marking Cache Integrity

A patch focused on security, data integrity, and production hardening. Parent share links are now persisted with per-link revocation, server-side upload limits prevent assessment upload-slot abuse, marking results no longer overwrite user_id across students, and spaced-repetition cards are scoped by subject/assessment/topic to prevent identity collisions.

Share:X (Twitter)

v2.8.10 is a targeted patch focused on security, data integrity, and production hardening. Here is what changed:

Parent Share Link Revocation

Parent share links are now fully revocable. When you create a share token, it is persisted in a new share_links table with an active flag and a 90-day expiry (corrected from the previous 30-day hardcode that claimed 90 days in the UI). The settings page now lists all your active links with Copy and Revoke (Trash2) buttons — click Revoke on any link and it stops working immediately, regardless of its original expiry date.

Two new server functions back this: revokeParentShareToken (authenticates the user, verifies ownership, and sets active = false) and listParentShareTokens (returns active/unexpired tokens for the current user). The read path in fetchParentViewData now calls checkTokenRevoked() via DB lookup by token_hash after crypto verification, so revoked or expired links return INVALID_TOKEN regardless of HMAC validity.

Per-Assessment Upload Limits (Server-Side)

The per-assessment upload limit was previously only enforced client-side (pre-upload button logic). A student could bypass the UI and insert unlimited materials directly via a Supabase client call. Now server-side enforcement is handled by a new uploadMaterial server function in materials.functions.ts that authenticates the user, looks up their tier, calls checkAssessmentUploadLimit(), and only then inserts into materials. Max tier (Infinity) shortcuts to { ok: true }. An in-memory cache avoids redundant DB queries within the same request.

7 unit tests in materials.test.ts cover Free tier (allow under 1/2, reject at 2/2, reject over 3/2), Pro tier (allow under 4/5, reject at 5/5), Max tier (always allow), and per-assessment scoping.

Roadmap Generation Quota Enforcement

generateRoadmap now checks the monthly roadmap quota upfront via checkAllQuotas before any expensive DB queries or AI scheduling work. If the quota is exceeded, it returns QUOTA_EXCEEDED with used/limit info. The inner routeAIRequest call now passes skipQuotaCheck: true to prevent double-counting. The frontend in roadmap.tsx and subjects.$subjectId.assessments.$assessmentId.tsx shows a friendly message with usage info and an upgrade hint.

Unit tests confirm Free is blocked on the 2nd, Pro on the 9th, and Max on the 31st monthly generation.

Marking Results Cache - user_id Integrity

This was a subtle but impactful data corruption bug. markAnswer used upsert({ ... }, { onConflict: "cache_key" }) on marking_results (which had cache_key UNIQUE), so two students submitting the same normalized answer to the same question produced the same cache_key — the second student's upsert overwrote the first student's user_id. The RLS SELECT policy (auth.uid() = user_id) then blocked the first student from reading any cached result.

Two changes fix this: (1) The UNIQUE constraint on cache_key was dropped, replaced with a unique index on (user_id, cache_key) (partial, non-NULL user_id only), and the SELECT policy changed to USING (true) so the cache is content-addressable regardless of which student created the row. (2) questionId formats in scan-to-mark and practice paper marking were updated to enable cross-student cache sharing — scan-mark.functions.ts changed from scan_{userId}_{questionHash} to scan_{questionHash}, and generate.functions.ts changed from paper_{sessionId}_q{absIdx} to a content-based hash paper_q_{questionHash} derived from subjectId, question text, marks, and sample answer.

Spaced Repetition Context Scoping

stableCardId previously hashed only the question text, so identical questions in different topics produced the same card_id and overwrote each other via the UNIQUE(card_id, user_id) constraint. Now an optional context parameter (subjectId, assessmentId, topic) is mixed into the hash, so two cards with the same question but different contexts produce different card_ids and are stored/retrieved independently.

The review module in review.tsx now tries three matching strategies in order: scoped hash (new cards), unscoped hash (legacy cards), then topic label (last resort). A migration adds subject_id and assessment_id columns to card_reviews. 11 unit tests confirm distinct IDs, deterministic IDs for identical context, backward compatibility, and schedule independence.

Additional Improvements

Email verification & password reset fixed. Both flows were calling the auth-actions edge function directly from the browser without Authorization headers. Since the edge function requires the service role key as a Bearer token, every call returned 401. Server-side proxy functions in auth-functions.ts now handle these operations using the admin Supabase client, with end-to-end tests for both flows.

Friend request duplicate prevention. A database trigger now rejects duplicate active friend requests between the same two users. A client-side check prevents self-requests. Composite indexes ensure fast duplicate detection.

Atomic abuse checks. checkDuplicate and applyCooldown now use setnx (atomic set-if-not-exists) instead of separate get+set operations, preventing race conditions where concurrent requests could both pass the cooldown check.

Chunk recovery re-reload guard. The single reload boolean was replaced with a sessionStorage-based sliding window (max 3 reloads per 60 seconds). When exhausted, a minimal error page is shown instead of infinitely reloading.

Push notification filter fix. The send-push-notification edge function was filtering with push_preferences->>type (literal 'type' key) instead of the dynamic type value. Fixed to use the correct preference type.

Load manager cleanup. The unused in-memory request queue (~90 lines of dead code) was removed. Tier-based delay is now applied directly in the router.

This release closes several data integrity gaps, strengthens security boundaries, and improves production reliability — keeping Claritii's infrastructure solid as the platform grows.

We use cookies to improve your experience and analyze usage. Learn more