1883 lines
72 KiB
Markdown
1883 lines
72 KiB
Markdown
# Server file capability infrastructure handoff
|
|
|
|
- 상태: 설계 완료, 서버 구현 대기
|
|
- 목표 상태: `AVAILABLE_NOT_COMPOSED`
|
|
- 대상: 이후 서버 template 또는 제품 backend에서 구현할 공통 file capability
|
|
- 비대상: 제품 domain, 제품 use case, HTTP route/controller, provider 선택
|
|
- 기준일: 2026-07-28
|
|
|
|
이 문서는 frontend에 준비된 File/Blob/picker/download capability와 이후 연결할
|
|
서버측 기반을 정의한다. 서버는 object storage, multipart, integrity,
|
|
signed capability, idempotency와 quarantine을 실제 provider adapter로 제공하되,
|
|
제품이 파일 기능을 선택하기 전에는 endpoint, background job, database migration과
|
|
runtime composition을 설치하지 않는다.
|
|
|
|
브라우저 쪽 capability envelope, bounded fetch, resume checkpoint와 Image CDN
|
|
descriptor 계약은
|
|
[`presigned-transfer-and-image-cdn.md`](./presigned-transfer-and-image-cdn.md)와
|
|
VD-12가 소유한다. Range resume/background 경계는 VD-14, top-level transfer
|
|
composition과 Image descriptor provider는 VD-16이 소유한다. 이 문서의 server
|
|
port를 그 frontend DTO에 직접 노출하지 않고, 제품 BFF/controller가 두 경계
|
|
사이를 매핑한다.
|
|
|
|
`S3`, `MinIO`, `GCS`, `Azure Blob`, PostgreSQL, Redis, scanner vendor는 이 문서의
|
|
port 구현 후보일 뿐이다. provider SDK type, bucket/key, multipart upload ID,
|
|
scanner 원문 결과는 port 밖으로 노출하지 않는다.
|
|
|
|
이 설계의 최상위 불변조건은 다음과 같다.
|
|
|
|
> object storage에 byte가 존재하는 것, multipart complete가 성공한 것,
|
|
> scan이 clean으로 끝난 것, 사용자가 접근 가능한 `AVAILABLE` 상태는 서로 다른
|
|
> commit point다. 어느 단계도 다음 단계를 암묵적으로 보장하지 않는다.
|
|
|
|
## 1. 설계 경계
|
|
|
|
### 1.1 이 문서가 제공하는 것
|
|
|
|
- streaming object read/write의 provider-neutral port와 불변조건
|
|
- multipart staging/complete/abort 상태 머신
|
|
- 실제 byte length와 checksum의 authoritative 검증 규칙
|
|
- 짧은 수명의 제한된 signed capability
|
|
- 동시 replay를 견디는 idempotency store
|
|
- immutable quarantined object를 검사하는 scan port
|
|
- provider locator를 숨기는 opaque object reference
|
|
- deadline, retry, cancellation과 resource cleanup 규칙
|
|
- provider exception을 닫는 공통 failure model
|
|
- 모든 provider가 통과해야 하는 동일 contract test
|
|
- 미선택 상태, 선택적 composition과 완전 제거 계약
|
|
|
|
### 1.2 이 문서가 만들지 않는 것
|
|
|
|
- `AttachContractDocument`, `UploadProfileImage` 같은 제품 use case
|
|
- `ContractAttachment`, `EvidenceDocument` 같은 domain model
|
|
- 업무별 MIME, 용량, retention, 소유권과 승인 규칙
|
|
- 실제 REST/GraphQL endpoint와 request/response DTO
|
|
- account/tenant authorization 구현
|
|
- 실제 bucket, region, database, queue, scanner 선택
|
|
- frontend offline sync 전체 protocol
|
|
- CDN/Service Worker/Cache Storage release protocol
|
|
|
|
제품 선택 이후에는 이 capability 위에 feature application port와 use case를
|
|
추가한다. 공통 capability가 domain 이름이나 aggregate ID를 알게 해서는 안 된다.
|
|
|
|
이 문서의 `create`, `putPart`, `complete`, `scan`, `promote`는 provider와 technical
|
|
lifecycle을 닫기 위한 platform protocol operation이지, 그대로 외부에 노출할 제품
|
|
use case가 아니다. 제품 use case가 생기기 전에는 caller authorization, aggregate
|
|
연결, 업무 정책 선택과 endpoint가 없으므로 실행 경로도 composition하지 않는다.
|
|
단, orphan reconciliation과 resource cleanup 규칙은 adapter가 만든 기술 자원의
|
|
정합성을 위한 infrastructure 책임으로 정의해 둘 수 있다.
|
|
|
|
### 1.3 계층과 방향
|
|
|
|
```text
|
|
future product feature
|
|
domain/use case
|
|
-> product-owned file port
|
|
-> server file capability facade
|
|
-> ObjectByteStorePort
|
|
-> MultipartStagingPort
|
|
-> ObjectRegistryPort
|
|
-> IdempotencyStorePort
|
|
-> ContentScannerPort
|
|
-> CleanPromotionPort
|
|
-> SignedTransferCapabilityPort
|
|
-> provider adapters
|
|
S3 / MinIO / GCS / Azure Blob
|
|
PostgreSQL / Redis
|
|
scanner / CDR
|
|
```
|
|
|
|
HTTP controller는 미래의 inbound adapter다. object store, metadata database,
|
|
scanner와 signer 구현은 outbound adapter다. 이 문서의 port와 protocol은
|
|
application-neutral server platform 경계다.
|
|
|
|
### 1.4 control plane과 data plane
|
|
|
|
두 plane을 하나의 범용 `FileService` method로 뭉개지 않는다.
|
|
|
|
| plane | 책임 | 일반적인 경로 |
|
|
| --- | --- | --- |
|
|
| control | authorization, session, 정책 snapshot, idempotency, 상태 전이, capability 발급 | Browser → Web API/BFF |
|
|
| data | bounded byte upload/download, range, checksum, backpressure | Browser → BFF proxy 또는 제한된 signed URL → object store |
|
|
| inspection | quarantine read, parser/scanner/CDR, verdict | worker → private object store/scanner |
|
|
|
|
브라우저는 provider 관리자 credential, internal bucket/key 또는 database
|
|
credential을 받지 않는다. direct transfer가 필요하면 control plane이 exact
|
|
operation에 한정된 short-lived capability만 발급한다.
|
|
|
|
### 1.5 권장 서버 모듈 경계
|
|
|
|
언어와 framework가 달라도 dependency 방향은 다음과 같이 유지한다.
|
|
|
|
```text
|
|
file-capability/
|
|
protocol/
|
|
identifiers, lifecycle, policy, failure, receipts
|
|
ports/
|
|
object-store, multipart, registry, idempotency, signer, scanner, promotion
|
|
runtime/
|
|
streaming, integrity, deadline, retry, resource-lease, reconciliation
|
|
adapters/
|
|
object-store/<provider>
|
|
registry/<provider>
|
|
idempotency/<provider>
|
|
signer/<provider>
|
|
scanner/<provider>
|
|
contract-tests/
|
|
reusable suites, fixtures, conformance evidence
|
|
composition/
|
|
optional provider profile, probes, workers, kill switches
|
|
```
|
|
|
|
- `protocol`과 `ports`는 HTTP/framework/provider SDK에 의존하지 않는다.
|
|
- `runtime`은 제품 domain을 모르며 port와 protocol만 사용한다.
|
|
- concrete SDK type과 exception은 각 adapter package 안에서 끝난다.
|
|
- concrete adapter를 동시에 import할 수 있는 곳은 composition root와 해당
|
|
provider contract fixture뿐이다.
|
|
- 제품 module은 capability facade 또는 product-owned port에만 의존한다.
|
|
- provider adapter는 별도 package/dependency로 격리해 미선택 build와 제거
|
|
profile에서 아예 포함되지 않게 한다.
|
|
- 미래 inbound controller와 제품 use case는 이 tree 밖의 제품 feature가
|
|
소유한다.
|
|
|
|
## 2. 공통 식별자와 영속 모델
|
|
|
|
### 2.1 외부에 노출 가능한 opaque reference
|
|
|
|
다음 reference는 URL-safe cryptographic random 값이거나 같은 수준의
|
|
registry-issued opaque 값이어야 한다.
|
|
|
|
```text
|
|
ObjectRef
|
|
UploadSessionRef
|
|
UploadPartRef
|
|
TransferCapabilityReceipt
|
|
IdempotencyKey
|
|
ScanJobRef
|
|
```
|
|
|
|
최소 요구사항:
|
|
|
|
- 최소 128-bit의 예측 불가능성
|
|
- account ID, email, 업무 key, filename, bucket, region을 인코딩하지 않음
|
|
- 대소문자와 Unicode normalization이 개입하지 않는 제한된 alphabet
|
|
- log/metric/trace label에 원문을 기록하지 않음
|
|
- reference만으로 authorization을 얻지 못함
|
|
- 서로 다른 종류의 reference를 type/namespace로 혼용하지 않음
|
|
- authorization scope와 object generation/version은 reference 문자열에 넣지 않고
|
|
server registry와 authorization decision에서 별도로 검증
|
|
- caller가 볼 권한이 없는 reference와 존재하지 않는 reference는 enumeration이
|
|
가능한 API에서 같은 외부 failure 표현 사용
|
|
|
|
`ObjectRef`는 provider object key가 아니다. server-owned registry가 아래 binding을
|
|
소유한다.
|
|
|
|
```text
|
|
ObjectBinding
|
|
objectRef
|
|
generation
|
|
providerProfileId
|
|
encryptedProviderLocator
|
|
state
|
|
byteLength
|
|
mediaType
|
|
wholeObjectDigest
|
|
createdAt
|
|
policySnapshotId
|
|
```
|
|
|
|
`encryptedProviderLocator`에는 provider가 요구하는 bucket/key/version locator만
|
|
보관한다. controller, domain과 frontend DTO에 반환하지 않는다. provider migration
|
|
시에도 `ObjectRef`는 유지하고 binding만 versioned CAS로 교체할 수 있어야 한다.
|
|
|
|
### 2.2 technical lifecycle
|
|
|
|
제품 domain 상태와 분리된 server file capability의 기술 상태는 다음과 같다.
|
|
|
|
```text
|
|
UploadSession
|
|
OPEN
|
|
-> COMPLETE_REQUESTED
|
|
-> PROVIDER_COMMITTED
|
|
-> QUARANTINE_RECORDED
|
|
-> QUARANTINED
|
|
-> ABORT_REQUESTED -> ABORTED
|
|
-> EXPIRED -> CLEANUP_PENDING -> CLEANED
|
|
|
|
StoredObject
|
|
STAGING
|
|
-> QUARANTINED
|
|
-> AVAILABLE
|
|
-> REJECTED
|
|
-> DELETING -> DELETED
|
|
```
|
|
|
|
불변조건:
|
|
|
|
- `AVAILABLE` 전에는 일반 download capability를 발급하지 않는다.
|
|
- `QUARANTINED` object는 private/non-executable 위치와 response policy를 사용한다.
|
|
- 상태 전이는 `(objectRef, generation, expectedState)` CAS로 수행한다.
|
|
- scan verdict는 exact generation과 digest에 binding한다.
|
|
- complete/abort/expiry race는 하나의 durable 상태만 승리한다.
|
|
- `PROVIDER_COMMITTED`는 provider complete 성공과 registry 응답 유실 사이를
|
|
복구하는 필수 fence다.
|
|
- `QUARANTINE_RECORDED`는 exact provider generation/length/digest와 scan outbox가
|
|
같은 database transaction 또는 동등한 atomic outbox로 durable해진 상태다.
|
|
- terminal state를 되돌리려면 새 generation과 새 audit record를 만든다.
|
|
- provider object 존재만으로 `AVAILABLE`이라고 판단하지 않는다.
|
|
|
|
### 2.3 정책 snapshot
|
|
|
|
미래 use case가 선택한 정책은 session 생성 시 immutable snapshot으로 binding한다.
|
|
|
|
```text
|
|
TransferPolicySnapshot
|
|
policyId
|
|
policyVersion
|
|
maxObjectBytes
|
|
minPartBytes
|
|
maxPartBytes
|
|
maxPartCount
|
|
maxConcurrency
|
|
allowedChecksumAlgorithms
|
|
requiredScanProfile
|
|
sessionExpiresAt
|
|
capabilityMaxTtl
|
|
orphanRetention
|
|
```
|
|
|
|
caller는 이 한도를 낮출 수만 있다. 진행 중인 session이 mutable configuration을
|
|
다시 읽어 한도가 상승하거나 의미가 바뀌어서는 안 된다. 긴급 차단은 별도
|
|
deny/kill-switch registry로 fail-closed하게 적용한다.
|
|
|
|
## 3. Server platform ports
|
|
|
|
아래 signature는 언어 중립적인 의미 계약이다. 실제 서버 언어의 native stream,
|
|
SDK response와 exception이 이 경계를 통과해서는 안 된다.
|
|
|
|
### 3.0 공통 실행 문맥과 숫자 표현
|
|
|
|
모든 I/O port 호출은 같은 bounded execution context를 받는다.
|
|
|
|
```text
|
|
OperationContext
|
|
registeredOperation
|
|
opaqueRequestId
|
|
opaqueTraceId?
|
|
monotonicDeadline
|
|
cancellation
|
|
```
|
|
|
|
- ingress timeout은 server-owned hard cap으로 clamp한다.
|
|
- 다른 process로 전달할 때 client의 absolute timestamp를 신뢰하지 않고 남은
|
|
timeout을 상한 안에서 전달한 뒤 각 process에서 monotonic deadline으로 바꾼다.
|
|
- child operation은 `min(parent remaining, operation cap)`만 사용할 수 있다.
|
|
- queue 대기, retry backoff와 cleanup 전의 본 작업도 전체 deadline에 포함한다.
|
|
- request/trace ID와 idempotency key는 서로 다른 식별자다.
|
|
- byte length, offset과 합계는 signed 64-bit 범위를 안전하게 표현하는
|
|
`bigint`/decimal value object를 사용한다. JSON number나 provider SDK의 좁은
|
|
정수로 암묵 변환하지 않고 모든 덧셈·곱셈 overflow를 검사한다.
|
|
|
|
### 3.1 `ObjectByteStorePort`
|
|
|
|
```text
|
|
ObjectByteStorePort
|
|
capabilities() -> Result<ObjectStoreCapabilities>
|
|
|
|
inspect(locator, context)
|
|
-> Result<ProviderObjectMetadata | NOT_FOUND>
|
|
|
|
openRead(
|
|
locator,
|
|
expectedVersion?,
|
|
range?,
|
|
context
|
|
) -> Result<OpenedObjectRead>
|
|
|
|
putAtomic(
|
|
newLocator,
|
|
expectedLength,
|
|
expectedDigest?,
|
|
mediaType,
|
|
byteStream,
|
|
context
|
|
) -> Result<ProviderWriteReceipt>
|
|
|
|
deleteExact(
|
|
locator,
|
|
expectedVersion,
|
|
context
|
|
) -> Result<DeleteReceipt>
|
|
```
|
|
|
|
`OpenedObjectRead`는 다음을 제공한다.
|
|
|
|
```text
|
|
OpenedObjectRead
|
|
providerVersion
|
|
declaredByteLength
|
|
mediaType
|
|
contentRange?
|
|
stream: bounded backpressure byte stream
|
|
close()
|
|
```
|
|
|
|
필수 규칙:
|
|
|
|
- read stream은 첫 실패 이후 byte를 더 전달하지 않는다.
|
|
- EOF 전에 declared length를 초과하거나 미달하면 `INTEGRITY_FAILED`다.
|
|
- range는 normalized inclusive/exclusive 의미를 port에서 하나로 고정한다.
|
|
- empty object와 empty range를 구분한다.
|
|
- conditional version mismatch는 `CONFLICT`이지 임의의 `NOT_FOUND`가 아니다.
|
|
- `putAtomic` 성공은 provider가 새 object/version을 durable하게 확정한 뒤만 반환한다.
|
|
- overwrite를 기본 허용하지 않는다. 새 locator 또는 exact version CAS만 허용한다.
|
|
- `deleteExact`의 late response loss는 inspect/reconcile로 결정하며 blind retry하지
|
|
않는다.
|
|
|
|
### 3.2 `MultipartStagingPort`
|
|
|
|
```text
|
|
MultipartStagingPort
|
|
create(
|
|
newLocator,
|
|
mediaType,
|
|
providerConstraints,
|
|
context
|
|
) -> Result<ProviderMultipartRef>
|
|
|
|
putPart(
|
|
multipartRef,
|
|
partNumber,
|
|
exactOffset,
|
|
expectedLength,
|
|
expectedDigest,
|
|
byteStream,
|
|
context
|
|
) -> Result<ProviderPartReceipt>
|
|
|
|
inspectParts(
|
|
multipartRef,
|
|
context
|
|
) -> Result<ProviderPartSet>
|
|
|
|
complete(
|
|
multipartRef,
|
|
orderedExactPartReceipts,
|
|
context
|
|
) -> Result<ProviderWriteReceipt>
|
|
|
|
abort(
|
|
multipartRef,
|
|
context
|
|
) -> Result<AbortReceipt>
|
|
```
|
|
|
|
`ProviderMultipartRef`, provider part ETag와 raw upload ID는 adapter-private
|
|
branded/opaque 값이다. frontend가 받는 `UploadSessionRef`와 동일하지 않다.
|
|
|
|
필수 규칙:
|
|
|
|
- part number는 연속성·범위·중복을 server ledger가 검증한다.
|
|
- part receipt는 multipartRef, part number, length, digest와 provider version에
|
|
binding한다.
|
|
- provider ETag를 SHA-256으로 해석하지 않는다.
|
|
- `complete`는 server ledger가 승인한 ordered receipt만 사용한다.
|
|
- `complete` 성공 후에도 object는 `QUARANTINED`다.
|
|
- `abort`는 idempotent하며 already-aborted를 성공으로 재생할 수 있다.
|
|
- complete가 먼저 commit됐다면 abort는 완료 object를 삭제하지 않고 `CONFLICT`를
|
|
반환한다.
|
|
- 실패한 create/put/complete는 provider multipart orphan을 남길 수 있으므로
|
|
bounded reconciliation 대상이 된다.
|
|
|
|
### 3.3 `ObjectRegistryPort`
|
|
|
|
```text
|
|
ObjectRegistryPort
|
|
reserveObject(bindingDraft, expectedAbsent, context) -> Result<ObjectReservation>
|
|
getObject(objectRef, context) -> Result<ObjectBinding | NOT_FOUND>
|
|
transitionObject(objectRef, generation, expectedState, nextState, context)
|
|
-> Result<ObjectBinding>
|
|
|
|
createSession(sessionSnapshot, context) -> Result<UploadSessionRecord>
|
|
getSession(sessionRef, context) -> Result<UploadSessionRecord | NOT_FOUND>
|
|
appendPart(sessionRef, expectedRevision, partRecord, context)
|
|
-> Result<UploadSessionRecord>
|
|
transitionSession(sessionRef, expectedRevision, expectedState, nextState, context)
|
|
-> Result<UploadSessionRecord>
|
|
|
|
listExpiredOpenSessions(cursor, maxRows, context) -> Result<SessionPage>
|
|
listReconciliationCandidates(cursor, maxRows, context) -> Result<ObjectPage>
|
|
```
|
|
|
|
registry transaction이 object provider transaction과 원자적이라고 가정하지 않는다.
|
|
모든 cross-system mutation은 durable phase와 reconciliation을 갖는 saga다.
|
|
|
|
### 3.4 `SignedTransferCapabilityPort`
|
|
|
|
```text
|
|
SignedTransferCapabilityPort
|
|
issueUploadPart(
|
|
subjectScope,
|
|
sessionRef,
|
|
providerLocator,
|
|
partNumber,
|
|
exactLength,
|
|
requiredDigest,
|
|
requiredHeaders,
|
|
expiresAt,
|
|
context
|
|
) -> Result<SignedTransferCapability>
|
|
|
|
issueDownload(
|
|
subjectScope,
|
|
objectRef,
|
|
generation,
|
|
providerLocator,
|
|
allowedRange?,
|
|
responseMetadata,
|
|
expiresAt,
|
|
context
|
|
) -> Result<SignedTransferCapability>
|
|
```
|
|
|
|
```text
|
|
SignedTransferCapability
|
|
receipt
|
|
href
|
|
method
|
|
requiredHeaders
|
|
objectRef
|
|
generation/sessionRef
|
|
exactLengthOrMaximum
|
|
expectedDigest?
|
|
mediaType
|
|
safeExtension?
|
|
expiresAt
|
|
```
|
|
|
|
capability는 이미 authorization된 control-plane 결과다. signer port가 업무 권한을
|
|
판단하지 않는다.
|
|
|
|
### 3.5 `IdempotencyStorePort`
|
|
|
|
```text
|
|
IdempotencyStorePort
|
|
begin(scope, operation, key, requestFingerprint, leaseUntil, context)
|
|
-> Result<ACQUIRED<LeaseAndFence> | IN_PROGRESS | REPLAY<CompletionReceipt>>
|
|
|
|
renew(leaseAndFence, extendUntil, context)
|
|
-> Result<LeaseAndFence>
|
|
|
|
complete(leaseAndFence, requestFingerprint, completionReceipt, expiresAt, context)
|
|
-> Result<CompletionReceipt>
|
|
|
|
releaseRetryable(
|
|
leaseAndFence,
|
|
requestFingerprint,
|
|
proofSideEffectNotStarted,
|
|
context
|
|
)
|
|
-> Result<void>
|
|
|
|
pruneExpired(cursor, maxRows, context)
|
|
-> Result<PrunePage>
|
|
```
|
|
|
|
`scope + operation + key`가 unique key다. 같은 key를 다른 request fingerprint로
|
|
재사용하면 `CONFLICT`다. idempotency record는 provider upload ID, signed URL,
|
|
credential, 원본 filename 또는 request body를 저장하지 않는다.
|
|
|
|
lease expiry 뒤 takeover는 fence를 증가시키고 이전 owner의 renew/complete를
|
|
거절한다. 외부 side effect가 시작됐는지 불명확하면 record를 지우거나
|
|
`releaseRetryable`하지 않고 `RECOVERY_REQUIRED`로 남겨 reconciler가 provider와
|
|
registry를 확인한다.
|
|
|
|
### 3.6 `ContentScannerPort`
|
|
|
|
```text
|
|
ContentScannerPort
|
|
capabilities() -> Result<ScannerCapabilities>
|
|
|
|
submit(
|
|
scanJobRef,
|
|
objectRef,
|
|
generation,
|
|
immutableDigest,
|
|
scanProfileId,
|
|
sourceFactory,
|
|
context
|
|
) -> Result<ScanSubmissionReceipt>
|
|
|
|
inspect(jobRef, context) -> Result<ScanStatus>
|
|
|
|
cancel(jobRef, context) -> Result<CancelReceipt>
|
|
```
|
|
|
|
```text
|
|
ScanStatus
|
|
PENDING | RUNNING
|
|
CLEAN {
|
|
objectRef, generation, immutableDigest,
|
|
engineProfile, definitionVersion, completedAt
|
|
}
|
|
REJECTED {
|
|
objectRef, generation, immutableDigest,
|
|
allowlistedReasonCodes, completedAt
|
|
}
|
|
FAILED_RETRYABLE { safeReasonCode }
|
|
FAILED_TERMINAL { safeReasonCode }
|
|
```
|
|
|
|
scanner의 raw stdout, path, vendor exception, signature name과 원본 filename은
|
|
application response나 telemetry로 전달하지 않는다.
|
|
|
|
### 3.7 `CleanPromotionPort`
|
|
|
|
검사와 availability commit 권한을 한 adapter에 함께 주지 않는다.
|
|
|
|
```text
|
|
CleanPromotionPort
|
|
promote(
|
|
objectRef,
|
|
expectedGeneration,
|
|
expectedQuarantineDigest,
|
|
sealedCleanScanReceipt,
|
|
idempotency,
|
|
context
|
|
) -> Result<AvailableObjectReceipt>
|
|
|
|
reject(
|
|
objectRef,
|
|
expectedGeneration,
|
|
expectedQuarantineDigest,
|
|
sealedRejectedScanReceipt,
|
|
idempotency,
|
|
context
|
|
) -> Result<RejectedObjectReceipt>
|
|
```
|
|
|
|
- scan receipt는 exact object/generation/digest/scan-policy version에 binding된
|
|
server-only sealed value다.
|
|
- scanner는 registry를 `AVAILABLE`로 전환할 권한을 갖지 않는다.
|
|
- promotion adapter는 raw scanner 결과를 해석하지 않고 sealed receipt만 검증한다.
|
|
- CDR/sanitized output은 원본과 다른 object generation/digest이므로 새 object로
|
|
length/digest를 다시 검증한다.
|
|
|
|
## 4. Streaming object read/write
|
|
|
|
### 4.1 공통 stream envelope
|
|
|
|
stream은 다음을 명시적으로 소유한다.
|
|
|
|
- backpressure와 최대 queued bytes
|
|
- immutable declared length 또는 `unknown`
|
|
- operation deadline과 cancellation
|
|
- 첫 실패에서 terminal close
|
|
- byte counter와 digest accumulator
|
|
- completion/close truth
|
|
- resource lease
|
|
|
|
전체 object를 하나의 byte array, string, base64 또는 temporary in-memory Blob으로
|
|
합치는 API를 공통 경로로 제공하지 않는다. small-object 편의 API가 필요하면
|
|
composition-owned hard cap 아래 별도 wrapper로만 제공한다.
|
|
|
|
### 4.2 proxy upload
|
|
|
|
```text
|
|
request body
|
|
-> transport maximum-body guard
|
|
-> decoded stream
|
|
-> authoritative byte counter
|
|
-> digest accumulator
|
|
-> provider write stream
|
|
-> provider commit
|
|
-> registry phase transition
|
|
```
|
|
|
|
- `Content-Length`는 preflight hint일 뿐 실제 count를 대체하지 않는다.
|
|
- chunked transfer나 HTTP/2/3에서도 실제 decoded payload를 센다.
|
|
- max bytes를 넘는 즉시 upstream read와 provider write를 모두 cancel한다.
|
|
- provider commit 전에 client disconnect가 발생하면 staging을 abort/cleanup한다.
|
|
- provider commit 뒤 client response가 유실되면 idempotency replay가 같은 receipt를
|
|
반환해야 한다.
|
|
|
|
### 4.3 proxy download
|
|
|
|
```text
|
|
authorized immutable ObjectBinding
|
|
-> exact provider version open
|
|
-> byte/digest/range verifier
|
|
-> response stream
|
|
-> close/cancel
|
|
```
|
|
|
|
- authorization은 open 전에 완료한다.
|
|
- `AVAILABLE`과 exact generation을 다시 확인한다.
|
|
- metadata preflight와 stream open이 분리된 provider에서는 둘 다 같은 immutable
|
|
provider version에 pin하고 `If-Match`와 동등한 조건을 강제한다. version pin을
|
|
지원하지 않으면 HEAD 뒤 GET 같은 TOCTOU 경로를 사용하지 않는다.
|
|
- response header를 확정하기 전에 media type, length, range와 filename을
|
|
normalize한다.
|
|
- range response는 exact provider version, normalized range와 실제 반환 byte
|
|
count를 검증한다. whole-object SHA-256만으로 부분 range의 무결성을 검증했다고
|
|
주장하지 않는다.
|
|
- 검증 가능한 range가 필요하면 immutable chunk digest/Merkle manifest를 별도로
|
|
설계하고 exact chunk proof를 검증한다. 그렇지 않은 range는 transport integrity와
|
|
version/range binding만 보장하며 `digestVerified=false`로 명시한다.
|
|
- `Content-Disposition` filename은 advisory metadata로 sanitize한다.
|
|
- sensitive object는 기본적으로 attachment, `nosniff`, private/no-store 정책을
|
|
사용한다.
|
|
- downstream disconnect는 provider read를 cancel한다.
|
|
- download 완료 metric은 server stream 종료이지 사용자 disk 저장 완료가 아니다.
|
|
|
|
### 4.4 direct provider transfer
|
|
|
|
signed URL을 사용하는 direct transfer에서는 server가 byte stream을 직접 보지
|
|
못한다. 그러므로 다음을 모두 만족해야 한다.
|
|
|
|
- capability가 method, exact provider locator, part/range, length, checksum header,
|
|
expiry와 subject/session scope에 binding
|
|
- provider가 해당 조건을 실제 request에서 강제
|
|
- complete 전에 server가 provider part/object metadata를 authoritative하게 재조회
|
|
- provider metadata가 충분하지 않으면 quarantined object를 server-side stream으로
|
|
다시 읽어 length와 whole-object digest 검증
|
|
- verification 완료 전 registry 상태를 `AVAILABLE`로 전환하지 않음
|
|
|
|
provider가 검증하지 않는 client-declared metadata를 signed request에 포함했다는
|
|
이유만으로 integrity를 주장하지 않는다.
|
|
|
|
## 5. Multipart protocol
|
|
|
|
### 5.1 control-plane 상태 흐름
|
|
|
|
frontend reference runtime과 연결하는 ordered multipart wire protocol literal은
|
|
`PRESIGNED_MULTIPART_V1`이다. create/status/part capability/complete/abort의
|
|
application contract, session response와 durable checkpoint 전체에서 이 protocol
|
|
값을 exact하게 검증한다. `UPLOAD_PART` capability binding 자체에도 protocol을
|
|
포함하고, part capability endpoint는 `sessionId`로 조회한 server-side session과
|
|
이 값이 일치하는지 확인해야 한다.
|
|
|
|
```text
|
|
create
|
|
authorize + policy snapshot + idempotency begin
|
|
reserve ObjectRef/generation
|
|
create provider multipart staging
|
|
persist OPEN session
|
|
return session constraints
|
|
|
|
put part
|
|
reauthorize session scope
|
|
validate OPEN + expiry + part plan
|
|
claim part idempotency
|
|
stream or issue exact signed part capability
|
|
persist verified ProviderPartReceipt by session revision CAS
|
|
|
|
complete
|
|
reauthorize
|
|
idempotency begin
|
|
CAS OPEN -> COMPLETE_REQUESTED
|
|
validate ordered complete part set
|
|
provider complete
|
|
persist PROVIDER_COMMITTED with exact provider generation
|
|
verify final length/digest
|
|
atomically persist object QUARANTINED + scan outbox
|
|
persist QUARANTINE_RECORDED -> QUARANTINED
|
|
return stable quarantined receipt
|
|
|
|
abort
|
|
reauthorize
|
|
idempotency begin
|
|
CAS OPEN -> ABORT_REQUESTED
|
|
provider abort/cleanup
|
|
persist ABORTED
|
|
```
|
|
|
|
### 5.2 create
|
|
|
|
server가 결정하고 snapshot해야 하는 값:
|
|
|
|
- object/session opaque reference
|
|
- max object bytes
|
|
- min/max part bytes와 max part count
|
|
- max client concurrency
|
|
- required checksum algorithm
|
|
- exact session expiry
|
|
- scan profile
|
|
- direct/proxy transfer mode
|
|
|
|
client가 bucket/key, provider upload ID, concurrency ceiling, checksum algorithm이나
|
|
expiry를 선택하지 않는다.
|
|
|
|
### 5.3 part
|
|
|
|
- client가 보낸 `requestBindingSha256`, `uploadBindingSha256`와 part digest를
|
|
authorization proof로 취급하지 않는다. `sessionId`로 server-owned session을
|
|
조회하고 subject/purpose/state/expiry를 재검증한 다음 canonical binding과 part
|
|
plan을 직접 재계산한다.
|
|
- `partNumber`, exact offset, expected length를 session plan과 대조한다.
|
|
- 같은 idempotency key와 같은 fingerprint 재전송은 같은 receipt를 반환한다.
|
|
- 같은 part number의 다른 length/digest는 `CONFLICT`다.
|
|
- concurrent upload의 aggregate in-flight bytes와 provider connections에 hard cap을
|
|
둔다.
|
|
- 마지막 part를 제외한 최소 part 크기는 provider와 platform ceiling을 모두
|
|
만족해야 한다.
|
|
- part 성공 response가 유실되면 provider inspect와 ledger reconcile로 결정한다.
|
|
|
|
### 5.4 complete
|
|
|
|
- client가 제출한 receipt 목록을 그대로 provider에 전달하지 않는다.
|
|
- server ledger의 exact verified part set과 ordered fingerprint를 다시 계산한다.
|
|
- part count, offset 연속성, byte sum과 중복/누락을 검증한다.
|
|
- provider complete의 ETag를 whole-object SHA-256으로 해석하지 않는다.
|
|
- provider complete 직후 exact provider locator/version을
|
|
`PROVIDER_COMMITTED`로 먼저 기록한다. 이 checkpoint 전후 crash와 응답 유실은
|
|
provider inspect + journal reconcile로 결정한다.
|
|
- object metadata와 scan outbox가 durable해진 뒤
|
|
`QUARANTINE_RECORDED`를 기록한다.
|
|
- complete 성공은 `QUARANTINED` object가 durable하다는 의미이며 clean/available을
|
|
의미하지 않는다.
|
|
|
|
### 5.5 abort, expiry와 orphan cleanup
|
|
|
|
abort와 expiry job은 같은 state CAS를 사용한다.
|
|
|
|
| 현재 상태 | complete | abort/expiry |
|
|
| --- | --- | --- |
|
|
| `OPEN` | `COMPLETE_REQUESTED` claim 가능 | `ABORT_REQUESTED` claim 가능 |
|
|
| `COMPLETE_REQUESTED` | resume/reconcile | object 삭제 금지, conflict |
|
|
| `PROVIDER_COMMITTED` | verify/resume | object 삭제 금지, conflict |
|
|
| `QUARANTINE_RECORDED` | outbox/session resume | 일반 abort 금지 |
|
|
| `QUARANTINED` | stable replay | 일반 abort 금지 |
|
|
| `ABORT_REQUESTED/ABORTED/EXPIRED` | conflict/expired | stable replay |
|
|
|
|
orphan janitor는 다음을 bounded page로 처리한다.
|
|
|
|
- registry OPEN이지만 session TTL이 지난 행
|
|
- registry에 provider multipart ref가 있으나 terminal state가 아닌 행
|
|
- provider staging은 있으나 registry binding이 없는 owned orphan
|
|
- provider complete 가능성이 있으나 response가 유실된 `COMPLETE_REQUESTED`
|
|
- provider object는 durable하지만 registry/outbox가 미완료인
|
|
`PROVIDER_COMMITTED`/`QUARANTINE_RECORDED`
|
|
- `DELETING`에서 provider delete response가 유실된 object
|
|
|
|
전체 bucket list/delete를 자동 실행하지 않는다. owned prefix/tag와 registry
|
|
binding이 동시에 확인된 대상만 정리한다.
|
|
|
|
## 6. Byte length와 checksum
|
|
|
|
### 6.1 authoritative source
|
|
|
|
| 값 | 신뢰 수준 |
|
|
| --- | --- |
|
|
| client `Content-Length` | preflight hint |
|
|
| client checksum | expected value, 단독 authority 아님 |
|
|
| provider ETag | opaque provider version |
|
|
| provider checksum field | contract test를 통과한 알고리즘/representation에서만 사용 |
|
|
| server streaming counter | proxy path의 authoritative length |
|
|
| server digest accumulator | proxy path의 authoritative digest |
|
|
| server post-complete readback | direct path의 authoritative fallback |
|
|
|
|
### 6.2 알고리즘
|
|
|
|
- 기본 whole-object algorithm은 SHA-256으로 제한한다.
|
|
- algorithm confusion을 막기 위해 digest에 algorithm tag를 항상 포함한다.
|
|
- lowercase/uppercase나 base64/hex encoding을 port 하나로 canonicalize한다.
|
|
- multipart part digest와 whole-object digest를 구분한다.
|
|
- OPFS의 `SHA-256-TREE-V1`, multipart ETag와 whole-object SHA-256은 서로 다른
|
|
digest다.
|
|
- digest가 맞아도 MIME/content safety가 증명되는 것은 아니다.
|
|
|
|
### 6.3 mismatch
|
|
|
|
다음 경우 provider commit 또는 availability promotion을 금지한다.
|
|
|
|
- 실제 byte length가 expected length와 다름
|
|
- object가 composition hard cap을 초과
|
|
- part byte sum이 whole-object length와 다름
|
|
- expected digest와 actual digest가 다름
|
|
- provider metadata와 server ledger가 다름
|
|
- digest algorithm 또는 encoding이 policy와 다름
|
|
|
|
이미 direct upload가 provider에 commit된 뒤 mismatch가 발견되면 object를
|
|
`REJECTED` 또는 cleanup-pending quarantine으로 유지하고 일반 read capability를
|
|
발급하지 않는다.
|
|
|
|
## 7. Short-lived signed capability
|
|
|
|
### 7.1 capability binding
|
|
|
|
capability 또는 그 server-side receipt는 최소 다음을 binding한다.
|
|
|
|
- issuer와 audience
|
|
- subject/session scope
|
|
- operation: upload part, object download 또는 authorized range
|
|
- `ObjectRef + generation` 또는 `UploadSessionRef + partNumber`
|
|
- exact HTTP method
|
|
- exact provider locator
|
|
- exact length 또는 server-enforced maximum
|
|
- required checksum와 signed headers
|
|
- upload 성공의 exact status, receipt response header와 expected response bytes
|
|
- media type과 safe extension
|
|
- issued-at/not-before/expiry
|
|
- capability policy version
|
|
- random receipt/nonce
|
|
|
|
값 하나라도 caller request, object registry와 다르면 발급 또는 handoff를
|
|
fail-closed한다.
|
|
|
|
### 7.2 lifetime
|
|
|
|
- session TTL과 per-request capability TTL을 분리한다.
|
|
- capability TTL은 composition이 정하고 implementation hard ceiling보다 낮출 수만
|
|
있다.
|
|
- 대용량 transfer 시간과 재발급 UX를 측정해 값을 선택한다.
|
|
- 만료 capability를 연장하지 않고 authorization 후 새 capability를 발급한다.
|
|
- proxy 경로는 stream admission 시 expiry를 검증하고, admission 뒤에는 별도의
|
|
bounded operation deadline과 maximum transfer duration을 적용한다. expiry가
|
|
지났다는 이유만으로 이미 허용된 stream을 임의의 시점에 자를지는 정책으로
|
|
명시하며 기본값은 새 요청/재시도만 거절하는 것이다.
|
|
- direct provider 경로는 provider가 “시작 시 유효”와 “전송 내내 유효” 중 어떤
|
|
의미를 실제로 강제하는지 contract test로 고정한다. 중간 만료의 강한 회수가
|
|
필요하면 provider URL이 아니라 proxy/relay 경로를 사용한다.
|
|
- signing key rotation 시 current/previous verification window와 강제 폐기 절차를
|
|
정의한다.
|
|
|
|
문서 출발점으로는 per-request capability를 수분 단위로 유지하되, 실제 값은
|
|
provider와 최대 part/object 크기의 production-like transfer evidence로 승인한다.
|
|
장기 URL을 session 전체와 동일하게 발급하지 않는다.
|
|
|
|
### 7.3 single-use의 한계
|
|
|
|
object store signed URL은 일반적으로 URL 자체만으로 single-use를 보장하지 않는다.
|
|
정확한 single-use가 요구되면 다음 중 하나를 선택한다.
|
|
|
|
- BFF proxy가 nonce를 원자 consume한 뒤 stream
|
|
- control plane receipt를 원자 consume하고 아주 짧은 provider capability 발급
|
|
- provider가 지원하는 조건부 write/version 정책과 server ledger를 결합
|
|
|
|
single-use를 구현하지 않았으면 문서나 API 이름으로 주장하지 않는다.
|
|
|
|
### 7.4 URL과 logging
|
|
|
|
- HTTPS 외 protocol은 local test 외 금지
|
|
- allowlisted provider/origin만 허용
|
|
- redirect는 기본 금지
|
|
- query의 signature/token을 log, analytics, trace attribute에 기록하지 않음
|
|
- `Referer`와 browser history 노출을 고려한 delivery 정책
|
|
- response header와 CORS expose 목록을 explicit하게 고정
|
|
- signed URL을 database의 장기 object locator로 저장하지 않음
|
|
|
|
## 8. Idempotency store
|
|
|
|
### 8.1 scope와 fingerprint
|
|
|
|
idempotency는 다음 연산에 기본 적용한다.
|
|
|
|
- upload session create
|
|
- part registration/stream upload
|
|
- multipart complete
|
|
- abort
|
|
- scan submission
|
|
- availability promotion
|
|
- delete
|
|
|
|
`requestFingerprint`는 operation별 canonical request의 SHA-256이다. 원문 payload,
|
|
filename, signed URL, token과 PII를 fingerprint input/record에 넣지 않는다.
|
|
업무 payload가 필요한 미래 use case는 feature-owned canonicalization을 추가한다.
|
|
fingerprint는 registered canonicalizer만 만들며 operation/version, exact
|
|
object/session generation, length, digest와 relevant precondition을 포함한다.
|
|
deadline, trace/request ID 같은 volatile 값은 제외한다.
|
|
|
|
### 8.2 concurrency
|
|
|
|
`begin`은 하나의 atomic operation이어야 한다.
|
|
|
|
```text
|
|
first caller -> ACQUIRED + lease/fence
|
|
same fingerprint -> IN_PROGRESS 또는 completed REPLAY
|
|
different fingerprint -> CONFLICT
|
|
```
|
|
|
|
- process-local lock만으로 correctness를 주장하지 않는다.
|
|
- lease owner가 죽으면 expiry 이후 같은 fingerprint만 reclaim할 수 있다.
|
|
- reclaim은 fence를 증가시키며 old owner의 complete를 거절한다.
|
|
- complete는 expected lease/fence와 request fingerprint를 다시 확인한다.
|
|
- durable side effect와 idempotency complete 사이 crash는 reconciliation 가능한
|
|
operation receipt로 해결한다.
|
|
- “exactly once”를 주장하지 않고 at-least-once delivery + idempotent effect로
|
|
설계한다.
|
|
- exactly-once가 실제로 필요하면 업무 mutation과 receipt를 같은 transaction에
|
|
넣거나 transactional inbox/outbox로 묶어야 한다. 별도 Redis `SETNX` 뒤
|
|
database/object mutation을 실행하는 구조는 duplicate suppression일 뿐이다.
|
|
- idempotency store가 unavailable한 keyed mutation은 fail-closed한다.
|
|
|
|
### 8.3 receipt와 retention
|
|
|
|
completion receipt에는 다음처럼 재생에 필요한 최소 정보만 둔다.
|
|
|
|
```text
|
|
CompletionReceipt
|
|
operation
|
|
stableResultCode
|
|
objectRef/sessionRef
|
|
generation/revision
|
|
safeResponseFingerprint
|
|
completedAt
|
|
```
|
|
|
|
- replay response는 최초 성공과 의미가 같아야 한다.
|
|
- transient provider error 전체를 영구 replay하지 않는다.
|
|
- retention은 최대 client retry/session window를 포함하되 무제한이 아니다.
|
|
- row/byte hard cap, expiry index와 bounded prune를 필수로 둔다.
|
|
- 아직 replay 가능한 receipt를 storage pressure만으로 삭제하지 않는다.
|
|
|
|
## 9. Quarantine과 scan
|
|
|
|
### 9.1 격리
|
|
|
|
- staging/quarantine object는 public ACL과 CDN 배포를 금지한다.
|
|
- 일반 download capability issuer가 `QUARANTINED`를 읽지 못하게 한다.
|
|
- scanner principal은 exact quarantine read와 verdict write만 가진다.
|
|
- clean destination writer와 destructive delete 권한을 최소화한다.
|
|
- 원본 filename으로 provider path를 만들지 않는다.
|
|
|
|
### 9.2 검사 pipeline
|
|
|
|
```text
|
|
QUARANTINED object + exact generation/digest
|
|
-> durable scan outbox
|
|
-> scanner/validator
|
|
malware
|
|
MIME sniff + allowlisted parser
|
|
archive traversal/symlink/nesting/expanded-size ratio
|
|
image dimension/pixel budget
|
|
PDF/active content
|
|
optional CDR
|
|
-> bound verdict
|
|
-> CAS promotion or rejection
|
|
```
|
|
|
|
scanner가 clean이라고 반환해도 submit 당시와 object generation/digest가 다르면
|
|
stale verdict로 폐기한다.
|
|
|
|
### 9.3 promotion
|
|
|
|
promotion은 provider별로 다음 중 하나다.
|
|
|
|
- immutable quarantine object를 그대로 유지하고 registry access state만
|
|
`AVAILABLE`로 CAS
|
|
- clean bucket/key로 server-side copy 후 length/digest/version을 재검증하고 binding
|
|
CAS
|
|
|
|
copy+delete를 atomic rename으로 주장하지 않는다. crash 단계마다 source/destination
|
|
binding을 재검증하는 saga와 cleanup phase가 필요하다. availability가 commit되기
|
|
전 source를 삭제하지 않는다.
|
|
|
|
### 9.4 scanner 장애
|
|
|
|
- required scanner unavailable은 clean으로 degrade하지 않는다.
|
|
- retryable failure는 bounded backoff와 retry count/age ceiling을 가진다.
|
|
- terminal failure는 quarantine을 유지하고 operator/user recovery를 요구한다.
|
|
- scan process/container에는 wall-clock, CPU, memory, file count, recursion depth,
|
|
expanded bytes와 output bytes hard limit를 둔다.
|
|
- scan definition/profile freshness가 composition의 maximum age를 넘으면 이전
|
|
`CLEAN` verdict로 promotion하지 않고 새 job/generation fence로 재검사한다.
|
|
- scan backlog가 SLO를 초과하면 신규 session 발급을 제한하거나 kill switch를
|
|
사용한다.
|
|
- scan timeout 이후에도 late verdict가 state를 바꾸지 못하게 generation/job
|
|
fence를 검증한다.
|
|
|
|
## 10. Deadline, retry와 resource cleanup
|
|
|
|
### 10.1 deadline budget
|
|
|
|
각 public capability invocation은 절대 deadline 또는 남은 budget을 받는다.
|
|
|
|
```text
|
|
request deadline
|
|
- authorization
|
|
- registry transaction
|
|
- provider call
|
|
- stream transfer
|
|
- verification
|
|
- response margin
|
|
```
|
|
|
|
하위 adapter가 각자 전체 timeout을 새로 시작해 총 시간이 무한히 늘어나서는 안
|
|
된다. 남은 budget이 최소 provider timeout보다 작으면 side effect 전에
|
|
`DEADLINE_EXCEEDED`로 종료한다. caller cancellation은 `CANCELLED`이고 server
|
|
deadline 소진은 `DEADLINE_EXCEEDED`다. 둘은 metric과 retry 판단에서도 합치지
|
|
않는다.
|
|
|
|
### 10.2 retry matrix
|
|
|
|
공통 retry directive는 세 종류뿐이다.
|
|
|
|
```text
|
|
NEVER
|
|
SAFE { afterMs? }
|
|
SAME_IDEMPOTENCY_KEY { afterMs? }
|
|
```
|
|
|
|
| operation | directive | 조건 |
|
|
| --- | --- | --- |
|
|
| immutable metadata/read open | `SAFE` | 같은 exact version과 남은 deadline |
|
|
| range read | `SAFE` | 같은 exact version/range와 아직 전달되지 않은 경계 |
|
|
| session create | `SAME_IDEMPOTENCY_KEY` | 같은 canonical fingerprint |
|
|
| put part | `SAME_IDEMPOTENCY_KEY` | 같은 key/part/length/digest |
|
|
| multipart complete | `SAME_IDEMPOTENCY_KEY` | stable receipt + inspect/reconcile |
|
|
| abort/delete | `SAME_IDEMPOTENCY_KEY` | exact state/version + inspect/reconcile |
|
|
| signed capability issue | `NEVER` | authorization/expiry 확인 후 새 operation으로 발급 |
|
|
| scan submit | `SAME_IDEMPOTENCY_KEY` | stable job ref + generation/digest binding |
|
|
|
|
retry는 exponential backoff, full jitter, max attempts와 전체 deadline을 가진다.
|
|
rate limit/provider overload에서는 `Retry-After` 또는 provider-safe backoff hint를
|
|
상한 안에서 반영한다.
|
|
|
|
- retry owner는 ingress/application orchestration, adapter wrapper 또는 provider
|
|
SDK 중 정확히 한 계층이다. service mesh와 SDK의 숨은 retry는 끄거나 동일한
|
|
total-attempt budget에 포함해 retry amplification을 막는다.
|
|
- mutation의 적용 여부가 `UNKNOWN`이면 새 idempotency key로 재시도하지 않는다.
|
|
같은 key로 receipt를 조회하거나 provider/registry reconciliation을 먼저 한다.
|
|
- 이미 response byte를 client에 전달한 stream read는 처음부터 자동 재시작하지
|
|
않는다. resumable protocol이 명시된 경우에만 검증된 다음 range에서 재개한다.
|
|
|
|
### 10.3 cleanup
|
|
|
|
모든 adapter는 다음 자원을 명시적으로 종료한다.
|
|
|
|
- input/output stream과 provider response body
|
|
- multipart writer/upload handle
|
|
- temporary file와 bounded buffer
|
|
- digest/scanner process stream
|
|
- database cursor/transaction/connection lease
|
|
- scheduled timeout/retry task
|
|
- lock/semaphore permit
|
|
- tracing span
|
|
|
|
resource owner는 다음과 같은 idempotent lease 계약을 구현한다.
|
|
|
|
```text
|
|
ResourceLease
|
|
state: OPEN | CLOSING | CLOSED
|
|
transferOwnership(newOwner)
|
|
close(reason, cleanupDeadline) -> CleanupReport
|
|
```
|
|
|
|
- ownership transfer는 명시적이며 transfer 뒤 이전 owner는 close하지 않는다.
|
|
- `close`는 여러 번 호출돼도 안전하고 첫 close reason과 cleanup 결과를 보존한다.
|
|
- request deadline과 별도로 짧고 bounded한 cleanup budget을 예약한다. 이 budget은
|
|
client response deadline을 연장하지 않으며, 즉시 끝낼 수 없는 provider cleanup은
|
|
durable reconciliation record로 넘긴다.
|
|
- finalizer/garbage collector는 correctness 경로가 아니라 마지막 누수 경보다.
|
|
|
|
success, failure, cancellation, timeout, downstream disconnect와 exception 모든 경로를
|
|
contract test한다. cleanup 자체의 실패가 최초 failure를 덮지 않으며 safe secondary
|
|
observation만 남긴다.
|
|
|
|
### 10.4 shutdown
|
|
|
|
- 신규 session/capability 발급 중지
|
|
- in-flight admission 중지
|
|
- bounded grace 동안 active stream drain
|
|
- 남은 stream cancel
|
|
- leased idempotency operation을 reclaim 가능 상태로 둠
|
|
- multipart/scanner reconciliation checkpoint 저장
|
|
- provider clients/executors close
|
|
|
|
무기한 graceful shutdown을 허용하지 않는다.
|
|
|
|
## 11. 공통 failure model
|
|
|
|
### 11.1 closed failure
|
|
|
|
```text
|
|
FileCapabilityFailure
|
|
code
|
|
operation
|
|
retry: NEVER
|
|
| SAFE { afterMs? }
|
|
| SAME_IDEMPOTENCY_KEY { afterMs? }
|
|
effect: NOT_APPLIED | APPLIED | UNKNOWN
|
|
recovery
|
|
safeReasonCode?
|
|
correlationId
|
|
```
|
|
|
|
권장 closed code:
|
|
|
|
```text
|
|
INVALID_INPUT
|
|
UNAUTHENTICATED
|
|
FORBIDDEN
|
|
NOT_FOUND
|
|
CONFLICT
|
|
POLICY_REJECTED
|
|
LIMIT_EXCEEDED
|
|
PAYLOAD_TOO_LARGE
|
|
UNSUPPORTED_MEDIA_TYPE
|
|
INTEGRITY_FAILED
|
|
EXPIRED_RESOURCE
|
|
QUARANTINED
|
|
REJECTED
|
|
RATE_LIMITED
|
|
IDEMPOTENCY_KEY_REUSED
|
|
OPERATION_IN_PROGRESS
|
|
CANCELLED
|
|
DEADLINE_EXCEEDED
|
|
DEPENDENCY_UNAVAILABLE
|
|
CONTRACT_MISMATCH
|
|
RECOVERY_REQUIRED
|
|
CORRUPT_DATA
|
|
INTERNAL
|
|
```
|
|
|
|
권장 operation:
|
|
|
|
```text
|
|
OBJECT_INSPECT
|
|
OBJECT_READ
|
|
OBJECT_WRITE
|
|
OBJECT_DELETE
|
|
MULTIPART_CREATE
|
|
MULTIPART_PART
|
|
MULTIPART_COMPLETE
|
|
MULTIPART_ABORT
|
|
CAPABILITY_ISSUE
|
|
IDEMPOTENCY
|
|
SCAN_SUBMIT
|
|
SCAN_INSPECT
|
|
OBJECT_PROMOTE
|
|
RECONCILE
|
|
```
|
|
|
|
`recovery`는 `RETRY`, `REAUTHORIZE`, `RESTART_SESSION`, `REOPEN`,
|
|
`READ_ONLY`, `SUPPORT`, `NONE` 같은 allowlist다.
|
|
|
|
- `NOT_APPLIED`는 side effect가 시작되지 않았음이 증명된 경우만 사용한다.
|
|
- `APPLIED`는 durable receipt로 effect를 증명할 수 있는 경우다.
|
|
- response loss, provider timeout 또는 process crash로 확정할 수 없으면
|
|
`UNKNOWN`이며, caller에게 성공이나 안전한 신규 요청을 암시하지 않는다.
|
|
- `SAFE`는 read 또는 side effect 전 실패에만 사용한다.
|
|
`SAME_IDEMPOTENCY_KEY`는 key/fingerprint/receipt 계약이 갖춰진 mutation에만
|
|
사용한다. 단순 `retryable: true`는 허용하지 않는다.
|
|
|
|
### 11.2 mapping
|
|
|
|
- provider status/exception class를 application failure로 한 곳에서 mapping한다.
|
|
- raw SDK exception, request ID, bucket/key, endpoint와 provider body를 port 밖으로
|
|
throw하지 않는다.
|
|
- unknown provider failure는 `INTERNAL` 또는 `DEPENDENCY_UNAVAILABLE` 중 사전에
|
|
정한 fail-closed mapping을 사용하고 effect는 보수적으로 `UNKNOWN`으로 둔다.
|
|
- authorization과 object existence를 노출하면 안 되는 API는 `FORBIDDEN`과
|
|
`NOT_FOUND` 외부 표현을 동일하게 만들 수 있다.
|
|
- HTTP status는 inbound adapter가 failure code에서 mapping하며 domain/application이
|
|
HTTP status를 반환하지 않는다.
|
|
- retry directive와 effect certainty는 provider message 문자열이 아니라 typed
|
|
mapping, operation semantics, idempotency receipt와 reconciliation evidence로
|
|
결정한다.
|
|
|
|
### 11.3 safe observability
|
|
|
|
다음을 기록하지 않는다.
|
|
|
|
- object/session/idempotency reference 원문
|
|
- filename과 user/account ID
|
|
- provider locator, bucket/key/version
|
|
- signed URL/query/header
|
|
- checksum 원문
|
|
- scanner raw result
|
|
- request/response body와 exception message/stack
|
|
|
|
허용 가능한 metric dimension:
|
|
|
|
- provider profile ID
|
|
- operation
|
|
- stable failure code
|
|
- byte/part/latency bucket
|
|
- transfer mode proxy/direct
|
|
- state transition
|
|
- scan profile와 safe verdict class
|
|
- retry count bucket
|
|
- retry directive와 effect certainty
|
|
|
|
high-cardinality identifier를 metric label로 사용하지 않는다.
|
|
|
|
## 12. Provider contract test
|
|
|
|
### 12.1 한 suite, 여러 adapter
|
|
|
|
각 port는 provider-neutral contract suite factory를 제공한다.
|
|
|
|
```text
|
|
objectByteStoreContract(createProviderFixture)
|
|
multipartStagingContract(createProviderFixture)
|
|
signedCapabilityContract(createProviderFixture)
|
|
rangeDownloadContract(createProviderFixture)
|
|
idempotencyStoreContract(createProviderFixture)
|
|
contentScannerContract(createProviderFixture)
|
|
objectRegistryContract(createProviderFixture)
|
|
cleanPromotionContract(createProviderFixture)
|
|
imageDescriptorContract(createProviderFixture)
|
|
```
|
|
|
|
동일 suite를 in-memory fake, emulator와 실제 provider adapter에 실행하되 결과
|
|
등급을 섞지 않는다.
|
|
|
|
- in-memory fake: orchestration 개발과 빠른 invariant 회귀
|
|
- emulator/container: SDK wiring과 local integration
|
|
- actual provider conformance: 선택한 provider product/API/version의 격리된
|
|
production-like account/region에서 실행한 promotion evidence
|
|
|
|
fake나 emulator 통과는 actual provider conformance를 대체하지 않는다. actual
|
|
provider에서 destructive/fault test를 실행할 수 없다면 누락 항목, 보완 통제,
|
|
승인 owner와 expiry가 있는 명시적 waiver가 필요하며 자동으로 “동등”하다고
|
|
간주하지 않는다.
|
|
|
|
### 12.2 object store matrix
|
|
|
|
- zero-byte와 boundary-size object
|
|
- exact bytes/media/version round trip
|
|
- range beginning/middle/end/invalid/empty
|
|
- declared length 미달·초과
|
|
- checksum mismatch
|
|
- provider metadata/ETag와 digest 구분
|
|
- mid-stream read/write failure
|
|
- cancellation/downstream disconnect
|
|
- deadline before call/during stream/after provider commit
|
|
- concurrent exact-version write/delete
|
|
- response loss 뒤 inspect/reconcile
|
|
- resource close exactly once
|
|
- object locator escaping/path traversal 거절
|
|
|
|
### 12.3 multipart matrix
|
|
|
|
- create/put/inspect/complete 정상 흐름
|
|
- minimum/maximum part size와 count
|
|
- duplicate part same fingerprint replay
|
|
- duplicate part different fingerprint conflict
|
|
- missing/duplicate/out-of-order receipt
|
|
- complete/abort/expiry races
|
|
- complete response loss와 recovery
|
|
- part success response loss와 inspect
|
|
- orphan multipart cleanup
|
|
- provider complete ETag가 whole SHA-256이 아님을 검증
|
|
- direct signed PUT의 required length/checksum header 강제
|
|
|
|
### 12.4 idempotency matrix
|
|
|
|
- 동시 100개 begin에서 정확히 하나만 `STARTED`
|
|
- 같은 fingerprint의 `IN_PROGRESS`와 stable replay
|
|
- 다른 fingerprint conflict
|
|
- lease expiry/reclaim
|
|
- crash between durable effect and complete
|
|
- retryable release
|
|
- receipt expiry와 bounded prune
|
|
- count/byte cap
|
|
- tenant/scope/operation key isolation
|
|
- clock skew/invalid expiry fail-closed
|
|
|
|
### 12.5 capability matrix
|
|
|
|
- wrong method/object/part/range/header 거절
|
|
- expiry/not-before
|
|
- TTL hard ceiling
|
|
- modified query/path/host 거절
|
|
- wrong audience/subject/session
|
|
- key rotation current/previous/expired
|
|
- redirect/CORS/header exposure policy
|
|
- token/query redaction
|
|
- direct provider가 signed constraint를 실제로 강제하는지 확인
|
|
|
|
### 12.6 scanner matrix
|
|
|
|
- clean/rejected/timeout/unavailable
|
|
- corrupt/truncated input
|
|
- stale generation/digest verdict 폐기
|
|
- duplicate submission replay
|
|
- archive path traversal/nesting/expanded-size limit
|
|
- scanner crash와 process/resource cleanup
|
|
- late verdict after cancellation/timeout
|
|
- scan backlog admission control
|
|
- clean verdict 전 download/promotion 불가
|
|
|
|
### 12.7 Range download matrix
|
|
|
|
- exact `RANGE_RESUMABLE_DOWNLOAD_V1`과 unknown/missing version 거절
|
|
- immutable generation/strong validator/total length/media/full digest binding
|
|
- beginning/middle/final segment의 exact `206 Content-Range`
|
|
- `If-Range` match와 mismatch의 `206`/full `200`
|
|
- 별도 generation precondition의 `412`
|
|
- before-start/at-end/beyond-end `416`과 authoritative total
|
|
- capability expiry/reissue의 same-binding 유지
|
|
- generation replacement 뒤 old partial 이어 쓰기 거절
|
|
- encoded/transform response와 redirect 거절
|
|
- direct provider와 BFF relay가 같은 constraint를 실제로 강제
|
|
|
|
### 12.8 Image descriptor/CDN matrix
|
|
|
|
- exact `IMAGE_CDN_DESCRIPTOR_V1`과 unknown/missing field/version 거절
|
|
- authorization/existence hiding/quarantine 상태
|
|
- immutable asset revision과 preset binding exact recomputation
|
|
- arbitrary source/transform/query 거절
|
|
- old/new key overlap, signer cutover와 old key drain
|
|
- expiry/reissue/emergency revoke
|
|
- public immutable/private no-store cache header
|
|
- cross-origin CORS/CSP와 ambient credential 비의존
|
|
- malformed/animated/oversize rendition 거절
|
|
- asset/preset mismatch와 provider response loss
|
|
|
|
### 12.9 fault injection과 evidence
|
|
|
|
- provider latency, throttle, connection reset, partial response
|
|
- database deadlock/serialization retry
|
|
- registry commit 전후 process termination
|
|
- provider commit 뒤 response loss
|
|
- queue duplicate/out-of-order
|
|
- scanner unavailable와 slow verdict
|
|
- clock movement은 wall clock/monotonic clock 책임에 맞춰 주입
|
|
|
|
CI가 만드는 immutable conformance evidence에는 최소 다음을 포함한다.
|
|
|
|
```text
|
|
ProviderConformanceEvidence
|
|
adapterName/version/artifactDigest
|
|
providerProduct/apiVersion/runtimeVersion
|
|
environmentClass/region
|
|
capabilityProfileDigest
|
|
contractSuiteName/version/artifactDigest
|
|
startedAt/completedAt
|
|
passed/failed/skipped counts
|
|
faultSuiteResult
|
|
waiverIds[]
|
|
evidenceExpiresAt
|
|
ciRunIdentity/signature
|
|
```
|
|
|
|
실제 provider evidence와 fake/emulator result는 별도 artifact로 보관한다. 필수
|
|
test skip, expired evidence, adapter/provider/config 변경 또는 contract suite
|
|
version 불일치는 promotion을 막는다. 최소 정기 schedule과 provider/SDK upgrade,
|
|
capability/config 변경 시 actual provider conformance를 다시 실행한다.
|
|
|
|
## 13. 선택적 composition
|
|
|
|
### 13.1 서로 독립적인 상태 축
|
|
|
|
source catalog 상태, runtime 설치, 현재 health와 traffic admission을 하나의
|
|
`ENABLED` boolean으로 합치지 않는다.
|
|
|
|
```text
|
|
InstallationState
|
|
NOT_SELECTED | INSTALLED | REMOVING
|
|
|
|
RuntimeState
|
|
UNKNOWN | AVAILABLE | DEGRADED | UNAVAILABLE | INCOMPATIBLE
|
|
|
|
TrafficAdmission
|
|
DISABLED | SHADOW | CANARY | ENABLED
|
|
```
|
|
|
|
이 문서의 목표인 `AVAILABLE_NOT_COMPOSED`는 source catalog에 port, adapter,
|
|
contract suite와 문서가 있지만 runtime은 `NOT_SELECTED`, traffic은 `DISABLED`인
|
|
상태다. provider client/route/job/migration을 만들지 않으므로 `RuntimeState`도
|
|
평가하지 않는다.
|
|
|
|
`INSTALLED`는 dependency와 immutable policy가 composition되었다는 뜻일 뿐,
|
|
provider가 현재 `AVAILABLE`하거나 traffic이 `ENABLED`라는 뜻이 아니다.
|
|
`INCOMPATIBLE`은 contract/API/capability 불일치이며 fail-closed한다.
|
|
|
|
### 13.2 composition root
|
|
|
|
선택 시 composition root만 concrete provider를 안다.
|
|
|
|
```text
|
|
ServerFileCapabilityComposition
|
|
objectByteStore
|
|
multipartStaging
|
|
objectRegistry
|
|
idempotencyStore
|
|
signedTransferCapability
|
|
contentScanner
|
|
cleanPromotion
|
|
clock
|
|
secureRandom
|
|
deadlinePolicy
|
|
retryPolicy
|
|
transferPolicyRegistry
|
|
safeObserver
|
|
```
|
|
|
|
composition 시 다음을 검증하고 immutable snapshot으로 고정한다.
|
|
|
|
- provider capability와 요구 기능 일치
|
|
- hard byte/part/TTL/retry/deadline ceiling
|
|
- registry와 provider profile binding
|
|
- signing key/audience
|
|
- scanner profile
|
|
- cleanup owner와 schedule
|
|
- idempotency retention/capacity
|
|
- metric/trace redaction policy
|
|
|
|
선택된 필수 dependency가 없거나 capability가 부족하면 startup 또는 feature
|
|
installation을 fail-closed한다. unavailable provider를 fake로 바꾸어 production을
|
|
계속하지 않는다.
|
|
|
|
### 13.3 capability probe와 readiness
|
|
|
|
probe는 다음 판단을 분리해 기록한다.
|
|
|
|
| probe | 답하는 질문 |
|
|
| --- | --- |
|
|
| static config | 필수 값, hard ceiling과 profile schema가 유효한가 |
|
|
| connectivity | DNS/TLS/network endpoint에 bounded하게 도달하는가 |
|
|
| credential/entitlement | 최소 권한 principal이 필요한 operation을 허용받는가 |
|
|
| compatibility | provider API/version/capability가 승인 contract와 일치하는가 |
|
|
| operational health | 현재 latency/error/throttle가 admission SLO 안인가 |
|
|
|
|
- probe는 짧은 timeout, cancellation과 bounded retry를 사용하고 동시 요청은
|
|
single-flight로 합친다.
|
|
- cached result에는 TTL와 jitter를 두며
|
|
`state, observedAt, validUntil, adapterVersion, contractVersion, safeReasonCode`
|
|
외 token, endpoint body, locator와 credential을 포함하지 않는다.
|
|
- probe를 매 product request의 authorization 또는 correctness check로 사용하지
|
|
않는다. request는 여전히 exact registry state, generation과 capability를
|
|
검증한다.
|
|
- last-known-good는 짧은 grace의 운영 신호일 뿐 expiry 뒤 authority가 아니다.
|
|
stale result는 `UNKNOWN`으로 낮춘다.
|
|
- read/write probe가 side effect를 만들면 별도의 owned canary namespace와 exact
|
|
cleanup receipt를 사용한다. 일반 customer object를 probe하지 않는다.
|
|
- `DISABLED/SHADOW`인 optional capability 장애는 base server readiness를 깨지
|
|
않는다. `ENABLED`이고 해당 제품 경로의 필수 dependency이면 feature readiness와
|
|
admission을 fail-closed한다. boot-blocking 여부는 composition policy로 명시한다.
|
|
|
|
`INSTALLED -> CANARY` promotion에는 유효한 actual-provider conformance evidence,
|
|
owner, policy snapshot, SLO/alert, runbook, cleanup drill과 rollback/kill switch가
|
|
필요하다.
|
|
|
|
### 13.4 미선택 상태
|
|
|
|
`AVAILABLE_NOT_COMPOSED`에서는 다음을 금지한다.
|
|
|
|
- public HTTP route/controller registration
|
|
- provider SDK client 초기화와 credential 요구
|
|
- database table/migration 자동 생성
|
|
- bucket/container 생성
|
|
- cleanup/scan worker와 scheduler 시작
|
|
- health/readiness 필수 dependency 등록
|
|
- product module이 concrete adapter를 직접 import
|
|
- provider endpoint로의 network/DNS 호출과 background capability probe
|
|
|
|
server template core는 file capability 디렉터리를 완전히 제거해도 build, base test,
|
|
application startup과 artifact 생성이 통과해야 한다.
|
|
|
|
### 13.5 rollout
|
|
|
|
1. 제품 use case와 owner 결정
|
|
2. 분류, size, retention, scan, fallback 정책 승인
|
|
3. provider와 deployment profile 선택
|
|
4. actual provider contract/fault test와 evidence 생성
|
|
5. `INSTALLED + UNKNOWN + DISABLED`로 composition
|
|
6. bounded operator probe 후 `SHADOW`
|
|
7. 내부 cohort proxy transfer를 `CANARY`
|
|
8. direct signed transfer가 필요하면 별도 canary
|
|
9. quarantine/scan backlog, reconciliation과 cleanup drill
|
|
10. SLO/alert/kill switch/rollback 확인 후 `ENABLED`로 점진 확대
|
|
|
|
rollout 중 evidence expiry, `INCOMPATIBLE`, integrity failure 또는 cleanup 불능은
|
|
자동 promotion을 중단한다. rollback은 code rollback뿐 아니라
|
|
`TrafficAdmission=DISABLED`, credential revoke와 worker drain 절차를 포함한다.
|
|
|
|
### 13.6 kill switch
|
|
|
|
- 신규 upload session 발급 off
|
|
- direct signed upload off → bounded proxy 또는 전체 off
|
|
- multipart off → 승인된 small simple upload
|
|
- 신규 download capability off
|
|
- scanner promotion off, quarantine 유지
|
|
- provider write off, read-only
|
|
- background reconciliation batch 축소/정지
|
|
|
|
kill switch가 기존 object를 자동 삭제하거나 quarantine을 available로 만들면 안 된다.
|
|
|
|
## 14. 제거 가능성
|
|
|
|
### 14.1 제거 순서
|
|
|
|
1. `InstallationState=REMOVING`, `TrafficAdmission=DISABLED`로 전환하고 신규
|
|
session/capability와 product write를 중지한다.
|
|
2. in-flight operation을 bounded drain/cancel한다.
|
|
3. `OPEN`, `COMPLETE_REQUESTED`, `PROVIDER_COMMITTED`,
|
|
`QUARANTINE_RECORDED`, `ABORT_REQUESTED` session을 reconcile한다.
|
|
4. quarantine/scan/reconciliation backlog와 orphan inventory를 0 또는 승인된
|
|
handoff 상태로 만든다.
|
|
5. `AVAILABLE` object의 새 provider/use case 이전과 read cutover를 완료한다.
|
|
6. route/controller, consumer와 product composition을 제거한다.
|
|
7. cleanup/scanner workers, queue subscription, webhook, scheduler와 provider
|
|
lifecycle rule을 중지·제거한다.
|
|
8. provider-owned IAM principal, signing key, credential와 secret을 revoke하고
|
|
config/SDK dependency를 제거한다.
|
|
9. alert/dashboard/SLO, runbook, on-call ownership과 비용 budget을 제거하거나 새
|
|
owner에게 명시적으로 이관한다.
|
|
10. owned registry/idempotency rows와 provider objects는 별도 승인된 data
|
|
migration으로 제거한다.
|
|
11. architecture, contract, startup, network와 dependency inventory gate를
|
|
재실행한다.
|
|
|
|
data 삭제를 code removal과 같은 단계에서 암묵적으로 실행하지 않는다.
|
|
일반 object 삭제도 logical tombstone/CAS와 exact provider generation purge를
|
|
분리하고 durable purge receipt를 남긴다. retention/legal hold가 있으면 physical
|
|
purge보다 우선하며 provider `NOT_FOUND`만으로 삭제 완료를 추정하지 않는다.
|
|
|
|
### 14.2 제거 gate
|
|
|
|
server 구현 저장소에는 격리 copy 또는 build profile에서 다음을 자동 검증하는
|
|
removal test를 둔다.
|
|
|
|
- file capability source와 provider dependency 제거
|
|
- file 전용 route/job/config/migration 제거
|
|
- 남은 source import 0개
|
|
- base type/compile/lint/unit/integration 통과
|
|
- application startup 통과
|
|
- dependency/SBOM에 provider SDK 부재
|
|
- generated API/schema에 file endpoint 부재
|
|
- 기본 health/readiness가 file provider를 요구하지 않음
|
|
- startup과 idle 기간 provider DNS/network 호출 0건
|
|
- secret/IAM/queue/webhook/scheduler/IaC inventory에 orphan 0건
|
|
- provider object와 database data는 삭제 완료 또는 새 owner에게 이관됐다는
|
|
별도 signed inventory 존재
|
|
|
|
## 15. Frontend와의 향후 계약
|
|
|
|
현재 frontend runtime과 연결할 때 control-plane API는 최소 다음 의미를 제공해야
|
|
한다. 실제 endpoint/DTO는 제품 feature가 소유한다.
|
|
|
|
whole-object presigned control envelope의 목표 protocol은
|
|
`PRESIGNED_TRANSFER_V1`, Image descriptor envelope은
|
|
`IMAGE_CDN_DESCRIPTOR_V1`, Range resume는
|
|
`RANGE_RESUMABLE_DOWNLOAD_V1`이다. 현재 frontend/server template에 이 세
|
|
control plane이 모두 구현됐다는 뜻은 아니다. BFF는 unknown/missing version을
|
|
fail-closed하고 N-1 client drain/rollback window를 명시해야 한다.
|
|
|
|
### 15.1 browser-managed download
|
|
|
|
server가 발급하는 capability는 frontend
|
|
`BrowserManagedDownloadCapability`와 다음 의미가 일치해야 한다.
|
|
|
|
```text
|
|
receipt
|
|
href
|
|
resourceId/ObjectRef
|
|
mediaType
|
|
safeExtension
|
|
maxBytes
|
|
expectedSha256?
|
|
expiresAt
|
|
```
|
|
|
|
endpoint는 capability가 주장한 max bytes, object generation, digest와 expiry를
|
|
실제 response에 enforce한다. frontend의 `BROWSER_HANDOFF`는 disk save 완료가
|
|
아니다.
|
|
|
|
### 15.2 authorized stream download
|
|
|
|
제품 연결 전 frontend의 `AUTHORIZED_STREAM_RESOURCE`는 단순 `resourceId`보다
|
|
좁은 server-issued capability를 받도록 확장해야 한다.
|
|
|
|
```text
|
|
AuthorizedStreamCapability
|
|
receipt
|
|
objectRef
|
|
generation
|
|
exactLength
|
|
expectedDigest
|
|
mediaType
|
|
expiresAt
|
|
```
|
|
|
|
stream opener는 이 capability를 검증해 exact provider/server response를
|
|
`FileByteSource`로 변환한다.
|
|
|
|
### 15.3 multipart upload
|
|
|
|
frontend feature가 필요한 최소 의미:
|
|
|
|
```text
|
|
protocol = PRESIGNED_MULTIPART_V1
|
|
create -> protocol, sessionRef, request binding, part constraints, expiry,
|
|
SHA-256-PARTS-V1 fingerprint
|
|
put/sign part -> protocol/session, request + upload binding, part number,
|
|
offset, exact length, digest, idempotency key
|
|
complete -> protocol/session + ordered verified receipts -> QUARANTINED
|
|
abort -> protocol/session -> stable terminal result
|
|
status -> protocol/session -> ACTIVE | QUARANTINED | ABORTED | EXPIRED | NOT_FOUND
|
|
```
|
|
|
|
browser control adapter는 `CREATE_SESSION`, `GET_STATUS`, `COMPLETE`, `ABORT`의
|
|
closed operation set을 composition-owned fixed HTTPS endpoint map으로 실행한다.
|
|
part/download capability 발급 adapter도 composition 시 fixed BFF endpoint를 한
|
|
번만 받는다. server route가 어떤 path를 선택하든 caller가 endpoint를 URL로
|
|
전달하거나 operation을 추가할 수 없는 closed contract를 유지한다.
|
|
|
|
canonical binding은 UTF-8 newline-separated field sequence의 SHA-256 lowercase
|
|
hex다.
|
|
|
|
```text
|
|
fingerprint digest fields:
|
|
SHA-256-PARTS-V1
|
|
fingerprint.byteLength
|
|
fingerprint.partSizeBytes
|
|
fingerprint.partCount
|
|
{partNumber}:{offset}:{byteLength}:{checksumSha256} for each ordered part
|
|
|
|
request binding fields:
|
|
RESUMABLE-UPLOAD-BINDING-V1
|
|
uploadKey, purpose, mediaType
|
|
fingerprint.algorithm, fingerprint.digestHex
|
|
fingerprint.byteLength, fingerprint.partSizeBytes, fingerprint.partCount
|
|
|
|
upload session binding fields:
|
|
RESUMABLE-UPLOAD-SESSION-BINDING-V1
|
|
requestBindingSha256, sessionId
|
|
fingerprint.algorithm, fingerprint.digestHex
|
|
fingerprint.byteLength, fingerprint.partSizeBytes, fingerprint.partCount
|
|
```
|
|
|
|
각 field/part line은 UTF-8 newline 하나로 연결하고 trailing newline을 붙이지
|
|
않는다. server는 complete 때 ledger의 ordered part set으로 fingerprint digest를
|
|
재계산한다.
|
|
|
|
BFF는 part capability 요청의 `sessionId`로 registry row와 immutable session
|
|
snapshot을 조회해 위 값을 다시 만든다. client-provided digest는 비교 입력이지
|
|
authorization, ownership, current state 또는 part eligibility를 증명하지 않는다.
|
|
`UPLOAD_PART` capability binding에도 exact
|
|
`protocol: PRESIGNED_MULTIPART_V1`을 포함하고 subject scope, expiry, protocol,
|
|
exact part plan과 idempotency를 별도로 검증한다.
|
|
|
|
PUT capability는 expected success status, receipt response header와
|
|
`expectedResponseByteLength`를 binding한다. object store/proxy는 exact
|
|
`Content-Length`를 보내야 한다. 단 204는 response bytes가 0이어야 하고 header
|
|
부재를 0으로 정규화한다. frontend는 hard cap과 transfer deadline 안에서 response
|
|
body를 EOF까지 drain한 뒤 receipt를 채택하므로 CORS는 custom receipt header를
|
|
명시적으로 expose해야 한다.
|
|
|
|
status의 HTTP 404/410은 각각 `NOT_FOUND`/`EXPIRED_RESOURCE` terminal 의미다.
|
|
frontend는 stale checkpoint를 CAS 제거하고 restart한다. network/429/모든 5xx는
|
|
server가 명시한 bounded `Retry-After`와 frontend attempt/deadline ceiling 안에서만
|
|
재시도된다. complete 내부 DTO는 session/fingerprint binding을 검증하지만
|
|
application-facing `QUARANTINED` 결과에는 state, opaque resource reference,
|
|
byte length와 replay 여부만 노출한다.
|
|
|
|
`AVAILABLE` 전에는 public URL, normal download capability나 active-content preview를
|
|
발급하지 않는다.
|
|
|
|
### 15.4 Range resumable download
|
|
|
|
Range capability는 whole-object download URL을 재사용하는 편의 header가 아니라
|
|
별도 `RANGE_RESUMABLE_DOWNLOAD_V1` control contract다.
|
|
|
|
server registry는 logical resource를 다음 immutable representation에 binding한다.
|
|
|
|
```text
|
|
representationBinding
|
|
objectRef
|
|
immutable generation
|
|
strong validator
|
|
exact total byte length
|
|
media type
|
|
whole-object SHA-256
|
|
```
|
|
|
|
- weak validator, multipart ETag의 digest 추정, last-modified나 file name을
|
|
representation identity로 사용하지 않는다.
|
|
- object provider의 version ID가 안정적이면 registry generation과 exact
|
|
provider locator를 server 내부에서 binding한다. 그렇지 않으면 BFF proxy가
|
|
immutable generation을 enforce한다.
|
|
- control response가 data-plane URL과 `If-Range` validator를 전달하더라도 raw
|
|
값은 adapter-owned in-memory vault에만 있고 application/checkpoint에는
|
|
representation binding digest만 남는다.
|
|
- capability는 exact `Range: bytes=start-end`,
|
|
`preconditionMode=STRONG_IF_RANGE | IMMUTABLE_GENERATION_PRECONDITION`, mode가
|
|
정한 exact header/value, `allowWholeObjectFallback`, policy-derived allowed
|
|
status subset, expected total/segment bytes, expiry와 single-use receipt를 묶는다.
|
|
- BFF/object provider가 이 제약을 실제로 강제하지 못하면 direct signed URL을
|
|
사용하지 않고 BFF relay를 사용한다.
|
|
|
|
data-plane 의미:
|
|
|
|
| 응답 | server 의미 | frontend 처리 계약 |
|
|
| --- | --- | --- |
|
|
| `206` | 같은 representation의 exact requested segment | exact `Content-Range`, length와 validator 검증 뒤 write |
|
|
| `200` | byte 0 full representation 또는 `If-Range` mismatch | 기존 partial에 append 금지, 새 generation/restart reconcile |
|
|
| `412` | 별도 `If-Match`/generation precondition 실패 | representation replacement로 checkpoint 격리 |
|
|
| `416` | requested range가 current representation에 유효하지 않음 | 자동 success 금지, total/destination/full digest 재검증 |
|
|
|
|
RFC 9110의 일반 `If-Range` mismatch는 `200` full response다. `412`를 받으려면
|
|
제품 계약이 별도 strong precondition을 명시해야 한다.
|
|
|
|
capability expiry 시 BFF는 current authorization과 registry를 다시 확인하고
|
|
같은 representation binding에 대해서만 새 segment capability를 발급한다.
|
|
object generation/length/media/digest가 달라지면 기존 partial resume를 거절한다.
|
|
final 성공은 frontend destination 전체 SHA-256 검증 뒤에만 가능하지만, server도
|
|
download capability가 주장한 digest/length가 registry의 authoritative object와
|
|
일치하도록 보장한다.
|
|
|
|
Range checkpoint retention과 seek/truncate 또는 OPFS staging은 browser local
|
|
계약이다. server는 raw local path/offset을 authorization proof로 받지 않는다.
|
|
|
|
### 15.5 Upload pause와 checkpoint lifecycle
|
|
|
|
pause는 기본적으로 browser work를 중지하는 local control이며 server abort가
|
|
아니다. 제품이 explicit server pause를 만들지 않더라도 status/reconcile은
|
|
paused client가 안전하게 돌아올 수 있도록 다음을 보장한다.
|
|
|
|
- session expiry와 terminal status의 안정적인 의미
|
|
- completed part의 ordered checksum과 bounded non-authorizing receipt
|
|
- list/status pagination 또는 절대 part-count ceiling
|
|
- 같은 idempotency key의 replay 결과
|
|
- abort/complete response loss 뒤 authoritative reconcile
|
|
- abandoned session TTL과 orphan janitor
|
|
|
|
frontend checkpoint inventory/retention API에는 presigned URL, server capability,
|
|
raw provider upload ID와 object key가 나타나지 않는다. checkpoint sweep가 local
|
|
row를 지웠다고 server session이 즉시 abort됐다고 가정하지 않으며 janitor SLO로
|
|
ambiguous orphan을 정리한다.
|
|
|
|
### 15.6 Image CDN delivery
|
|
|
|
backend asset registry는 scan/promotion을 통과한 immutable object generation만
|
|
opaque asset ID/revision에 binding한다. descriptor에는 registry-owned named
|
|
preset, static raster media type, exact natural/rendition dimensions, delivery
|
|
class와 private capability expiry를 포함한다. arbitrary external source URL 또는
|
|
caller transform query를 signer/CDN에 전달하지 않는다.
|
|
|
|
private capability가 서명하는 allowed preset binding ID는 versioned
|
|
server-owned immutable preset registry의 key다. CDN/BFF는 각 요청마다 이 ID를
|
|
조회하고 width/height/DPR/fit/format/quality query를 registry의 exact candidate와
|
|
재계산해 불일치를 거절한다. client query, binding digest 또는 signature의 단순
|
|
존재는 transform authorization이 아니다.
|
|
|
|
public rendition은 revisioned URL과 `public, max-age=..., immutable`, private
|
|
rendition은 short-lived capability와 실제 `Cache-Control: no-store`를 제공한다.
|
|
private response도 CORS에서 browser probe가 읽을 `Content-Type`,
|
|
`Content-Length`, `Cache-Control`을 허용해야 하며 credential cookie에 의존하지
|
|
않는다. CDN은 application과 다른 HTTPS origin에서 제공한다. 이는
|
|
`<img crossorigin="anonymous">`의 same-origin credential mode가 application
|
|
cookie를 보내는 경로를 차단하기 위한 배포 계약이다. frontend는
|
|
PNG/JPEG/WebP/AVIF static header metadata와
|
|
pixel/decoded-byte budget을 native decode 전에 검증하고, private delivery는
|
|
mandatory probe와 fetch/body/decode 전체 timeout을 적용한다.
|
|
|
|
private descriptor BFF는 fixed endpoint에서
|
|
`IMAGE_CDN_DESCRIPTOR_V1` strict bounded response를 반환한다. caller는 opaque
|
|
asset/preset reference만 제출하고 URL, origin, width, DPR, format, quality, fit,
|
|
cache policy와 key ID를 선택하지 않는다.
|
|
|
|
descriptor 발급과 refresh는 매번 current authorization, promotion state, immutable
|
|
asset revision, preset registry와 signing key registry를 다시 확인한다. expiry
|
|
뒤 stale descriptor 사용을 허용하지 않는다. key rotation은 bounded old/new
|
|
verification overlap, signer 전환, maximum descriptor lifetime + clock skew +
|
|
client rollout drain 이후 old key 제거 순서다. emergency revoke는 client
|
|
signature/expiry만 기다리지 않고 registry/CDN/BFF에서 capability를 거절한다.
|
|
|
|
generic Query cache나 durable client persistence가 private descriptor URL의
|
|
lifetime owner가 아니다. account/logout 뒤 늦은 refresh가 성공하더라도 frontend
|
|
runtime generation fence가 이를 채택하지 않으며, backend authorization도 old
|
|
session을 거절해야 한다.
|
|
|
|
### 15.7 failure mapping
|
|
|
|
server의 closed failure를 frontend의 allowlisted failure로 한 곳에서 mapping한다.
|
|
HTTP status/message를 application에 그대로 전달하지 않는다.
|
|
|
|
| server meaning | frontend meaning 예 |
|
|
| --- | --- |
|
|
| invalid request | `INVALID_INPUT` |
|
|
| size/part ceiling | `LIMIT_EXCEEDED` |
|
|
| revision/idempotency conflict | `CONFLICT` |
|
|
| checksum/length mismatch | `INTEGRITY_FAILED` |
|
|
| expired session/capability | `EXPIRED_RESOURCE` |
|
|
| denied policy/authorization | `POLICY_REJECTED` 또는 auth failure |
|
|
| provider/scanner temporary failure | `DEPENDENCY_UNAVAILABLE` + server retry directive |
|
|
| caller cancellation | `CANCELLED` |
|
|
| server deadline exhausted | `DEADLINE_EXCEEDED` |
|
|
| mutation outcome unknown | 같은 idempotency key로 status/reconcile, 신규 요청 금지 |
|
|
|
|
## 16. 제품 선택 시 결정할 항목
|
|
|
|
다음 표가 채워지기 전에는 endpoint와 composition을 만들지 않는다.
|
|
|
|
| 결정 | owner |
|
|
| --- | --- |
|
|
| 파일의 업무 목적과 aggregate 관계 | product/domain |
|
|
| authorization와 existence-hiding 정책 | security/domain |
|
|
| provider/region/data residency | platform/security |
|
|
| proxy/direct transfer 선택 | platform/product |
|
|
| object/part 최대 크기와 concurrency | performance/platform |
|
|
| session/capability TTL | security/product |
|
|
| checksum algorithm과 encoding | platform |
|
|
| MIME/parser/archive/CDR/scan profile | security/product |
|
|
| quarantine/available/rejected UX | product |
|
|
| retention/legal hold/delete | domain/legal |
|
|
| idempotency replay window/capacity | platform/product |
|
|
| provider/scanner SLO와 fallback | operations/product |
|
|
| logging, audit와 privacy retention | security/operations |
|
|
| rollout, kill switch와 removal owner | operations |
|
|
|
|
## 17. Server 구현 순서
|
|
|
|
1. 공통 identifier, `Result`와 closed failure model
|
|
2. in-memory deterministic fakes와 provider contract suite
|
|
3. `ObjectRegistryPort`와 `IdempotencyStorePort`
|
|
4. 첫 database adapter 및 concurrency/fault evidence
|
|
5. `ObjectByteStorePort`와 첫 object provider adapter
|
|
6. streaming counter/digest/deadline/resource wrappers
|
|
7. `MultipartStagingPort`와 orphan reconciliation
|
|
8. `SignedTransferCapabilityPort`와 expiry/key-rotation tests
|
|
9. optional Range capability와 immutable-generation/provider contract
|
|
10. `ContentScannerPort`, quarantine와 promotion saga
|
|
11. optional Image descriptor/preset/signing control plane
|
|
12. optional composition/removal gate
|
|
13. server template에서는 `AVAILABLE_NOT_COMPOSED`로 종료
|
|
14. 실제 제품에서만 domain/use case/controller와 provider composition 추가
|
|
|
|
## 18. 완료 기준
|
|
|
|
- 모든 요청 항목에 port, 불변조건, failure와 lifecycle이 정의됨
|
|
- native provider SDK type/exception이 port 밖으로 유출되지 않음
|
|
- streaming 경로에 whole-buffer 기본 구현이 없음
|
|
- byte length와 digest authority가 proxy/direct 경로별로 명확함
|
|
- multipart complete/abort/expiry/response-loss race가 결정적임
|
|
- signed capability가 exact operation/resource/limit/expiry에 binding됨
|
|
- Range를 선택한 경우 immutable representation, exact
|
|
`200/206/412/416`, reissue와 provider constraint가 같은 contract suite를 통과
|
|
- idempotency가 concurrent replay와 fingerprint mismatch를 처리함
|
|
- scan verdict가 exact generation/digest에 binding됨
|
|
- Image CDN을 선택한 경우 `IMAGE_CDN_DESCRIPTOR_V1`, preset exact recomputation,
|
|
key rotation/revocation과 private no-store가 실제 BFF/CDN에서 검증됨
|
|
- deadline/retry/cancel 모든 경로에서 resource cleanup이 검증됨
|
|
- 동일 provider contract suite가 fake와 실제 선택 provider에 실행됨
|
|
- 미선택 상태에 route/job/migration/provider client가 없음
|
|
- capability 전체 제거 후 server template core가 정상 동작함
|
|
- 실제 제품 domain/use case와 provider 선택이 이 문서에 하드코딩되지 않음
|
|
|
|
## 19. 관련 frontend 설계
|
|
|
|
- `docs/architecture/browser-data-capability-completion-ledger.md`
|
|
- `docs/architecture/browser-file-and-origin-storage.md`
|
|
- `docs/architecture/decisions/VD-11-browser-file-and-origin-storage.md`
|
|
- `docs/architecture/presigned-transfer-and-image-cdn.md`
|
|
- `docs/architecture/decisions/VD-12-presigned-transfer-and-image-cdn.md`
|
|
- `docs/architecture/decisions/VD-14-resumable-download-and-background-transfer.md`
|
|
- `docs/architecture/decisions/VD-16-browser-transfer-composition-and-image-delivery.md`
|
|
- `docs/architecture/frontend-ports-adapters-and-boundaries.md`
|
|
- `docs/architecture/optional-adapter-recipes.md`
|
|
- `docs/operations/browser-file-storage-recovery.md`
|
|
- `docs/operations/browser-transfer-recovery.md`
|
|
|
|
## 20. 참고 표준과 보안 가이드
|
|
|
|
- RFC 9110, HTTP Semantics: <https://www.rfc-editor.org/rfc/rfc9110>
|
|
- RFC 9530, Digest Fields: <https://www.rfc-editor.org/rfc/rfc9530>
|
|
- OWASP File Upload Cheat Sheet:
|
|
<https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html>
|