start-vibing-stacks 2.17.0 → 2.19.0

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.
@@ -1,42 +1,97 @@
1
1
  ---
2
2
  name: zod-validation
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: "Zod 4 (stable Aug 2025) runtime validation for TypeScript boundaries — forms, API responses, env vars, server actions. Zod 4 brings 14× faster string parsing, 7× array, 6.5× object, 57% smaller bundle, 2× faster TS compile, 100× fewer tsc instantiations. Major API shifts: top-level format validators (`z.email()`, `z.url()`, `z.uuid()`) for tree-shaking; unified `error` parameter replaces `required_error` / `invalid_type_error` / `errorMap`. Codemod available: `npx @zod/codemod`. Covers schema design, RHF + zodResolver integration, API response validation, split server/client env schemas (security-critical for `NEXT_PUBLIC_*`), reusable schemas, transforms, and Next.js Server Actions. Mentions Valibot/ArkType when bundle/inference cost matters. Invoke at any trust boundary."
4
5
  ---
5
6
 
6
- # Zod Validation — Runtime Type Safety
7
+ # Zod 4 — Runtime Type Safety (2026)
7
8
 
8
- **ALWAYS use Zod for form validation, API responses, and data boundaries.**
9
+ **ALWAYS use Zod 4 for form validation, API responses, env vars, server actions.**
9
10
 
10
- ## Why Zod
11
+ ## Why Zod 4 (stable since August 2025)
11
12
 
12
- - Runtime validation (TypeScript types disappear at runtime)
13
- - Auto-infer TypeScript types from schemas
14
- - Works with React Hook Form, tRPC, Next.js Server Actions
15
- - Single source of truth: schema type validation
13
+ - **14× faster** string parsing, **7×** array, **6.5×** object
14
+ - **57% smaller** core bundle
15
+ - **~2× faster** TS compilation on large schemas; **100× fewer** tsc instantiations
16
+ - Ecosystem locked in: tRPC, Drizzle, React Hook Form, OpenAPI generators
17
+ - 15M+ weekly downloads — the de-facto standard
18
+
19
+ ### When to consider an alternative
20
+
21
+ | Situation | Tool |
22
+ |---|---|
23
+ | **Bundle size** is critical (edge functions, embeds) | **Valibot** — sub-1KB simple schemas |
24
+ | You want **faster TS check** in giant codebases | **Valibot** (~18× faster type-check vs Zod 4) |
25
+ | You like TS-syntax string schemas | **ArkType** |
26
+ | Mainstream ecosystem + maturity | **Zod 4** ✅ |
27
+
28
+ ## Migration v3 → v4
29
+
30
+ ```bash
31
+ npx @zod/codemod --transform v3-to-v4 ./src
32
+ ```
33
+
34
+ The two breaking changes you'll see most:
35
+
36
+ ### 1. Top-level format validators (tree-shaking)
37
+
38
+ ```tsx
39
+ // v3 (works in v4 but not tree-shakable)
40
+ z.string().email().min(5);
41
+
42
+ // v4 — recommended
43
+ z.email();
44
+ z.url();
45
+ z.uuid();
46
+ z.iso.datetime(); // ISO-8601
47
+ z.iso.date();
48
+ z.cuid2();
49
+ z.ipv4();
50
+ z.ipv6();
51
+ z.base64();
52
+ z.jwt();
53
+ ```
54
+
55
+ ### 2. Unified `error` parameter
56
+
57
+ ```tsx
58
+ // v3 — three different parameters
59
+ z.string({
60
+ required_error: "Required",
61
+ invalid_type_error: "Must be a string",
62
+ errorMap: ({ code }) => ({ message: code === "too_small" ? "Too short" : "Invalid" }),
63
+ });
64
+
65
+ // v4 — one parameter, string or function
66
+ z.string({
67
+ error: (issue) => issue.code === "too_small" ? "Too short" : "Required",
68
+ });
69
+ // Or just a string:
70
+ z.string({ error: "Name is required" });
71
+ ```
16
72
 
17
73
  ## Core Patterns
18
74
 
19
- ### Schema Definition
75
+ ### Schema definition
20
76
 
21
77
  ```tsx
22
78
  import { z } from 'zod';
23
79
 
24
- // Define schema
25
80
  const UserSchema = z.object({
26
- name: z.string().min(2, 'Name must be at least 2 characters'),
27
- email: z.string().email('Invalid email address'),
28
- age: z.number().min(18, 'Must be 18+').max(120),
29
- role: z.enum(['admin', 'user', 'moderator']),
30
- bio: z.string().max(500).optional(),
31
- tags: z.array(z.string()).min(1, 'At least one tag required'),
81
+ name: z.string().min(2, "Name must be at least 2 characters"),
82
+ email: z.email({ error: "Enter a valid email" }), // v4 top-level
83
+ age: z.number().min(18, "Must be 18+").max(120),
84
+ role: z.enum(["admin", "user", "moderator"]),
85
+ bio: z.string().max(500).optional(),
86
+ tags: z.array(z.string()).min(1, "At least one tag required"),
32
87
  metadata: z.record(z.string(), z.unknown()).optional(),
33
88
  });
34
89
 
35
- // Infer type from schema (SINGLE SOURCE OF TRUTH)
90
+ // SINGLE SOURCE OF TRUTH schema type
36
91
  type User = z.infer<typeof UserSchema>;
37
92
  ```
38
93
 
39
- ### Form Validation (React Hook Form + Zod)
94
+ ### Form Validation (React Hook Form + Zod 4)
40
95
 
41
96
  ```tsx
42
97
  import { useForm } from 'react-hook-form';
@@ -44,7 +99,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
44
99
 
45
100
  const CreateUserSchema = z.object({
46
101
  name: z.string().min(2),
47
- email: z.string().email(),
102
+ email: z.email(), // v4 top-level
48
103
  password: z.string().min(8, 'Password must be at least 8 characters'),
49
104
  confirmPassword: z.string(),
50
105
  }).refine((data) => data.password === data.confirmPassword, {
@@ -150,24 +205,47 @@ export const clientEnv = ClientEnvSchema.parse({
150
205
 
151
206
  **Rule:** If a variable contains a key, secret, token, or password, it MUST be in `ServerEnvSchema` without `NEXT_PUBLIC_` prefix.
152
207
 
153
- ### Reusable Schemas
208
+ ### Reusable Schemas (Zod 4 top-level)
154
209
 
155
210
  ```tsx
156
- // Shared between frontend and backend
157
- // schemas/user.ts
158
-
159
- export const EmailSchema = z.string().email().toLowerCase().trim();
211
+ // schemas/common.ts — shared between frontend and backend
212
+ export const EmailSchema = z.email().transform(v => v.toLowerCase().trim());
160
213
  export const PasswordSchema = z.string().min(8).max(128);
161
- export const UUIDSchema = z.string().uuid();
214
+ export const UUIDSchema = z.uuid();
215
+ export const URLSchema = z.url();
216
+ export const ISODateSchema = z.iso.datetime();
217
+
162
218
  export const PaginationSchema = z.object({
163
- page: z.coerce.number().min(1).default(1),
219
+ page: z.coerce.number().min(1).default(1),
164
220
  limit: z.coerce.number().min(1).max(100).default(20),
165
221
  });
166
222
 
167
223
  export const DateRangeSchema = z.object({
168
224
  from: z.coerce.date(),
169
- to: z.coerce.date(),
170
- }).refine((d) => d.to > d.from, 'End date must be after start date');
225
+ to: z.coerce.date(),
226
+ }).refine(d => d.to > d.from, "End date must be after start date");
227
+ ```
228
+
229
+ ### Discriminated unions (polymorphic payloads)
230
+
231
+ ```tsx
232
+ const PaymentSchema = z.discriminatedUnion("type", [
233
+ z.object({ type: z.literal("card"), last4: z.string().length(4) }),
234
+ z.object({ type: z.literal("pix"), key: z.string() }),
235
+ z.object({ type: z.literal("boleto"), barcode: z.string() }),
236
+ ]);
237
+ type Payment = z.infer<typeof PaymentSchema>;
238
+ ```
239
+
240
+ ### Branded types — distinguish IDs at the type level
241
+
242
+ ```tsx
243
+ const UserIdSchema = z.uuid().brand<"UserId">();
244
+ type UserId = z.infer<typeof UserIdSchema>;
245
+
246
+ function getUser(id: UserId) { /* … */ }
247
+ getUser("not a uuid"); // ❌ type error
248
+ getUser(UserIdSchema.parse(input)); // ✅
171
249
  ```
172
250
 
173
251
  ### Transform & Preprocess
@@ -253,20 +331,64 @@ export function LeadForm() {
253
331
  }
254
332
  ```
255
333
 
334
+ ## Use with React 19 Actions + `useActionState`
335
+
336
+ ```tsx
337
+ "use client";
338
+ import { useActionState } from "react";
339
+ import { z } from "zod";
340
+
341
+ const Schema = z.object({
342
+ email: z.email(),
343
+ password: z.string().min(8),
344
+ });
345
+
346
+ async function loginAction(_prev: { error: string | null }, formData: FormData) {
347
+ const parsed = Schema.safeParse(Object.fromEntries(formData));
348
+ if (!parsed.success) return { error: parsed.error.issues[0].message };
349
+ return await loginRequest(parsed.data);
350
+ }
351
+
352
+ export function LoginForm() {
353
+ const [state, action, pending] = useActionState(loginAction, { error: null });
354
+ return (
355
+ <form action={action}>
356
+ <input name="email" type="email" />
357
+ <input name="password" type="password" />
358
+ {state.error && <p className="text-destructive">{state.error}</p>}
359
+ <button disabled={pending}>{pending ? "Signing in…" : "Sign in"}</button>
360
+ </form>
361
+ );
362
+ }
363
+ ```
364
+
256
365
  ## FORBIDDEN
257
366
 
258
367
  | Don't | Do |
259
368
  |---|---|
260
- | Trust API responses blindly | `Schema.parse(response)` |
261
- | Manual `if/else` validation | Zod schema |
369
+ | Trust API responses blindly | `Schema.parse(response)` (or `safeParse` for soft fail) |
370
+ | Manual `if/else` validation | Zod schema (single source of truth) |
262
371
  | Duplicate types + validation | `z.infer<typeof Schema>` |
263
372
  | `any` or `unknown` without validation | Parse with Zod first |
264
- | Validate only on server | Validate on BOTH client + server |
373
+ | Validate only on server | Validate on BOTH client and server |
374
+ | `z.string().email()` in v4 | `z.email()` (tree-shakable, top-level) |
375
+ | `required_error` / `invalid_type_error` / `errorMap` (v3) | Unified `error` parameter (v4) |
376
+ | Dump secrets into `NEXT_PUBLIC_*` | Server-only `ServerEnvSchema`; client gets only public keys |
377
+ | Re-implement common formats (URL, UUID, datetime, JWT) | `z.url()`, `z.uuid()`, `z.iso.datetime()`, `z.jwt()` |
265
378
 
266
379
  ## Rules
267
380
 
268
381
  1. **SINGLE SOURCE OF TRUTH** — schema defines type AND validation
269
- 2. **VALIDATE AT BOUNDARIES** — forms, API responses, env vars
270
- 3. **SAFE PARSE FOR UI** — `safeParse()` for user-facing, `parse()` for internal
271
- 4. **SHARED SCHEMAS** — same schema on frontend and backend
272
- 5. **TRANSFORM DATA** — clean/normalize during validation, not after
382
+ 2. **VALIDATE AT BOUNDARIES** — forms, API responses, env vars, queue messages
383
+ 3. **SAFE PARSE FOR UI** — `safeParse()` for user-facing forms, `parse()` for internal/trusted
384
+ 4. **SHARED SCHEMAS** — same schema on frontend and backend (workspace package)
385
+ 5. **TRANSFORM DURING VALIDATION** — `.toLowerCase()`, `.trim()`, `.transform(...)` happen as part of `parse`, not afterwards
386
+ 6. **PREFER TOP-LEVEL FORMAT VALIDATORS** in v4 — better tree-shaking + clearer intent
387
+
388
+ ## See Also
389
+
390
+ - `react-patterns` v2 — `useActionState`, `useOptimistic`, `use()` hook
391
+ - `react-ui-patterns` v2 — RHF + Zod + TanStack Query patterns
392
+ - `shadcn-ui` v2 — `<Form>` component built on RHF + Zod
393
+ - `_shared/skills/security-baseline` v2 — input validation as defense layer
394
+ - `_shared/skills/openapi-design` v2 — schemas → OpenAPI 3.2
@@ -1,11 +1,7 @@
1
1
  ---
2
2
  name: axios-laravel-api
3
- version: 1.0.0
4
- description: Axios HTTP client for Laravel 12 + Sanctum SPA `withCredentials`,
5
- `withXSRFToken`, `/sanctum/csrf-cookie` flow, and `401/403/419/422/5xx`
6
- interceptors. Use when wiring a React (or any SPA) frontend to a Laravel
7
- cookie-authenticated JSON API. Pairs with `laravel-api-architecture` and
8
- `react-api-standards`.
3
+ version: 2.0.0
4
+ description: "Axios HTTP client for Laravel 12 + Sanctum SPA in React 19 (Tailwind v4) projects. `withCredentials: true` + `withXSRFToken: true` + `/sanctum/csrf-cookie` priming flow, plus a single response interceptor that handles 401 (logout), 403 (forbidden toast), 419 (CSRF stale → re-prime + retry once), 422 (validation errors surfaced to RHF), 5xx (toast + log). Pairs with `laravel-api-architecture` (backend) and `react-api-standards` (frontend conventions). 2026 polish: Zod 4 response validation at the boundary, TanStack Query v5 `signal` threading for cancellation, React 19 forms via `useActionState` for client-side mutations. Invoke when wiring or auditing the API client, login flow, or CSRF/cookie configuration."
9
5
  ---
10
6
 
11
7
  # Axios + Laravel Sanctum SPA Client
@@ -1,25 +1,23 @@
1
1
  ---
2
2
  name: react-api-standards
3
- version: 1.0.0
4
- description: React 19 + Vite + Axios standards for a Laravel-backed JSON API SPA
5
- — Page shell + skeleton, async data via `api.get`, form 422 binding, route
6
- catch-all, no Inertia. Use for any new React page in a Laravel + Sanctum SPA
7
- project. Pairs with `axios-laravel-api` and `laravel-api-architecture`.
3
+ version: 2.0.0
4
+ description: "React 19 + Vite 6 + Axios standards for a Laravel-backed JSON API SPA. Page shell + skeleton-first paint, async data via `api.get`, 422 validation binding to React Hook Form, protected-route + catch-all routing, NO Inertia. Pairs with `axios-laravel-api` (HTTP client) and `laravel-api-architecture` (backend). 2026 polish: TanStack Query v5 (`isPending` semantics + `signal` threading), Zod 4 (`z.email()`/`z.url()` top-level), `useOptimistic` for instant feedback, `useActionState` for form submissions, `useFormStatus` to wire Submit buttons without prop drilling. Tailwind v4 with `@theme` and OKLCH tokens. Invoke for any new page, layout, or component in a Laravel + Sanctum SPA."
8
5
  ---
9
6
 
10
- # React 19 API-First Standards (Laravel + Axios)
7
+ # React 19 API-First Standards (Laravel + Axios) — 2026
11
8
 
12
9
  **ALWAYS invoke when creating React pages, components, or layouts in a project
13
10
  that uses a Laravel JSON API as backend (NOT Inertia.js).**
14
11
 
15
12
  ## Version Requirements
16
13
 
17
- - **React >= 19** — MANDATORY (`useActionState`, `useOptimistic`, `use`)
18
- - **Vite >= 5** with `@vitejs/plugin-react`
19
- - **TailwindCSS >= 4**
20
- - **Axios >= 1.6** (for `withXSRFToken` support)
21
- - **TanStack Query >= 5** (recommended for cached lists/details)
22
- - **react-router-dom >= 6** (client-side routing)
14
+ - **React 19.0** — MANDATORY (`useActionState`, `useOptimistic`, `useFormStatus`, `use()`)
15
+ - **Vite 6** with `@vitejs/plugin-react`
16
+ - **TailwindCSS 4** (CSS-first `@theme`, Oxide engine)
17
+ - **Axios 1.7** (for `withXSRFToken` and modern interceptor types)
18
+ - **TanStack Query 5** — `isPending` (no data yet) vs `isFetching` (any in-flight); thread `signal` through `queryFn`
19
+ - **Zod 4** — top-level `z.email()`/`z.url()` for tree-shaking
20
+ - **react-router-dom ≥ 7** (`<RouterProvider>` + data routers)
23
21
 
24
22
  ## Folder Structure
25
23
 
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: api-security-python
3
- version: 1.0.0
4
- description: Production-grade API hardening for Python (FastAPI, Django, Flask). Rate limit, CORS, JWT, secure cookies, CSRF, OAuth2.
3
+ version: 2.0.0
4
+ description: "Python (FastAPI / Django / Flask) overlay on top of _shared/security-baseline v2 (OWASP Top 10:2025). Production-grade hardening: security headers via Starlette middleware and SECURE_* settings, strict CORS allowlist, rate limiting (slowapi / django-ratelimit), HttpOnly+Secure+SameSite cookies, JWT with algorithms=[ALG] pinning + jti for revocation using PyJWT (python-jose is unmaintained as of 2025), CSRF built-in for Django and double-submit cookie for FastAPI, Pydantic V2 extra='forbid' against mass-assignment, file-upload magic-byte sniffing, Argon2id passwords, parameterised SQL/ORM. Cross-references 2025-A03 (Software Supply Chain Failures) and 2025-A10 (Mishandling Exceptional Conditions)."
5
5
  ---
6
6
 
7
- # API Security — Python
7
+ # API Security — Python (FastAPI / Django / Flask)
8
8
 
9
9
  **ALWAYS invoke when building API endpoints, auth flows, or admin actions.**
10
10
 
11
- > Pair this with `security-baseline` for OWASP Top 10. This skill is stack-specific hardening.
11
+ > Stack-specific overlay on top of `_shared/skills/security-baseline` v2 (OWASP Top 10:2025). The shared skill defines §A01–§A10 anchors; this one wires the Python equivalents.
12
12
 
13
13
  ## Layered Defense
14
14
 
@@ -136,13 +136,16 @@ response.set_cookie(
136
136
 
137
137
  ## 5. JWT / OAuth2 — FastAPI
138
138
 
139
+ > **Library choice (2026):** Use **PyJWT** (`pyjwt[crypto]`) or **authlib** for OAuth2/OIDC flows. `python-jose` has been effectively unmaintained since 2024 and should be removed from dependencies. For the resource-server side that just verifies tokens, PyJWT is enough.
140
+
139
141
  ```python
140
142
  from datetime import datetime, timedelta, timezone
141
- from jose import jwt, JWTError
142
143
  import os, uuid
144
+ import jwt
145
+ from jwt import InvalidTokenError
143
146
 
144
147
  SECRET = os.environ["JWT_SECRET"]
145
- ALG = "HS256"
148
+ ALG = "HS256" # use RS256/EdDSA when verifying tokens minted elsewhere
146
149
 
147
150
  def issue_access_token(user_id: str, role: str) -> str:
148
151
  now = datetime.now(timezone.utc)
@@ -151,8 +154,11 @@ def issue_access_token(user_id: str, role: str) -> str:
151
154
  "sub": user_id,
152
155
  "role": role,
153
156
  "iat": now,
157
+ "nbf": now,
154
158
  "exp": now + timedelta(minutes=15),
155
159
  "jti": str(uuid.uuid4()),
160
+ "iss": os.environ["JWT_ISSUER"],
161
+ "aud": os.environ["JWT_AUDIENCE"],
156
162
  },
157
163
  SECRET,
158
164
  algorithm=ALG,
@@ -160,9 +166,18 @@ def issue_access_token(user_id: str, role: str) -> str:
160
166
 
161
167
  async def current_user(token: str = Depends(oauth2_scheme)) -> User:
162
168
  try:
163
- payload = jwt.decode(token, SECRET, algorithms=[ALG]) # pin algorithm
164
- except JWTError:
169
+ payload = jwt.decode(
170
+ token,
171
+ SECRET,
172
+ algorithms=[ALG], # pin — never accept "alg: none"
173
+ audience=os.environ["JWT_AUDIENCE"],
174
+ issuer=os.environ["JWT_ISSUER"],
175
+ options={"require": ["exp", "iat", "sub", "jti"]},
176
+ )
177
+ except InvalidTokenError:
165
178
  raise HTTPException(401, "Invalid token")
179
+ if await is_revoked(payload["jti"]): # check revocation list (Redis set)
180
+ raise HTTPException(401, "Token revoked")
166
181
  user = await User.get_or_none(id=payload["sub"])
167
182
  if not user:
168
183
  raise HTTPException(401, "User not found")
@@ -170,9 +185,11 @@ async def current_user(token: str = Depends(oauth2_scheme)) -> User:
170
185
  ```
171
186
 
172
187
  Rules:
173
- - Access tokens: ≤ 15 min. Refresh tokens: rotate on use, store hash in DB, revocable.
174
- - Pin `algorithms=[ALG]`. Never accept `alg: none`.
175
- - Include `jti` for revocation lists.
188
+ - Access tokens ≤ 15 min. Refresh tokens: rotate on use, store **hash** in DB, revocable.
189
+ - Pin `algorithms=[ALG]`. Never accept `alg: none` or omit the kwarg (confusion attack).
190
+ - Always validate `aud` and `iss` when present.
191
+ - Include `jti` and check a revocation set for sensitive scopes.
192
+ - Store JWTs in **HttpOnly+Secure+SameSite cookies**, not `localStorage` (XSS exfiltration).
176
193
 
177
194
  ---
178
195
 
@@ -280,6 +297,91 @@ user = await User.get(id=user_id) # Tortoise / SQLAlchemy / Dj
280
297
 
281
298
  ---
282
299
 
300
+ ## 11. Outbound calls — SSRF guard (OWASP 2025 still relevant)
301
+
302
+ SSRF was demoted from a top-level OWASP category in the 2025 list but remains in scope. Any time you fetch a URL the user controls (preview cards, webhooks, image proxies), validate the destination:
303
+
304
+ ```python
305
+ import ipaddress
306
+ import socket
307
+ from urllib.parse import urlparse
308
+ import httpx
309
+
310
+ PRIVATE = ipaddress.collapse_addresses([
311
+ ipaddress.ip_network("10.0.0.0/8"),
312
+ ipaddress.ip_network("172.16.0.0/12"),
313
+ ipaddress.ip_network("192.168.0.0/16"),
314
+ ipaddress.ip_network("169.254.0.0/16"), # link-local + AWS metadata
315
+ ipaddress.ip_network("127.0.0.0/8"),
316
+ ipaddress.ip_network("::1/128"),
317
+ ])
318
+
319
+ def safe_url(url: str) -> str:
320
+ p = urlparse(url)
321
+ if p.scheme not in {"http", "https"}:
322
+ raise ValueError("scheme")
323
+ host = p.hostname
324
+ if not host:
325
+ raise ValueError("host")
326
+ for fam, _, _, _, sockaddr in socket.getaddrinfo(host, None):
327
+ ip = ipaddress.ip_address(sockaddr[0])
328
+ if any(ip in net for net in PRIVATE):
329
+ raise ValueError("private")
330
+ return url
331
+
332
+ async def fetch_user_url(url: str):
333
+ safe_url(url)
334
+ async with httpx.AsyncClient(timeout=10.0, follow_redirects=False) as c:
335
+ return await c.get(url)
336
+ ```
337
+
338
+ Disable redirect-following or re-validate every hop — otherwise an attacker can redirect from a public IP to `169.254.169.254` (cloud metadata).
339
+
340
+ ## 12. OWASP 2025 deltas — Python specifics
341
+
342
+ ### §A03 — Software Supply Chain Failures (NEW in 2025)
343
+
344
+ ```toml
345
+ # pyproject.toml — pin everything; uv produces a deterministic lockfile
346
+ [project]
347
+ dependencies = ["fastapi>=0.115,<0.116", "pydantic>=2.6,<3"]
348
+
349
+ [tool.uv]
350
+ dev-dependencies = ["pip-audit>=2.7", "ruff>=0.5"]
351
+ ```
352
+
353
+ ```bash
354
+ # Lock + verify in CI
355
+ uv lock --check # fails if lockfile drifted
356
+ uv export --format requirements-txt --no-dev | pip-audit -r /dev/stdin --strict
357
+ ```
358
+
359
+ - Use `uv` (or Poetry) to produce a deterministic lockfile; never `pip install` without one in production.
360
+ - Run `pip-audit` (PyPA) **and** subscribe to GHSA advisories for your deps.
361
+ - Pin GitHub Actions by SHA, not tag — see `_shared/skills/secrets-management`.
362
+
363
+ ### §A10 — Mishandling Exceptional Conditions (NEW in 2025)
364
+
365
+ ```python
366
+ # WRONG — silently swallows everything, including programming bugs
367
+ try:
368
+ user = await get_user(id)
369
+ except Exception:
370
+ user = None
371
+
372
+ # CORRECT — narrow except + log + propagate or translate
373
+ try:
374
+ user = await get_user(id)
375
+ except UserNotFoundError:
376
+ raise HTTPException(404, "user not found")
377
+ except DatabaseUnavailableError as e:
378
+ logger.exception("db-down", extra={"user_id": id})
379
+ raise HTTPException(503, "service unavailable") from e
380
+ # Programming errors (KeyError, TypeError) are NOT caught — let the global handler 500 + log
381
+ ```
382
+
383
+ Bare `except Exception:` (or worse, `except:`) is now an explicit OWASP pattern to flag. Use `logger.exception()` so the stack trace lands in logs, return a **generic** message to the client, never the original exception text.
384
+
283
385
  ## Endpoint Checklist
284
386
 
285
387
  - [ ] `Depends(current_user)` for protected routes
@@ -306,7 +408,8 @@ user = await User.get(id=user_id) # Tortoise / SQLAlchemy / Dj
306
408
 
307
409
  ## See Also
308
410
 
309
- - `security-baseline` — OWASP Top 10
310
- - `secrets-management` — env vars, rotation
311
- - `pydantic-validation` — schema patterns
312
- - `observability` — structured logs without PII
411
+ - `_shared/skills/security-baseline` v2 — OWASP Top 10:2025 (§A01–§A10 anchors)
412
+ - `_shared/skills/secrets-management` v2 OIDC federation, gitleaks 3-layer, SOPS+age
413
+ - `_shared/skills/observability` v2 structured logs without PII, GenAI semconv 1.41+
414
+ - `pydantic-validation` v2 `extra="forbid"` against mass-assignment
415
+ - `fastapi-patterns` v2 — lifespan, DI, exception handlers