# [fix] PR-D2 isAllowedCrmUrl — egress 게이트 마감 (쿼리/프래그먼트 거부 + :444 no-fetch 통합테스트)

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "extension/**"
  forbidden_paths:
    - "src/**"
    - "server/**"
    - ".github/**"
    - "docs/**"
  commands:
    - "npx vitest"
    - "git"
  merge_policy: "none"
  ttl_hours: 6
```
> ★ merge_policy=none. 브랜치 **`task/task-2836-dev1`**(PR #120)에 이어서 커밋(새 PR 금지).

## 배경 (Codex 재검토 2 잔여)
포트 갭(`:444`)은 직전 커밋 `1c2a9d6`로 수정 완료(prod 호스트 `""`/`443`만). Codex 재검토가 남은 2개 지적:
1. **`:444 → fetch 미호출` 통합 케이스 미증명**: allowlist bool=false 는 테스트하나, egress 함수(예: `crmFetch`/`sendCrm*`)를 실제로 통과시켜 **fetch가 호출되지 않고 `URL_NOT_ALLOWED`(또는 동등) 반환**되는 end-to-end 케이스가 없음.
2. **쿼리스트링/프래그먼트 미검증**: `isAllowedCrmUrl` 이 pathname 만 exact-match 하고 `url.search`/`url.hash` 를 안 봐서 `https://insuro.biz/<허용경로>?foo=bar` 가 통과. (URL 은 코드 상수 구성이라 실제 주입 벡터는 아니나, exact-origin·fail-closed 원칙상 닫는다.)

## 수정 (surgical — 2줄 수준)
1. `isAllowedCrmUrl`: pathname exact-match 뒤에 **`if (url.search !== "" || url.hash !== "") return false;`** 추가. 즉 허용 경로라도 쿼리/프래그먼트 있으면 거부. (프로그램의 exact-origin 마감.)
2. `isAllowedCrmUrl` **외 다른 로직/함수 건드리지 마라.** 포트/username-password/protocol/pathname 기존 검증 그대로.

## 검증 (신규 테스트 2건 + 회귀)
1. **:444 no-fetch 통합**: 실제 egress 함수(crm 검색 또는 생성 전송 경로)를 `https://insuro.biz:444/...` 로 유도 → **fetch 0회 호출**(mock 확인) + `URL_NOT_ALLOWED`(또는 코드상 실패값) 반환. 단순 allowlist bool 이 아니라 **전송 함수 관통**.
2. **쿼리스트링 거부**: `isAllowedCrmUrl("https://insuro.biz/<CRM_SEARCH_PATH>?x=1")` → false. `.../<path>#frag` → false. 순수 `.../<path>` → 여전히 true.
3. 기존 crm 테스트(core·single-egress·canary·ui) + 확장 전체(**372**) 회귀 0. `npx vitest run extension/__tests__/`.

## 완료
- `extension/background/crm.js` + 테스트 외 변경 0 (`git diff --stat`)
- **dev6 금지** — dev1. worktree finish → push(PR #120 반영·새 PR 금지) → ANU 독립검증. **머지 금지**.
- **ANU callback**(UTF-8 ≤3900 bytes, envelope). ⚠️ 세션 사망 대비: 완료 즉시 finish-task(push) 우선.

## goal_assertions (auto-generated)
- `npx vitest run extension/__tests__/`
