autoworkflow 3.1.5 → 3.5.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.
Files changed (123) hide show
  1. package/.claude/commands/analyze.md +19 -0
  2. package/.claude/commands/audit.md +26 -0
  3. package/.claude/commands/build.md +39 -0
  4. package/.claude/commands/commit.md +25 -0
  5. package/.claude/commands/fix.md +23 -0
  6. package/.claude/commands/plan.md +18 -0
  7. package/.claude/commands/suggest.md +23 -0
  8. package/.claude/commands/verify.md +18 -0
  9. package/.claude/hooks/post-bash-router.sh +20 -0
  10. package/.claude/hooks/post-commit.sh +140 -0
  11. package/.claude/hooks/pre-edit.sh +129 -0
  12. package/.claude/hooks/session-check.sh +79 -0
  13. package/.claude/settings.json +40 -6
  14. package/.claude/settings.local.json +3 -1
  15. package/.claude/skills/actix.md +337 -0
  16. package/.claude/skills/alembic.md +504 -0
  17. package/.claude/skills/angular.md +237 -0
  18. package/.claude/skills/api-design.md +187 -0
  19. package/.claude/skills/aspnet-core.md +377 -0
  20. package/.claude/skills/astro.md +245 -0
  21. package/.claude/skills/auth-clerk.md +327 -0
  22. package/.claude/skills/auth-firebase.md +367 -0
  23. package/.claude/skills/auth-nextauth.md +359 -0
  24. package/.claude/skills/auth-supabase.md +368 -0
  25. package/.claude/skills/axum.md +386 -0
  26. package/.claude/skills/blazor.md +456 -0
  27. package/.claude/skills/chi.md +348 -0
  28. package/.claude/skills/code-review.md +133 -0
  29. package/.claude/skills/csharp.md +296 -0
  30. package/.claude/skills/css-modules.md +325 -0
  31. package/.claude/skills/cypress.md +343 -0
  32. package/.claude/skills/debugging.md +133 -0
  33. package/.claude/skills/diesel.md +392 -0
  34. package/.claude/skills/django.md +301 -0
  35. package/.claude/skills/docker.md +319 -0
  36. package/.claude/skills/doctrine.md +473 -0
  37. package/.claude/skills/documentation.md +182 -0
  38. package/.claude/skills/dotnet.md +409 -0
  39. package/.claude/skills/drizzle.md +293 -0
  40. package/.claude/skills/echo.md +321 -0
  41. package/.claude/skills/eloquent.md +256 -0
  42. package/.claude/skills/emotion.md +426 -0
  43. package/.claude/skills/entity-framework.md +370 -0
  44. package/.claude/skills/express.md +316 -0
  45. package/.claude/skills/fastapi.md +329 -0
  46. package/.claude/skills/fastify.md +299 -0
  47. package/.claude/skills/fiber.md +315 -0
  48. package/.claude/skills/flask.md +322 -0
  49. package/.claude/skills/gin.md +342 -0
  50. package/.claude/skills/git.md +116 -0
  51. package/.claude/skills/github-actions.md +353 -0
  52. package/.claude/skills/go.md +377 -0
  53. package/.claude/skills/gorm.md +409 -0
  54. package/.claude/skills/graphql.md +478 -0
  55. package/.claude/skills/hibernate.md +379 -0
  56. package/.claude/skills/hono.md +306 -0
  57. package/.claude/skills/java.md +400 -0
  58. package/.claude/skills/jest.md +313 -0
  59. package/.claude/skills/jpa.md +282 -0
  60. package/.claude/skills/kotlin.md +347 -0
  61. package/.claude/skills/kubernetes.md +363 -0
  62. package/.claude/skills/laravel.md +414 -0
  63. package/.claude/skills/mcp-browser.md +320 -0
  64. package/.claude/skills/mcp-database.md +219 -0
  65. package/.claude/skills/mcp-fetch.md +241 -0
  66. package/.claude/skills/mcp-filesystem.md +204 -0
  67. package/.claude/skills/mcp-github.md +217 -0
  68. package/.claude/skills/mcp-memory.md +240 -0
  69. package/.claude/skills/mcp-search.md +218 -0
  70. package/.claude/skills/mcp-slack.md +262 -0
  71. package/.claude/skills/micronaut.md +388 -0
  72. package/.claude/skills/mongodb.md +319 -0
  73. package/.claude/skills/mongoose.md +355 -0
  74. package/.claude/skills/mysql.md +281 -0
  75. package/.claude/skills/nestjs.md +335 -0
  76. package/.claude/skills/nextjs-app-router.md +260 -0
  77. package/.claude/skills/nextjs-pages.md +172 -0
  78. package/.claude/skills/nuxt.md +202 -0
  79. package/.claude/skills/openapi.md +489 -0
  80. package/.claude/skills/performance.md +199 -0
  81. package/.claude/skills/php.md +398 -0
  82. package/.claude/skills/playwright.md +371 -0
  83. package/.claude/skills/postgresql.md +257 -0
  84. package/.claude/skills/prisma.md +293 -0
  85. package/.claude/skills/pydantic.md +304 -0
  86. package/.claude/skills/pytest.md +313 -0
  87. package/.claude/skills/python.md +272 -0
  88. package/.claude/skills/quarkus.md +377 -0
  89. package/.claude/skills/react.md +230 -0
  90. package/.claude/skills/redis.md +391 -0
  91. package/.claude/skills/refactoring.md +143 -0
  92. package/.claude/skills/remix.md +246 -0
  93. package/.claude/skills/rest-api.md +490 -0
  94. package/.claude/skills/rocket.md +366 -0
  95. package/.claude/skills/rust.md +341 -0
  96. package/.claude/skills/sass.md +380 -0
  97. package/.claude/skills/sea-orm.md +382 -0
  98. package/.claude/skills/security.md +167 -0
  99. package/.claude/skills/sequelize.md +395 -0
  100. package/.claude/skills/spring-boot.md +416 -0
  101. package/.claude/skills/sqlalchemy.md +269 -0
  102. package/.claude/skills/sqlx-rust.md +408 -0
  103. package/.claude/skills/state-jotai.md +346 -0
  104. package/.claude/skills/state-mobx.md +353 -0
  105. package/.claude/skills/state-pinia.md +431 -0
  106. package/.claude/skills/state-redux.md +337 -0
  107. package/.claude/skills/state-tanstack-query.md +434 -0
  108. package/.claude/skills/state-zustand.md +340 -0
  109. package/.claude/skills/styled-components.md +403 -0
  110. package/.claude/skills/svelte.md +238 -0
  111. package/.claude/skills/sveltekit.md +207 -0
  112. package/.claude/skills/symfony.md +437 -0
  113. package/.claude/skills/tailwind.md +279 -0
  114. package/.claude/skills/terraform.md +394 -0
  115. package/.claude/skills/testing-library.md +371 -0
  116. package/.claude/skills/trpc.md +426 -0
  117. package/.claude/skills/typeorm.md +368 -0
  118. package/.claude/skills/vitest.md +330 -0
  119. package/.claude/skills/vue.md +202 -0
  120. package/.claude/skills/warp.md +365 -0
  121. package/README.md +135 -52
  122. package/package.json +1 -1
  123. package/system/triggers.md +152 -11
@@ -0,0 +1,382 @@
1
+ # SeaORM Skill
2
+
3
+ ## Setup and Migrations
4
+ \`\`\`bash
5
+ # Install sea-orm-cli
6
+ cargo install sea-orm-cli
7
+
8
+ # Initialize migration directory
9
+ sea-orm-cli migrate init
10
+
11
+ # Create migration
12
+ sea-orm-cli migrate generate create_users
13
+
14
+ # Run migrations
15
+ sea-orm-cli migrate up
16
+
17
+ # Generate entities from database
18
+ sea-orm-cli generate entity -o src/entities --with-serde both
19
+ \`\`\`
20
+
21
+ ## Entity Definition
22
+ \`\`\`rust
23
+ // src/entities/user.rs
24
+ use sea_orm::entity::prelude::*;
25
+ use serde::{Deserialize, Serialize};
26
+
27
+ #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
28
+ #[sea_orm(table_name = "users")]
29
+ pub struct Model {
30
+ #[sea_orm(primary_key, auto_increment = false)]
31
+ pub id: Uuid,
32
+ #[sea_orm(unique)]
33
+ pub email: String,
34
+ pub name: String,
35
+ #[serde(skip_serializing)]
36
+ pub password_hash: String,
37
+ pub is_active: bool,
38
+ pub created_at: DateTimeUtc,
39
+ pub updated_at: DateTimeUtc,
40
+ }
41
+
42
+ #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
43
+ pub enum Relation {
44
+ #[sea_orm(has_many = "super::post::Entity")]
45
+ Posts,
46
+ #[sea_orm(has_one = "super::profile::Entity")]
47
+ Profile,
48
+ }
49
+
50
+ impl Related<super::post::Entity> for Entity {
51
+ fn to() -> RelationDef {
52
+ Relation::Posts.def()
53
+ }
54
+ }
55
+
56
+ impl Related<super::profile::Entity> for Entity {
57
+ fn to() -> RelationDef {
58
+ Relation::Profile.def()
59
+ }
60
+ }
61
+
62
+ impl ActiveModelBehavior for ActiveModel {}
63
+
64
+ // src/entities/post.rs
65
+ #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
66
+ #[sea_orm(table_name = "posts")]
67
+ pub struct Model {
68
+ #[sea_orm(primary_key, auto_increment = false)]
69
+ pub id: Uuid,
70
+ pub title: String,
71
+ #[sea_orm(column_type = "Text")]
72
+ pub content: String,
73
+ pub published: bool,
74
+ pub author_id: Uuid,
75
+ pub created_at: DateTimeUtc,
76
+ pub updated_at: DateTimeUtc,
77
+ }
78
+
79
+ #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
80
+ pub enum Relation {
81
+ #[sea_orm(
82
+ belongs_to = "super::user::Entity",
83
+ from = "Column::AuthorId",
84
+ to = "super::user::Column::Id"
85
+ )]
86
+ Author,
87
+ #[sea_orm(has_many = "super::comment::Entity")]
88
+ Comments,
89
+ }
90
+
91
+ impl Related<super::user::Entity> for Entity {
92
+ fn to() -> RelationDef {
93
+ Relation::Author.def()
94
+ }
95
+ }
96
+ \`\`\`
97
+
98
+ ## Database Connection
99
+ \`\`\`rust
100
+ use sea_orm::{Database, DatabaseConnection, ConnectOptions};
101
+ use std::time::Duration;
102
+
103
+ pub async fn create_connection() -> Result<DatabaseConnection, DbErr> {
104
+ let database_url = std::env::var("DATABASE_URL")
105
+ .expect("DATABASE_URL must be set");
106
+
107
+ let mut opts = ConnectOptions::new(database_url);
108
+ opts.max_connections(100)
109
+ .min_connections(5)
110
+ .connect_timeout(Duration::from_secs(10))
111
+ .idle_timeout(Duration::from_secs(300))
112
+ .sqlx_logging(true)
113
+ .sqlx_logging_level(log::LevelFilter::Debug);
114
+
115
+ Database::connect(opts).await
116
+ }
117
+ \`\`\`
118
+
119
+ ## CRUD Operations
120
+ \`\`\`rust
121
+ use sea_orm::*;
122
+ use crate::entities::{user, post};
123
+ use crate::entities::prelude::{User, Post};
124
+
125
+ pub struct UserRepository {
126
+ db: DatabaseConnection,
127
+ }
128
+
129
+ impl UserRepository {
130
+ pub fn new(db: DatabaseConnection) -> Self {
131
+ Self { db }
132
+ }
133
+
134
+ // Create
135
+ pub async fn create(&self, email: &str, name: &str, password: &str) -> Result<user::Model, AppError> {
136
+ let password_hash = hash_password(password)?;
137
+
138
+ let new_user = user::ActiveModel {
139
+ id: Set(Uuid::new_v4()),
140
+ email: Set(email.to_string()),
141
+ name: Set(name.to_string()),
142
+ password_hash: Set(password_hash),
143
+ is_active: Set(true),
144
+ created_at: Set(Utc::now()),
145
+ updated_at: Set(Utc::now()),
146
+ };
147
+
148
+ new_user.insert(&self.db).await.map_err(AppError::from)
149
+ }
150
+
151
+ // Find by ID
152
+ pub async fn find_by_id(&self, id: Uuid) -> Result<Option<user::Model>, AppError> {
153
+ User::find_by_id(id)
154
+ .one(&self.db)
155
+ .await
156
+ .map_err(AppError::from)
157
+ }
158
+
159
+ // Find by email
160
+ pub async fn find_by_email(&self, email: &str) -> Result<Option<user::Model>, AppError> {
161
+ User::find()
162
+ .filter(user::Column::Email.eq(email))
163
+ .one(&self.db)
164
+ .await
165
+ .map_err(AppError::from)
166
+ }
167
+
168
+ // List with pagination
169
+ pub async fn list(&self, page: u64, per_page: u64) -> Result<(Vec<user::Model>, u64), AppError> {
170
+ let paginator = User::find()
171
+ .filter(user::Column::IsActive.eq(true))
172
+ .order_by_desc(user::Column::CreatedAt)
173
+ .paginate(&self.db, per_page);
174
+
175
+ let total = paginator.num_items().await?;
176
+ let users = paginator.fetch_page(page - 1).await?;
177
+
178
+ Ok((users, total))
179
+ }
180
+
181
+ // Update
182
+ pub async fn update(&self, id: Uuid, name: Option<String>, email: Option<String>) -> Result<user::Model, AppError> {
183
+ let user = User::find_by_id(id)
184
+ .one(&self.db)
185
+ .await?
186
+ .ok_or(AppError::NotFound)?;
187
+
188
+ let mut active: user::ActiveModel = user.into();
189
+
190
+ if let Some(n) = name {
191
+ active.name = Set(n);
192
+ }
193
+ if let Some(e) = email {
194
+ active.email = Set(e);
195
+ }
196
+ active.updated_at = Set(Utc::now());
197
+
198
+ active.update(&self.db).await.map_err(AppError::from)
199
+ }
200
+
201
+ // Delete
202
+ pub async fn delete(&self, id: Uuid) -> Result<DeleteResult, AppError> {
203
+ User::delete_by_id(id)
204
+ .exec(&self.db)
205
+ .await
206
+ .map_err(AppError::from)
207
+ }
208
+ }
209
+ \`\`\`
210
+
211
+ ## Relationships and Eager Loading
212
+ \`\`\`rust
213
+ // Find user with posts
214
+ pub async fn find_user_with_posts(&self, id: Uuid) -> Result<Option<(user::Model, Vec<post::Model>)>, AppError> {
215
+ User::find_by_id(id)
216
+ .find_with_related(Post)
217
+ .all(&self.db)
218
+ .await
219
+ .map(|results| results.into_iter().next())
220
+ .map_err(AppError::from)
221
+ }
222
+
223
+ // Find posts with authors
224
+ pub async fn find_posts_with_authors(&self) -> Result<Vec<(post::Model, Option<user::Model>)>, AppError> {
225
+ Post::find()
226
+ .filter(post::Column::Published.eq(true))
227
+ .find_also_related(User)
228
+ .order_by_desc(post::Column::CreatedAt)
229
+ .all(&self.db)
230
+ .await
231
+ .map_err(AppError::from)
232
+ }
233
+
234
+ // Custom select with partial fields
235
+ #[derive(FromQueryResult, Serialize)]
236
+ pub struct UserSummary {
237
+ pub id: Uuid,
238
+ pub email: String,
239
+ pub name: String,
240
+ }
241
+
242
+ pub async fn list_summaries(&self) -> Result<Vec<UserSummary>, AppError> {
243
+ User::find()
244
+ .select_only()
245
+ .column(user::Column::Id)
246
+ .column(user::Column::Email)
247
+ .column(user::Column::Name)
248
+ .filter(user::Column::IsActive.eq(true))
249
+ .into_model::<UserSummary>()
250
+ .all(&self.db)
251
+ .await
252
+ .map_err(AppError::from)
253
+ }
254
+ \`\`\`
255
+
256
+ ## Complex Queries
257
+ \`\`\`rust
258
+ use sea_orm::{QueryOrder, QuerySelect, Condition};
259
+
260
+ // Complex filtering
261
+ pub async fn search_users(
262
+ &self,
263
+ query: Option<&str>,
264
+ is_active: Option<bool>,
265
+ page: u64,
266
+ per_page: u64,
267
+ ) -> Result<Vec<user::Model>, AppError> {
268
+ let mut condition = Condition::all();
269
+
270
+ if let Some(q) = query {
271
+ condition = condition.add(
272
+ Condition::any()
273
+ .add(user::Column::Name.contains(q))
274
+ .add(user::Column::Email.contains(q))
275
+ );
276
+ }
277
+
278
+ if let Some(active) = is_active {
279
+ condition = condition.add(user::Column::IsActive.eq(active));
280
+ }
281
+
282
+ User::find()
283
+ .filter(condition)
284
+ .order_by_desc(user::Column::CreatedAt)
285
+ .offset((page - 1) * per_page)
286
+ .limit(per_page)
287
+ .all(&self.db)
288
+ .await
289
+ .map_err(AppError::from)
290
+ }
291
+
292
+ // Aggregate query with raw SQL
293
+ use sea_orm::{FromQueryResult, Statement};
294
+
295
+ #[derive(FromQueryResult)]
296
+ pub struct UserStats {
297
+ pub user_id: Uuid,
298
+ pub post_count: i64,
299
+ pub comment_count: i64,
300
+ }
301
+
302
+ pub async fn get_user_stats(&self, user_id: Uuid) -> Result<UserStats, AppError> {
303
+ UserStats::find_by_statement(Statement::from_sql_and_values(
304
+ DbBackend::Postgres,
305
+ r#"
306
+ SELECT
307
+ u.id as user_id,
308
+ COUNT(DISTINCT p.id) as post_count,
309
+ COUNT(DISTINCT c.id) as comment_count
310
+ FROM users u
311
+ LEFT JOIN posts p ON p.author_id = u.id
312
+ LEFT JOIN comments c ON c.user_id = u.id
313
+ WHERE u.id = $1
314
+ GROUP BY u.id
315
+ "#,
316
+ [user_id.into()],
317
+ ))
318
+ .one(&self.db)
319
+ .await?
320
+ .ok_or(AppError::NotFound)
321
+ }
322
+ \`\`\`
323
+
324
+ ## Transactions
325
+ \`\`\`rust
326
+ use sea_orm::TransactionTrait;
327
+
328
+ pub async fn create_user_with_profile(
329
+ &self,
330
+ email: &str,
331
+ name: &str,
332
+ password: &str,
333
+ bio: &str,
334
+ ) -> Result<(user::Model, profile::Model), AppError> {
335
+ self.db.transaction::<_, (user::Model, profile::Model), AppError>(|txn| {
336
+ Box::pin(async move {
337
+ // Create user
338
+ let user = user::ActiveModel {
339
+ id: Set(Uuid::new_v4()),
340
+ email: Set(email.to_string()),
341
+ name: Set(name.to_string()),
342
+ password_hash: Set(hash_password(password)?),
343
+ is_active: Set(true),
344
+ created_at: Set(Utc::now()),
345
+ updated_at: Set(Utc::now()),
346
+ }
347
+ .insert(txn)
348
+ .await?;
349
+
350
+ // Create profile
351
+ let profile = profile::ActiveModel {
352
+ id: Set(Uuid::new_v4()),
353
+ user_id: Set(user.id),
354
+ bio: Set(bio.to_string()),
355
+ }
356
+ .insert(txn)
357
+ .await?;
358
+
359
+ Ok((user, profile))
360
+ })
361
+ })
362
+ .await
363
+ .map_err(|e| match e {
364
+ TransactionError::Connection(e) => AppError::Database(e),
365
+ TransactionError::Transaction(e) => e,
366
+ })
367
+ }
368
+ \`\`\`
369
+
370
+ ## ✅ DO
371
+ - Use \`Set()\` to set values in ActiveModel
372
+ - Use \`NotSet\` for optional fields in updates
373
+ - Use \`.paginate()\` for pagination
374
+ - Use \`find_with_related()\` for eager loading
375
+ - Use transactions for multi-step operations
376
+ - Use \`FromQueryResult\` for custom query results
377
+
378
+ ## ❌ DON'T
379
+ - Don't forget to derive \`DeriveEntityModel\` for entities
380
+ - Don't use \`.unwrap()\` - handle \`DbErr\` properly
381
+ - Don't load relationships separately when you can use \`find_with_related\`
382
+ - Don't forget to define \`Relation\` enum for relationships
@@ -0,0 +1,167 @@
1
+ # Security Skill
2
+
3
+ ## Input Validation
4
+ \`\`\`typescript
5
+ // ✅ ALWAYS validate with Zod
6
+ import { z } from 'zod';
7
+
8
+ const userSchema = z.object({
9
+ email: z.string().email().max(255),
10
+ name: z.string().min(1).max(100).trim(),
11
+ age: z.number().int().min(0).max(150).optional()
12
+ });
13
+
14
+ const result = userSchema.safeParse(input);
15
+ if (!result.success) throw new ValidationError(result.error);
16
+ \`\`\`
17
+
18
+ ## SQL Injection Prevention
19
+ \`\`\`typescript
20
+ // ❌ NEVER: String concatenation
21
+ db.query(\`SELECT * FROM users WHERE id = '\${userId}'\`);
22
+
23
+ // ✅ ALWAYS: Parameterized queries or ORM
24
+ await prisma.user.findUnique({ where: { id: userId } });
25
+ \`\`\`
26
+
27
+ ## XSS Prevention
28
+ \`\`\`tsx
29
+ // ❌ NEVER with user content
30
+ <div dangerouslySetInnerHTML={{ __html: userInput }} />
31
+
32
+ // ✅ React auto-escapes
33
+ <div>{userInput}</div>
34
+
35
+ // ✅ If HTML needed, sanitize
36
+ import DOMPurify from 'dompurify';
37
+ <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }} />
38
+ \`\`\`
39
+
40
+ ## CORS Configuration
41
+ \`\`\`typescript
42
+ // Express with cors middleware
43
+ import cors from 'cors';
44
+
45
+ // ❌ DON'T: Allow all origins in production
46
+ app.use(cors()); // Allows everything
47
+
48
+ // ✅ DO: Whitelist specific origins
49
+ app.use(cors({
50
+ origin: ['https://myapp.com', 'https://admin.myapp.com'],
51
+ methods: ['GET', 'POST', 'PUT', 'DELETE'],
52
+ allowedHeaders: ['Content-Type', 'Authorization'],
53
+ credentials: true, // If using cookies
54
+ maxAge: 86400 // Cache preflight for 24 hours
55
+ }));
56
+ \`\`\`
57
+
58
+ ## Secure Headers (Helmet.js)
59
+ \`\`\`typescript
60
+ import helmet from 'helmet';
61
+
62
+ app.use(helmet()); // Applies all defaults
63
+
64
+ // Or configure individually:
65
+ app.use(helmet({
66
+ contentSecurityPolicy: {
67
+ directives: {
68
+ defaultSrc: ["'self'"],
69
+ scriptSrc: ["'self'", "'unsafe-inline'"], // Avoid if possible
70
+ styleSrc: ["'self'", "'unsafe-inline'"],
71
+ imgSrc: ["'self'", "data:", "https:"],
72
+ connectSrc: ["'self'", "https://api.myapp.com"],
73
+ },
74
+ },
75
+ hsts: { maxAge: 31536000, includeSubDomains: true },
76
+ frameguard: { action: 'deny' }, // Prevent clickjacking
77
+ }));
78
+ \`\`\`
79
+
80
+ ## Rate Limiting
81
+ \`\`\`typescript
82
+ import rateLimit from 'express-rate-limit';
83
+
84
+ // General API rate limiting
85
+ const apiLimiter = rateLimit({
86
+ windowMs: 15 * 60 * 1000, // 15 minutes
87
+ max: 100, // 100 requests per window
88
+ message: { error: 'Too many requests, try again later' },
89
+ standardHeaders: true, // Return rate limit info in headers
90
+ });
91
+ app.use('/api/', apiLimiter);
92
+
93
+ // Stricter limit for auth endpoints
94
+ const authLimiter = rateLimit({
95
+ windowMs: 60 * 60 * 1000, // 1 hour
96
+ max: 5, // 5 attempts per hour
97
+ skipSuccessfulRequests: true, // Don't count successful logins
98
+ });
99
+ app.use('/api/auth/login', authLimiter);
100
+ \`\`\`
101
+
102
+ ## CSRF Protection
103
+ \`\`\`typescript
104
+ // For traditional forms (not needed for JWT-only APIs)
105
+ import csrf from 'csurf';
106
+
107
+ app.use(csrf({ cookie: true }));
108
+
109
+ // Include token in forms
110
+ app.get('/form', (req, res) => {
111
+ res.render('form', { csrfToken: req.csrfToken() });
112
+ });
113
+
114
+ // In HTML: <input type="hidden" name="_csrf" value="{{csrfToken}}">
115
+ \`\`\`
116
+
117
+ ## Authentication Patterns
118
+ \`\`\`typescript
119
+ // ✅ Password hashing (12+ rounds)
120
+ const hash = await bcrypt.hash(password, 12);
121
+
122
+ // ✅ JWT with short expiry + refresh tokens
123
+ const accessToken = jwt.sign({ userId }, ACCESS_SECRET, { expiresIn: '15m' });
124
+ const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' });
125
+
126
+ // ✅ Secure cookie settings
127
+ res.cookie('refreshToken', refreshToken, {
128
+ httpOnly: true, // Not accessible via JavaScript
129
+ secure: true, // HTTPS only
130
+ sameSite: 'strict', // CSRF protection
131
+ maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
132
+ });
133
+ \`\`\`
134
+
135
+ ## Environment Variables
136
+ \`\`\`typescript
137
+ // ✅ Use a validation library
138
+ import { z } from 'zod';
139
+
140
+ const envSchema = z.object({
141
+ DATABASE_URL: z.string().url(),
142
+ JWT_SECRET: z.string().min(32),
143
+ NODE_ENV: z.enum(['development', 'production', 'test']),
144
+ });
145
+
146
+ // Validate at startup - fail fast if missing
147
+ export const env = envSchema.parse(process.env);
148
+ \`\`\`
149
+
150
+ ## ❌ DON'T
151
+ - Store passwords in plain text
152
+ - Expose stack traces in production
153
+ - Trust client-side validation alone
154
+ - Store secrets in code
155
+ - Use * for CORS origin in production
156
+ - Skip rate limiting on auth endpoints
157
+ - Log sensitive data (passwords, tokens)
158
+
159
+ ## ✅ DO
160
+ - Validate ALL input server-side
161
+ - Use environment variables for secrets
162
+ - Implement rate limiting
163
+ - Use HTTPS everywhere
164
+ - Keep dependencies updated
165
+ - Use Helmet for secure headers
166
+ - Configure CORS properly
167
+ - Validate env vars at startup