The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
779 lines
28 KiB
Markdown
779 lines
28 KiB
Markdown
# VD-23: API transport selection과 REST execution
|
|
|
|
- 상태: Accepted — REST v2 security/execution baseline composed, advanced profiles pending
|
|
- 결정일: 2026-07-28
|
|
- installed REST reference vertical: `COMPOSED`
|
|
- REST v2 security/execution baseline: `COMPOSED`
|
|
- provider/path/auth profile baseline: `COMPOSED`
|
|
- conditional execution/provider-conformance delta: `DESIGNED_NOT_IMPLEMENTED`
|
|
- GraphQL/Connect/gRPC-Web/REST Gateway product selection: `NOT_SELECTED`
|
|
- 관련 결정: VD-13, VD-24, VD-25, VD-26, VD-27, VD-29, VD-30
|
|
- 상세 설계:
|
|
[API contract, Schema, Mapper와 Server State](../api-contract-schema-mapper-and-server-state.md)
|
|
- browser Protobuf/gateway 설계:
|
|
[Protobuf browser transport와 REST Gateway](../protobuf-browser-transport-and-rest-gateway.md)
|
|
|
|
## 배경
|
|
|
|
현재 reference feature는 operation registry, request Zod schema, shared HTTP
|
|
executor, response envelope/payload schema, mapper, gateway, application input과
|
|
TanStack Query까지 실제 production composition에 연결된다. 따라서 REST 경로
|
|
자체를 미구현으로 표시하지 않는다.
|
|
|
|
현재 reference operation은 v2 metadata, collision-aware composition, allowlisted
|
|
credential patch, auth fail-before-fetch, prefix-preserving target, shared logical
|
|
deadline, exact JSON media/success status, bounded decoder와 typed mapping failure를
|
|
실행한다. correlation header, success status group과 401 replay를 포함한 physical
|
|
attempt도 terminal observation에 연결한다.
|
|
|
|
operation은 path placeholder↔codec key exact join, named provider/bearer/CSRF
|
|
profile, provider credential-mode ceiling과 encoded query byte ceiling도 실행한다.
|
|
남은 production delta는 cookie-CSRF/CORS evidence, 204/304/412 conditional
|
|
transaction, artifact digest와 실제 provider conformance다.
|
|
`API_CONTRACT_VERSION` 문자열 일치만 backend compatibility 증거로 사용하지 않는다.
|
|
|
|
## 결정
|
|
|
|
### 1. Protocol 선택은 operation registry가 소유한다
|
|
|
|
feature application port는 protocol-neutral이다.
|
|
|
|
```text
|
|
feature use case
|
|
-> feature gateway
|
|
-> exact semantic operation
|
|
-> REST adapter
|
|
-> persisted GraphQL adapter
|
|
-> Connect adapter
|
|
-> gRPC-Web adapter
|
|
```
|
|
|
|
- 한 `operationId`는 한 protocol에만 binding한다.
|
|
- UI, query hook과 use case는 protocol을 선택하지 않는다.
|
|
- URL, GraphQL document, service/method와 generated request를 application input으로
|
|
받지 않는다.
|
|
- 같은 command를 provider failure 때문에 다른 protocol로 자동 replay하지 않는다.
|
|
- read fallback도 동일 auth/freshness/schema/mapper/query identity와 하나의 total
|
|
retry budget을 증명한 registered policy만 허용한다.
|
|
- GraphQL/Connect/gRPC-Web adapter를 공통 `HttpClient`의 mode flag로 넣지 않는다.
|
|
공통으로 공유하는 것은 execution context, auth collaboration, failure vocabulary와
|
|
observation뿐이다.
|
|
|
|
### 2. REST operation은 실행 source와 정적 manifest를 분리하지 않는다
|
|
|
|
목표 API는 feature가 generic이 연결된 definition을 만든다.
|
|
|
|
```text
|
|
defineRestOperation<
|
|
Path,
|
|
Search,
|
|
Body,
|
|
SuccessWire,
|
|
SuccessValue,
|
|
Failure
|
|
>({
|
|
common,
|
|
providerId,
|
|
method,
|
|
uriTemplate,
|
|
pathCodec,
|
|
searchCodec,
|
|
bodyCodec,
|
|
successCodec,
|
|
errorCodec,
|
|
mapper,
|
|
policies
|
|
})
|
|
```
|
|
|
|
실행 definition에서 registry manifest를 결정적으로 투영한다. schema metadata,
|
|
실제 codec map, operation-specific TypeScript map과 mapper를 서로 다른 string
|
|
dispatch table에 수기로 중복하지 않는다.
|
|
|
|
manifest 최소 field:
|
|
|
|
```text
|
|
protocol = REST
|
|
registryVersion
|
|
providerId
|
|
operationId
|
|
owner
|
|
semantics
|
|
method
|
|
relativePathTemplate
|
|
pathSchemaId
|
|
searchSchemaId
|
|
bodySchemaId
|
|
requestMediaProfile
|
|
successStatusProfiles[]
|
|
errorStatusProfiles[]
|
|
responseSchemaId
|
|
mapperId
|
|
authProfileId
|
|
csrfProfileId
|
|
replayPolicy
|
|
idempotencyKeyPolicy
|
|
deadlineProfileId
|
|
retryProfileId
|
|
paginationProfileId | null
|
|
conditionalProfileId
|
|
serverStateProfileId | null
|
|
maxRequestBytes
|
|
maxDecodedResponseBytes
|
|
maxResponseItems
|
|
observabilityProfileId
|
|
compatibility
|
|
```
|
|
|
|
### 3. Contribution composer는 collision 전에 실패한다
|
|
|
|
```text
|
|
feature contributions[]
|
|
-> preserve every source row
|
|
-> validate contribution owner/version
|
|
-> detect duplicate operation/schema/mapper/query/topic ID
|
|
-> resolve every codec/mapper/profile reference
|
|
-> validate semantic coherence
|
|
-> freeze installed registry
|
|
-> emit compatibility manifest
|
|
```
|
|
|
|
object spread의 last-write-wins를 금지한다. duplicate ID가 payload까지 동일해도
|
|
owner를 하나 선택하지 않고 build를 실패시킨다. alias/rename은 versioned migration
|
|
row로만 허용한다.
|
|
|
|
semantic coherence:
|
|
|
|
- `GET`/`HEAD`는 body 없음, `SAFE`만 허용
|
|
- `HEAD`는 body success codec 없음
|
|
- `POST`/`PATCH` retry는 `IDEMPOTENT | KEYED_COMMAND` 또는 명시적
|
|
`SAFE` semantics 필요
|
|
- `KEYED_COMMAND`는 `idempotencyKeyPolicy=REQUIRED`, 다른 replay policy의 key
|
|
attach/금지는 exact key profile과 일치
|
|
- `NON_REPLAYABLE`은 retry와 401 replay 금지
|
|
- success/error status가 겹치지 않음
|
|
- 204 profile은 body/schema 없음
|
|
- 304는 query + conditional profile + existing cache binding 필요
|
|
- 412는 precondition profile 필요
|
|
- path placeholder 집합과 path codec key 집합이 exact match
|
|
- query/cache profile은 `QUERY` operation에만 연결
|
|
- mutation invalidation은 registered topic만 사용
|
|
|
|
### 4. Provider endpoint와 URI
|
|
|
|
application은 provider URL을 받지 않는다.
|
|
|
|
```text
|
|
RestProviderProfile
|
|
providerId
|
|
baseOrigin
|
|
basePathPrefix
|
|
allowedCredentialsModes
|
|
corsProfile
|
|
referrerPolicy
|
|
redirectPolicy = ERROR
|
|
defaultHeaders
|
|
allowedResponseOrigins
|
|
```
|
|
|
|
- non-local은 HTTPS만 허용한다.
|
|
- provider는 credential mode ceiling만 제공하고 operation의 auth transport
|
|
profile이 exact mode를 선택한다. anonymous는 `omit`, same-origin cookie는
|
|
approved `same-origin`, approved cross-origin cookie만 `include`다.
|
|
- base URL의 username, password, query와 fragment를 금지한다.
|
|
- exact origin과 canonical base path prefix를 보존한다.
|
|
- operation template은 relative API path이며 scheme, authority, query와 fragment를
|
|
포함하지 않는다.
|
|
- leading slash가 base prefix를 제거하는 `new URL()` ambiguity를 쓰지 않는다.
|
|
- encoded slash, dot segment, NUL/control, duplicate slash와 overlong path를
|
|
정책대로 거절한다.
|
|
- redirect는 기본 `error`다. 로그인/다운로드 handoff는 일반 JSON REST operation과
|
|
다른 capability다.
|
|
|
|
path parameter:
|
|
|
|
- path codec의 parsed output만 encode한다.
|
|
- missing, extra, empty와 length 초과 parameter를 network 전에 거절한다.
|
|
- Unicode normalization을 업무 ID에 임의 적용하지 않는다.
|
|
- path value와 최종 URL은 diagnostics에 기록하지 않는다.
|
|
|
|
query:
|
|
|
|
- key ordering, repeated-array/comma style, boolean, null/absent/empty semantics를
|
|
profile에 고정한다.
|
|
- URL encoded byte ceiling을 적용한다.
|
|
- raw `URLSearchParams`, query string과 next URL을 caller에게 받지 않는다.
|
|
- sensitive/private value를 GET query에 넣는 operation은 별도 security review가
|
|
없으면 금지한다.
|
|
|
|
### 5. Request projection과 final invariant
|
|
|
|
request는 다음 소유 순서로 만든다.
|
|
|
|
```text
|
|
operation + validated input
|
|
-> immutable request binding
|
|
-> body canonical serialization + digest
|
|
-> transport-owned headers/options
|
|
-> constrained auth/CSRF patch
|
|
-> final invariant validation
|
|
-> fetch
|
|
```
|
|
|
|
transport-owned header:
|
|
|
|
- `Accept`, `Content-Type`
|
|
- contract/media version
|
|
- bounded correlation/trace context
|
|
- idempotency key
|
|
- conditional validator
|
|
- approved CSRF header
|
|
|
|
caller와 feature mapper가 arbitrary header를 추가하지 않는다.
|
|
|
|
auth owner target:
|
|
|
|
```text
|
|
CredentialPatch
|
|
credentialMode
|
|
allowlisted header name/value
|
|
proof expiry/generation
|
|
```
|
|
|
|
operation auth profile은 final Fetch `credentials`를 exact하게 고정한다.
|
|
`ANONYMOUS | BEARER_HEADER`는 ambient cookie가 섞이지 않게 `omit`,
|
|
same-origin cookie session은 `same-origin`, cross-origin cookie는 별도 CORS/CSRF
|
|
provider evidence가 있는 profile만 `include`다. provider ceiling과 맞지 않으면
|
|
fetch 0회다.
|
|
|
|
auth owner가 `Request` 전체를 반환하지 않는다. transition 기간에 current port를
|
|
사용한다면 attach 전후의 다음 값이 exact하게 같아야 한다.
|
|
|
|
- URL/origin/path/query
|
|
- method
|
|
- body digest
|
|
- content type/length
|
|
- correlation, idempotency, conditional와 CSRF binding
|
|
- redirect/cache/referrer/credentials/mode
|
|
|
|
다르면 `AUTH_INTEGRATION_FAILURE`, fetch 0회다.
|
|
|
|
auth-required operation은 session state가 `authenticated`가 아니면 fetch하지 않는다.
|
|
`integration-failed`, `unauthenticated`와 credential attach rejection을 anonymous
|
|
request로 downgrade하지 않는다.
|
|
|
|
#### 5-0. Logical effect certainty는 단조 증가한다
|
|
|
|
`PhysicalAttemptState`는 현재 attempt만 설명한다. logical execution 전체에는
|
|
별도의 monotonic accumulator를 두고 `joinMutationEffectCertainty`로 join한다.
|
|
join 순서는 보수적이다.
|
|
|
|
```text
|
|
NOT_STARTED < NOT_APPLIED < MAYBE_APPLIED < APPLIED_CONFIRMED
|
|
```
|
|
|
|
`fetch()` dispatch 시점에 command는 즉시 `MAYBE_APPLIED`를 기록한다. 이후 retry
|
|
loop entry, pre-dispatch final invariant, scope fence, cancellation, timeout
|
|
return은 모두 accumulator를 읽는다. 아직 보내지 않은 새 retry가 있다는 이유로
|
|
전체 logical operation을 `NOT_STARTED`로 되돌리지 않는다. query operation은
|
|
`NOT_APPLICABLE`로 남고 이 lattice를 쓰지 않는다.
|
|
|
|
#### 5-1. Installed auth profile registry (V3 집행)
|
|
|
|
`installRestAuthProfileRegistry()`가 composition 시점에 profile을 한 번 설치하고
|
|
`INSTALLED_REST_AUTH_PROFILES`가 유일한 authority다. contract composition
|
|
(`assertExecutionPolicy`)은 등록되지 않은 `authProfileId`를 거절하므로 executor는
|
|
runtime에 profile을 발명하지 않는다. profile은 다음을 exact하게 소유한다.
|
|
|
|
- Fetch `credentials` (credential collaborator가 바꿀 수 없다)
|
|
- `allowedCredentialHeaders`: 이 operation이 허용하는 정확한 proof header 집합
|
|
- `requiredCredentialHeaders`: dispatch 전에 반드시 관찰되어야 하는 집합
|
|
|
|
`CredentialPatchOutcome.READY`는 proof header만 담는다. `credentials` field는
|
|
제거되었다. credential owner가 transport-owned header(`accept`, `content-type`,
|
|
`idempotency-key`)나 forbidden header를 넣거나, profile이 허용하지 않는 header를
|
|
넣거나, required header를 빠뜨리면 `AUTH_INTEGRATION_FAILURE`이고 fetch 0회이며
|
|
command effect는 `NOT_STARTED`다. `idempotency-key`는 contract-owned이므로 더
|
|
구체적인 `UNEXPECTED_IDEMPOTENCY_KEY` request violation으로 남는다.
|
|
`UNAUTHENTICATED`는 user/session state이지 integration failure가 아니다.
|
|
|
|
transport-owned header는 credential header 뒤에 기록되어 key ordering으로도
|
|
shadow될 수 없고, final invariant가 `init.credentials`와 profile을 다시 대조하며
|
|
allowed/required credential header 집합을 독립적으로 재검증한다.
|
|
|
|
`AUTH_MODE=demo`는 profile을 약화시키지 않는다. `createDemoSessionAdapter`가
|
|
고정된 비밀 아닌 `DEMO_AUTHORIZATION_MARKER` proof header를 제공하여 strict
|
|
`REFERENCE_EXTERNAL_BEARER`를 그대로 만족시킨다. 진짜 anonymous backend는 별도
|
|
anonymous contract/profile을 composition에서 선택해야 한다.
|
|
|
|
credential collaborator는 `AuthOperationContext { signal, deadlineAtMonotonicMs }`를
|
|
받는다. cooperative owner는 스스로 중단하고, non-cooperative owner도 executor가
|
|
같은 lifetime signal과 race하므로 operation 수명을 넘기지 못하며 late completion은
|
|
관찰되지 않는다.
|
|
|
|
### 6. Cookie auth, CSRF와 CORS
|
|
|
|
same-origin BFF cookie session을 기본 권장한다.
|
|
|
|
- cookie는 Secure/HttpOnly이며 provider가 SameSite 정책을 소유한다.
|
|
- unsafe method는 server의 exact Origin/Fetch Metadata 검증과 composition-issued
|
|
anti-CSRF proof를 요구한다.
|
|
- CSRF proof는 application/query/cache에 노출하거나 persistence하지 않는다.
|
|
- custom content type/preflight가 있다는 사실만 CSRF 방어로 간주하지 않는다.
|
|
|
|
cross-origin profile은 다음을 actual provider에서 증명한다.
|
|
|
|
- exact `Access-Control-Allow-Origin`, wildcard 금지
|
|
- credentials mode와 allow-credentials 일치
|
|
- exact allow-method/allow-header
|
|
- 필요한 request ID, ETag, Retry-After만 expose
|
|
- OPTIONS와 actual response의 policy 동등성
|
|
- redirect 없음
|
|
|
|
### 7. Replay와 idempotency
|
|
|
|
```text
|
|
ReplayPolicy
|
|
SAFE
|
|
IDEMPOTENT
|
|
KEYED_COMMAND
|
|
NON_REPLAYABLE
|
|
```
|
|
|
|
- `SAFE`: read-only이며 network/recovery retry 가능
|
|
- `IDEMPOTENT`: 같은 principal/operation/payload/precondition의 반복 request가
|
|
의도한 server effect를 추가로 만들지 않으며 duplicate response/status mapping을
|
|
backend contract가 명시한다. response byte가 항상 동일하다는 뜻은 아니다.
|
|
- `KEYED_COMMAND`: application logical command lease가 key를 생성하고 lifecycle
|
|
전체에서 유지
|
|
- `NON_REPLAYABLE`: ambiguous result에서 자동 재실행 금지
|
|
|
|
`KEYED_COMMAND` binding:
|
|
|
|
```text
|
|
principal scope
|
|
operation ID/version
|
|
canonical payload digest
|
|
idempotency key
|
|
server retention/expiry
|
|
```
|
|
|
|
backend는 atomic claim, concurrent same-key join/replay, same-key different-payload
|
|
rejection과 terminal receipt를 제공한다. header를 보냈다는 사실만 replay safety가
|
|
아니다. provider conformance가 없으면 retry/401 recovery traffic을 켜지 않는다.
|
|
|
|
current client가 execute 호출마다 key를 생성하는 방식은 network attempt 안에서는
|
|
재사용되지만 ambiguous terminal 뒤 사용자 retry와 연결되지 않는다. 목표 command
|
|
owner가 effect certainty를 다음처럼 반환한다.
|
|
|
|
```text
|
|
NOT_APPLIED | COMMITTED | UNKNOWN
|
|
```
|
|
|
|
`UNKNOWN`은 새 key 자동 retry가 아니라 status/reconcile 또는 명시적 사용자 복구로
|
|
닫는다.
|
|
|
|
### 8. Logical deadline, cancel과 retry
|
|
|
|
```text
|
|
LogicalExecutionBudget
|
|
totalDeadlineMs
|
|
attemptTimeoutMs
|
|
maxAttempts
|
|
maxCumulativeSleepMs
|
|
maxRetryAfterMs
|
|
authRecoveryCount = 0 | 1
|
|
```
|
|
|
|
total deadline은 다음 모두를 포함한다.
|
|
|
|
- operation/schema lookup과 request encode
|
|
- credential/CSRF attachment
|
|
- fetch attempt
|
|
- body read/decode/schema/mapper
|
|
- 401 recovery
|
|
- retry backoff/Retry-After
|
|
|
|
각 phase는 남은 total budget보다 긴 timer를 만들지 않는다. timer, abort listener,
|
|
response reader와 auth waiter는 모든 terminal path에서 정리한다.
|
|
|
|
초회, network/status retry와 401 recovery replay를 포함한 **모든 API provider
|
|
fetch**는 하나의 monotonic `physicalAttemptCount`를 증가시키고 `maxAttempts`를
|
|
소비한다. `authRecoveryCount`는 추가 상한일 뿐 attempt counter, sleep budget이나
|
|
total deadline을 reset하거나 우회하지 않는다.
|
|
|
|
retry status는 operation의 exact subset만 허용한다.
|
|
|
|
```text
|
|
network failure
|
|
408
|
|
429
|
|
502
|
|
503
|
|
504
|
|
```
|
|
|
|
- 429와 503의 `Retry-After`를 injected clock으로 parse한다.
|
|
- hard ceiling을 넘는 Retry-After는 sleep하지 않고 terminal로 닫는다.
|
|
- full jitter와 attempt/sleep/elapsed 세 상한을 모두 적용한다.
|
|
- schema, mapper, 4xx validation/authz/conflict와 redirect failure는 retry하지 않는다.
|
|
- caller cancel, navigation supersede, runtime teardown, attempt timeout과 logical
|
|
deadline을 다른 safe failure로 유지한다.
|
|
- 401 recovery는 auth-required이면서 replay-safe한 operation만 한 번 수행한다.
|
|
- 동시 401은 session owner의 single-flight recovery를 공유한다.
|
|
- physical attempt, logical retry와 auth replay를 따로 관측한다.
|
|
|
|
### 9. Closed execution
|
|
|
|
public executor는 promise rejection 대신 항상 closed result로 끝난다.
|
|
|
|
```text
|
|
RestExecutionResult<T>
|
|
SUCCESS
|
|
VALIDATION_REJECTED
|
|
AUTH_REQUIRED | AUTH_INTEGRATION_FAILURE
|
|
REQUEST_ABORTED
|
|
REQUEST_ATTEMPT_TIMEOUT | REQUEST_DEADLINE_EXCEEDED
|
|
NETWORK_UNREACHABLE
|
|
RATE_LIMITED | SERVER_FAILURE
|
|
HTTP_FAILURE
|
|
CONTENT_TYPE_MISMATCH | BODY_LIMIT_EXCEEDED | MALFORMED_BODY
|
|
SCHEMA_MISMATCH | MAPPING_CONTRACT_VIOLATION
|
|
PRECONDITION_FAILED | CONFLICT
|
|
CONTRACT_INCOMPATIBLE
|
|
```
|
|
|
|
attempt timer와 전체 logical deadline은 error registry, safe user copy와 telemetry
|
|
bucket에서도 별도 closed kind로 유지한다. 둘 다 raw URL/timing detail을
|
|
노출하지 않는다.
|
|
|
|
operation lookup, URL construction, `Headers`, body serialization, `Request`,
|
|
auth collaboration, fetch, read, parse, schema와 mapper를 모두 catch/normalize
|
|
경계 안에 둔다. thrown value/body/header/URL을 failure에 복사하지 않는다.
|
|
|
|
### 10. Response admission과 bounded decoder
|
|
|
|
body를 `Response.json()`으로 바로 읽지 않는다.
|
|
|
|
```text
|
|
response
|
|
-> final URL/status/header admission
|
|
-> exact media type parser
|
|
-> present/valid Content-Length advisory preflight
|
|
-> bounded stream reader
|
|
-> actual browser-visible decoded byte count
|
|
-> UTF-8/profile decoder
|
|
-> JSON structural ceiling
|
|
-> envelope/status codec
|
|
-> operation response codec
|
|
-> mapper
|
|
```
|
|
|
|
browser Fetch의 `Response.body`는 일반적으로 content decoding 뒤 stream이므로
|
|
client가 actual wire/encoded byte를 신뢰성 있게 세었다고 주장하지 않는다.
|
|
|
|
- BFF/proxy/CDN가 encoded transfer와 decompression ratio ceiling을 집행한다.
|
|
- browser client는 present/valid `Content-Length`를 advisory rejection에만 쓰고
|
|
actual decoded bytes를 hard cap으로 센다.
|
|
- 표준 `JSON.parse` profile은 decoded-byte cap이 pre-parse resource guard이고
|
|
depth/node/key/string/item cap은 materialization 뒤 admission guard다.
|
|
- 구조 cap을 parse 중 강제해야 하는 더 큰 profile은 bounded tokenizing JSON
|
|
parser를 별도로 선택하고 actual browser evidence를 가져야 한다.
|
|
|
|
cap 초과, truncation과 invalid UTF-8에서 reader를 cancel하고 cache에 쓰지 않는다.
|
|
|
|
media parser는 type/subtype/parameter를 exact하게 해석한다.
|
|
|
|
- `application/json`
|
|
- approved vendor `application/*+json`
|
|
- RFC Problem Details profile
|
|
- explicit 204 no-content
|
|
|
|
`includes("application/json")` 검사는 목표 계약이 아니다.
|
|
|
|
각 success status는 response codec을 가진다. 현재 envelope는
|
|
`REST_ENVELOPE_V1` profile로 유지할 수 있지만 모든 REST provider에 강제하지
|
|
않는다. success status와 error body가 모순되면 status/profile 계약 실패다.
|
|
|
|
### 11. Error projection
|
|
|
|
backend error는 먼저 status/media별 codec을 통과한다.
|
|
|
|
- raw message, stack, body와 arbitrary extensions를 버린다.
|
|
- code/category는 operation error profile의 allowlist로 mapping한다.
|
|
- unknown backend code는 closed generic failure다.
|
|
- validation issue는 최대 count, path/code byte/charset와 allowed path를 제한한다.
|
|
- request ID, trace ID와 correlation ID도 length/charset cap을 적용한다.
|
|
- `retryable` backend boolean을 client retry authority로 사용하지 않는다.
|
|
- 401/403/404 existence-hiding은 provider/product policy를 따른다.
|
|
- 409 business conflict와 412 representation precondition failure를 분리한다.
|
|
|
|
### 12. Cursor pagination
|
|
|
|
```text
|
|
CursorPageWire<T>
|
|
items
|
|
nextCursor | null
|
|
hasMore
|
|
snapshotToken | null
|
|
```
|
|
|
|
response codec은 item count, item size, cursor/snapshot byte와 total decoded ceiling을
|
|
검증한다. mapper는 immutable `CursorPage<ApplicationProjection>`을 만든다.
|
|
|
|
- cursor는 opaque이며 decode/로그/telemetry 금지
|
|
- arbitrary next URL을 따라가지 않음
|
|
- filters/sort/scope/snapshot과 cursor를 exact binding
|
|
- same cursor 반복, `hasMore=true`인데 cursor 없음, non-progress loop 거절
|
|
- maximum pages/items/cache bytes 이후 fetch 중지
|
|
- TanStack infinite query의 root key에는 semantic filter만 넣고 cursor는 bounded
|
|
`pageParam`으로 관리
|
|
- offset pagination은 stable small dataset이 증명된 별도 profile만 허용
|
|
|
|
current reference list array는 complete pagination 구현이 아니다.
|
|
|
|
### 13. Conditional read와 optimistic concurrency
|
|
|
|
operation별 owner:
|
|
|
|
```text
|
|
ConditionalProfile
|
|
NO_STORE
|
|
APP_ETAG
|
|
BROWSER_HTTP_CACHE
|
|
APPLICATION_REVISION
|
|
```
|
|
|
|
모든 operation은 한 profile을 가져야 한다.
|
|
|
|
- `NO_STORE`, `APP_ETAG`, `APPLICATION_REVISION`은 Fetch `cache=no-store`.
|
|
- `BROWSER_HTTP_CACHE`만 exact browser cache mode와 server
|
|
`Cache-Control`/`Vary` contract를 사용한다.
|
|
- caller/library default에 맡기는 implicit `NONE`은 없다.
|
|
|
|
`APP_ETAG`:
|
|
|
|
- strong ETag/generation은 adapter-private metadata
|
|
- exact operation/query/scope/representation binding과 함께 memory에 보존
|
|
- `If-None-Match`를 transport가 생성
|
|
- 304는 same binding의 mapped cached value가 있을 때만 freshness 갱신
|
|
- 304는 same query-entry cache revision CAS가 성공할 때만 commit
|
|
- cached value가 없으면 one-time unconditional request 또는 closed failure
|
|
- app-managed conditional operation은 fetch cache mode를 `no-store`로 고정
|
|
- full 200 mapped commit과 validator install/update는 같은 entry transaction
|
|
- query removal/GC, scope/logout, release/contract/schema/mapper epoch reset에서
|
|
validator sidecar도 함께 폐기
|
|
- ordinary invalidation에서 validator 보존 여부는 profile이 고정
|
|
|
|
`BROWSER_HTTP_CACHE`:
|
|
|
|
- standard browser cache가 revalidation을 소유
|
|
- application이 hidden validator/304 logic을 중복 구현하지 않음
|
|
- HTTP `Cache-Control`을 TanStack `staleTime`으로 자동 변환하지 않음
|
|
|
|
write precondition:
|
|
|
|
- exact resource revision을 `If-Match` 또는 body contract로 binding
|
|
- 412는 `PRECONDITION_FAILED`
|
|
- current server representation을 refetch한 뒤 feature use case가 overwrite,
|
|
merge 또는 cancel을 결정
|
|
- 409 business conflict와 합치지 않음
|
|
|
|
ETag/revision은 authorization proof가 아니며 raw value를 diagnostics에 넣지 않는다.
|
|
|
|
### 14. Schema와 Mapper 연결
|
|
|
|
VD-24의 typed codec/mapper를 사용한다.
|
|
|
|
```text
|
|
unknown
|
|
-> RuntimeCodec<ValidatedResponseDto>
|
|
-> Mapper<ValidatedResponseDto, ApplicationProjection>
|
|
-> Result
|
|
```
|
|
|
|
request는 strict/normalized parsed output만 serialize한다. ordinary additive
|
|
response는 required/discriminant를 검증하고 unknown field를 폐기한다. control,
|
|
authorization와 sealed union은 unknown을 거절한다.
|
|
|
|
generated OpenAPI client/DTO를 선택해도 adapter-private이다. handwritten gateway와
|
|
mapper를 제거하지 않는다.
|
|
|
|
### 15. Query cache와의 관계
|
|
|
|
REST transport는 raw response cache를 소유하지 않는다. VD-25가 mapped application
|
|
projection을 TanStack Query에 admission한다.
|
|
|
|
- operation registry의 cache profile만 query를 만들 수 있다.
|
|
- Query retry는 `false`; REST transport가 network retry를 소유한다.
|
|
- URL, ETag, idempotency key, Response와 DTO를 query key/value에 넣지 않는다.
|
|
- mutation success 뒤 registered invalidation topic 또는 exact typed seed policy만
|
|
사용한다.
|
|
- REST timeout/error를 Query가 다시 network retry하지 않는다.
|
|
|
|
### 16. Contract source와 release coherence
|
|
|
|
OpenAPI가 backend authority인 제품:
|
|
|
|
```text
|
|
authenticated immutable OpenAPI artifact
|
|
-> source digest/provenance
|
|
-> lint + breaking diff
|
|
-> pinned deterministic generation
|
|
-> runtime codec parity
|
|
-> adapter-private DTO/client
|
|
-> handwritten mapper/gateway
|
|
```
|
|
|
|
CI:
|
|
|
|
- generator/runtime/plugin/Node version pin
|
|
- clean checkout regenerate diff 0
|
|
- stable operation ID
|
|
- source/artifact/generated output digest
|
|
- runtime codec vs specification fixtures
|
|
- N/N-1 and future-major failure
|
|
- generated import boundary
|
|
|
|
global `API_CONTRACT_VERSION`은 selected contract-set compatibility와 digest에
|
|
연결한다. runtime config와 release manifest의 같은 문자열만으로 backend
|
|
compatibility를 주장하지 않는다.
|
|
|
|
major version 전략은 URI version 또는 vendor media version 중 provider가 하나를
|
|
선택한다. 동시에 둘을 임의 증가시키지 않는다. v1/v2 adapters는 같은
|
|
application gateway를 구현할 수 있지만 같은 query entry에 representation을
|
|
섞지 않는다.
|
|
|
|
### 17. Security와 observability
|
|
|
|
관측 허용:
|
|
|
|
- semantic operation/provider/profile ID
|
|
- method/semantics
|
|
- outcome/AppFailure kind/HTTP status group
|
|
- logical retry, auth recovery와 physical attempt bucket
|
|
- duration/deadline/request-response byte/item bucket
|
|
- conditional/cache outcome
|
|
|
|
금지:
|
|
|
|
- URL, path/search/header/body
|
|
- cursor/snapshot/ETag/revision
|
|
- idempotency/CSRF/auth token
|
|
- raw backend code/message/request/trace value
|
|
- application resource/account/tenant ID
|
|
|
|
client correlation ID는 bounded syntax로 request에 전달하고 server-projected
|
|
request/trace ID는 safe failure/observation에만 제한한다. 한 logical execution은
|
|
terminal event 하나를 만든다.
|
|
|
|
### 18. Composition과 readiness
|
|
|
|
REST v2 composition owner가 다음을 atomic하게 만든다.
|
|
|
|
```text
|
|
parse provider/config
|
|
-> compose collision-free operation registry
|
|
-> resolve codec/mapper/policy references
|
|
-> install auth/CSRF owner
|
|
-> run compatibility/provider probes
|
|
-> publish READY facade
|
|
```
|
|
|
|
partial registry/client를 application에 노출하지 않는다.
|
|
|
|
```text
|
|
Selection
|
|
TrafficAdmission
|
|
RuntimeHealth
|
|
PromotionEvidence
|
|
```
|
|
|
|
primary status가 `COMPOSED`여도 provider/auth/CSRF/idempotency/conditional
|
|
conformance가 없으면 `TrafficAdmission=DISABLED`와
|
|
`PromotionEvidence=MISSING | PARTIAL | EXPIRED`로 닫는다.
|
|
|
|
kill switch:
|
|
|
|
- provider 전체
|
|
- operation family
|
|
- keyed retry/401 replay
|
|
- conditional request
|
|
- optimistic mutation
|
|
- query cache admission
|
|
|
|
### 19. Test와 provider conformance
|
|
|
|
deterministic:
|
|
|
|
- contribution collision, missing codec/mapper/profile와 owner mismatch
|
|
- URI template/path/query canonicalization/base-prefix preservation
|
|
- method/body/replay/status/media coherence
|
|
- anonymous/cookie/bearer credentials와 Fetch cache mode matrix
|
|
- auth final-request mutation 공격과 unavailable auth fetch 0
|
|
- attempt timeout vs total deadline, timer/listener/reader cleanup
|
|
- concurrent 401 single-flight
|
|
- 401 replay를 포함한 monotonic physical-attempt cap
|
|
- retry matrix, injected-clock 429/503 Retry-After
|
|
- same key/same payload replay와 same key/different payload rejection
|
|
- 204/304/412/422/problem/envelope
|
|
- oversized/truncated/malformed/decompression overflow
|
|
- cursor loop/snapshot/page ceiling
|
|
- ETag 304 without cache, If-Match 412
|
|
- mapper failure와 redaction
|
|
|
|
actual staging provider:
|
|
|
|
- HTTPS/base path/CORS/preflight/credential
|
|
- cookie/Origin/CSRF
|
|
- idempotency concurrent claim/TTL/reconcile
|
|
- status/media/error codec
|
|
- cursor/snapshot/conditional semantics
|
|
- rate limit/Retry-After
|
|
- correlation/request/trace projection
|
|
- proxy/CDN content encoding and body cap
|
|
- browser decoded-byte cap과 provider encoded/decompression ceiling
|
|
- outbound correlation, success status group와 401 physical-attempt observation
|
|
|
|
MSW 통과는 provider conformance가 아니다.
|
|
|
|
### 20. Rollout
|
|
|
|
1. collision-aware v2 registry/codec과 boot-time binding 검증을 설치한다.
|
|
2. auth fail-closed, final invariant, bounded decoder와 total deadline을 local
|
|
reference vertical에서 검증한다.
|
|
3. actual provider에 같은 fixture를 실행하고 read operation을 canary한다.
|
|
4. keyed command는 backend idempotency conformance 뒤 별도 canary한다.
|
|
5. pagination/conditional operation을 각각 별도 traffic gate로 올린다.
|
|
6. provider/browser/operations evidence가 complete인 operation만 enabled한다.
|
|
7. rollback은 우선 safe unavailable로 내리고 contract artifact/frontend/backend를
|
|
coherent set으로 복구한다. v1 fallback은 해당 operation의 unexpired
|
|
provider/security evidence가 있고 incident가 v1/shared boundary에 영향이 없으며
|
|
auth fail-close/final invariant hardening이 유지될 때만 허용한다.
|
|
|
|
### 21. Removal
|
|
|
|
GraphQL/Connect/gRPC-Web/REST Gateway 선택을 취소해도 REST v2 common execution
|
|
context는 남을 수 있다.
|
|
REST provider 제거 시:
|
|
|
|
1. 신규 operation admission 중지
|
|
2. read cancel, command effect certainty reconcile
|
|
3. auth/CSRF/retry timer와 response reader close
|
|
4. current scope query cache clear/invalidate
|
|
5. operation/schema/mapper/query profile 제거
|
|
6. provider config/proxy/dependency/fixture 제거
|
|
7. production module inventory와 backend route retirement evidence
|
|
|
|
## 완료 기준
|
|
|
|
- installed REST operation이 typed path/search/body/success/error codec과 mapper에
|
|
하나의 definition으로 연결된다.
|
|
- auth unavailable 또는 mutated final request에서 fetch가 0회다.
|
|
- 모든 throw/response size/status/media/schema/mapper failure가 closed result다.
|
|
- total logical deadline이 auth/recovery/backoff/decode/mapper를 포함한다.
|
|
- replay는 declared policy와 actual backend idempotency evidence를 가진다.
|
|
- complete cursor page와 conditional/412 state가 bounded하게 동작한다.
|
|
- response DTO/URL/header/token/validator가 application/query/log에 없다.
|
|
- actual provider conformance와 rollback/removal drill이 통과한다.
|