chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user