start-vibing-stacks 2.1.1 → 2.3.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.
@@ -0,0 +1,267 @@
1
+ # React 19+ Standards (with Inertia.js)
2
+
3
+ ## Version Requirements
4
+
5
+ - **ReactJS >= 19** — MANDATORY
6
+ - **TailwindCSS >= 4** — MANDATORY
7
+ - **Inertia.js >= 2** — MANDATORY
8
+
9
+ ## Translation Pattern (via Inertia shared props)
10
+
11
+ ```tsx
12
+ import __ from '@/Utils/translate';
13
+
14
+ // CORRECT: Translations as CONST at the top, BEFORE hooks
15
+ const LABELS = {
16
+ title: __('dashboard.title'),
17
+ save: __('common.save'),
18
+ cancel: __('common.cancel'),
19
+ errorRequired: __('errors.field_required'),
20
+ welcome: __('dashboard.welcome', { name: 'User' }),
21
+ };
22
+
23
+ export default function Dashboard() {
24
+ const [data, setData] = useState(null);
25
+
26
+ return <h1>{LABELS.title}</h1>;
27
+ }
28
+
29
+ // WRONG: __() inside JSX (Hook violation — usePage() is called internally)
30
+ return <h1>{__('dashboard.title')}</h1>; // NEVER
31
+ ```
32
+
33
+ **Rules:**
34
+ - Translations in `CONST` variables before state hooks
35
+ - New strings must be added to `lang/en/*.php` AND `lang/pt/*.php`
36
+ - Error strings centralized in `lang/*/errors.php`
37
+ - Use replacements for dynamic values: `__('key', { name: value })`
38
+
39
+ ## Debug Logging
40
+
41
+ ```tsx
42
+ const ENABLE_DASHBOARD_DEBUG = false;
43
+
44
+ const debugLog = (...args: unknown[]) => {
45
+ if (ENABLE_DASHBOARD_DEBUG) console.log('[Dashboard]', ...args);
46
+ };
47
+
48
+ export default function Dashboard() {
49
+ debugLog('Rendering with data:', data);
50
+ }
51
+ ```
52
+
53
+ **Rule:** Never leave raw `console.log`. Always use controlled debug pattern.
54
+
55
+ ## TailwindCSS Class Organization
56
+
57
+ ```tsx
58
+ // CORRECT: Classes as CONST — clean JSX
59
+ const STYLES = {
60
+ container: 'flex flex-col gap-4 p-6 bg-white rounded-lg shadow-sm',
61
+ title: 'text-2xl font-bold text-gray-900',
62
+ button: 'px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition',
63
+ grid: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6',
64
+ };
65
+
66
+ export default function Dashboard() {
67
+ return (
68
+ <div className={STYLES.container}>
69
+ <h1 className={STYLES.title}>{LABELS.title}</h1>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ // WRONG: Inline class soup
75
+ <div className="flex flex-col gap-4 p-6 bg-white rounded-lg shadow-sm"> // NEVER
76
+ ```
77
+
78
+ ## SVG Icons
79
+
80
+ ```tsx
81
+ // CORRECT: Separate files, import with ?react
82
+ import CheckIcon from '@/Icons/CheckIcon.svg?react';
83
+ import { CheckIcon, AlertIcon } from '@/Icons';
84
+
85
+ // WRONG: Inline SVG (bloats JSX)
86
+ <svg viewBox="0 0 24 24">...</svg> // NEVER
87
+ ```
88
+
89
+ **Structure:**
90
+ ```
91
+ resources/js/Icons/
92
+ ├── index.js # Barrel export
93
+ ├── CheckIcon.svg
94
+ ├── AlertIcon.svg
95
+ └── SpinnerIcon.svg
96
+ ```
97
+
98
+ ## Inertia.js Hooks & Navigation
99
+
100
+ ### Accessing Shared Props
101
+
102
+ ```tsx
103
+ import { usePage } from '@inertiajs/react';
104
+
105
+ export default function Header() {
106
+ const { auth, locale, flash } = usePage().props;
107
+
108
+ return (
109
+ <nav>
110
+ <span>{auth.user?.name}</span>
111
+ {flash.success && <Alert>{flash.success}</Alert>}
112
+ </nav>
113
+ );
114
+ }
115
+ ```
116
+
117
+ ### Forms with useForm
118
+
119
+ ```tsx
120
+ import { useForm } from '@inertiajs/react';
121
+
122
+ export default function CreateOrder() {
123
+ const { data, setData, post, processing, errors } = useForm({
124
+ product_id: '',
125
+ quantity: 1,
126
+ notes: '',
127
+ });
128
+
129
+ const handleSubmit = (e) => {
130
+ e.preventDefault();
131
+ post(route('orders.store'));
132
+ };
133
+
134
+ return (
135
+ <form onSubmit={handleSubmit}>
136
+ <Input
137
+ value={data.product_id}
138
+ onChange={(e) => setData('product_id', e.target.value)}
139
+ error={errors.product_id}
140
+ />
141
+ <Button type="submit" loading={processing}>
142
+ {processing ? <LoadingSpinner /> : LABELS.save}
143
+ </Button>
144
+ </form>
145
+ );
146
+ }
147
+ ```
148
+
149
+ **Rules:**
150
+ - Use `useForm` for ALL form submissions (handles CSRF, errors, loading)
151
+ - `processing` boolean for button loading states
152
+ - `errors` object maps to Form Request validation errors
153
+ - Never use `fetch()` or `axios` for form submissions
154
+
155
+ ### Navigation
156
+
157
+ ```tsx
158
+ import { Link, router } from '@inertiajs/react';
159
+
160
+ // SPA links (no full page reload)
161
+ <Link href={route('orders.index')}>Orders</Link>
162
+
163
+ // Programmatic navigation
164
+ router.visit(route('dashboard'));
165
+
166
+ // Partial reload (only refresh specific props)
167
+ router.reload({ only: ['stats', 'recentOrders'] });
168
+ ```
169
+
170
+ ## Loading States
171
+
172
+ ```tsx
173
+ // CORRECT: Always show loading feedback
174
+ export default function DataTable() {
175
+ const [loading, setLoading] = useState(true);
176
+
177
+ if (loading) {
178
+ return <SectionLoader />;
179
+ }
180
+
181
+ return <Table data={data} />;
182
+ }
183
+
184
+ // Button loading (from useForm)
185
+ <Button onClick={handleSave} disabled={processing}>
186
+ {processing ? <LoadingSpinner /> : LABELS.save}
187
+ </Button>
188
+ ```
189
+
190
+ **Rule:** Every data-heavy section needs a loading state.
191
+
192
+ ## Modal Data Flow
193
+
194
+ ```tsx
195
+ interface EditModalProps {
196
+ item: Item;
197
+ isOpen: boolean;
198
+ onClose: () => void;
199
+ onUpdated: () => void;
200
+ }
201
+
202
+ function EditModal({ item, isOpen, onClose, onUpdated }: EditModalProps) {
203
+ const { data, setData, put, processing } = useForm({
204
+ name: item.name,
205
+ });
206
+
207
+ const handleSave = (e) => {
208
+ e.preventDefault();
209
+ put(route('items.update', item.id), {
210
+ onSuccess: () => {
211
+ onUpdated();
212
+ onClose();
213
+ },
214
+ });
215
+ };
216
+ }
217
+
218
+ // Parent: use router.reload for refresh after modal mutation
219
+ <EditModal
220
+ item={selectedItem}
221
+ isOpen={showModal}
222
+ onClose={() => setShowModal(false)}
223
+ onUpdated={() => router.reload({ only: ['items'] })}
224
+ />
225
+ ```
226
+
227
+ ## Third-Party Libraries (Charts)
228
+
229
+ ```tsx
230
+ // CORRECT: Let React handle re-rendering
231
+ {chartData && (
232
+ <ApexChart
233
+ key={JSON.stringify(chartData)}
234
+ options={chartOptions}
235
+ series={chartData}
236
+ type="area"
237
+ />
238
+ )}
239
+
240
+ // WRONG: Manual DOM manipulation
241
+ const chartRef = useRef(null);
242
+ chartRef.current.updateSeries(newData); // NEVER
243
+
244
+ // Memoize expensive computations
245
+ const processedData = useMemo(() => {
246
+ return heavyTransform(rawData);
247
+ }, [rawData]);
248
+ ```
249
+
250
+ **Rules:**
251
+ - No `useRef` for updating third-party components
252
+ - Conditional rendering: `data && <Component />`
253
+ - `useMemo` for expensive computations
254
+ - Loading states before rendering charts
255
+
256
+ ## Forbidden Patterns
257
+
258
+ | Pattern | Reason | Use Instead |
259
+ |---------|--------|-------------|
260
+ | `fetch()` / `axios` for pages | Bypasses Inertia | `Inertia::render()` props |
261
+ | `__()` inside JSX | Hook violation | CONST at top |
262
+ | Inline SVGs | Bloats components | SVG files + `?react` |
263
+ | `<a href>` for internal links | Full reload | `<Link href>` |
264
+ | `window.location` | Full reload | `router.visit()` |
265
+ | Raw `console.log` | Uncontrolled | Debug constant pattern |
266
+ | Inline Tailwind soup | Unreadable | STYLES const object |
267
+ | `axios.post()` for forms | No CSRF/errors | `useForm().post()` |
@@ -0,0 +1,431 @@
1
+ # API Security — NSA-Level Hardening for Laravel + Octane
2
+
3
+ **ALWAYS invoke when building APIs, auth endpoints, or handling user input.**
4
+
5
+ ## Security Layers
6
+
7
+ ```
8
+ Internet → Cloudflare WAF → Rate Limiting → CORS → Auth Middleware
9
+ → Input Validation → Business Logic → Output Sanitization → Response
10
+
11
+ Every layer is a wall. Assume the previous one failed.
12
+ ```
13
+
14
+ ## 1. Authentication (Sanctum + Octane)
15
+
16
+ ### Token-Based (API)
17
+
18
+ ```php
19
+ // config/sanctum.php
20
+ 'expiration' => 60 * 24, // 24h token expiry (MANDATORY)
21
+ 'token_prefix' => 'flk_', // Prefix for easy log scanning
22
+
23
+ // Issue token with abilities (least privilege)
24
+ $token = $user->createToken('api-client', [
25
+ 'read:leads',
26
+ 'write:leads',
27
+ // NOT '*' — never wildcard abilities
28
+ ])->plainTextToken;
29
+
30
+ // Middleware: check specific ability
31
+ Route::middleware(['auth:sanctum', 'ability:read:leads'])->group(function () {
32
+ Route::get('/api/v1/leads', [LeadController::class, 'index']);
33
+ });
34
+ ```
35
+
36
+ ### Token Rotation
37
+
38
+ ```php
39
+ // Rotate on every sensitive action
40
+ public function changePassword(Request $request): JsonResponse
41
+ {
42
+ // ... change password logic
43
+
44
+ // Revoke ALL tokens (force re-login everywhere)
45
+ $request->user()->tokens()->delete();
46
+
47
+ // Issue fresh token
48
+ $newToken = $request->user()->createToken('session', ['*'])->plainTextToken;
49
+
50
+ return $this->success(['token' => $newToken]);
51
+ }
52
+ ```
53
+
54
+ ### Octane Session Safety
55
+
56
+ ```php
57
+ // app/Providers/AppServiceProvider.php
58
+ use Laravel\Octane\Facades\Octane;
59
+
60
+ public function boot(): void
61
+ {
62
+ // MANDATORY: flush auth state between requests
63
+ Octane::prepare(function ($sandbox) {
64
+ $sandbox->forgetScopedInstances();
65
+ $sandbox->flushDatabaseConnections();
66
+ });
67
+ }
68
+ ```
69
+
70
+ ## 2. Input Validation (Trust NOTHING)
71
+
72
+ ### FormRequest (Always)
73
+
74
+ ```php
75
+ // app/Http/Requests/StoreLeadRequest.php
76
+ class StoreLeadRequest extends FormRequest
77
+ {
78
+ public function authorize(): bool
79
+ {
80
+ return $this->user()->tokenCan('write:leads');
81
+ }
82
+
83
+ public function rules(): array
84
+ {
85
+ return [
86
+ 'name' => ['required', 'string', 'min:2', 'max:255'],
87
+ 'email' => ['required', 'email:rfc,dns', 'max:320'], // RFC + DNS check
88
+ 'phone' => ['nullable', 'string', 'regex:/^\+?[1-9]\d{1,14}$/'], // E.164
89
+ 'domain_id' => ['required', 'uuid', 'exists:domains,id'],
90
+ 'metadata' => ['nullable', 'json', 'max:10000'], // Size limit on JSON
91
+ 'tags' => ['nullable', 'array', 'max:10'],
92
+ 'tags.*' => ['string', 'max:50', 'alpha_dash'],
93
+ ];
94
+ }
95
+
96
+ // Sanitize AFTER validation
97
+ public function validated($key = null, $default = null): array
98
+ {
99
+ $data = parent::validated($key, $default);
100
+ $data['email'] = strtolower(trim($data['email']));
101
+ $data['name'] = strip_tags($data['name']);
102
+ return $data;
103
+ }
104
+ }
105
+ ```
106
+
107
+ ### SQL Injection Prevention
108
+
109
+ ```php
110
+ // ✅ ALWAYS Eloquent or parameterized
111
+ Lead::where('email', $request->validated('email'))->first();
112
+
113
+ // ✅ Raw with bindings
114
+ DB::select('SELECT * FROM leads WHERE email = ?', [$email]);
115
+
116
+ // ❌ NEVER interpolate user input
117
+ DB::select("SELECT * FROM leads WHERE email = '{$email}'"); // ❌ SQL INJECTION
118
+ ```
119
+
120
+ ### Mass Assignment Protection
121
+
122
+ ```php
123
+ class Lead extends Model
124
+ {
125
+ // Explicit fillable (whitelist approach)
126
+ protected $fillable = [
127
+ 'name', 'email', 'phone', 'domain_id', 'metadata',
128
+ ];
129
+
130
+ // NEVER: protected $guarded = []; ← allows EVERYTHING
131
+ }
132
+ ```
133
+
134
+ ## 3. Rate Limiting (Multi-Layer)
135
+
136
+ ```php
137
+ // app/Providers/AppServiceProvider.php
138
+ use Illuminate\Cache\RateLimiting\Limit;
139
+ use Illuminate\Support\Facades\RateLimiter;
140
+
141
+ public function boot(): void
142
+ {
143
+ // Global API limit
144
+ RateLimiter::for('api', function ($request) {
145
+ return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
146
+ });
147
+
148
+ // Strict limit on auth endpoints
149
+ RateLimiter::for('auth', function ($request) {
150
+ return [
151
+ Limit::perMinute(5)->by($request->ip()), // 5/min per IP
152
+ Limit::perHour(20)->by($request->ip()), // 20/hour per IP
153
+ Limit::perDay(50)->by($request->ip()), // 50/day per IP
154
+ ];
155
+ });
156
+
157
+ // Strict limit on sensitive actions
158
+ RateLimiter::for('sensitive', function ($request) {
159
+ return Limit::perMinute(3)->by($request->user()->id); // 3/min per user
160
+ });
161
+
162
+ // Webhook endpoints
163
+ RateLimiter::for('webhooks', function ($request) {
164
+ return Limit::perMinute(100)->by($request->ip());
165
+ });
166
+ }
167
+
168
+ // Routes
169
+ Route::middleware('throttle:auth')->group(function () {
170
+ Route::post('/login', [AuthController::class, 'login']);
171
+ Route::post('/register', [AuthController::class, 'register']);
172
+ Route::post('/forgot-password', [AuthController::class, 'forgotPassword']);
173
+ });
174
+
175
+ Route::middleware('throttle:sensitive')->group(function () {
176
+ Route::post('/change-password', [AuthController::class, 'changePassword']);
177
+ Route::post('/change-email', [AuthController::class, 'changeEmail']);
178
+ Route::delete('/account', [AuthController::class, 'deleteAccount']);
179
+ });
180
+ ```
181
+
182
+ ## 4. CORS (Restrictive)
183
+
184
+ ```php
185
+ // config/cors.php
186
+ return [
187
+ 'paths' => ['api/*'],
188
+ 'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
189
+ 'allowed_origins' => [
190
+ env('APP_URL'), // Only your domain
191
+ // NOT '*' — NEVER wildcard in production
192
+ ],
193
+ 'allowed_headers' => [
194
+ 'Content-Type', 'Authorization', 'X-Requested-With',
195
+ 'X-Timezone', 'Accept', 'X-CSRF-TOKEN',
196
+ ],
197
+ 'exposed_headers' => ['X-Request-ID', 'Retry-After'],
198
+ 'max_age' => 86400, // 24h preflight cache
199
+ 'supports_credentials' => true,
200
+ ];
201
+ ```
202
+
203
+ ## 5. Security Headers Middleware
204
+
205
+ ```php
206
+ // app/Http/Middleware/SecurityHeaders.php
207
+ class SecurityHeaders
208
+ {
209
+ public function handle($request, Closure $next)
210
+ {
211
+ $response = $next($request);
212
+
213
+ return $response
214
+ ->header('X-Content-Type-Options', 'nosniff')
215
+ ->header('X-Frame-Options', 'DENY')
216
+ ->header('X-XSS-Protection', '0') // Modern browsers use CSP instead
217
+ ->header('Referrer-Policy', 'strict-origin-when-cross-origin')
218
+ ->header('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
219
+ ->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload')
220
+ ->header('X-Request-ID', $request->attributes->get('request_id', (string) Str::uuid()))
221
+ ->header('Content-Security-Policy', implode('; ', [
222
+ "default-src 'self'",
223
+ "script-src 'self' 'unsafe-inline' https://js.stripe.com",
224
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
225
+ "img-src 'self' data: https:",
226
+ "font-src 'self' https://fonts.gstatic.com",
227
+ "connect-src 'self' https://api.stripe.com",
228
+ "frame-src https://js.stripe.com",
229
+ "object-src 'none'",
230
+ "base-uri 'self'",
231
+ ]));
232
+ }
233
+ }
234
+ ```
235
+
236
+ ## 6. Output Sanitization
237
+
238
+ ```php
239
+ // app/Http/Resources/LeadResource.php
240
+ class LeadResource extends JsonResource
241
+ {
242
+ use FormatsDatesForApi;
243
+
244
+ public function toArray(Request $request): array
245
+ {
246
+ return [
247
+ 'id' => $this->id,
248
+ 'name' => e($this->name), // HTML entity encode
249
+ 'email' => $this->email,
250
+ 'status' => $this->status->value,
251
+ 'created_at' => $this->formatDateTime($this->created_at, $request),
252
+ // NEVER expose:
253
+ // 'password' → NEVER
254
+ // 'api_token' → NEVER
255
+ // 'ip_address' → only if admin
256
+ // 'remember_token' → NEVER
257
+ ];
258
+ }
259
+ }
260
+
261
+ // Model hidden attributes (defense in depth)
262
+ class User extends Authenticatable
263
+ {
264
+ protected $hidden = [
265
+ 'password',
266
+ 'remember_token',
267
+ 'two_factor_secret',
268
+ 'two_factor_recovery_codes',
269
+ ];
270
+ }
271
+ ```
272
+
273
+ ## 7. Encryption at Rest
274
+
275
+ ```php
276
+ // Encrypt sensitive data in database
277
+ use Illuminate\Database\Eloquent\Casts\Attribute;
278
+
279
+ class ApiCredential extends Model
280
+ {
281
+ // Laravel encrypted cast (AES-256-CBC)
282
+ protected $casts = [
283
+ 'api_key' => 'encrypted',
284
+ 'api_secret' => 'encrypted',
285
+ 'webhook_secret' => 'encrypted',
286
+ ];
287
+ }
288
+
289
+ // For searchable encrypted fields
290
+ use Illuminate\Support\Facades\Crypt;
291
+
292
+ class Lead extends Model
293
+ {
294
+ // Store hash for lookup, encrypted value for display
295
+ protected static function booted(): void
296
+ {
297
+ static::creating(function ($lead) {
298
+ $lead->email_hash = hash('sha256', strtolower($lead->email));
299
+ $lead->email_encrypted = Crypt::encryptString($lead->email);
300
+ });
301
+ }
302
+ }
303
+ ```
304
+
305
+ ## 8. Audit Logging
306
+
307
+ ```php
308
+ // app/Traits/Auditable.php
309
+ trait Auditable
310
+ {
311
+ protected static function bootAuditable(): void
312
+ {
313
+ foreach (['created', 'updated', 'deleted'] as $event) {
314
+ static::$event(function ($model) use ($event) {
315
+ AuditLog::create([
316
+ 'auditable_type' => $model->getMorphClass(),
317
+ 'auditable_id' => $model->getKey(),
318
+ 'event' => $event,
319
+ 'old_values' => $event === 'updated' ? $model->getOriginal() : null,
320
+ 'new_values' => $event !== 'deleted' ? $model->getAttributes() : null,
321
+ 'user_id' => auth()->id(),
322
+ 'ip_address' => request()->ip(),
323
+ 'user_agent' => request()->userAgent(),
324
+ ]);
325
+ });
326
+ }
327
+ }
328
+ }
329
+
330
+ // Usage on sensitive models
331
+ class Lead extends Model
332
+ {
333
+ use HasUuids, Auditable;
334
+ }
335
+ ```
336
+
337
+ ## 9. Brute Force Protection
338
+
339
+ ```php
340
+ // app/Http/Controllers/Auth/LoginController.php
341
+ public function login(LoginRequest $request): JsonResponse
342
+ {
343
+ $key = 'login_attempts:' . $request->ip() . ':' . Str::lower($request->email);
344
+
345
+ // Check lockout (progressive delay)
346
+ $attempts = (int) Cache::get($key, 0);
347
+ if ($attempts >= 5) {
348
+ $lockoutMinutes = min(pow(2, $attempts - 5), 60); // 1, 2, 4, 8, 16, 32, 60 min
349
+ $ttl = Cache::get("{$key}:lockout");
350
+ if ($ttl && now()->lt($ttl)) {
351
+ return $this->error('Too many attempts. Try again later.', 429);
352
+ }
353
+ }
354
+
355
+ if (!Auth::attempt($request->validated())) {
356
+ // Increment with exponential TTL
357
+ Cache::put($key, $attempts + 1, now()->addHours(1));
358
+ if ($attempts + 1 >= 5) {
359
+ $lockoutMinutes = min(pow(2, $attempts - 4), 60);
360
+ Cache::put("{$key}:lockout", now()->addMinutes($lockoutMinutes), now()->addMinutes($lockoutMinutes));
361
+ }
362
+
363
+ // Generic message (don't reveal if user exists)
364
+ return $this->error('Invalid credentials', 401);
365
+ }
366
+
367
+ // Reset on success
368
+ Cache::forget($key);
369
+ Cache::forget("{$key}:lockout");
370
+
371
+ $token = $request->user()->createToken('session')->plainTextToken;
372
+ return $this->success(['token' => $token]);
373
+ }
374
+ ```
375
+
376
+ ## 10. Request ID Tracking
377
+
378
+ ```php
379
+ // app/Http/Middleware/RequestId.php
380
+ class RequestId
381
+ {
382
+ public function handle($request, Closure $next)
383
+ {
384
+ $requestId = $request->header('X-Request-ID', (string) Str::uuid());
385
+ $request->attributes->set('request_id', $requestId);
386
+
387
+ // Add to all log entries in this request
388
+ Log::shareContext(['request_id' => $requestId]);
389
+
390
+ $response = $next($request);
391
+ $response->header('X-Request-ID', $requestId);
392
+
393
+ return $response;
394
+ }
395
+ }
396
+ ```
397
+
398
+ ## Security Checklist — Before Deploy
399
+
400
+ - [ ] All endpoints have auth middleware
401
+ - [ ] All input validated via FormRequest
402
+ - [ ] Rate limiting on auth + sensitive endpoints
403
+ - [ ] CORS restricted to your domain only
404
+ - [ ] Security headers middleware active
405
+ - [ ] Sensitive fields `$hidden` on models
406
+ - [ ] API keys encrypted at rest
407
+ - [ ] Audit logging on sensitive models
408
+ - [ ] Brute force protection on login
409
+ - [ ] Request ID tracking in all logs
410
+ - [ ] No `env()` in code (only `config()`)
411
+ - [ ] No `*` in token abilities
412
+ - [ ] No `$guarded = []` on models
413
+ - [ ] CSP headers configured
414
+ - [ ] HSTS enabled
415
+ - [ ] Webhook signatures verified
416
+ - [ ] Error responses don't expose internals
417
+
418
+ ## FORBIDDEN
419
+
420
+ | ❌ Don't | ✅ Do |
421
+ |---|---|
422
+ | `$guarded = []` | Explicit `$fillable` |
423
+ | `'allowed_origins' => ['*']` | Your domain only |
424
+ | `createToken('x', ['*'])` | Specific abilities |
425
+ | `"WHERE email = '$email'"` | Parameterized queries |
426
+ | `dd($user)` in production | `Log::info()` structured |
427
+ | Generic error messages with stack traces | Clean error + request_id for debugging |
428
+ | Store API keys in plaintext | `'encrypted'` cast |
429
+ | Same rate limit for all endpoints | Progressive: auth(5/min) < api(60/min) |
430
+ | Trust `X-Forwarded-For` directly | Use trusted proxies config |
431
+ | No token expiry | 24h max, rotate on sensitive actions |