feat(auth): add annotation refresh endpoint and related cookie handling - #473
Conversation
- Introduced `AnnotationRefresh` method in `AuthHandler` to handle POST requests for refreshing annotation tokens. - Added new cookies: `annotation_access_token` and `annotation_refresh_token` for browser extension use. - Updated `setTokenCookies` and `clearCookies` methods to manage the new annotation cookies. - Implemented `enforceTokenScope` to restrict access based on token scope, specifically for annotation tokens. - Enhanced CORS middleware to allow credentialed access for annotation-related routes. - Added tests for new functionality, including annotation token handling and scope enforcement.
There was a problem hiding this comment.
Important
The annotation-refresh flow can't actually keep an extension-only session alive — paca_port/paca_scheme are never refreshed on this endpoint (see the inline comment on AnnotationRefresh). One fix to make before merge; everything else reviewed clean.
Reviewed changes
ScopeAnnotationtoken pair — newscope="annotation"JWT claim;Login/Refresh/RefreshAnnotationmint the narrow pair from the same identity/family;applyAuthnfalls back to theannotation_access_tokencookie andenforceTokenScoperestricts it toAnnotationExtensionPathPattern. Unknown scopes fail closed; main vs. annotation refresh tokens cross-reject viarotateRefreshToken(wantScope).POST /auth/annotation-refresh— dedicated rotation endpoint reading only the path-scopedannotation_refresh_tokencookie;Refreshalso reissues (not rotates) a fresh annotation pair so the extension piggybacks on main-session cadence.- Discovery cookies —
paca_schemeadded;paca_port/paca_schememade unconditionally non-Secure(readable viadocument.cookiefrom plain-HTTP forwarded ports) and cleared on logout. - CORS — the same-hostname credentialed exception is now keyed to the shared
AnnotationExtensionPathPatterninstead of a hand-synced copy, and/auth/refreshis removed from it. - Extension — scheme-aware
baseUrl, refresh switched to/auth/annotation-refresh, plus README/paca-port docs.
I verified the load-bearing assumptions against the code directly: chi v5.3.0 never rewrites r.URL.Path for nested subrouters (so the ^/api/v1/... anchor holds inside the mounted /projects/{projectId} subtree), and the foreign-origin CSRF surface on the SameSite=None cookies is bounded (preflight blocks credentialed cross-site reads; non-JSON form posts fail BindJSON). Ran go test internal/{service/auth,platform/token,transport/http/middleware,transport/http/handler,transport/http/router} — all pass, and the new tests genuinely fail without the checks they pin.
ℹ️ Extension and API must ship together
/api/v1/auth/refresh is dropped from the credentialed same-hostname CORS exception in the same change that moves the extension onto /auth/annotation-refresh. A browser still running the previous content script (which rotates via credentialed POST /auth/refresh) will have that refresh response CORS-blocked the moment this API deploys — even same-scheme — and permanently break at the first 15-minute token expiry, since nothing in the response is readable and no retry path recovers. If the extension is distributed independently of the API, confirm the coordinated-ship plan (or a compat window) before merging.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…Type for specific routes
golangci-lint's noctx check flagged the bare httptest.NewRequest calls added for RequireJSONContentType's tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Summary
This branch has accumulated 40 commits since diverging from
master. The headline feature is page annotations end-to-end (a Chrome extension that lets you comment directly on a running environment's forwarded preview, turning comments into tasks), plus several other features that landed on the same branch along the way:provider_cliagents, sprint planning UI, task board pagination, custom field colors, conversation permissions/parallelism, and migration-tracking infrastructure.Page annotations & the Chrome extension
apps/extensionpackage: a Chrome extension that detects when you're on a forwarded environment preview and shows a commenting toolbar, backed by a newAnnotationHandler/annotation domain+service+repository (list/create/resolve/reopen annotations, add comments, screenshot upload, create-task-from-annotation).page_annotations/page_annotation_commentstables and REST routes, wired into the router withannotations.read/annotations.write/annotations.resolvepermissions.port-forward-detail.tsx,port-forward-comments-tab.tsx,comment-detail-view.tsx), a comment-to-task flow, and BlockNote support for pasting/rendering annotation cards.files.idbeing used as an annotation screenshot.extension-pr-ci.yml, plus thecd.ymlrelease pipeline).Auth: making the extension work across scheme mismatches
The extension authenticates purely by relying on browser-attached cookies — it never reads or stores a token itself. That breaks whenever the forwarded preview page and the main Paca app don't share a scheme (e.g. the app sits behind HTTPS while a project's own dev server is plain HTTP, or vice versa): modern browsers only treat a request as "same-site" — eligible for
SameSite=Lax/Strictcookies — when the scheme also matches, not just the hostname.paca_schemecookie alongside the existingpaca_portone, so the extension no longer assumes the API shares its current page'slocation.protocol.ScopeAnnotationtoken pair (annotation_access_token/annotation_refresh_token,SameSite=None) issued alongside login, usable only against the small route set the extension actually calls (middleware.AnnotationExtensionPathPattern: port-forward resolution, its own refresh endpoint, and page-annotation CRUD) — never a substitute for a full session anywhere else. Enforced centrally inapplyAuthnviaenforceTokenScope, which fails closed on any unrecognized scope.POST /auth/annotation-refresh, independent from the main/auth/refresh— each rejects the other's token kind outright, so a credential scoped to the extension can never mint a full session./auth/refreshnow also reissues a fresh annotation pair on every call, so the annotation session's lifetime piggybacks on ordinary web-app usage instead of depending solely on the extension's own activity.paca_port/paca_schemeare now unconditionally non-Secure, regardless ofCOOKIE_SECURE— they exist specifically to be read viadocument.cookiefrom a forwarded preview that's very often plain HTTP even when the main app is correctly HTTPS-only, and aSecurecookie is invisible todocument.cookieon a non-HTTPS page.paca_scheme's value is derived directly fromCOOKIE_SECURErather thanX-Forwarded-Proto/r.TLS— the header-based approach broke silently whenever a reverse proxy in front (e.g. Caddy) didn't faithfully forward it through every hop, which isn't something this codebase controls.Provider CLI agents
provider_cliagent type: an agent can run via a local CLI provider (Claude Code, Codex, Cursor Agent, Gemini CLI) instead of calling a model API directly, with newcli_provider/cli_model/cli_auth_mode/cli_api_key_secret/cli_login_verified_atfields onagents.Sprint management UI
SprintFormModal; warns before starting a sprint while another is already active.Task board performance
Custom field colors
ColorSwatchPickercomponent and a migration to backfill existing options into the new format (withUnmarshalJSONsupport for the legacy plain-string shape).Conversation permissions & agent parallelism
conversations.read/conversations.writepermissions (with dedup logic so role displays don't show redundant grants), andagents.readnow required forGetConversationForAgent.parallelism_limitwith anagent_pending_triggersqueue and a newAgentQueueConsumerto advance queued triggers as conversations finish, plus anonBusyparameter and folder-capacity checks for conversation dispatching.Database migration infrastructure
schema_migrationstracking table plus an advisory lock, so each migration file runs exactly once and concurrent deploys can't race each other applying migrations.🤖 Generated with Claude Code