Files
tech-log-frontend/docs/architecture/presigned-transfer-and-image-cdn.md

758 lines
41 KiB
Markdown
Raw Permalink 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.
# Presigned transfer, resumable upload, streaming download and Image CDN
이 문서는 presigned URL, multipart/resumable upload, streaming download와
Image CDN을 브라우저 애플리케이션에 넣을 때의 control plane/data plane 경계,
무결성, 재개, 만료, 캐시와 복구 계약을 정의한다.
현재 구현된 개별 reference runtime은 실제 browser `fetch`와 bounded byte
stream을 사용한다. 제품별 endpoint, bucket, CDN vendor, asset schema와
authorization owner는 아직 조합하지 않는다. 이 개별 runtime은
`AVAILABLE_NOT_COMPOSED`이고, Range/composition/provider 같은 후속 delta는 아래
표처럼 다른 상태다. 어느 경로도 임의의 URL이나 object key를 application caller가
직접 전달하는 범용 HTTP facade가 아니다.
## 0. 현재 구현과 목표 상태
이 문서에서 “설계됨”, “reference source가 있음”, “제품에 조합됨”과 “target
browser에서 보장 가능함”은 서로 다른 사실이다. primary current status는
`NOT_SELECTED`, `DESIGNED_NOT_IMPLEMENTED`, `AVAILABLE_NOT_COMPOSED`, `COMPOSED`,
`PLATFORM_LIMITED` 다섯 값 중 하나만 사용한다. production evidence와 traffic
admission은 이 status와 별도 축이다.
| capability | primary current status | 현재 있는 것 | 남은 목표 |
| --- | --- | --- | --- |
| whole-object Presigned GET/part PUT | `AVAILABLE_NOT_COMPOSED` | strict BFF response validation, in-memory identity vault, bounded GET/PUT executor | top-level wire version, 실제 endpoint/auth/revocation/provider contract |
| multipart/resumable upload | `AVAILABLE_NOT_COMPOSED` | server-authoritative session flow, part retry, IDB CAS checkpoint, Web Lock, cross-tab abort signal | non-destructive pause, safe inventory/retention sweep, 실제 BFF/storage/scan |
| whole-object foreground streaming download | `AVAILABLE_NOT_COMPOSED` | `200` stream, length/SHA-256, picker save, bounded object URL와 browser handoff mechanism | strategy selector, browser-managed capability issuer와 제품 save UX |
| Range resumable download | `DESIGNED_NOT_IMPLEMENTED` | VD-14 production design | port/runtime/checkpoint, `206/200/412/416`, seek/truncate 또는 OPFS staging |
| Image CDN policy/verification engine | `AVAILABLE_NOT_COMPOSED` | opaque asset/preset, responsive descriptor, P-256, static-image probe | descriptor HTTP provider/refresh, renderer, 실제 BFF/CDN |
| app-managed background download | `NOT_SELECTED` | VD-14 경계와 금지 조건 | 제품이 별도 선택한 지원 browser에서만 optional 구현 |
| cross-browser background-download guarantee | `PLATFORM_LIMITED` | browser-managed handoff fallback | 공통 baseline으로 구현 완료를 선언하지 않음 |
| app-managed background upload | `NOT_SELECTED` | pause/foreground resume와 명시적으로 분리 | durable source staging/worker auth를 가진 별도 protocol |
| cross-browser background-upload guarantee | `PLATFORM_LIMITED` | foreground checkpoint resume fallback | worker lifetime/source permission을 공통 보장하지 않음 |
현재 runtime은 테스트 전용 mock이 아니라 실제 browser API를 호출하지만, 위
`남은 목표`가 구현됐다는 뜻은 아니다. 특히 foreground whole-object streaming
증거를 Range resume나 app-managed background download 증거로 재사용하지 않는다.
Range와 background download의 상세 상태 머신, destination, fallback, rollout과 완료 기준은
[VD-14](./decisions/VD-14-resumable-download-and-background-transfer.md)가
소유한다. top-level runtime composition, account teardown, Image descriptor
provider/refresh와 safe presentation projection은
[VD-16](./decisions/VD-16-browser-transfer-composition-and-image-delivery.md)이
소유한다.
## 1. 경계와 topology
```text
feature use case
-> feature-specific transfer facade
-> BFF/Web API control plane
- authorization
- upload session / download capability
- resource metadata and lifecycle authority
-> browser transfer data plane
- capability validation
- bounded fetch / stream / part transfer
- integrity and cancellation
-> object/file storage or CDN
- bytes only
image use case
-> ImageDeliveryPort
-> backend/CDN-issued immutable asset descriptor
-> policy-validated responsive candidates
-> presentation-safe <picture>/<img> attributes
```
브라우저는 S3, MinIO, GCS, Azure Blob의 관리자 credential, bucket policy, object
key 생성 규칙이나 signing key를 소유하지 않는다. control plane이 짧은 수명의
제한된 capability를 발급하고, data plane은 그 capability가 묶은 정확한
operation과 bytes만 전송한다.
## 2. 공통 capability 규칙
Presigned URL은 단순 URL이 아니라 bearer capability다. 구현은 적어도 다음
binding을 하나의 immutable snapshot으로 검증해야 한다.
- protocol/version과 opaque capability ID
- `DOWNLOAD` 또는 특정 upload session/part operation
- logical resource/session ID
- exact HTTP method
- HTTPS와 composition allowlist에 속한 origin/path
- capability가 발급한 exact query와 signed request headers
- expected media type, byte range 또는 exact part offset/length
- hard maximum bytes와 선택한 checksum algorithm/digest
- upload 성공 response의 exact status, `Content-Length`,
`expectedResponseByteLength`와 opaque receipt header
- 발급·만료 시각과 composition의 더 짧은 lifetime ceiling
- single-use가 필요한 경우 원자적인 server/provider consumption
### 2.1 Wire version 목표
현재 multipart DTO는 `PRESIGNED_MULTIPART_V1`을 exact하게 포함하지만, whole-object
presigned capability response에는 top-level transfer protocol literal이 아직
없다. strict exact-key schema만으로 현재 payload drift는 막지만, 호환되지 않는
wire 변경과 구 client drain을 명시적으로 운영하기에는 부족하다.
향후 presigned control-plane envelope에는 다음 literal을 추가한다.
```text
protocol = PRESIGNED_TRANSFER_V1
```
- request/response, vault registration과 executor consumption에서 exact match한다.
- 알 수 없는 값, 누락과 newer version은 fail-closed한다.
- Range는 이 literal에 암묵적으로 섞지 않고
`RANGE_RESUMABLE_DOWNLOAD_V1` 별도 protocol을 사용한다.
- Image descriptor HTTP envelope도 `IMAGE_CDN_DESCRIPTOR_V1`로 versioning한다.
- version을 추가하기 전까지 현재 구현 상태는 계속
`AVAILABLE_NOT_COMPOSED`이며 wire-version 목표가 구현됐다고 기록하지 않는다.
URL의 query와 signed headers는 credential material로 취급한다. persistence,
checkpoint, diagnostics, analytics, error message, referrer와 application state에
넣지 않는다. runtime caller가 URL/query/header를 조립하거나 capability의
method, origin, byte limit과 expiry를 늘릴 수 없다.
Data-plane `fetch` 기본값은 다음과 같다.
```text
credentials = omit
redirect = error
referrerPolicy = no-referrer
cache = no-store
mode = same-origin or explicitly approved CORS
```
cross-origin object storage/CDN은 exact origin allowlist, CORS method/header,
노출할 response header와 CSP `connect-src`/`img-src` 계약이 있어야 한다.
redirect를 따라가며 signed query를 다른 origin으로 전달하지 않는다.
## 3. Presigned URL
### 3.1 Control plane
실제 발급은 feature-specific BFF adapter가 소유한다. composition은 presigned
capability 발급 endpoint를 HTTPS absolute URL 하나로 고정해 factory에 한 번
주입한다. application caller는 endpoint를 고르거나 path/query를 조립할 수 없다.
공통 runtime은 이 고정 endpoint와 strict envelope만 알고 provider-specific
signing DTO는 알지 않는다.
권장 흐름:
```text
browser -> authenticated BFF capability request
BFF -> resource/session authorization and metadata lookup
BFF -> object service signer
BFF -> opaque bound capability
browser -> validated direct data-plane request
```
발급 endpoint의 `2xx`만으로 authorization을 추론하지 않는다. runtime schema가
전체 capability를 검증하고, data request 시에도 object storage/BFF가 method,
expiry, size/checksum 조건을 강제해야 한다. presigned URL은 underlying credential
revocation이나 server policy 때문에 표기된 expiry보다 일찍 무효화될 수 있다.
upload part capability 요청에서 전달되는 `requestBindingSha256`,
`uploadBindingSha256`와 part digest는 authorization proof가 아니다.
`UPLOAD_PART` capability binding은 `protocol: PRESIGNED_MULTIPART_V1`을 exact하게
포함한다. BFF는 `sessionId`로 server-owned session을 다시 읽고 protocol,
subject/purpose/state/expiry와 part plan을 authorization한 뒤 canonical binding을
직접 재계산해야 한다. client digest를 그대로 신뢰하거나 단순 echo해서 signed
URL을 발급하면 안 된다.
### 3.2 Consumption
- capability를 async 대기하는 동안 caller 입력과 dependency method를 snapshot한다.
- 사용 전에 만료뿐 아니라 최소 잔여 lifetime도 검사한다.
- 만료/403은 임의 retry가 아니라 control plane의 새 capability 발급으로 복구한다.
- ambiguous network failure 뒤 upload part를 새 bytes로 덮어쓰지 않는다.
- client-side single-use map은 UX 최적화일 뿐이다. cross-tab/replay authority는
server 또는 composition-owned atomic consumer다.
- signed response의 URL, query, raw header와 exception text를 관측성에 남기지 않는다.
## 4. Multipart/resumable upload
이 reference runtime의 기본 모델은
`PRESIGNED_MULTIPART_V1` capability 기반 ordered multipart protocol이다. 이
literal은 모든 upload control-plane request/response, session과 durable
checkpoint에서 일치해야 하며 다른 값이나 누락은 fail-closed한다. 특정 S3 DTO를
application port로 노출하지 않으며 tus 같은 offset protocol을 선택하면 별도
protocol/version과 wire adapter가 동일한 상위 session contract를 구현한다.
### 4.1 Session contract
Control plane이 소유하는 operation:
1. `create`: authorization, purpose, declared bytes/media와 source binding을 확인
2. `status/list parts`: server-authoritative session/part state 반환
3. `issue part capability`: exact session, part number, offset, length와 checksum binding
4. `complete`: ordered part receipt와 checksum을 검증
5. `abort`: server upload를 중단하고 orphan cleanup을 예약
browser HTTP adapter는 `CREATE_SESSION`, `GET_STATUS`, `COMPLETE`, `ABORT`
closed operation set을 composition-owned fixed HTTPS endpoint map에 연결한다.
모두 bounded `POST application/json`이고 caller가 URL을 제공하지 못한다.
upload control endpoint response는 exact URL/status/content type과
request/response byte cap, deadline, `Retry-After` ceiling을 검증한다. part
capability는 앞 절의 별도 fixed BFF endpoint를 redirect 금지로 사용하고 strict
response envelope 및 request/response의 `UPLOAD_PART` binding protocol이
`PRESIGNED_MULTIPART_V1`인지 대조한다.
Session은 적어도 opaque ID, protocol version, exact total bytes, media type,
part size, part count, concurrency ceiling, checksum algorithm과 expiry를 묶는다.
part number는 1부터 연속적이어야 하고 마지막 part를 제외한 part length는
고정한다.
`PRESIGNED_MULTIPART_V1`의 canonical digest 계약은 다음과 같다. 각 field는
아래 순서의 UTF-8 line으로 직렬화하고 SHA-256 lowercase hex를 사용한다.
```text
fingerprint.digestHex =
SHA-256(
"SHA-256-PARTS-V1"
fingerprint.byteLength
fingerprint.partSizeBytes
fingerprint.partCount
"{partNumber}:{offset}:{byteLength}:{checksumSha256}" for each ordered part
)
requestBindingSha256 =
SHA-256(
"RESUMABLE-UPLOAD-BINDING-V1"
uploadKey
purpose
mediaType
fingerprint.algorithm
fingerprint.digestHex
fingerprint.byteLength
fingerprint.partSizeBytes
fingerprint.partCount
)
uploadBindingSha256 =
SHA-256(
"RESUMABLE-UPLOAD-SESSION-BINDING-V1"
requestBindingSha256
sessionId
fingerprint.algorithm
fingerprint.digestHex
fingerprint.byteLength
fingerprint.partSizeBytes
fingerprint.partCount
)
```
각 괄호 안의 항목은 실제로 줄바꿈 하나로 연결하며 마지막 빈 line은 추가하지
않는다. BFF는 create에서 받은 선언을 server policy/session snapshot과 함께
보관하고, part capability 발급 때 그 snapshot으로 request/upload digest와 exact
part offset/length/checksum/idempotency를 재계산한다. complete에서는 server
ledger의 ordered part set으로 fingerprint digest도 다시 계산한다. digest 일치는
input binding의 무결성 신호일 뿐 subject authorization, session ownership 또는
session state 검사를 대체하지 않는다.
### 4.2 Browser transfer
- source는 bounded `readRange(offset, length)`를 제공한다.
- part bytes 하나와 digest 계산에 필요한 copy만 메모리에 둔다.
- `partSize × concurrency × copyFactor`가 composition memory ceiling을 넘으면
시작 전에 거절한다.
- 각 part는 exact offset/length와 SHA-256을 계산한 뒤 capability를 발급받는다.
- retry는 동일 session/part/offset/length/checksum/idempotency binding에만 허용한다.
- 429/모든 `5xx`/network retry는 composition의 bounded attempt, bounded
`Retry-After`와 abortable backoff 안에서만 수행한다.
- 만료/authorization failure는 최대 정책 범위 안에서 capability를 재발급한다.
- upload response의 opaque receipt/ETag를 whole-file digest로 해석하지 않는다.
- PUT capability는 성공 status, receipt header와
`expectedResponseByteLength`를 묶는다. runtime은 exact `Content-Length`
확인하고 response body를 hard cap과 동일 deadline 안에서 끝까지 bounded
drain한 뒤에만 receipt를 성공으로 채택한다. `204`는 expected response bytes가
`0`일 때만 허용하고 `Content-Length` 부재를 0으로 정규화한다.
- complete 전 server-authoritative status와 local receipt를 reconcile한다.
- complete 성공은 scan 완료가 아니라 `QUARANTINED`다.
### 4.3 Resume와 checkpoint
Checkpoint에는 다음만 저장할 수 있다.
- schema version, exact `PRESIGNED_MULTIPART_V1`, revision과 lifecycle state
- opaque upload key, session ID와 `requestBindingSha256`
- `SHA-256-PARTS-V1` fingerprint의 total bytes, part size/count와 digest
- session expiry와 server concurrency ceiling
- 완료 part의 number/offset/length/checksum/opaque receipt
- 마지막 reconciliation 시각
Presigned URL, query, signed header, bearer token, file name, local path, account ID와
raw backend error는 저장하지 않는다.
위에 명시한 SHA-256 fingerprint/part checksum과 bounded opaque part receipt는
서버 reconcile에 필요한 비권한성 checkpoint field이므로 예외적으로 해당 account
partition에만 보존한다. raw provider ETag를 임의로 저장하는 것이 아니며 이
필드들도 diagnostics, telemetry, ticket 또는 application-facing 결과에는
노출하지 않는다.
resume 시 checkpoint만 신뢰하지 않는다.
1. 사용자가 다시 선택한 source의 exact byte length와 source binding을 검사한다.
2. server status/list-parts를 authoritative하게 읽는다.
3. 완료되었다고 주장하는 각 part의 local bytes를 다시 bounded hash하여
server checksum/receipt와 대조한다.
4. 불일치하면 해당 session을 complete하지 않고 abort/restart 또는 사용자 복구로
전환한다.
5. 새 part만 업로드한 뒤 전체 ordered set을 다시 reconcile한다.
`GET_STATUS`가 HTTP `404` 또는 `410`을 반환하거나 decoded status가
`NOT_FOUND`/`EXPIRED`이면 해당 session은 terminal이다. runtime은 CAS revision을
확인해 checkpoint를 제거하고 새 session으로 restart하거나
`EXPIRED_RESOURCE/RESTART`를 반환한다. 사라진 session의 checkpoint를 다음
invocation까지 반복해서 붙잡지 않는다. abort의 `404/410`도 checkpoint를
정리하고 application에는 `ORPHANED`로 닫는다.
한 session은 cross-tab mutation lock으로 직렬화한다. lock은 correctness의 유일한
근거가 아니며 server idempotency와 part CAS가 최종 authority다.
명시적 abort는 같은 runtime의 controller뿐 아니라 strict
`RESUMABLE_UPLOAD_CANCEL_V1` BroadcastChannel을 통해 같은 origin의 다른
runtime에도 opaque upload key의 ephemeral cancel 신호를 보낸다. 수신 runtime은
진행 중 fetch/read/backoff의 AbortSignal을 먼저 중단해 Web Lock을 내보내고,
abort 요청 runtime이 lock 안에서 durable checkpoint를 `ABORT_PENDING`으로
바꾼 뒤 server abort/reconcile을 실행한다. 이 메시지는 session ID, capability,
signed URL이나 receipt를 포함하거나 저장하지 않으며 server state authority가
아니다. BroadcastChannel이 없으면 correctness는 유지되지만 abort는 caller가
정한 bounded deadline 아래 다른 context의 lock 해제를 기다린다.
사용자 cancel은 현재 browser work 중지이고 server abort와 다르다. 명시적 abort를
요청했는데 결과가 불명확하면 checkpoint를 즉시 성공으로 삭제하지 않고 다음
reconcile에서 server 상태를 확인한다. backend는 만료된 orphan multipart를
정리하는 TTL job을 가져야 한다.
application-facing upload 성공값은 `state`, opaque `resourceId`, `byteLength`,
`replayed`만 반환한다. control-plane 검증에 사용한 `sessionId`,
`requestBindingSha256`와 file fingerprint는 public outcome에 노출하지 않는다.
### 4.4 아직 구현되지 않은 pause와 checkpoint lifecycle 목표
현재 `ResumableUploadPort``upload()`와 server-side `abort()`만 제공한다.
caller가 자신의 `AbortSignal`을 중단하면 committed checkpoint가 남아 다음
`upload()`에서 resume할 수 있지만, 이것은 명시적 pause protocol이 아니다. 다른
tab의 active upload를 non-destructive하게 pause하는 API도 없으며 현재 cross-tab
cancel signal은 explicit server abort를 준비하기 위한 신호다.
향후 pause를 선택하면 다음을 별도 version으로 구현한다.
```text
pause(uploadKey)
-> active read/fetch/backoff cancel
-> RESUMABLE_UPLOAD_PAUSE_V1 ephemeral cross-context signal
-> per-key mutation lock
-> exact revision CAS to PAUSED
-> in-memory part capability retirement
-> no server multipart abort
resume through upload()
-> PAUSED checkpoint validation
-> server-authoritative status
-> local completed-part re-hash
-> CAS to ACTIVE
-> missing parts only
```
현재 checkpoint `schemaVersion: 1`의 state는 `ACTIVE | ABORT_PENDING`뿐이다.
`PAUSED`를 durable state로 추가한다면 unknown-old-writer behavior와 migration을
정한 `schemaVersion: 2`가 필요하다. 기존 schema에 필드를 몰래 추가하지 않는다.
현재 checkpoint store도 single-key `read/CAS/remove`와 account partition 전체
삭제만 제공한다. abandoned upload가 같은 `uploadKey`로 다시 열리지 않으면 local
checkpoint를 retention 기준으로 자동 발견·정리하지 못한다. 향후 admin lifecycle은
다음을 제공한다.
- account partition 안에서 cursor 기반 bounded safe-summary inventory
- maximum age, count와 logical-byte budget
- expired/terminal candidate의 server status 재확인
- active lock/lease와 `ABORT_PENDING`을 무조건 삭제하지 않는 분류
- checkpoint와 owned local staging이 있다면 같은 cleanup journal로 처리
- cleanup response loss, blocked database와 CAS conflict reconciliation
- logout/account deletion용 exact partition maintenance authority
inventory는 file name, path, account/resource/session ID, digest, receipt와 capability를
application이나 operator UI에 반환하지 않는다. 제품 resume UI가 display metadata를
필요로 하면 제품 repository가 opaque `uploadKey`와 별도로 소유한다.
이 pause/inventory/retention 항목은 현재 **설계 목표이며 구현 완료가 아니다**.
app-managed background upload도 이 항목에 포함되지 않는다. worker upload는 source
bytes의 durable staging, worker auth/reissue, version drain과 platform support가
승인된 별도 optional capability다.
## 5. Streaming download
Streaming download는 server resource를 전체 `Blob`/`ArrayBuffer`로 materialize하지
않고 `Response.body`를 closed-result byte source로 변환한다.
- response는 200, non-opaque, non-redirect이고 body가 있어야 한다.
- capability의 media type, content encoding 정책과 response header가 일치해야 한다.
- native chunk는 configured output chunk ceiling으로 다시 분할한다.
- 누적 bytes가 expected/hard maximum을 넘으면 즉시 reader를 cancel한다.
- EOF에서 expected bytes보다 작으면 truncated failure다.
- integrity-required policy는 vetted incremental verifier를 사용하고 destination
close 전에 digest를 확인한다.
- first failed chunk 뒤에는 더 이상 bytes를 노출하지 않는다.
- abort/consumer early return에서 reader를 cancel하고 lock/capability lease를
해제한다.
- byte source는 기본 one-shot이며 두 번째 stream 소비를 거절한다.
File System Access save picker가 있으면 user activation 안에서 destination을 먼저
열고 stream을 쓴다. close와 integrity verification이 끝난 경우만 `SAVED`다.
anchor/navigation은 `BROWSER_HANDOFF`이며 disk write 완료를 의미하지 않는다.
save picker가 없는 browser의 whole-Blob fallback은 별도 small-artifact hard cap
아래에서만 허용한다.
Range 기반 resumable download는 이 streaming contract와 다른 capability다.
도입하려면 validator-bound `Range`, `206 Content-Range`, destination seek/truncate,
ETag/If-Range와 final whole-object integrity를 별도 계약으로 추가한다. 단순히
partial bytes를 기존 파일 뒤에 append하지 않는다.
현재 presigned provider는 `Range` request header를 금지하고 GET success를
`200`으로 고정하며 executor는 `Content-Range`를 거절한다. save destination도
순차 writable만 제공한다. 따라서 이 문단은 구현 설명이 아니라 미구현 경계를
뜻한다.
목표 `RANGE_RESUMABLE_DOWNLOAD_V1`은 다음을 모두 포함한다.
- immutable generation 또는 strong validator와 representation binding
- 비권한성 account-partitioned checkpoint와 CAS offset
- exact `206`, Range 무시/validator mismatch의 `200`, mode가 계약한 `412`,
`416` 상태 머신
- capability 재발급 뒤 representation binding 재검증
- seek/truncate destination 또는 journaled OPFS staging
- durable segment commit 뒤에만 checkpoint offset 전진
- 완료 뒤 destination 전체 재읽기와 whole-object SHA-256
- partial count/byte/age retention, crash/logout/account-switch cleanup
- system picker 미지원 환경의 browser-managed handoff/server-generation fallback
상세 불변조건과 promotion gate는
[VD-14](./decisions/VD-14-resumable-download-and-background-transfer.md)를 따른다.
Range의 primary current status는 `DESIGNED_NOT_IMPLEMENTED`다.
### 5.1 아직 구현되지 않은 strategy selector
현재 download strategy는 composition-registered file policy 하나에
`BROWSER_MANAGED`, `PROMPT_AND_STREAM` 또는 `BOUNDED_OBJECT_URL`로 정적으로
고정된다. source kind, exact size와 실제 picker/seek/OPFS 지원을 입력으로 안전한
fallback을 선택하는 공통 headless selector는 없다.
목표 selector는 user-agent 문자열이 아니라 capability probe를 사용한다.
| 조건 | 목표 결정 |
| --- | --- |
| server file, 탭 종료 뒤 계속 필요 | `BROWSER_MANAGED_HANDOFF` |
| verified foreground save, picker 지원 | `WHOLE_OBJECT_PICKER_STREAM` |
| Range resume 필수, destination 계약 충족 | `RANGE_RESUMABLE_FOREGROUND` |
| 작은 generated artifact | `BOUNDED_OBJECT_URL` |
| 큰 generated artifact, picker 미지원 | `SERVER_GENERATION_REQUIRED` 또는 `UNSUPPORTED` |
| background download가 선택되고 OPFS만 지원 | `APP_MANAGED_BACKGROUND_DOWNLOAD`; app-private staging 후 foreground export |
위 값은 application-level execution plan이다. 현재 file-delivery primitive에는
VD-16의 고정 mapping으로만 투영한다. source kind
`BROWSER_MANAGED_RESOURCE`를 strategy로 사용하거나 `PROMPT_AND_STREAM`
Range plan으로 재사용하지 않는다.
picker 미지원 때문에 unbounded Blob ceiling을 올리지 않는다. browser-managed
handoff는 disk save나 integrity 완료를 관찰할 수 없으므로 계속
`BROWSER_HANDOFF`다. 이 selector와 picker 미지원 failure normalization도 현재
구현돼 있지 않다.
## 6. Image CDN
### 6.1 Asset와 preset
application은 raw source URL이나 arbitrary transform query 대신 opaque asset
reference와 composition-registered preset reference를 전달한다. backend/CDN
descriptor는 다음을 묶는다.
- opaque asset ID와 immutable asset revision
- public/private delivery class
- source pixel dimensions와 안전 판정을 통과한 raster media type
- named preset와 exact crop/fit intent
- rendition별 format, natural width/height, URL와 expiry
- private rendition이면 server-issued capability binding
CDN은 임의 external source URL을 transform parameter로 받지 않는다. backend
asset registry가 quarantine/scan을 통과한 source object만 CDN asset ID로
promotion한다.
### 6.2 Policy validation
Composition policy가 소유하는 값:
- application origin과 그 origin과 다른 allowed HTTPS CDN origins/path prefix
- named preset와 allowed widths/DPR/formats
- max natural/output width·height·pixels와 decoded/encoded bytes
- crop/fit, quality와 static-raster 제한
- maximum candidates와 minimum private URL lifetime
- maximum concurrent capability verification
- public/private cache, referrer와 credential policy
runtime은 caller가 preset의 width, DPR, quality나 format ceiling을 늘리지 못하게
한다. SVG/HTML/data/blob/javascript URL과 active/unknown media type은 기본
거절한다. 현재 protocol은 static raster만 지원하므로 animated format은 항상
거절하며, 도입하려면 별도 frame/decode budget protocol이 필요하다.
Composition이 전달하는 hard limit은
`IMAGE_CDN_IMPLEMENTATION_CEILINGS`보다 항상 작거나 같아야 한다. 이 값은 제품
기본값이 아니라 adapter-owned 절대 상한이며 intrinsic/source/output pixel,
decoded/encoded byte, candidate, URL, capability lifetime와 동시 cryptographic
verification guard를 구성 실수로 해제하지 못하게 한다.
private capability의 signature는 versioned preset binding ID의 허용 집합을
묶는다. CDN/BFF는 그 ID를 server-owned immutable preset registry에서 조회하고
요청 query의 width/height/DPR/fit/format/quality가 registry가 산출한 exact
candidate인지 다시 계산해 불일치 요청을 거절해야 한다. 브라우저가 만든 query,
binding digest 또는 signature 문자열을 단순히 echo하거나 query 자체를
authorization proof로 취급하지 않는다.
capability policy의 `acceptedKeyIds`는 bounded unique overlap set이며 현재
signing key를 고르는 selector가 아니다. verifier의 immutable public-key
registry는 runtime 생성 시 이 집합 전체를 포함해야 하고, descriptor의 단일
`signature.keyId`는 policy와 verifier 양쪽에 exact membership이 있어야 한다.
rotation은 새 public key와 old/new overlap policy 배포, client 채택 확인,
backend signer 전환, `maxCapabilityLifetimeMs + maxClockSkewMs`와 client rollout
기간 경과, old key 제거 순서를 따른다. 유출 key는 overlap 절차 대신 backend
revocation과 runtime 재조합/강제 rollout 대상으로 다룬다.
browser probe는 encoded body를 hard cap 안에서 읽은 직후 native decoder 호출
전에 PNG/JPEG/WebP/AVIF header/container metadata를 파싱한다. 선언된
width/height, pixel 수와 decoded-byte ceiling을 먼저 확인하고 APNG/WebP
animation, AVIF sequence/derived image와 ambiguous/malformed container를
fail-closed한다. 이 pre-decode 검사가 통과한 static raster만
`createImageBitmap`으로 실제 dimensions를 재검증한다.
### 6.3 Responsive descriptor
width descriptor를 쓰는 candidate는 모두 양의 고유 width를 가지며 오름차순으로
정렬한다. 같은 source set에서 `w``x` descriptor를 섞지 않는다. `sizes`
registry가 승인한 layout token에서 결정하고 arbitrary presentation 문자열을
CDN query에 넣지 않는다.
반환값은 presentation-safe descriptor다.
- fallback `src`, intrinsic width/height
- ordered format별 `srcset`
- registry-owned `sizes`
- `loading`, `decoding`, `fetchPriority`
- `referrerPolicy=no-referrer`
- application과 분리된 CDN origin의 asset은 `crossOrigin=anonymous`
private signed image는 expiry 전에 실제 load가 시작될 수 있는 eager/priority
정책만 사용하거나 load 직전에 새 descriptor를 발급한다. 오래된 signed URL을
DOM, persisted state, telemetry 또는 query cache에 장기 보관하지 않는다.
private delivery는 `PRIMARY_REQUIRED` browser probe가 필수이며 이를 `NONE`으로
낮출 수 없다. probe는 `credentials: omit`, redirect 금지, exact response URL과
`Cache-Control: no-store`를 실제 response에서 확인한다.
실제 `<img crossorigin="anonymous">`는 same-origin일 때 cookie를 보낼 수
있으므로 registry는 CDN origin이 composition의 application origin과 같으면
생성 단계에서 거절한다. private CDN 응답은 cookie나 ambient authorization에
의존하지 않는다.
### 6.4 CDN cache와 invalidation
- public rendition은 asset revision을 URL에 포함하고
`public, max-age=..., immutable`로 제공한다.
- content가 바뀌면 purge에 의존해 같은 URL을 재사용하지 않고 revision을 바꾼다.
- format은 URL에서 명시하거나 `Vary: Accept` 계약과 cache key를 정확히 맞춘다.
- private rendition은 짧은 expiry와 필수 `no-store`를 쓴다.
- CDN cache hit 여부는 authorization이나 asset safety proof가 아니다.
image probe의 단일 bounded deadline은 response header fetch, streamed body read와
native decode 전체를 포함한다. timeout/cancel/error 시 reader를 cancel하고,
abort 뒤 늦게 resolve한 response body도 cancel하며 늦게 생성된 `ImageBitmap`
즉시 `close()`한다.
Image CDN runtime의 `close()`는 terminal/idempotent다. application teardown,
logout, account/tenant partition 변경 또는 runtime 교체 시 composition owner가
한 번 호출한다. runtime lifetime signal은 진행 중 capability verification과
probe를 중단하고, accepted capability WeakMap은 새 WeakMap으로 교체되어 기존
reference를 즉시 revoke하면서 strong reference를 남기지 않는다. 닫힌 runtime은
accept/resolve를 `UNAVAILABLE`로 거절하며 재개하지 않고 새 runtime을 조합한다.
### 6.5 아직 구현되지 않은 descriptor provider와 refresh
현재 Image CDN runtime은 trusted gateway가 이미 strict하게 decode했다고 가정한
`BackendIssuedImageAsset` 또는 composition-owned public descriptor를
`runtime.assets`에 전달받는다. opaque asset/preset validation, P-256 signature,
responsive URL 생성과 browser probe는 구현돼 있지만 BFF에서 private descriptor를
가져오는 concrete HTTP provider는 없다.
향후 `IMAGE_CDN_DESCRIPTOR_V1` provider는 다음 계약을 가진다.
- composition-owned fixed HTTPS BFF endpoint
- caller가 전달하는 값은 opaque product asset reference와 registered intention뿐
- authenticated control-plane request, redirect 금지와 no-store
- exact final response URL/status/content type/content length
- fatal UTF-8 bounded JSON body와 unknown-field rejection
- issuer, asset/revision, dimensions, delivery class, preset binding ID,
issued/expiry와 P-256 signature의 exact schema
- request deadline, caller/runtime abort와 late response-body cleanup
- descriptor와 raw backend body를 query cache, persistence, log와 telemetry에
저장하지 않음
private descriptor가 minimum remaining lifetime 아래로 내려가면 presentation
runtime이 기존 signed URL을 임의 연장하지 않는다. product-owned facade가 같은
opaque asset/intention에 대해 single-flight reissue를 수행하고, 새 descriptor를
다시 signature/registry/probe 경계에 통과시킨다. asset revision, preset binding,
issuer 또는 account scope가 달라지면 기존 reference를 폐기하고 새 결과로
교체한다. logout, tenant switch, key compromise와 kill switch에서는 refresh를
중단하고 runtime을 `close()`한다.
P-256 key overlap과 runtime close는 현재 구현돼 있지만 dynamic key-set fetch,
revocation epoch/list, descriptor auto-refresh와 backend scope revoke는 구현돼
있지 않다. 정상 key rotation은 composition의 immutable old/new registry 교체로,
긴급 회수는 backend revoke, runtime close와 forced rollout으로 처리한다.
presentation-safe descriptor를 실제 `<picture>/<source>/<img>`에 적용하는 renderer도
현재 공통 runtime 범위에는 없다. renderer primitive는 raw URL override를 받지 않고
descriptor 속성만 투영할 수 있지만, `alt`, placeholder, error/retry, SSR/preload와
analytics는 제품 presentation이 소유한다.
## 7. Failure와 recovery
| 조건 | 결과 | 복구 |
| --- | --- | --- |
| capability expired/revoked | `EXPIRED_RESOURCE` | 새 capability 발급 |
| method/origin/path/binding mismatch | `POLICY_REJECTED` | 요청 재구성 금지 |
| part status conflict | `CONFLICT` | server reconcile |
| part checksum mismatch | `INTEGRITY_FAILED` | 같은 bytes 재검증 후 retry/abort |
| response overrun/truncation | `INTEGRITY_FAILED` | destination abort, 새 download |
| session missing/gone | `EXPIRED_RESOURCE` | checkpoint 폐기 후 새 session |
| image preset/URL/pixel violation | `POLICY_REJECTED` | 안전한 placeholder/original policy |
| image capability verification concurrency ceiling | `LIMIT_EXCEEDED` | 진행 작업 종료 대기 또는 runtime 부하 조사 |
| closed Image CDN runtime 사용 | `UNAVAILABLE` | 새 runtime composition |
| network/429/모든 5xx | `UNAVAILABLE` | bounded retry/backoff |
관측성에는 operation, safe outcome, byte/part/candidate bucket, retry bucket과
failure code만 기록한다. capability ID, URL, query, asset/resource/session ID,
file name, digest, raw ETag, receipt와 backend message는 log/diagnostics/telemetry에
기록하지 않는다. 앞 절의 strict checkpoint allowlist만 durable 예외다.
## 8. 조합 조건
현재 `src/adapters/browser-transfer/index.ts`는 개별 presigned, upload와 Image CDN
factory를 export할 뿐 이들을 하나의 lifecycle과 account partition으로 묶는
top-level composition factory를 제공하지 않는다. browser file runtime, capability
vault, upload runtime과 Image CDN runtime은 각자 `dispose()`/`close()`를 가지지만
logout, account switch와 partial cleanup을 하나의 admission fence 아래 실행하는
owner도 아직 없다.
제품 composition 전에 반드시 정할 것:
- fixed BFF capability endpoint, closed upload endpoint map과 runtime schema
- `PRESIGNED_TRANSFER_V1``IMAGE_CDN_DESCRIPTOR_V1` rollout/drain 계획
- `PRESIGNED_MULTIPART_V1` canonical binding 재계산과 server-side session lookup
- same-origin proxy 또는 cross-origin CORS/object-storage topology
- per-account/partition Web Lock namespace와 ephemeral BroadcastChannel cancel
namespace, 미지원 환경의 bounded abort fallback
- per-operation maximum bytes, part size/count/concurrency와 retry budget
- upload success status/receipt header/`expectedResponseByteLength`와 response cap
- checksum algorithm과 full/composite 의미
- quarantine scan/promotion/status protocol
- checkpoint classification, account scope, retention과 logout handling
- application과 분리된 CDN origin, versioned preset exact 재계산,
format/pixel/decoded-byte/cache/CSP contract, key rotation,
verification concurrency와 runtime/probe deadline
- download save/handoff UX와 partial destination recovery
- browser matrix, fault injection, orphan cleanup과 CDN rollback runbook
이 값이 없으면 runtime factory를 bootstrap에 넣지 않는다. 선택하지 않은 runtime은
production module inventory와 removal gate로 기본 bundle에서 제외한다.
### 8.1 목표 top-level composition
향후 공통 factory의 책임은 dependency를 편리하게 묶는 것보다 구성 시 불변조건과
teardown 순서를 한 곳에서 강제하는 것이다.
```text
BrowserTransferComposition
product facade references
strict runtime config snapshot
browser file runtime
browser-managed capability provider/vault
presigned provider/vault/executor
upload control transport/runtime/checkpoint admin
download strategy selector
optional Range runtime/checkpoint/destination registry
image descriptor provider/runtime
account lifecycle fence
readiness/compatibility result
safe observer
kill switches
close()
```
browser-managed handoff mechanism에는 synchronous resolver seam이 있지만, 실제
BFF에서 capability를 발급받아 user activation 전에 in-memory identity vault에
준비하는 concrete provider는 현재 없다. 목표 provider는 fixed endpoint, strict
versioned response, safe receipt와 exact resource/media/extension/length/digest/expiry
binding을 검증하고 raw href는 resolver 내부에만 둔다. handoff 시점에는 async
발급을 시작하지 않고 이미 준비된 exact identity만 synchronous consume한다.
composition의 `close()`는 terminal/idempotent하고 다음 순서를 보장한다.
1. 신규 issue/upload/download/image resolve admission을 닫는다.
2. active foreground work와 worker/channel을 abort한다.
3. in-memory signed capability와 image reference를 revoke한다.
4. writer, reader, Web Lock, BroadcastChannel과 runtime을 close한다.
5. upload/Range checkpoint와 owned staging을 정책에 따라 reconcile한다.
6. logout/account deletion이면 maintenance authority로 exact partition cleanup을
실행한다.
7. blocked/ambiguous cleanup을 성공으로 표시하지 않고 safe recovery 결과로 남긴다.
primary status가 `COMPOSED`여도 completion ledger의 `RuntimeHealth`,
`PromotionEvidence``TrafficAdmission`은 별도다. config schema, endpoint/auth,
account partition, actual provider evidence, cleanup owner와 browser fallback 중
하나라도 없으면 `TrafficAdmission=DISABLED`,
`PromotionEvidence=MISSING | PARTIAL | EXPIRED`로 fail-closed한다.
### 8.2 Contract harness 목표
현재 unit test와 Playwright route interception은 reference runtime의 failure와
browser API behavior를 검증하지만 reusable BFF/provider conformance suite는 아니다.
`server-file-capability-infrastructure.md`의 provider contract matrix도 설계이며 실제
S3/MinIO/GCS/Azure/CDN adapter에 실행되는 source는 이 repository에 없다.
향후 harness는 같은 versioned fixture를 다음 네 등급에 실행한다.
| 등급 | 증명 범위 |
| --- | --- |
| deterministic fake | state machine, canonical binding과 failure mapping |
| intercepted browser route | Fetch/CORS/stream/abort와 native destination |
| emulator/container | provider SDK, multipart와 response header wiring |
| actual staging provider | 실제 product/API/version/region의 constraint 강제 |
필수 frontend/BFF contract:
- `PRESIGNED_TRANSFER_V1` exact request/response와 unknown version rejection
- expiry, wrong method/resource/range/part/header/query/origin rejection
- URL/query/header/error/telemetry redaction
- capability reissue와 server revocation
- upload create/status/part/complete/abort, response-loss와 orphan reconcile
- pause/checkpoint inventory가 구현될 경우 v1→v2 migration과 old-writer drain
- browser-managed capability preload/consume와 `BROWSER_HANDOFF` truth
- `IMAGE_CDN_DESCRIPTOR_V1`, signature/key overlap, expiry/reissue와 revocation
- CDN preset exact recomputation, private no-store와 public immutable cache
- Range를 구현할 경우 VD-14의 `200/206/412/416` 및 destination crash matrix
actual provider evidence에는 adapter/provider/config/contract version, artifact
digest, environment/region, pass/fail/skip, fault result, waiver/owner와 expiry를
기록한다. required case skip, expired evidence와 contract/config 변경은 production
promotion을 막는다. fake나 emulator success를 actual provider evidence로
승격하지 않는다.
## 9. 표준·vendor 참고
- [Browser data capability completion ledger](./browser-data-capability-completion-ledger.md)
- [Fetch Standard](https://fetch.spec.whatwg.org/)
- [RFC 9110 HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html)
- [File System Standard](https://fs.spec.whatwg.org/)
- [VD-14 resumable download와 background download](./decisions/VD-14-resumable-download-and-background-transfer.md)
- [VD-16 browser transfer composition과 image delivery](./decisions/VD-16-browser-transfer-composition-and-image-delivery.md)
- [HTML responsive images](https://html.spec.whatwg.org/multipage/images.html)
- [tus resumable upload protocol](https://tus.io/protocols/resumable-upload)
- [Amazon S3 presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html)
- [Amazon S3 multipart upload](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html)