@wooojin/forgen 0.4.8 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/assets/dev-guide/be/README.md +226 -0
  3. package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
  4. package/assets/dev-guide/be/principles/common.md +433 -0
  5. package/assets/dev-guide/be/principles/go.md +469 -0
  6. package/assets/dev-guide/be/principles/node.md +388 -0
  7. package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
  8. package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
  9. package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
  10. package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
  11. package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
  12. package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
  13. package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
  14. package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
  15. package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
  16. package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
  17. package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
  18. package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
  19. package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
  20. package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
  21. package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
  22. package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
  23. package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
  24. package/assets/dev-guide/fe/README.md +197 -0
  25. package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
  26. package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
  27. package/assets/dev-guide/fe/principles/common.md +160 -0
  28. package/assets/dev-guide/fe/principles/react.md +183 -0
  29. package/assets/dev-guide/fe/principles/vue.md +196 -0
  30. package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
  31. package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
  32. package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
  33. package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
  34. package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
  35. package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
  36. package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
  37. package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
  38. package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
  39. package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
  40. package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
  41. package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
  42. package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
  43. package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
  44. package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
  45. package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
  46. package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
  47. package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
  48. package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
  49. package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
  50. package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
  51. package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
  52. package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
  53. package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
  54. package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
  55. package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
  56. package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
  57. package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
  58. package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
  59. package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
  60. package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
  61. package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
  62. package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
  63. package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
  64. package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
  65. package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
  66. package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
  67. package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
  68. package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
  69. package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
  70. package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
  71. package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
  72. package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
  73. package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
  74. package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
  75. package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
  76. package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
  77. package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
  78. package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
  79. package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
  80. package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
  81. package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
  82. package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
  83. package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
  84. package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
  85. package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
  86. package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
  87. package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
  88. package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
  89. package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
  90. package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
  91. package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
  92. package/dist/cli.js +42 -0
  93. package/dist/core/dashboard-cli.d.ts +12 -0
  94. package/dist/core/dashboard-cli.js +226 -0
  95. package/dist/core/dev-guide-injector.d.ts +26 -0
  96. package/dist/core/dev-guide-injector.js +137 -0
  97. package/dist/core/init.js +53 -0
  98. package/dist/core/lifecycle-classifier.d.ts +23 -0
  99. package/dist/core/lifecycle-classifier.js +104 -0
  100. package/dist/core/observability-backfill.d.ts +31 -0
  101. package/dist/core/observability-backfill.js +178 -0
  102. package/dist/core/observability-store.d.ts +58 -0
  103. package/dist/core/observability-store.js +195 -0
  104. package/dist/core/session-store.js +4 -0
  105. package/dist/core/spawn.d.ts +17 -0
  106. package/dist/core/spawn.js +179 -2
  107. package/dist/core/statusline-cli.js +34 -1
  108. package/dist/engine/compound-extractor.js +39 -0
  109. package/dist/engine/compound-loop.js +6 -0
  110. package/dist/engine/compound-retire.d.ts +20 -0
  111. package/dist/engine/compound-retire.js +85 -0
  112. package/dist/hooks/context-guard.js +25 -1
  113. package/dist/hooks/post-tool-use.js +48 -0
  114. package/dist/hooks/solution-injector.js +93 -0
  115. package/dist/host/install-claude.d.ts +6 -2
  116. package/dist/host/install-claude.js +74 -2
  117. package/dist/host/install-codex.d.ts +4 -0
  118. package/dist/host/install-codex.js +71 -0
  119. package/dist/host/install-orchestrator.js +1 -0
  120. package/package.json +6 -6
  121. package/plugin.json +1 -1
  122. package/scripts/postinstall.js +134 -0
@@ -0,0 +1,362 @@
1
+ ---
2
+ name: be-security-go
3
+ description: Go 서비스를 OWASP API Security Top 10 기준으로 진단. 카테고리별 체크리스트와 Go 특화 픽스 패턴을 제공한다.
4
+ ---
5
+
6
+ # be-security (Go)
7
+
8
+ > **호출 시점**: "이 핸들러 보안 검토해줘", "OWASP 관점에서 체크해줘", "Go 서비스 보안 감사".
9
+ > **선행 로딩**: `principles/common.md` (E섹션) + `principles/go.md` 필수.
10
+
11
+ ## 0. 절대 금지
12
+
13
+ 1. gosec 린터가 잡는 이슈를 무시 (`//nolint:gosec`) 하려면 이유 주석 필수.
14
+ 2. 보안 이슈를 [LOW]로 다운그레이드 금지.
15
+ 3. `crypto/md5`, `crypto/sha1` 비밀번호 해싱 사용 금지 — bcrypt/argon2 사용.
16
+
17
+ ## 1. 워크플로우
18
+
19
+ ### Step 1 — gosec 자동 스캔
20
+
21
+ ```bash
22
+ # gosec 설치 및 실행
23
+ go install github.com/securego/gosec/v2/cmd/gosec@latest
24
+ gosec -fmt sarif -out gosec-report.sarif ./...
25
+
26
+ # 또는 golangci-lint (gosec 포함)
27
+ golangci-lint run --enable gosec ./...
28
+ ```
29
+
30
+ 자동 스캔 후 수동 체크리스트 실행.
31
+
32
+ ### Step 2 — OWASP Top 10 체크리스트
33
+
34
+ ### Step 3 — 취약점 보고 및 픽스
35
+
36
+ ## 2. OWASP API Top 10 체크리스트 (Go 특화)
37
+
38
+ ### API1 — Broken Object Level Authorization
39
+
40
+ ```go
41
+ // WRONG: URL params 신뢰
42
+ func (h *OrderHandler) Get(w http.ResponseWriter, r *http.Request) {
43
+ orderID := chi.URLParam(r, "orderID")
44
+ order, _ := h.repo.FindByID(r.Context(), orderID)
45
+ json.NewEncoder(w).Encode(order) // 다른 유저 주문 접근 가능
46
+ }
47
+
48
+ // RIGHT: 소유권 검증
49
+ func (h *OrderHandler) Get(w http.ResponseWriter, r *http.Request) {
50
+ orderID := chi.URLParam(r, "orderID")
51
+ userID := userIDFromCtx(r.Context()) // JWT에서 추출
52
+
53
+ order, err := h.repo.FindByID(r.Context(), orderID)
54
+ if err != nil {
55
+ if errors.Is(err, domain.ErrNotFound) {
56
+ respondError(w, http.StatusNotFound, "NOT_FOUND", "Order not found", r)
57
+ return
58
+ }
59
+ respondError(w, http.StatusInternalServerError, "INTERNAL", "서버 오류", r)
60
+ return
61
+ }
62
+ if order.UserID != userID { // 소유권 검증
63
+ respondError(w, http.StatusForbidden, "FORBIDDEN", "접근 권한 없음", r)
64
+ return
65
+ }
66
+ json.NewEncoder(w).Encode(order)
67
+ }
68
+ ```
69
+
70
+ ```
71
+ [ ] 모든 리소스 핸들러에 소유권 검증
72
+ [ ] JWT claims에서 userID 추출 (URL params 신뢰 금지)
73
+ [ ] 관리자 핸들러에도 역할 검증 (미들웨어 단독 의존 금지)
74
+ ```
75
+
76
+ ### API2 — Broken Authentication
77
+
78
+ ```go
79
+ import "github.com/golang-jwt/jwt/v5"
80
+
81
+ // WRONG: 알고리즘 검증 없음
82
+ token, _ := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
83
+ return secret, nil // 알고리즘 검증 안 함 → none 공격 가능
84
+ })
85
+
86
+ // RIGHT: 알고리즘 명시
87
+ token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
88
+ if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
89
+ return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
90
+ }
91
+ return publicKey, nil
92
+ }, jwt.WithExpirationRequired()) // 만료 검증 강제
93
+ ```
94
+
95
+ ```
96
+ [ ] JWT 서명 알고리즘 명시 (RS256 권장)
97
+ [ ] 만료 검증 (exp claim)
98
+ [ ] 비밀키 환경변수에서 로드
99
+ [ ] API Key 해시 저장 (평문 DB 저장 금지)
100
+ ```
101
+
102
+ ### API3 — Broken Object Property Level Authorization
103
+
104
+ ```go
105
+ // WRONG: 전체 모델 반환
106
+ type User struct {
107
+ ID string `json:"id"`
108
+ Email string `json:"email"`
109
+ PasswordHash string `json:"passwordHash"` // 노출!
110
+ InternalFlag bool `json:"internalFlag"` // 노출!
111
+ }
112
+
113
+ // RIGHT: 응답 전용 구조체
114
+ type UserResponse struct {
115
+ ID string `json:"id"`
116
+ Email string `json:"email"`
117
+ Name string `json:"name"`
118
+ }
119
+
120
+ func toUserResponse(u *User) UserResponse {
121
+ return UserResponse{ID: u.ID, Email: u.Email, Name: u.Name}
122
+ }
123
+ ```
124
+
125
+ ```
126
+ [ ] 응답 전용 DTO/구조체 정의 (DB 모델 직접 직렬화 금지)
127
+ [ ] password, hash, secret, internal 태그 필드 응답 제외
128
+ [ ] json:"-" 태그로 민감 필드 직렬화 차단
129
+ ```
130
+
131
+ ### API4 — Unrestricted Resource Consumption
132
+
133
+ ```go
134
+ import "golang.org/x/time/rate"
135
+
136
+ // Rate limiter (토큰 버킷)
137
+ type RateLimitMiddleware struct {
138
+ limiter *rate.Limiter
139
+ }
140
+
141
+ func NewRateLimitMiddleware(rps float64, burst int) *RateLimitMiddleware {
142
+ return &RateLimitMiddleware{limiter: rate.NewLimiter(rate.Limit(rps), burst)}
143
+ }
144
+
145
+ func (m *RateLimitMiddleware) Handler(next http.Handler) http.Handler {
146
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
147
+ if !m.limiter.Allow() {
148
+ w.Header().Set("Retry-After", "60")
149
+ respondError(w, http.StatusTooManyRequests, "TOO_MANY_REQUESTS", "요청 한도 초과", r)
150
+ return
151
+ }
152
+ next.ServeHTTP(w, r)
153
+ })
154
+ }
155
+
156
+ // 요청 바디 크기 제한
157
+ r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB
158
+ ```
159
+
160
+ ```
161
+ [ ] Rate limiting 미들웨어 (전역 + 민감 엔드포인트)
162
+ [ ] http.MaxBytesReader 으로 바디 크기 제한
163
+ [ ] 페이지네이션 limit 상한 (예: min(limit, 100))
164
+ [ ] 429 응답에 Retry-After 헤더
165
+ ```
166
+
167
+ ### API5 — Broken Function Level Authorization
168
+
169
+ ```go
170
+ // 역할 미들웨어
171
+ func RequireRole(roles ...string) func(http.Handler) http.Handler {
172
+ return func(next http.Handler) http.Handler {
173
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
174
+ userRole := roleFromCtx(r.Context())
175
+ for _, role := range roles {
176
+ if userRole == role {
177
+ next.ServeHTTP(w, r)
178
+ return
179
+ }
180
+ }
181
+ respondError(w, http.StatusForbidden, "FORBIDDEN", "권한 없음", r)
182
+ })
183
+ }
184
+ }
185
+
186
+ // 라우터 설정
187
+ r.With(RequireRole("admin")).Delete("/api/admin/users/{id}", deleteUser)
188
+ ```
189
+
190
+ ```
191
+ [ ] 관리자 경로에 역할 미들웨어
192
+ [ ] /debug, /internal, /admin 경로 외부 노출 여부
193
+ [ ] HTTP 메서드별 권한 (DELETE가 GET보다 강한 권한 필요)
194
+ ```
195
+
196
+ ### API6 — Unrestricted Access to Sensitive Flows
197
+
198
+ ```go
199
+ // IP + 이메일 기반 rate limit
200
+ type AuthRateLimiter struct {
201
+ mu sync.Mutex
202
+ buckets map[string]*rate.Limiter
203
+ }
204
+
205
+ func (l *AuthRateLimiter) Allow(ip, email string) bool {
206
+ key := ip + ":" + email
207
+ l.mu.Lock()
208
+ limiter, ok := l.buckets[key]
209
+ if !ok {
210
+ limiter = rate.NewLimiter(rate.Every(time.Minute), 5) // 분당 5회
211
+ l.buckets[key] = limiter
212
+ }
213
+ l.mu.Unlock()
214
+ return limiter.Allow()
215
+ }
216
+ ```
217
+
218
+ ```
219
+ [ ] 로그인 IP + 이메일 조합 rate limit
220
+ [ ] OTP 코드 rate limit
221
+ [ ] 비밀번호 재설정 토큰 단기 만료 (15분)
222
+ [ ] 비밀번호 재설정 토큰 1회 사용 후 무효화
223
+ [ ] bcrypt/argon2id 비밀번호 해싱 (md5/sha1 금지)
224
+ ```
225
+
226
+ ### API7 — SSRF
227
+
228
+ ```go
229
+ var allowedHosts = map[string]bool{
230
+ "api.trustedpartner.com": true,
231
+ }
232
+
233
+ func isPrivateIP(ip net.IP) bool {
234
+ private := []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"}
235
+ for _, cidr := range private {
236
+ _, network, _ := net.ParseCIDR(cidr)
237
+ if network.Contains(ip) { return true }
238
+ }
239
+ return false
240
+ }
241
+
242
+ func validateWebhookURL(rawURL string) error {
243
+ u, err := url.Parse(rawURL)
244
+ if err != nil { return fmt.Errorf("invalid URL: %w", err) }
245
+ if u.Scheme != "https" { return errors.New("only https allowed") }
246
+ if !allowedHosts[u.Hostname()] { return errors.New("host not allowed") }
247
+
248
+ addrs, err := net.LookupHost(u.Hostname())
249
+ if err != nil { return fmt.Errorf("DNS lookup: %w", err) }
250
+ for _, addr := range addrs {
251
+ if isPrivateIP(net.ParseIP(addr)) {
252
+ return errors.New("private IP not allowed")
253
+ }
254
+ }
255
+ return nil
256
+ }
257
+ ```
258
+
259
+ ```
260
+ [ ] 사용자 입력 URL fetch 전 allowlist 검증
261
+ [ ] private IP 차단
262
+ [ ] https만 허용
263
+ [ ] HTTP redirect follow 횟수 제한
264
+ ```
265
+
266
+ ### API8 — Security Misconfiguration
267
+
268
+ ```go
269
+ // 보안 헤더 미들웨어
270
+ func SecurityHeaders(next http.Handler) http.Handler {
271
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
272
+ w.Header().Set("X-Content-Type-Options", "nosniff")
273
+ w.Header().Set("X-Frame-Options", "DENY")
274
+ w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
275
+ w.Header().Set("Content-Security-Policy", "default-src 'self'")
276
+ next.ServeHTTP(w, r)
277
+ })
278
+ }
279
+
280
+ // CORS 설정
281
+ corsMiddleware := cors.New(cors.Options{
282
+ AllowedOrigins: []string{"https://app.example.com"},
283
+ AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
284
+ AllowCredentials: true,
285
+ })
286
+ ```
287
+
288
+ ```
289
+ [ ] 보안 헤더 설정 (HSTS, X-Frame-Options 등)
290
+ [ ] CORS origin allowlist (* 사용 금지)
291
+ [ ] 에러 응답에 스택 트레이스 미포함 (production)
292
+ [ ] /debug/pprof 프로덕션 비활성화 또는 인증 보호
293
+ [ ] 불필요한 HTTP 메서드 비활성화
294
+ ```
295
+
296
+ ### API9 — Improper Inventory Management
297
+
298
+ ```
299
+ [ ] API 버전 목록 문서화
300
+ [ ] 구 버전 폐기 계획 및 Sunset 날짜
301
+ [ ] 스테이징 엔드포인트 인터넷 노출 여부
302
+ [ ] pprof, expvar 엔드포인트 보호
303
+ ```
304
+
305
+ ### API10 — Unsafe Consumption of APIs
306
+
307
+ ```go
308
+ // 외부 API 응답 검증
309
+ type ExternalPaymentResponse struct {
310
+ TransactionID string `json:"transactionId"`
311
+ Status string `json:"status"`
312
+ Amount float64 `json:"amount"`
313
+ }
314
+
315
+ func parsePaymentResponse(body io.Reader) (*ExternalPaymentResponse, error) {
316
+ var resp ExternalPaymentResponse
317
+ if err := json.NewDecoder(body).Decode(&resp); err != nil {
318
+ return nil, fmt.Errorf("decode payment response: %w", err)
319
+ }
320
+ // 추가 검증
321
+ if resp.TransactionID == "" {
322
+ return nil, errors.New("missing transactionId in response")
323
+ }
324
+ validStatuses := map[string]bool{"success": true, "pending": true, "failed": true}
325
+ if !validStatuses[resp.Status] {
326
+ return nil, fmt.Errorf("unexpected status: %s", resp.Status)
327
+ }
328
+ return &resp, nil
329
+ }
330
+ ```
331
+
332
+ ```
333
+ [ ] 외부 API 응답 구조체 검증
334
+ [ ] 외부 API 타임아웃 설정
335
+ [ ] tls.Config InsecureSkipVerify = false (기본값)
336
+ [ ] 외부 오류 응답 내부로 전파 금지 (사용자에게 내부 서비스 정보 노출)
337
+ ```
338
+
339
+ ## 3. 출력 형식
340
+
341
+ ```
342
+ ## 보안 감사 결과 (Go)
343
+
344
+ ### gosec 자동 스캔: N issues
345
+ [G201] internal/repo/order.go:42 — SQL string formatting (해결 필요)
346
+
347
+ ### OWASP 수동 체크
348
+ [HIGH] API1 internal/handler/order.go:55 — 소유권 검증 없음
349
+ [HIGH] API6 internal/handler/auth.go:20 — 로그인 rate limit 없음
350
+
351
+ ### 통과
352
+ - API2: JWT RS256 + 만료 검증 ✅
353
+ - API8: 보안 헤더 미들웨어 ✅
354
+
355
+ ### 권고
356
+ - pprof 엔드포인트에 인증 추가 권장
357
+ ```
358
+
359
+ ## 4. 관련 문서
360
+
361
+ - 원칙: [`principles/common.md`](../../../principles/common.md) E섹션
362
+ - 코퍼스: `sources/owasp-api/`
@@ -0,0 +1,239 @@
1
+ ---
2
+ name: be-build-node
3
+ description: Node.js/TypeScript 요구사항을 받아 합의된 사내 원칙대로 구현. 명세→API contract→구현→테스트 매핑을 강제하고, 에러 모델·관찰가능성·보안 기준선을 적용한다.
4
+ ---
5
+
6
+ # be-build (Node.js)
7
+
8
+ > **호출 시점**: "이 API 명세대로 구현해줘", "이 요구사항 기반으로 서비스 만들어줘" 같은 신규 기능/API/서비스 구현 요청.
9
+ > **선행 로딩**: `principles/common.md` + `principles/node.md` 필수.
10
+
11
+ ## 0. 절대 금지
12
+
13
+ 1. 명세/요구사항 읽기 전에 코드 쓰지 마라.
14
+ 2. OpenAPI 스펙 없이 구현 먼저 하지 마라 — 계약이 구현보다 먼저.
15
+ 3. 명세에서 optional인 필드를 검증에서 required 취급하지 마라.
16
+ 4. `catch (err) {}` 빈 catch 블록 금지.
17
+ 5. DB 쿼리를 문자열 concatenation으로 작성 금지 (SQL injection).
18
+ 6. `process.on('unhandledRejection')` 미등록 상태로 완료 선언 금지.
19
+
20
+ ## 1. 워크플로우 (이 순서를 깨지 마라)
21
+
22
+ ### Step 1 — 요구사항 → 체크리스트 변환
23
+
24
+ 명세/요구사항 받자마자, 다른 어떤 작업도 하기 전에:
25
+
26
+ ```markdown
27
+ ## 요구사항 체크리스트
28
+ - [ ] R-01: <요구사항 한 줄 — 명세 원문 직접 인용>
29
+ - [ ] R-02: ...
30
+ ```
31
+
32
+ - 각 항목은 명세 원문 직접 인용. 해석/추론 금지.
33
+ - optional/required 구분 명시. optional은 "없어도 통과" 케이스로 매핑.
34
+ - 사용자 확인 후 다음 단계.
35
+
36
+ ### Step 2 — API Contract 정의
37
+
38
+ 체크리스트 확정 후, 구현 전에 OpenAPI 스펙 또는 TypeScript 인터페이스로 계약 작성:
39
+
40
+ ```typescript
41
+ // types/order.ts — 계약 먼저
42
+ interface CreateOrderRequest {
43
+ userId: string; // required
44
+ items: OrderItem[]; // required, min: 1
45
+ couponCode?: string; // optional — 검증에서 required 취급 금지
46
+ }
47
+
48
+ interface CreateOrderResponse {
49
+ orderId: string;
50
+ status: 'pending' | 'confirmed';
51
+ createdAt: string; // ISO 8601 UTC
52
+ }
53
+
54
+ interface OrderItem {
55
+ productId: string;
56
+ quantity: number; // positive integer
57
+ }
58
+ ```
59
+
60
+ ### Step 3 — 체크리스트 → 테스트 매핑표
61
+
62
+ ```markdown
63
+ ## 매핑표
64
+ | 요구사항 | 모듈/함수 | 테스트 파일:케이스 |
65
+ |----------|-----------|---------------------|
66
+ | R-01 | OrderController.create | order.test.ts:"주문 생성 성공" |
67
+ | R-02 | OrderService.validate | order.test.ts:"쿠폰 없이도 주문 가능" |
68
+ ```
69
+
70
+ - optional 항목은 반드시 "값 없을 때 통과" 케이스를 매핑.
71
+ - 에러 케이스도 매핑 (400, 404, 422 등).
72
+
73
+ ### Step 4 — 아키텍처 결정 (레이어 분리)
74
+
75
+ ```
76
+ [Controller] → 입력 검증 (Zod) → HTTP 계층
77
+ [Service] → 비즈니스 로직 → 도메인 계층
78
+ [Repository]→ DB 접근 → 영속 계층
79
+ ```
80
+
81
+ 결정 기록 한 줄: "OrderService는 트랜잭션 경계. Repository는 순수 쿼리만."
82
+
83
+ ### Step 5 — TDD (Red → Green → Refactor)
84
+
85
+ 매핑표의 각 행마다:
86
+ 1. 테스트 먼저 작성 (실패 확인)
87
+ 2. 최소 구현으로 통과
88
+ 3. `principles/common.md` + `principles/node.md` 원칙 적용
89
+
90
+ ### Step 6 — 셀프 체크리스트
91
+
92
+ ```markdown
93
+ - [ ] Zod 스키마 검증 — optional 필드 optional().처리
94
+ - [ ] 에러 응답 구조: { error: { code, message, requestId } }
95
+ - [ ] unhandledRejection 핸들러 등록
96
+ - [ ] 구조화 로그 (JSON) — 민감정보 제외
97
+ - [ ] DB 쿼리 파라미터화 (SQL injection 방지)
98
+ - [ ] 트랜잭션 경계 명시
99
+ - [ ] TypeScript strict 통과 (tsc --noEmit)
100
+ - [ ] N+1 쿼리 없음
101
+ ```
102
+
103
+ ### Step 7 — 매핑표 갱신 후 완료 선언
104
+
105
+ 매핑표 모든 행 ✅ + 모든 테스트 green + 셀프 체크리스트 통과. 그제서야 "완료" 선언.
106
+
107
+ ## 2. 구현 디폴트
108
+
109
+ ### 2.1 입력 검증 (Zod)
110
+
111
+ ```typescript
112
+ import { z } from 'zod';
113
+
114
+ const CreateOrderSchema = z.object({
115
+ userId: z.string().uuid(),
116
+ items: z.array(z.object({
117
+ productId: z.string().uuid(),
118
+ quantity: z.number().int().positive(),
119
+ })).min(1),
120
+ couponCode: z.string().optional(), // optional — 없어도 통과
121
+ });
122
+
123
+ // Controller
124
+ async function createOrder(req: Request, res: Response) {
125
+ const parsed = CreateOrderSchema.safeParse(req.body);
126
+ if (!parsed.success) {
127
+ return res.status(400).json({
128
+ error: {
129
+ code: 'VALIDATION_ERROR',
130
+ message: '입력값이 올바르지 않습니다',
131
+ details: parsed.error.flatten().fieldErrors,
132
+ requestId: req.id,
133
+ },
134
+ });
135
+ }
136
+ // ...
137
+ }
138
+ ```
139
+
140
+ ### 2.2 에러 모델
141
+
142
+ ```typescript
143
+ // 도메인 에러 클래스
144
+ class AppError extends Error {
145
+ constructor(
146
+ public readonly code: string,
147
+ public readonly statusCode: number,
148
+ message: string,
149
+ public readonly details?: unknown,
150
+ ) {
151
+ super(message);
152
+ }
153
+ }
154
+
155
+ class NotFoundError extends AppError {
156
+ constructor(resource: string, id: string) {
157
+ super('RESOURCE_NOT_FOUND', 404, `${resource} not found: ${id}`);
158
+ }
159
+ }
160
+
161
+ // 글로벌 에러 핸들러
162
+ app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
163
+ const statusCode = err instanceof AppError ? err.statusCode : 500;
164
+ logger.error({ err, requestId: req.id }, 'Request error');
165
+ res.status(statusCode).json({
166
+ error: {
167
+ code: err instanceof AppError ? err.code : 'INTERNAL_ERROR',
168
+ message: statusCode < 500 ? err.message : '서버 오류가 발생했습니다',
169
+ requestId: req.id,
170
+ },
171
+ });
172
+ });
173
+ ```
174
+
175
+ ### 2.3 DB + 트랜잭션
176
+
177
+ ```typescript
178
+ // Prisma 예시
179
+ async function createOrderWithStock(input: CreateOrderInput) {
180
+ return await prisma.$transaction(async (tx) => {
181
+ // TX: order 생성 + stock 차감 원자적 처리
182
+ const order = await tx.order.create({ data: { userId: input.userId } });
183
+ for (const item of input.items) {
184
+ await tx.stock.update({
185
+ where: { productId: item.productId },
186
+ data: { quantity: { decrement: item.quantity } },
187
+ });
188
+ }
189
+ return order;
190
+ });
191
+ }
192
+ ```
193
+
194
+ ### 2.4 관찰가능성
195
+
196
+ ```typescript
197
+ import { trace, SpanStatusCode } from '@opentelemetry/api';
198
+ import pino from 'pino';
199
+
200
+ const logger = pino({ level: 'info', redact: ['*.password', 'req.headers.authorization'] });
201
+ const tracer = trace.getTracer('order-service');
202
+
203
+ async function createOrder(input: CreateOrderInput) {
204
+ const span = tracer.startSpan('OrderService.createOrder');
205
+ try {
206
+ span.setAttributes({ 'order.userId': input.userId, 'order.itemCount': input.items.length });
207
+ const result = await orderRepo.create(input);
208
+ span.setStatus({ code: SpanStatusCode.OK });
209
+ return result;
210
+ } catch (err) {
211
+ span.recordException(err as Error);
212
+ span.setStatus({ code: SpanStatusCode.ERROR });
213
+ throw err;
214
+ } finally {
215
+ span.end();
216
+ }
217
+ }
218
+ ```
219
+
220
+ ## 3. 출력 형식
221
+
222
+ 작업 완료 시:
223
+
224
+ ```
225
+ ## 완료 보고
226
+ - 체크리스트: N/N ✅
227
+ - 매핑표: 모든 행 테스트 green
228
+ - 변경 파일: <목록>
229
+ - 셀프 체크: 6/6 통과
230
+ - API contract: <파일 경로>
231
+ - 의사결정: <레이어 분리 기준 1-2줄>
232
+ ```
233
+
234
+ ## 4. 관련 문서
235
+
236
+ - 원칙: [`principles/common.md`](../../../principles/common.md), [`principles/node.md`](../../../principles/node.md)
237
+ - 리뷰: [`skills/node/be-review/SKILL.md`](../be-review/SKILL.md)
238
+ - 성능: [`skills/node/be-perf/SKILL.md`](../be-perf/SKILL.md)
239
+ - 보안: [`skills/node/be-security/SKILL.md`](../be-security/SKILL.md)