chore: initialize from frontend template 4dc033c

This commit is contained in:
DongHyeonka
2026-08-13 18:23:26 +09:00
commit 40107eec84
897 changed files with 234824 additions and 0 deletions
@@ -0,0 +1,581 @@
# API contract와 Server State recovery
- 상태: current REST 절차 + future GraphQL/Connect/gRPC-Web/REST Gateway/
schema/cache 절차 분리
- backend 구조와 handoff:
[Backend API와 Server State contract](../architecture/backend-api-and-server-state-contract.md)
- 기준일: 2026-07-28
- 설계:
[API contract, Schema, Mapper와 Server State](../architecture/api-contract-schema-mapper-and-server-state.md)
- browser Protobuf/gateway 설계:
[Protobuf browser transport와 REST Gateway](../architecture/protobuf-browser-transport-and-rest-gateway.md)
## 1. 적용 범위
현재 reference REST → Schema → Mapper → TanStack Query vertical은 `COMPOSED`다.
하지만 다음 target delta는 아직 `DESIGNED_NOT_IMPLEMENTED`다.
- REST v2 auth fail-close/final invariant
- total logical deadline와 bounded response decoder
- complete cursor/conditional request
- typed schema/mapper registry
- strict query/mutation definition과 concurrency coordinator
- GraphQL reference adapter
- Connect-Web reference adapter
- gRPC-Web reference adapter
- Protobuf REST Gateway
GraphQL/Connect/gRPC-Web/REST Gateway 절차는 해당 runtime과 actual provider가
구현·조합된 뒤에만 운영 gate로 활성화한다. 문서나 fake fixture만으로 capability를
`AVAILABLE_NOT_COMPOSED`, `COMPOSED` 또는 production-ready라고 기록하지 않는다.
canonical readiness:
```text
Selection
TrafficAdmission
RuntimeHealth
PromotionEvidence
```
이 readiness model은 target v2 control plane이다. current v1에는 operation별
`TrafficAdmission` runtime switch가 없다.
| control | current v1 | target v2 |
| --- | --- | --- |
| operation traffic switch | 없음 | provider/family/operation별 `TrafficAdmission` |
| frontend containment | promotion 중지와 known-good release rollback; feature composition 변경은 재배포 필요 | atomic admission disable |
| provider containment | actual BFF/API owner의 route/kill switch가 있을 때만 사용 | registered provider kill switch와 probe |
| query cleanup | composed QueryClient/coordinator의 cancel·invalidate·local clear | generation fence와 profile-scoped cleanup |
| mutation recovery | backend 상태/idempotency owner와 수동 reconcile | effect certainty + runtime/scope coordinator |
따라서 current v1 incident에서 존재하지 않는 switch를 내렸다고 기록하지 않는다.
target v2가 composed된 operation만 `TrafficAdmission`을 먼저 내린다. 그 전에는
신규 promotion을 중지하고 known-good release rollback 또는 실제 backend/BFF
route 차단을 owner와 수행한다. primary status나 contract version을 운영자가
임의로 낮춰 현재 상태를 숨기지 않는다.
current v1으로의 fallback은 (1) 해당 operation의 provider/security evidence가
만료되지 않았고, (2) incident class가 v1 또는 shared boundary에 영향이 없으며,
(3) auth fail-close/final invariant hardening이 유지되는 경우만 허용한다. 하나라도
증명하지 못하면 v1을 사용하지 않고 safe unavailable 또는 실제 provider route
차단으로 닫는다.
## 2. 관측 허용과 금지
허용된 bounded aggregate:
- semantic operation/profile/provider ID
- protocol/semantics
- safe failure kind
- HTTP status group, GraphQL safe category, gRPC status bucket
- logical/physical attempt, auth recovery와 deadline bucket
- provider가 측정한 encoded transfer bucket과 browser가 측정한
decoded/result/message/frame/page/item byte/count bucket
- schema/mapper/query profile version과 compatibility outcome
- cache hit/miss/stale/refresh/admission/eviction
- mutation concurrency/optimistic/rollback/conflict/effect/invalidation outcome
- stream terminal/gap/overflow/idle bucket
금지:
- URL/path/search/header/body
- credential, cookie, CSRF, idempotency key
- GraphQL document/hash/variables/data/error message/path/extensions
- protobuf payload/debug JSON/metadata/trailer/status-details
- DTO/application/cache value/optimistic patch
- cursor/snapshot/ETag/revision/resume token
- account/tenant/resource/file ID
- raw backend request/trace/correlation value
incident ticket에 금지 값을 복사하지 않는다. credential/payload 노출 의심이면
일반 contract incident가 아니라 security incident로 분류하고 provider revoke,
key/session rotation과 로그 보존 범위를 별도 owner에게 escalate한다.
## 3. 최초 containment
1. 영향 operation family/protocol/provider/browser/release cohort를 safe aggregate로
좁힌다.
2. target v2 control이 실제 composed됐으면 해당 operation의
`TrafficAdmission``DISABLED` 또는 안전한 canary cohort로 낮춘다. current
v1이면 promotion을 중지하고 known-good release rollback 또는 실제
backend/BFF route control을 사용한다.
3. query/read는 cancel하고 stream reader를 닫는다.
4. command는 무조건 retry/rollback하지 않고 effect certainty를
`NOT_APPLIED | COMMITTED | UNKNOWN`으로 분류한다.
5. `UNKNOWN` command는 backend idempotency/status/reconcile owner에게 전달한다.
6. current scope의 suspect query를 invalidate한다. wrong-scope/contract-poison
가능성이 있으면 cancel 후 current runtime cache를 clear한다.
7. GraphQL/Connect/gRPC-Web failure를 REST command로 자동 failover하지 않는다.
8. contract tuple과 artifact를 확인한다.
```text
frontend release/build
runtime config/API contract version
installed operation registry digest
runtime schema/mapper/query policy digest
REST/OpenAPI artifact
GraphQL schema + persisted manifest
protobuf descriptor/generated artifact
Connect client/server/provider profile
HttpRule/ProtoJSON/OpenAPI/gateway artifact
BFF/router/proxy/backend release
```
실제 digest 값을 high-cardinality telemetry label로 보내지 않고 incident evidence
artifact에서 access-controlled하게 비교한다.
## 4. 공통 증상 분류
| 증상 | 우선 확인 |
| --- | --- |
| 특정 release에서 전 operation 실패 | runtime config, provider endpoint, auth owner, contract set |
| schema/mapper mismatch 급증 | backend artifact rollout, unknown/null/enum/scalar 변화, wrong codec |
| retry/traffic 폭증 | retry owner 중복, 401 loop, Retry-After, total deadline |
| stale/wrong account data | scope generation, late result, query key collision, invalidation |
| command duplicate/rollback 이상 | idempotency tuple, input-aware concurrency, optimistic revision |
| list가 반복/무한 증가 | cursor loop/snapshot/page/item/byte ceiling |
| provider 200인데 client 실패 | status/media/envelope/schema/mapper/body cap |
| 로그에서 payload/ID 발견 | redaction/allowlist bypass, diagnostics producer |
## 5. Auth와 CSRF
### 5.1 Auth-required request가 무자격으로 전송됨
현재 v1은 external auth integration이 unavailable이어도 unchanged request가
전송될 수 있으므로 target v2 전까지 제품 auth-required traffic을 provider
evidence 없이 활성화하지 않는다.
target 점검:
1. session state가 authenticated인지 확인한다.
2. auth owner가 `CredentialPatch`만 반환하는지 또는 final request invariant를
재검증하는지 확인한다.
3. URL/origin/method/body digest/idempotency/conditional/CSRF가 attach 전후 같은지
확인한다.
4. unavailable/integration-failed/attach rejected에서 fetch가 0회인지 증명한다.
5. 401 recovery가 logical execution당 1회인지 확인한다.
6. concurrent 401이 single-flight recovery를 공유하는지 확인한다.
7. non-replayable command가 recovery 뒤 재실행되지 않았는지 확인한다.
의심 시 operation auth traffic을 즉시 닫는다. anonymous fallback을 만들지 않는다.
### 5.2 CSRF/CORS
1. unsafe cookie request의 exact Origin/Fetch Metadata/CSRF proof를 server에서
확인한다.
2. SameSite/custom header/preflight 하나만으로 방어했다고 기록하지 않는다.
3. cross-origin allow-origin/credentials/method/header/expose가 exact한지 확인한다.
4. wildcard credential, redirect와 unexpected origin을 복구한다.
5. CSRF token/header 값을 ticket에 기록하지 않는다.
## 6. REST incident
### 6.0 Current v1 available checks
1. current per-attempt timeout/bounded retry, Query retry off, AbortSignal과 401
recovery observation을 확인한다.
2. installed request/envelope/payload Zod schema와 mapper failure kind를 확인하되
current `Response.json()`이 body cap 증거가 아님을 기록한다.
3. QueryClient/coordinator로 affected read를 cancel/invalidate/clear한다.
4. cursor response/page runtime, APP_ETAG와 total deadline은 아직 없으므로 해당
control을 실행했다고 기록하지 않는다.
5. command effect는 backend와 reconcile한다. basic optimistic snapshot 문제가
의심되면 runtime switch를 가정하지 말고 no-optimistic/known-good release로
rollback한다.
아래 6.1~6.5는 target v2 coordinator가 해당 operation에 composed된 뒤에만
운영 절차로 활성화한다.
### 6.1 Timeout/retry storm — target v2
1. Query retry가 꺼지고 REST transport 하나만 network retry하는지 확인한다.
2. total logical deadline 안에 auth, fetch, body, mapper, recovery와 sleep이 모두
포함되는지 확인한다.
3. attempt timeout과 logical deadline exhaustion을 별도 kind로 확인한다.
4. 초회/retry/401 replay 모든 provider fetch가 하나의 monotonic
physical-attempt cap을 소비하는지 확인한다.
5. exact retry status가 network/408/429/502/503/504 중 operation subset인지
확인한다.
6. 429/503 `Retry-After`가 injected clock, max sleep/elapsed ceiling을 지키는지
확인한다.
7. timer/listener/reader leak가 있는 cohort는 traffic을 닫는다.
8. keyed command는 backend dedupe evidence 없이는 retry를 끈다.
### 6.2 Body/media/schema failure — target v2
1. final URL/status/content type을 확인한다.
2. present/valid Content-Length advisory preflight와 actual browser-visible decoded
byte cap을 확인한다.
3. actual encoded transfer/decompression ratio cap은 BFF/proxy/CDN evidence에서
확인한다. browser reader가 encoded bytes를 셌다고 가정하지 않는다.
4. standard JSON parse profile이면 decoded byte는 pre-parse guard,
depth/node/key는 post-parse admission guard인지 확인한다.
5. truncated, content encoding/decompression overflow와 invalid UTF-8을 구분한다.
6. success status/media/envelope profile이 operation registry와 일치하는지 확인한다.
7. schema unknown-field policy와 actual codec fingerprint를 확인한다.
8. failure body/value를 출력하지 않는다.
9. schema/mapper failure result를 cache하지 않았는지 확인한다.
10. backend/frontend artifact가 coherent하지 않으면 operation traffic을 닫고
N/N-1 compatible set으로 rollback한다.
### 6.3 Idempotency/ambiguous command — target v2
1. principal/operation/payload digest/key tuple과 backend TTL/status를
access-controlled evidence에서 확인한다.
2. 같은 key + 다른 payload가 거절되는지 확인한다.
3. concurrent same-key request가 duplicate commit을 만들지 않았는지 확인한다.
4. `UNKNOWN` effect에 새 key를 생성해 retry하지 않는다.
5. backend status/receipt로 `COMMITTED`면 cache reconcile/invalidate한다.
6. `NOT_APPLIED`가 증명된 경우만 정책에 따라 retry한다.
### 6.4 Cursor loop — target v2
1. query root filters/sort/scope/snapshot을 확인한다.
2. same cursor 반복, `hasMore`/cursor 모순과 non-progress page를 확인한다.
3. max pages/items/bytes를 초과했으면 추가 fetch를 중지한다.
4. snapshot이 바뀌었으면 old/new pages를 합치지 않고 root부터 재시작한다.
5. raw cursor/next URL을 로그에 남기거나 임의 follow하지 않는다.
### 6.5 Conditional request — target v2
1. operation cache owner가 `APP_ETAG`인지 browser HTTP cache인지 확인한다.
2. 304에서 exact query/scope/generation/validator binding과 mapped cached value가
모두 있는지 확인한다.
3. 304가 same query-entry cache revision CAS를 통과했는지 확인한다.
4. cached value가 없으면 304를 success로 만들지 않는다.
5. full 200 value와 validator가 atomic하게 교체됐는지 확인한다.
6. Query removal/GC, scope/logout와 release/contract/schema/mapper epoch에서
sidecar가 함께 폐기됐는지 확인한다.
7. 412 precondition과 409 business conflict를 구분한다.
8. ETag/revision 값을 incident ticket에 기록하지 않는다.
## 7. GraphQL incident
이 절은 VD-26 runtime 조합 후 적용한다.
### 7.1 Persisted operation mismatch
1. frontend schema/operation manifest와 router manifest 호환성을 확인한다.
2. operation ID, hash, schema digest와 retirement window를 artifact에서 비교한다.
3. full document fallback/APQ miss fallback을 활성화하지 않는다.
4. affected GraphQL operation traffic을 닫는다.
5. frontend + manifest + router를 coherent N/N-1 set으로 rollback한다.
### 7.2 HTTP/media + data/errors
1. selected GraphQL-over-HTTP revision, persisted-envelope extension,
`Accept`/response media와 status/body matrix를 확인한다.
2. `application/graphql-response+json`이면 허용 status에서도 body를 bounded
GraphQL envelope로 처리했는지 확인한다. legacy `application/json` non-2xx
intermediary body를 GraphQL error로 해석하지 않는다.
3. `errors=[]`, null/absent data + no errors를 envelope mismatch로 닫았는지
확인한다.
4. data/errors state branch와 operation partial policy를 확인한다.
5. raw message/path/extensions를 보지 않고 allowlisted safe code만 확인한다.
6. query partial이 `ALLOW_TYPED_PARTIAL` 조건을 모두 만족하는지 확인한다.
7. unauthorized/contract error field를 previous cache로 자동 채우지 않는다.
8. mutation이면 effect certainty/receipt를 확인한다.
9. partial/mutation error를 ordinary success로 cache하지 않는다.
### 7.3 Cost/depth/rate failure
1. persisted operation manifest의 max cost/depth/aliases/list와 router 실제 budget을
비교한다.
2. variables/response size가 frontend/server 양쪽 ceiling 안인지 확인한다.
3. client retry storm을 끈다.
4. arbitrary operation 또는 introspection을 허용해 우회하지 않는다.
5. field authorization과 cost budget을 별도 확인한다.
### 7.4 Incremental/subscription
선택된 경우에만:
- exact incremental/subscription protocol/provider revision
- exact Accept/Content-Type parameter와 payload discriminant
- multipart boundary/part/path/terminal/byte/patch count
- truncated/duplicate/out-of-order
- proxy buffering
- sequence/gap/resume/heartbeat
- bounded queue/overflow
- logout/unsubscribe
terminal completeness 전 staging을 complete query cache로 commit하지 않는다.
## 8. gRPC-Web incident
이 절은 VD-27 runtime 조합 후 적용한다.
### 8.1 HTTP success인데 RPC 실패/멈춤
1. exact client runtime/wire-spec revision/content type/transport/rpc-kind capability
matrix를 확인한다. official XHR runtime과 Connect-Web/custom Fetch runtime을
같은 cancel/deadline/frame 관측 경로로 취급하지 않는다.
2. `responseHttpStatusProfileId`와 HTTP status/gRPC media/terminal-source
교차 matrix를 확인한다.
3. custom Fetch framed runtime이면 frame prefix/length/flag/compression을
확인한다. official runtime이면 runtime-owned callback/status API와
`ClientReadableStream.cancel()` 경로를 확인하며 raw `ReadableStream` parser가
있다고 가정하지 않는다.
4. body trailer frame 또는 zero-body trailers-only response header 중 terminal
status source가 정확히 하나인지 확인한다.
5. `grpc-status`가 있으면 canonical decimal `0..16`으로 exactly once인지, body
trailer name lowercase와 rich-details outer code가 맞는지 확인하며 그 status를
authority로 사용한다.
6. `grpc-status`가 없으면 공식 fallback만 적용한다:
`400→INTERNAL`, `401→UNAUTHENTICATED`, `403→PERMISSION_DENIED`,
`404→UNIMPLEMENTED`, `429|502|503|504→UNAVAILABLE`, 그 외 `UNKNOWN`.
이것을 wire header로 위조하지 않고 internal normalized failure로만 사용한다.
7. HTTP success만으로 RPC success를 기록하지 않는다. duplicate/conflicting/
malformed terminal source는 provider incompatibility로 traffic을 닫는다.
8. Envoy의 upstream HTTP/2, filter order, route/idle/max-stream timeout과 buffering
profile을 확인한다. default route timeout으로 long stream이 잘렸는지 확인한다.
9. raw grpc-message/status-details를 ticket에 복사하지 않는다.
### 8.2 Unary response
1. OK status에서 response message가 exact 1개인지 확인한다.
2. `google.protobuf.Empty`도 zero-length payload data frame 하나여야 한다.
trailers-only OK + zero-message unary는 contract mismatch다.
3. two-or-more message/data-after-trailer/extra bytes를 contract mismatch로 닫는다.
4. generated decode 뒤 semantic schema/mapper가 통과했는지 확인한다.
5. failure/late generation에서 cache write가 0인지 확인한다.
### 8.3 Server stream
1. message/frame/total bytes/count/idle/total deadline을 확인한다.
2. bounded queue와 consumer backpressure를 확인한다.
3. duplicate/order/gap/resume token protocol을 확인한다.
4. event 전달 뒤 transport-only auto replay가 없었는지 확인한다.
5. overflow/gap/truncation에서 current aggregate를 complete success로 쓰지 않는다.
6. finite aggregate는 valid OK terminal status/completeness 뒤 atomic cache
commit인지 확인한다.
7. actual proxy buffering/flush와 affected browser cohort를 확인한다.
### 8.4 Proxy/CORS
- Envoy/BFF/selected gateway filter와 upstream route
- exact allow/expose headers
- credential/CSRF/Origin
- trailer preservation
- content type
- compression/message/deadline cap
- stream buffering
actual proxy를 복구한다. client decoder를 느슨하게 만들어 provider drift를
수용하지 않는다.
## 9. Connect와 REST Gateway incident
이 절은 VD-29/VD-30 runtime이 실제 조합된 operation에만 적용한다.
### 9.1 Connect unary/server stream
1. exact Connect-Web package/runtime, `CONNECT | GRPC_WEB`, JSON/binary,
POST/GET와 rpc kind row를 확인한다. content type으로 다른 protocol을 자동
선택하지 않는다.
2. Connect unary는 HTTP status + bounded Connect error, stream은 HTTP admission +
exactly-one final EndStream을 각각 authority로 사용한다.
3. missing/early/duplicate EndStream, terminal 뒤 data, malformed/oversize
envelope와 compression mismatch에서 call을 cancel하고 cache write를 막는다.
4. stock runtime이 whole unary body를 읽는 profile이면 edge encoded/decompressed
cap과 selected version의 actual overflow evidence를 확인한다. interceptor가
raw-byte cap을 제공했다고 기록하지 않는다.
5. remaining total deadline이 0 이하면 runtime `timeoutMs=0`을 호출하지 않고
local deadline failure로 닫는다. abort 뒤 command는 backend receipt로
reconcile한다.
6. GET은 descriptor `NO_SIDE_EFFECTS`, input classification, URL cap, cache key,
`Vary`와 credential/preflight profile이 모두 일치하는지 확인한다.
7. raw Connect message/metadata/detail/debug를 ticket에 복사하지 않는다.
### 9.2 Protobuf REST Gateway
1. selected gateway kind/version과 descriptor, canonical HttpRule manifest,
ProtoJSON profile, OpenAPI와 runtime config digest를 coherent set으로 비교한다.
2. method/path/query/body/additional binding, path escaping과 response-body
projection drift를 확인한다.
3. ProtoJSON field name/default/enum/int64/bytes/null/presence/unknown behavior를
N/N-1 fixture와 비교한다.
4. current REST envelope/status/error를 direct ProtoJSON body나 raw
`google.rpc.Status`로 잘못 decode했는지 확인한다.
5. `201/204/304/412`, ETag, idempotency, pagination과 safe error rewrite가 실제
BFF/gateway owner의 contract인지 확인한다. transcoder가 자동 제공한다고
가정하지 않는다.
6. browser abort→gateway context→upstream gRPC→application work의 cancellation/
deadline 전파를 확인한다. 이미 commit된 command는 receipt로 reconcile한다.
7. runtime response rewrite와 generated OpenAPI가 다르면 affected route admission을
닫고 coherent gateway/contract set으로 rollback한다.
Connect/gRPC-Web/REST 사이 command replay는 금지한다. read-only registered
fallback도 old generation을 cancel한 뒤 새 logical query로만 시작한다.
## 10. Schema/Mapper incident
### 10.1 Current v1 available checks
1. operation의 string schema ID가 installed Zod resolver에 있는지 확인한다.
2. envelope/payload parse와 mapper/domain factory 중 어느 current boundary에서
실패했는지 safe failure/diagnostics로 좁힌다.
3. raw DTO/value를 출력하지 않고 affected query cache write를 확인한다.
4. current registry에는 semantic fingerprint/provenance/typed mapper Result가
없으므로 이 값을 비교했다고 기록하지 않는다.
5. frontend release/runtime config/manifest의 current string contract coherence를
확인하고 known-good coherent release로 rollback한다.
### 10.2 Target v2 composed 이후
1. operation → schema ID/fingerprint → mapper ID/version binding을 확인한다.
2. source artifact/generated output/runtime codec digest를 비교한다.
3. missing/null/enum/numeric/time/unknown-field change를 classification한다.
4. request strict와 response strip/reject 방향 정책을 확인한다.
5. provider encoded-transfer와 browser decoded-byte owner를 분리하고,
item/depth/node/string admission ceiling을 확인한다.
6. mapper expected semantic failure가 throw/`UNKNOWN_FAILURE`로 소실되지 않았는지
확인한다.
7. cache write와 partial collection이 0인지 확인한다.
8. raw validation value/path/DTO를 log에 남기지 않는다.
9. incompatible schema/mapper/query cache epoch를 coherent set으로 rollback/clear한다.
## 11. Server State Cache incident
### 11.0 Current v1 available checks
1. Query retry off, global stale/gc, AbortSignal과 affected query의
cancel/invalidate/local clear를 확인한다.
2. current arbitrary key/canonicalize와 hook-local pending Promise를 관측하고
collision/incorrect join cohort cache를 clear한다.
3. current whole-snapshot optimistic rollback이 다른 commit을 덮었는지 backend
authoritative state로 reconcile한다.
4. strict key/identity token, cursor page와 optimistic layer CAS는 아직 없으므로
해당 fixture/control이 실행됐다고 기록하지 않는다.
5. 필요하면 known-good/no-optimistic release로 rollback한다. operation별 runtime
optimistic switch가 있다고 가정하지 않는다.
아래 10.1~10.4는 target v2 cache/mutation coordinator가 composed된 뒤 활성화한다.
### 11.1 Wrong/stale scope — target v2
1. VD-13 frozen scope projection과 runtime generation을 확인한다.
2. account/logout/release switch 전 query를 cancel했는지 확인한다.
3. late response/mapper/cache write fence를 확인한다.
4. raw account/resource ID를 key에서 찾거나 출력하지 않는다.
5. current runtime cache를 clear/remount하고 backend authoritative data를 refetch한다.
### 11.2 Query key collision — target v2
1. arbitrary key API가 남아 있는지 확인한다.
2. undefined/NaN/Date/class/accessor/cycle/sparse/oversize fixture를 실행한다.
3. collision 가능 cohort의 cache를 clear한다.
4. strict key codec/profile 없는 신규 query traffic을 닫는다.
5. URL/document/protobuf serialization을 key로 급히 넣지 않는다.
### 11.3 Mutation ordering/rollback — target v2
1. 전체 validated semantic input의 runtime-private exact equality + identity
token이 아닌 hook instance로 join/reject를 판정했는지 확인한다.
2. `JOIN_IDENTICAL | REJECT_DUPLICATE | ALLOW_INDEPENDENT` 결과와 fetch/Promise
횟수가 profile대로인지 확인한다.
3. runtime/scope coordinator에서 same logical key의 two-hook/out-of-order
execution을 확인한다.
4. whole snapshot rollback이 다른 committed mutation을 덮었는지 확인한다.
5. 자기 optimistic layer만 base cache revision CAS로 제거/역적용했는지 확인한다.
6. CAS miss/unknown effect에서는 다른 commit을 보존하고 invalidate/refetch한다.
7. optimistic update를 끄고 authoritative pending/refetch UX로 downgrade한다.
8. server commit 뒤 invalidation failure는 command failure로 바꾸지 않는다.
### 11.4 Cache size/pagination — target v2
1. result/page/item/estimated-byte cap을 확인한다.
2. identity intern entry/total canonical bytes/active lease와 GC/terminal release
leak를 확인한다. token/canonical bytes 자체는 출력하지 않는다.
3. unbounded collection/stream history를 cancel/evict한다.
4. binary/generated DTO를 cache value로 넣지 않았는지 확인한다.
5. max pages와 page eviction/refetch policy를 적용한다.
## 12. Rollback
target v2 coordinator가 composed된 operation:
```text
TrafficAdmission=DISABLED
-> cancel reads/streams
-> reconcile command effects
-> fence runtime generation
-> clear suspect current-scope memory cache
-> detach provider/listener/auth collaboration
-> restore coherent frontend + contract artifacts + provider/backend
-> run compatibility/provider probes
-> canary
```
current v1은 위 control이 있다고 가정하지 않는다. 신규 promotion을 중지하고
known-good release를 rollback하며, 실제 backend/BFF owner가 제공하는 route/kill
switch가 있을 때만 provider traffic을 차단한다. v1 fallback은 unexpired
provider/security evidence, incident가 v1/shared boundary 밖이라는 증거와 auth
fail-close/final invariant 유지가 모두 있을 때만 선택하고, 아니면 safe
unavailable로 닫는다.
금지:
- GraphQL/Connect/gRPC command를 REST 또는 gateway route로 자동 replay
- schema number/digest만 낮추기
- incompatible cache value를 cast/loose parse
- validation/mapper를 끄고 traffic 유지
- unknown command effect를 optimistic rollback만으로 해결
- auth unavailable을 anonymous request로 downgrade
## 13. Security containment
다음은 security owner에게 즉시 escalate한다.
- credential/CSRF/idempotency/presigned material이 cache/log에 노출
- auth owner가 endpoint/method/body를 변경
- unauthorized request가 실제 전송
- GraphQL arbitrary document/field authorization bypass
- gRPC metadata/status details payload 노출
- cross-account cache observation
신규 traffic을 닫고 credential/session/provider key/manifest를 필요한 scope에서
revoke/rotate한다. 삭제/보존은 incident response 정책을 따른다.
## 14. Drill과 evidence
분기 또는 major contract 변경 전:
- REST auth unavailable/final-request mutation
- total deadline/retry/401 single-flight
- oversized/truncated/schema/mapper
- idempotency concurrent replay/unknown effect
- cursor loop/ETag 304/412
- GraphQL persisted miss/partial/cost
- gRPC missing/conflicting terminal status source, corrupt frame와 stream gap
- query key collision/account late result
- optimistic concurrent rollback
- kill switch/coherent rollback
- GraphQL/Connect/gRPC/Gateway generated dependency/provider removal
evidence record:
```text
protocol/operation/provider/profile
frontend/backend/router/proxy/browser version
contract artifact compatibility outcome
fault/drill ID
safe result
started/completed timestamp
owner/reviewer
artifact retention/expiry
```
raw payload, URL, document, protobuf와 identifier를 evidence record에 넣지 않는다.
## 15. 관련 문서
- [VD-23 API transport selection과 REST execution](../architecture/decisions/VD-23-api-transport-selection-and-rest-execution.md)
- [VD-24 Runtime Schema와 boundary Mapper](../architecture/decisions/VD-24-runtime-schema-and-boundary-mapper.md)
- [VD-25 Server State Cache lifecycle](../architecture/decisions/VD-25-server-state-cache-lifecycle.md)
- [VD-26 Persisted GraphQL operation](../architecture/decisions/VD-26-persisted-graphql-operation.md)
- [VD-27 gRPC-Web unary와 server stream](../architecture/decisions/VD-27-grpc-web-unary-and-server-stream.md)
- [VD-29 Connect-Web와 browser Protobuf runtime](../architecture/decisions/VD-29-connect-web-and-browser-protobuf-runtime.md)
- [VD-30 Protobuf contract와 REST Gateway](../architecture/decisions/VD-30-protobuf-contract-and-rest-gateway.md)
- [VD-13 Client cache scope와 persistence](../architecture/decisions/VD-13-client-cache-scope-and-persistence.md)
@@ -0,0 +1,514 @@
# Browser file and storage recovery runbook
이 runbook은 VD-11 capability를 실제 프로젝트에서 선택한 뒤 사용하는
production 운영 template이다. 현재 skeleton에는 native reference runtime이
`AVAILABLE_NOT_COMPOSED`로 존재하지만 제품 dataset과 bootstrap에는 연결되지
않았으므로 이 문서 자체가 현재 서비스 incident procedure를 활성화하지 않는다.
설치 branch는 owner, dashboard, alert threshold, kill-switch key,
backend/storage 연락처와 evidence 위치를 채워야 한다.
VD-15가 설계한 origin-wide pressure/GC coordinator, OPFS/Cache forward
migration, OPFS real readiness preflight, bounded Cache cleanup과 preview decode
probe는 현재 `DESIGNED_NOT_IMPLEMENTED`다. 아래 절차에서 이 기능을 전제로 한
자동 조치는 해당 runtime이 구현·조합된 제품에서만 실행한다. 현재 reference
primitive를 coordinator 완료 증거로 사용하지 않는다.
## 1. 공통 원칙
incident 중에도 다음 작업은 금지한다.
- 자동 또는 일괄 `deleteDatabase()`
- origin의 모든 `caches.keys()` 삭제
- user-authored/unsynced IndexedDB 또는 OPFS data 자동 purge
- schema version downgrade
- 무한 reload/update loop
- filename, path, record/object key, URL/query, digest, body를 incident log에 복사
- fake/unavailable adapter로 바꾸고 정상 복구로 선언
공통 안전한 degrade 순서는 다음과 같다.
```text
선택 capability의 신규 write/activation 중지
-> 진행 중 operation 정리
-> read-only
-> online-only/network-only
-> authoritative server re-read
```
read-only나 online-only가 사용자 작성 내용을 잃게 한다면 먼저 export/sync
경로를 제공하고 UI에 명시적으로 알린다.
## 2. 최초 10분
1. 영향 release, browser family/version, capability와 최초 발생 시각을 확인한다.
2. user dismissal과 실제 failure를 분리한다.
3. safe metric의 `failure_kind`, `phase`, `strategy`, `backend`, `size_bucket`,
`release_id`만으로 범위를 좁힌다.
4. integrity mismatch, authorization/cache classification breach, committed
user-data corruption이면 신규 write/cache activation을 즉시 중지한다.
5. kill switch를 가장 좁은 범위로 적용한다.
6. current와 N-1 release의 schema/cache compatibility를 확인한다.
7. 복구 전후 diagnostic count와 browser smoke evidence를 저장한다.
심각도 시작점:
| 상황 | 시작 심각도 |
| --- | --- |
| authorization 우회, private/auth response cache, content integrity mismatch | P1 |
| committed user-authored data unreadable/corrupt, widespread migration failure | P1 |
| upload/download 실패 급증, IndexedDB blocked 증가, OPFS reconstructable loss | P2 |
| enhanced picker만 실패하고 baseline 정상 | P3 |
실제 조직 severity policy가 있으면 그 정책이 우선한다.
## 3. File/picker/download와 별도 upload example
### 신호
- `PERMISSION_DENIED`, `NOT_READABLE`, `INTEGRITY_FAILED`
- closed byte stream이 failure chunk 뒤 추가 byte를 내보내거나 raw exception을 throw
- browser handoff는 정상인데 confirmed save가 감소
- large download에서 memory/stall failure
- 별도 backend upload를 설치한 feature만 session expiry/conflict,
quarantined/available 전환 정체
### 확인 순서
1. 사용자가 실제 취소한 flow가 error로 집계되지 않았는지 확인한다.
2. user activation 전에 비동기 작업이 추가됐는지 확인한다.
3. native input fallback이 Chromium/Firefox/WebKit에서 동작하는지 확인한다.
4. count, per-file, total bytes, MIME/extension/signature policy version을 확인한다.
raw policy가 port caller에서 들어오지 않고 composition registry의 정확한
`FilePolicyReference` identity로 resolve되는지, verification receipt가 같은
profile/file snapshot에 binding됐는지 확인한다. 같은
`policyKey`/`intention`을 가진 새 객체는 거절되어야 한다.
5. File/OPFS/Cache/download byte source가 chunk마다 closed Result를 반환하고 첫
failure에서 consumer와 producer 모두 종료되는지 확인한다.
6. download `Content-Type`, `Content-Disposition`, CORS exposed header와
short-lived capability expiry를 확인한다. browser-managed resolver receipt가
caller의 branded receipt와 정확히 같고 resource ID, media type, safe
extension, server max, optional digest와 expiry를 synchronous하게 정확히
binding하는지 확인한다.
현재 save path는 whole-object foreground streaming이며 Range resume가 아니다.
partial destination에 append하지 않고, Range를 선택한 제품은 VD-14와
browser-transfer runbook의 별도 절차를 따른다.
7. whole-buffer API 또는 object URL lease 누수가 배포됐는지 확인한다.
8. image preview를 설치했다면 object URL 발급 전에 header dimensions, pixel,
decoded-byte와 animation/static-only 제한을 검사하는 VD-15 probe가 실제
조합됐는지 확인한다. 현재 byte/signature 검사만으로 decode safety를
주장하지 않는다.
9. upload를 별도 설치했다면 backend 401/403/409/413/415/422/429/5xx,
CORS/preflight, session TTL/clock skew, part checksum, idempotency, orphan
cleanup과 quarantine queue를 그 feature runbook에서 확인한다.
### 안전한 조치
- enhanced open/save picker off → native input/direct authorized download
- active-content preview off
- preview pixel/decode/frame probe가 없거나 실패하면 object URL preview off
- client-generated large download off → server-side artifact generation
- broken stream adapter off → bounded fallback 또는 operation 중지; raw exception을
성공/EOF로 변환하지 않음
- upload를 별도 설치했다면 multipart concurrency/part size 하향,
resumable off → approved hard cap의 simple upload, 문제 MIME 임시 차단
- expired upload session은 authorization 후 새 session; 기존 session을
재활성화하지 않음
성공 판정:
- user cancellation 제외 file/download start 대비 success rate 회복
- integrity/authorization mismatch 0
- 세 browser baseline smoke 통과
- object URL/file ref cleanup count 0 leak
- preview를 설치한 경우 malformed/oversize/animated image가 object URL 발급
전에 거절되고 native decode resource 0 leak
- upload를 별도 설치했다면 orphan session/quarantine backlog SLO와 session
cleanup 0 leak
## 4. IndexedDB upgrade blocked/versionchange
### 신호
- `BLOCKED`, `UPGRADE_BLOCKED` 또는 blocked duration bucket 증가
- `versionchange` 뒤 connection이 남음
- repeated reload/update loop
- open/maintenance의 `POLICY_REJECTED`: immutable dataset binding missing/mismatch
### 확인 순서
1. target schema와 current/future schema, release ID를 확인한다.
2. registry-issued `authorityToken`, `namespaceToken`, `partitionToken`이 같은
dataset의 승인된 값인지 확인한다. readable namespace/account/business ID를
token이나 physical DB name에 넣지 않는다.
3. physical DB name이 세 opaque token에서만 파생됐는지, caller
`databaseNameAssertion`이 exact match인지 확인한다.
4. governance store의 immutable scope + full `BrowserStoragePolicy` binding을
versionchange, post-open, maintenance 모두에서 확인한다. raw token이나 binding
내용을 incident log에 복사하지 않는다.
5. old tab, worker 또는 test/debug context가 connection을 닫지 않는지 확인한다.
6. 모든 connection에 `versionchange`/forced-close handler가 설치됐는지 확인한다.
7. BroadcastChannel prepare hint는 참고만 하고 실제 open/connection registry를
확인한다.
8. N-1 bundle이 future schema를 read-only/online-only로 처리하는지 확인한다.
### 안전한 조치
- 신규 offline write와 background migration 중지
- 사용자에게 다른 tab close와 명시적 retry UI 제공
- current connection을 draining 후 close
- upgrade release rollout 중지 또는 compatible N-1 online-only로 rollback
- binding mismatch를 caller DB 이름 변경, policy 덮어쓰기 또는 DB 전체 삭제로
우회하지 않고 registry/composition 오류를 forward fix
remote tab 강제 종료, database 삭제, 무한 reload는 금지한다.
성공 판정:
- connection registry와 blocked request 0
- fresh tab과 two-tab versionchange/blocked smoke 통과
- future schema에서 N-1이 destructive write 없이 기동
- open과 maintenance에서 동일 scope/policy binding 검증 통과
## 5. IndexedDB migration/corruption
### 신호
- `MIGRATION_FAILED`, unknown codec, schema postcondition failure
- `CORRUPT_DATA`, `NOT_READABLE`, forced close
- transaction request success 후 commit failure
- `LIMIT_EXCEEDED`: dataset logical byte/receipt budget 초과
- lifecycle batch가 proof 없이 실행되거나 TTL/UNTIL_SYNCED record가 잘못 노출·삭제
- migration/lifecycle batch가 500 rows 또는 30,000ms를 넘거나 checkpoint 없이 중단
### 확인 순서
1. DDL schema version과 record codec version을 구분한다.
2. migration ID, last safe keyset checkpoint, revision fence,
`budgetExhausted`, processed/remaining bucket과 old-writer drain proof를
확인한다.
3. caller budget과 구현 absolute budget(500 rows/30,000ms), async operation 사이
deadline check를 확인한다.
4. codec `measureStoredBytes`, retention sidecar `measuredBytes`,
`dataset-budget.usedBytes/receiptCount`가 CAS/remove/lifecycle/migration
transaction과 함께 갱신됐는지 확인한다. 이 값은 native physical size가 아니다.
5. actual `StoredRecord``key/codecVersion/revision/payload`만 포함하고
`writtenAtEpochMs`, synchronization, `measuredBytes`, `eligibleAtEpochMs`
`retentionStore` sidecar에 분리돼 있는지 확인한다.
6. receipt retention이 31일 이하인지, configured `maxIdempotencyReceipts`
1,000,000 이하인지, prune/purge가 receipt count를 같은 transaction에서
감소시켰는지 확인한다.
7. TTL record가 sweep 전 read에서 `EXPIRED_RESOURCE`, query에서 skip되는지,
`UNTIL_SYNCED``CONFIRMED`만 eligible한지 확인한다.
8. lifecycle action마다 composition의
`authorizeLifecycle(action, scope, frozen policy)`가 호출됐고 opaque proof가
검증 후 비영속·비관측 상태로 폐기됐는지 확인한다.
9. historical schema fixture에서 같은 migration을 재현한다.
10. reconstructable, synced copy, unsynced/user-authored 분류를 확인한다.
11. partial batch가 row/sidecar/budget/checkpoint와 원자적으로 rollback됐는지
확인한다.
12. full partition purge가 record/retention/idempotency와 등록된 모든
`lifecycleMetadataStores`를 bounded transaction으로 정리하고 immutable
governance binding은 유지하는지 확인한다.
13. raw record, opaque scope/proof를 ticket/log에 복사하지 않고 승인된 local
recovery tooling만
사용한다.
### 안전한 조치
- migration 중지, repository read-only/online-only
- reconstructable만 purge/re-fetch
- synced copy는 revision 확인 후 rehydrate
- unsynced/user-authored는 quarantine marker + export/sync/recovery
- adapter bounded reopen 최대 한 번
- schema/codec compatible fix를 새 forward migration으로 배포
- migration은 old-writer drain authority를 복구한 뒤 더 작은 bounded batch로
checkpoint부터 resume
- lifecycle authority가 불명확하면 deletion을 재시도하지 않고 read-only;
proof를 운영자가 수동 생성·재사용하지 않음
database 전체 삭제는 data owner가 export/recovery와 영향 scope를 승인한 별도
action이다.
성공 판정:
- historical fixture의 fresh, interrupted, resume, replay가 모두 통과
- corrupted user-authored row 자동 삭제 0
- migration canary failure 0
- N-1 rollback/read-only smoke 통과
- dataset budget/sidecar/receipt count 재검증과 bounded lifecycle fixture 통과
## 6. Origin quota, persistence denial, storage eviction
### 신호
- usage ratio pressure bucket 증가
- `QUOTA_EXCEEDED`, `STORAGE_EVICTED`
- startup sentinel과 logical manifest 불일치
- persistence request denied
### 확인 순서
1. `estimate()`가 rough origin total임을 전제로 IndexedDB/OPFS/Cache 공동 증가를
확인한다.
2. exact free space를 계산하거나 예약했다고 가정하지 않는다.
3. expired/reconstructable, synced copy, user-authored 사용량 bucket을 분리한다.
4. IndexedDB의 exact logical dataset budget과 origin-wide `estimate()`
구분한다. 전자는 codec measurement + conservative reservation이고 native
physical usage/free space가 아니다.
5. private mode, WebView, browser storage policy와 user clear 여부를 확인한다.
6. persistence denial을 capability failure가 아닌 best-effort outcome으로 처리했는지
확인한다.
### 안전한 조치
현재 공통 coordinator가 없으면 다음 순서를 자동 실행하거나 “exact one retry”를
보장한다고 기록하지 않는다. 제품 owner가 store별 primitive와 retention policy를
확인해 수동/feature-local로 안전하게 수행하거나 write를 read-only/online-only로
닫는다. VD-15 coordinator가 조합된 경우에만 동일 mutation owner 안에서 다음을
bounded orchestration으로 실행한다.
1. 신규 speculative/reconstructable write admission 중지
2. incomplete candidate와 stale staging 제거
3. expired reconstructable record/object 제거
4. grace가 지난 unreferenced immutable chunk 제거
5. inactive public cache release 제거
6. authoritative revision/sync receipt가 확인된 synced copy compact
7. unsynced/user-authored sync/export UI
8. 필요하면 offline read-only/online-only
각 GC invocation은 기본 100 items/5초, 구현 절대 상한 500 items/30초와 opaque
cursor를 지킨다. failed write의 자동 retry는 다음 조건이 모두 맞을 때만 정확히
한 번 허용한다.
- 첫 write가 실제 quota failure로 원자적 rollback됨
- 같은 idempotency key, revision fence와 payload digest
- 외부 side effect/cross-store publish가 commit되지 않음
- bounded GC가 실제 candidate를 제거했거나 pressure가 내려감
- 현재 revision/generation 재확인과 새 admission token 발급
두 번째 failure나 effect certainty가 unknown이면 retry하지 않는다.
`persist()`를 boot나 반복 loop에서 요청하지 않는다. 사용자가 durable offline
기능을 선택하고 unsynced data 보호 이유를 이해하는 동작에서만 요청한다.
성공 판정:
- coordinator를 설치한 경우 pressure가 hysteresis lower bound 아래
- partial logical/physical marker mismatch는 rehydrate 또는 explicit
read-only/export-required로 닫힘
- 모든 local marker가 소실된 경우 backend opaque installation epoch로
authorization 후 구분하거나 `storage-reset-possible` ambiguity UX를 표시함;
이를 “첫 설치로 판별 완료”라고 기록하지 않음
- user-authored 자동 purge 0
- storage clear 뒤 reconstructable rehydrate/explicit degraded UI 통과
## 7. OPFS partial write/corruption/worker crash
현재 capability/property probe와 native conformance test는 존재하지만 composition
readiness에서 worker/lock/journal/small write-read-delete-cleanup을 한 번에
검증하는 real preflight는 아직 없다. preflight success를 운영 전제에 넣으려면
VD-15 목표 runtime을 먼저 구현한다.
### 신호
- journal이 `PREPARING`/`FILES_READY`에 오래 머묾
- committed logical row와 physical manifest/file 불일치
- digest/length mismatch
- worker crash, handle lock timeout, `NoModificationAllowedError`
- sensitive policy maintenance의 `POLICY_REJECTED`, proof replay/expiry 또는
authority provider/consumer unavailable
### 확인 순서
1. mutation Web Lock owner와 timeout을 확인한다.
2. worker protocol/version과 모든 sync handle의 `finally close`를 확인한다.
3. journal operation phase별 count와 age bucket을 확인한다.
4. VD-15 forward migrator를 설치하지 않은 현재 v1 reference runtime에서는
physical layout이
`/ca-frontend-opfs-v1/authorities/<authorityToken>/<namespaceToken>/<partitionToken>/`
아래의 `objects`, `chunks/sha256`, `staging`인지 확인한다. readable namespace,
filename, account ID가 path segment이면 신규 write를 중지한다. migrator를
설치한 조합에는 이 v1 path를 authoritative target 조건으로 적용하지 않는다.
5. journal의 physical scope binding과 logical namespace binding이 둘 다 존재하고,
같은 scope/policy fingerprint를 가리키는지 확인한다. 한쪽만 없거나 mismatch면
자동 재생성하지 않는다.
6. runtime/byte-store composition의 frozen scope와 policy namespace가 일치하는지
확인한다.
7. `LOGOUT`, `UNTIL_SYNCED`, `ACCOUNT_DELETION` maintenance에는 composition의
`requestMaintenanceAuthority` provider와 `consumeMaintenanceAuthority`
consumer가 둘 다 주입됐는지 확인한다. application request에는 proof 필드가
없어야 한다.
8. provider가 exact reason/frozen scope/frozen policy에 묶인 새 proof와 최대
5분 expiry를 발급하고, consumer가 같은 binding을 원자적으로 검증·consume해
replay를 거절하는지 확인한다. proof/token/raw scope는 log에 복사하지 않는다.
9. physical path/ID/digest를 log에 복사하지 않고 reconciliation tool이
manifest schema와 digest를 검증하게 한다.
10. logical row가 commit authority인지 확인한다.
11. VD-15 forward migrator를 설치했다면 source/target physical layout와 journal
version, migration checkpoint, copy-on-write generation, publish authority,
rollback window와 N-1 reader 결과를 확인한다.
### recovery
- `PREPARING`: partial staging 검증 후 idempotent resume 또는 purge
- `FILES_READY`: expected generation/digest 일치 시 logical commit, 아니면
quarantine
- `COMMITTED` + file missing/corrupt: reconstructable만 server rehydrate;
user-created private는 read-only/export/recovery
- stale staging/orphan chunk는 grace period와 bounded mark/sweep 후 제거
- lock/worker 문제면 OPFS write off → approved capped fallback 또는 online-only
- scope binding mismatch면 affected scope를 read-only로 격리하고 registry/
composition을 forward fix; 다른 token path로 bytes를 이동하거나 추측 복구 금지
- sensitive maintenance authority provider/consumer가 없거나 replay/expiry
검증이 실패하면 삭제를 재시도하지 않고 read-only로 전환한다. 운영자가 proof를
직접 생성·주입·재사용하지 않는다.
- forward migration 중단이면 target generation을 publish하지 않고 bounded
checkpoint에서 resume한다. publish 뒤에는 source generation을 rollback
window까지 보존하고, N-1이 target을 이해하지 못하면 read-only/online-only로
닫는다. source/target을 in-place 혼합하거나 layout version을 내리지 않는다.
committed object를 sync handle로 in-place repair하지 않는다. 새 generation에
정상 object를 만들고 generation CAS로 logical pointer를 전환한다.
성공 판정:
- unresolved old journal 0
- committed read integrity failure 0
- worker/handle registry 0 leak
- 각 journal phase fault injection과 concurrent put/delete/GC 통과
- sensitive maintenance의 fresh proof consume 성공, replay/expired/mismatched
proof와 provider/consumer 누락은 모두 deletion 전 fail-closed
## 8. Cache Storage/Service Worker stale or poisoned release
public static Cache release runtime은 존재하지만 cursor/count/deadline이 있는
bounded inspect/cleanup과 Service Worker waiting/client-drain controller는 아직
없다. Service Worker는 제품이 PWA/offline fetch를 별도로 선택한 경우에만 아래
worker 절차를 적용한다.
### 신호
- incomplete candidate activation
- integrity/type/size mismatch
- auth/private/no-store/opaque policy rejection
- Service Worker를 별도 선택한 조합의 stale worker/update/reload loop
- active asset miss 또는 offline boot failure
### 확인 순서
1. hosting/CDN cache와 browser Cache Storage를 별개로 확인한다.
2. active/candidate/previous release ID와 manifest digest를 확인한다.
3. candidate 모든 entry의 status, `expectedContentType`, size, integrity 검증
marker를 확인한다. canonical manifest digest가 정규화된 Content-Type까지
binding하는지 확인한다.
4. 실제 response의 정규화된 `Content-Type`과 manifest
`expectedContentType`이 정확히 일치하는지 확인한다. body digest가 맞아도 type
mismatch candidate는 폐기한다.
5. config/release manifest/auth/API가 network-only인지 확인한다.
6. query/Vary exact match와 `ignoreSearch/ignoreVary` 미사용을 확인한다.
7. Service Worker를 별도 선택했다면 old controlled clients와 current
waiting/active worker 상태를 확인한다.
8. Service Worker를 별도 선택했다면 unregister만 하고 owned cache cleanup을
빠뜨리지 않았는지 확인한다.
9. cleanup caller가 cache name, release registry ID 또는 retain list를 제출할 수
없고 보존 집합이 verified active pointer와 composition retention에서만
계산되는지 확인한다.
10. active pointer/release marker control JSON이 정확히 2 MiB(2,097,152 bytes)
cap의 stream reader와 strict UTF-8/runtime schema를 통과하며 oversized body를
초과 지점에서 cancel하는지 확인한다.
### 안전한 조치
static Cache-only 조합:
- 신규 candidate activation/cache write 중지
- verified current 또는 previous release로 explicit rollback
- incomplete candidate와 runtime-public cache부터 owned-prefix cleanup
- 현재 cleanup/inspect가 cache 개수에 대해 unbounded임을 고려해 incident
invocation을 추가 budget으로 반복 실행하지 않는다. VD-15 bounded cursor
runtime 구현 전에는 큰 namespace를 자동 sweep하지 않는다.
Service Worker를 별도 선택한 조합에서만 추가:
- fetch interception을 network-only kill switch로 전환
- new worker activation 중지
- 필요하면 unregister + 다음 navigation cleanup migration
- old controlled client가 drain되기 전 해당 release cleanup 금지
hosting/CDN purge는 두 branch와 분리된 provider 절차로 실행한다.
auth/private response가 cache에 실제 저장됐을 가능성이 있으면 P1로 승격하고
owned affected namespace를 정확히 식별해 제거한다. origin의 unrelated cache는
삭제하지 않는다.
성공 판정:
- static Cache-only 조합은 verified active/previous release와 candidate cleanup이
일관되고 network-only fallback smoke를 통과
- Service Worker를 별도 선택한 조합만 clean/old controlled client가 같은
verified release로 수렴하고 online/offline/update/rollback smoke를 통과
- auth/private cache entry 0
- update/reload loop 0
- config/release manifest network-only header contract 통과
## 9. Drill evidence
실제 capability를 project catalog의 `INSTALLED`로 바꾸기 전에 최소 다음 drill을
실행하고 release-specific evidence를 저장한다. 이 legacy selection label은
primary status `COMPOSED`와 별도
`TrafficAdmission/RuntimeHealth/PromotionEvidence` 축이 준비된 제품 상태를
뜻하며 새로운 primary status literal이 아니다. 각 축의 literal과
contract/provider/browser/operations component gate에서 `PromotionEvidence`
계산하는 규칙은 completion ledger를 그대로 따른다.
| drill | 필수 증적 |
| --- | --- |
| 별도 upload workflow를 설치한 경우 session abort/expiry/checksum | orphan cleanup, quarantine state, retry/idempotency |
| large save abort/integrity | partial commit 0, memory high-water |
| IndexedDB two-tab upgrade blocked | close/retry UX, connection leak 0 |
| IndexedDB governance/lifecycle | opaque scope binding, logical budget/TTL, proof discard, receipt cap |
| interrupted historical migration | old-writer drain proof, 500 rows/30s budget, checkpoint resume, atomicity, N-1 fallback |
| quota/persistence denied/storage clear | reconstructable GC, user data 보존, degraded UI |
| VD-15 coordinator를 설치한 경우 pressure/one-retry | hysteresis, bounded cursor/deadline, store priority와 retry 최대 1회 |
| OPFS journal phase crash/scope mismatch/maintenance authority | bidirectional binding, actual opaque layout, invisible partial object, reconcile summary, integrity, provider/consumer one-time proof |
| OPFS preflight/migration을 설치한 경우 | worker/lock/journal/small operation cleanup, interrupted copy-on-write와 N-1 fallback |
| cache candidate poison/update rollback | expectedContentType binding, active release 불변, previous rollback, caller retain 부재, 2 MiB control cap, auth rejection |
| bounded Cache/SW lifecycle을 설치한 경우 | cursor/count/deadline, waiting activation, controlled-client drain과 network-only rollback |
| preview probe를 설치한 경우 | pixel/decoded-byte/static-only rejection, timeout와 decode resource cleanup |
evidence에는 raw 사용자 data를 포함하지 않는다. 허용 metadata는 release/build,
browser engine/version, fixture ID, fault phase, bounded counts/buckets와 PASS/FAIL
결과뿐이다.
promotion 직전에는 다음 repository evidence도 함께 보존한다.
- `test:browser-capabilities`가 만든 JUnit에서 Chromium, Firefox, WebKit이 동일
14개 testcase set(File 2, IndexedDB 4, OPFS/Cache/StorageManager 각 1,
cross-context invalidation 2, presigned streaming download/multipart
upload/Image CDN 각 1)을 실제 실행해 총 42개이며 failure/error/skipped가 모두
0이어야 한다.
`verify:browser-capability-evidence`가 engine 집합과 testcase 동일성을
기계적으로 검증한다. 현재 artifact는 Chromium/Firefox 14개씩 총 28개가
통과했지만 WebKit 실행에 필요한 native libraries(예:
`libbacktrace.so.0`, `libevent-2.1.so.7`, `libjxl.so.0.8`,
`libavif.so.16`과 WPE 계열)가 이 host에 없으므로 아직 promotion 가능 상태가
아니다.
- `artifacts/quality/vite-module-inventory.json`에서 optional runtime source root가
production chunk에 없음을 `check:optional-recipes`로 검증한다.
- `check:browser-file-storage-boundaries`로 native API 경계 위반을 막고,
`test:browser-file-storage-removal`로 runtime과 전용 gate를 제거한 격리 copy가
base typecheck/lint/architecture/test/build/catalog/CI contract를 통과함을
검증한다.
- catalog의 `referenceRuntime.status``AVAILABLE_NOT_COMPOSED`,
`productionComposition``false`로 유지한다. dataset owner가 모든 policy와
evidence를 승인하고 실제 composition을 추가하기 전에는 `INSTALLED`
해석하지 않는다.
## 10. 관련 문서
- [Browser data capability completion ledger](../architecture/browser-data-capability-completion-ledger.md)
- [Browser file and origin-storage platform](../architecture/browser-file-and-origin-storage.md)
- [VD-11 Browser file and origin-storage 경계](../architecture/decisions/VD-11-browser-file-and-origin-storage.md)
- [VD-15 Origin storage lifecycle and migration](../architecture/decisions/VD-15-origin-storage-lifecycle-and-migration.md)
- [VD-14 Resumable download와 background download](../architecture/decisions/VD-14-resumable-download-and-background-transfer.md)
- [Browser transfer recovery](./browser-transfer-recovery.md)
@@ -0,0 +1,320 @@
# Browser transfer and Image CDN recovery
이 runbook은 presigned capability, multipart/resumable upload, streaming download와
Image CDN reference runtime을 제품에 조합한 경우에만 적용한다. skeleton의 기본
bootstrap은 이 runtime을 실행하지 않는다.
현재 Range resumable download와 top-level transfer/Image descriptor composition은
각각 `DESIGNED_NOT_IMPLEMENTED`다. app-managed background download와 upload는
각각 `NOT_SELECTED`, 그 cross-browser guarantee는 `PLATFORM_LIMITED`다. 아래에서
이 기능을 다루는 절차는 VD-14/VD-16 구현과 제품 조합이 완료된 뒤에만
활성화한다. 기존 whole-object streaming, foreground upload resume나
browser-managed handoff를 그 기능의 운영 증거로 사용하지 않는다.
## 1. 관측 신호
허용된 aggregate 신호:
- capability issue/claim의 success, expired, policy-rejected bucket
- upload session create/resume/reconcile/complete/abort outcome
- part size/count/concurrency와 retry bucket
- download expected/actual byte bucket과 truncated/overrun/integrity outcome
- Image CDN preset/format/width/candidate bucket과 policy rejection
- session age, orphan count, quarantine backlog와 promotion latency
금지:
- presigned URL, query, signed request/response header
- resource/session/asset/account ID와 object key
- file name, local path, raw ETag, checksum, receipt/token의 실제 값
strict upload checkpoint의 protocol-defined SHA-256 fingerprint/part checksum과
bounded opaque non-authorizing part receipt는 server reconcile을 위한 durable
예외다. account partition/retention 안에서만 보존하고 관측 신호나 ticket에는
절대 투영하지 않는다.
- raw backend/body/DOMException message와 stack
## 2. 초기 분류
| 증상 | 우선 확인 |
| --- | --- |
| capability expiry/403 급증 | BFF/object-store clock, signer credential rotation, expiry ceiling |
| CORS/preflight 실패 | exact origin, method, signed headers, exposed receipt/checksum headers |
| redirect/network failure | proxy/CDN redirect 변경, `redirect:error`, URL origin/path policy |
| 특정 part 반복 실패 | exact offset/length/checksum/idempotency, capability expiry |
| resume conflict | server status와 checkpoint revision/part binding |
| status 404/410 반복 | terminal checkpoint 제거와 새 session 생성 여부 |
| complete 뒤 file 불일치 | ordered parts, checksum type, ETag 오해, scan/promotion |
| download truncation/overrun | capability length, content encoding, proxy buffering/transform |
| destination partial save | picker writable abort/close, integrity-before-close |
| image policy rejection 급증 | asset revision, preset registry, dimensions, format/CDN origin |
| stale/broken private image | lazy load 시점과 signed URL minimum lifetime |
| image timeout/decode 실패 급증 | encoded cap, pre-decode metadata, decoded-byte ceiling, probe deadline |
| CDN cache poison 의심 | immutable revision, cache key, `Vary`, source asset registry |
## 3. Presigned capability
1. 신규 capability 발급을 중지할 kill switch가 동작하는지 확인한다.
2. BFF와 object storage의 clock skew와 signer credential lifetime을 확인한다.
3. whole-object control plane이 versioned target을 구현한 제품이면 request,
response, vault와 executor의 `PRESIGNED_TRANSFER_V1` exact match 및
unknown/missing version rejection을 확인한다. 현재 reference wire에 이
target이 아직 없으면 구현되지 않은 protocol을 통과했다고 기록하지 않는다.
4. capability가 method, object/part, content constraints와 expiry를 server-side로
강제하는지 확인한다.
5. upload part 발급 BFF가 `sessionId`로 server session을 조회하고
`PRESIGNED_MULTIPART_V1` canonical request/upload binding과 part plan을
재계산하는지, `UPLOAD_PART` capability binding에도 exact protocol을 넣는지
확인한다. client digest 일치만으로 authorization하지 않는다.
6. redirect가 추가되었으면 allowlist를 넓히지 말고 direct endpoint를 복구한다.
7. URL/query를 ticket 또는 log에 복사하지 않는다. 노출 가능성이 있으면 signer
credential/policy와 해당 capability scope를 폐기한다.
8. 만료 capability를 retry하지 않고 control plane에서 새 capability를 발급한다.
9. client claim은 server replay authority가 아니므로 중복 사용 여부는 server
access/audit의 안전한 aggregate로 판단한다.
## 4. Multipart/resumable upload
### 4.1 안전한 일시 중지
현재 runtime에는 공식 `pause()`와 checkpoint inventory/retention sweep이 없다.
caller abort 뒤 checkpoint가 남아 재개될 수 있다는 사실을 `PAUSED` 완료로
기록하지 않는다. 다음 절차의 `PAUSED` state와 탭 간 pause는 VD-16 목표
runtime이 구현된 제품에만 적용한다.
1. 신규 session create를 중지한다.
2. active part는 사용자가 취소하지 않았다면 현재 bounded attempt만 마무리한다.
3. complete를 중지하고 server status reconcile만 허용한다.
4. presigned URL은 checkpoint에 쓰지 않고 즉시 메모리에서 폐기한다.
5. 현재 runtime은 checkpoint를 `ACTIVE` 또는 `ABORT_PENDING`으로 유지한다.
목표 pause runtime은 exact scope/generation을 확인해
`PAUSE_REQUESTED -> PAUSED`로 전환한다.
6. 목표 pause runtime은 cross-context
`RESUMABLE_UPLOAD_PAUSE_V1`, checkpoint `schemaVersion: 2`
`ACTIVE | PAUSED | ABORT_PENDING` closed state를 사용한다. v1 old writer
drain, historical migration과 N-1 fail-closed를 확인한다.
7. 목표 runtime의 inventory/sweep는 count/cursor/deadline 안에서 abandoned와
expired 후보만 반환하고, server reconcile/CAS 없이 checkpoint를 삭제하지
않는다.
### 4.2 Resume
1. session/checkpoint/control DTO의 protocol이 모두
`PRESIGNED_MULTIPART_V1`인지 확인하고, session expiry와 source
binding/total bytes/part layout을 검사한다.
2. server-authoritative status/list-parts를 읽는다.
3. 완료 part의 local range SHA-256을 다시 계산해 exact checksum/receipt와 대조한다.
4. checkpoint-only part와 server-only part를 자동 complete하지 않는다.
5. 불일치는 session을 격리하고 새 session/명시적 abort를 선택한다.
6. missing part만 동일 bytes/checksum/idempotency binding으로 전송한다.
7. complete 직전에 1부터 연속적인 ordered receipt set을 다시 확인한다.
8. status HTTP 404/410 또는 `NOT_FOUND`/`EXPIRED`이면 stale checkpoint를 CAS
제거하고 새 session으로 restart한다. 같은 terminal session을 반복 조회하지
않는다.
9. network/429/모든 5xx retry가 attempt count, `Retry-After`, backoff와
operation timeout ceiling 안인지 확인한다.
### 4.3 PUT acknowledgement
1. capability가 expected success status, receipt response header와
`expectedResponseByteLength`를 exact하게 묶는지 확인한다.
2. 실제 `Content-Length`가 expected bytes와 같고 hard response cap 이하인지
확인한다. 204는 expected bytes가 0이어야 하며 header 부재를 0으로 정규화한다.
3. response body EOF까지 동일 deadline 안에서 bounded drain된 뒤 receipt가
accepted됐는지 확인한다.
4. receipt/ETag는 part acknowledgement일 뿐 whole-file digest나 authorization
proof로 해석하지 않는다.
### 4.4 Abort/orphan
- 사용자 cancel은 browser work cancel이며 server abort가 아니다.
- 다른 tab에서 명시적 abort하면 `RESUMABLE_UPLOAD_CANCEL_V1`의 opaque
upload-key-only BroadcastChannel 신호가 active read/fetch/backoff를 먼저
중단하고 Web Lock이 bounded 시간 안에 반환되는지 확인한다.
- BroadcastChannel 미지원/정책 차단 환경에서는 abort caller deadline 안에
lock을 얻지 못하면 성공으로 가장하지 않고 `ABORT_PENDING` 또는 safe
non-terminal failure로 닫은 뒤 server reconcile을 재시도한다.
- abort 결과가 불명확하면 checkpoint를 제거하지 않고 `ABORT_PENDING`으로 남긴다.
- 다음 invocation에서 server status가 `ABORTED/EXPIRED/NOT_FOUND`임을 확인한 뒤
checkpoint를 제거한다.
- abort endpoint의 HTTP 404/410은 terminal orphan으로 mapping하고 checkpoint를
제거한다.
- backend TTL cleanup은 비용과 보안 경계다. orphan upload count/bytes/age SLO를
운영한다.
- multipart complete는 `QUARANTINED`; scanner/CDR와 domain metadata commit이
끝나기 전 public delivery capability를 발급하지 않는다.
- application-facing success/log에는 session ID, request binding 또는 file
fingerprint를 노출하지 않는다.
## 5. Streaming download
1. capability media/length/digest와 실제 response header/body count를 비교한다.
2. `Content-Encoding`이 capability byte semantics와 다른지 확인한다.
3. overrun/truncation/integrity failure면 reader와 destination writable을 abort한다.
4. partial local destination을 성공으로 보고하지 않는다.
5. same resource를 새 capability와 새 destination으로 restart한다.
6. Range resume가 별도 승인되지 않았으면 기존 partial destination에 append하지
않는다.
7. anchor/navigation 결과는 browser handoff일 뿐 saved/verified 증거가 아니다.
### 5.1 Range resumable download
이 절차는 `RANGE_RESUMABLE_DOWNLOAD_V1` port/runtime, seekable destination 또는
owned OPFS staging과 provider contract가 구현·조합된 경우에만 적용한다.
1. checkpoint의 protocol, opaque account partition, representation binding,
total bytes, next offset, destination binding, revision과 retention을
검증한다. raw URL, signed header, validator, file path는 checkpoint나
incident ticket에 없어야 한다.
2. destination의 실제 durable length가 checkpoint offset과 다르면 append하지
않는다. 더 긴 uncommitted tail은 정책과 destination capability가 허용할 때만
`truncate()`하고, 더 짧으면 마지막 confirmed segment로 rollback하거나
새 staging으로 재시작한다.
3. 새 capability가 같은 immutable generation/strong validator, total length,
media type와 whole-object digest에 묶였는지 확인한다. capability의
`preconditionMode`, `allowWholeObjectFallback`과 exact `allowedStatuses`
확인하며 허용되지 않은 status에서는 body나 destination을 소비하지 않는다.
4. 허용된 `206`은 exact `Content-Range: bytes S-E/T`, `S=requestedStart`,
`E+1=requestedEndExclusive`, `Content-Length=E-S+1`, identity encoding과
capability precondition binding이 모두 맞을 때만 segment를 쓴다.
5. 허용된 full `200`은 response validator/generation evidence가 capability
representation binding과 같을 때만 평가한다. mismatch면 body를 쓰지 않고
staging/checkpoint를 quarantine한 뒤 새 representation으로 restart한다.
binding이 같아도 start가 0이고 whole-object fallback이 허용된 경우만 fresh
destination에서 consume하며, 그 밖에는 body를 쓰지 않고 Range reissue,
byte-0 restart, handoff 또는 unsupported로 닫는다. 일반 `If-Range` mismatch의
표준 응답은 full `200`이지 `412`가 아니다.
6. 허용된 `412`는 별도 `If-Match` 또는 provider generation precondition을 쓴
계약에서만 representation replacement로 처리한다.
7. 허용된 `416`은 checkpoint 완료 증거가 아니다. body를 data로 소비하지 않고,
final URL/header와 mode별 validator/generation binding을 먼저 검증한다.
provider response만으로 binding을 증명할 수 없으면 BFF reconcile이 exact
immutable generation을 증명해야 한다. 그 뒤 server total이 expected total과
같은지, `nextOffset <= total`인지, destination length와 offset이 일치하는지를
순서대로 reconcile한다. length mismatch는 먼저 truncate/verified rollback
또는 quarantine하고 같은 invocation에서 fall through하지 않는다.
`length == nextOffset == total`과 처음부터 다시 계산한 whole-object SHA-256이
모두 일치할 때만 final commit 후보가 된다. `length == nextOffset < total`이면
exact missing range를 재발급하고 반복 `416`은 contract mismatch로 닫는다.
8. capability expiry는 같은 representation binding으로 재발급한다. binding이
달라지면 기존 partial에 이어 쓰지 않는다.
9. segment flush와 destination length 재확인 뒤에만 checkpoint CAS를 전진한다.
10. final whole-object SHA-256과 destination close/commit 뒤에만
`SAVED_VERIFIED`로 기록한다.
malformed `Content-Range`, encoded representation drift, validator mismatch,
seek/truncate 실패 또는 crash 뒤 ambiguous tail에서는 신규 Range traffic을
중지하고 whole-object restart, browser-managed handoff 또는 명시적 unsupported로
degrade한다.
### 5.2 Background download와 browser-managed handoff
- `BROWSER_HANDOFF`는 브라우저 download manager가 이후 작업을 소유한다는
outcome이며 application progress, retry, destination integrity를 증명하지 않는다.
- page abort/reload 뒤 checkpoint가 남는 것은 app-managed background download 실행
증거가 아니다.
- app-managed background download를 선택한 제품만 별도 worker control plane, owned OPFS
staging, runtime/version/logout fence와 foreground export 절차를 운영한다.
- worker가 종료되거나 capability가 만료되면 checkpoint로 foreground recovery를
시도하며 장시간 keepalive를 가정하지 않는다.
- 미지원 Safari/Firefox/WebView에서는 browser-managed handoff 또는 명시적
unsupported UX를 사용한다. unbounded Blob fallback은 금지한다.
## 6. Image CDN
1. 신규 private descriptor 발급 또는 영향 preset을 kill switch로 중지한다.
2. 안전한 placeholder/original-approved rendition으로 fallback한다.
3. asset revision과 CDN cache key, format, width/height metadata를 확인한다.
PNG/JPEG/WebP/AVIF header를 native decode 전에 파싱해 선언 dimensions,
pixel/decoded-byte budget과 static-only 조건을 통과했는지 확인한다.
versioned preset binding ID를 server registry에서 조회해 요청의
width/height/DPR/fit/format/quality 전체를 재계산하고 query mismatch를
거절하는지도 확인한다.
4. 같은 immutable URL의 content가 변경됐다면 purge만으로 봉합하지 않고 새
asset revision을 발급한다.
5. signed private URL이 노출됐으면 expiry를 기다리지 말고 backend asset/capability
scope를 revoke한다.
6. private descriptor가 `PRIMARY_REQUIRED` probe를 사용하고 실제 response에서
exact URL, credential omission과 `Cache-Control: no-store`를 확인했는지
검사한다.
CDN origin이 application origin과 분리되어 실제
`<img crossorigin="anonymous">` 요청에도 application cookie가 실리지
않는지 배포 설정을 함께 확인한다.
7. fetch header/body/decode 전체 timeout과 abort cleanup을 확인한다. 늦게
resolve한 response body/`ImageBitmap`이 즉시 cancel/close되는지 fault
injection으로 재현한다.
8. source-fetch SSRF가 의심되면 arbitrary source URL transform을 차단하고
quarantine을 통과한 asset registry ID만 허용한다.
9. dimensions/pixel/decoded-byte budget 위반은 CDN transform과 descriptor
양쪽을 중지한다.
10. 정상 signing key 회전은 verifier와 `acceptedKeyIds`에 old/new key를 먼저
함께 배포하고 client 채택을 확인한 뒤 backend signer를 전환한다. 기존
capability lifetime, clock skew와 client rollout 기간이 모두 지난 후에만
old key를 제거한다.
11. key 유출, logout, tenant/account partition 변경 또는 feature teardown이면
backend capability를 revoke하고 기존 Image CDN runtime을 `close()`해 진행
중 verification/probe와 기존 reference를 폐기한 뒤 새 runtime을 조합한다.
12. descriptor HTTP provider를 설치했다면 response가
`IMAGE_CDN_DESCRIPTOR_V1`, fixed endpoint, strict body/content-type/deadline과
현재 runtime generation을 만족하는지 확인한다.
13. refresh는 asset/preset/scope/generation별 single-flight인지, 기존 descriptor
expiry 뒤 stale-while-error를 허용하지 않는지 확인한다.
14. logout/account switch 뒤 늦게 끝난 descriptor fetch/signature
verification/decode가 새 runtime에 채택되지 않는지 확인한다.
15. safe picture primitive가 verified presentation descriptor를 그대로
projection할 뿐 URL/query/transform을 재조립하지 않는지 확인한다.
## 7. Kill switch
- direct object-storage transfer off → same-origin BFF proxy 또는 기능 중지
- concurrent multipart → sequential part
- resumable off → 승인된 small-file simple upload 또는 upload 중지
- complete off → active session 유지/reconcile only
- upload pause/resume off → 신규 part 중지, explicit reconcile only
- streaming save off → authorized browser handoff
- Range resume off → whole-object restart 또는 authorized browser handoff
- app-managed background download off → browser-managed handoff 또는 foreground only
- app-managed background upload off → foreground checkpoint resume 또는 upload 중지
- Image descriptor refresh off → fresh descriptor expiry까지만 사용 후 placeholder
- private Image CDN off → authenticated same-origin placeholder
- advanced formats off → approved JPEG/PNG preset
- responsive candidates off → 한 개의 bounded fallback rendition
kill switch는 hard byte/pixel/security ceiling을 늘리지 않는다.
## 8. 복구 완료 조건
- capability expiry/replay/CORS 실패율이 baseline으로 복귀
- 신규·resume upload의 part checksum과 ordered complete 증적 통과
- orphan TTL cleanup과 quarantine backlog가 SLO 안으로 복귀
- download truncation/overrun/integrity fault injection 통과
- Range를 선택한 경우 `200/206/412/416`, capability reissue, destination
seek/truncate/crash와 final whole-object integrity matrix 통과
- pause/inventory를 구현한 경우 abandoned checkpoint retention/reconcile drill 통과
- image preset/revision/cache/CSP matrix 통과
- Image descriptor provider를 설치한 경우 expiry/refresh/logout generation
fence와 safe projection 통과
- image pre-decode static metadata, private no-store와 timeout cleanup fault 통과
- Chromium/Firefox/WebKit 동일 case set, zero failure/skipped
- raw URL/query/signed header/bearer token/capability가 log, telemetry,
checkpoint에 없음을 확인
- checkpoint allowlist의 SHA-256 fingerprint/part checksum/opaque receipt가
account partition과 retention 안에만 있고 log/telemetry에는 없음을 확인
## 9. 제거
1. 신규 capability/session/descriptor 발급 중지
2. active upload complete 또는 explicit abort와 orphan cleanup
3. non-secret checkpoint를 scope에 맞게 purge
4. private capability revoke와 CDN grace window drain
5. feature transfer/image facade와 composition 제거
6. browser-transfer source와 전용 tests/catalog wiring 제거
7. runtime removal gate, production module inventory, 전체 test/build 재검증
## 10. 관련 문서
- [Browser data capability completion ledger](../architecture/browser-data-capability-completion-ledger.md)
- [Presigned transfer and Image CDN](../architecture/presigned-transfer-and-image-cdn.md)
- [VD-14 Resumable download와 background download](../architecture/decisions/VD-14-resumable-download-and-background-transfer.md)
- [VD-16 Browser transfer composition과 Image delivery](../architecture/decisions/VD-16-browser-transfer-composition-and-image-delivery.md)
- [Server file capability infrastructure](../architecture/server-file-capability-infrastructure.md)
+381
View File
@@ -0,0 +1,381 @@
# CI quality-gate orchestration
`config/ci/gates.json` is the executable registry for all 26 gates and the sole
workflow model. `.gitea/workflows/quality-gates.yml` is generated from that
strict v2 contract; it is not an independent configuration authority. Update
the registry, run `corepack pnpm generate:ci-workflow`, and commit the generated
bytes together. `corepack pnpm check:ci-workflow` rejects missing, stale, CRLF,
or otherwise byte-different output. `corepack pnpm check:ci` runs artifact-schema
checking, the typed contract checker, and this workflow drift check. The Gitea
adapter runs each gate as an independent matrix check with full fan-out and no
soft-fail wiring.
Contract loading validates every registered command entry against the
authoritative root package-script graph and lifecycle/install policy before the
runner enters its execution callback. Foreign cwd/workspace script dispatch,
argument-sensitive dispatchers, and ineffective lifecycle suppression therefore
fail preflight even when the later `check:ci` command would never run.
For npm, that root-only boundary also parses options following an explicit
`run`/`run-script` target or implicit `test`/`start`/`stop`/`restart` target:
`--workspace`, `-w`, `--workspaces`, and `--prefix` are foreign manifest scope
and are rejected before execution. The first literal `--` ends npm option
parsing, so later tokens are ordinary script arguments. pnpm and Yarn differ:
their options following the selected script name are forwarded to that script;
their cwd/workspace selectors are rejected only where the manager consumes
them before script selection.
The npm graph includes existing root-manifest `pre<script>` and `post<script>`
hooks around every explicit `run`/`run-script` dependency and implicit
`start`/`stop`/`restart`/`test` dependency. Hooks are omitted only when ordered
npm options establish an unambiguous effective `--ignore-scripts` value before
the first literal `--`; bare and explicitly true forms omit hooks, while false,
negative, contradictory, malformed, and post-delimiter forms keep traversal or
fail closed. Case-insensitive `npm_config_workspace`, `npm_config_workspaces`,
`npm_config_prefix`, `npm_config_userconfig`, and `npm_config_globalconfig`
assignments are rejected whenever the tokenized command invokes npm. Direct
assignments, static paths whose basename is `env`, exact `command`/`exec`
prefix chains, prior exports, and `set -a` assignments share one prefix grammar.
Only modeled non-scope `env -i`, `env -u`, `env --unset`, and `env --` forms are
allowed; cwd-changing or unknown options fail closed. Dynamic assignment names,
unmodeled environment mutation, npm `--userconfig`/`--globalconfig`, and
unquoted pathname expansion before the npm argument delimiter are rejected.
The gate runner checks the same inherited environment names before loading the
contract or entering the execution callback, so they cannot reach a child gate
process.
The dependency graph is:
```text
MERGE_READY
-> RELEASE_READY
-> PROD_PROMOTION_READY
-> FIELD_SLO_READY
DOCUMENTATION_READY (off-chain)
```
`check:architecture`는 dependency-cruiser 결과와 별도로 Babel parser/Node
resolver 기반 `staticImportGraph``artifacts/quality/dependency-report.json`
기록한다. TS/TSX의 static, dynamic, type, CommonJS 및 JSDoc import를 검사하고,
로컬 source import에는 실제 TypeScript 확장자를 요구한다. 실행 source나 로컬
specifier에 `.js/.jsx/.mjs/.cjs`가 있거나 unresolved dependency, parse failure,
error-severity layer violation, cycle 또는 지원하지 않는 architecture rule
shape가 하나라도 있으면 gate는 실패한다. resolver, unresolved import, layer
edge와 cycle regression fixture도 같은 명령에서 실행된다.
Gitea workflow는 run ID/attempt, checkout SHA와 ref에서 build/release
식별자를 만들고 최소 `contents: read` 권한만 요청한다. Gate runner는 `HEAD`
full commit ID와 commit timestamp를 한 번 읽어 `SOURCE_DATE_EPOCH`를 유도한
뒤 workflow의 SHA와 실제 checkout이 일치하는지 확인한다. `CI=true`에서 이
식별자 중 하나라도 없거나 불일치하면 build step 전에 fail-closed하며, build와
release manifest도 같은 build ID, commit SHA, release ID와 timestamp를 가져야
한다.
Release tier의 `immutable_build``dist`의 hidden `.vite` entries, raw
`pnpm-lock.yaml`, build/module inventory와 local release/security evidence를 한
번 생성해 run ID/attempt로 구분된 archive로 게시한다. Raw lock bytes의 SHA-256은
dependency inventory와 candidate manifest 양쪽의 `lockfileSha256`과 같아야 한다.
Producer는 게시 전에 manifest가 선언한 exact member set과 각 byte digest를
검증하고 archive SHA-256을 job output으로 고정한다. Vulnerability/provenance
provider job은 같은 archive를 각각 내려받아 output SHA-256과 exact member set을
확인한 뒤 checkout과 분리된 root에만 추출하고, `CANDIDATE_LOCKFILE_PATH`
`CANDIDATE_DIST_SHA256`를 외부 command에 전달한다. Provider command 실행 후에는
archive와 추출된 candidate를 다시 검증하고, strict provider JSON과 candidate
digest가 일치할 때만 report를 업로드한다. Promotion은 같은 archive와 두 report를
별도 경로로 내려받고 SHA/member 검증을 마친 뒤 격리된 root에 추출하여 local
evidence를 read-only로 다시 계산하고 Ed25519 signature/digest를 확인한다.
Promotion job에는 build/rebuild command가 없으며 검증한 archive 자체를 변경 없이
그대로 승격한다. Local evidence 관계 검증에 필요한 policy와 verifier source도
archive member로 고정되며, checkout 밖의 격리된 cwd에서도 archive path와 기대
digest만으로 candidate-internal check를 재계산하고 archived secret-scan
policy/rule/SARIF/zero-finding/digest 관계를 검증한다. Archive에 없는 checkout
source를 다시 scan했다고 주장하지 않는다.
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. Linux
runner에는 실행 가능한 `/usr/bin/bwrap`, `/usr/bin/prlimit`,
`/usr/bin/systemd-run`, `/usr/bin/systemctl`, `bwrap --size` 지원, active user
bus, systemd user manager 254 이상, unified cgroup v2와 delegated
memory/pids/CPU controller가 모두 필요하다. 실행 파일, user manager/version,
trust/archive/path 같은 host-side preflight 실패는 raw report 생성 전에 차단된다.
`bwrap --size` 수용 여부와 실제 delegated controller/limit 값은 owned raw inode를
만든 뒤 scope 안에서만 확정할 수 있으며, 이 단계의 실패는 해당 inode를
identity-bound cleanup하고 fail closed한다.
각 provider는 고유한 collected user scope에서 실행된다. Supervisor는 실행 전에
실제 cgroup membership과 `memory.max=1073741824`, `memory.swap.max=0`,
`pids.max=64`, `cpu.max="100000 100000"`을 확인한다. 내부 process에는 core 0,
file-size 8,388,607 bytes, open FD 64, CPU 1,200초 상한도 적용된다.
같은 UID 전체에 합산되는 `RLIMIT_NPROC`로 provider별 32개를 보장한다고 주장하지
않으며 aggregate PID authority는 cgroup `TasksMax=64`다.
Bubblewrap는 network namespace를 분리해 완전 offline으로 실행하고 workspace,
verified candidate, `.git` mask, `/tmp`, `/etc`, `/proc`, `/dev`를 read-only로
유지한다. Archive 검증·추출과 sandbox/trust preflight가 끝난 뒤 supervisor가
생성하고 inode를 고정한 정확한 raw report 파일 하나만 read-write bind된다.
주변 `untrusted` directory 전체는 writable이 아니다. 실행 전 또는 provider
실패 시, 그리고 성공적으로 sealed evidence를 게시한 뒤에도 supervisor가 소유한
inode만 atomic quarantine을 거쳐 제거하므로 빈 stale report 없이 재시도할 수 있다.
Provider command와 provider-prefixed environment는 bounded length-prefixed bwrap vector로
`systemd-run` stdin에 전달되어 supervisor/systemd/bwrap wrapper argv나 unit
metadata에 노출되지 않는다. 단, 최종 provider executable의 일반 argv는 같은
UID의 process inspection에 보일 수 있으므로 command 문자열과 인자에 token,
password, private-key material을 넣으면 안 된다. Credential은 반드시 해당 종류의
`VULNERABILITY_PROVIDER_*` 또는 `PROVENANCE_PROVIDER_*` environment로만 전달하고
`*_COMMAND`에는 넣지 않는다. 정상 provider 종료까지 같은 stdin을 parent-liveness
pipe로 열어 두며 supervisor hard death의 EOF를 받은 in-scope wrapper는 provider
process group 전체를 종료하고 dev/inode가 일치하는 raw report만 정리한다.
별도의 trusted guardian child는 provider scope 밖에서 filesystem transaction 전체를
소유한다. Client는 spawn 전에 canonical raw/evidence directory를
`O_DIRECTORY|O_NOFOLLOW`로 열고 identity를 확인한 뒤 provider kind와 nonce에서
canonical raw/final 및 nonce-private raw-staging/sealed-temp exact leaf를 확정한다.
Client가 두 private file을 `O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW` mode `0600`으로
미리 할당하고 dev/inode를 기록한다. Directory descriptor는 guardian fd 3/4,
private file descriptor는 fd 5/6으로 상속되며 guardian argv에는 Node와 helper
경로만 있다. 최초 canonical v2 frame에는 provider kind, absolute deadline,
32-byte nonce만 전달한다.
Guardian bootstrap은 procfs link를 읽기 전에 fd 5/6을 fstat한다. Procfs pathname은
canonical directory의 exact direct-child grammar를 만족하고 descriptor-relative
lstat이 이미 확보한 fd identity/type/mode/size/link count와 일치할 때만 해당
identity의 cleanup alias가 된다. Guard frame 검증 뒤 raw staging을 fixed raw leaf에
no-overwrite hard link하고 두 alias의 link count 2를 확인한 다음 private alias를
제거하고 raw directory를 sync한다. Canonical raw가 같은 identity와 link count 1로
남은 뒤에만 READY를 응답한다. READY 전 종료 시 client는 pre-spawn raw identity로
raw staging/canonical을, sealed identity로 temp/final을 각각 확인해 일치하는 alias만
정리한다. 현재 canonical pathname을 새 ownership identity로 승격하지 않으므로 외부
file과 concurrent same-kind winner를 보존하면서 같은 workspace를 즉시 재시도할 수
있다. Supervisor는 READY identity와 canonical target도 정확히 확인한다.
검증된 JSON bytes는 pinned temp inode에만 기록하고 `0400` 적용과 file sync를 마친다.
Guardian은 authenticated publish의 size/hash/identity를 재검증하고 같은 directory에서
no-replace `link(temp, final)`, temp unlink, directory sync를 수행한 뒤 PUBLISHED를
응답한다. `GITHUB_OUTPUT` append 이후 commit은 raw inode를 제거하고
`commitPending`으로만 전이하며, 깨끗한 control EOF까지 확인해야 PASS가 된다. 그 전의
EOF, deadline, 잘못된 frame/nonce, commit 뒤 추가 byte 또는 guardian 조기 종료는
identity가 일치하는 raw/temp/final을 모두 정리하고 fail closed한다. Provider wall
timeout 30분에 post-processing 10분을 더한 guardian lease 상한은 40분이다.
Client가 private allocation 뒤 guardian spawn 전에 hard stop되면 empty mode-`0600`
nonce-private leaf만 남을 수 있다. 기록되지 않은 pathname은 ownership 근거가 아니므로
자동 sweeping하지 않는다. 이 private leaf는 fixed raw/final name을 점유하지 않아
same-kind retry를 막지 않는다.
`GITHUB_OUTPUT`은 runner가 소유한 regular file이라는 실행기 계약을 전제로 한다.
Sealing/output I/O의 OS-level cancellation을 보장하지 않는다. Provider scope가 이미
수집된 뒤 guardian이 종료되면 scope-active latch는 늦은 kill을 시작하지 않고 lifecycle
error만 기록한다. 이후 publish/commit의 awaited failure가 identity가 고정된
raw/temp/final fallback을 모두 정리하고 fail closed한다.
Provider stdout/stderr는 credential을 포함할 수 있는 untrusted bytes이므로 CI log로
재전송하거나 보관하지 않고, byte 수만 합산해 1 MiB 상한을 적용한다. Guardian의
stderr/control fd가 닫혀 진단 출력이 `EPIPE`/`EBADF`가 되어도 cleanup 뒤 nonzero
종료는 생략되지 않는다. Provider wall-clock 상한은 30분이다. Wall
timeout, aggregate output 초과, parent-liveness loss, 실행 중 guardian loss는 provider process group을
명시적으로 SIGKILL한다. 일반 command 실패와 FD/CPU RLIMIT 종료는 실제 exit/signal로
systemd completion을 거치며, 모든 경로에서 wrapper 종료와 systemd unit/cgroup
collection을 확인한다. Adapter는 사전에 배치된 offline data와 supervisor candidate binding만
읽어 정확한 report inode에 기록해야 한다. Scope/cgroup limit drift, residual unit,
workspace·host home/toolcache·sealed evidence 접근 의존성은 모두 blocking failure다.
Workflow가 실행하는 action은 `scripts/contracts/ci-gates.ts`의 단일 typed,
runtime-frozen registry에서만 resolve된다. `uses:`에는 repository 별칭, tag,
branch 또는 short SHA를 허용하지 않고 다음 absolute upstream URL과 full 40-hex
commit만 사용한다.
- `https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5`
(`v4.3.1`)
- `https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020`
(`v4.4.0`)
- `https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7`
(patched `v4` branch)
- `https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7`
(patched `v4` branch)
Gitea 1.22 공식 Actions 문서는 artifact v4 호환 경로로 이
`ChristopherHX` patched upload/download fork를 권고한다. 이 권고는 실제
설치 환경의 호환성을 증명하지 않으므로, 지원 baseline은 계속 Gitea 1.26.4+
및 Gitea Runner 1.0.0+이며 action 변경은 registry, semantic tests, snapshot과
generated workflow를 함께 갱신해야 한다.
이 workflow를 required check로 전환하기 전에 staging instance에서
`permissions`, `gitea.run_attempt`, 위 digest-pinned patched artifact action을
포함한 한 번의 실제 end-to-end provider smoke를 반드시 통과시켜야 한다.
mock/local generation 결과는 required-check enablement evidence가 아니다. 모든 setup step은
`node-version-file: .nvmrc`를 사용하므로 CI Node.js 버전은 `.nvmrc`의 exact
pin과 같다. CI contract는 `.nvmrc`가 full semantic version인지, 모든 job이 이
파일을 사용하는지 함께 검사한다. `ubuntu-latest` runner label은
관리자가 임의 환경에 매핑할 수 있으므로 provenance로 사용하지 않는다. 대신
그 label을 immutable container image에 매핑하고 동일한 image digest를
repository variable `RUNNER_IMAGE_DIGEST`에 설정한다. Workflow는 이 값을
runtime environment의 `CI_RUNNER_IMAGE`로 전달하며, 값이 비어 있으면 모든
gate가 실행 전에 실패한다. Gitea repository variable 이름에는 `CI` prefix를
사용할 수 없으므로 두 이름을 의도적으로 구분한다.
Pull requests and `develop` pushes evaluate merge readiness. Version tags
evaluate merge then release readiness. Production and field evaluation require
an explicit workflow dispatch. The field tier cannot pass until the 28-day
sample threshold decision is recorded. Documentation readiness consumes the
canonical project-note evidence in which both scoped diagrams already received
100/100 `PASS_SCOPED`; the repo ledger preserves the evidence scope and
canonical digests.
All jobs upload the shared `artifacts/` tree even after failure. Numeric
retention remains an organization/provider decision; the workflow intentionally
does not invent `retention-days`. The relative minimums are recorded in the
registry: merge evidence through the PR decision, coherent release evidence
through the next release promotion, drill evidence through the next production
promotion, and field evidence through aggregation.
Runbook jobs write to the stable path
`artifacts/runbooks/<runbook-id>/record.json`; the dynamic release identity is
stored inside the record. This keeps gate evidence lookup independent from
slashes or other provider-specific characters in `RELEASE_ID`, while each
workflow artifact remains scoped to its own run.
Browser-backed merge gates install and execute the pinned Chromium, Firefox,
and WebKit engines. This makes route behavior, reflow, native dialog semantics,
theme persistence, and automated accessibility a cross-engine contract rather
than a Chromium-only smoke check.
`FE-GATE-005`의 unit suite에는 client cache/storage의 deterministic contract가
포함된다. `storage-registry.test.ts`는 key별 codec, schema/TTL, 16,384-byte
상한, quota/security/corrupt cleanup과 memory fallback을,
`cross-tab-invalidation.test.ts`는 2,048-byte exact wire, topic/release epoch,
self/duplicate/out-of-order/gap, BroadcastChannel → localStorage → local-only
degrade와 cleanup을 검증한다. `tanstack-cache-coordinator.test.ts`는 topic을
local namespace로만 resolve하고 query key/data를 wire에 보내지 않으며 mutation
중 remote hint를 coalesce하는지 검증한다. 결과는 기존
`artifacts/tests/unit.xml`과 coverage evidence에 포함된다.
`FE-GATE-006`과 runtime adapter unit은
`QueryInvalidationProvider`/coordinator가 실제 production provider tree와
bootstrap에 존재하고 browser capability getter가 실패해도 boot가
`DEGRADED_LOCAL_ONLY`로 계속되는지 확인한다.
`FE-GATE-008``test:browser-capabilities` suite는 File/Blob, native file
input, IndexedDB, OPFS, Cache Storage와 StorageManager reference runtime을 Vite
dev origin의 실제 browser API에 연결한다. API가 없는 engine에서는 skip하지 않고
adapter의 명시적인 `UNSUPPORTED` fallback을 검증한다. 생성한 database, OPFS
namespace와 owned cache는 각 test가 자신이 만든 opaque namespace만 정리한다.
`FE-GATE-010`은 application/domain/presentation에서 raw browser storage와
picker global 접근을 막고, 선택되지 않은 reference runtime을 bootstrap이나
installed feature가 import하는 것도 거절한다. catalog의
`referenceRuntime.sourceRoots`와 conformance script가 실제로 존재해야 하며,
`productionComposition`은 project capability 결정 전까지 `false`다. 같은
gate의 realtime source/fixture 검사는 native SSE/WebSocket/Web Push API가
소유 adapter 밖으로 새는 것, presentation timer owner, 미선택 runtime의
bootstrap/installed-feature 조합을 차단한다. optional recipe source gate는
uncomposed runtime 전체를 tree-shaking 없이 합성해 catalog의 gzip 예산도
blocking으로 검증한다.
`FE-GATE-020`은 reference runtime source·전용 test·catalog metadata를 제거한
임시 repository에서도 base typecheck, architecture, test, build가 통과하는지
검증해 skeleton의 선택성을 유지한다. browser file/storage와 realtime runtime은
각각 독립 removal fixture와 JUnit evidence를 가지며, realtime fixture는 공통
event authority, SSE, WebSocket, bounded Polling, Web Push source와 공개 export,
전용 boundary script를 제거한 뒤 base gate를 다시 실행한다.
같은 `FE-GATE-010``FE-REG-QUERY` governance는 installed query마다
namespace, serialization/identity, invalidation topic, version,
`crossContext: "invalidate-only"``persistence: "disabled"`를 요구한다.
따라서 cross-tab invalidation은 조립됐지만 query persistence와 기존 IndexedDB
reference runtime은 계속 미조립이다.
현재 `FE-GATE-008` browser-capability suite에는 실제 두 page의
BroadcastChannel/localStorage fallback scenario가 아직 없다. unit fake 통과를
native multi-tab promotion evidence로 간주하지 않는다. 이 gap은
[Client cache and browser storage platform](../architecture/client-cache-and-storage.md)의
완료 기준에 미완료로 남아 있다. 현재 장애 분류와 안전한 local-only 복구 절차는
[Client cache and Web Storage recovery](./client-cache-and-storage-recovery.md)를
따른다.
Repository variables required by higher tiers:
- `RUNNER_IMAGE_DIGEST` for the immutable job-container image digest used by the
`ubuntu-latest` runner label; the workflow exposes it to gates as
`CI_RUNNER_IMAGE` (required by every tier)
- `HOSTING_BASE_URL` for live header verification
- `FIELD_WEB_VITALS_INPUT` for the privacy-approved field sample document
- `MIN_ELIGIBLE_SAMPLES` after the baseline decision
- `VULNERABILITY_PROVIDER_COMMAND` and `PROVENANCE_PROVIDER_COMMAND` for the
independently operated scanner and attestor commands
- `VULNERABILITY_PUBLIC_KEY_PATH`, `VULNERABILITY_KEY_ID`,
`PROVENANCE_PUBLIC_KEY_PATH`, and `PROVENANCE_KEY_ID` for separately managed
trusted Ed25519 verification material
두 provider role은 서로 다른 key ID뿐 아니라 canonical DER-SPKI public-key bytes도
사용해야 한다. 동일 key를 서로 다른 ID로 재등록한 구성도 finalizer가 거절한다.
If any external provider command, report, trust path, or key ID is absent,
promotion remains unavailable with `FAIL_UNVERIFIED`; there is no local
generator/restore fallback.
Each provider command must atomically emit strict provider evidence v2 from
the supervisor bindings for evidence type, issued/expires timestamps, run
ID/attempt, `PROVIDER_INVOCATION_NONCE`, source identity, candidate digests,
key ID, and DER-SPKI key fingerprint. Promotion receives the two supervisor
job outputs as `VULNERABILITY_INVOCATION_NONCE` and
`PROVENANCE_INVOCATION_NONCE`; do not replace them with constants or values
parsed from provider reports.
Promotion verification/staging step과 promoted-release upload action 사이에는
어떤 step도 둘 수 없고 upload에는 `if: always()`를 사용할 수 없다. 이 인접성은
실패한 검증의 publication을 막지만 staging path를 upload action에
cryptographically hand off하지는 않는다. 따라서 promotion runner는 trusted,
exclusive, single-tenant여야 하고 staging step 종료부터 바로 다음 upload action이
파일을 여는 시점까지 같은 UID로 실행되는 provider command나 다른 process가
살아남지 않는다는 운영 가정을 만족해야 한다. 업로드된 artifact의 downstream
consumer도 artifact service나 transfer action을 신뢰 경계 밖으로 보고, candidate
manifest와 signed provider evidence에 바인딩된 digest를 다운로드 후 다시
검증해야 한다. 현재 producer-side adjacency 자체는 consumer-side digest
revalidation을 대신하지 않는다.
Promotion job에는 job-level `if`가 없다. 기본 `needs` 성공 의미론으로 immutable
build, vulnerability provider, provenance provider 세 job이 모두 성공해야 하며,
`always()``cancelled()`로 cancellation을 덮어쓰지 않는다. Bare `always()`
step cleanup에만 사용된다. 다만 cancellation 시 cleanup 실행 여부는 workflow
정적 타입이나 단위 테스트로 증명하지 않았으며 runner/native smoke에서 확인해야
하는 신뢰 경계다.
Finalizer output은 `RUNNER_TEMP` 아래 random private directory이며 exact-five
upload는 `${{ steps.finalize.outputs.staging_root }}` 아래 다음 다섯 canonical
pathname만 사용한다: `release-candidate.tar.gz`, `vulnerability-report.json`,
`provenance-attestation.json`, `provider-verification.json`,
`promotion-verification.json`. 바로 다음
`always()` cleanup은 staging path/token, runner-temp device/inode와 staging-leaf
device/inode 여섯 output을 모두 요구한다. cleanup은 pin한 leaf descriptor에서
exact-five name만 unlink하고 non-recursive `rmdir`만 사용하므로 교체된 directory나
canary tree를 recursive 삭제하지 않는다. stable `.release/promoted-staging`
directory를 만들거나 재사용하지 않는다. exact five는 captured archive 한 개,
captured report 두 개와 process 안에서 생성한 provider/promotion verification v3
두 개이며 promotion
record는 provider record, local assessment, report hashes와 run/source/candidate/
nonces/key identities/trust-policy hash 및 signed `secretScanAttestation`을 함께
bind한다. 이 attestation은 PASS와 local-assessment/source-set/policy/SARIF/
scan-input digest를 포함한다. Supervisor/finalizer는 captured archive에서 기대
tuple을 유도해 exact equality를 확인하지만, 실제로 같은 source-set 전체를
독립 스캔하고 forged empty SARIF에 서명하지 않을 책임은 trusted vulnerability
provider에 있다. Staging은 restrictive
umask와 무관하게 directory `0700`, file `0400`을 강제하고, 모든 write 뒤 live
time으로 exact-five signature/freshness를 다시 확인한 뒤에만 output을 공개한다.
Descriptor-relative 정리는 ancestor/leaf 교체와 symlink를 fail-closed로 처리하지만
upload action의 same-UID pathname reopen 또는 atomic
`renameat2` handoff를 보장하지 않는다. staging Gitea smoke/native adapter 확인
전에는 그 경계를 닫았다고 보고하지 않는다. 실제 smoke는 exact-five
upload-download와 success, validation failure, upload failure, cancellation 각각의
cleanup을 관찰해야 한다. 현재 repository에는 native uploader나 `renameat2`
보장이 없다.
또한 portable Node의 `mkdir`와 최초 pathname `lstat`는 atomic하지 않다. 구현은
mkdir 직후 metadata를 저장하고 이후 `O_DIRECTORY|O_NOFOLLOW` descriptor의
device/inode와 비교한 뒤에만 permission을 바꾸지만, 최초 lstat보다 앞서 성공한
malicious same-UID 교체는 native/privilege 경계로 남는다. 따라서 `RUNNER_TEMP`
private `0700` ancestor와 exclusive single-tenant runner가 필수다.
실패 cleanup도 created device/inode와 opened descriptor가 일치한 뒤에만 활성화된다.
불일치 descriptor는 close만 수행하며 현재 visible replacement pathname은 unlink나
`rmdir`하지 않는다. 공격자가 original directory를 다른 이름이나 parent 밖으로
이동한 경우 portable Node parent scan으로 안전하게 회수할 수 없으므로, 공격자를
배제한 trusted runner/native cleanup 또는 격리된 test fixture가 잔여 directory를
후처리해야 한다.
Branch protection must mark each `FE-GATE-* / <name>` check required for its
declared tier. This repository cannot configure server-side protection by
committing a file.
@@ -0,0 +1,203 @@
# Client cache and Web Storage recovery
- 적용 대상: production-composed TanStack memory cache, registered Web Storage,
cross-tab invalidate-only runtime
- 비대상: query persistence와 IndexedDB query cache
- 기준일: 2026-07-28
현재 query persistence는 정책과 runtime에서 `disabled`다. 기존 IndexedDB
reference runtime은 query cache에 조립되지 않았으므로 query-cache incident에서
database 삭제, migration 또는 hydration 조치를 수행하지 않는다.
session/account Query lifecycle, strict query policy와 Web Storage v2 lifecycle도
현재 `DESIGNED_NOT_IMPLEMENTED`다. 아래 목표 절차를 현재 runtime의 보장으로
해석하지 않는다.
## 1. 변경할 수 없는 복구 원칙
- 서버가 server state와 authorization의 source of truth다.
- cross-tab event는 invalidate hint다. delivery나 server commit 증명이 아니다.
- remote event로 `queryClient.clear()`, account logout 또는 credential 폐기를
수행하지 않는다.
- `BroadcastChannel` 실패 후 localStorage fallback도 실패하면
`DEGRADED_LOCAL_ONLY`가 정상 fallback이다. 현재 tab의 local invalidation과
server request는 계속 동작해야 한다.
- origin 전체 `localStorage.clear()``deleteDatabase()`를 복구 명령으로
사용하지 않는다.
- query key, storage value, event/topic/epoch/source ID, user/account/tenant ID를
incident log에 복사하지 않는다.
- incident 대응 중 `QUERY_PERSISTENCE`를 켜거나 server response를 Web Storage에
저장하지 않는다.
## 2. 최초 확인
1. 영향 release ID, browser family/version과 최초 발생 시각을 확인한다.
2. `cache.operation.failed``storage.operation.failed`의 allowlisted
operation/outcome/reason만으로 범위를 좁힌다.
3. runtime status가 `ACTIVE_BROADCAST`, `ACTIVE_STORAGE_FALLBACK`,
`DEGRADED_LOCAL_ONLY`, `CLOSED` 중 무엇인지 확인한다.
4. 현재 tab의 mutation 성공 후 local namespace가 stale 처리되는지 확인한다.
5. 다른 tab의 오래된 화면이 단순 stale 표시인지, 실제 server authorization
우회인지 분리한다. authorization 우회는 cache incident가 아니라 P1 security
incident다.
6. affected release의 query registry topic/version과 release cache epoch가
일치하는지 확인한다.
## 3. Cross-tab invalidation degradation
### 신호
- `BROADCAST_OPEN_FAILED`
- `BROADCAST_PUBLISH_FAILED`
- `STORAGE_LISTENER_FAILED`
- `STORAGE_PUBLISH_FAILED`
- runtime status `DEGRADED_LOCAL_ONLY`
### 확인
1. browser policy, embedded/sandbox context 또는 privacy mode가
BroadcastChannel/Web Storage를 제한하는지 확인한다.
2. BroadcastChannel publish 실패 뒤 fixed localStorage pulse key로 정확히 한 번
fallback하는지 확인한다.
3. publishing tab은 `storage` event를 받지 않으므로 local invalidation을
coordinator가 직접 수행했는지 확인한다.
4. 다른 tab은 focus/reconnect 또는 명시적 refresh에서 server를 다시 읽는지
확인한다.
### 안전한 조치
- local-only 상태에서는 사용자에게 현재 tab refresh action을 유지한다.
- fallback regression이 특정 release에서 시작됐으면 이전 compatible release로
rollback한다.
- event payload에 query key/data를 추가하거나 TTL/size/source tracking limit을
임시 확대하지 않는다.
- 현재 runtime에는 별도 dynamic kill switch가 없다. 존재하지 않는 flag로
복구됐다고 선언하지 않는다.
## 4. Duplicate, sequence gap과 refetch 증가
### 신호
- `DUPLICATE`, `STALE`, sequence-gap observation 증가
- 여러 namespace의 active refetch 동시 증가
- backend read traffic 증가
### 확인
1. 같은 source/epoch의 sequence gap인지 새 page epoch의 정상 sequence reset인지
구분한다.
2. duplicate가 BroadcastChannel과 storage fallback 양쪽에서 들어온 것인지
확인한다.
3. mutation lease가 유지되는 동안 remote hint가 topic별로 coalesce되는지
확인한다.
4. gap에서 inactive query까지 즉시 refetch하거나 remote hint를 다시 publish하는
echo가 없는지 확인한다.
### 안전한 조치
- gap은 등록 namespace를 stale 처리하고 active query만 refetch한다.
- backend가 압박을 받으면 API degradation runbook의 server read 보호 정책을
적용한다. client query와 HTTP 양쪽 retry를 동시에 늘리지 않는다.
- persistent event queue나 localStorage counter를 급히 추가하지 않는다.
## 5. Web Storage quota, corruption과 denial
### 신호
- `STORAGE_QUOTA_EXCEEDED`
- `SIZE_LIMIT_EXCEEDED`
- `VALUE_REJECTED`
- `STORAGE_UNAVAILABLE`
### 확인
1. 실패 key가 registry backend/value codec/schema/TTL과 일치하는지 확인한다.
2. value가 기본 16,384-byte hard cap 안인지 확인한다.
3. corrupt/expired record가 exact physical key에서만 제거되고 다른 application
key는 유지되는지 확인한다.
4. `COLOR_SCHEME` quota fallback이 동일 serialized envelope와 TTL 규칙을 쓰는지
확인한다.
5. native exception message나 value가 diagnostics에 포함되지 않았는지 확인한다.
### 안전한 조치
- preference persistence 실패는 memory fallback 또는 safe default로 degrade한다.
- exact corrupt/expired registered key만 제거한다.
- credential key를 임시 storage key로 재등록하지 않는다.
- origin 전체 clear, arbitrary key enumeration과 query-cache persistence 전환을
금지한다.
## 6. Logout, account 전환과 stale data
현재 구현은 release cache epoch만 제공하고 session/account epoch와 opaque account
partition은 아직 없다. 따라서 다음을 과장해 보장하지 않는다.
- cross-tab hint가 모든 tab의 logout을 완료했다.
- old in-flight result가 새 account runtime에 기록되지 않는다.
- persisted query data가 account별로 분리된다.
실제 account-switching 제품을 배포하기 전에 session owner 기반 local admission
fence, old QueryClient cancel/clear, late-result generation fence와 account epoch를
구현해야 한다. 그 전에는 external auth owner와 서버 authorization을
authoritative하게 유지하고, 의심되는 stale personal data incident는 security
owner에게 escalation한다.
VD-13 lifecycle이 구현된 제품의 안전한 전환 순서는 다음과 같다.
```text
local session authority revoke
-> old generation FENCED
-> new query/mutation admission 거절
-> in-flight cancel
-> provider/listener detach
-> old QueryClient clear + dispose
-> old Web Storage/optional persistence partition purge policy
-> 새 opaque scope와 새 QueryClient remount
```
각 async callback은 terminal cache write 직전에 captured generation을 다시
검증한다. broadcast logout 수신 여부와 무관하게 local 전환이 완료되어야 한다.
optional query persistence를 선택한 제품은 durable namespace epoch/CAS와 old
record resurrection fault까지 통과한 경우에만 restore를 다시 연다.
## 7. Cleanup과 종료
정상 dispose는 coordinator subscription, BroadcastChannel, storage listener,
dedupe/high-watermark state와 pending remote set을 idempotent하게 정리한다.
확인:
- HMR/unmount 반복 뒤 listener/channel 수가 증가하지 않음
- dispose 뒤 event가 query invalidation을 실행하지 않음
- fixed pulse key가 publish 후 best-effort 제거됨
- cleanup failure가 cached value나 identifier를 log하지 않음
## 8. 복구 완료 조건
- current tab의 committed mutation 후 local namespace invalidation 성공
- available transport에서는 two-tab remote invalidation 성공
- unavailable transport에서는 explicit `DEGRADED_LOCAL_ONLY`와 manual/focus
revalidation 성공
- duplicate/self/stale event가 추가 refetch를 만들지 않음
- sequence gap은 bounded active-query reconciliation으로 종료
- Web Storage corrupt/quota scenario가 exact-key cleanup 또는 documented fallback
으로 종료
- raw key/value/query/user identifier가 diagnostic evidence에 없음
- affected browser의 manual two-tab smoke 기록 보존
two-page native cross-context transport suite는 존재하지만 현재 보존 evidence는
Chromium/Firefox에 한정되고, production QueryClient/coordinator의 account
lifecycle과 late-result fence까지 연결한 end-to-end case는 아직 없다. 따라서
manual smoke나 transport-only suite를 production promotion evidence의 영구
대체물로 사용하지 않는다. Chromium/Firefox/WebKit에서 scope 전환을 포함한
동일 case set이 통과할 때 이 runbook의 promotion close criteria를 충족한다.
## 9. 관련 문서
- [API contract와 Server State recovery](./api-contract-and-server-state-recovery.md)
- [VD-25 Server State Cache lifecycle](../architecture/decisions/VD-25-server-state-cache-lifecycle.md)
- [Client cache and browser storage platform](../architecture/client-cache-and-storage.md)
- [Browser data capability completion ledger](../architecture/browser-data-capability-completion-ledger.md)
- [VD-13 Client cache scope and persistence](../architecture/decisions/VD-13-client-cache-scope-and-persistence.md)
- [TypeScript, 상태 소유권, 데이터 흐름](../architecture/typescript-state-and-data-flow.md)
- [Backend API degradation](../runbooks/FE-RB-003.md)
- [Release, cache, and rollback contract](./release-cache-rollback.md)
+22
View File
@@ -0,0 +1,22 @@
# Performance evidence contract
Performance evidence is deliberately split by measurement context:
- `bundle.json` records production build output and enforces initial JavaScript
at 200 KiB gzip and every lazy chunk at 120 KiB gzip.
- `lab.json` records Chromium/runner/viewport/network/CPU/cache/build context and
enforces LCP 2.5 s, CLS 0.10, and the named route interaction at 200 ms.
- `field-web-vitals.json` records consent-filtered, route-ID aggregated,
release-specific production samples over 28 days and evaluates p75 LCP, CLS,
and INP against 2.5 s, 0.10, and 200 ms.
The field minimum eligible-sample threshold is intentionally unresolved until
a privacy-approved telemetry baseline exists. Therefore the field command
fails closed with `FAIL_UNVERIFIED` when run against the example input. Provide
`FIELD_WEB_VITALS_INPUT` and `MIN_ELIGIBLE_SAMPLES` only after that decision is
recorded. The external input must identify a production release and an exact
28-day export window, name the source/export, carry privacy-approval and
threshold-decision references, and contain only non-negative route-ID samples.
The environment threshold must be a positive integer equal to the approved
decision embedded in the input. Invalid metadata fails as `FAIL_UNVERIFIED`;
the example can never serve as production evidence.
+32
View File
@@ -0,0 +1,32 @@
# Release, cache, and rollback contract
Each deployment is an immutable `releases/<releaseId>/` artifact set. The
provider adapter must upload assets, release manifest, runtime config, and
verify asset reachability before atomically switching the active HTML pointer.
The post-switch boot, route, API, telemetry, and reload-loop smoke checks close
the deployment.
Rollback selects a prior release tuple, confirms its assets and runtime/API
compatibility, atomically switches the complete set, performs the provider
cache action, and repeats the smoke checks. Rebuilding an old commit, replacing
HTML alone, or declaring recovery from cache-purge completion is prohibited.
Recovery is established by old/new reachability probes.
The provider-independent cache defaults are:
- hashed assets: `public, max-age=31536000, immutable`
- HTML: `no-cache`
- runtime config and release manifest: `no-store`
- public source maps: disabled
- service worker/offline cache: disabled
HTML, JSON config/manifest, and hashed JavaScript MIME types are also compared
to the declared allowlist; a cache-correct response with a mismatched
`Content-Type` still fails the hosting gate.
`corepack pnpm verify:hosting-headers` uses a deterministic fixture locally.
Set `HOSTING_BASE_URL` to probe deployed responses; production promotion
requires the artifact to report `mode: "live"`. The live target must be its
canonical, non-loopback HTTPS root URL. Each required surface must return HTTP
200 without leaving that origin before its cache, content-type, and security
headers can count as deployment evidence.