203 lines
13 KiB
Markdown
203 lines
13 KiB
Markdown
# 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`
|