Files
clean-architecture-frontend…/docs/reviews/adapters/04-browser-transfer.md
T
DongHyeonkaandClaude Opus 5 4e87bacdf3 fix: enforce installed HTTP auth profiles
Install the REST auth profile registry once at composition and make it the
single transport authority for V3. Contract composition now rejects an
unregistered authProfileId, so the executor never resolves a profile at
runtime.

The credential collaborator contributes proof headers only: Fetch credentials
come from the resolved profile, transport-owned and forbidden headers are
rejected, headers outside the profile's allowed set are rejected, and a missing
required header fails closed as AUTH_INTEGRATION_FAILURE with zero fetch calls.
The final invariant re-proves credentials mode and the exact header sets.

Demo mode satisfies the strict bearer profile with a fixed non-secret marker
instead of weakening REFERENCE_EXTERNAL_BEARER. Credential owners now receive
the operation lifetime through AuthOperationContext.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:56:44 +09:00

31 KiB

Adapter Review — Browser Transfer

검토 기준: develop / 4dc033c (2026-08-13)

범위: src/adapters/browser-transfer/**, 직접 연결된 application port, unit test, docs/architecture/presigned-transfer-and-image-cdn.md

결론

브라우저 전송 계열은 URL·header·subscription material을 application/presentation에서 차단하고, identity capability와 strict decoder를 사용하는 방향이 좋다. 특히 presigned single-use vault, multipart checkpoint CAS, image preset registry와 private descriptor 서명 검증은 유지해야 한다.

다만 실제 조합 전에 해결해야 할 P1 항목이 세 개 있다.

  1. presigned download는 open()에서 이미 fetch와 timeout을 시작하지만 반환된 source에는 close()가 없다. 호출자가 stream을 늦게 열거나 열지 않으면 정상 API 사용만으로 body/timeout 자원이 방치된다 (BT-PRE-01).
  2. IndexedDB checkpoint partition 삭제는 BLOCKED를 반환한 뒤에도 native deleteDatabase()가 늦게 commit될 수 있다. 반환 결과가 실제 effect certainty를 표현하지 못한다 (BT-UP-03).
  3. presigned capability wire envelope에는 top-level protocol literal이 없다. 이미 아키텍처 문서가 요구한 PRESIGNED_TRANSFER_V1을 실제 request/response decoder가 아직 강제하지 않는다 (BT-PRE-02).

파일 크기만을 이유로 나누면 안 되지만, resumable-upload-runtime.ts 2,196줄과 image-cdn-runtime.ts 1,340줄은 각각 state transition, I/O orchestration, retry, persistence, presentation projection을 동시에 소유한다. characterization test를 먼저 고정한 뒤 State Machine·Saga·Strategy 경계로 분리하는 것이 안전하다.

판정 기준

표기 의미
P1 조합 또는 배포 전에 수정. 결과 거짓 보고, 보안/정합성, 자원 수명주기 결함
P2 다음 리팩터링 묶음에서 수정. 계약 모호성, 실패 격리, 유지보수 위험
P3 동작을 고정한 뒤 정리. 테스트 seam, 중복, 가독성
VERIFIED_DEFECT 현재 코드 경로만으로 재현 가능한 결함
CONTRACT_GAP provider/consumer 간 의미가 타입이나 decoder에 충분히 고정되지 않음
REFACTOR 현재 외부 동작은 보존하면서 내부 책임을 재배치
PLANNED_GAP 기존 아키텍처 문서가 이미 미구현으로 선언한 항목. 현재 구현의 회귀로 계산하지 않음
KEEP 의도와 테스트가 일치하므로 변경하지 않음

전체 파일 판정

모듈 현재 역할 판정 후속 항목
browser-transfer/index.ts 하위 capability export KEEP public export 증가는 각 capability 계획에서만 수행
presigned/index.ts presigned public surface KEEP BT-PRE-04에서 vault issuer 노출만 축소 검토
presigned/incremental-sha256.ts streaming SHA-256 KEEP WebCrypto digest()로 바꾸면 전체 buffering이 되므로 교체 금지
presigned/presigned-capability-http-provider.ts BFF capability 발급, strict decode CONTRACT_GAP BT-PRE-02, BT-PRE-03, BT-X-01
presigned/presigned-capability-vault.ts identity capability 보관/폐기 REFACTOR BT-PRE-04
presigned/presigned-transfer-executor.ts GET stream/PUT part 실행 VERIFIED_DEFECT BT-PRE-01, BT-PRE-03, BT-X-01
resumable-upload/checkpoint-schema.ts durable schema guard KEEP schema V1 golden fixture 유지
resumable-upload/fetch-json-transport.ts bounded JSON control transport VERIFIED_DEFECT BT-UP-01, BT-UP-02, BT-X-01
resumable-upload/http-control-plane-adapter.ts operation별 wire decoder KEEP/REFACTOR runtime 분리 뒤 decoder만 남김
resumable-upload/index.ts resumable public surface KEEP facade 호환 유지
resumable-upload/indexeddb-checkpoint-store.ts scope-bound CAS store/admin VERIFIED_DEFECT BT-UP-03
resumable-upload/presigned-upload-part-executor.ts multipart와 presigned bridge VERIFIED_DEFECT BT-UP-04
resumable-upload/resumable-upload-runtime.ts session state/retry/part scheduling/commit REFACTOR BT-UP-05, BT-UP-06
resumable-upload/runtime-policy.ts hard bound snapshot KEEP 값 변경은 contract migration으로만 수행
resumable-upload/upload-byte-source.ts stream/range source snapshot과 hashing KEEP/REFACTOR runtime에서 source preparation Strategy로 주입
resumable-upload/upload-cancellation-channel.ts best-effort cross-context cancel hint KEEP backend/CAS가 authority라는 주석과 동작 유지
resumable-upload/upload-mutation-lock.ts Web Lock exclusive mutation PLANNED_GAP BT-UP-07
image-cdn/README.md 안전한 composition 예제 KEEP resolve signal 결정 반영 필요
image-cdn/browser-image-probe.ts bounded fetch/header/static decode probe VERIFIED_DEFECT BT-IMG-02
image-cdn/image-cdn-policy.ts origin/preset/hard-limit registry KEEP composition-owned identity reference 유지
image-cdn/image-cdn-runtime.ts asset acceptance, signature, URL/projection REFACTOR BT-IMG-01, BT-IMG-03
image-cdn/image-header-metadata.ts PNG/JPEG/WebP/AVIF static header parser KEEP 별도 fuzz/golden corpus로 보호; 작은 parser로 임의 분해 금지
image-cdn/p256-image-capability-verifier.ts P-256 P1363 verifier KEEP key overlap contract 유지
image-cdn/index.ts image public surface KEEP descriptor provider가 생길 때만 export 확장

직접 연결 경계도 다음과 같이 대조했다: src/application/ports/browser-transfer/authorized-download.ts, src/application/ports/browser-transfer/image-cdn.ts, src/application/ports/browser-transfer/presigned-transfer.ts, src/application/ports/browser-transfer/resumable-upload.ts, barrel src/application/ports/browser-transfer/index.ts, 그리고 presigned source의 직접 consumer src/adapters/browser-files/download-delivery-adapter.ts. native URL/header/File/Response를 application port로 올리지 않는 방향은 유지하며, BT-PRE-01close() migration은 이 consumer까지 포함한다.

Presigned transfer 상세

BT-PRE-01 — open()이 반환되기 전에 download lease가 시작됨

  • 우선순위/분류: P1 / VERIFIED_DEFECT
  • 근거: presigned-transfer-executor.ts:114-192, :408-631
  • 현재 동작:
    • openDownload()이 capability를 claim/consume한 뒤 즉시 fetch()를 수행한다.
    • timeout scope도 open() 안에서 시작한다.
    • response body와 scope는 반환된 PresignedDownloadByteSource.stream()을 완주하거나 실패해야만 해제된다.
    • source port에는 close()/dispose()가 없다.
  • 영향:
    • 호출자가 source를 받은 뒤 stream 시작을 늦추면, 실제 consumer deadline이 아니라 open() 시점의 timeout으로 실패한다.
    • 호출자가 stream을 열지 않으면 body cancellation과 listener/timer cleanup을 명시적으로 수행할 방법이 없다.
    • capability는 이미 single-use로 소비되므로 동일 source를 복구할 수도 없다.

결정: lazy, single-start lease로 변경한다. open()은 policy/vault 검증과 capability consume까지만 수행하고 fetch는 첫 stream(signal) 진입 시 시작한다. source에 close(): void를 추가해 미사용 lease도 명시적으로 폐기한다. close()와 stream의 first-start는 하나의 state machine을 공유한다.

type PresignedDownloadByteSource = Readonly<{
  byteLength: number;
  capability: PresignedDownloadCapability;
  integrity: "VERIFIED_ON_SUCCESSFUL_EXHAUSTION";
  stream(signal: AbortSignal): AsyncIterable<BrowserDataResult<Uint8Array>>;
  close(): void;
}>;

type DownloadLeaseState = "READY" | "STREAMING" | "CLOSED";

구현 규칙:

  1. READY -> STREAMING만 fetch를 시작한다.
  2. READY -> CLOSED는 network I/O 없이 끝낸다.
  3. STREAMING -> CLOSED는 composed signal abort, reader/body cancel, timer/listener release를 한 번만 수행한다.
  4. 두 번째 stream()은 기존처럼 CONFLICT / REISSUE_CAPABILITY다.
  5. digest 성공 전 chunk는 현재의 VERIFIED_ON_SUCCESSFUL_EXHAUSTION 의미를 유지한다. consumer는 최종 success 전 파일을 commit하면 안 된다.
  6. stream() 직전에 capability expiry와 minimum remaining lifetime을 다시 확인하고, open() 때 받은 outer signal과 stream signal을 함께 적용한다. 오래 보관되어 만료된 source는 fetch를 시작하지 않는다.

테스트 추가 (tests/unit/presigned-transfer.test.ts):

  • does not fetch until the returned download source starts streaming
  • closes an unused source without issuing a request
  • starts the transfer deadline at first stream consumption
  • close during a pending read cancels the reader and releases listeners once
  • stream after close returns one terminal conflict without fetching

마이그레이션: port에 close()를 추가한 뒤 직접 consumer인 src/adapters/browser-files/download-delivery-adapter.ts를 포함한 모든 consumer를 source 획득 직후 try/finally { source.close(); }로 감싼다. size reject, createWritable()/prompt 실패, object-URL strategy의 stream 전 실패도 tests/unit/browser-file-download.test.ts로 고정한다. 그 다음 fetch를 lazy로 옮긴다. rollback은 eager fetch 구현으로 되돌릴 수 있지만 close() API는 유지한다.

완료 조건: 위 테스트와 기존 presigned suite가 통과하고, source를 생성만 한 테스트에서 fetch 호출 수와 active timer가 모두 0이다.

BT-PRE-02 — capability wire envelope의 protocol version 부재

  • 우선순위/분류: P1 / CONTRACT_GAP, 기존 문서의 미완료 항목
  • 근거: presigned-capability-http-provider.ts:173-210, :436-462; docs/architecture/presigned-transfer-and-image-cdn.md:93-108
  • 현재 동작: request body와 strict response key set에 top-level transfer protocol이 없다. multipart binding 내부 protocol만으로 전체 capability envelope version을 식별한다.
  • 영향: 서버가 필드를 추가/재해석할 때 old/new client가 같은 shape를 서로 다른 의미로 받아들일 수 있다. strict decoder라서 단순 필드 추가도 곧바로 장애가 되지만, 장애가 version mismatch로 분류되지 않는다.

결정:

  • request와 response에 protocol: "PRESIGNED_TRANSFER_V1"을 필수로 추가한다.
  • missing/unknown protocol은 현재 closed taxonomy의 POLICY_REJECTED, retryable false, recovery REISSUE_CAPABILITY로 닫는다. 이 변경에서 새 failure code를 만들지 않는다.
  • multipart의 PRESIGNED_MULTIPART_V1은 하위 binding protocol로 그대로 유지한다.
  • protocol은 PresignedTransferCapability, PresignedCapabilityRegistration/Binding, vault snapshot, executor common-binding validator까지 전파해 request → registration → consumption exact parity를 보장한다.
  • server는 request shape를 협상해 legacy request에는 legacy response, V1 request에는 V1 response를 반환한다. strict legacy decoder를 깨뜨리므로 legacy response에 V1 field를 먼저 emit하거나 한 response에 dual fields를 넣지 않는다.

테스트 추가:

  • request body exact-key snapshot과 protocol literal
  • missing, V0, V2 protocol response 거절
  • V1 download와 V1 multipart capability 수락
  • protocol mismatch가 vault register() 전에 종료됨

배포 순서: request-shape negotiated provider 배포 → V1 client 배포 → old-client drain 기간 관찰 → provider legacy request/response 제거. rollback 시 provider는 두 request shape를 계속 수락하되 각각 matching exact response를 반환한다.

BT-PRE-03 — timeout이 non-cooperative fetch를 실제로 bound하지 못함

  • 우선순위/분류: P2 / VERIFIED_DEFECT
  • 근거: capability provider :186-214, executor presigned-transfer-executor.ts:157-190, 각 파일의 createAbortScope()
  • 현재 동작: timer는 AbortController만 abort한다. injected fetcher 또는 host가 signal을 무시하면 await fetcher(...) 자체는 끝나지 않는다.
  • 영향: API가 선언한 timeout이 hard bound가 아니며, teardown도 fetch settlement에 묶인다.

결정: 공통 AbortableOperationScoperace(task, onLateValue)를 사용한다 (BT-X-01). deadline/caller abort가 먼저 끝나면 즉시 typed failure를 반환하고, 늦게 온 Response는 body를 취소한다. timer 생성 실패 시 이미 붙인 external listener를 즉시 제거한다.

테스트 추가:

  • signal을 무시하는 fetch Promise가 timeout 뒤에도 pending인 fixture
  • timeout 결과가 정시에 반환되고 late response body가 취소되는지 검증
  • scheduler setTimeout/clearTimeout throw 시 listener 누수와 public rejection이 없는지 검증

BT-PRE-04 — vault가 스스로 registration invariant를 소유하지 않음

  • 우선순위/분류: P2 / REFACTOR
  • 근거: presigned-capability-vault.ts:112-174
  • 현재 동작: HTTP provider가 URL, header, expiry, byte/digest를 검사하지만 exported vault의 register()는 전달받은 registration을 그대로 snapshot한다.
  • 영향: 다른 issuer adapter가 추가되거나 테스트/조합 코드가 vault를 직접 사용하면 동일한 capability 타입에 더 약한 invariant가 들어갈 수 있다.

결정: issuer/consumer 권한을 wiring 단계에서 분리하고 공통 invariant validator를 적용한다.

  1. createPresignedCapabilityVault(){ issuer: PresignedCapabilityIssuer; consumer: PresignedCapabilityConsumer }를 반환한다. provider option에는 issuer만, executor option에는 consumer만 전달한다. root barrel에는 factory와 consumer-facing type만 export하고 issuer type은 provider의 구조적 parameter로 숨긴다.
  2. issuer 등록 직전 공통 validatePresignedCapabilityRegistration()으로 method/binding/URL/header/status/bytes/digest/expiry를 다시 검증한다.
  3. HTTP decoder는 wire-specific shape를 검사하고, vault validator는 runtime invariant만 검사한다. decoder 로직을 통째로 중복하지 않는다.

테스트: malformed registration을 직접 issuer seam에 넣는 table test와, HTTP provider의 valid 결과가 동일 snapshot으로 등록되는 parity test를 추가한다.

BT-PRE-05 — encoded path의 provider 해석 차이

  • 우선순위/분류: P2 / SECURITY_HARDENING
  • 근거: presigned-capability-http-provider.ts:517-533, :1078-1097
  • 현재 동작: literal ./..와 backslash는 거절하지만 %2f, %5c, %25... 같은 encoded separator가 object-store/CDN에서 한 번 더 decode되는지 계약이 없다.
  • 결정: raw URL.pathname의 각 segment를 strict UTF-8 percent-decode한다. decoded segment에서 /, backslash, NUL, ./.., 그리고 literal % 뒤 두 hex digit을 거절한 뒤, 대문자 percent-hex canonical encoder 결과와 raw segment를 비교한다. 이 규칙은 %252e%252e double encoding을 닫고 valid opaque UTF-8 segment는 허용한다. CDN/provider conformance fixture가 같은 canonicalizer를 사용한다.
  • 테스트: %2F, %5C, %252e%252e, mixed-case encoding, valid UTF-8 opaque segment를 포함한다.

Resumable upload 상세

BT-UP-01 — AbortSignal 구조 검증과 cleanup 사용이 불일치

  • 우선순위/분류: P2 / VERIFIED_DEFECT
  • 근거: fetch-json-transport.ts:548-582, :670-677
  • 현재 동작: isAbortSignal()abortedaddEventListener만 검사하지만 FetchAttempt.release()removeEventListener()를 무조건 호출한다.
  • 영향: 구조적으로 허용된 signal이 finally에서 throw하여 typed result 대신 Promise rejection을 만든다.
  • 수정: native getter 기반 또는 최소한 removeEventListener까지 포함한 공통 guard를 사용하고, release cleanup은 terminal result를 덮지 않도록 catch한다.
  • 테스트: remove가 없는 structural fake는 입력에서 INVALID_INPUT; remove가 cleanup 중 throw하는 hostile facade는 typed terminal result를 보존.

BT-UP-02 — transport clock/scheduler가 전역에 고정됨

  • 우선순위/분류: P3 / REFACTOR
  • 근거: fetch-json-transport.ts:571-580, :626-636
  • 현재 동작: request timeout은 global timer, HTTP-date Retry-AfterDate.now()를 직접 사용한다.
  • 결정: dependencies에 clock.now()scheduler를 추가하고 snapshot/validate한다. delta-seconds와 HTTP-date parsing은 같은 captured now를 사용한다.
  • 테스트: fake clock으로 경계값, clock rollback, invalid date, max clamp를 결정론적으로 검증.

BT-UP-03 — deleteDatabase() timeout 뒤 late delete effect

  • 우선순위/분류: P1 / VERIFIED_DEFECT
  • 근거: indexeddb-checkpoint-store.ts:375-439
  • 현재 동작: deletePartition()onblocked 후 timer가 끝나면 BLOCKED를 반환한다. 그러나 IndexedDB delete request는 취소할 수 없고, 다른 tab이 닫히면 반환 이후 onsuccess로 실제 DB가 삭제될 수 있다.
  • 영향: caller가 BLOCKEDNOT_APPLIED로 해석할 수 있지만 native request는 나중에 성공/실패할 수 있어 반환값과 effect certainty가 모순된다. 다른 realm의 open/delete ordering까지 현재 증거 없이 단정하지 않는다.

결정: delete dispatch 이후에는 failure certainty를 NOT_APPLIED로 표현하지 않는다. port outcome을 다음처럼 명시한다.

type PartitionDeleteOutcome =
  | { state: "DELETED"; effect: "APPLIED" }
  | { state: "PENDING"; effect: "UNKNOWN"; reason: "BLOCKED_DEADLINE" };
  • pre-dispatch invalid/aborted/unsupported만 기존 failure다.
  • PENDING을 받은 runtime은 해당 store instance를 terminal closed로 유지한다. 같은 JS realm에서는 (IDBFactory identity, databaseName) pending-deletion registry가 새 factory 생성을 막고 late onsuccess/onerror에서 해제한다. 다른 realm은 native IndexedDB blocked ordering과 명시적 recovery UX로 처리하며 client-only global registry를 주장하지 않는다.
  • late onsuccess/onerror는 observer에 기록한다. 다시 확인하려면 별도 inspectPartitionDeletion() 또는 새 page generation에서 DB 목록/open 결과를 사용한다.
  • 단순히 timer를 제거해 무한 대기시키지는 않는다.

테스트 추가 (tests/unit/resumable-upload-checkpoint.test.ts): blocked deadline → PENDING → late success, blocked deadline → late error, PENDING 뒤 store method가 UNAVAILABLE, caller abort before dispatch, concurrent new runtime 금지.

BT-UP-04 — bridge clock의 non-finite 값이 expiry 검사를 통과함

  • 우선순위/분류: P2 / VERIFIED_DEFECT
  • 근거: presigned-upload-part-executor.ts:36-70
  • 현재 동작: now()NaN 또는 음수이면 expiry 비교를 우회한다. +Infinity는 현재도 expiry 비교에서 거절되지만 dependency failure가 capability policy failure로 잘못 분류된다.
  • 수정: Number.isSafeInteger(nowEpochMs) && nowEpochMs >= 0을 먼저 검사하고 실패 시 UNAVAILABLE / RESUME을 반환한다.
  • 테스트: NaN/음수의 현재 bypass, +Infinity의 현재 rejection, 수정 후 모든 non-finite/negative clock의 UNAVAILABLE / RESUME, throw, 만료 경계 expiresAt === now, 유효 now + 1.

BT-UP-05 — runtime의 상태 전이와 side effect가 한 파일에 결합됨

  • 우선순위/분류: P2 / REFACTOR
  • 근거: resumable-upload-runtime.ts 2,196줄; session resolution, retry, hashing, scheduler, CAS, abort saga, validation과 telemetry를 함께 소유
  • 외부 facade는 유지하고 다음 내부 경계만 추출한다.
새 내부 모듈 책임 적용 패턴
upload-session-state-machine.ts ACTIVE/ABORT_PENDING/completed transition의 순수 함수 State Machine
upload-session-reconciler.ts local checkpoint와 server status 수렴 Reconciler
upload-part-scheduler.ts memory/server/client concurrency와 receipt serialization Bounded Work Queue
upload-retry-executor.ts retry budget, Retry-After, jitter, attempt deadline Policy + Template Method
upload-abort-saga.ts local tombstone → backend abort → checkpoint removal Saga/Compensation
resumable-upload-runtime.ts public facade, lifecycle, mutation lock orchestration만 Facade

추출 순서:

  1. 기존 tests/unit/resumable-upload-runtime.test.ts에 observable call-order characterization를 추가한다.
  2. 순수 transition 함수와 table test를 먼저 만든다.
  3. retry executor, reconciler, part scheduler, abort saga 순서로 한 모듈씩 이동한다.
  4. 각 이동 뒤 기존 suite 전체를 그대로 실행한다. fixture expected 값을 리팩터링에 맞춰 바꾸지 않는다.

변경 금지:

  • part idempotency key derivation
  • server-authoritative status reconciliation
  • accepted receipt의 순차 CAS persistence
  • checkpoint에 URL/credential을 저장하지 않는 규칙
  • first part failure 뒤 이미 시작한 sibling의 확정 receipt를 기다려 저장하는 현재 정책. 이를 즉시 cancel하면 remote success가 ambiguous해질 수 있으므로 별도 behavior change로 다룬다.

BT-UP-06 — sync close()가 drain 완료를 증명하지 못함

  • 우선순위/분류: P2 / LIFECYCLE_REFACTOR
  • 근거: resumable-upload-runtime.ts:280-287
  • 현재 동작: lifetime abort 직후 checkpoint store를 닫고 반환한다. native fetch/IDB가 signal에 반응해 정리될 것으로 기대하지만 caller는 active operation의 terminal settlement를 기다릴 수 없다.
  • 결정: application ResumableUploadPortclose(): void는 admission을 닫고 같은 single-flight drain을 시작하는 호환 facade로 유지한다. adapter runtime lifecycle surface에 향후 composition owner가 awaitdispose(): Promise<void>를 추가한다. dispose()는 이미 시작된 drain promise를 공유하고 active operation registry를 abort한 뒤 bounded allSettled 후 store/channel을 닫는다. 현재 production bootstrap consumer가 있다고 가정하지 않는다.
  • 테스트: close 중 신규 admission 거절, active fetch/IDB abort, 중복 dispose single-flight, cleanup deadline, late provider success가 checkpoint를 다시 쓰지 못함.

BT-UP-07 — Web Locks 비지원 정책이 composition 결과로 표현되지 않음

  • 우선순위/분류: P1 before composition / PLANNED_GAP
  • 근거: upload-mutation-lock.ts:19-58; 아키텍처 completion ledger의 optional capability decision
  • 현재 동작: factory는 LockManager가 없으면 throw한다. multi-tab 안전성을 희생하는 in-memory fallback은 없다.
  • 결정: silent fallback은 추가하지 않는다. composition이 Web Locks 미지원 시 resumable upload capability를 UNSUPPORTED로 명시하고 일반 foreground upload 또는 재선택 UX로 degrade한다. 실제 지원 browser matrix가 확정되기 전 default composition에는 설치하지 않는다.

Image CDN 상세

BT-IMG-01 — resolve signal을 일관되게 필수화할지에 대한 API 단순화

  • 우선순위/분류: P3 / API CONSISTENCY DECISION, 현재 동작 결함 아님
  • 근거: application port image-cdn.ts:167-172; runtime image-cdn-runtime.ts:518-527
  • 현재 동작: optional signal을 허용하고 PRIMARY_REQUIRED preset은 signal 부재를 명시적 UNSUPPORTED로 표현한다. 문서가 signal 없는 probe 성공을 약속하지 않으므로 defect는 아니다.
  • 결정: hidden preset precondition을 줄이기 위해 다음 major contract 정리에서 resolve() signal을 필수화한다. 이는 runtime correctness fix가 아니라 API consistency 개선이다.
  • 마이그레이션: tests/fixtures/typecheck/invalid-image-cdn-resolve-signal.ts와 대응 typecheck script를 먼저 추가하고 모든 caller/README에 lifecycle signal을 전달한 뒤 port와 optional 분기를 바꾼다. P1/P2와 같은 PR에 섞지 않는다.

BT-IMG-02 — Cache-Control quoted value parser가 malformed 값을 수락

  • 우선순위/분류: P2 / VERIFIED_DEFECT
  • 근거: browser-image-probe.ts:272-373
  • 현재 동작: rawValue.replace(/^"|"$/gu, "")는 한쪽 quote만 있는 max-age="60 또는 max-age=60"도 숫자 60으로 만들 수 있다.
  • 영향: probe가 malformed cache policy를 immutable public response로 승인할 수 있다.
  • 수정:
    • comma split 전에 quote/escape-aware tokenizer를 사용해 quoted extension의 comma를 directive 경계로 취급하지 않는다.
    • quoted-string은 시작/종료 quote가 모두 있고 escape/control 문자가 유효할 때만 unquote한다.
    • numeric directives는 unquoted digits 또는 완전한 quoted digits만 허용한다.
    • private response는 no-store가 필수이며 public, private, immutable, max-age, s-maxage, no-cache, must-revalidate, proxy-revalidate가 함께 있으면 fail-closed한다. 문법상 유효한 unknown extension만 무시한다.
    • parser는 중복 directive를 계속 거절한다.
  • 테스트: unmatched quote, escaped quote, duplicate, comma-in-quoted extension, contradictory public/private directives, valid quoted max-age.

BT-IMG-03 — acceptance, verification, URL projection의 응집도 분리

  • 우선순위/분류: P3 / REFACTOR
  • 근거: image-cdn-runtime.ts 1,340줄
  • facade와 WeakMap capability identity는 유지하고 다음 내부 모듈만 추출한다.
새 내부 모듈 책임
image-asset-decoder.ts public/private exact shape snapshot
image-capability-verification.ts canonical payload, digest, key verifier deadline
image-presentation-projector.ts candidate URL/srcset/descriptor 생성
image-cdn-runtime.ts issued reference WeakMap, close, facade orchestration

image-header-metadata.ts는 format parser라는 단일 책임을 이미 가진다. LOC만 보고 더 쪼개지 말고 fuzz corpus와 malformed container table을 보강한다.

BT-IMG-04 — descriptor provider/refresh는 아직 구현 대상

  • 우선순위/분류: P1 before composition / PLANNED_GAP
  • 근거: docs/architecture/presigned-transfer-and-image-cdn.md:574-610
  • 현재 상태: signature 검증/runtime/probe는 있으나 BFF에서 descriptor를 가져오고 single-flight refresh하는 provider와 <picture> renderer는 없다.
  • 결정: 현재 runtime을 직접 product composition에 노출하지 않는다. 향후 provider는 protocol: "IMAGE_CDN_DESCRIPTOR_V1", exact authority/request binding, minimum remaining TTL, single-flight refresh, close-generation fence를 필수로 한다. renderer는 descriptor 필드만 투영하고 alt/error/placeholder 정책은 feature 소유로 둔다.

공통 개선

BT-X-01 — abort/deadline/late-result mechanics 통합

  • 우선순위: P2 / REFACTOR
  • 중복 근거: presigned provider/executor, image probe/runtime, resumable runtime, browser files, HTTP, Web Push에 createAbortScope, combineAbortSignals, awaitWithAbort, readWithSignal 변형이 반복된다.
  • 결정: result taxonomy는 각 adapter에 남기고 mechanics만 src/adapters/platform/abortable-operation.ts로 추출한다.

필수 API:

type AbortableOperationScope = Readonly<{
  signal: AbortSignal;
  terminal(): "OPEN" | "CALLER_ABORT" | "DEADLINE" | "CLOSED";
  race<T>(
    task: Promise<T>,
    onLateValue?: (value: T) => void,
  ): Promise<
    | { kind: "VALUE"; value: T }
    | { kind: "TERMINAL"; terminal: "CALLER_ABORT" | "DEADLINE" | "CLOSED" }
  >;
  close(): void;
}>;

불변식:

  • caller abort와 deadline 중 최초 하나만 terminal authority다.
  • close()는 idempotent하고 timer/listener cleanup throw를 삼킨다.
  • late rejection은 항상 관찰되어 unhandled rejection이 되지 않는다.
  • late Response/ImageBitmap/native handle은 caller가 제공한 compensator로 닫고 값을 버린다. 각 subsystem adapter가 TERMINAL을 자기 Result taxonomy로 변환한다.
  • 이 utility는 BrowserDataResult, WebPushResult, HTTP outcome을 import하지 않는다.

적용 순서: 새 utility golden test → presigned → image → resumable transport → 다른 adapter. 한 PR에서 모든 subsystem을 동시에 바꾸지 않는다.

유지해야 할 설계

  • raw presigned URL/header가 application port를 통과하지 않고 identity capability vault 안에만 존재한다.
  • capability는 exact WeakMap identity이며 single-use claim 후 vault에서 제거된다.
  • upload byte는 hash/network await 전에 snapshot한다.
  • multipart checkpoint에는 URL, credential, capability material을 저장하지 않는다.
  • multipart receipt는 revision CAS로 순차 commit하고 server status가 복구 authority다.
  • cross-context cancellation은 hint일 뿐 backend idempotency/Web Lock/CAS를 대체하지 않는다.
  • public image는 revision rollover, private image는 signed expiry/revocation으로 구분한다.
  • private image는 exact signed URL, credential omit, no-store, static container와 decode budget을 확인한다.
  • composition hard limit은 adapter implementation ceiling보다 느슨해질 수 없다.
  • P-256 key overlap set과 terminal close() generation fence를 유지한다.

실행 순서와 의존성

  1. BT-UP-03, BT-PRE-01, BT-PRE-02를 각각 독립 PR로 해결한다.
  2. BT-X-01 utility golden test를 만들고 BT-PRE-03, BT-UP-01, BT-UP-02를 이관한다.
  3. BT-UP-04, BT-IMG-01, BT-IMG-02, BT-PRE-04/05를 작은 contract-hardening PR로 처리한다.
  4. behavior suite가 모두 green인 뒤 BT-UP-05/06, BT-IMG-03 구조 분리를 수행한다.
  5. 실제 product 선택이 있을 때만 BT-UP-07, BT-IMG-04를 composition plan으로 연다.

각 PR 공통 gate:

corepack pnpm exec vitest run tests/unit/presigned-transfer.test.ts \
  tests/unit/resumable-upload-checkpoint.test.ts \
  tests/unit/resumable-upload-fetch-transport.test.ts \
  tests/unit/resumable-upload-http-control-plane.test.ts \
  tests/unit/resumable-upload-runtime.test.ts \
  tests/unit/image-cdn-runtime.test.ts
corepack pnpm check:types
corepack pnpm check:architecture
corepack pnpm lint
git diff --check

실제 browser gate도 capability promotion 전에 실행한다.

corepack pnpm test:browser-capabilities -- \
  tests/browser-capabilities/presigned-streaming.spec.ts \
  tests/browser-capabilities/resumable-upload.spec.ts \
  tests/browser-capabilities/image-cdn.spec.ts

해당 browser/provider 환경이 없으면 이 gate는 UNVERIFIED로 남기며 capability availability를 승격하지 않는다.

구현 완료 정의

  • 모든 P1 항목에 failing-before/fixed-after test가 있다.
  • wire version과 migration 순서가 provider fixture에 반영된다.
  • 어떤 timeout 경로도 non-cooperative Promise 때문에 public API를 무한 대기시키지 않는다.
  • delete partition 결과가 late native commit 가능성을 숨기지 않는다.
  • runtime facade의 public capability identity, failure taxonomy, persisted V1 schema는 명시된 migration 외에는 바뀌지 않는다.
  • 기존 문서의 AVAILABLE_NOT_COMPOSED/PLANNED_GAP 상태를 code defect 완료로 오인하지 않는다.