C.W.K.
Stream
Lesson 02 of 04 · published

도메인 모델

~15 min · firelink, domain-model, capability, types

Level 0식은 재
0 XP0/32 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"명사 넷, 그리고 그중 하나도 raw command 가 아냐."

네 개의 객체

Firelink 세계 전부가 네 개의 typed 객체야. FamilyMember 는 repository 또는 제품 정체성 — id, display metadata, lifecycle 상태, repository 정체성, host affinity, 그리고 0개 이상의 surface 와 capability. LaunchSurface 는 멤버가 만들어내는 사용자용 서비스나 앱 — 안정된 target id, 종류(web 또는 native-app), label, 그리고 context-specific URL 이거나 native bundle id. Capability 는 허브가 멤버에 제공할 수 있는 typed operation. ObservedState 는 census 중 측정된 read-only 사실.

없는 걸 봐. '유저가 타이핑한 명령'을 위한 객체가 없어. operation 은 절대 시스템을 떠도는 raw 문자열이 아냐. policy 가 이름 붙이고 resolve 하는 typed capability 야. 그 하나의 부재가 data 모델에 새겨진 보안 결정이야.

Capability 는 backend 가 resolve 하지, 절대 추론 안 해

capability 는 inspect-git, commit-and-push, birth-resume, 특정 adapter 용 deploy-native, 특정 service id 용 restart-service 같은 값이야. 결정적 규칙: capability 는 backend policy 가 resolve 하고, frontend 는 non-null label 이나 시각적 card 타입에서 operation 을 절대 추론하지 않아. UI 는 카드를 보고 '이거 앱 같네' 정하고 Deploy 버튼을 보이지 않아. backend 가 '이 멤버는 이 capability 들이 있고, 이건 이 이유로 disabled 야'라고 말하고, UI 는 딱 그걸 렌더해. deploy capability 없는 멤버는 selector 를 아예 안 렌더해.

Observed state 는 자기가 모르는 걸 알아

ObservedState 는 선언이 아니라 측정이고, 자기 정직함을 지녀. 모든 관찰이 measured_at 타임스탬프와 probe source 를 기록하고 — 여기가 미묘한 부분 — unknownstoppedmissing 과 분리해서 유지해. '알 수 없었다'는 '죽었다'와도, '설치 안 됨'과도 다른 사실이야. 셋을 하나의 boolean 으로 뭉개는 모델은 언젠가 probe 실패를 outage 로, 또는 outage 를 '괜찮음'으로 보고해. Firelink 는 그 틈에서 추측하기를 거부해.

operation 을 policy 가 resolve 하는 typed capability 로 모델링해, UI 가 추론하는 문자열로 하지 말고. 가능한 operation 집합이 서버에서 resolve 되는 닫힌 typed 목록이면, 클라이언트는 backend 가 authorize 안 한 액션을 지어낼 수 없어 — 타입 시스템이 권한 경계가 돼.

Code

네 객체 — 그리고 Capability 는 닫힌 typed 집합·typescript
type Lifecycle = "scaffold" | "active" | "attic" | "legacy";

type Capability =
  | { kind: "inspect-git" }
  | { kind: "commit-and-push" }
  | { kind: "birth-resume" }
  | { kind: "deploy-native"; adapterId: string }
  | { kind: "deploy-tree"; adapterId: string }
  | { kind: "restart-service"; serviceId: string };
// There is NO { kind: "run"; command: string }. Raw commands aren't in the model.

type ObservedState = {
  measuredAt: string;                 // every fact is timestamped
  source: string;                     // which probe produced it
  service: "running" | "stopped" | "missing" | "unknown";  // 'unknown' != 'stopped'
};

type FamilyMember = {
  id: string;
  lifecycle: Lifecycle;
  surfaces: LaunchSurface[];
  capabilities: Capability[];         // resolved by BACKEND policy, not the UI
  observed: ObservedState;
};

External links

Exercise

네가 짓거나 쓴, 항목마다 액션 버튼을 보이는 UI 를 봐. 클라이언트가 어느 버튼을 보일지 정해(항목을 검사해서), 아니면 서버가 authorize 된 목록을 넘겨줘? 가시성이 클라이언트에서 추론되는 버튼 하나를 찾아. 그 추론이 틀리는 공격이나 버그를 설명해봐 — 보이면 안 되는 버튼이 보이거나, 서버가 authorize 안 한 액션이 실행되거나 — 그리고 서버-resolve capability 목록이 그걸 어떻게 닫는지.
Hint
클라이언트-추론 버튼은 두 방향으로 실패해. 그다음 에러 나는 액션을 보이거나, 더 나쁘게, UI 가 '숨긴' 액션을 조작된 요청이 트리거하는데 backend 는 여전히 받아. 서버-resolve capability 는 authority 가 한 곳에 살아서 둘 다 고쳐.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.