# VD-12: Presigned transfer, resumable upload와 Image CDN 경계 - 상태: Accepted — reference runtime available, not composed - 결정일: 2026-07-28 - catalog recipe availability: `RECIPE_AVAILABLE` (primary status/selection과 별도) - reference runtime 상태: `AVAILABLE_NOT_COMPOSED` - 관련 결정: VD-10, VD-11, VD-14, VD-16 - current status ledger: `docs/architecture/browser-data-capability-completion-ledger.md` - 재검토: 제품이 server file upload/download 또는 Image CDN delivery를 선택할 때 ## 배경 Presigned URL은 URL 문자열이 아니라 짧은 수명의 bearer capability다. multipart/resumable upload는 단순 PUT 반복이 아니라 session, part identity, checksum, authoritative reconciliation, completion과 orphan cleanup protocol이다. streaming download는 전체 payload를 메모리에 올리지 않지만 response binding, truncation/overrun, destination commit과 integrity를 별도로 처리해야 한다. Image CDN URL도 arbitrary transform builder로 노출하면 cache poisoning, pixel/decode bomb, signed-query 유출과 source-fetch SSRF 경계가 사라진다. 이 네 capability를 범용 `HttpClient`나 `FileService` mega-port 하나로 합치면 control plane authorization과 byte data plane, local browser lifecycle과 server authority가 섞인다. ## 결정 1. BFF/Web API control plane과 object-storage/CDN data plane을 분리한다. 2. 브라우저는 signing key, cloud 관리자 credential, bucket/container, raw object key 생성 규칙을 소유하지 않는다. 3. application caller는 raw URL/query/signed headers를 전달하지 않는다. composition/provider가 발급한 exact capability만 adapter가 소비한다. 4. presigned capability는 version, opaque identity, method, logical resource 또는 session/part, exact URL, origin/path policy, byte/media/checksum 조건과 expiry를 immutable하게 binding한다. 5. signed URL은 bearer credential로 취급하고 persistence, checkpoint, telemetry, analytics, referrer와 raw exception에 넣지 않는다. 6. data-plane fetch는 기본적으로 `credentials: omit`, `redirect: error`, `referrerPolicy: no-referrer`, `cache: no-store`를 사용한다. cross-origin은 composition allowlist와 CORS/CSP 계약이 있을 때만 연다. 7. client-side single-use 표시는 UX와 accidental replay를 줄이는 보조책이다. cross-tab/replay의 최종 authority는 server 또는 composition-owned atomic consumer다. 8. streaming download는 response body를 closed-result stream으로 변환하고 output chunk, total bytes, media type, encoding과 선택적 incremental integrity를 검증한다. overrun/truncation/abort 시 native reader와 destination을 닫는다. 9. `BROWSER_HANDOFF`와 destination close 이후의 `SAVED`를 계속 분리한다. 10. Range resumable download는 별도 capability다. `206`, `Content-Range`, validator, destination seek/truncate와 final integrity 없이는 append resume를 허용하지 않는다. 11. upload 상위 계약은 server-authoritative session/status/part/complete/abort를 소유하고 data-plane part executor는 capability 타입에 generic하다. 따라서 S3-style presigned multipart와 BFF proxy part를 같은 application contract 뒤에 둘 수 있지만 wire DTO를 공유하지 않는다. 12. reference upload protocol literal은 `PRESIGNED_MULTIPART_V1`이다. 모든 control-plane request/response, session과 checkpoint가 이를 exact하게 포함하며 다른 값이나 누락을 거절한다. 13. session은 exact source binding, total bytes, media type, part size/count, concurrency, checksum algorithm과 expiry를 묶는다. part number는 1부터 연속적이고 offset/length/checksum/idempotency를 정확히 binding한다. 14. control transport는 `CREATE_SESSION`, `GET_STATUS`, `COMPLETE`, `ABORT`의 closed operation을 composition-owned fixed HTTPS endpoint map으로만 실행한다. presigned 발급도 factory에 고정된 단일 BFF endpoint를 사용하며 caller-provided URL을 받지 않는다. 15. `requestBindingSha256`와 `uploadBindingSha256`는 `RESUMABLE-UPLOAD-BINDING-V1` 및 `RESUMABLE-UPLOAD-SESSION-BINDING-V1` canonical field sequence의 SHA-256이다. `UPLOAD_PART` capability binding은 exact `protocol: PRESIGNED_MULTIPART_V1`을 포함한다. BFF는 `sessionId`로 server session을 조회하고 snapshot으로 protocol, binding과 part plan을 재계산한다. client digest는 authorization이나 ownership 증명이 아니다. 16. part memory는 `partSize × concurrency × copyFactor` hard ceiling으로 제한한다. retry는 같은 bytes/checksum/idempotency에만 허용한다. 17. retryable network, 429와 모든 5xx는 bounded attempt/`Retry-After`/abortable backoff 안에서만 재시도한다. status의 404/410 또는 `NOT_FOUND`/`EXPIRED`는 terminal로 보고 checkpoint를 CAS 제거한다. 18. PUT 성공은 capability-bound status, receipt header, `expectedResponseByteLength`와 exact `Content-Length`를 검증하고 hard cap과 deadline 안에서 response body를 끝까지 drain한 뒤에만 확정한다. 204는 expected response bytes가 0일 때만 허용하며 `Content-Length` 부재를 0으로 정규화한다. 19. resume는 local checkpoint만 신뢰하지 않는다. server status를 다시 읽고 완료 part의 local range digest와 server checksum/receipt를 대조한 뒤 missing part만 전송한다. 20. checkpoint에는 opaque session/source binding과 reconciliation에 필요한 protocol-defined SHA-256 file fingerprint, per-part checksum, bounded opaque non-authorizing part receipt token만 저장한다. 이 값도 account partition과 retention을 적용하고 diagnostics/telemetry에는 내보내지 않는다. presigned URL, signed header, bearer token/capability, file name, path, account ID, raw provider ETag와 raw server error는 금지한다. 21. cancel과 server abort를 분리한다. same-origin 다른 tab의 active upload는 strict `RESUMABLE_UPLOAD_CANCEL_V1` BroadcastChannel 신호로 먼저 중단한 뒤 per-key Web Lock 안에서 server abort/reconcile을 수행한다. 이 ephemeral 신호는 opaque upload key만 운반하고 persistence하지 않으며 authority가 아니다. channel이 없으면 abort caller의 bounded signal 아래 lock을 기다린다. complete/abort가 불명확하면 server reconcile 전까지 성공으로 기록하거나 checkpoint를 파기하지 않는다. 22. multipart complete는 ordered receipt 검증 뒤에도 `QUARANTINED`다. backend scan/CDR/promotion이 끝나기 전 available/public URL을 발급하지 않는다. application-facing 성공값은 state/resource/byte length/replay 여부만 노출하고 session ID, request binding과 fingerprint를 제거한다. 23. Image CDN application contract는 opaque asset reference와 composition-registered named preset만 받는다. arbitrary source URL과 raw transform query는 금지한다. 24. asset descriptor는 immutable revision, delivery class, safe raster media, natural dimensions, rendition dimensions/formats/URLs와 private expiry를 묶는다. 25. CDN policy는 allowed HTTPS origin/path, preset width/DPR/format/quality/fit, output pixel/decoded-byte/encoded-byte/candidate/lifetime ceiling과 cache/referrer policy를 소유한다. composition limit은 exported adapter implementation ceiling을 초과할 수 없고 capability verification concurrency도 절대 상한 아래에서 제한한다. CDN origin은 composition이 명시한 application origin과 달라야 한다. ``가 same-origin 요청에서는 cookie를 보낼 수 있기 때문에 private URL의 credential omission을 probe에만 맡기지 않는다. 26. private capability signature가 허용하는 값은 versioned preset binding ID다. CDN/BFF는 그 ID를 server-owned immutable preset registry에서 조회하고, 요청의 width/height/DPR/fit/format/quality가 그 preset의 exact candidate인지 재계산해 하나라도 다르면 거절한다. signed URL에 붙은 raw transform query나 client 계산값은 authorization proof가 아니다. signing key policy는 bounded unique `acceptedKeyIds` overlap set이고 verifier registry가 모든 ID를 포함해야 한다. descriptor의 단일 key ID는 양쪽 registry에 exact membership이 있어야 한다. 27. browser probe는 native decode 전에 PNG/JPEG/WebP/AVIF header metadata와 static-only container를 검사한다. 선언 dimensions, pixels와 decoded-byte budget을 넘거나 APNG/WebP animation, AVIF sequence/derived image, ambiguous/malformed container이면 decode 전에 거절한다. 28. private signed delivery는 `PRIMARY_REQUIRED` probe를 강제하고 `credentials: omit`, exact response URL과 실제 `Cache-Control: no-store`를 검증한다. fetch/body/decode 전체에 하나의 timeout을 적용하고 abort/late completion에서 reader와 bitmap을 닫는다. 29. SVG/HTML/data/blob/javascript와 unknown active media는 기본 거절한다. animation은 frame/decode budget이 승인된 별도 protocol 전에는 허용하지 않는다. 30. responsive candidate는 한 source set에서 하나의 descriptor 종류만 사용하고, 고유한 양수 width를 오름차순으로 반환한다. `sizes`는 registry-owned layout token에서 결정한다. 31. public rendition은 immutable revision URL과 public immutable cache를 사용하고, private rendition은 short-lived capability와 필수 no-store를 사용한다. 같은 URL의 content를 purge로 바꿔치기하지 않는다. 32. Image CDN runtime `close()`는 terminal/idempotent다. runtime lifetime signal로 진행 중 verification/probe를 중단하고 accepted WeakMap을 새 WeakMap으로 교체해 기존 reference를 즉시 revoke한다. 닫힌 runtime은 재개하지 않고 새 composition으로 교체한다. 33. 공통 runtime은 concrete browser mechanism과 policy validation을 제공하지만 backend endpoint/vendor schema와 제품 asset/upload owner가 없으므로 bootstrap에 조합하지 않는다. 34. runtime source는 production module inventory와 removal gate로 기본 bundle에서 제외됨을 증명한다. 35. Range resume의 detailed state machine과 app-managed background의 플랫폼 경계는 VD-14가 소유한다. VD-12의 whole-object streaming 구현을 그 capability의 구현 증거로 사용하지 않는다. 36. top-level transfer runtime, account-scoped teardown, upload pause/inventory, Image descriptor HTTP provider/refresh와 safe presentation projection은 VD-16이 소유한다. 개별 runtime factory의 존재를 operational composition 완료로 해석하지 않는다. ## Backend와 맞출 계약 - fixed BFF capability endpoint, closed session endpoint map과 runtime schema - `PRESIGNED_MULTIPART_V1` canonical binding, server-side session lookup, authorization/revocation - object storage CORS, allowed method/headers, exposed receipt/checksum headers - PUT 성공 status, receipt header, response byte length/body cap - session expiry, 404/410 terminal 의미, list/status pagination, idempotency와 orphan cleanup - part/full-object checksum의 정확한 알고리즘·composite 의미 - quarantine scan, promotion, status와 reject/delete lifecycle - CDN source registry, immutable asset revision, versioned named preset의 exact candidate 재계산과 query mismatch 거절 - image signing key overlap 배포, signer 전환, capability/client drain과 emergency revocation/forced rollout runbook - CDN `Content-Type`, static header metadata, dimensions/decoded-byte budget, private `no-store`, application과 분리된 CDN origin, cache key, `Vary`, CORS와 CSP 브라우저의 local file reference, native `File`/`Blob`, IndexedDB checkpoint physical schema, OPFS path, signed URL query와 cloud object key는 backend 공유 계약이 아니다. ## 선택하지 않은 대안 - application caller가 arbitrary presigned URL을 직접 전달 - browser bundle에서 cloud signing - 범용 JSON `HttpClient`로 binary streaming/part protocol까지 처리 - complete 응답을 scan 완료 또는 public availability로 간주 - local checkpoint만 보고 upload complete - ETag를 무조건 MD5/SHA-256으로 해석 - private signed image URL을 query cache나 persistence에 장기 저장 - raw transform query로 CDN URL 조립 - large download의 unbounded Blob fallback ## 증적 - application ports: `src/application/ports/browser-transfer/` - concrete adapters: `src/adapters/browser-transfer/` - unit/fault tests: `tests/unit/` - real browser cases: `tests/browser-capabilities/` - 상세 설계: `docs/architecture/presigned-transfer-and-image-cdn.md` - Range/background 결정: `docs/architecture/decisions/VD-14-resumable-download-and-background-transfer.md` - composition/Image provider 결정: `docs/architecture/decisions/VD-16-browser-transfer-composition-and-image-delivery.md` - 운영 복구: `docs/operations/browser-transfer-recovery.md`