Skip to content

fix: «Delete account» отзывает грант Sign in with Apple — свежий код с устройства, обмен и /auth/revoke на сервере (#407) - #427

Merged
efymd9 merged 8 commits into
mainfrom
fix/apple-token-revocation-407
Oct 11, 2026

Conversation

@efymd9

@efymd9 efymd9 commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Что сделано и почему

App Store 5.1.1(v): приложение с Sign in with Apple при удалении аккаунта обязано отозвать токены Apple через appleid.apple.com/auth/revoke. «Delete account» (#309) стирал наши данные и Clerk-пользователя, а грант Apple оставался.

Ответ на п.1 спеки — Clerk этого не делает и сделать не может (доказательства — в комментарии к #407, коротко):

  • установленный @clerk/expo 4.7.1 (dist/hooks/useSignInWithApple.ios.js) отдаёт Clerk только identityToken (strategy: "oauth_token_apple"); authorizationCode не читается вовсе;
  • Apple Revoke tokens: token — «refresh token or access token»; их даёт только обмен authorization code (Generate and validate tokens) — Clerk кода не получал, значит токенов у него нет;
  • @clerk/backend 3.22.0: getUserOauthAccessToken → ресурс OauthAccessToken без поля refresh-токена; deleteUser — голый DELETE /users/{id}; документация Clerk про отзыв Apple при удалении молчит.

Поэтому вариант (c) — документированный путь Apple (Offering account deletion):

Изменено после ревью (коммит 291db13)

  1. /devops (.claude/skills/devops/SKILL.md, строка Apple / EAS): три переменные Vercel (прод обязательно, стенд по желанию), исходящий вызов на appleid.apple.com, грабля App Transfer на DEEP ORDINARY LTD — НОВЫЙ ключ в новой команде, новые APPLE_TEAM_ID / APPLE_SIGN_IN_KEY_ID / APPLE_SIGN_IN_PRIVATE_KEY, передеплой.
  2. Маршрут: код Apple без ключа → предупреждение в Sentry (только userId + step); кейс в лог-аудите — кода нет ни в консоли, ни в Sentry, к Apple ничего не ушло.
  3. Маршрут: отзыв Apple идёт параллельно с удалением в Clerk (см. выше); тесты на порядок переписаны, добавлен тест «Clerk спрошен, пока обмен Apple ещё в полёте» и тест «503 Clerk рядом с отзывом → sweep Стирание аккаунта: остаточные дыры самообслуживания (#309) — гонка heal, поздний user.created, несостоявшаяся отмена Stripe, таймаут клиента #336 после обоих».
  4. Приложение: дедлайн 2 мин на проверку доступности + лист Apple (settleOrNull), три теста на фейковых таймерах.
  5. Карта данных: отзыв помечен «когда ключ задан (APPLE_* в Vercel)» в строках Clerk и Apple; Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407 перенесён в «Закрыто:» §5 — «построено; ops-хвост (ключ APPLE_* в Vercel, проверка на устройстве) — docs/registry.md» (как Стирание: вебхук Clerk user.deleted не обрабатывается — данные пользователя переживают удаление аккаунта #161/Ретеншен: ни одной джобы чистки — trial_sessions/visitors/watch_days/show_reminders копятся бессрочно, политика обещает 30 дней и 25 месяцев #162).
  6. CLAUDE.md: шаг Apple в записи api/v1/account/delete/ и в описании account/delete в правиле «Mobile app», lib/apple-revoke.ts в карте lib/.
  7. Реестр Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407: (5) веб-гранты Apple (соединение Clerk на вебе, Services ID) не проверены и отзываются, только если Services ID сгруппирован под tv.matio.app — часть проверки на устройстве; (6) лист Apple показывается и без ключа на сервере (код отбрасывается) — apple вне рубильника, TestFlight тоже, до ключа (строка APP_SOCIAL_SIGNIN и mobile-builds.md приведены в согласие).
  8. Ключевое слово закрытия issue заменено на «Refs»: Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407 остаётся открытой до ключа и проверки на устройстве.

Семантика отмены листа Apple в этом коммите не менялась — её решил владелец, следующий раздел.

Решение владельца: вариант (а) (коммит 415a5d7)

Владелец 11.10 выбрал вариант (а): лист Apple — до последнего необратимого подтверждения, чтобы «Отмена» на ЛЮБОМ шаге значила «пока не удалять». Порядок для аккаунта с Apple на iOS:

  1. первый вопрос «Delete your account?» — тот же текст + одна строка, что дальше Apple попросит «Sign in with Apple» ещё раз (deleteConfirmAppleNote);
  2. лист Apple → askAppleForDeletionCode() отвечает одним из трёх: code / cancelled (только ERR_REQUEST_CANCELED самого Apple) / failed (любая другая ошибка, Apple недоступен, нет кода в ответе, дедлайн 2 мин);
  3. последний вопрос «This can't be undone» → «Delete permanently»;
  4. запрос с кодом.
  • Отмена листа → запроса нет, последнего вопроса нет; спокойное «Your account is still here — Nothing was deleted. You can try again whenever you like.» (es: «Tu cuenta sigue aquí — No se ha eliminado nada. Puedes volver a intentarlo cuando quieras.»), строка снова нажимается.
  • Код получен, затем «Отмена» на последнем вопросе → запроса нет, код просто выброшен.
  • Лист не дал кода (сбой, нет Apple ID на устройстве — сопоставление НЕ проверено, см. «После ревью варианта (а)», п. 2, — дедлайн). Решение: не жёсткий стоп и не тихое удаление. Последний вопрос приходит всё равно, но с другим текстом — «Sign in with Apple didn't go through, so Matio stays connected to your Apple Account — you can remove it yourself in Settings, under Sign in with Apple. Your Matio account is still deleted for good and can't be restored.» (deleteFinalBodyAppleFailed, es тоже); «Delete permanently» удаляет без кода, «Отмена» — ничего. Почему так, а не стоп: сбой — не выбор человека, а у кого лист не работает вовсе (вошёл по коду почты на устройстве без Apple ID, ограничения Экранного времени / MDM, Apple лежит), жёсткий стоп отнял бы единственный способ удалить аккаунт в приложении — это прямо против 5.1.1(v) и ст. 17; грант Apple человек может снять сам, и текст говорит, где. Пожелание владельца «отмена на любом шаге = не удалять» при этом держится: без нажатия «Delete permanently» ПОСЛЕ шага Apple ничего не удаляется, даже если iOS отдаст закрытие листа как ошибку, а не как отмену (тогда человек увидит этот вариант вопроса — с той же кнопкой «Отмена»).
  • Аккаунты без Apple и Android — ровно сегодняшний поток (два вопроса, запрос без тела); askAppleForDeletionCode не вызывается.
  • Код живёт 5 мин: кто просидел над последним вопросом дольше, получит на сервере failed (invalid_grant) — удаление всё равно завершается, грант остаётся (реестр, п. 4).
  • Мобильное: удаление аккаунта в приложении — App Store 5.1.1(v) [после #299] #309 / Стирание аккаунта: остаточные дыры самообслуживания (#309) — гонка heal, поздний user.created, несостоявшаяся отмена Stripe, таймаут клиента #336 / Пред-релизная проверка 0.18.0: 5 низких находок (ложный «удалён» в приложении, pnpm-баннер с адресом, дрейф CLAUDE.md/services.md) #398 (проверки сессии, пути signOut, дедлайн 45 с) не тронуты: deleteOrSay тот же, ему теперь просто передают код; на время листа строка показывает «Please wait…» и не нажимается, как на время запроса.

Тесты (проверено подменой account.tsx на прежний порядок «два вопроса → лист → запрос» с новым API модуля листа: 8 красных; зелёными там остаются только два охранных — отмена первого вопроса и #336 после кода): вкладка «Аккаунт», блок «for an Apple account (#407, option (а))» — порядок «вопрос → лист → вопрос → запрос» (лист приходит после ровно одного диалога), отмена листа = нет запроса + спокойный текст + строка снова работает, отмена последнего вопроса после кода = нет запроса, отмена первого вопроса = листа нет, сбой листа = вопрос с текстом про Apple → подтверждение удаляет без кода / отмена ничего не шлёт, «Please wait» на время листа, #336 после кода, испанский; плюс тест «аккаунт без Apple: листа нет, два вопроса как раньше, запрос без кода». apple-revocation.test.ts переписан под три ответа: cancelled только на ERR_REQUEST_CANCELED, все прочие ошибки / недоступность / отказ проверки / ответ без кода / оба дедлайна → failed, needsAppleSheet — только iOS + Apple.

Документы приведены в согласие: строка реестра #407 (п. 4), docs/services.md, ранбук gdpr-requests.md (две строки), CLAUDE.md (описание account/delete), комментарии в lib/api/types.ts, lib/apple-revoke.ts, маршруте и подпись одного кейса route.test.ts.

После ревью варианта (а) (коммит e68ceb9)

  1. Код чужого Apple ID больше не даёт ложный revoked. Лист подписывает вход Apple ID этого устройства, а он не обязательно тот, с которым создан аккаунт Matio (вошёл на другом iPhone, семейное устройство). Теперь вкладка передаёт в askAppleForDeletionCode список providerUserId внешних аккаунтов apple из Clerk (ExternalAccountResource.providerUserId в @clerk/shared, clerk-js берёт его из provider_user_id; для Apple это sub identity token — та же строка, что credential.user листа); код, чей credential.user не из списка, отбрасывается, ответ — failed, и последний вопрос показывает уже существующий текст «Sign in with Apple didn't go through, so Matio stays connected to your Apple Account…» — для этого случая он честен, новой строки не понадобилось. Нет id от Clerk — не блокируем, код уходит без сверки, как раньше (записано в реестре). Остаток (реестр, п. 4): лист сам мог только что создать грант на том чужом Apple ID — он тоже остаётся; если это важно, следующий шаг — сверка sub обмена на сервере с отзывом лишнего гранта без revoked. Совпадение providerUserId с credential.user подтверждает уже стоящее условие закрытия Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407 — одно удаление из TestFlight дало revoked (при расхождении каждое удаление отбрасывало бы код).
  2. «На устройстве нет Apple ID → failed» — НЕ проверено. Если iOS отдаёт закрытие своего запроса «войдите в Apple ID» как ERR_REQUEST_CANCELED, приложение прочтёт это как отмену листа и на каждой попытке скажет «Your account is still here» — удалить в приложении нельзя. Нового UI сейчас нет (рычаг apple для публичных сборок выключен до проверки Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407). Помечено в docs/registry.md Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407 (п. 4), ранбуке gdpr-requests.md (§4 и таблица процессоров — до исправления оператор удаляет за человека), docs/services.md и комментарии модуля. В условие закрытия Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407 добавлены ворота перед публичной отправкой и перед apple в APP_SOCIAL_SIGNIN: (a) закрыть системный запрос на устройстве без Apple ID; (b) «Отмена» на листе устройства с ДРУГИМ Apple ID; (c) оставить лист открытым дольше 2 минут. Запасной план в строке реестра: если (a) придёт отменой, Alert «still here» получает второе действие, которое открывает последний вопрос с текстом про Apple («Отмена» по-прежнему ничего не удаляет).
  3. Android. У аккаунта с Apple на Android (листа нет — expo-apple-authentication только iOS) последний вопрос теперь с текстом deleteFinalBodyAppleFailed: человек узнаёт, что грант остаётся; первый вопрос без строки про лист. Добавлено в реестр Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407 п. 3, ранбук, docs/services.md, CLAUDE.md (одна фраза в описании account/delete) и /gdpr (SKILL.md, карта данных — сверка id только в памяти устройства, никуда не уходит).

Тесты: apple-revocation.test.ts +9 (linkedAppleUserIds; свой код → код, любой из нескольких привязанных → код, чужой → failed без кода, нет id → код как раньше); вкладка «Аккаунт» +4 (лист получает providerUserId Apple из Clerk и ничего от других провайдеров; через НАСТОЯЩИЙ модуль листа над фейковым нативным: код чужого Apple ID → запрос без кода + текст про Apple, код своего → запрос с кодом; Android → два вопроса, листа нет, последний — текст про Apple, запрос без кода). Падают без изменения: на прежнем account.tsx — 3 красных (передача id, чужой код, Android); без строки сверки в модуле — 2 красных (чужой код в модуле и во вкладке).

Как проверено

  • pnpm lint — exit 0
  • pnpm typecheck — exit 0
  • cd mobile && npm run typecheck — exit 0
  • pnpm test:unit — exit 0, 157 файлов / 2083 теста (после варианта (а), до второго слияния main: 157 файлов / 2128 тестов, exit 0; после слияния fix(deps): Stripe — stripe 23, stripe-js 10, react-stripe-js 7, API 2026-09-30.endive (#433) #441 локально красны только два Stripe-теста версии SDK — общий node_modules главного чекаута ещё на stripe 22, к изменению не относится; арбитр — CI)
  • pnpm qa:styles — exit 0
  • После ревью варианта (а) (e68ceb9): pnpm lint 0 · cd mobile && npm run typecheck 0 · pnpm qa:styles 0 · pnpm typecheck — только тот же TS2322 в lib/stripe.ts (stripe 22 в общем node_modules) · pnpm test:unit — 157 файлов / 2145 тестов зелёные, красны только два Stripe-теста версии SDK (та же причина); арбитр — CI
  • После варианта (а): pnpm lint 0 · pnpm typecheck 0 (до второго слияния main; после слияния fix(deps): Stripe — stripe 23, stripe-js 10, react-stripe-js 7, API 2026-09-30.endive (#433) #441 — один TS2322 в lib/stripe.ts от того же stripe 22 в общем node_modules, не от изменения) · cd mobile && npm run typecheck 0 · pnpm qa:styles 0
  • Покрытие изменённых серверных файлов: lib/apple-revoke.ts — 100% строк / 100% ветвей; app/api/v1/account/delete/route.ts — 100% строк, ветвей и функций (vitest --coverage по этим файлам, после ревью).
  • Тесты падают без фикса: до ревью — 9 тестов красные при заглушке revokeApple и без appleCodeForDeletion; после ревью проверено подменой файлов на версию до ревью: маршрут → 2 красных (runs Apple BESIDE the Clerk delete…, no Apple key in the env … Sentry warning by id), лог-аудит → 1 красный (a code that arrives with no APPLE_* key…), apple-revocation.ts → 2 красных (лист и проверка доступности без ответа).
  • Новые тесты: lib/apple-revoke.test.ts (24); route.test.ts (+11: стирание до Apple, Apple рядом с Clerk, отказ Apple, таймаут, нет env + предупреждение Sentry, нет/битое/пустое тело без Sentry, сбой стирания не тратит код, отказ Clerk рядом с отзывом, 503 Clerk → sweep после обоих); lib/log-audit.test.ts (+3); mobile/src/auth/apple-revocation.test.ts (17, из них 3 — дедлайн листа; после варианта (а)); вкладка «Аккаунт» (+10 после варианта (а)); клиент (+1: код уходит JSON-телом).
  • На устройстве не проверено — ключа Apple в env нет (действие владельца), строка реестра ниже.

Критерии приёмки — по пунктам

  1. Ответ на п.1 с доказательством — да: Clerk не отзывает и не держит токенов Apple для нативного входа (исходники @clerk/expo 4.7.1 / @clerk/shared / @clerk/backend 3.22.0 + документация Apple и Clerk, ссылки выше и в комментарии к issue).
  2. Реализация с тестом (мок Apple) — да: успех, ошибка Apple, таймаут, не настроено; во всех случаях стирание и удаление в Clerk завершаются (route.test.ts, блок «Sign in with Apple's grant (Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407)»).
  3. Кейс лог-аудита — да, три (lib/log-audit.test.ts, «· the Sign in with Apple revocation (Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407)»).
  4. Обновлённая строка реестра — да (docs/registry.md: строка Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407 — «построено, ждёт ключ владельца и проверку на устройстве», пункты 1–6; условие в строке рычага APP_SOCIAL_SIGNIN — «ключ в Vercel + одно удаление из TestFlight дало revoked»).
  5. Секреты только в env Vercel, действие владельца — да, код их только читает.

Отложено (строки в docs/registry.md)

Строка «Sign in with Apple revocation is built but waits for the owner's key, and is unproven on a device» (#407):

  1. APPLE_* не заданы в Vercel — до этого каждое удаление Apple-аккаунта даёт skipped_unconfigured (+ предупреждение в Sentry);
  2. не прогнано с настоящим кодом на устройстве (второй лист при удалении, обмен без redirect_uri, Matio исчезает из «Вход с Apple»);
  3. пути без кода (дашборд Clerk, вебхук user.deleted, pnpm erase-user, «Delete account» в Android-приложении — там последний вопрос предупреждает) отозвать не могут — ранбук gdpr-requests.md §4 говорит человеку снять Matio в настройках Apple ID;
  4. лист Apple — между первым и последним вопросом (вариант (а)): закрытый лист останавливает удаление; лист, не давший кода (сбой, нет Apple ID, дедлайн), ведёт к последнему вопросу с текстом, что Matio останется в Apple ID, и подтверждение удаляет без кода — грант остаётся; так же — код чужого Apple ID (отброшен в приложении; грант, созданный этим листом на чужом Apple ID, тоже остаётся); код старше 5 мин — failed, грант остаётся; не проверено, что «нет Apple ID на устройстве» приходит как сбой, а не отмена — ворота (a)–(c) проверки на устройстве и запасной план в строке реестра;
  5. веб-гранты Apple (Services ID) не проверены — отзываются, только если Services ID сгруппирован под tv.matio.app;
  6. без ключа лист показывается зря — apple вне рубильника до ключа.

Документы

docs/services.md (раздел про отзыв + три переменные), CLAUDE.md (список env, условие рычага apple, запись account/delete/, lib/apple-revoke.ts), docs/registry.md, docs/runbooks/gdpr-requests.md §4, docs/runbooks/mobile-builds.md, docs/mobile-app-plan.md, /devops (SKILL.md: строка Apple / EAS), /gdpr: SKILL.md (канон стирания), references/data-map.md (строки Clerk и Apple — «когда ключ задан», #407 в «Закрыто:» §5), references/processors.md (Apple остаётся «не процессором», записан поток отзыва).

GDPR: применимо — ст. 17: при удалении аккаунта отзываем доступ Matio к Apple ID у самостоятельного контролёра (Apple). Новых данных наружу нет: Apple получает свой же authorization code и наш client secret; ответ (токены, id_token с адресом) только в памяти запроса, не хранится и не логируется (кейсы лог-аудита). Новый секрет APPLE_SIGN_IN_PRIVATE_KEY — в env Vercel. Карта данных и таблица процессоров обновлены в этом PR; Apple — не процессор (классификация уже в вопросах к юристу, #277/#168).

Refs #407

🤖 Generated with Claude Code

https://claude.ai/code/session_0184yUssUtNnEmKVaft78cbT

efymd9 and others added 2 commits October 9, 2026 10:51
…с устройства, обмен и /auth/revoke на сервере (#407)

Clerk токены Apple не отзывает и отзывать ему нечем: нативный поток отдаёт
ему только identity token, а /auth/revoke принимает лишь refresh- или
access-токен, который даёт только обмен authorization code. Поэтому
приложение при удалении аккаунта с Apple берёт у листа Apple свежий код и
шлёт его телом запроса; маршрут после стирания нашей стороны и до удаления
в Clerk меняет код на токены и отзывает refresh-токен. Best-effort: без кода
или без ключа Apple в env удаление работает как раньше; сбой — лог и Sentry
по id, без кода, токенов и тела ответа Apple.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0184yUssUtNnEmKVaft78cbT
@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
matio Ready Ready Preview Oct 11, 2026 1:47pm UTC
matio-staging Ready Ready Preview Oct 11, 2026 1:47pm UTC

Request Review

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Покрытие

=============================== Coverage summary ===============================
Statements   : 57.02% ( 5189/9099 )
Branches     : 50.69% ( 3448/6801 )
Functions    : 50.26% ( 1139/2266 )
Lines        : 57.78% ( 4597/7956 )
================================================================================

Изменённые строки (diff-cover, порог 85%)

-------------
Diff Coverage
Diff: origin/main...HEAD, staged and unstaged changes
-------------
app/api/v1/account/delete/route.ts (100%)
lib/apple-revoke.ts (100%)
-------------
Total:   65 lines
Missing: 0 lines
Coverage: 100%
-------------

…редупреждение без ключа, карты (#407, ревью)

Ревью PR #427:
- маршрут: отзыв гранта Apple идёт ПАРАЛЛЕЛЬНО с удалением в Clerk (после
  eraseUser), а не перед ним — два запроса Apple по 5 с больше не съедают
  45-секундный дедлайн клиента; оба исхода в строке «account delete: done»,
  пост-Clerk проверка users (#336) — после обоих ответов;
- код Apple без ключа APPLE_* в env — ещё и предупреждение в Sentry
  (только userId и step), кейс в лог-аудите: кода нет ни в консоли, ни в Sentry;
- приложение: лист Apple под щедрым дедлайном (2 мин, settleOrNull) —
  зависший нативный промис больше не держит удаление вечно;
- /devops: ключ Sign in with Apple (три переменные Vercel), исходящий вызов
  на appleid.apple.com и грабля App Transfer на LTD (новый ключ, новые
  APPLE_TEAM_ID / APPLE_SIGN_IN_KEY_ID, передеплой);
- карта данных: отзыв — «когда ключ задан», #407 в «Закрыто:» §5 с ops-хвостом;
- CLAUDE.md: шаг Apple у account/delete и lib/apple-revoke.ts в карте lib/;
- реестр #407: веб-гранты Apple (Services ID) не проверены; лист без ключа
  показывается зря — `apple` вне рубильника до ключа.

Семантика отмены листа Apple не менялась — вопрос владельцу.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0184yUssUtNnEmKVaft78cbT
@efymd9

efymd9 commented Oct 9, 2026

Copy link
Copy Markdown
Owner Author

Замечания ревью применены — коммит 291db13 (ветка до этого уже была актуальна с main, слияние не понадобилось). По пунктам:

  1. (should-fix) /devops — ключ и вызов в карте инфраструктуры. Сделано: строка «Apple / EAS» в .claude/skills/devops/SKILL.md — три переменные Vercel (APPLE_TEAM_ID, APPLE_SIGN_IN_KEY_ID, APPLE_SIGN_IN_PRIVATE_KEY; прод обязательно, стенд по желанию), исходящий вызов fra1 → appleid.apple.com (/auth/token → /auth/revoke, 5 с, без ретраев) и грабля App Transfer на DEEP ORDINARY LTD: новый ключ в новой команде, новые APPLE_TEAM_ID / APPLE_SIGN_IN_KEY_ID / APPLE_SIGN_IN_PRIVATE_KEY, передеплой.
  2. (nit) Sentry при skipped_unconfigured. Сделано: код пришёл, а ключа нет → Sentry.captureMessage(level: warning, tags {userId, step: "apple"}) — только эти два тега. Тест маршрута (вызов ровно с этими опциями, кода в вызовах Sentry нет; при отсутствии кода — Sentry молчит) и кейс лог-аудита: код и адрес не попадают ни в консоль, ни в Sentry, к Apple ничего не ушло. Оба теста красные на версии до ревью. docs/services.md и /gdpr SKILL.md описывают предупреждение.
  3. (nit) Бюджет времени. Сделано: после eraseUser отзыв Apple и users.deleteUser идут параллельно — Promise.all по двум шагам, которые не могут отклониться (Clerk-шаг deleteAtClerk возвращает результат вместо throw, revokeAppleAuthorization не бросает), то есть семантика allSettled. Оба исхода — в account delete: done и в логе отказа Clerk; sweep Стирание аккаунта: остаточные дыры самообслуживания (#309) — гонка heal, поздний user.created, несостоявшаяся отмена Stripe, таймаут клиента #336 — после ответа обоих (тест «503 Clerk рядом с отзывом → erased_again»). Тесты порядка переписаны на «стирание первым, затем три вызова в любом порядке, token до revoke»; новый тест: обмен Apple отвечает только после того, как спрошен Clerk — на последовательной версии он красный. Комментарии про порядок обновлены в маршруте, mobile/src/api/client.ts, docs/services.md, /gdpr SKILL.md.
  4. (nit) Дедлайн листа Apple в приложении. Сделано: проверка доступности + signInAsync — под settleOrNull с APPLE_SHEET_TIMEOUT_MS = 120_000 (2 мин — человек может стоять на Face ID/пароле); дедлайн = нет кода = удаление идёт как сегодня. Три теста на фейковых таймерах: лист без ответа → undefined ровно на дедлайне (не раньше), зависшая проверка доступности → undefined без листа, ответ через 90 с → код доходит. Первые два красные на версии до ревью.
  5. (nit) Карта данных. Сделано: строки Clerk (§1) и Apple (§3) — «когда ключ задан (APPLE_* в Vercel)», без ключа код отбрасывается; в «Закрыто:» §5 — «Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407 — отзыв … построен; ops-хвост (ключ APPLE_* в Vercel, проверка на устройстве) — docs/registry.md», по образцу Стирание: вебхук Clerk user.deleted не обрабатывается — данные пользователя переживают удаление аккаунта #161/Ретеншен: ни одной джобы чистки — trial_sessions/visitors/watch_days/show_reminders копятся бессрочно, политика обещает 30 дней и 25 месяцев #162. Заодно та же оговорка в ранбуке gdpr-requests.md (обе строки про Apple).
  6. (nit) CLAUDE.md. Сделано, минимально: запись account/delete/ в карте файлов (шаг Apple рядом с Clerk, Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407, lib/apple-revoke.ts), описание account/delete в правиле «Mobile app» (лист Apple, дедлайн 2 мин, только с ключом), lib/apple-revoke.ts в карте lib/.
  7. (nit) Строка реестра Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407. Сделано: (5) веб-гранты Apple (соединение Clerk на вебе, Services ID) не проверены и отзываются, только если Services ID сгруппирован под tv.matio.app — часть проверки на устройстве (и в условии закрытия); (6) лист Apple показывается и без ключа на сервере, код отбрасывается — apple вне рубильника, TestFlight тоже, до ключа. Чтобы не противоречить, в строке APP_SOCIAL_SIGNIN (Приложение: вход через Google (и Sign in with Apple — обязателен по правилу App Store 4.8) #277) и в docs/runbooks/mobile-builds.md добавлено «только с ключом».
  8. Тело PR: Refs #407. Сделано — ключевое слово закрытия убрано, closingIssuesReferences = 0; тело PR дополнено разделом «Изменено после ревью».

Не трогал: смысл отмены листа Apple (порядок и семантика потока те же — вопрос владельцу) и пороги в vitest.config.mts (их двигает другой PR).

Проверки: pnpm lint 0 · pnpm typecheck 0 · cd mobile && npm run typecheck 0 · pnpm test:unit 0 (157 файлов / 2083 теста) · pnpm qa:styles 0 · покрытие route.ts 100% строк/ветвей/функций, lib/apple-revoke.ts 100% строк/ветвей.

efymd9 and others added 3 commits October 11, 2026 14:01
… любом шаге ничего не удаляет (#407, решение владельца: вариант а)

Для аккаунта с входом через Apple на iOS порядок теперь такой: первый
вопрос «Delete account» → лист Apple за свежим кодом → последний вопрос
«Delete permanently» → запрос с кодом. Отмена листа — запроса нет,
приложение спокойно говорит, что аккаунт на месте и можно повторить.
Код, за которым последовала отмена последнего вопроса, просто выброшен.

Лист, не давший кода, — сбой Apple, нет Apple ID на устройстве, дедлайн
2 мин — не тихое удаление и не жёсткий стоп: последний вопрос всё равно
приходит, но с текстом, что Matio останется подключён к Apple ID, и
подтверждение удаляет без кода. Жёсткий стоп оставил бы такого человека
без способа удалить аккаунт в приложении.

Аккаунты без Apple и Android — прежние два вопроса без изменений.
Поведение #309, #336, #398 не тронуто. Тесты вкладки и модуля листа
переписаны под новый порядок; реестр, services.md, ранбук GDPR и
CLAUDE.md приведены в согласие.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0184yUssUtNnEmKVaft78cbT
@efymd9

efymd9 commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

Решение владельца применено — вариант (а), коммит 415a5d7 (+ слияние main, ba4fcb7).

Аккаунт с Apple на iOS: первый вопрос «Delete your account?» (с одной строкой, что дальше Apple ещё раз попросит «Sign in with Apple») → лист Apple → последний вопрос «Delete permanently» → запрос с кодом.

Тесты: вкладка «Аккаунт» — +10 (порядок «вопрос → лист → вопрос → запрос», отмена листа, отмена последнего вопроса после кода, отмена первого, сбой листа ×2, «Please wait» на время листа, #336 после кода, испанский, аккаунт без Apple); apple-revocation.test.ts переписан под ответы code / cancelled / failed. На прежнем порядке — 8 красных. Документы: реестр #407 (п. 4), docs/services.md, gdpr-requests.md, CLAUDE.md, комментарии типов/маршрута; тело PR — раздел «Решение владельца: вариант (а)».

Проверки: pnpm lint 0 · pnpm typecheck 0 · cd mobile && npm run typecheck 0 · pnpm test:unit 0 (157 / 2128) · pnpm qa:styles 0 — до второго слияния main; после #441 локально красны только Stripe-тесты версии SDK (общий node_modules ещё на stripe 22), арбитр — CI.

…ndroid предупреждает о гранте (#407, ревью #427)

Замечания ревью PR #427 по варианту владельца (а):

1. Лист Apple получает Apple user id, к которым привязан аккаунт:
   providerUserId внешних аккаунтов apple из Clerk. Код, чей
   credential.user не из этого списка, отбрасывается, ответ — failed:
   последний вопрос говорит, что Matio останется в Apple ID, а сервер
   не отзывает чужой грант под видом revoked. Нет id от Clerk — код
   уходит без сверки, как раньше.
2. Сопоставление «на устройстве нет Apple ID → failed» помечено как
   непроверенное в реестре #407, ранбуке gdpr-requests.md,
   docs/services.md и комментарии модуля; в условие закрытия #407
   добавлены три проверки на устройстве как ворота перед публичной
   сборкой и перед apple в APP_SOCIAL_SIGNIN, и запасной план, если
   закрытие системного запроса придёт отменой.
3. Android: у аккаунта с Apple листа нет, последний вопрос — текст
   deleteFinalBodyAppleFailed; реестр #407 п. 3 и ранбук.

Тесты: модуль листа — сверка id, вкладка «Аккаунт» — id из Clerk
передаётся в лист, код чужого Apple ID через настоящий модуль не
уходит и показывается текст про Apple, код своего уходит, Android.
Без изменения красные: 3 теста вкладки на прежнем account.tsx, 2 —
без строки сверки.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0184yUssUtNnEmKVaft78cbT
@efymd9

efymd9 commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

Замечания ревью варианта (а) применены — коммит e68ceb9 (ветка уже содержала свежий main, слияние не понадобилось). По пунктам:

  1. (should-fix) Код чужого Apple ID → ложный revoked. Сделано на стороне приложения: вкладка передаёт в askAppleForDeletionCode список providerUserId внешних аккаунтов apple из Clerk (linkedAppleUserIds, mobile/src/auth/apple-revocation.ts; поле — ExternalAccountResource.providerUserId в @clerk/shared, clerk-js заполняет его из provider_user_id; для Apple это sub identity token, та же строка, что credential.user). Код, чей credential.user не из списка, отбрасывается, ответ — failed: последний вопрос показывает уже существующий текст «Sign in with Apple didn't go through, so Matio stays connected to your Apple Account…» — он здесь честен, отдельная строка не нужна. Нет id от Clerk — не блокируем, код уходит без сверки, как раньше (записано в реестре). Остаток, записан в реестре п. 4: лист сам мог создать грант на чужом Apple ID — он тоже остаётся; возможное продолжение — сверка sub обмена на сервере с отзывом лишнего гранта без revoked. Тесты: в модуле — свой код / любой из нескольких привязанных → код, чужой → failed без кода, нет id → код; во вкладке — лист получает id Apple из Clerk (и ничего от Google); через настоящий модуль листа над фейковым нативным: чужой Apple ID → запрос без кода + текст про Apple, свой → запрос с кодом.
  2. (should-fix) «Нет Apple ID на устройстве → failed» не проверено. Нового UI нет. Помечено как непроверенное в docs/registry.md Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407 (п. 4), docs/runbooks/gdpr-requests.md (§4: до проверки оператор удаляет за человека; таблица процессоров), docs/services.md, комментарии модуля и теле PR. В условие закрытия Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407 добавлены ворота перед публичной отправкой и перед apple в APP_SOCIAL_SIGNIN: (a) закрыть системный запрос на устройстве без Apple ID; (b) «Отмена» на листе устройства с другим Apple ID; (c) оставить лист открытым дольше 2 минут. Запасной план в строке реестра: если (a) придёт отменой, Alert «still here» получает второе действие, открывающее последний вопрос с текстом про Apple («Отмена» по-прежнему ничего не удаляет).
  3. (nit) Android. Аккаунт с Apple на Android: листа нет, первый вопрос без строки про лист, последний — текст deleteFinalBodyAppleFailed, подтверждение удаляет без кода. Тест во вкладке; реестр Sign in with Apple: при удалении аккаунта отзывать токены Apple (App Store 5.1.1(v)) — до публичной подачи #407 п. 3, ранбук, docs/services.md, одна фраза в CLAUDE.md и /gdpr (SKILL.md, карта данных: сверка id идёт только в памяти устройства, никуда не уходит). Замечу: в этом тексте «in Settings» указывает на настройки устройства Apple — на Android человек, скорее всего, создавал аккаунт на iPhone; текст оставлен как велено, отдельная строка для Android — если понадобится.

Падают без изменения: на прежнем account.tsx — 3 красных теста вкладки (передача id, чужой код, Android); без строки сверки в модуле — 2 красных (чужой код в модуле и во вкладке).

Проверки: pnpm lint 0 · cd mobile && npm run typecheck 0 · pnpm qa:styles 0 · pnpm typecheck — только известный TS2322 в lib/stripe.ts (в общем node_modules ещё stripe 22) · pnpm test:unit — 2145 зелёных, красны только два Stripe-теста версии SDK по той же причине; арбитр — CI. Тело PR дополнено разделом «После ревью варианта (а)».

This branch was successfully deployed

2 active deployments
Preview – matio-staging — 682bb483 Deployed Oct 11, 2026 by vercel[bot]
Preview – matio — 682bb483 Deployed Oct 11, 2026 by vercel[bot]
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