perf(mcp): кэш collab-токена в клиенте — чтобы кэш CollabSession (#400) попадал (#435) #439

Merged
vvzvlad merged 1 commits from perf/435-collab-token-cache into develop 2026-07-10 07:01:34 +03:00
Collaborator

Summary

Кэширует collab-токен в DocmostClient, чтобы реестр живых CollabSession (#400/#431) реально попадал. Реестр ключуется на wsUrl + " " + pageId + " " + token; клиент чеканил СВЕЖИЙ токен на КАЖДУЮ мутацию (in-app — пере-подписывает JWT с новым iat/exp раз в секунду; внешний MCP — POST /auth/collab-token на каждый вызов), поэтому token-компонент ключа менялся каждый раз → сессия почти никогда не переиспользовалась (connect-штормы, 25s таймауты, зомби-сессии). Теперь серия мутаций держит один токен → одну сессию.

closes #435

  • пер-инстансное поле collabTokenCache {token, mintedAt}; getCollabTokenWithReauth(forceRefresh=false) смотрит кэш первым (гейт !forceRefresh && ttl>0 && свежесть), оборачивает ОБЕ ветки чеканки (provider-путь in-app агента И REST) через rememberCollabToken (пустой токен не кэшируется);
  • readCollabTokenTtlMs() — env MCP_COLLAB_TOKEN_TTL_MS, дефолт 5 мин (<< 24h жизни токена и ≤ max-age сессии, окно устаревания прав не расширяется сверх #431); ЯВНЫЙ 0/отрицательное отключают кэш (rollback-knob), unset/непарсибельное → дефолт;
  • reauth (401/403) в обеих ветках рекурсит forceRefresh=true (обход кэша → свежий токен), гард !forceRefresh ограничивает ровно одним повтором;
  • изоляция идентичностей (инвариант 4): кэш пер-клиентный, сбрасывается на КАЖДОЙ смене идентичности — в login() и в response-интерцепторе (где this.token зануляется перед пере-логином). Единственные две точки установки this.token обе зануляют кэш.

How verified

Прогон на стенде (worktree, node --test):

  • pnpm --filter @docmost/mcp build → чисто (tsc).
  • node --test test/mock/collab-token-cache.test.mjs10/10 passed: попадание в пределах TTL чеканит один раз (provider+REST), пере-чеканка после TTL, TTL=0 fetch-every-call (обе ветки), 401 обходит кэш (без петли), forceRefresh обходит тёплый кэш, REST 401 re-login+refetch, login() чистит кэш, две последовательные мутации → идентичный токен (стабильный ключ сессии — цель #435).
  • node --test collab-token-cache + reauth + collab-session38/38 passed (нет регресса reauth/session).
  • 2 предсуществующих падения markdown-converter.test.mjs (отступы вложенных списков) — ортогональны, подтверждено на базе.

Внутренний цикл: 1 проход внутреннего ревью (APPROVE WITH SUGGESTIONS). Правка по ревью: уточнён docstring readCollabTokenTtlMs — расхождение с поведением (непарсибельное значение даёт дефолт 5мин с ВКЛючённым кэшем, а не отключает; отключает только явный 0/отрицательное). Необязательную дедупликацию конкурентных первых mint-ов сознательно не делал (не баг, не регресс; последовательный всплеск по странице сериализуется withPageLock и попадает в кэш) — оставил форком для ревьюера.

Checklist

  • критерии приёмки из #435 выполнены
  • вне заявленного scope ничего не менялось
## Summary Кэширует collab-токен в `DocmostClient`, чтобы реестр живых CollabSession (#400/#431) реально попадал. Реестр ключуется на `wsUrl + " " + pageId + " " + token`; клиент чеканил СВЕЖИЙ токен на КАЖДУЮ мутацию (in-app — пере-подписывает JWT с новым iat/exp раз в секунду; внешний MCP — `POST /auth/collab-token` на каждый вызов), поэтому token-компонент ключа менялся каждый раз → сессия почти никогда не переиспользовалась (connect-штормы, 25s таймауты, зомби-сессии). Теперь серия мутаций держит один токен → одну сессию. closes #435 - пер-инстансное поле `collabTokenCache {token, mintedAt}`; `getCollabTokenWithReauth(forceRefresh=false)` смотрит кэш первым (гейт `!forceRefresh && ttl>0 && свежесть`), оборачивает ОБЕ ветки чеканки (provider-путь in-app агента И REST) через `rememberCollabToken` (пустой токен не кэшируется); - `readCollabTokenTtlMs()` — env `MCP_COLLAB_TOKEN_TTL_MS`, дефолт 5 мин (<< 24h жизни токена и ≤ max-age сессии, окно устаревания прав не расширяется сверх #431); ЯВНЫЙ `0`/отрицательное отключают кэш (rollback-knob), unset/непарсибельное → дефолт; - reauth (401/403) в обеих ветках рекурсит `forceRefresh=true` (обход кэша → свежий токен), гард `!forceRefresh` ограничивает ровно одним повтором; - **изоляция идентичностей (инвариант 4)**: кэш пер-клиентный, сбрасывается на КАЖДОЙ смене идентичности — в `login()` и в response-интерцепторе (где `this.token` зануляется перед пере-логином). Единственные две точки установки `this.token` обе зануляют кэш. ## How verified Прогон на стенде (worktree, `node --test`): - `pnpm --filter @docmost/mcp build` → чисто (`tsc`). - `node --test test/mock/collab-token-cache.test.mjs` → **10/10 passed**: попадание в пределах TTL чеканит один раз (provider+REST), пере-чеканка после TTL, `TTL=0` fetch-every-call (обе ветки), 401 обходит кэш (без петли), `forceRefresh` обходит тёплый кэш, REST 401 re-login+refetch, `login()` чистит кэш, две последовательные мутации → идентичный токен (стабильный ключ сессии — цель #435). - `node --test collab-token-cache + reauth + collab-session` → **38/38 passed** (нет регресса reauth/session). - 2 предсуществующих падения `markdown-converter.test.mjs` (отступы вложенных списков) — ортогональны, подтверждено на базе. Внутренний цикл: 1 проход внутреннего ревью (APPROVE WITH SUGGESTIONS). Правка по ревью: уточнён docstring `readCollabTokenTtlMs` — расхождение с поведением (непарсибельное значение даёт дефолт 5мин с ВКЛючённым кэшем, а не отключает; отключает только явный `0`/отрицательное). Необязательную дедупликацию конкурентных первых mint-ов сознательно не делал (не баг, не регресс; последовательный всплеск по странице сериализуется `withPageLock` и попадает в кэш) — оставил форком для ревьюера. ## Checklist - [x] критерии приёмки из #435 выполнены - [x] вне заявленного scope ничего не менялось
agent_coder added 1 commit 2026-07-10 05:22:27 +03:00
CollabSession-реестр (#400/#431) ключуется на `wsUrl + " " + pageId + " " +
token`. Клиент чеканил СВЕЖИЙ collab-токен на КАЖДУЮ мутацию (in-app —
пере-подписывает JWT с новым iat/exp раз в секунду; внешний MCP — POST
/auth/collab-token на каждый вызов), поэтому token-компонент ключа менялся
каждый раз и сессия почти никогда не переиспользовалась (connect-штормы, 25s
таймауты, зомби-сессии).

- новое пер-инстансное поле collabTokenCache {token, mintedAt};
- getCollabTokenWithReauth(forceRefresh=false) сначала смотрит кэш (гейт
  !forceRefresh && ttl>0 && свежесть), оборачивает ОБЕ ветки чеканки
  (provider-путь in-app агента И REST POST /auth/collab-token), обе через
  rememberCollabToken (пустой токен не кэшируется);
- readCollabTokenTtlMs() читает env MCP_COLLAB_TOKEN_TTL_MS свежо; дефолт 5
  мин (сильно ниже 24h жизни токена и <= max-age сессии, окно устаревания
  прав не расширяется сверх #431); ЯВНЫЙ 0/отрицательное отключают кэш
  (rollback-knob = точный fetch-per-call), unset/непарсибельное -> дефолт;
- reauth-ретрай (401/403) в обеих ветках рекурсит forceRefresh=true (обход
  кэша -> свежий токен), гард !forceRefresh ограничивает ровно одним
  повтором;
- кэш сбрасывается на КАЖДОЙ смене идентичности: в login() и в
  response-интерцепторе (где this.token зануляется перед пере-логином) —
  инвариант-4 (изоляция идентичностей) сохранён, кэш пер-клиентный.

Внутренний цикл: 1 проход внутреннего ревью (APPROVE WITH SUGGESTIONS);
правка по ревью — уточнён docstring readCollabTokenTtlMs (расхождение с
поведением: непарсибельное значение даёт дефолт 5мин с ВКЛючённым кэшем, а
не отключает; отключает только явный 0/отрицательное).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-10 05:22:28 +03:00
Collaborator

Ревью — #439 perf(mcp): кэш collab-токена в клиенте — чтобы кэш CollabSession (#400) реально попадал (#435)

Вердикт: PASS

Полный веер из 9 аспектов по всему диффу PR (merge-base ee33a293). Гейт зелёный (clean-room /home/claude/gm-ci): mcp build (tsc) 0, node --test 571/571 (+282 collab-token-cache). drawio-wire коммит (f46d89ea) — тот же, что в #438, уже проверен (дрейф-гард зелёный).

Безопасность токен-кеша — все guard'ы проверены (клик)
  • Per-user изоляция (КРИТИЧНО) — PASS: токен-кеш это ПРИВАТНОЕ instance-поле DocmostClient; клиент строится СВЕЖИМ на каждый запрос/чат-ход (buildDocmostClient per-call, не пул/синглтон) → кеш умирает с запросом. Статик/модульного кеша нет. Токен одного юзера не утечёт другому.
  • Сброс на смене identity: кеш чистится в login() — токен не переживает смену личности (тест 9). На 401/403 — кеш очищается (interceptor) + getCollabTokenWithReauth(forceRefresh=true) минует кеш (тест 4).
  • Окно устаревания прав НЕ расширяется: проверил — client-side кеширование НЕ даёт доп. доступа: свежий и кешированный токен — оба валидные JWT на юзера, дают ИДЕНТИЧНЫЙ доступ; enforcement на collab-СЕРВЕРЕ, независим от клиентского кеша. #439 лишь избегает ре-минта эквивалентного токена. TTL 5 мин << 24ч жизни токена, комментарий (client.ts:185) точен.
  • Токен не логируется: только DEBUG-превью (15 символов + «...»), ошибки очищены.
  • Coherence (#435): стабильный токен 5 мин → ключ CollabSession (wsUrl,pageId,token) стабилен → registry-хит → сессия переиспользуется (mint-once в TTL — тест 6). Цель достигнута: токен-черн, ломавший кеш (#400/#431), устранён.
  • Stability: TTL-математика (Date.now()-mintedAt<ttl) верна, без await-гэпов check→use; service-account/stdio путь сохранён (тесты 8-9). Architecture (per-instance + env-TTL как #431), conventions, documentation, simplification — LGTM. Rollback-кнопка (MCP_COLLAB_TOKEN_TTL_MS=0 отключает) протестирована (тест 3).
DROP — кодеру НЕ делать (лог калибровки)

Security/correctness покрыты (изоляция, re-auth, login-clear, coherence, rollback-0); остаток — perf-completeness edges:

  • [below-threshold] low/med [test-coverage] env-парсинг garbage/negative не тестируется отдельно: Number.isFinite(raw) ? Math.max(0,raw) : 5min — импл верна, load-bearing кейс (0 отключает) ПОКРЫТ; непокрыты «abc»/«5min»→дефолт-ON и «-1»→off (вторичные, при поломке = перф-регресс, не корректность/безопасность). Рекомендация: добавить кейсы (env="abc"/"-1"/"" ) — дёшево.
  • [below-threshold] low/med [stability] конкурентные getCollabTokenWithReauth() на пустом кеше оба минтят (нет dedup как у loginPromise). Смягчено: #431 сериализует мутации одной страницы через withPageLock → фактически последовательно → кеш-хит; оба токена валидны. Perf-edge, не баг.

DO/ESCALATE нет. Токен-кеш безопасен (per-user изоляция instance-scoped, re-auth/login чистят кеш, окно прав не расширено), цель #435 достигнута и покрыта, гейт зелёный. Approve.

9 аспектов. Per-user изоляция (per-request инстанс, нет статика), re-auth/login-clear, окно-прав-не-расширено, coherence #435 — проверены (+ комментарий staleness сверен мной). Остаток — perf-edge покрытие (env-garbage/concurrent), below-threshold. Escalate нет.

<!-- state:review reviewed_head=574267de round=1 verdict=approved --> ## Ревью — #439 `perf(mcp): кэш collab-токена в клиенте — чтобы кэш CollabSession (#400) реально попадал (#435)` **Вердикт: PASS** ✅ Полный веер из 9 аспектов по всему диффу PR (merge-base `ee33a293`). Гейт зелёный (clean-room `/home/claude/gm-ci`): mcp build (tsc) 0, **node --test 571/571** (+282 `collab-token-cache`). drawio-wire коммит (f46d89ea) — тот же, что в #438, уже проверен (дрейф-гард зелёный). <details> <summary>Безопасность токен-кеша — все guard'ы проверены (клик)</summary> - **Per-user изоляция (КРИТИЧНО) — PASS**: токен-кеш это ПРИВАТНОЕ instance-поле `DocmostClient`; клиент строится СВЕЖИМ на каждый запрос/чат-ход (`buildDocmostClient` per-call, не пул/синглтон) → кеш умирает с запросом. Статик/модульного кеша нет. Токен одного юзера не утечёт другому. - **Сброс на смене identity**: кеш чистится в `login()` — токен не переживает смену личности (тест 9). На 401/403 — кеш очищается (interceptor) + `getCollabTokenWithReauth(forceRefresh=true)` минует кеш (тест 4). - **Окно устаревания прав НЕ расширяется**: проверил — client-side кеширование НЕ даёт доп. доступа: свежий и кешированный токен — оба валидные JWT на юзера, дают ИДЕНТИЧНЫЙ доступ; enforcement на collab-СЕРВЕРЕ, независим от клиентского кеша. #439 лишь избегает ре-минта эквивалентного токена. TTL 5 мин << 24ч жизни токена, комментарий (client.ts:185) точен. - **Токен не логируется**: только DEBUG-превью (15 символов + «...»), ошибки очищены. - **Coherence (#435)**: стабильный токен 5 мин → ключ CollabSession `(wsUrl,pageId,token)` стабилен → registry-хит → сессия переиспользуется (mint-once в TTL — тест 6). Цель достигнута: токен-черн, ломавший кеш (#400/#431), устранён. - Stability: TTL-математика (`Date.now()-mintedAt<ttl`) верна, без await-гэпов check→use; service-account/stdio путь сохранён (тесты 8-9). Architecture (per-instance + env-TTL как #431), conventions, documentation, simplification — LGTM. Rollback-кнопка (`MCP_COLLAB_TOKEN_TTL_MS=0` отключает) протестирована (тест 3). </details> <details> <summary>⛔ DROP — кодеру НЕ делать (лог калибровки)</summary> Security/correctness покрыты (изоляция, re-auth, login-clear, coherence, rollback-0); остаток — perf-completeness edges: - `[below-threshold]` `low/med` **[test-coverage]** env-парсинг garbage/negative не тестируется отдельно: `Number.isFinite(raw) ? Math.max(0,raw) : 5min` — импл верна, load-bearing кейс (0 отключает) ПОКРЫТ; непокрыты «abc»/«5min»→дефолт-ON и «-1»→off (вторичные, при поломке = перф-регресс, не корректность/безопасность). Рекомендация: добавить кейсы (env="abc"/"-1"/"" ) — дёшево. - `[below-threshold]` `low/med` **[stability]** конкурентные `getCollabTokenWithReauth()` на пустом кеше оба минтят (нет dedup как у `loginPromise`). Смягчено: #431 сериализует мутации одной страницы через `withPageLock` → фактически последовательно → кеш-хит; оба токена валидны. Perf-edge, не баг. </details> DO/ESCALATE нет. Токен-кеш безопасен (per-user изоляция instance-scoped, re-auth/login чистят кеш, окно прав не расширено), цель #435 достигнута и покрыта, гейт зелёный. Approve. <sub>9 аспектов. Per-user изоляция (per-request инстанс, нет статика), re-auth/login-clear, окно-прав-не-расширено, coherence #435 — проверены (+ комментарий staleness сверен мной). Остаток — perf-edge покрытие (env-garbage/concurrent), below-threshold. Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 05:36:34 +03:00
vvzvlad merged commit 199fc9aa21 into develop 2026-07-10 07:01:34 +03:00
Sign in to join this conversation.