fix(auth): 기기 세션 캐시 디스크 도장 검증 + 죽은 토큰 재전송 게이트 — 모듈 사본 둘의 재사용이 부르던 세션 사망 제거 - #429
Merged
Merged
Conversation
… GoTrue 가족 폐기를 부르던 사망 원인 제거 - Next는 src/devicesession.mjs를 instrumentation(동기화 루프)과 API 라우트에 따로 번들한다(상주 .next/server에 사본 2개 실측). 라우트 사본의 낡은 캐시가 회전 8분 뒤 옛 리프레시 토큰으로 갱신을 보냈고, GoTrue는 재사용 간격(10초) 밖 재사용을 RevokeTokenFamily로 처리해 새 토큰까지 죽였다(2026-09-03 15:11 회전 → 15:19 라우트 재사용 → 16:10 Already Used, 인증 로그 대조). → 캐시는 파일 mtime+size 도장이 같을 때만 쓴다(사본·프로세스 간 회전 즉시 반영). - 사망 마커에 거절된 토큰 지문(sha256 앞 16자)을 남기고, 같은 토큰이면 네트워크 없이 null. 8초 동기화 루프가 죽은 토큰을 계속 보내 기기당 하루 약 1만 회, 24기기 11만 회/일의 "Possible abuse attempt"가 쌓이고 있었다(2026-09-03~04 auth_logs). 토큰이 바뀌면(재로그인·다른 사본 회전) 지문이 달라져 게이트가 열린다. - 테스트: 도장 무효화·게이트·구형 마커 호환 3건 추가, 반복 거절 count 계약은 토큰이 바뀐 재거절로 유지. 변이 5종 red. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, skipped 사유 마스킹 - MEDIUM-1: 실운영 회전은 파일 길이가 같아 mtime만이 판별자 → 픽스처를 같은 바이트 수 회전으로(size만 도장 변이 red) - MEDIUM-2: 게이트를 reused·revoked·expired에 한정하고 마지막 시도 1시간 뒤 1회 재시도(하루 24회 자가 치유, 난사 1/450) - LOW-1: 거절 후 재독 캐시에 도장(stat → read 순서) · LOW-3: skipped 로그 reason mask · LOW-4: count·lastAt 계약 주석 - 변이 8종 red(추가: size만 도장·밸브 제거·kind 한정 제거), 전체 스위트 1450/0 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
beyondworks
added a commit
that referenced
this pull request
Sep 5, 2026
윈도우 CI 실패로 드러난 잔여 창(#429 후속): 실운영 회전은 파일 크기가 같아 도장의 판별자가 mtime 하나뿐인데, 파일시스템 시각 해상도에 따라 두 쓰기가 같은 mtime을 받을 수 있다. 그러면 낡은 캐시가 살아남아 옛 리프레시 토큰으로 갱신을 보내고, GoTrue가 토큰 가족을 폐기한다(#429가 없애려던 바로 그 사슬). getFreshDeviceSession은 락 안에서 사이클당 1회만 도니 디스크 1회 읽기가 도장 비교보다 싸고 확실하다. loadDeviceSession의 도장 캐시는 user.id만 읽는 잦은 호출부(sync·market·auth)를 위해 그대로 둔다. 핀: 같은 크기 + utimes로 mtime까지 동일하게 만든 회전에서 새 토큰을 보내는지(윈도우 조건 결정적 재현). 캐시 복귀 변이 red. 전체 1464/0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
beyondworks
added a commit
that referenced
this pull request
Sep 5, 2026
…430) * fix(runners): 호환 러너 SDK 격리를 main에 반영 + 엔드포인트 404를 "모델 문제"로 오역하던 안내 교정 실사용 제보(2026-09-05, VPS 셀프호스트): OpenRouter 턴이 "There's an issue with the selected model (qwen/qwen3.7-max). It may not exist or you may not have access to it."로 실패. 잔액 있음, 모델을 바꿔도 동일. - 문구 정체 확정(로컬 스텁 상태코드 대조, SDK 0.3.258): **404면 본문과 무관하게** 이 문구가 나오고, 400은 원문(`API Error: 400 …`)이 그대로 나온다. 즉 404 전용 신호이며 모델과 무관하다. 두 모델 id는 OpenRouter에 실재하고 우리 키로 200(스모크 재확인). CLI는 모델명을 변형 없이 보낸다(로컬 프록시 캡처). → 원문은 보존한 채 404 확인 순서(데이터 정책·base URL·아웃바운드 프록시)를 러너별로 덧붙인다. - 호환 러너(openrouter·glm·kimi·grok) SDK 격리를 main에 반영(abfcf06 재상륙 — 메신저 브랜치에만 있어 v0.1.60 발행본에 빠져 있었다). 없으면 호스트 ~/.claude 로그인 시 401, CLI 2.1 도구 검색은 비 Anthropic 엔드포인트에서 400. 실행 경로 E2E: 격리 전 400(deferred tools) → 격리 후 400 소멸(남는 건 계정 잔액 402). 테스트 9건(404 판정·러너별 안내·chat 배선 구간 불변식 + 격리 4건), 변이 6종 red, 전체 1459/0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(runners): 검수 반영 — 안내 재부착 가드·원인 단정 완화·oneshot 패리티·매처 음성 배터리·스코프 접두 - MEDIUM-1: 재시도 프레임(fresh-retry·crash-retry·인증 자가치유)이 안쪽 안내를 다시 감싸 원문이 문장 중간에서 잘리고 안내가 두 번 붙던 결함 — `!e?.endpointNotFound` 가드(아래 인증 갈래의 !e?.authError와 같은 계약) - MEDIUM-2: 404의 원인을 단정하지 않는다. 벤더가 모델 슬러그를 내려도 404이고(:free는 수시로 내려간다) 그때는 모델 교체가 정답이다 — "바꿔도 소용없다"를 빼고 후보 ④(모델 회수, 다른 모델 1회 시험)를 추가 - MEDIUM-3: oneshot(온보딩·영입·루틴·기억정리)에도 같은 안내. base URL이 어긋난 셀프호스트가 가장 먼저 만나는 화면이 여기다. 러너별 원인 대장은 보존하고 안내만 덧붙인다 - MEDIUM-4: 음성 배터리에 근접 문자열 4종 추가(매처를 "There's an issue"까지 넓히는 변이가 통과하던 구멍) - LOW-1: 러너 env를 조립하는 테스트의 HOME 임시화 + **규칙 스위프**(목록이 아니라 규칙으로 — 새 테스트도 자동 적용) - LOW-2: 스코프 네임스페이스 분리(ws-<id> / hostfb-<runner>) — 슬러그가 hostfb-glm인 회사가 호스트 폴백 버킷(세션 전사본)을 함께 쓰던 충돌 차단 - LOW-3·4·5: 호스트 폴백이 배포 전역 스코프인 이유 주석, glm·kimi·grok은 미검증 축임을 명시, mkdir 실패 1회 경고 변이 10종 red(재부착 가드·단정 부활·oneshot 제거·매처 과확장·접두 제거·grok만 누락 포함), 전체 1463/0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(runners): 2차 검수 잔여 — 스위프에 sdkEnvFor 추가(미탐 사각), 셀프호스트 문서에 base URL 정본 표 - 스위프 패턴에 sdkEnvFor 누락: 자격만 심고 sdkEnvFor를 부르는 테스트가 실 홈에 폴더를 만들면서도 통과했다 (2차 검수 실측). 토큰 추가 후 usage-billing.test.mjs가 실제로 걸려 HOME 임시화까지 반영 - docs/selfhost.md: 러너 base URL 정본 4종 표 + "끝에 /v1 금지"(이번 장애의 후보 ②) + 데이터 정책·프록시 후보. 지금까지 이 값의 유일한 출처가 오류 안내뿐이었다 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(auth): 기기 세션 회전 경로는 캐시를 쓰지 않는다 — 도장(mtime+size)이 같아도 옛 토큰을 보내지 않게 윈도우 CI 실패로 드러난 잔여 창(#429 후속): 실운영 회전은 파일 크기가 같아 도장의 판별자가 mtime 하나뿐인데, 파일시스템 시각 해상도에 따라 두 쓰기가 같은 mtime을 받을 수 있다. 그러면 낡은 캐시가 살아남아 옛 리프레시 토큰으로 갱신을 보내고, GoTrue가 토큰 가족을 폐기한다(#429가 없애려던 바로 그 사슬). getFreshDeviceSession은 락 안에서 사이클당 1회만 도니 디스크 1회 읽기가 도장 비교보다 싸고 확실하다. loadDeviceSession의 도장 캐시는 user.id만 읽는 잦은 호출부(sync·market·auth)를 위해 그대로 둔다. 핀: 같은 크기 + utimes로 mtime까지 동일하게 만든 회전에서 새 토큰을 보내는지(윈도우 조건 결정적 재현). 캐시 복귀 변이 red. 전체 1464/0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
무엇
상주·앱 기기 세션이 "Invalid Refresh Token: Already Used"로 죽는 재발 원인 두 가지를 닫는다.
src/devicesession.mjs를 instrumentation(동기화 루프)과 API 라우트 번들에 따로 복제한다(상주.next/server에서devsess:문자열 사본 2개 실측: chunks/6022.js = instrumentation, chunks/7921.js = 라우트 54개). 모듈 전역cache·인메모리withLock이 사본마다 따로라, 라우트 사본이 옛 토큰으로 갱신을 보냈다..device-session.log): 15:11:23Z 동기화 사본 회전(200) → 15:19:46Z 라우트 사본(/api/me 기기 폴백)이 옛 토큰으로 갱신 → GoTrueRevokeTokenFamily(재사용 간격 10초 밖, "Possible abuse attempt") → 디스크 재독으로 겉보기 자가 치유 → 16:10:24Z 정상 회전이 Already Used → 사망.rejected는 종전처럼 재시도.영향 반경(문맥 행렬)
loadDeviceSession호출부 6곳(sync.mjs 5·market.mjs·app/auth.mjs)은 필드만 읽어 statSync 1회 추가 외 동작 동일.발행본
main 머지 ≠ 반영. 상주 재배포 + **다음 발행(v0.1.61 후보)**에 실어야 설치 기기에 닿는다. 이미 죽은 기기는 재로그인이 필요하다(마커는 그대로 사망 판정, 게이트는 난사만 멈춘다).
🤖 Generated with Claude Code