appstore
요약. appstore: 자율 개선 회차 18회, 릴리즈 12건. 최근 릴리즈 v2.6.0 (자산 1개). 건강 D
- 18회차
- 1프로젝트
- 12배포 준비 완료
- 0릴리즈 진행 중
- 0병합 완료
- 3검토 대기
- 2검증 실패
- 0변경 없음
- 1실행 오류
- $89.50비용
- 3시간 7분에이전트 시간
현황
- 저장소
- https://github.com/hkjang/appstore
- 마지막 회차
- 2026-09-12 14:43 KST — 🚀 릴리즈 merged PR #15, released v2.6.0
- 최근 릴리즈
- v2.6.0 — released · 자산 1개 (이전 v2.5.8: 1개) 전체 릴리즈 →
회차 이력
| 일시 | 프로젝트 | 결과 |
|---|---|---|
| 2026-09-12 14:43 | appstore | 배포 준비 완료 merged PR #15, released v2.6.0 |
| 2026-09-11 01:32 | appstore | 검토 대기 review held, PR open PR #14 |
| 2026-09-10 19:46 | appstore | 배포 준비 완료 merged PR #13, released v2.5.7 |
| 2026-09-10 12:07 | appstore | 배포 준비 완료 merged PR #12, released v2.5.6 |
| 2026-09-09 15:31 | appstore | 검토 대기 review held, PR open PR #11 |
| 2026-09-09 15:21 | appstore | 실행 오류 hold: budget |
| 2026-09-09 07:52 | appstore | 배포 준비 완료 merged PR #10, released v2.5.5 |
| 2026-09-09 00:45 | appstore | 배포 준비 완료 merged PR #9, released v2.5.4 |
| 2026-09-08 19:56 | appstore | 배포 준비 완료 merged PR #8, released v2.5.3 |
| 2026-09-08 17:01 | appstore | 검토 대기 review held, PR open PR #7 |
| 2026-09-07 11:35 | appstore | 배포 준비 완료 merged PR #6, released v2.5.2 |
| 2026-09-07 00:36 | appstore | 검증 실패 verify failed: secrets in diff |
| 2026-09-06 02:49 | appstore | 검증 실패 verify failed: secrets in diff |
| 2026-09-05 01:43 | appstore | 배포 준비 완료 merged PR #5, released v2.5.1 |
| 2026-09-03 08:05 | appstore | 배포 준비 완료 merged PR #4, released v2.1.4 |
| 2026-09-03 02:16 | appstore | 배포 준비 완료 merged PR #3, released v2.1.3 |
| 2026-09-02 19:56 | appstore | 배포 준비 완료 merged PR #2, released v2.1.2 |
| 2026-09-02 13:56 | appstore | 배포 준비 완료 merged PR #1, released v2.1.1 |
비용·사용량
| 시각 | 프로젝트 | 단계 | 시간 | 턴 | 비용 | 토큰 입력/출력 | 종료 |
|---|---|---|---|---|---|---|---|
| 14:37 | appstore | 릴리즈 | 7분 | 42 | $2.42 | 2.6M / 19K | success |
| 14:29 | appstore | review | 3분 | 20 | $1.60 | 1.2M / 12K | success |
| 14:25 | appstore | 개선 | 17분 | 95 | $11.10 | 14.0M / 76K | success |
| 03:49 | appstore | 릴리즈 | 7분 | 52 | $2.68 | 2.9M / 20K | success |
| 01:32 | appstore | review | 4분 | 32 | $1.99 | 1.9M / 16K | success |
| 01:28 | appstore | 개선 | 18분 | 128 | $11.91 | 16.3M / 71K | success |
| 19:39 | appstore | 릴리즈 | 5분 | 33 | $1.49 | 1.4M / 11K | success |
| 19:34 | appstore | review | 6분 | 29 | $1.64 | 982K / 22K | success |
| 19:29 | appstore | 개선 | 9분 | 84 | $5.35 | 6.8M / 32K | success |
| 11:59 | appstore | 릴리즈 | 4분 | 40 | $1.76 | 1.8M / 10K | success |
| 11:53 | appstore | review | 3분 | 23 | $1.03 | 700K / 12K | success |
| 11:49 | appstore | 개선 | 9분 | 76 | $5.59 | 7.2M / 34K | success |
| 15:31 | appstore | review | 3분 | 22 | $0.92 | 517K / 11K | success |
| 15:28 | appstore | 개선 | 7분 | 69 | $4.15 | 4.7M / 31K | success |
| 07:46 | appstore | 릴리즈 | 4분 | 30 | $1.15 | 1.0M / 9K | success |
| 07:40 | appstore | review | 3분 | 13 | $0.65 | 295K / 10K | success |
| 07:37 | appstore | 개선 | 7분 | 69 | $4.66 | 5.7M / 30K | success |
| 00:38 | appstore | 릴리즈 | 4분 | 33 | $1.38 | 1.3M / 10K | success |
| 00:30 | appstore | review | 1분 | 11 | $0.43 | 199K / 5K | success |
| 00:28 | appstore | 개선 | 8분 | 61 | $4.32 | 5.2M / 26K | success |
| 19:49 | appstore | 릴리즈 | 4분 | 25 | $1.17 | 976K / 8K | success |
| 19:44 | appstore | review | 3분 | 17 | $0.98 | 669K / 11K | success |
| 19:41 | appstore | 개선 | 11분 | 84 | $5.51 | 6.6M / 40K | success |
| 17:01 | appstore | review | 3분 | 21 | $0.84 | 462K / 10K | success |
| 16:58 | appstore | 개선 | 8분 | 46 | $2.95 | 2.6M / 34K | success |
| 11:29 | appstore | 릴리즈 | 3분 | 30 | $1.12 | 978K / 8K | success |
| 11:23 | appstore | review | 2분 | 10 | $0.56 | 293K / 7K | success |
| 11:21 | appstore | 개선 | 5분 | 45 | $2.38 | 2.3M / 23K | success |
| 00:36 | appstore | 개선 | 7분 | 51 | $2.54 | 2.5M / 24K | success |
| 02:49 | appstore | 개선 | 10분 | 70 | $5.21 | 6.0M / 39K | success |
아이디어 백로그 — 대기 9 / 전체 10
| 아이디어 | 가치/위험/크기 | 상태 | 메모 | 갱신 |
|---|---|---|---|---|
| 앱 카드가 status를 표시하지 않아 /my/apps에서 초안·검토 대기·보관됨을 구분할 수 없음 | 3/1/S | 대기 | AppCard에 소유자 문맥에서만 status 배지를 켜는 prop 하나. 사용자 가이드가 '내 홈의 숫자 카드로 확인하라'고 우회해 적고 있어 근거가 하나 더 있다. 캡처(my-apps·my-home)가 바뀌므로 manifest 갱신 필요. | 2026-09-12 |
| 캡처 fixture에 반려된 앱이 없어 반려 안내 화면을 찍을 수 없음 | 3/2/M | 대기 | web/e2e/mock-api.ts의 /me/apps에 rejected 앱과 review를 넣으면 my-apps·my-home·my-app-edit 캡처가 실제 안내를 담는다. 이번 회차에 make screenshots가 이 환경(APPSTORE_PREVIEW_PORT로 포트 충돌 회피)에서 도는 것을 확인했으므로 fixture만 더하면 된다. my-home-desktop은 렌더링이 흔들려 재캡처 시 바뀔 수 있음. | 2026-09-12 |
| clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서 rate limit이 전역이 됨 | 3/3/M | 대기 | 신뢰 프록시 설정이 필요한데 환경변수 계약이 네 개로 고정(check-env-contract.sh). DB 설정(system_settings)에 두는 쪽이 이 저장소 관례와 맞다. 관리자 가이드 장애 대응 표에는 '프록시 IP 기준'이라고 적혀 있음. | 2026-09-12 |
| 소유자가 볼 수 있는 검토 이력이 마지막 한 건뿐 | 2/1/M | 대기 | LatestReviewsByApp은 앱마다 최신 한 건. /me/apps/{id}/reviews 같은 소유자 전용 읽기 경로 + OpenAPI + 화면 필요. | 2026-09-12 |
| 방문 추적 화면에 현재 설정이 만들 CSP 문자열 미리보기 | 2/1/S | 대기 | httpapi.pagePolicy가 순수 함수라 GET /admin/analytics/policy 한 개와 화면의 <code> 블록이면 된다. 관리자가 브라우저를 띄우기 전에 어떤 출처가 허용되는지 볼 수 있다. | 2026-09-12 |
| branding URL 가져오기의 오류 메시지에 upstream 상태 코드와 err 원문이 샘 | 2/2/S | 대기 | settings:write 권한자만 호출 가능. 응답 메시지에서 upstream 상태 코드와 err 원문만 줄이는 편이 안전. 검증 하드닝 계열 PR이 반려된 전례가 있어 우선순위 낮음. | 2026-09-12 |
| pending_review 상태의 앱을 수정해도 검토가 level 1로 되돌아가지 않음 | 2/3/S | 대기 | resubmitAfterEdit에 pending_review를 더하면 한 줄이지만 정책 결정이 먼저 필요. ReapprovalAfterEdit를 끈 설치도 영향. | 2026-09-12 |
| httpapi.SPAHandler.fmtInt는 strconv.Itoa 재구현 | 1/1/S | 대기 | 이번 회차가 spa.go에 Decorate 훅을 더했지만 무관한 정리를 섞지 않으려 그대로 두었다. 다음 spa.go 변경 때 함께. | 2026-09-12 |
| auth_handlers.go에 남은 Retry-After: 60 정리 | 1/1/S | 대기 | retryAfterSeconds가 main에 있으므로(9d81c75) 고정 60만 남았다. 브라우저 로그인 폼만 읽는 경로라 실제 영향 없음. | 2026-09-12 |
| [캠페인 tracking-2026-09] 관리자가 화면에서 붙이는 방문 추적 스크립트와 nonce 기반 CSP | 5/2/M | 완료 | internal/analytics 패키지, /admin/analytics 화면, ContentSecurityPolicy 미들웨어(요청마다 nonce, 'unsafe-inline' 없음), csp-report 수신과 차단 출처 표, /momento/* 같은 오리진 프록시. 기본값 꺼짐. ADMIN_GUIDE 4.9와 PDF 갱신. 커밋 1294a77. 실제 수집기로 수집 확인만 환경 부재로 남음. | 2026-09-12 |
교훈 (깨졌던 변경)
- 2026-09-10 rejected-by-human — 사람이 PR 을 반려함. 같은 접근은 피할 것. (링크)
- 2026-09-08 rejected-by-human — 사람이 PR 을 반려함. 같은 접근은 피할 것. (링크)
원장 (에이전트가 남긴 기록)
2026-09-02
- 선택: 로그인 returnTo의 open redirect 우회 차단 (가치 5 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
auth.SafeReturnTo와 프런트엔드safeReturnTo가 선행 슬래시 하나만 검사해서/\evil.test,/<TAB>/evil.test같은 값이 통과했고, 브라우저가 backslash를 slash로 정규화하고 tab·CR·LF를 제거하면서//evil.test(protocol-relative)로 해석되어 OIDC 로그인 후 외부 리디렉션이 가능했다. 양쪽 모두 backslash와 제어문자를 거부하고 Go 쪽은url.Parse로 scheme·host·userinfo가 없는지 재확인하도록 고쳤으며, 프런트엔드 guard는web/src/lib/utils.ts로 옮겨 단위 테스트를 붙였다. 검증:go test -race(전체 패키지),npm test,npm run lint, prettier check,npm run build+check-offline-assets.sh/check-env-contract.sh/check-docs.sh모두 통과. 커밋 53a7c5d. - 보류 아이디어:
AccessLog의responseRecorder가http.Flusher·http.Hijacker를 직접 구현하지 않음(현재는Unwrap()+http.NewResponseController덕에 SSE 동작). 가치 2 / 위험 2 / Sinternal/httpapi커버리지 5.5% — DB 없이 테스트 가능한SPAHandler, middleware,DecodeJSON,parseID등에 단위 테스트 추가. 가치 3 / 위험 1 / Mhttpapi.validHTTPURL강화(호스트의 공백·제어문자 거부, IDN/포트 검증). 가치 2 / 위험 2 / S- rate limiter의
Retry-After: 60고정값을 현재 분 윈도우 잔여 시간으로 계산. 가치 2 / 위험 1 / S httpapi.decodeSetting이 Unmarshal 실패 시 부분 변형된 fallback을 반환할 수 있음 — 임시 값으로 디코드 후 성공 시에만 대입. 가치 2 / 위험 1 / S
- 릴리즈: v2.1.1 (2026-09-02)
2026-09-02
- 선택: 앱·사용자 검색의 LIKE 메타문자 이스케이프 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
store.ListApps와store.ListUsers가 검색어를 그대로ILIKE '%' || $n || '%'패턴에 넣어 사용자가 입력한%,_,\가 와일드카드로 해석됐다.%한 글자만 검색하면 전체 목록이,100%나a_b를 검색하면 무관한 행이 함께 반환됐다(공개 카탈로그 검색, 관리자 앱·사용자 검색, MCP search tool 모두 영향).internal/store/repository.go에likePattern헬퍼를 추가해 메타문자를 이스케이프하고 각ILIKE절에ESCAPE '\'를 명시했으며, 앱 검색은plainto_tsquery용 원문 파라미터와 ILIKE용 패턴 파라미터를 분리했다. 검증:likePattern단위 테스트 추가 +go test ./...(전체 통과),go test -race ./internal/store/... ./internal/httpapi/...,go vet,gofmt -l,check-env-contract.sh,check-docs.sh통과. 통합 테스트(DSN 필요, CI에서는 skip)에도 와일드카드 검색이 0건을 반환하는지 확인하는 단언을 추가. 커밋 96e2daf. - 보류 아이디어:
internal/httpapi커버리지 5.5% — DB 없이 테스트 가능한SPAHandler, middleware,DecodeJSON,parseID등에 단위 테스트 추가. 가치 3 / 위험 1 / M- rate limiter의
Retry-After: 60고정값을 현재 분 윈도우 잔여 시간으로 계산. 가치 2 / 위험 1 / S httpapi.decodeSetting이 Unmarshal 실패 시 부분 변형된 fallback을 반환할 수 있음 — 임시 값으로 디코드 후 성공 시에만 대입. 가치 2 / 위험 1 / Shttpapi.validHTTPURL강화(호스트의 공백·제어문자 거부, 포트 검증). 가치 2 / 위험 2 / Sai.sanitizeProviderError가 512 byte로 자를 때 UTF-8 rune 경계를 무시해 한국어 provider 오류 메시지 끝이 깨짐. 가치 2 / 위험 1 / S
- 릴리즈: v2.1.2 (2026-09-02)
2026-09-03
- 선택: maxOutputTokens 미설정 provider의
max_tokens: 0전송 버그 수정 (가치 4 / 위험 1 / 작업량 S) - 결과: 성공
- 요약:
store.validateAIProvider와httpapi.minimumPositive는maxOutputTokens = 0을 “AppStore 측 출력 제한 없음”으로 취급하는데,ai.Stream은 그 값을 그대로 upstream payload의"max_tokens"에 넣어 OpenAI 호환 서버가 거부하는max_tokens: 0을 보냈다(해당 provider로는 모든 AI 요청이 HTTP 400).input.MaxTokens > 0일 때만 필드를 포함하도록 고쳤고, 함께 미뤄 뒀던sanitizeProviderError의 512 byte 절단이 UTF-8 rune 경계를 깨뜨리던 문제도 마지막 불완전 rune을 잘라내도록 수정했다. 검증: payload를 캡처하는 httptest 기반 테이블 테스트와 rune 경계 테스트를 추가해 수정 전 코드에서 두 테스트가 모두 실패하는 것을 확인한 뒤go test -race(전체 패키지),go vet ./...,gofmt -l,go build ./cmd/server,check-env-contract.sh,check-docs.sh통과. Frontend는 변경하지 않아 npm 검사는 생략. 커밋 4450a58. - 보류 아이디어:
internal/httpapi커버리지 5.5% — DB 없이 테스트 가능한SPAHandler, middleware,DecodeJSON,parseID등에 단위 테스트 추가. 가치 3 / 위험 1 / Mai.consumeOpenAIStream이 SSE 다중data:줄을 이어 붙이지 않아 한 이벤트를 여러 줄로 쪼개 보내는 provider에서 JSON 디코드 실패. 가치 3 / 위험 2 / M- rate limiter의
Retry-After: 60고정값을 현재 분 윈도우 잔여 시간으로 계산. 가치 2 / 위험 1 / S httpapi.validHTTPURL강화(호스트의 공백·제어문자 거부, 포트 검증). 가치 2 / 위험 2 / Shttpapi.SPAHandler.fmtInt는strconv.Itoa재구현 — 표준 라이브러리로 교체. 가치 1 / 위험 1 / S
- 릴리즈: v2.1.3 (2026-09-03)
2026-09-03
- 선택: AI provider 응답을 줄 단위가 아닌 SSE 이벤트 단위로 파싱 (가치 3 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약:
ai.consumeOpenAIStream이data:줄 하나를 곧바로 하나의 JSON 문서로 디코드해서, 긴 청크를 여러data:줄에 나눠 보내는(SSE 규격상 정상) OpenAI 호환 provider에서는 첫 청크부터 “decode AI stream chunk” 오류가 나며 채팅 요청 전체가 중단됐다. 이제 빈 줄이 이벤트를 끝낼 때까지data필드를 모아 개행으로 이어 붙이고 나머지 필드(event:,id:, 주석:)는 무시하며, 종료 빈 줄 없이 연결이 끊긴 마지막 이벤트도 EOF에서 처리한다. 검증: 멀티라인data·CRLF·빈 줄 없는 마지막 이벤트·finish 누락·비 JSON 청크를 다루는consumeOpenAIStream단위 테스트 3개를 추가(멀티라인 테스트는 수정 전 코드에서 반드시 실패)한 뒤go test -race(전체 패키지),go vet ./...,gofmt -l,go build ./cmd/server,check-env-contract.sh,check-docs.sh통과. 프런트엔드는 이미 블록 단위로 SSE를 파싱하고 있어 변경하지 않았고 npm 검사는 생략. 커밋 60969eb. - 보류 아이디어:
internal/httpapi커버리지 5.5% — DB 없이 테스트 가능한SPAHandler, middleware,DecodeJSON,parseID등에 단위 테스트 추가. 가치 3 / 위험 1 / M- rate limiter의
Retry-After: 60고정값을 현재 분 윈도우 잔여 시간으로 계산. 가치 2 / 위험 1 / S httpapi.validHTTPURL강화(호스트의 공백·제어문자 거부, 포트 검증). 가치 2 / 위험 2 / S- 프런트엔드
streamAiChat이 스트림 종료 시TextDecoder플러시와 잔여 버퍼 처리를 하지 않아, 마지막 이벤트가 종료 빈 줄 없이 끝나면 유실됨. 가치 2 / 위험 1 / S httpapi.SPAHandler.fmtInt는strconv.Itoa재구현 — 표준 라이브러리로 교체. 가치 1 / 위험 1 / S
- 릴리즈: v2.1.4 (2026-09-03)
2026-09-05
- 선택: 앱 입력의 단일행 필드 길이 상한과 제어문자 거부 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
httpapi.ValidateAppInput이 name·slug·summary·description만 검사하고 icon·gradient·serviceUrl·language·framework·team·version은 전혀 검사하지 않아, 2MB 본문 한도까지의 문자열이 그대로 저장되어 앱을 나열하는 모든 진열대에 렌더링됐다. 각 필드에 폼의 maxLength와 같은 상한을 넣고(누락됐던 serviceUrl·language·framework·version·team의 maxLength도 폼에 추가), 단일행 필드의 탭·개행·제어문자를 거부했으며(tags·screenshots는cleanStrings가 기존처럼 조용히 버림), description의 NUL은 PostgreSQL text 컬럼이 저장할 수 없어 INSERT 실패 → 불투명한 500이 되던 것을 422 필드 오류로 바꿨다. OpenAPIAppInput스키마에도 실제 강제되는 상한을 모두 문서화했다. 검증: 필드별 테이블 테스트 2개 추가 후go test ./...,go test -race ./internal/httpapi ./openapi,go vet ./...,gofmt -l,npm run lint,npm test,npm run build, prettier check,check-env-contract.sh·check-docs.sh·check-offline-assets.sh모두 통과. 커밋 3a21896. - 보류 아이디어:
- MCP
featured_appstool이Sort: "updated"를 강제해 v2.5.0의 featured_rank 편집 순서를 무시함(REST/api/v1/apps?featured=true와 불일치). 가치 2 / 위험 1 / S - 프런트엔드
streamAiChat이 종료 시 잔여 버퍼·TextDecoder플러시를 하지 않고, chunk 경계에 걸친\r\n을 정규화하지 못해 이벤트가 유실됨. 가치 2 / 위험 1 / S - rate limiter의
Retry-After: 60고정값을 현재 분 윈도우 잔여 시간으로 계산. 가치 2 / 위험 1 / S brandingURL 가져오기(fetchBrandingSource)에 SSRF 가드가 없어 admin이 내부 주소를 탐침할 수 있고 오류 메시지로 상태 코드가 새어 나감. 가치 3 / 위험 3 / Minternal/httpapi커버리지 — DB 없이 테스트 가능한SPAHandler, middleware,DecodeJSON에 단위 테스트 추가. 가치 3 / 위험 1 / M
- MCP
- 릴리즈: v2.5.1 (2026-09-05)
2026-09-06
- 선택: bcrypt가 해시할 수 없는 Bootstrap 비밀번호 거부 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
config.Load와ensureBootstrapAdmin이BOOTSTRAP_ADMIN_PASSWORD의 최소 12 byte만 검사하고 상한을 두지 않아, bcrypt의 72 byte key 한도를 넘는 값(문서가 권장하는 “long random password”, 한글이면 25자면 충분)을 쓰면 최초 설치에서 migration까지 끝낸 뒤hash bootstrap password: bcrypt: password length exceeds 72 bytes로 기동이 실패하고 관리자 계정 없이 남았다. 상한·하한을 해싱 코드 옆auth.ValidatePassword로 모으고config.Load가 PostgreSQL에 접속하기 전에 거부하도록 했으며, bcrypt가 72 byte까지만 비교해 더 긴 후보가 실제 해시된 비밀번호로 인증되던 것도CheckPassword에서 막았다(수정 전 코드로HashPassword(80바이트)실패와CheckPassword(72바이트해시, 73바이트) == true를 직접 확인). README와 오프라인 설치 가이드의 “최소 12자” 표기도 실제 강제되는 12~72 byte로 고쳤다. 검증:auth·config·database에 경계 테스트 4개 추가 후gofmt -l,go vet ./...,go test -race(전체 패키지),go build ./cmd/server,check-env-contract.sh,check-docs.sh모두 통과. 프런트엔드는 변경하지 않아 npm 검사는 생략. 커밋 b94f99f. - 보류 아이디어: MCP
featured_apps가Sort:"updated"를 강제해 featured_rank 편집 순서를 무시(가치 2/위험 1/S) ·bootstrapLogin이 길이 제한 없는 username을 rate limiter map key와 DB 파라미터로 그대로 사용(가치 3/위험 1/S) ·clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 rate limit bucket을 공유(가치 3/위험 3/M) ·httpapi.decodeSetting이 Unmarshal 실패 시 부분 변형된 fallback을 반환(가치 2/위험 1/S) · rate limiter의Retry-After: 60고정값을 현재 분 윈도우 잔여 시간으로 계산(가치 2/위험 1/S)
2026-09-07
- 선택: bootstrap 로그인 username 길이 상한 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
bootstrapLogin이 본문의 username을 길이 검사 없이"bootstrap:"+IP+":"+username형태의 rate limiter key와GetBootstrapCredential의 DB 파라미터로 그대로 썼다. 본문 한도는 2MB이고fixedWindowLimiter는 1024번째 호출에서만, 그것도 이미 끝난 윈도우의 bucket만 지우므로, 비인증 클라이언트가 매번 다른 메가바이트급 username을 보내면 map에 수백 MB가 몇 분간 남았다(분당 IP별 API 한도가 낮을수록 sweep이 늦게 와 더 오래 유지됨). 이제normalizedLoginUsername이 소문자·trim 후 빈 값이거나 120 rune을 넘으면 limiter와 PostgreSQL을 건드리지 않고 기존과 동일한 401 문구로 응답하며, 조회에는 쿼리가 어차피lower()로 비교하던 정규화된 이름을 넘긴다. 로그인 폼의 username에도 같은maxLength={120}을, OpenAPIbootstrapLogin스키마에도 minLength/maxLength를 문서화했다. 검증: 순수 함수 경계 테이블 테스트와 “사용 불가 username은 limiter bucket을 만들지 않는다”는 핸들러 테스트를 추가한 뒤gofmt -l,go vet ./...,go test ./...,go test -race ./internal/httpapi ./internal/auth,go build ./cmd/server,npm run lint,npm test,npm run build, prettier check,check-env-contract.sh·check-docs.sh·check-offline-assets.sh모두 통과. 커밋 ee70048. - 보류 아이디어: MCP
featured_apps가Sort:"updated"를 강제해 featured_rank 편집 순서를 무시(가치 2/위험 1/S) ·httpapi.decodeSetting이 Unmarshal 실패 시 부분 변형된 fallback을 반환(가치 2/위험 1/S) · rate limiter의Retry-After: 60고정값을 현재 분 윈도우 잔여 시간으로 계산(가치 2/위험 1/S) ·clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 bucket을 공유(가치 3/위험 3/M) · 앱screenshotsURL에 scheme 검증이 없어cleanStrings길이 검사만 통과하면 저장됨(가치 2/위험 2/S)
2026-09-07
- 선택: rate limiter bucket을 윈도우마다 한 번 쓸어내기 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
fixedWindowLimiter.allow이 1024번째 호출에서만 지난 윈도우 bucket을 지워, 분당 요청이 적은 설치에서는 만료된 key가 수십 분 남았다(직전 회차가 지적한 “key를 바꿔가며 보내면 map이 자란다”는 문제의 나머지 절반). 이제 새 윈도우의 첫 요청이 map을 한 번 훑어 끝난 bucket을 모두 지우므로 메모리가 현재 윈도우에서 본 key 수로 묶인다. 함께 v2.5.0 회귀 하나도 고쳤다 — MCPfeatured_appstool이Sort:"updated"를 강제해 관리 콘솔에서 손으로 정한featured_rank를 무시했고, 그래서 REST/api/v1/apps?featured=true와 순서가 달랐다. ORDER BY 선택을store.appListOrder순수 함수로 빼내 DB 없이 우선순위 규칙을 단위 테스트로 덮었다. 검증: 저트래픽 sweep 테스트(수정 전 코드에서는 bucket 51개가 남아 실패)와appListOrder테이블 테스트를 추가한 뒤gofmt -l,go vet ./...,go test ./...,go test -race ./internal/httpapi ./internal/store,go build ./cmd/server,check-env-contract.sh,check-docs.sh모두 통과. 프런트엔드는 변경하지 않아 npm 검사는 생략. 커밋 537b36c, ab67119. -
보류 아이디어: rate limiter의
Retry-After: 60고정값을 현재 분 윈도우 잔여 시간으로 계산 — 세 곳 중 하나가 미머지 브랜치가 건드린 hunk라 충돌을 피해 미룸(가치 2/위험 1/S) ·clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 bucket을 공유(가치 3/위험 3/M) · 앱screenshotsURL에 scheme 검증이 없음 — 다만 현재 프런트엔드가 screenshots를 렌더링하지 않아 가치를 낮춤(가치 1/위험 2/S) · Alpine 런타임에/etc/mime.types가 없어 SPAHandler가.woff2·.ico를 Content-Type 없이 내보냄(가치 2/위험 1/S) ·internal/httpapi커버리지 —SPAHandler, middleware 등 DB 없이 테스트 가능한 경로 보강(가치 3/위험 1/M) - 릴리즈: v2.5.2 (2026-09-07, run 2026-09-07-111546-appstore-improve)
2026-09-08
- 선택: 429 응답의 Retry-After를 현재 윈도우 잔여 초로 계산 (가치 2 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
apiPolicy와mcpRateLimit이 429에 항상Retry-After: 60(윈도우 전체 길이)을 실어, 헤더를 존중하는 클라이언트가 남은 윈도우에 더해 다음 윈도우의 거의 전부를 놀렸다 —fixedWindowLimiter.allow은 지난 윈도우에서 넘어온 bucket을 초기화하므로 거절은 항상 현재 분 안에서 일어나고, 한도는 보통 분이 끝나기 한참 전에 찬다. 새 순수 함수retryAfterSeconds가 남은 초를 올림해 돌려주도록 하고(광고한 시각이 리셋보다 앞서지 않도록) 두 미들웨어가 같은now로 한도 확인과 헤더 계산을 하게 했다.auth_handlers.go의 세 번째Retry-After: 60은 미머지 브랜치 ee70048이 바로 그 hunk를 건드리고 있어 이번에도 손대지 않았다(브라우저 로그인 폼만 읽는 경로라 영향도 없다). 함께internal/httpapi의 DB 없이 테스트 가능한 경로에 단위 테스트를 채웠다 — SPAHandler(캐시 헤더·content type·HEAD·404·traversal, Alpine 런타임에 /etc/mime.types가 없어도 woff2가 폰트로 나가는 것 포함)와 middleware(request id 필터·보안 헤더·panic 봉투·access log 상태/flush 유지). 검증:retryAfterSeconds경계 테이블 테스트와 “광고한 시간만큼 기다리면 반드시 통과한다”는 윈도우 전 구간 스윕 테스트를 추가한 뒤gofmt -l,go vet ./...,go test -race(CI와 같은 패키지 목록),go build ./cmd/server,check-env-contract.sh,check-docs.sh통과. 프런트엔드는 변경하지 않아 npm 검사는 생략. 커밋 9d81c75, f7aaeac. - 보류 아이디어:
clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 bucket을 공유 — 신뢰 프록시 설정이 필요한데 환경변수 계약이 네 개로 고정(가치 3/위험 3/M) · 프런트엔드streamAiChat이 스트림 종료 시 잔여 버퍼·TextDecoder를 플러시하지 않고 chunk 경계 CRLF를 놓침(가치 2/위험 2/S) ·brandingURL 가져오기(fetchBrandingSource)의 오류 메시지에 upstream 상태 코드와 err 원문이 새어 나감(가치 2/위험 2/S) ·auth_handlers.go의 남은Retry-After: 60— ee70048 머지 후 정리(가치 1/위험 1/S) · 앱screenshotsURL에 scheme 검증이 없음 — 현재 프런트엔드가 렌더링하지 않아 실제 표면 없음(가치 1/위험 2/S)
2026-09-08
- 선택: 카탈로그 필터가 PostgreSQL에 보내는 바이트 정규화 (가치 4 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약: URL의 query string과 path segment는 텍스트가 아니라 raw byte를 실어 나르므로
/api/v1/apps?q=%FF가 pool에 잘못된 UTF-8로 도달했고, PostgreSQL이 SQLSTATE 22021(“invalid byte sequence for encoding UTF8”)로 statement 전체를 거절해 비인증 공개 카탈로그가 500 INTERNAL_ERROR를 돌려줬다(%00도 동일 — text column이 NUL을 담을 수 없다). JSON 본문은 디코더가 이미 잘못된 바이트를 치환하므로 영향이 없고, 오직 URL 경로로만 DB에 닿았다. 이제 모든 호출자 제공 필터(apps의 q·category·language·status·slug, users의 q·role·authSource, audit의 action·resource·requestId, reviews의 status)가store.normalizeFilter를 거쳐 그 바이트를 U+FFFD로 바꾸고 200 rune으로 잘리며(질의 문자열은 최대 1MB, MCP 본문은 2MB까지 커져 ILIKE 패턴을 그만큼 넓혔다), OpenAPI의 q·category·language에도 그 상한을 문서화했다. 검증: 로컬 postgres:16-alpine 컨테이너를 띄워 수정 전 코드에서 store 통합 테스트가 실제 22021 오류로, httpapi 통합 테스트가status=500 INTERNAL_ERROR로 실패하는 것을 확인한 뒤 수정 후 통과를 확인했고,normalizeFilter경계 테이블 테스트를 더해gofmt -l,go vet ./...,go test -race(CI와 같은 패키지 목록, 그리고 DSN을 준 재실행),go build ./cmd/server,check-env-contract.sh,check-docs.sh통과. 함께 store 통합 테스트가 “migration은 정확히 1개”라고 단언해 두 번째 migration 이후 계속 첫 단언에서 실패하던 것을 임베드된 파일 수를 세도록 고쳤다(그래서 통합 테스트를 다시 쓸 수 있게 됐다). 프런트엔드는 변경하지 않아 npm 검사는 생략. 커밋 51574ee. -
보류 아이디어:
clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 rate limit bucket을 공유 — 신뢰 프록시 설정이 필요한데 환경변수 계약이 네 개로 고정(가치 3/위험 3/M) · 프런트엔드streamAiChat이 스트림 종료 시 잔여 버퍼·TextDecoder를 플러시하지 않고 chunk 경계 CRLF를 놓침(가치 2/위험 2/S) ·brandingURL 가져오기(fetchBrandingSource)의 오류 메시지에 upstream 상태 코드와 err 원문이 샘(가치 2/위험 2/S) · 관리자 write 경로(예: key permission의{key}path param)도 잘못된 UTF-8에 500을 돌려줌 — 읽기 필터와 달리 조용한 정규화가 옳지 않아 422가 필요(가치 2/위험 1/S) ·httpapi.SPAHandler.fmtInt는strconv.Itoa재구현, 다른 spa.go 변경과 함께 정리(가치 1/위험 1/S) - 릴리즈: v2.5.3 (2026-09-08, run 2026-09-08-193057-appstore-improve)
2026-09-09
- 선택: 검토 반려 사유(reason) 길이·제어문자 검증 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
rejectReview가 본문의 reason을 trim만 하고 그대로 store에 넘겨, 유일한 상한이 2MB 요청 본문이었다 — 그 크기가 reviews.reason에 저장되고 검토 상세 화면에 렌더링되며 감사 로그의 before/after 양쪽에 복사돼 한 번의 붙여넣기가 세 번 저장됐다. NUL은 더 나빠서 PostgreSQL text 컬럼이 담을 수 없어 UPDATE가 SQLSTATE 22021로 실패하고 검토자에게 필드 오류 대신 불투명한 500이 갔다(로컬 postgres:16-alpine 컨테이너로INSERT ... VALUES ($1)에 NUL을 넣어 22021을, 70만 rune 값이 아무 저항 없이 저장되는 것을 직접 확인). 새ValidateReviewReason이 2000 rune 상한과 개행·탭을 제외한 제어문자를 422 필드 오류로 돌려주고(textarea이므로 줄바꿈은 사용자가 쓴 텍스트의 일부), 빈 사유는 여기서 통과시킨다 — 필수 여부는 workflow 정책만 알고 그 검사는 이미 store 트랜잭션 안에 있다. 반려 dialog의 textarea에 빠져 있던 maxLength와 OpenAPIrejectReview스키마의 maxLength도 채웠다. 검증: 경계 테이블 테스트 2개 추가 후gofmt -l,go vet ./...,go test -race(CI와 같은 패키지 목록),go build ./cmd/server,npm run lint,npm test, prettier check,npm run build,check-offline-assets.sh·check-env-contract.sh·check-docs.sh모두 통과. 커밋 fec02b9. -
보류 아이디어:
clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 rate limit bucket을 공유 — 신뢰 프록시 설정이 필요한데 환경변수 계약이 네 개로 고정(가치 3/위험 3/M) · 프런트엔드streamAiChat이 스트림 종료 시 잔여 버퍼·TextDecoder를 플러시하지 않고 chunk 경계 CRLF를 놓침(가치 2/위험 2/S) · 개인 API 키 이름(createKey)은 100 rune 상한만 있고 제어문자·NUL 검사가 없어 NUL이 오면 500(가치 2/위험 1/S) ·brandingURL 가져오기(fetchBrandingSource)의 오류 메시지에 upstream 상태 코드와 err 원문이 샘(가치 2/위험 2/S) · 관리자 write 경로의 path param(예: key permission의{key})이 잘못된 UTF-8이면 여전히 500 — 조용한 정규화가 옳지 않아 422가 필요(가치 2/위험 1/S) - 릴리즈: v2.5.4 (2026-09-09, run 2026-09-09-002059-appstore-improve)
2026-09-09
- 선택: 브랜딩 업로드 본문에 상한을 두어 디스크로 spooling되기 전에 거절 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약:
readBrandingUpload가ParseMultipartForm(maxBrandingBytes)를 호출했지만 그 인자는 크기 제한이 아니라 메모리 예산이라, 그 예산을 넘는 file part는 크기 제한 없이 임시 파일로 spooling된다 — 1MB 검사는 이미 디스크에 다 쓰인 바이트를 두고 뒤늦게 돌았고,settings:write권한자가 아무리 큰 업로드도 서비스가 먼저 디스크에 쓰게 만들 수 있었으며(동시 요청이면 디스크 고갈), 관리자가 큰 파일을 잘못 골랐을 때도 전부 올라간 뒤에야 거절됐다. 실제로 수정 전 코드에서 64MB 본문을 끝까지(67,109,009 byte) 읽는 것을 테스트로 확인했다. 이제http.MaxBytesReader가 본문을 이미지 상한 + multipart envelope 여유(64KB)로 묶고 그*http.MaxBytesError를 핸들러가 이미 쓰던 “이미지는 1MB 이하여야 합니다.” 422로 매핑한다 — JSON 분기는DecodeJSON이 이미 묶고 있었으므로 이것이 유일하게 상한 없는 요청 본문이었다. 관리 콘솔도 같은 상한을brandingSizeError로 먼저 검사해 큰 로고는 업로드하지 않고 즉시 알린다. 검증: 오버사이즈 본문이 4MB 미만만 읽히고 422가 나는 테스트(수정 전 코드에서 반드시 실패), 정확히 1MB인 이미지가 여전히 통과하는 테스트, 이미지가 아닌 part를 거절하는 테스트 3개를 추가한 뒤gofmt -l,go vet ./...,go test(CI와 같은 패키지 목록),go test -race ./internal/httpapi ./internal/ai,go build ./cmd/server,npm run lint,npm test,prettier --check,npm run build,check-env-contract.sh·check-docs.sh·check-offline-assets.sh모두 통과. OpenAPI는 이미 “최대 1MB”와 422를 문서화하고 있어 변경하지 않았다. 커밋 f005d7b. -
보류 아이디어:
clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 rate limit bucket을 공유 — 신뢰 프록시 설정이 필요한데 환경변수 계약이 네 개로 고정(가치 3/위험 3/M) · 개인 API 키 이름(createKey)은 100 rune 상한만 있고 제어문자·NUL 검사가 없어 NUL이 오면 500(가치 2/위험 1/S) ·updateMySettings의language에 NUL이 들어가면 jsonb가 \u0000 이스케이프를 담을 수 없어 500(가치 2/위험 1/S) ·fetchBrandingSource의 오류 메시지에 upstream 상태 코드와 err 원문이 샘(가치 2/위험 2/S) · 관리자 write 경로의 path param(예: key permission의{key})이 잘못된 UTF-8이면 여전히 500 — 조용한 정규화가 옳지 않아 422가 필요(가치 2/위험 1/S) - 릴리즈: v2.5.5 (2026-09-09, run 2026-09-09-073059-appstore-improve)
2026-09-09
- 선택: 공개 카테고리 화면이 렌더링하는 필드에 상한과 제어문자 검증 (가치 3 / 위험 1 / 작업량 S)
- 결과: 성공
- 요약: 카테고리는 앱과 함께 공개 store front가 스스로 렌더링하는 두 번째 레코드다 —
/categories가 이름·아이콘·설명으로 카드를 채우고 slug는 그 카드가 링크하는 라우트 세그먼트가 된다. 그런데 서버는store.validateCategory에서 slug·name이 비어 있지 않은지만 확인해, 2MB 본문이 허용하는 모든 값이 그대로 저장되고 익명 방문자 전원에게 렌더링됐으며 두 가지는 필드 오류 대신 DB에서 터졌다(로컬 postgres:16-alpine 컨테이너로 직접 확인:position: 3000000000→is greater than maximum value for int4 (OID 23), 설명의 NUL → SQLSTATE 22021, 30만 rune 이름 → 아무 저항 없이 저장). 두 오류 모두 불투명한 500이었다. 새ValidateCategoryInput이 store로 넘어가는 값 자체를 검사해(아이콘 기본값이 적용된 뒤라 빈 아이콘을 볼 일이 없다) slug 패턴·100자, 이름 1~60자, 아이콘 1~16자, 설명 240자(textarea라 줄바꿈은 허용), position 0~9999를 422 필드 오류로 돌려준다. slug 패턴은 앱 slug와 달리 밑줄을 허용한다 — 시드 분류가enterprise_ops·ai_agents를 싣고 있고 이미 살아 있는 URL이라 앱 패턴을 쓰면 관리자가 설치 기본 카테고리를 편집할 수 없게 된다. 관리 콘솔 폼도 첫 타이핑에 바로 그 밑줄을 하이픈으로 바꿔 쓰고 있어 필터를 같이 고쳤고, 각 입력에 서버와 같은 maxLength를 넣었다. OpenAPI에는 두 admin 오퍼레이션에 아예 없던CategoryInput스키마와 422를 채웠다. 검증: 경계 테이블 테스트 2개 추가 후gofmt -l,go vet ./...,go test ./...,go test -race ./internal/httpapi ./internal/store ./openapi, DSN을 준 통합 테스트 재실행(store·httpapi·database 모두 통과),go build ./cmd/server,npm run lint,npm test,prettier --check,npm run build,check-env-contract.sh·check-docs.sh·check-offline-assets.sh모두 통과. 커밋 6854375. - 보류 아이디어:
clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 rate limit bucket을 공유 — 신뢰 프록시 설정이 필요한데 환경변수 계약이 네 개로 고정(가치 3/위험 3/M) ·adminUpdateUser의 username·email·displayName·team에 길이·제어문자 검증이 없어 NUL이면 500이고 megabyte 값이 앱 소유자·감사 로그·키 목록에 렌더링됨(가치 3/위험 1/S) · 개인 API 키 이름(createKey)은 100 rune 상한만 있고 제어문자 검사가 없으며updateMySettings의language도 jsonb가 NUL 이스케이프를 담을 수 없어 같은 500 — 한 커밋으로 묶기 좋음(가치 2/위험 1/S) ·roleRequest의 key·name·description에도 같은 검증이 없음(가치 2/위험 1/S) ·fetchBrandingSource의 오류 메시지에 upstream 상태 코드와 err 원문이 샘(가치 2/위험 2/S)
2026-09-10
- 선택: 반려된 앱을 소유자가 다시 검토로 올릴 수 있게 (가치 4 / 위험 2 / 작업량 S)
- 결과: 성공
- 요약: 소유자에게 제출 경로는
PUT /apps/{app}하나뿐인데(별도 submit endpoint가 없다)store.UpdateApp은 status를 전혀 건드리지 않고,updateApp·mcpUpdateApp의 재제출 분기는before.Status == published일 때만 돌았다. 그래서 반려된 앱은 소유자가 지적받은 내용을 그대로 고쳐도rejected에 머물고 pending review가 하나도 생기지 않아, 관리자가 손으로 상태를 바꿔 주지 않으면 영구히 카탈로그에 못 올라갔다 — 정작 사용자 가이드는 “반려 사유를 반영하고 다시 제출”하라고 안내한다(docs/guides/user/index.html:90). 로컬 postgres:16-alpine 컨테이너로 전체 수명주기를 직접 확인했다: 반려 뒤 소유자가 serviceUrl을 고쳐UpdateApp을 호출하면 status는rejected, pending review는 0개였고, 재제출을 끼우면pending_review+ level 1 review가 생겨 검토자가 승인해published까지 도달했다. 새 순수 함수resubmitAfterEdit를 REST·MCP 두 수정 경로가 함께 쓰게 해 workflow가 켜져 있으면 반려된 앱을 재제출하고, 게시된 앱은 기존대로 재승인 정책을 따르게 했다. workflow가 꺼져 있으면SubmitApp이 곧바로 published로 만들기 때문에 반려된 앱을 검토 없이 카탈로그로 밀어 넣지 않도록Enabled조건을 반드시 유지했다. MCPapp_updatetool 설명과 OpenAPIupdateApp에도 이 재제출 의미를 문서화했다. 검증: 상태·정책 조합 8개 테이블 테스트를 추가하고 store 통합 테스트에 “수정만으로는 rejected에서 벗어나지 못하고 재제출은 level 1로 되돌아온다”는 단언을 더한 뒤gofmt -l,go vet ./...,go test -race(CI와 같은 패키지 목록), DSN을 준go test -race ./internal/store ./internal/httpapi ./internal/database재실행,go build ./cmd/server,check-env-contract.sh·check-docs.sh모두 통과. 프런트엔드는 변경하지 않아 npm 검사는 생략(앱 수정 완료 화면이 이미 “승인 Workflow 설정에 따라 즉시 게시되거나 검토 대기 상태가 됩니다”라고 안내한다). 커밋 698b5e1. -
보류 아이디어: 소유자가 반려 사유를 볼 수 있는 화면이 없음 — 이번에 재제출 경로는 열었지만 /me/apps 응답에 reason이 없어 무엇을 고쳐야 하는지 추측이 된다(가치 3/위험 2/M) ·
clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 rate limit bucket을 공유 — 신뢰 프록시 설정이 필요한데 환경변수 계약이 네 개로 고정(가치 3/위험 3/M) · pending_review 상태의 앱을 수정해도 검토가 level 1로 되돌아가지 않아 다단계 승인에서 level 2 검토자가 level 1이 본 적 없는 내용을 승인함 — 정책 결정이 먼저 필요(가치 2/위험 3/S) ·fetchBrandingSource의 오류 메시지에 upstream 상태 코드와 err 원문이 샘(가치 2/위험 2/S) · 검증 하드닝 계열(adminUpdateUser·개인 키 이름·roleRequest·관리자 write path param)은 사람이 같은 접근의 PR 6854375를 반려했으므로 모두 rejected로 내렸다. - 릴리즈: v2.5.6 (2026-09-10, run 2026-09-10-114115-appstore-improve)
2026-09-10
- 선택: 소유자에게 반려 사유를 보여 주기 (가치 4 / 위험 1 / 작업량 M)
- 결과: 성공
- 요약: 소유자는
reviews:read권한이 없어 반려 사유를 볼 수 있는 화면이 하나도 없었다 —/me/apps응답에 review가 없고, 개인 대시보드·내 앱 목록·앱 수정 화면 모두 status만 렌더링하며, 사유는 검토자 전용/reviews뒤에만 있었다. 정작 사용자 가이드는 “반려 사유를 반영하고 다시 제출”하라고 안내해(docs/guides/user/index.html:90) 지난 회차에 연 재제출 경로가 추측이 됐다. 새store.LatestReviewsByApp(app_id별DISTINCT ON, created_at DESC)가 각 앱을 지금 상태로 만든 마지막 검토를 돌려주고,myApps핸들러가 그것을 항목마다review로 실어 준다 — 재제출하면 마지막 검토가 다시 pending이 되므로 끝난 사유가 남지 않는다. 프런트엔드는RejectionNotice하나를 내 앱 카드 아래·개인 대시보드·앱 수정 폼 위에 두어 사유와 결정 시각·검토자·검토 단계를 보여 준다(사유는 textarea라 줄바꿈을whitespace-pre-line으로 유지). OpenAPI에Review·MyApp·MyAppPage스키마를 추가하고listMyApps응답을 그것으로 바꿨다. 검증: store 통합 테스트에 “반려 뒤에는 사유가 담긴 rejected 검토가, 재제출 뒤에는 level 1 pending 검토가 돌아온다”는 단언을 더하고(ORDER BY를 ASC로 뒤집어 실제로 실패하는 것을 확인) MyAppsPage 렌더링 테스트 2개를 새로 썼다.gofmt -l,go vet ./...,go test -race(CI와 같은 패키지 목록), 로컬 postgres:16-alpine 컨테이너로go test -race ./internal/store ./internal/httpapi ./internal/database재실행,go build ./cmd/server,npm run lint,npm test,prettier --check,npm run build,check-env-contract.sh·check-docs.sh·check-offline-assets.sh모두 통과. Playwright는 mock 데이터에 review 필드가 없어 렌더링이 달라지지 않으므로 CI에 맡겼다. 커밋 80c4fac. -
보류 아이디어:
clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서는 익명 사용자 전체가 하나의 rate limit bucket을 공유 — 신뢰 프록시 설정이 필요한데 환경변수 계약이 네 개로 고정(가치 3/위험 3/M) · 검토 이력(승인·반려 반복)이 소유자에게 마지막 한 건만 보여 여러 번 반려된 앱의 앞선 지적이 사라짐 —/me/apps/{id}/reviews같은 읽기 경로가 필요(가치 2/위험 1/M) · pending_review 상태의 앱을 수정해도 검토가 level 1로 되돌아가지 않아 다단계 승인에서 level 2 검토자가 level 1이 본 적 없는 내용을 승인함 — 정책 결정이 먼저 필요(가치 2/위험 3/S) ·fetchBrandingSource의 오류 메시지에 upstream 상태 코드와 err 원문이 샘(가치 2/위험 2/S) · 앱 카드가 status를 전혀 표시하지 않아/my/apps에서 초안·검토 대기·보관됨을 구분할 수 없음(가치 3/위험 1/S) - 릴리즈: v2.5.7 (2026-09-10, run 2026-09-10-192110-appstore-improve)
2026-09-11
-
(원장 항목에 비밀/내부 정보 의심 문자열이 있어 비공개 기록으로 옮김 — run 2026-09-11-011110-appstore-improve)
- 릴리즈: v2.5.8 (2026-09-11, run 2026-09-11-034149-appstore-approve)
2026-09-12
- 선택: [캠페인 tracking-2026-09] 관리자가 화면에서 붙이는 방문 추적 스크립트와 nonce 기반 CSP (가치 5 / 위험 2 / 작업량 M)
- 결과: 성공
- 요약: 이 저장소는 React SPA를 제공하므로 캠페인 대상이다. TRACKING-STANDARD.md와 kanpic 참조 구현(analytics.go·violations.go·policyFor)을 읽고 같은 능력을 이 저장소 구조에 맞게 만들었다 — 새
internal/analytics패키지(Config·Validate·Snippet·PolicySources·SnippetOrigins·Recorder),system_settings.analytics한 행에 저장(기본값 꺼짐, 시드·migration 없음 → 새 설치와 기존 설치 모두 화면을 손대기 전까지 아무것도 달라지지 않는다), 관리자 라우트GET/PUT /admin/analytics·GET/DELETE /admin/analytics/violations·POST /admin/analytics/allowed-hosts, 비인증POST /api/v1/analytics/csp-report(8KB 상한, 언제나 204),/momento/*같은 오리진 프록시(세션 쿠키·Authorization 제거, 15초·256KB 상한, 추적 꺼짐이면 404), 관리 콘솔의/admin/analytics화면(provider 선택은 Momento 첫 자리, 차단된 출처 표와 “허용에 추가” 한 번 클릭). CSP는 기존SecurityHeaders의 고정 문자열을basePagePolicy상수로 빼고 새ContentSecurityPolicy미들웨어가 추적이 켜진 페이지 요청마다 nonce를 만들어script-src에 넣고 SPAHandler의 새Decorate훅이 같은 nonce를 스니펫의 모든<script>에 붙여 index.html에 삽입한다;'unsafe-inline'은 쓰지 않았고 끄면 원래 문자열로 돌아가며,/api/*·/mcp·/health*·/momento/*는default-src 'none'으로 더 좁아진다. 정적 자산과 비화면 경로는 설정을 읽지 않는다. 검증: 표준의 테스트 항목을 그대로 덮는 Go 테스트 23개(꺼져 있으면 스니펫·정책 변화 없음, placement별 삽입 위치, 요청마다 다른 nonce가 정책과 스니펫에 함께, 스니펫에서 읽은 출처가 script/connect/img-src에, 비화면 경로 narrow policy, include_admin, 8KB 초과 저장 거부, 신고 수신 시 출처·지시어 기록과 중복 미증가, 100건 상한, 프록시가 자격 증명을 떼는지)와 React 테스트 3개를 더했고gofmt -l,go vet ./...,go test -race(CI와 같은 패키지 목록), 임시 postgres:16-alpine 컨테이너로go test -race ./internal/store ./internal/httpapi ./internal/database(store 통합 테스트에 “시드 없는 DB는 꺼짐으로 읽히고 8KB 초과는 행을 만들지 않으며 저장 후 정규화된다”는 단언 추가),go build ./cmd/server,npm run lint,npm test,prettier --check,npm run build,check-offline-assets.sh·check-env-contract.sh·check-docs.sh, 그리고make screenshots VERSION=v2.5.8(Playwright desktop·mobile 전체 통과)까지 모두 통과. 새 라우트는 check-docs.sh가 router.tsx의 모든 경로에 캡처를 요구하므로 admin-analytics 캡처 2장과 사이드바 항목이 늘어난 관리자 캡처 19장을 갱신했다(공개·개인 화면 캡처는 바이트 동일; 렌더링이 흔들린 my-home-desktop 한 장만 HEAD로 되돌렸다). docs/ADMIN_GUIDE.md에 4.9 절(설정표, Momento 프록시, CSP 동작 3단계, 차단된 출처 사용법)과 장애 대응 3행·보안 기본값 1행·7.2 헤더 설명을 더하고 aidev/tools/guide/md2pdf.mjs로 PDF를 다시 구웠다. OpenAPI에 5개 오퍼레이션과AnalyticsSettings·CspViolation스키마를 추가. 실제 수집기로 수집이 들어오는 것은 이 환경에 Momento가 없어 확인하지 못했다(표준 검증 항목 중 유일하게 남은 것). 커밋 1294a77. -
보류 아이디어: 앱 카드가 status를 표시하지 않아 /my/apps에서 초안·검토 대기·보관됨을 구분할 수 없음 — 소유자 문맥에서만 배지를 켜는 prop 하나면 된다(가치 3/위험 1/S) ·
clientAddress가 RemoteAddr만 보아 reverse proxy 뒤에서 rate limit이 전역이 됨 — 신뢰 프록시 설정이 필요한데 환경변수 계약이 네 개로 고정(가치 3/위험 3/M) · 캡처 fixture에 반려된 앱이 없어 반려 안내 화면을 찍을 수 없음 — 이번에 캡처 파이프라인이 이 환경에서 도는 것을 확인했으므로 다음 회차에 fixture만 더하면 된다(가치 3/위험 2/M) · 방문 추적 화면에 “정책 미리보기”(저장 전 현재 설정이 만들 CSP 문자열 표시)를 두면 관리자가 브라우저를 띄우기 전에 출처를 확인할 수 있음 —pagePolicy가 이미 순수 함수라 GET 한 개면 된다(가치 2/위험 1/S) · 소유자가 볼 수 있는 검토 이력이 마지막 한 건뿐 —/me/apps/{id}/reviews읽기 경로 필요(가치 2/위험 1/M) - 릴리즈: v2.6.0 (2026-09-12, run 2026-09-12-140953-appstore-improve)