# task-3003 — M7: 푸시 알림 발송 완성 (웹푸시 payload 암호화)

**레벨**: Lv.2 · **팀**: dev5-team (마르둑) · **회장 승인 완료** (2026-08-24)

## 배경 — 푸시 알림이 지금 동작하지 않는다

**ANU 실측 (`supabase/functions/send-push/index.ts`, origin/main)**
```js
const response = await fetch(subscription.endpoint, {
  method: 'POST',
  headers: {
    'Authorization': `vapid t=${jwt}, k=${vapidPublicKey}`,
    'Content-Type': 'application/json',
    'TTL': '86400',
    'Content-Length': '0',        ← body 없음
  },
});
// 주석 원문: "For now, send unencrypted (TTL 0 for urgency) - simplified push"
```
함수는 `payload: string` 인자를 **받고도 싣지 않는다.** 내용 없는 "tickle" 푸시만 나간다.

**원인**: 웹푸시(RFC 8291)는 payload 를 실으려면 **암호화가 필수**다.
`aes128gcm` + ECDH(P-256) + HKDF 로 구독자의 `p256dh`/`auth` 키를 써서 암호화해야 한다.
그 구현이 없어서 body 를 뺀 채로 둔 것이다. **단순 버그가 아니라 미완성 구현이다.**

## ★ 선행 조건 — 이미 충족됨 (순서 확인)
```
H2 (엔드포인트 오리진 화이트리스트)  ✅ 완료 (task-3000, PR #247)
3단계 (anon 정책 3건 DROP)          ✅ 완료 (2026-08-22 ANU 집행)
→ 지금 M7 착수 가능
```
★ 이 순서가 중요했던 이유: H2 없이 payload 를 실으면 **상담 미리보기가 공격자 서버로 평문 전송**된다.
지금은 엔드포인트가 FCM/Mozilla/Apple/WNS 계열로 제한돼 있어 안전하다.
**이 화이트리스트를 우회하거나 완화하지 마라.**

---

## 고칠 것

### ① 웹푸시 payload 암호화 구현 (RFC 8291 `aes128gcm`)
- 구독자의 `p256dh`(공개키) · `auth`(인증 시크릿)로 암호화
- Deno(Supabase Edge Function) 환경이다. **웹 표준 `crypto.subtle` 로 구현 가능한지 먼저 조사**하고,
  검증된 라이브러리가 있으면 그것을 쓰는 편을 우선 검토하라. 직접 구현은 암호 오류 위험이 크다.
- ★ 라이브러리 선택 시 **Deno/Edge 런타임 호환**과 **의존성 신뢰성**을 근거와 함께 보고하라.
- `Content-Length: 0` 하드코딩 제거. 실제 본문 길이로.

### ② 실제 전달 검증
암호화가 틀리면 푸시 서비스가 **201 을 주고도 기기에 아무것도 안 뜬다.**
따라서 "요청이 성공했다"로 완료 판정하지 마라.
- 실제 브라우저에서 **알림이 화면에 뜨는 것**을 확인하고 스크린샷을 첨부하라
- 알림 제목·본문이 의도한 내용인지 확인하라

### ③ 실패를 조용히 넘기지 마라
- 푸시 서비스 응답 코드별 처리: `404`/`410` = 구독 만료 → 해당 구독 비활성화
- 그 외 실패는 로그에 남긴다
- ★ **로그에 endpoint 전체를 남기지 마라.** 경로에 푸시 토큰(비밀값)이 들어 있다.
  호스트만 남긴다(task-3000 이 세운 원칙과 동일).

### ④ 미리보기 내용 범위
현재 코드가 `messagePreview?.slice(0, 100)` 로 상담 메시지 앞 100자를 싣는다.
**이게 실제로 전송되기 시작한다는 뜻이다.** 개인정보 관점에서 적절한지 판단하고,
과하다고 보이면 **줄이지 말고 ANU 에 보고**하라(회장 결정 사항).

---

## 검증 (전부 실측)
1. **변경 전 재현** — 현재 상태로 푸시를 보내 **내용 없는 알림**이거나 아무것도 안 뜨는 것을 확인.
2. **변경 후 실제 수신** — 브라우저에서 **제목·본문이 보이는 알림** 스크린샷. 픽스처 아님을 명시.
3. **다중 브라우저** — 최소 Chrome(FCM) 1종은 필수. 가능하면 Firefox 도.
4. **만료 구독 처리** — 폐기된 endpoint 로 보내 `404`/`410` 시 구독이 비활성화되는지 확인.
5. **화이트리스트 무손상** — task-3000 의 `is_allowed_push_endpoint` 가 여전히 작동하는지.
   `javascript:` · 미허용 호스트가 차단되는지 재확인(회귀 방지).
6. **비밀 유출 없음** — 로그·보고서에 endpoint 전체·VAPID 개인키·`auth` 시크릿이 남지 않았는지 grep 으로 확인.
7. **봉인 증명** — 새 테스트를 **변이시켜** 실제로 FAIL 하는지 확인하고 복원하라.
8. 기존 회귀 유지 (**base 재측정 기준선**. 명세 수치 아님).

## 범위 · 금지
**수정 허용**: `supabase/functions/send-push/**` · `supabase/functions/_shared/**`(필요 시 최소) · 관련 테스트

**금지**:
- `is_allowed_push_endpoint` · `chat_register_push_subscription` · `chat_unsubscribe_push` **변경**
  (task-2999/3000 결과물. 화이트리스트·상한 완화 절대 금지)
- `push_subscriptions` RLS 정책 변경
- `src/lib/push-utils.ts` (task-2999 결과물)
- `src/pages/PolicyAnalysis.tsx` · `src/components/Silson*` — task-3002 분석 대상 영역
- `scripts/youtube-pipeline/**` (다른 저장소)
- VAPID 키를 코드·로그·보고서에 노출

## allowed_resources

```yaml
allowed_resources:
  paths:
    - "supabase/functions/send-push/**"
    - "supabase/functions/_shared/**"
    - "supabase/functions/**/__tests__/**"
    - "tests/**"
    - "memory/reports/task-3003.md"
  forbidden_paths:
    - "supabase/migrations/**"
    - "src/lib/push-utils.ts"
    - "src/pages/**"
    - "src/components/**"
    - "server/**"
    - "ops/**"
    - ".github/**"
  commands:
    - "npm run build"
    - "npx vitest run"
    - "deno"
    - "python3"
  merge_policy: "tiered"
  ttl_hours: 72
```

## 운영 계약
- `origin/main`(=`51718e8`) 기준 `git pull --ff-only` 후 시작
- ★ `gh` 호출 시 `GH_TOKEN="$BOT_GITHUB_TOKEN"` 주입 필수 (회장 개인 PAT 금지 — 감사기록 오염)
- 워크플로우 `/home/jay/workspace/prompts/DIRECT-WORKFLOW.md` · QC `/home/jay/workspace/teams/shared/QC-RULES.md`
- `WORKSPACE_ROOT=/home/jay/workspace` · `CHAT_ID=6937032012` · 수집자 key `ANU_KEY=c119085addb0f8b7`
- 완료 경로는 `finish-task.sh` 실행이 유일하다. 수동 `.done` 금지.
- **명세와 실측이 다르면 임의 판단하지 말고 ANU 에 보고하라.**

★ **참고 — 현재 실사용 규모**: 대화 1건 · 상담 토큰 1건 · 푸시 구독 **0행**.
즉 이 기능은 아직 실사용자가 없다. **지금이 제대로 만들 최적기**이며, 고치다 깨질 사용자도 없다.
반대로 말하면 **서두를 이유도 없다.** 암호화를 어림짐작으로 구현하느니 조사에 시간을 더 써라.

## 완료 조건
payload 암호화 구현 · **실제 알림 수신 스크린샷** · 만료 구독 처리 · 화이트리스트 무손상 재확인 ·
비밀 유출 0 · 변이로 봉인 실증 · 회귀 유지

## 보고
**★★★ PR 생성까지가 범위다. 머지는 ANU 가 한다. 절대 직접 머지하지 마라.**
`memory/reports/task-3003.md` 작성 후 표준 완료 콜백 등록. 콜백 프롬프트 **UTF-8 3900 bytes 이하**.