Skip to content

Guest identity + upload size limit for everyone (1/3) - #14

Merged
DHYEY166 merged 3 commits into
mainfrom
feature/guest-identity
Oct 2, 2026
Merged

DHYEY166 merged 3 commits into
mainfrom
feature/guest-identity

Conversation

@DHYEY166

@DHYEY166 DHYEY166 commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Stack: 1 of 3. This PR targets main. #15 (guest limits) is stacked on this branch, and #16 (guest demo) is stacked on PR2. Merge them in order.

What

  • Upload size limit for everyone. /api/upload now returns a presigned POST instead of a PUT. The policy pins the key and content type and includes content-length-range: 100 MB by default (MAX_UPLOAD_MB), 2 MB for guests. The route answers 413 when the declared size is over the limit, and GET /api/upload returns the caller's limit so the form can refuse a large file before it creates a pipeline. The profiler checks ContentLength before reading the object and fails the run with "File is X; the upload limit is Y per file."
  • Guest identity. src/lib/guest.ts issues and verifies an httpOnly cs_guest cookie of the form guest_<22>.<exp>.<HMAC-SHA256>, valid for 24 h. It uses Web Crypto only, so it is edge-safe. auth() falls back to the guest id only when no Clerk user is signed in, and the guest id becomes the team_id. The middleware lets a valid guest cookie through on protected routes.
  • /api/guest. POST creates a session, GET returns its status, DELETE ends it. Each session is recorded in guest_sessions (migration 003_guest_sessions.sql) with an HMAC of the IP. Caps are 5 per IP and 200 overall per 24 h, enforced in a single INSERT ... SELECT. A Turnstile check runs when TURNSTILE_SECRET_KEY is set.
  • Env. New GUEST_COOKIE_SECRET (≥ 32 chars; guest access is off while it is unset), TURNSTILE_SECRET_KEY / NEXT_PUBLIC_TURNSTILE_SITE_KEY (optional), and MAX_UPLOAD_MB, all in src/lib/env.ts and .env.example.

Why

Guests need an identity that works with existing tenant isolation, and every upload path needs a size limit that S3 enforces, not only the client.

Do not set GUEST_COOKIE_SECRET in production until PR2 is merged. PR2 adds guest quotas, AI caps and the blocked features.

Look hardest at

  • auth() ordering: Clerk first, then the guest cookie.
  • The cap query in src/app/api/guest/route.ts.
  • The presigned POST conditions.
  • The profiler's early return.

Deploy prerequisites (AWS, by hand)

  • Raw bucket CORS must allow POST.
  • The profiler S3 trigger must include s3:ObjectCreated:Post (or *). With Put only, browser uploads never trigger profiling.
  • Redeploy the profiler.
  • Run migration 003.

Verified (locally)

  • npx tsc --noEmit, npm run lint (0 errors), npx vitest run: 175 passed.
  • pytest: 93 passed on pandas 2.2.3 and 93 passed on pandas 3.0.6.
  • npm run test:integration: 12 passed, including the new guest-access.test.ts (cookie + guest_sessions row, 429 on the 6th session from one IP, a guest's 2 MB policy, and an oversized object rejected by the profiler).
  • CLEANSTACK_INTEGRATION=1 pytest lambdas/tests/integration: 6 passed.
  • npm run test:e2e: 10 passed. New guest.spec.ts covers guest → upload → review, and a 2.1 MB file refused in the form and with 413. The guard servers now also reject a forged cs_guest cookie.

Caveat

LocalStack 3.8 does not enforce POST policies, so the S3-side rejection is covered by the policy unit tests rather than by a real S3 round trip.

Guests get an httpOnly cookie guest_<id>.<exp>.<HMAC-SHA256> (24 h) signed
with GUEST_COOKIE_SECRET; auth() returns the guest_ id as the user id, so it
is the team_id and existing tenant queries need no change. Clerk users always
win over a guest cookie. The middleware accepts a valid guest cookie on
protected routes (Clerk and test mode). Web Crypto only, so it runs on edge.

POST /api/guest records the session in guest_sessions (migration 003) with
an HMAC of the client IP, capped at 5 per IP and 200 overall per 24 h in one
INSERT ... SELECT so concurrent requests cannot overshoot. Optional
Turnstile check when TURNSTILE_SECRET_KEY is set (fails closed on network
errors). Everything is off while GUEST_COOKIE_SECRET is unset.

Rejected: a JWT library (one HMAC is enough and keeps the edge bundle
small) and storing sessions only in the cookie (no way to cap per IP).
A presigned PUT cannot limit the body size, so /api/upload now returns a
presigned POST whose policy pins the key and content type and carries
content-length-range (100 MB, MAX_UPLOAD_MB; guests 2 MB). The route refuses
a declared size over the limit with 413 before creating a run, GET returns
the caller's limit so the form refuses a large file before creating a
pipeline, and the profiler checks ContentLength before reading the object
and fails the run (backstop for clients that bypass the form; LocalStack
does not enforce POST policies, so tests cover this path).

Deploy note: the raw bucket CORS must allow POST and the profiler trigger
must include s3:ObjectCreated:Post.
@vercel

vercel Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
clean-stack Ready Ready Preview Oct 2, 2026 3:57pm UTC
cleanstack Ready Ready Preview, v0 Oct 2, 2026 3:57pm UTC

This branch was successfully deployed

2 active deployments
Preview – cleanstack — 28a2e0bd Deployed Oct 2, 2026 by vercel[bot]
Preview – clean-stack — 28a2e0bd Deployed Oct 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant