758 lines
41 KiB
Markdown
758 lines
41 KiB
Markdown
# 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)
|