Skip to content

feat(auth): add annotation refresh endpoint and related cookie handling - #473

Merged
pikann merged 3 commits into
masterfrom
feature/enhance-annotation-feature
Sep 8, 2026
Merged

pikann merged 3 commits into
masterfrom
feature/enhance-annotation-feature

Conversation

@pikann

@pikann pikann commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

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_cli agents, sprint planning UI, task board pagination, custom field colors, conversation permissions/parallelism, and migration-tracking infrastructure.

Page annotations & the Chrome extension

  • New apps/extension package: a Chrome extension that detects when you're on a forwarded environment preview and shows a commenting toolbar, backed by a new AnnotationHandler/annotation domain+service+repository (list/create/resolve/reopen annotations, add comments, screenshot upload, create-task-from-annotation).
  • New page_annotations/page_annotation_comments tables and REST routes, wired into the router with annotations.read/annotations.write/annotations.resolve permissions.
  • Web app additions: a Port Forwards tab and comments UI (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.
  • Extension polish: icons/manifest, a privacy policy page for the Chrome Web Store listing, idempotent task creation from an annotation, and user-facing error handling/toasts for comment actions instead of silent failures.
  • Security fix: reject a foreign files.id being used as an annotation screenshot.
  • New CI/CD workflows for the extension (extension-pr-ci.yml, plus the cd.yml release 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/Strict cookies — when the scheme also matches, not just the hostname.

  • Added a paca_scheme cookie alongside the existing paca_port one, so the extension no longer assumes the API shares its current page's location.protocol.
  • Added a second, narrowly-scoped ScopeAnnotation token 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 in applyAuthn via enforceTokenScope, which fails closed on any unrecognized scope.
  • New 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/refresh now 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_scheme are now unconditionally non-Secure, regardless of COOKIE_SECURE — they exist specifically to be read via document.cookie from a forwarded preview that's very often plain HTTP even when the main app is correctly HTTPS-only, and a Secure cookie is invisible to document.cookie on a non-HTTPS page.
  • paca_scheme's value is derived directly from COOKIE_SECURE rather than X-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

  • New provider_cli agent 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 new cli_provider/cli_model/cli_auth_mode/cli_api_key_secret/cli_login_verified_at fields on agents.
  • New endpoints to verify CLI login status for both agents and environments, surfaced in the agent card/create-agent dialog UI.
  • Skill-name validation to prevent directory traversal, plus permission/visibility checks and regression tests for agent-type selection.

Sprint management UI

  • Planned sprints with a collapsible sidebar section and status badges.
  • Refactored sprint modals around a single SprintFormModal; warns before starting a sprint while another is already active.

Task board performance

  • Infinite scroll for task columns with backoff-retry on failed loads, cancellation on unmount, and retry-scheduling fixes under React StrictMode.

Custom field colors

  • Custom field options can now carry an optional color, with a new ColorSwatchPicker component and a migration to backfill existing options into the new format (with UnmarshalJSON support for the legacy plain-string shape).

Conversation permissions & agent parallelism

  • New conversations.read/conversations.write permissions (with dedup logic so role displays don't show redundant grants), and agents.read now required for GetConversationForAgent.
  • Per-agent parallelism_limit with an agent_pending_triggers queue and a new AgentQueueConsumer to advance queued triggers as conversations finish, plus an onBusy parameter and folder-capacity checks for conversation dispatching.

Database migration infrastructure

  • New schema_migrations tracking table plus an advisory lock, so each migration file runs exactly once and concurrent deploys can't race each other applying migrations.
  • Dedicated end-to-end tests that apply every migration file against a real Postgres database; removed the now-superseded ad hoc migration tests.

🤖 Generated with Claude Code

- 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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • ScopeAnnotation token pair — new scope="annotation" JWT claim; Login/Refresh/RefreshAnnotation mint the narrow pair from the same identity/family; applyAuthn falls back to the annotation_access_token cookie and enforceTokenScope restricts it to AnnotationExtensionPathPattern. Unknown scopes fail closed; main vs. annotation refresh tokens cross-reject via rotateRefreshToken(wantScope).
  • POST /auth/annotation-refresh — dedicated rotation endpoint reading only the path-scoped annotation_refresh_token cookie; Refresh also reissues (not rotates) a fresh annotation pair so the extension piggybacks on main-session cadence.
  • Discovery cookiespaca_scheme added; paca_port/paca_scheme made unconditionally non-Secure (readable via document.cookie from plain-HTTP forwarded ports) and cleared on logout.
  • CORS — the same-hostname credentialed exception is now keyed to the shared AnnotationExtensionPathPattern instead of a hand-synced copy, and /auth/refresh is 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.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread services/api/internal/transport/http/handler/auth_handler.go
pikann and others added 2 commits September 8, 2026 08:00
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>
@pikann
pikann merged commit 6ad73e9 into master Sep 8, 2026
6 checks passed
@pikann
pikann deleted the feature/enhance-annotation-feature branch September 8, 2026 08:19
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