На 31 июля 2026 года строгий аудит проходит 184 из 358 созданных материалов. Остальные 174 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
На 31 июля 2026 года строгий аудит проходит 187 из 358 созданных материалов. Остальные 171 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
- Голос: М6, февраль 2023. Системный практик называет владельца состояния, цену ошибки, условие отказа и отдельный способ проверки; без выдуманных инцидентов, метрик и опыта эксплуатации.
- Общая модель: browser cookie переносит непрозрачный ID; server record и lineage pointer решают, какой ID current. Rotation переводит predecessor в `rotated`; logout-current переводит current record в `revoked` и возвращает clear cookie. Другой scope (`logout-lineage`, `logout-all`) намеренно не подменяется этим правилом.
- Граница: sidecar не делает HTTP, browser storage/network trace, browser cookie-jar, криптографию, random ID, clock, базу, конкурентный доступ, authz, CSRF или production проверку. Все ID, время и cookie policy заданы в памяти; `fixture-s-*` не являются образцом production secret.
- Созданы ровно пять файлов этой партии: этот review, `web/scripts/upgrade-2023-02.mjs` и три SVG с префиксом `sessions-auth-2023-`. Registry, README, `articles.json`, очередь, Git, существующие материалы и чужие untracked-файлы не менялись.
## Проход 1 — факты и модель
| Статья | Главный тезис | Первичные или официальные источники | Ограничение модели | Fixture |
| --- | --- | --- | --- | --- |
| `editorial-2023-02-practice-sessions-auth` | Cookie — носитель ID, а server record — источник валидности; rotation и logout меняют оба слоя по одному контракту. | IETF RFC 6265 (04.2011); IETF draft-ietf-httpbis-rfc6265bis-11 (07.11.2022, work in progress); NIST SP 800-63B (06.2017, updates through 02.03.2020). | Нет браузера, сети, таймера, crypto, secret store и гонок. | да |
| `editorial-2023-02-mechanism-sessions-auth` | Secure, HttpOnly, SameSite, Path и `__Host-` задают узкий scope доставки; server status/current остаются отдельной проверкой. | Те же три датированных первичных/официальных документа. | Flags не доказывают authz, CSRF-защиту, реальный TTL или browser compatibility. | да |
| `editorial-2023-02-field-sessions-auth` | Для stale ID надо различать unknown, rotated и revoked, а logout scope назвать до изменения endpoint. | Те же три датированных первичных/официальных документа. | Нет incident/browser trace, production HTTP response или данных о конкретном сервисе. | да |
- Фактологические оговорки сверены с официальными страницами: RFC 6265 — Standards Track от апреля 2011; draft-11 опубликован 7 ноября 2022 и везде назван work in progress, а не финальным RFC; PDF NIST SP 800-63B датирован июнем 2017 с обновлениями до 2 марта 2020. Все источники существовали не позднее 2023-02.
-`createTeachingSessionState()` создаёт один active record, а`rotateTeachingSession()` принимает только current ID, переводит predecessor в `rotated`, создаёт successor и двигает pointer lineage. Fixture проверяет 15 invariants, включая отсутствие `fixture-s-3` после stale rotation и сохранение `fixture-s-2` active после stale logout.
-`logoutTeachingSession()` в выбранной политике принимает только current active ID, revoke-ит server record, удаляет pointer и создаёт clear cookie той же формы с `Expires` в прошлом. Stale logout старого ID не заявлен как универсальное правило: статьи явно отделяют `logout-current` от `logout-lineage` и `logout-all`.
- Не заявлены browser trace, запуск endpoint, настоящая конкуренция, эффект в production, измерение, инцидент или безопасность конкретной системы. Код и текст прямо ограничивают in-memory fixture.
Вердикт: пройти. Источники, статусы документов, переходы модели и границы утверждений совпадают.
## Проход 2 — язык, полнота и объём
| Статья | Знаки основного текста по `audit:draft` | Проблема и цена в первых 2 абзацах | Артефакты | Голос и анахронизмы |
| --- | ---: | --- | --- | --- |
| `editorial-2023-02-practice-sessions-auth` | 8 903 | да | таблица, JS fixture, lifecycle SVG, ordered route, ограничения, 3 источника | М6: контракт state/cookie, без метрик и выдуманного опыта |
| `editorial-2023-02-mechanism-sessions-auth` | 8 547 | да | таблица слоёв, JS example, cookie-contract SVG, ordered route, ограничения, 3 источника | М6: условия и границы flags, draft назван draft |
| `editorial-2023-02-field-sessions-auth` | 8 158 | да | диагностическая таблица, JS fixture, stale-events SVG, ordered route, ограничения, 3 источника | М6: не выдана гипотеза о вкладках за trace |
- Все три текста находятся в требуемом узком диапазоне 8–10 тыс. знаков основного текста и в общем стандарте 5–15 тыс.; источники не учитываются в объёме.
- В каждой статье есть упорядоченный маршрут «симптом → причина → проверка → действие», а также отдельный шаг отката, который не оживляет stale ID.
- Каждый обязательный артефакт присутствует: доступная таблица, воспроизводимый фрагмент/fixture, содержательная figure с `alt` и caption, ограничения модели и минимум две ссылки на первичные/официальные источники.
- Удалены общие оценки и шаблонные обороты. Термины `current`, `lineage`, `stale`, `rotation` привязаны к полям и переходам, а `__Host-`, Secure, HttpOnly, SameSite и Path объяснены через конкретную область действия и то, что они не решают.
Вердикт: пройти. Тон соответствует системному практику 2022–2024: короткая прагматичная техническая речь, проверяемый следующий шаг и честные ограничения.
## Проход 3 — SVG, безопасность и выпуск
| Asset | Назначение | `alt` и caption | XML и safety scan | Sharp 375 px |
| --- | --- | --- | --- | --- |
| `sessions-auth-2023-lifecycle.svg` | active → rotated → successor → revoked lifecycle | да | PASS | PASS, 375×238 |
| `sessions-auth-2023-cookie-contract.svg` | browser delivery против server validation | да | PASS | PASS, 375×238 |
| `sessions-auth-2023-stale-events.svg` | stale rotation/logout не меняют successor | да | PASS | PASS, 375×238; заголовок сокращён после первого просмотра, обрезания нет |
-`cd web && npm run audit:draft -- scripts/upgrade-2023-02.mjs` — PASS для всех трёх slug. `npm` вывел три предупреждения о существующих unsupported user config, но аудит завершился успешно.
- Import-safe export — PASS: ровно 3 revisions, у каждой отсутствуют поля `date` и `author`.
-`xmllint --noout` — PASS для всех SVG. Safety scan не нашёл `script`, `foreignObject`, event attributes, `href`, `javascript:`, `data:` или `<image>`.
- Sharp отрендерил все SVG на ширине 375 px; выполнен ручной просмотр PNG. В SVG нет внешних URL, интерактивного поведения, пользовательского ввода или raster content.
Вердикт: пройти. Проверки подтверждают только sidecar-пакет и статические visual assets; публикация, registry update, production build, commit и push не выполнялись.
## Интеграционная приёмка
- Основной редактор заменил учебное удаление cookie с `Max-Age=0` на `Expires` в прошлом: это соответствует примеру RFC 6265 для удаления и не выдаёт спорную производительскую форму за универсальный шаблон. Fixture после изменения — PASS 15/15.
- Ревизии добавлены в `web/data/editorial-revisions.mjs` поверх трёх исходных slug. `web/data/articles.json`, даты и авторы не изменялись; в registry 178 уникальных ревизий без дубликатов.
- Строгий аудит `npm run audit:articles -- editorial-2023-02-practice-sessions-auth editorial-2023-02-mechanism-sessions-auth editorial-2023-02-field-sessions-auth` — PASS: у каждой статьи 1 figure, 1 table и 1 code example.
- Production-сборка `cd web && npm run build` — PASS: 374 статические страницы. README обновлён с 184/358 до 187/358 принятых материалов; осталось 171.
## Итог
Статус: готово к отдельному коммиту.
Изменения после self-review: уменьшен основной текст до 8–10 тыс. знаков; после Sharp-проверки сокращены title/subtitle третьей SVG, чтобы они не обрезались на 375 px.
<titleid="sessions-auth-2023-cookie-contract-title">Контракт cookie и серверной сессии</title>
<descid="sessions-auth-2023-cookie-contract-desc">Cookie с именем __Host-session несёт непрозрачный ID. Secure, HttpOnly, SameSite, Path и отсутствие Domain ограничивают доставку в браузере. Сервер отдельно проверяет status, current lineage и expiry.</desc>
<titleid="sessions-auth-2023-lifecycle-title">Жизненный цикл одной учебной сессии</title>
<descid="sessions-auth-2023-lifecycle-desc">Вход создаёт active session fixture-s-1. Rotation выводит её в rotated и создаёт fixture-s-2 active. Stale операции старого ID отклоняются. Logout текущего ID revoke-ит запись и возвращает clear cookie.</desc>
<titleid="sessions-auth-2023-stale-events-title">Stale rotation и logout в одной учебной lineage</title>
<descid="sessions-auth-2023-stale-events-desc">Сервер заменяет fixture-s-1 на fixture-s-2. Повторная rotation и logout старого ID отвергаются и не меняют successor. Logout нового ID revoke-ит его и формирует clear cookie.</desc>
note:'стандарт IETF, опубликованный в 2011 году. Он задаёт передачу Cookie/Set-Cookie, scope, Secure и HttpOnly; он не задаёт прикладной срок валидности серверной сессии.',
},
{
title:'IETF draft-ietf-httpbis-rfc6265bis-11: Cookies: HTTP State Management Mechanism, November 2022',
note:'датированный Internet-Draft, опубликованный 7 ноября 2022 года и доступный к февралю 2023. Это work in progress, а не финальный RFC; здесь он нужен для SameSite и префикса __Host-.',
},
{
title:'NIST SP 800-63B: Digital Identity Guidelines — Authentication and Lifecycle Management, June 2017 with updates through 2 March 2020',
note:'официальный датированный документ NIST, доступный к февралю 2023. Его требования относятся к заданному NIST контексту; он помогает отделить cookie от серверного timeout, logout и reauthentication.',
excerpt:'Учебный контракт одной сессии: cookie выбирает идентификатор, сервер хранит состояние, rotation выводит старый ID из обращения, logout завершает оба слоя.',
readingMinutes:12,
},[
p('В обработчике входа часто видны четыре независимые строчки: выдать cookie, поставить Secure, обновить ID и сделать logout. Проблема начинается, когда каждая строчка живёт по своей логике. Старый идентификатор продолжает проходить после rotation, cookie очищается без серверного revoke или срок в браузере принимают за срок доступа. Цена ошибки конкретна: пользователь считает сессию закрытой, а сервер ещё принимает её; либо новый вход ломается поздним ответом со старым состоянием.'),
p('Ниже — один контракт учебной сессии: браузер хранит непрозрачный ID, сервер хранит статус, и оба слоя меняются одним решением. После входа запись active; rotation выпускает successor и выводит predecessor из обращения; logout revoke-ит current ID и просит клиент удалить cookie. Это не browser trace и не запуск приложения, а in-memory fixture с явно заданными состояниями и идентификаторами.'),
h2('Сначала разделите носитель и источник истины'),
p('Cookie не является сессией. Это способ вернуть серверу строку в заголовке Cookie, если браузер решил, что её scope подходит запросу. RFC 6265 описывает именно такую передачу и допускает, что user agent удалит cookie раньше срока. Поэтому Max-Age полезен как клиентский предел хранения, но не должен быть единственной проверкой timeout. В контракте active, rotated и revoked принадлежат серверной записи; cookie только выбирает запись, которую сервер ещё обязан проверить.'),
p('Это разделение меняет и формулировку logout. Удалить cookie недостаточно: другой клиент, сохранённый запрос или уже отправленный заголовок может всё ещё предъявить идентификатор. Сервер должен решить, принимает ли он его. В учебной модели после logout запись получает статус revoked и исчезает из currentByLineage. Отдельный Set-Cookie с Expires в прошлом просит браузер убрать прежнее значение. Оба действия нужны, но они доказывают разное: первое закрывает доступ в модели, второе убирает удобный носитель на клиенте.'),
table('Контракт одной сессии: кто владеет решением',['Объект','Минимальное поле','Кто проверяет','Что происходит при logout','Чего этого недостаточно'],[
['Cookie','__Host-session=ID; Path=/; Secure; HttpOnly; SameSite=Lax','user agent решает, приложить ли его к запросу','получает Set-Cookie с Expires в прошлом той же формы','доказать, что сервер уже revoke-ил ID'],
['Session record','id, lineage, generation, status','сервер перед обработкой защищённого действия','current ID становится revoked','принять решение за другой сервис без общей политики'],
['Lineage pointer','lineage → active ID','операция rotation/logout','ссылка на active ID удаляется','описать logout всех устройств'],
['Set-Cookie при rotation','successor ID и та же политика flags','сервер формирует ответ','не выдаётся при stale reference','синхронизировать вкладки или порядок сетевых ответов'],
]),
h2('Минимальный fixture: проверить переходы, не притворяясь браузером'),
p('Скрипт этой партии экспортирует createTeachingSessionState, rotateTeachingSession и logoutTeachingSession. Первый вызов создаёт fixture-s-1 с состоянием active. Rotation принимает только текущий ID для линии fixture-lineage-a, помечает его rotated и создаёт fixture-s-2. Поздняя операция со старым ID возвращает stale-session и получает тот же объект state. Это намеренно узкая проверка: нет HTTP, хранилища браузера, случайных чисел, конкурентных запросов, clock skew, пользователя или настоящего сервера.'),
code(fixtureExample),
p('Здесь stale logout для fixture-s-1 не revoke-ит fixture-s-2: это политика logout только предъявленной current-сессии. Другой продукт может выбрать logout всей lineage или всех устройств, но это должна быть отдельная операция с собственным scope и проверкой. Иначе поздний запрос молча закроет другую активную сессию.'),
h2('Rotation — замена права, а не продление строки'),
p('Rotation начинается с проверки текущей записи, а не с Set-Cookie. Предшественник остаётся rotated для диагностики, но уже не current; successor получает новый ID, generation и ту же lineage. Так сервер различает unknown и уже устаревший ID. В production это изменение требует реальной гарантии хранилища; синхронный fixture не называет себя atomic.'),
p('Для двух одновременных запросов заранее определите один исход: один создаёт successor, другой получает stale/rejected. Без этого появятся два successor. Fixture не моделирует гонку, но фиксирует инвариант для реализации: после rotation один current ID, а повторный старый ID не создаёт fixture-s-3.'),
figure('/assets/editorial/2023/sessions-auth-2023-lifecycle.svg','Жизненный цикл учебной сессии: после входа fixture-s-1 active, rotation переводит его в rotated и выпускает fixture-s-2 active, stale операции со старым ID отклоняются, logout текущего ID делает запись revoked и очищает cookie.','Схема показывает контракт одного идентификатора и одной lineage в памяти. Это не trace браузера, не последовательность настоящих HTTP-ответов и не модель нескольких устройств.'),
h2('Cookie flags описывают область доставки, не авторизацию'),
p('Для учебного ID выбран __Host-session. В draft-ietf-httpbis-rfc6265bis-11 префикс __Host- требует Secure, Path=/ и отсутствия Domain, то есть фиксирует host-only область. Он не обещает пользователя, права, logout или серверный срок. В феврале 2023 это был Internet-Draft ноября 2022, не финальный RFC. __Host- не годится, если cookie обязана жить на нескольких поддоменах.'),
p('Secure ограничивает канал доставки, HttpOnly — доступ из browser API, SameSite=Lax — часть cross-site доставок. Ни один не заменяет серверную проверку ID, прав и запроса. Path задаёт маршрут, но не является security boundary. Поэтому каждый flag записывают с его узкой причиной, а не как список безопасности.'),
h2('Logout: две синхронные обязанности'),
p('Logout имеет серверную и клиентскую ветви. Сервер revoke-ит только current active ID и снимает pointer; клиент получает то же имя и Path=/ с пустым значением и Expires в прошлом. RFC 6265 связывает замену/удаление с name, Domain и Path: если issue содержал Domain, а clear нет, браузер может оставить другую cookie. Учебный контракт исключает Domain с самого начала.'),
p('Удаление cookie не доказывает, что logout дошёл до сервера: ответ мог не примениться, а другой запрос уже нести ID. Критерий доступа — server reject после revoke. Очищение остаётся нужно для UX. CSRF-защита logout, если она есть, — отдельный контракт; SameSite не отменяет его.'),
h2('Маршрут: симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> В выписке change найдите одно из трёх наблюдений: старый ID принимается после renewal, logout меняет только интерфейс или cookie живёт дольше серверной записи. Не называйте это инцидентом, пока нет проверяемых данных.',
'<strong>Причина.</strong> Для одного ID нарисуйте record, lineage pointer, Set-Cookie при issue/rotation и Set-Cookie при clear. Если один из объектов не имеет владельца, контракт уже разорван.',
'<strong>Проверка fixture.</strong> Запустите <code>node web/scripts/upgrade-2023-02.mjs --verify-fixture</code>. Он проверяет только 15 предзаданных assertions учебной модели, включая stale rotation и stale logout; он не проверяет браузер, endpoint или конфигурацию.',
'<strong>Проверка реализации.</strong> Отдельно выберите разрешённую среду и проверьте, что сервер отклоняет rotated/revoked ID, а clear cookie повторяет name, Domain при наличии и Path исходной cookie. Не переносите в отчёт реальные session ID.',
'<strong>Действие.</strong> Сделайте server-side status обязательным до защищённой операции; rotation обновляет current pointer, logout revoke-ит current pointer. Добавьте явное отдельное действие, если бизнесу нужен logout всех сессий.',
'<strong>Откат.</strong> Не используйте fixture как план отката. Для production заранее опишите совместимость старых ID, миграцию хранилища, ожидаемый reject и способ вернуть change без повторной легитимации stale идентификатора.',
]),
h2('Границы и следующий проверяемый шаг'),
p('Модель не генерирует секреты: fixture-s-1 и fixture-s-2 нельзя копировать как production ID. В ней нет шифрования, CSRF, пользователя, reauthentication, browser Network/Storage и реальной гонки. NIST SP 800-63B помогает отделить session secret, минимальный cookie scope и server timeout; это контекст документа NIST, а не готовая политика продукта.'),
p('Следующий шаг — зафиксировать для команды status, lineage, reject rotated/revoked ID и отдельный план logout всех устройств. Затем провести разрешённую интеграционную проверку вне fixture. Если нельзя назвать current ID после rotation, cookie-конфигурации ещё рано доверять.'),
h2('Историческая граница февраля 2023'),
p('К февралю 2023 уже существовали RFC 6265 от апреля 2011, NIST SP 800-63B от июня 2017 с обновлениями до 2 марта 2020 и draft-ietf-httpbis-rfc6265bis-11 от 7 ноября 2022. Последний в статье назван work in progress, а не стандартом. Источники объясняют свойства cookie и сессий, но не подтверждают состояние конкретного браузера, балансировщика, базы данных или auth-платформы.'),
]);
constmechanism=revision({
slug:'editorial-2023-02-mechanism-sessions-auth',
title:'Cookie, rotation и logout: модель одной сессии без скрытого источника истины',
excerpt:'Разбор границы между cookie и серверной записью: что проверяют Secure, HttpOnly и SameSite, почему rotation меняет current ID, а logout не заканчивается удалением строки в браузере.',
readingMinutes:12,
},[
p('Ошибка в сессиях часто выглядит спором о flag: добавить SameSite, HttpOnly или Max-Age. Проблема глубже, если непонятно, где решается валидность ID после rotation/logout. Тогда один handler верит cookie, другой TTL в базе, третий очищает только клиент. Цена — противоречивый доступ или потеря новой сессии поздней операцией со старым ID.'),
p('Достаточно выбрать один поток и владельца каждого факта. Здесь cookie переносит непрозрачный session ID; server record хранит status и generation; lineage указывает на один active ID. Rotation меняет record и выдаёт cookie, logout revoke-ит record и возвращает clear cookie. Fixture проверяет это в памяти, без браузера, storage/network trace и вывода о реальной платформе.'),
h2('Пять состояний, которые не стоит склеивать'),
p('Фраза «сессия истекла» скрывает разные факты: cookie нет, но record ещё жив; cookie есть, но record revoked; ID известен, но уже не current; доступ отклонён по правам при active сессии. Поздний Set-Cookie тоже не доказывает порядок его применения браузером. Эти состояния нельзя склеивать без риска случайно изменить authorization.'),
p('Модель не выбирает SQL, cache, signed token или framework store. Она требует лишь отличить current от rotated/revoked до защищённого эффекта. Порядок Cookie и похожее имя не должны решать доступ: RFC 6265 не советует полагаться на сериализацию одинаковых cookie.'),
table('Слои одной сессии и вопрос, на который отвечает каждый',['Слой','Вопрос','Данные в учебной модели','Нормальный переход','Неверный вывод'],[
['Аутентификация','кто прошёл вход?','в fixture не представлена','создаёт initial session record','cookie сама доказывает личность'],
['Cookie delivery','какую строку браузер приложит?','__Host-session и flags','issue, rotate, clear','Max-Age решает серверный timeout'],
['Server record','принимать ли этот ID сейчас?','status и generation','active → rotated или revoked','наличие ID равно доступу'],
['Lineage','какой ID является current?','fixture-lineage-a → fixture-s-2','pointer сдвигается при rotation','старый ID можно повторно продлить'],
['Authorization','можно ли выполнить действие?','в fixture не представлена','проверяется отдельно после ID','active session даёт любое право'],
]),
h2('Flags: точные границы вместо списка галочек'),
p('Secure ограничивает канал доставки с точки зрения user agent, но не подписывает server record. HttpOnly закрывает значение от не-HTTP API, но не лечит XSS и не отменяет запросы браузера с cookie. Оба flag описывают рядом с их узким эффектом.'),
p('SameSite=Lax ограничивает часть cross-site доставок. Draft-11 различает Strict, Lax и None; Lax допускает safe top-level navigation помимо same-site запросов. Это защита в глубину, не доказательство корректности mutation endpoint. Embed или federation могут потребовать другой контекст и отдельную проверку server request.'),
p('Max-Age/Expires задают максимум browser хранения, но user agent может удалить cookie раньше. В draft-11 Max-Age приоритетнее Expires; сервер не обязан принимать ID до того же момента. NIST SP 800-63B не предлагает полагаться на expiry cookie для timeout: browser lifetime и server expiry хранят отдельно.'),
h2('Почему __Host- полезен только при подходящем scope'),
p('Имя __Host-session означает не расширять cookie на поддомены. Для conformant user agent префикс требует Secure, Path=/ и отсутствия Domain, поэтому issue и clear имеют один scope. Это не namespace безопасности и не замена ревью hostnames. Если cookie нужна на нескольких поддоменах, __Host- не подходит: Domain и его владельцев надо описать явно.'),
p('Path=/ даёт cookie доступ на всём host и выполняет условие префикса, но не делает путь security boundary. Разделение административной области обеспечивает server authz и архитектура hostnames.'),
figure('/assets/editorial/2023/sessions-auth-2023-cookie-contract.svg','Контракт учебной cookie __Host-session: Secure ограничивает отправку защищённым каналом, HttpOnly скрывает значение от JavaScript API, SameSite=Lax ограничивает часть cross-site доставок, Path=/ и отсутствие Domain нужны для __Host-, а серверная запись отдельно проверяет active status.','Рисунок разделяет browser delivery и server validation. Он не показывает совместимость конкретного браузера, реальный заголовок ответа или достаточность защиты от CSRF.'),
h2('Rotation: сохранить lineage, заменить предъявляемый ID'),
p('Rotation меняет значение, которое сервер считает current. Fixture оставляет fixture-s-1 rotated, добавляет fixture-s-2 generation=2 и переписывает pointer. Старый ID получает stale-session, а не новый TTL: сервер не смешивает известный, но заменённый ID с действующим.'),
p('Реализация должна определить линейзацию: транзакцию, conditional update, compare-and-swap или другую гарантию хранилища. Иначе два запроса выпустят два successor. Fixture не моделирует параллелизм, но его staleRotationDoesNotCreateAnotherSession — требование к реальному механизму.'),
h2('Logout: явный scope важнее красивого 204'),
p('Scope logout называют до кода. Logout-current revoke-ит один record; logout-all ищет все records identity; logout-lineage лежит между ними. Их нельзя скрыть за единым endpoint без явного правила: stale запрос иначе либо оставит доступ, либо выключит новую сессию.'),
p('Fixture выбирает logout-current: fixture-s-1 после rotation получает stale-session и не трогает fixture-s-2. Это инвариант выбранного contract, не правило для любого сервиса. Если нужен другой эффект, создают logout-lineage/logout-all и проверяют его последствия отдельно.'),
h2('Проверяемый код и ожидаемые границы'),
p('Пример воспроизводим, потому что не зависит от времени, сети или secret store. В нём нет пользователя, браузерных API и настоящего Set-Cookie parser. Он проверяет только переходы: rotation меняет pointer, stale операции не изменяют successor, current logout revoke-ит record и формирует clear cookie.'),
if (oldIdLogout.accepted) throw new Error('stale logout changed successor');
if (newIdLogout.state.records['fixture-s-2'].status !== 'revoked') {
throw new Error('current logout did not revoke record');
}`),
p('Для полного набора assertions используется команда <code>node web/scripts/upgrade-2023-02.mjs --verify-fixture</code>. До запуска в проекте прочитайте сами assertions: fixture не умеет обнаружить неверный reverse proxy, заголовки framework, маршрут logout, race в базе или поведение browser cookie jar. Он удобен как короткий договор на review, но не как тест, который заменяет интеграционный сценарий. Производственный тест должен иметь разрешённую среду, безопасные тестовые данные и отдельно названные наблюдения.'),
h2('Маршрут: симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> Найдите место, где код заключает «cookie есть — сессия валидна», или «Max-Age истёк — сервер уже всё закрыл». Это наблюдаемый пробел контракта, а не диагноз всей системы.',
'<strong>Причина.</strong> Разделите browser delivery, server record, lineage и authorization. Для каждого напишите один владелец и один момент изменения.',
'<strong>Проверка flags.</strong> Сверьте name, Secure, HttpOnly, SameSite, Path, Domain и client lifetime с нужным scope. Отдельно запишите, что каждый флаг не решает.',
'<strong>Проверка rotation.</strong> В разрешённом тесте отправьте старый ID после successful replacement и убедитесь, что он не становится current. Для маленького договора сначала выполните fixture.',
'<strong>Действие.</strong> Сделайте серверную проверку status/current обязательной перед защищённым эффектом; определите один явный механизм линейзации rotation и отдельные операции logout-current, logout-lineage или logout-all.',
'<strong>Откат.</strong> Не продлевайте retired ID ради быстрого отката. Подготовьте совместимую ветку или migration plan, которые сохраняют reject старого ID и позволяют объяснить состояние сессии пользователю.',
]),
h2('Ограничения модели и следующий шаг'),
p('Статья не утверждает, что __Host- или SameSite=Lax подходят каждому приложению. В ней нет оценки AAL, криптографии, устройств, distributed store, CORS/CSRF, browser trace и измерения. fixture-s-1, fixture-t0 и 900 секунд учебные. IETF/NIST задают словарь и ограничения, не принимают за команду решение о scope, UX или гонках.'),
p('Следующий шаг — выписать для одного protected handler допустимые status, смысл rotated, владельца lineage и logout scope, затем добавить отдельный интеграционный сценарий с безопасными тестовыми данными. Если client expiry смешан с server revocation, сначала исправляют эту границу.'),
h2('Историческая граница февраля 2023'),
p('Все три источника в конце существовали не позднее февраля 2023: RFC 6265 опубликован в 2011, NIST SP 800-63B — в 2017 с обновлениями 2020 года, а draft-ietf-httpbis-rfc6265bis-11 — 7 ноября 2022. В частности, draft-11 остаётся work in progress и приведён именно с этим статусом. Ссылки нужны для проверки описанной модели, а не как свидетельство запуска или конфигурации неизвестного сервиса.'),
]);
constfield=revision({
slug:'editorial-2023-02-field-sessions-auth',
title:'Старый session ID после rotation: маршрут проверки logout без выдуманного trace',
excerpt:'Диагностический маршрут для случая, когда logout, renewal и cookie выглядят как разные события: как собрать факты, отделить stale ID от текущего и не объявить fixture browser trace.',
readingMinutes:12,
},[
p('Симптом часто звучит так: «в одной вкладке вышли, в другой ещё есть доступ» или «после renewal разлогинило». Из этого не видно, какой ID пришёл на сервер, какой был current и какой logout scope выбран. Цена правки наугад — старая сессия остаётся принятой либо поздний logout закрывает новую.'),
p('Ниже — маршрут, не отчёт об инциденте: нет пользователя, вкладок, Network/Storage или HTTP-запуска. Есть fixture одной lineage: fixture-s-1 становится rotated, fixture-s-2 active, старый ID stale, logout нового ID revoke-ит его. Это задаёт вопросы для реального расследования, не выдумывает trace.'),
h2('Соберите четыре факта до изменения флага'),
p('Начинать стоит с формата записи: безопасный surrogate ID, server-side status в момент обработки, lineage/current ID и тип операции. Полный bearer secret в лог не попадает. Нужна корреляция, по которой reviewer сопоставит request с решением, но не повторит сессию.'),
p('Отделяйте факт от интерпретации. «Cookie не удалена» требует разрешённой проверки клиента; «logout не дошёл» — evidence запроса или server record; «старый ID прошёл» — статус именно старого ID. Если данных нет, причина не подтверждена: fixture подсказывает инвариант, не историю браузера.'),
table('Симптом → причина → проверка → действие для одной lineage',['Симптом','Вероятная причина','Проверка без секрета','Действие'],[
['После renewal старый ID ещё выполняет защищённое действие','record не стал rotated или handler не проверяет current','сопоставить безопасный ID записи, status и lineage pointer в разрешённой среде','отклонять rotated ID до эффекта и линейризовать rotation'],
['Logout меняет UI, но следующий запрос принят','очищена только cookie или revoke не дошёл до server record','проверить audit event logout и status того же record','revoke server record и вернуть clear cookie одинакового scope'],
['Поздний logout отключает новую сессию','не определён scope, stale ID трактуется как current','сравнить предъявленный ID с currentByLineage на момент обработки','выбрать явный logout-current или logout-lineage и добавить invariant'],
['После clear cookie видна другая cookie того же имени','issue/clear расходятся в Path или Domain','сравнить атрибуты Set-Cookie без значения bearer ID','удалять cookie теми же name, Path и Domain при наличии'],
['Проблема есть только в одном браузере','политика хранения или порядок ответов отличается','повторить разрешённый минимальный сценарий на конкретной версии','не менять server contract до подтверждения различия'],
]),
h2('Старый ID — это не обязательно неизвестный ID'),
p('Различайте unknown, rotated и revoked. Unknown — store не нашёл ID; rotated — запись известна, но не current и имеет successor; revoked — сессия завершена. Внешне они могут дать один 401, но внутри должны отличаться, иначе нельзя доказать, что rotation вывела старый ID из обращения.'),
p('Fixture хранит predecessor ради различия, не как совет хранить records вечно. На период возможных повторов сервер должен отклонять старый ID. RFC 6265 задаёт browser scope доставки; прикладную семантику Cookie определяет сервер.'),
h2('Воспроизводимый fixture и invariants stale событий'),
p('Fixture sidecar создаёт current record, делает accepted rotation, затем повторяет rotate/logout со старым ID. Обе stale операции возвращают accepted=false и оставляют successor active. Logout current ID делает record revoked, удаляет pointer и формирует clear cookie. Есть и положительная, и отрицательная ветви.'),
if (staleRotation.accepted || staleLogout.accepted) {
throw new Error('old ID changed current session');
}
if (replacement.state.records['fixture-s-2'].status !== 'active') {
throw new Error('successor is not active');
}`),
p('Команда <code>node web/scripts/upgrade-2023-02.mjs --verify-fixture</code> запускает полный набор assertions скрипта. Он воспроизводим только потому, что значения и порядок вызовов фиксированы. Его PASS не подтверждает, что web server выдал Set-Cookie, что браузер применил ответ, что база сделала compare-and-swap или что logout endpoint защищён от подделки. В review укажите этот предел рядом с результатом. Иначе маленький тест станет ложным evidence для нескольких неиспытанных уровней системы.'),
figure('/assets/editorial/2023/sessions-auth-2023-stale-events.svg','Маршрут stale событий в учебной lineage: rotation заменяет fixture-s-1 на fixture-s-2, повторная rotation и поздний logout со старым ID получают reject и не меняют successor, logout текущего ID revoke-ит fixture-s-2 и выдаёт clear cookie.','Схема задаёт проектное правило для одного ID и синхронных вызовов в памяти. Она не является browser trace, журналом реального инцидента или доказательством порядка HTTP-ответов.'),
h2('Порядок ответа и порядок решения — разные задачи'),
p('Даже с правильным server reject клиент может получить ответы в неудобном порядке. Два запроса ушли со старым ID, один принёс новый Set-Cookie, другой ошибку. Browser cookie jar и fetch/navigation имеют свои правила; fixture их не моделирует. Реальный дефект требует отдельного разрешённого repro с версиями, шагами, маскированными значениями и HTTP-метаданными.'),
p('Серверный инвариант остаётся: принимается только current active ID; stale событие не создаёт successor и не revoke-ит нового при logout-current. UX после reject выбирает продукт. Не прячьте его бесконечным retry без определения перехода пользователя.'),
h2('Проверьте clear cookie как договор, а не как строку в шаблоне'),
p('Удаление cookie требует того же имени и scope, что issue. В модели clear cookie: __Host-session, Path=/, Secure, HttpOnly, SameSite=Lax и Expires в прошлом. __Host- исключает Domain, поэтому issue/clear не расходятся по поддоменному scope. Если Domain нужен в другом контракте, он обязан быть в документе и проверке очистки; не добавляйте его только на logout.'),
p('Flags clear cookie не отменяют server revoke. RFC 6265 связывает замену с name, domain и path и не советует полагаться на порядок одинаковых Cookie. При нескольких Set-Cookie нужен один owner, один scope и проверка отсутствия дубликата в разрешённом сценарии.'),
h2('Маршрут: симптом → причина → проверка → действие'),
ol([
'<strong>Симптом.</strong> Запишите наблюдаемое последствие без вывода: какой экран, какой внешний status, какая безопасная корреляция операции. Не называйте браузерный trace, если его не собирали.',
'<strong>Причина.</strong> Проверьте гипотезы по очереди: server record не изменился, handler не сверил current, stale logout имеет неясный scope или issue/clear cookies различаются по атрибутам.',
'<strong>Проверка модели.</strong> Выполните fixture и прочитайте assertions staleRotationDoesNotCreateAnotherSession и staleLogoutDoesNotRevokeSuccessor. Это проверка выбранного правила, не production доказательство.',
'<strong>Проверка реализации.</strong> В разрешённой среде соберите ID surrogate, record status, lineage pointer и Set-Cookie attributes без bearer значения. Сверьте момент обработки, а не только время на клиенте.',
'<strong>Действие.</strong> Сделайте status/current обязательным перед защищённым side effect, выберите понятный logout scope и проверяйте clear cookie на совпадение с issue policy.',
'<strong>Откат.</strong> При проблеме UX не возвращайте старый ID в active. Сначала сохраните reject stale ID, затем примените отдельный совместимый rollback для экрана, маршрута или новой политики cookie.',
]),
h2('Ограничения и безопасный результат расследования'),
p('Fixture не хранит секреты, не реализует random ID/TLS/часы и не знает browser API, пользователей, устройств, cache, CSRF или конкурентности. Она не доказывает stale logout в продукте и не выбирает HTTP-ответ. Она фиксирует лишь правило logout-current: старый ID не оживляет и не отменяет successor. Другой scope требует другой fixture.'),
p('Следующий шаг — добавить audit contract: корреляция без bearer secret, server status до protected effect, logout scope и причина reject. Затем проводить отдельный repro только в разрешённой среде. Несобранный факт остаётся неизвестным; это безопаснее правдоподобной, но неповторяемой истории про вкладки.'),
h2('Историческая граница февраля 2023'),
p('Этот маршрут опирается на документы, которые существовали к февралю 2023: RFC 6265 от 2011 года, NIST SP 800-63B от 2017 года с обновлениями до 2020 и draft-ietf-httpbis-rfc6265bis-11 от ноября 2022. Draft не объявляется финальной спецификацией. Ни один из источников не даёт trace конкретного браузера и не заменяет целевую проверку реального logout flow.'),
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.