Files
tech-log-frontend/docs/architecture/decisions/VD-12-presigned-transfer-and-image-cdn.md
T

203 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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과 달라야 한다.
`<img crossorigin="anonymous">`가 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`