# 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/ registry/ idempotency/ signer/ scanner/ 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 inspect(locator, context) -> Result openRead( locator, expectedVersion?, range?, context ) -> Result putAtomic( newLocator, expectedLength, expectedDigest?, mediaType, byteStream, context ) -> Result deleteExact( locator, expectedVersion, context ) -> Result ``` `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 putPart( multipartRef, partNumber, exactOffset, expectedLength, expectedDigest, byteStream, context ) -> Result inspectParts( multipartRef, context ) -> Result complete( multipartRef, orderedExactPartReceipts, context ) -> Result abort( multipartRef, context ) -> Result ``` `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 getObject(objectRef, context) -> Result transitionObject(objectRef, generation, expectedState, nextState, context) -> Result createSession(sessionSnapshot, context) -> Result getSession(sessionRef, context) -> Result appendPart(sessionRef, expectedRevision, partRecord, context) -> Result transitionSession(sessionRef, expectedRevision, expectedState, nextState, context) -> Result listExpiredOpenSessions(cursor, maxRows, context) -> Result listReconciliationCandidates(cursor, maxRows, context) -> Result ``` 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 issueDownload( subjectScope, objectRef, generation, providerLocator, allowedRange?, responseMetadata, expiresAt, context ) -> Result ``` ```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 | IN_PROGRESS | REPLAY> renew(leaseAndFence, extendUntil, context) -> Result complete(leaseAndFence, requestFingerprint, completionReceipt, expiresAt, context) -> Result releaseRetryable( leaseAndFence, requestFingerprint, proofSideEffectNotStarted, context ) -> Result pruneExpired(cursor, maxRows, context) -> Result ``` `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 submit( scanJobRef, objectRef, generation, immutableDigest, scanProfileId, sourceFactory, context ) -> Result inspect(jobRef, context) -> Result cancel(jobRef, context) -> Result ``` ```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 reject( objectRef, expectedGeneration, expectedQuarantineDigest, sealedRejectedScanReceipt, idempotency, context ) -> Result ``` - 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에서 제공한다. 이는 ``의 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: - RFC 9530, Digest Fields: - OWASP File Upload Cheat Sheet: