chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user