start-vibing-stacks 2.1.1 → 2.2.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.
package/dist/ui.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Start Vibing Stacks — Terminal UI
3
3
  */
4
4
  import chalk from 'chalk';
5
- const VERSION = '2.1.1';
5
+ const VERSION = '2.2.0';
6
6
  const gradient = (text) => {
7
7
  const colors = [chalk.hex('#FF6B6B'), chalk.hex('#FF8E53'), chalk.hex('#FFBD2E'), chalk.hex('#48BB78'), chalk.hex('#4299E1'), chalk.hex('#9F7AEA')];
8
8
  return text.split('').map((c, i) => colors[i % colors.length](c)).join('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-vibing-stacks",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "AI-powered multi-stack dev workflow for Claude Code. Supports PHP, Node.js, Python and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 |