13 KiB
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가 섞인다.
결정
- BFF/Web API control plane과 object-storage/CDN data plane을 분리한다.
- 브라우저는 signing key, cloud 관리자 credential, bucket/container, raw object key 생성 규칙을 소유하지 않는다.
- application caller는 raw URL/query/signed headers를 전달하지 않는다. composition/provider가 발급한 exact capability만 adapter가 소비한다.
- presigned capability는 version, opaque identity, method, logical resource 또는 session/part, exact URL, origin/path policy, byte/media/checksum 조건과 expiry를 immutable하게 binding한다.
- signed URL은 bearer credential로 취급하고 persistence, checkpoint, telemetry, analytics, referrer와 raw exception에 넣지 않는다.
- data-plane fetch는 기본적으로
credentials: omit,redirect: error,referrerPolicy: no-referrer,cache: no-store를 사용한다. cross-origin은 composition allowlist와 CORS/CSP 계약이 있을 때만 연다. - client-side single-use 표시는 UX와 accidental replay를 줄이는 보조책이다. cross-tab/replay의 최종 authority는 server 또는 composition-owned atomic consumer다.
- streaming download는 response body를 closed-result stream으로 변환하고 output chunk, total bytes, media type, encoding과 선택적 incremental integrity를 검증한다. overrun/truncation/abort 시 native reader와 destination을 닫는다.
BROWSER_HANDOFF와 destination close 이후의SAVED를 계속 분리한다.- Range resumable download는 별도 capability다.
206,Content-Range, validator, destination seek/truncate와 final integrity 없이는 append resume를 허용하지 않는다. - 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를 공유하지 않는다.
- reference upload protocol literal은
PRESIGNED_MULTIPART_V1이다. 모든 control-plane request/response, session과 checkpoint가 이를 exact하게 포함하며 다른 값이나 누락을 거절한다. - session은 exact source binding, total bytes, media type, part size/count, concurrency, checksum algorithm과 expiry를 묶는다. part number는 1부터 연속적이고 offset/length/checksum/idempotency를 정확히 binding한다.
- control transport는
CREATE_SESSION,GET_STATUS,COMPLETE,ABORT의 closed operation을 composition-owned fixed HTTPS endpoint map으로만 실행한다. presigned 발급도 factory에 고정된 단일 BFF endpoint를 사용하며 caller-provided URL을 받지 않는다. requestBindingSha256와uploadBindingSha256는RESUMABLE-UPLOAD-BINDING-V1및RESUMABLE-UPLOAD-SESSION-BINDING-V1canonical field sequence의 SHA-256이다.UPLOAD_PARTcapability binding은 exactprotocol: PRESIGNED_MULTIPART_V1을 포함한다. BFF는sessionId로 server session을 조회하고 snapshot으로 protocol, binding과 part plan을 재계산한다. client digest는 authorization이나 ownership 증명이 아니다.- part memory는
partSize × concurrency × copyFactorhard ceiling으로 제한한다. retry는 같은 bytes/checksum/idempotency에만 허용한다. - retryable network, 429와 모든 5xx는 bounded attempt/
Retry-After/abortable backoff 안에서만 재시도한다. status의 404/410 또는NOT_FOUND/EXPIRED는 terminal로 보고 checkpoint를 CAS 제거한다. - PUT 성공은 capability-bound status, receipt header,
expectedResponseByteLength와 exactContent-Length를 검증하고 hard cap과 deadline 안에서 response body를 끝까지 drain한 뒤에만 확정한다. 204는 expected response bytes가 0일 때만 허용하며Content-Length부재를 0으로 정규화한다. - resume는 local checkpoint만 신뢰하지 않는다. server status를 다시 읽고 완료 part의 local range digest와 server checksum/receipt를 대조한 뒤 missing part만 전송한다.
- 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는 금지한다.
- cancel과 server abort를 분리한다. same-origin 다른 tab의 active upload는
strict
RESUMABLE_UPLOAD_CANCEL_V1BroadcastChannel 신호로 먼저 중단한 뒤 per-key Web Lock 안에서 server abort/reconcile을 수행한다. 이 ephemeral 신호는 opaque upload key만 운반하고 persistence하지 않으며 authority가 아니다. channel이 없으면 abort caller의 bounded signal 아래 lock을 기다린다. complete/abort가 불명확하면 server reconcile 전까지 성공으로 기록하거나 checkpoint를 파기하지 않는다. - multipart complete는 ordered receipt 검증 뒤에도
QUARANTINED다. backend scan/CDR/promotion이 끝나기 전 available/public URL을 발급하지 않는다. application-facing 성공값은 state/resource/byte length/replay 여부만 노출하고 session ID, request binding과 fingerprint를 제거한다. - Image CDN application contract는 opaque asset reference와 composition-registered named preset만 받는다. arbitrary source URL과 raw transform query는 금지한다.
- asset descriptor는 immutable revision, delivery class, safe raster media, natural dimensions, rendition dimensions/formats/URLs와 private expiry를 묶는다.
- 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에만 맡기지 않는다. - 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
acceptedKeyIdsoverlap set이고 verifier registry가 모든 ID를 포함해야 한다. descriptor의 단일 key ID는 양쪽 registry에 exact membership이 있어야 한다. - 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 전에 거절한다.
- private signed delivery는
PRIMARY_REQUIREDprobe를 강제하고credentials: omit, exact response URL과 실제Cache-Control: no-store를 검증한다. fetch/body/decode 전체에 하나의 timeout을 적용하고 abort/late completion에서 reader와 bitmap을 닫는다. - SVG/HTML/data/blob/javascript와 unknown active media는 기본 거절한다. animation은 frame/decode budget이 승인된 별도 protocol 전에는 허용하지 않는다.
- responsive candidate는 한 source set에서 하나의 descriptor 종류만 사용하고,
고유한 양수 width를 오름차순으로 반환한다.
sizes는 registry-owned layout token에서 결정한다. - public rendition은 immutable revision URL과 public immutable cache를 사용하고, private rendition은 short-lived capability와 필수 no-store를 사용한다. 같은 URL의 content를 purge로 바꿔치기하지 않는다.
- Image CDN runtime
close()는 terminal/idempotent다. runtime lifetime signal로 진행 중 verification/probe를 중단하고 accepted WeakMap을 새 WeakMap으로 교체해 기존 reference를 즉시 revoke한다. 닫힌 runtime은 재개하지 않고 새 composition으로 교체한다. - 공통 runtime은 concrete browser mechanism과 policy validation을 제공하지만 backend endpoint/vendor schema와 제품 asset/upload owner가 없으므로 bootstrap에 조합하지 않는다.
- runtime source는 production module inventory와 removal gate로 기본 bundle에서 제외됨을 증명한다.
- Range resume의 detailed state machine과 app-managed background의 플랫폼 경계는 VD-14가 소유한다. VD-12의 whole-object streaming 구현을 그 capability의 구현 증거로 사용하지 않는다.
- 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_V1canonical 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, privateno-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