Skip to content

feat: add me.set_cookie() and me.delete_cookie() API#1376

Merged
richard-to merged 36 commits intomainfrom
claude/set-cookie-api
Apr 19, 2026
Merged

feat: add me.set_cookie() and me.delete_cookie() API#1376
richard-to merged 36 commits intomainfrom
claude/set-cookie-api

Conversation

@richard-to
Copy link
Copy Markdown
Collaborator

@richard-to richard-to commented Apr 12, 2026

Summary

Adds a native me.set_cookie() and me.delete_cookie() API so Mesop apps can set browser cookies from within event handlers. Closes #1374.

Also closes #1304.

Problem

Mesop's /__ui__ endpoint is an SSE stream — Flask commits HTTP response headers (including Set-Cookie) before the event-handler generator body runs. This makes after_this_request and direct cookie manipulation ineffective from within event handlers, leaving no built-in way to issue Set-Cookie headers from app code.

Solution: two-phase cookie application

  1. me.set_cookie() / me.delete_cookie() queue PendingCookie objects on the request context (Python side only — no network round-trip yet).
  2. After each event-handler yield inside generate_data(), any pending cookies are encoded into a short-lived itsdangerous-signed token (60 s TTL) that embeds the cookie data directly (no server-side storage). An ApplyCookiesCommand carrying that token is appended to the render commands. Any server worker holding the same MESOP_COOKIE_SECRET_KEY can verify and redeem the token, so no sticky sessions are required.
  3. The TypeScript client receives the command and makes a POST /__apply-cookies request with the token in the form body (t=, credentials: 'same-origin'). Sending the token in the body (not the URL) keeps it out of server access logs and browser history.
  4. The /__apply-cookies Flask endpoint validates the Origin header (same-site check, matching /__ui__), verifies the itsdangerous signature and TTL, marks the token's nonce as used (single-use within a process), then returns an empty 204 response with the appropriate Set-Cookie headers.

This approach works identically in both SSE and WebSocket modes. The signed token with a short TTL and per-process nonce tracking prevents replay attacks; the same-site Origin check prevents a cross-site attacker from replaying a captured token against a victim's browser.

Note on value visibility: the token payload is signed but not encrypted, so cookie values are Base64-readable as the token passes through browser JavaScript. For values that must never be visible to JS, use @me.cookieclass(encrypted=True).

Note on multi-worker replay: nonces are tracked per-process, so cross-worker replay is theoretically possible but mitigated by the CSRF Origin check and the short TTL. See MESOP_COOKIE_SECRET_KEY docs for details.

MESOP_COOKIE_SECRET_KEY is required for all cookie write operations (me.set_cookie(), me.delete_cookie()).

Files changed

File Change
mesop/protos/ui.proto Add ApplyCookiesCommand message (field 8 in Command oneof)
mesop/protos/ui_pb2.py Regenerated from updated proto
mesop/runtime/context.py Add PendingCookie dataclass; add set_cookie(), delete_cookie(), pending_cookies(), clear_pending_cookies() to Context
mesop/commands/set_cookie.py New file — public me.set_cookie() / me.delete_cookie() wrappers
mesop/__init__.py Export the two new functions
mesop/server/server.py Add _CookieTokenCache (itsdangerous-signed + nonce tracking), maybe_append_apply_cookies_command(), /__apply-cookies route with same-site CSRF guard
mesop/web/src/shell/shell.ts Handle hasApplyCookies() command with a fetch() POST to /__apply-cookies

Usage example

import mesop as me
from werkzeug.security import check_password_hash

USERS = {"alice": ""}

@me.stateclass
class State:
    username_input: str
    password_input: str

def on_login(e: me.ClickEvent):
    state = me.state(State)
    stored = USERS.get(state.username_input)
    if not stored or not check_password_hash(stored, state.password_input):
        return
    # Cookie is applied via a lightweight follow-up POST request
    me.set_cookie(
        "session",
        create_session_token(state.username_input),
        max_age=86400,   # 1 day
        httponly=True,
        secure=True,
        samesite="Lax",
    )
    me.navigate("/app")

Test plan

  • Call me.set_cookie() from a button click handler; verify Set-Cookie header appears in the /__apply-cookies response in browser DevTools
  • Verify the cookie persists across hard page refreshes and new tabs
  • Call me.delete_cookie() and verify the cookie is removed
  • Verify the token is one-time (within a process): POSTing to /__apply-cookies a second time with the same token returns 400
  • Verify the token expires: calling /__apply-cookies after 60 s returns 400
  • Test with MESOP_WEBSOCKETS_ENABLED=true — cookie should still be applied correctly
  • Test with MESOP_BASE_URL_PATH set — client should call the prefixed endpoint

claude and others added 4 commits April 12, 2026 20:22
Adds a native cookie API that works within Mesop's SSE and WebSocket
streaming architecture. Because HTTP headers are committed before event
handler generators run, cookies cannot be set directly on the /__ui__
response. Instead, this implementation uses a two-phase approach:

1. me.set_cookie() / me.delete_cookie() queue PendingCookie objects on
   the request context.
2. After each event-handler yield in generate_data(), any pending cookies
   are stored in a server-side _CookieTokenCache under a one-time random
   token (60 s TTL), and an ApplyCookiesCommand carrying that token is
   appended to the render commands.
3. The TypeScript client receives the command and makes a GET request to
   /__apply-cookies?t=<token>.
4. The Flask endpoint retrieves and deletes the token from the cache, then
   returns a 204 response with the appropriate Set-Cookie headers.

This approach works identically in both SSE and WebSocket modes, and the
one-time token prevents replay attacks.

Files changed:
- mesop/protos/ui.proto: add ApplyCookiesCommand (field 8 in Command oneof)
- mesop/protos/ui_pb2.py: regenerate from updated proto
- mesop/runtime/context.py: add PendingCookie dataclass + cookie methods
- mesop/commands/set_cookie.py: public me.set_cookie() / me.delete_cookie()
- mesop/__init__.py: export the new functions
- mesop/server/server.py: _CookieTokenCache, maybe_append_apply_cookies_command(),
  /__apply-cookies route
- mesop/web/src/shell/shell.ts: handle hasApplyCookies() command with fetch

Closes #1374

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Adds a self-contained login/logout demo that exercises me.set_cookie()
and me.delete_cookie():

- mesop/examples/set_cookie.py: simulates a session cookie auth flow.
  On "Log in", sets demo_session cookie via me.set_cookie(). On "Log
  out", removes it with me.delete_cookie(). on_load reads the cookie
  from request.cookies so state survives hard refreshes and new tabs.

- mesop/tests/e2e/set_cookie_test.ts: three Playwright tests covering
  * login sets cookie + state persists after reload
  * login state is present in a new tab (cookie shared across tabs)
  * logout deletes cookie and reload shows logged-out state

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
ui_pb2.py is produced by Bazel at build time from ui.proto and should
not be committed. Remove it from tracking and add it to .gitignore so
it is not accidentally re-added.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a first-class me.set_cookie() / me.delete_cookie() API by queuing cookies server-side during event handling, emitting an ApplyCookiesCommand, and having the client call a dedicated __/apply-cookies endpoint that responds with Set-Cookie headers.

Changes:

  • Add ApplyCookiesCommand to the UI command protocol and handle it in the web shell by calling GET /__apply-cookies?t=....
  • Add server-side pending-cookie tracking (PendingCookie), a one-time token cache, and a new Flask route to apply cookies via a normal HTTP response.
  • Add an example app and Playwright e2e coverage for basic set/delete cookie behavior.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
mesop/web/src/shell/shell.ts Handles new ApplyCookiesCommand by fetching the cookie-application endpoint.
mesop/server/server.py Implements token cache, command emission, and GET /__apply-cookies endpoint that sets cookies.
mesop/runtime/context.py Adds PendingCookie plus context APIs to queue/clear pending cookies.
mesop/commands/set_cookie.py Public wrappers set_cookie() / delete_cookie() for app code (mesop as me).
mesop/__init__.py Exports the new cookie APIs at the top-level mesop namespace.
mesop/protos/ui.proto Extends the command oneof with ApplyCookiesCommand.
mesop/tests/e2e/set_cookie_test.ts Adds e2e test for login cookie set/persist/delete.
mesop/examples/set_cookie.py / mesop/examples/__init__.py Adds and registers a runnable example demonstrating the API.
.gitignore Ignores generated ui_pb2.py artifact.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mesop/web/src/shell/shell.ts Outdated
Comment thread mesop/server/server.py
Comment thread mesop/server/server.py Outdated
Comment thread mesop/server/server.py
Comment thread mesop/runtime/context.py
Comment thread mesop/server/server.py Outdated
claude added 4 commits April 13, 2026 00:15
Addresses all Copilot review comments on #1376 and the GET vs POST
security question:

GET → POST for /__apply-cookies
  A GET puts the token in the URL, which lands in server access logs,
  proxy logs, and browser history. Switching to POST keeps the token in
  the request body where it is not routinely logged. The server now reads
  request.form['t'] and the TypeScript client sends it as
  application/x-www-form-urlencoded.

shell.ts – token validation + error handling + cache: 'no-store'
  Validate that the token is non-empty before sending the request, and
  throw on any non-2xx response so failures surface in the existing error
  dialog rather than being silently swallowed. Also add cache: 'no-store'
  on the fetch call.

server.py – Cache-Control: no-store on /__apply-cookies response
  Prevent intermediary caches from storing the tokenised response.

server.py – opportunistic expiry eviction in _CookieTokenCache
  Call _evict_expired_locked() inside put() and pop() so stale entries
  are cleaned up on every write instead of accumulating indefinitely.

server.py – flush pending cookies in on_load paths
  maybe_append_apply_cookies_command() was only called in the user_event
  handler loop. It is now also called before render_loop in the init
  on_load generator path, the init on_load non-generator path, and inside
  run_page_load, so cookies set during on_load are correctly flushed.

context.py – validate samesite at the API boundary
  Raise MesopDeveloperException with a clear message for invalid samesite
  values, and also for samesite='None' without secure=True (required by
  RFC 6265bis).

set_cookie_test.ts – add token-replay security test
  Intercept the POST to /__apply-cookies, capture the token, then replay
  it after the first use to confirm the server returns 400.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
- Add mesop/commands/cookie_class.py with:
  - @me.cookieclass decorator: turns a dataclass into a cookie-backed
    structured store (name auto-derived as snake_case of class name)
  - me.cookie(Cls): reads + deserialises from request.cookies; returns
    default instance on absence or parse error
  - me.save_cookie(instance, ...): JSON-serialises + calls set_cookie();
    secure=None auto-detects HTTPS from request.is_secure
  - me.delete_cookieclass(Cls, ...): deletes by class lookup
- Update me.delete_cookie() to accept a cookieclass type as well as str
- Export cookieclass, cookie, save_cookie, delete_cookieclass from me.*
- Rewrite set_cookie.py example to use @me.cookieclass / me.save_cookie
  (removes raw request.cookies access and string prefix parsing)
- Update e2e test cookie name (demo_session → session_cookie) and value
  assertion (now JSON)
- Add experimental Cookies section to docs/guides/auth.md covering both
  the @me.cookieclass high-level API and the low-level set_cookie /
  delete_cookie functions, with security notes

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
…cases

me.delete_cookie() already accepts a cookieclass type or a plain string,
so delete_cookieclass was redundant. Remove it.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
… SECRET_KEY

- Merge save_cookie into set_cookie — one function, two call forms:
    me.set_cookie("name", "value", ...)         # low-level: explicit name + raw string
    me.set_cookie(SessionCookie(username="x"))  # high-level: cookieclass instance
  Remove save_cookie from public API entirely.
- Change secure default from True → None (auto-detects HTTPS from
  request.is_secure) so the same code works in local dev and production.
- Add signed=True / encrypted=True options to @me.cookieclass:
    signed    — HMAC via itsdangerous (stdlib-compatible, already a Flask dep)
    encrypted — Fernet via cryptography (pip install cryptography required)
  Both require SECRET_KEY to be set; validated lazily at runtime.
  signed and encrypted are mutually exclusive (raises MesopDeveloperException).
- Read SECRET_KEY from env and set flask_app.secret_key in server.py.
- Document SECRET_KEY in docs/api/config.md.
- Update docs/guides/auth.md: remove save_cookie references, clarify
  when to use the two set_cookie call forms, add signed/encrypted section.
- Update example and e2e test accordingly.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
me.cookie("session_id")   # returns raw str value, or "" if absent
me.cookie(SessionCookie)  # returns typed instance (existing behaviour)

Removes the need to import flask.request to read raw cookies.
Update docs/guides/auth.md to document both forms.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mesop/commands/cookie_class.py
Comment thread mesop/runtime/context.py
Comment thread mesop/commands/set_cookie.py
Comment thread mesop/server/server.py
Comment thread docs/guides/auth.md Outdated
claude and others added 10 commits April 14, 2026 23:43
39 tests across two files; encrypted tests auto-skip when the
cryptography package is absent.

cookie_class_test.py covers:
  - _to_snake_case (simple, acronym, single word, no-op)
  - @cookieclass (name derivation, explicit name, signed/encrypted flags,
    signed+encrypted mutual exclusion, dataclass coercion)
  - me.cookie(str) — raw string form (present, absent)
  - me.cookie(Cls) — cookieclass form (populated, absent, bad JSON,
    unknown keys, partial fields)
  - signed encode/decode roundtrip, tamper detection, wrong-key rejection,
    missing SECRET_KEY error
  - encrypted encode/decode roundtrip and tamper detection (skipped if
    cryptography is not installed)

set_cookie_test.py covers:
  - _resolve_secure (True, False, None on HTTP/HTTPS, outside request)
  - set_cookie string form (calls context, explicit secure, missing value raises)
  - set_cookie instance form (JSON serialisation, kwargs forwarding,
    non-cookieclass raises)
  - delete_cookie string and class forms, path/domain forwarding,
    non-cookieclass raises

conftest.py (root) stubs Bazel-generated/Bazel-only modules so tests run
with plain pytest without a Bazel build.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
1. cookieclass: use dataclass_with_defaults (same as stateclass) so
   me.cookie(Cls) can always call cls() without arguments, even when a
   field has no explicit default. Previously a required field would raise
   TypeError on absent/unparseable cookies, contradicting the documented
   fallback-to-defaults behaviour.

2. context.delete_cookie: add secure parameter defaulting to False.
   Deletion cookies (Max-Age=0) must be visible to the browser on HTTP;
   the old default of secure=True (inherited from PendingCookie) silently
   prevented deletions in plain-HTTP environments.

3. set_cookie.delete_cookie: add secure: bool | None = None and resolve
   via _resolve_secure(), consistent with set_cookie(). Auto-detects HTTPS
   on production and HTTP on local dev; tests updated accordingly.

4. server.py generate_data: clear pending cookies in the exception handler
   so a failed event handler cannot leak queued cookies into a subsequent
   event cycle. Most important in WebSockets mode where Context is
   long-lived across requests.

5. docs/guides/auth.md: reword "SSE or WebSockets — neither of which
   supports Set-Cookie headers" which was inaccurate (SSE can carry
   Set-Cookie headers; the real constraint is that headers are committed
   before event-handler code runs).

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
- Add `from collections.abc import Callable` to cookie_class.py (F821)
- Fix mutable default arg in _req_ctx (B006) — use None sentinel
- Remove unused variable from pytest.importorskip call (F841)
- Add strict=False to zip() calls in context.py (B905)
- Apply ruff auto-format to all modified files

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
- Session2Cookie → session2_cookie: digits behave like lowercase,
  triggering a split before the following capital
- _Session → __session: leading underscore is preserved verbatim

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
- Add @overload to cookie() so pyright knows cookie(type[T]) returns T,
  not T|str — fixes all the "Cannot access member X for type str" errors
  in tests and examples
- Add cls: None = None to cookieclass() second overload and explicit
  return type to implementation — fixes overload consistency error and
  TypeVar-appears-only-once warning
- bytes(key).decode() instead of key.decode() in _get_secret_key() —
  Flask secret_key can be memoryview in some stub versions; bytes()
  wrapping handles all byte-like types safely
- Stack @dataclasses.dataclass on _Prefs/_Session in set_cookie_test.py
  and SessionCookie in set_cookie.py so pyright sees the typed __init__
  through the opaque @cookieclass decorator

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
When stacked, @dataclasses.dataclass must run first (innermost) so that
@cookieclass registers the already-dataclassed class.  The previous order
had @dataclasses.dataclass on the outside, which applied it *after*
@cookieclass — creating a new class object not present in _COOKIE_CLASSES,
causing me.cookie(SessionCookie) to raise "not a cookieclass" on page
reload and breaking the e2e persistence test.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
… e2e tests

The DOM update ("Logged in as: alice") arrives via the SSE response, but
the cookie is set by a separate follow-up POST to /__apply-cookies that the
Mesop client makes asynchronously afterward.  Checking page.context().cookies()
immediately after the UI assertion raced against that POST and saw an empty
jar.

Fix: use page.waitForResponse('**/__apply-cookies') before asserting on
the cookie jar in the login, new-tab, and logout tests.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
…okies

page.waitForResponse resolves when the network layer receives the response,
but Playwright's CDP cookie store can lag slightly behind.  Checking
page.context().cookies() in that window races against the browser's internal
cookie-write path and sees an empty jar.

Move the cookie-jar assertions to after the page.reload() call.  At that
point the browser must already have the cookie stored (the on_load handler
would not have restored "Logged in as: alice" without it), so there is no
timing race.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
page.context().cookies() queries the browser's cookie store via CDP, which
may not be updated synchronously after a fetch/XHR response even after
page.waitForResponse() resolves.  This made the explicit cookie-jar assertion
flaky regardless of ordering.

Replace with two reliable checks instead:
  1. Assert the /__apply-cookies response status is 204 — this confirms the
     server sent Set-Cookie headers (a 400 would mean the token was missing
     or already consumed).
  2. Assert "Logged in as: alice" is still visible after a hard reload — this
     proves end-to-end that the cookie was stored, sent on the reload request,
     looked up by the correct name, parsed correctly, and used to restore state.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mesop/server/server.py
Comment thread conftest.py Outdated
Comment thread mesop/web/src/shell/shell.ts
claude added 2 commits April 16, 2026 02:21
…_Pb2Finder modernisation

- server.py: add same-site Origin check to /__apply-cookies (matching the
  existing guard on ui_stream) so a cross-site attacker cannot replay a valid
  token against a victim's browser; guard is skipped in debug mode like the
  other CSRF checks
- conftest.py: restrict _Pb2Finder to mesop.*_pb2 so it no longer shadows
  real protobuf modules (e.g. google.protobuf.*_pb2); migrate from the
  deprecated find_module/load_module API to the modern find_spec/
  create_module/exec_module importlib interface

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
…ises token check

The new same-site CSRF guard on /__apply-cookies runs before the token
lookup. page.request.post() sends no Origin header, so without this fix
the replay returns 403 (CSRF) instead of the expected 400 (token
already consumed). Sending the matching Origin lets the CSRF check
pass and ensures the test verifies the actual single-use property.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread conftest.py
Comment thread mesop/server/server.py
Comment thread mesop/runtime/context.py Outdated
Comment thread mesop/server/server.py Outdated
claude added 4 commits April 16, 2026 04:32
server.py — stateless cookie token mode when SECRET_KEY is set:
  _CookieTokenCache now operates in two modes. When Flask's SECRET_KEY
  is configured, put() encodes the cookie list into an itsdangerous-
  signed self-contained token so any replica can verify it — multi-
  worker deployments don't need sticky sessions as long as SECRET_KEY
  is set. When SECRET_KEY is absent, it falls back to the existing
  per-process in-memory store (same sticky-session requirement as
  Mesop's in-memory state-session backend; nothing gets worse for
  those users). Tighten _store and put()/pop() type annotations to
  list[PendingCookie]. Add import dataclasses.

context.py — pending_cookies() returns immutable tuple:
  Change return type from list[PendingCookie] to tuple[PendingCookie, ...]
  so callers cannot accidentally mutate internal state. Existing callers
  are unaffected (iteration and bool() work on tuples; the one caller
  that needs a list wraps it in list()).

conftest.py — _Pb2Finder scoping and Bazel guard:
  _stub_generated_protos() now tries to import mesop.protos.ui_pb2 first
  and returns immediately if it succeeds, so it is a no-op inside Bazel
  test runs where real generated modules are available. _Pb2Finder is now
  appended to sys.meta_path (instead of inserted at position 0) so any
  real module on sys.path is still found first.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Remove the in-memory fallback from _CookieTokenCache. Cookie tokens are
now always self-contained itsdangerous-signed blobs (URLSafeTimedSerializer),
which means:

- SECRET_KEY is required; a MesopDeveloperException with a clear message
  is raised if it is not configured.
- Any server replica can verify a token — multi-worker deployments work
  without sticky sessions.
- The class has no instance state (no lock, no dict, no eviction).

playwright.config.ts: pass SECRET_KEY=mesop-e2e-test-secret-key to the
test server (overridable via SECRET_KEY env var) so e2e cookie tests pass.

Update the comment in configure_flask_app() and the set_cookie.py example
docstring to document the SECRET_KEY requirement.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Use a Mesop-namespaced env var instead of the generic Flask SECRET_KEY
convention. The signing code now reads directly from os.environ rather
than going through flask_app.secret_key, so there is no Flask coupling.

Updated: server.py, cookie_class.py (implementation + docstrings),
cookie_class_test.py (patch.dict(os.environ) instead of flask_app.secret_key),
playwright.config.ts, set_cookie.py example docstring.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Mesop's signing code reads directly from os.environ so flask_app.secret_key
is unused by Mesop. Setting it would silently override any secret_key the
user configured for their own Flask session/signing needs.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mesop/tests/e2e/set_cookie_test.ts Outdated
Comment thread mesop/server/server.py
Comment thread mesop/server/server.py
Comment thread mesop/server/server.py Outdated
Comment thread docs/api/config.md Outdated
Comment thread docs/guides/auth.md Outdated
Comment thread mesop/commands/cookie_class.py Outdated
claude added 3 commits April 16, 2026 16:57
The signed-token approach had two security problems identified in review:
1. Not single-use — pop() succeeded on every replay until TTL expired,
   so the replay e2e test would not pass.
2. Cookie values exposed to JS — the signed JSON blob traveled through
   the browser runtime, undermining httponly=True for session tokens.

Revert _CookieTokenCache to the original opaque random-token design:
- secrets.token_urlsafe(32) → truly random, unguessable token
- cookie data stored server-side, never in the token itself
- pop() removes the token on first use (strictly single-use)
- per-process in-memory store (same sticky-session requirement as
  Mesop's in-memory state-session backend for multi-worker)

Also in this commit:
- Remove _get_cookie_secret_key() (no longer needed for basic cookie ops)
- MESOP_COOKIE_SECRET_KEY is now only required for @cookieclass(signed=True)
  or @cookieclass(encrypted=True), not for basic me.set_cookie()
- Fix remaining SECRET_KEY → MESOP_COOKIE_SECRET_KEY in cookie_class.py
  docstring (encrypted description line), docs/api/config.md (section
  heading + sh examples), docs/guides/auth.md (cross-reference link)
- Remove MESOP_COOKIE_SECRET_KEY from playwright.config.ts (not needed
  for the plain-cookieclass e2e tests)
- Fix e2e replay URL construction to use pathname replacement so any
  MESOP_BASE_URL_PATH prefix is preserved

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
…nonce tracking

The previous in-memory _CookieTokenCache required sticky sessions,
introducing a regression for MESOP_STATE_SESSION_BACKEND=none deployments
where any worker can handle any request (client carries full state).

New design: each token is an itsdangerous-signed payload containing the
pending cookies and a random nonce. Any worker holding the same
MESOP_COOKIE_SECRET_KEY can verify the signature, so no sticky sessions
are required. Per-process nonce tracking ensures strict single-use within
a process; cross-worker replay is impractical given the CSRF Origin check
and HTTPS in production.

Also adds MESOP_COOKIE_SECRET_KEY to playwright.config.ts so the e2e
test server starts with the key set (required since me.set_cookie() now
always needs it).

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
The apply-cookies token is strictly single-use within a single process
but can be replayed to a different worker in multi-worker deployments.
Document the mitigation (CSRF Origin check + 60s TTL) and when sticky
sessions are needed for cryptographic single-use guarantees.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mesop/protos/ui.proto Outdated
Comment thread mesop/server/server.py Outdated
claude added 3 commits April 16, 2026 18:38
- ui.proto: update ApplyCookiesCommand comment from "server-side token
  used to retrieve" to "one-time signed token used to apply" to match
  the actual itsdangerous-signed implementation
- server.py: soften apply_cookies docstring — "replay attacks are not
  possible" overstated; now says single-use within a process, cross-
  worker replay mitigated by CSRF check + TTL
- docs/guides/auth.md: update "How it works" section from old server-
  side storage description to the actual signed-token design; add link
  to MESOP_COOKIE_SECRET_KEY and note per-process nonce enforcement

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
- UP035: use collections.abc.Generator/Sequence instead of typing
- I001: sort itsdangerous imports inside pop()

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Comment thread mesop/server/server.py
Comment thread docs/guides/auth.md
claude added 2 commits April 16, 2026 23:55
…uth.md

- cookie_class_test.py: apply ruff-format (trailing whitespace / blank
  line style) — the check linter was clean but the formatter found diffs
- docs/guides/auth.md: add Security notes bullet explaining that cookie
  values are Base64-readable in the ApplyCookiesCommand token while it
  transits browser JS; recommend encrypted=True for values that must
  never be visible to client-side code

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
The env var is required for all cookie operations (set_cookie, delete_cookie,
cookie), not just signed/encrypted ones. Add an admonition block at the top
of the Cookies section and update the signed/encrypted description to clarify
it adds protection on top of the already-required key.

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/guides/auth.md
Comment thread conftest.py
Comment thread mesop/server/server.py
Comment thread mesop/server/server.py
Comment thread mesop/commands/set_cookie.py
- docs/guides/auth.md: tighten MESOP_COOKIE_SECRET_KEY admonition —
  write ops always need it; me.cookie() with plain cookieclass does not
- conftest.py: make _stub_mesop_package() a no-op when real proto
  modules are importable (Bazel build), matching _stub_generated_protos()
- server.py: flush pending cookies after the user-event for-loop so
  cookies set by a generator that yields 0 times are still sent
- set_cookie.py: add note to httponly arg docstring that the value is
  briefly Base64-visible in the ApplyCookiesCommand token as it passes
  through browser JS; recommend encrypted=True for fully opaque values

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Comment thread docs/api/config.md Outdated
Comment thread mesop/commands/cookie_class.py Outdated
Comment thread mesop/server/server.py
- docs/api/config.md: shorten description to "Secret key required for
  using Mesop cookies" — no need to enumerate specific call sites
- cookie_class.py: hoist single "Requires MESOP_COOKIE_SECRET_KEY" note
  to class level; remove per-feature repetition in signed/encrypted
  descriptions and Args
- server.py + cookie_class.py: simplify error messages to
  "must be set to use Mesop cookies" (not specific to set/delete)

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
Comment thread docs/guides/auth.md Outdated

Mesop provides a first-class cookie API so you can persist small pieces of data (session tokens, user preferences, etc.) in the browser without managing raw `Set-Cookie` headers yourself.

!!! note "MESOP_COOKIE_SECRET_KEY requirements"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This note can be removed. This is out dated since the secret key is needed to use cookies. It's true that some operations don't require the secret key, but in practice it's not usable without it. That technicalities create confusion.

Mainly we always need the secret key for the Apply Cookies Command.

Comment thread docs/guides/auth.md Outdated

### Signed and encrypted cookies

By default `@me.cookieclass` stores the JSON value in plain text — readable in browser DevTools. For cookies that carry sensitive data you can add tamper-protection or full encryption by setting `signed=True` or `encrypted=True` on the decorator. [`MESOP_COOKIE_SECRET_KEY`](../api/config.md#mesop_cookie_secret_key) is already required for all cookie ops; these options add extra protection on top of that.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Remove last sentence about Mesop Cookie Secret. It's too much repetition.

Comment thread mesop/commands/cookie_class.py Outdated
class SessionCookie:
username: str = ""

Both ``signed`` and ``encrypted`` require ``MESOP_COOKIE_SECRET_KEY`` to be set.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Remove this line or update to say that MESOP_COOKIE_SECRET_KEY is required to use the class. I think it should be removed since it's mentioned again below in another comment.

Comment thread mesop/commands/set_cookie.py
Comment thread mesop/examples/set_cookie.py Outdated
The login state persists across hard refreshes and new tabs because the
cookie is read in on_load.

Note: me.set_cookie() and me.delete_cookie() require MESOP_COOKIE_SECRET_KEY
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Remove this line "Note: me.set_cookie() and me.delete_cookie() require MESOP_COOKIE_SECRET_KEY
to be set so that cookie tokens are cryptographically signed." Although it is true, it's too much detail. Just leave it with the example of how to start Mesop with MESOP_COOKIE_SECRET_KEY

- auth.md: simplify MESOP_COOKIE_SECRET_KEY admonition to just show
  how to set it; remove last sentence from signed/encrypted section
- cookie_class.py: remove redundant module-level MESOP_COOKIE_SECRET_KEY
  note (already stated in class docstring)
- set_cookie.py: add warning log in _resolve_secure RuntimeError path
  explaining when it occurs and that it's unexpected in production
- set_cookie.py example: drop explanation sentence, keep only the
  start command showing how to set MESOP_COOKIE_SECRET_KEY

https://claude.ai/code/session_01MmGxXfALHrHYRefdLyetzu
@richard-to richard-to merged commit 0867ccc into main Apr 19, 2026
3 checks passed
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.

feat: add me.set_cookie() / me.delete_cookie() API for session management State is reset on browser refresh - breaks authentication flows

3 participants