Compare commits

...

30 Commits

Author SHA1 Message Date
3fec791f28 Merge branch 'betterauth-migration' 2026-04-06 23:38:56 -04:00
5922a835cb style: fix unused variable warnings
Prefix unused error variables with underscore to indicate
intentionally unused catch parameters.
2026-04-06 23:27:32 -04:00
bd861e56c0 chore: remove stale FIXME.md
Remove outdated FIXME file about minimatch types from 2025.
2026-04-06 23:26:26 -04:00
403f41f078 fix(auth): sanitize error messages in error page
Add basic XSS sanitization by removing angle brackets from error
URL parameters before rendering.
2026-04-06 23:26:09 -04:00
4e7c56eec9 fix(ui): add error handling and loading state to sign-in
Add try/catch with toast notification and loading state for sign-in
button to improve UX and error visibility.
2026-04-06 23:25:40 -04:00
8d9329050d fix(ui): add error handling to sign-out
Add try/catch with toast notification for sign-out failures.
2026-04-06 23:24:40 -04:00
e1fd7dc5a3 feat(api): add input validation to AI endpoints
Add prompt validation to ai-event (non-empty string, max 2000 chars)
and events array length validation to ai-summary (max 100 items)
to prevent abuse and injection attacks.
2026-04-06 23:24:15 -04:00
a4656520f8 fix(db): ensure pgcrypto extension for UUID generation
Add CREATE EXTENSION IF NOT EXISTS pgcrypto to migration for
compatibility with older PostgreSQL versions.
2026-04-06 23:23:26 -04:00
3b5934dbfd fix(auth): correct session check in sign-out page
Change !session to !session?.user to properly detect unauthenticated
state. useSession() returns an object, not null.
2026-04-06 23:23:08 -04:00
cfa93da149 refactor(auth): remove unused SessionProvider wrapper
Remove the passthrough AuthSessionProvider component and its usage
in layout. better-auth hooks work without a provider wrapper.
2026-04-06 23:22:33 -04:00
c6017b2f78 fix(db): wrap migration in transaction
Add BEGIN/COMMIT transaction wrapper to migration to ensure
atomicity and prevent partial migration failures.
2026-04-06 23:21:42 -04:00
bcd488e2d3 fix(auth): use correct sign-in method for genericOAuth
Add genericOAuthClient plugin to auth client and change sign-in
call from signIn.social() to signIn.oauth2() with correct
providerId parameter.
2026-04-06 23:20:39 -04:00
c3026c8262 feat(api): add auth check to ai-summary endpoint
Require authentication for ai-summary endpoint to prevent
unauthorized API key usage and cost leakage.
2026-04-06 23:18:46 -04:00
4c6f880a3f feat(auth): configure trustedOrigins for CSRF protection
Add trustedOrigins to better-auth config to ensure proper origin
validation behind reverse proxy.
2026-04-06 23:18:20 -04:00
ece03a9124 feat(auth): validate required env vars at startup
Add explicit validation for BETTER_AUTH_SECRET, BETTER_AUTH_URL, and
Authentik config variables. Set secret explicitly in better-auth config
to prevent silent session loss on restart.
2026-04-06 23:17:51 -04:00
2a808f8ca1 fix(db): preserve OAuth user verified status during migration
Update emailVerified type conversion to set OAuth users (those with
account records) as verified before converting timestamp to boolean.
2026-04-06 23:16:58 -04:00
afb27eb66d fix(db): remove authenticator references from relations
Remove dropped authenticator table references from drizzle relations
to prevent drizzle-kit from attempting to recreate the table.
2026-04-06 23:16:34 -04:00
15be2399c6 refactor: migrate session usage to better-auth API 2026-04-06 22:41:57 -04:00
d7d52ef1a8 refactor: migrate auth pages to better-auth client 2026-04-06 22:41:37 -04:00
490c601dc1 refactor: remove next-auth SessionProvider wrapper 2026-04-06 22:41:25 -04:00
08a894577b refactor: replace next-auth with better-auth core and client 2026-04-06 22:41:11 -04:00
febc57b240 refactor: update DB schema for better-auth conventions 2026-04-06 22:41:00 -04:00
3ab77cc21f refactor: update env vars for better-auth 2026-04-06 22:40:41 -04:00
8a500f07de refactor: replace next-auth with better-auth dependency 2026-04-06 22:40:27 -04:00
d8875e587e feat: add PostgreSQL dev container compose file 2026-04-06 22:40:15 -04:00
47251dad3f chore: add debug.log to .gitignore 2026-04-06 22:40:01 -04:00
35a7f0a7c2 ai skills
Signed-off-by: Dmytro Stanchiev <git@dmytros.dev>
2026-04-06 20:48:30 -04:00
f70a416fea devenv update
Signed-off-by: Dmytro Stanchiev <git@dmytros.dev>
2026-04-06 20:48:24 -04:00
206f028fdf init ruler
Signed-off-by: Dmytro Stanchiev <git@dmytros.dev>
2026-04-06 20:48:07 -04:00
4db02f47bf Merge branch 'main' of git.cloud.dmytros.dev:old4ever/local-cal 2026-04-06 20:40:10 -04:00
84 changed files with 11975 additions and 288 deletions

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/auth-implementation-patterns",
"installedAt": "2026-04-07T00:45:24.777Z"
}

View File

@@ -0,0 +1,638 @@
---
name: auth-implementation-patterns
description: Master authentication and authorization patterns including JWT, OAuth2, session management, and RBAC to build secure, scalable access control systems. Use when implementing auth systems, securing APIs, or debugging security issues.
---
# Authentication & Authorization Implementation Patterns
Build secure, scalable authentication and authorization systems using industry-standard patterns and modern best practices.
## When to Use This Skill
- Implementing user authentication systems
- Securing REST or GraphQL APIs
- Adding OAuth2/social login
- Implementing role-based access control (RBAC)
- Designing session management
- Migrating authentication systems
- Debugging auth issues
- Implementing SSO or multi-tenancy
## Core Concepts
### 1. Authentication vs Authorization
**Authentication (AuthN)**: Who are you?
- Verifying identity (username/password, OAuth, biometrics)
- Issuing credentials (sessions, tokens)
- Managing login/logout
**Authorization (AuthZ)**: What can you do?
- Permission checking
- Role-based access control (RBAC)
- Resource ownership validation
- Policy enforcement
### 2. Authentication Strategies
**Session-Based:**
- Server stores session state
- Session ID in cookie
- Traditional, simple, stateful
**Token-Based (JWT):**
- Stateless, self-contained
- Scales horizontally
- Can store claims
**OAuth2/OpenID Connect:**
- Delegate authentication
- Social login (Google, GitHub)
- Enterprise SSO
## JWT Authentication
### Pattern 1: JWT Implementation
```typescript
// JWT structure: header.payload.signature
import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";
interface JWTPayload {
userId: string;
email: string;
role: string;
iat: number;
exp: number;
}
// Generate JWT
function generateTokens(userId: string, email: string, role: string) {
const accessToken = jwt.sign(
{ userId, email, role },
process.env.JWT_SECRET!,
{ expiresIn: "15m" }, // Short-lived
);
const refreshToken = jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: "7d" }, // Long-lived
);
return { accessToken, refreshToken };
}
// Verify JWT
function verifyToken(token: string): JWTPayload {
try {
return jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new Error("Token expired");
}
if (error instanceof jwt.JsonWebTokenError) {
throw new Error("Invalid token");
}
throw error;
}
}
// Middleware
function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "No token provided" });
}
const token = authHeader.substring(7);
try {
const payload = verifyToken(token);
req.user = payload; // Attach user to request
next();
} catch (error) {
return res.status(401).json({ error: "Invalid token" });
}
}
// Usage
app.get("/api/profile", authenticate, (req, res) => {
res.json({ user: req.user });
});
```
### Pattern 2: Refresh Token Flow
```typescript
interface StoredRefreshToken {
token: string;
userId: string;
expiresAt: Date;
createdAt: Date;
}
class RefreshTokenService {
// Store refresh token in database
async storeRefreshToken(userId: string, refreshToken: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
await db.refreshTokens.create({
token: await hash(refreshToken), // Hash before storing
userId,
expiresAt,
});
}
// Refresh access token
async refreshAccessToken(refreshToken: string) {
// Verify refresh token
let payload;
try {
payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as {
userId: string;
};
} catch {
throw new Error("Invalid refresh token");
}
// Check if token exists in database
const storedToken = await db.refreshTokens.findOne({
where: {
token: await hash(refreshToken),
userId: payload.userId,
expiresAt: { $gt: new Date() },
},
});
if (!storedToken) {
throw new Error("Refresh token not found or expired");
}
// Get user
const user = await db.users.findById(payload.userId);
if (!user) {
throw new Error("User not found");
}
// Generate new access token
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: "15m" },
);
return { accessToken };
}
// Revoke refresh token (logout)
async revokeRefreshToken(refreshToken: string) {
await db.refreshTokens.deleteOne({
token: await hash(refreshToken),
});
}
// Revoke all user tokens (logout all devices)
async revokeAllUserTokens(userId: string) {
await db.refreshTokens.deleteMany({ userId });
}
}
// API endpoints
app.post("/api/auth/refresh", async (req, res) => {
const { refreshToken } = req.body;
try {
const { accessToken } =
await refreshTokenService.refreshAccessToken(refreshToken);
res.json({ accessToken });
} catch (error) {
res.status(401).json({ error: "Invalid refresh token" });
}
});
app.post("/api/auth/logout", authenticate, async (req, res) => {
const { refreshToken } = req.body;
await refreshTokenService.revokeRefreshToken(refreshToken);
res.json({ message: "Logged out successfully" });
});
```
## Session-Based Authentication
### Pattern 1: Express Session
```typescript
import session from "express-session";
import RedisStore from "connect-redis";
import { createClient } from "redis";
// Setup Redis for session storage
const redisClient = createClient({
url: process.env.REDIS_URL,
});
await redisClient.connect();
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production", // HTTPS only
httpOnly: true, // No JavaScript access
maxAge: 24 * 60 * 60 * 1000, // 24 hours
sameSite: "strict", // CSRF protection
},
}),
);
// Login
app.post("/api/auth/login", async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findOne({ email });
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Store user in session
req.session.userId = user.id;
req.session.role = user.role;
res.json({ user: { id: user.id, email: user.email, role: user.role } });
});
// Session middleware
function requireAuth(req: Request, res: Response, next: NextFunction) {
if (!req.session.userId) {
return res.status(401).json({ error: "Not authenticated" });
}
next();
}
// Protected route
app.get("/api/profile", requireAuth, async (req, res) => {
const user = await db.users.findById(req.session.userId);
res.json({ user });
});
// Logout
app.post("/api/auth/logout", (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: "Logout failed" });
}
res.clearCookie("connect.sid");
res.json({ message: "Logged out successfully" });
});
});
```
## OAuth2 / Social Login
### Pattern 1: OAuth2 with Passport.js
```typescript
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as GitHubStrategy } from "passport-github2";
// Google OAuth
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: "/api/auth/google/callback",
},
async (accessToken, refreshToken, profile, done) => {
try {
// Find or create user
let user = await db.users.findOne({
googleId: profile.id,
});
if (!user) {
user = await db.users.create({
googleId: profile.id,
email: profile.emails?.[0]?.value,
name: profile.displayName,
avatar: profile.photos?.[0]?.value,
});
}
return done(null, user);
} catch (error) {
return done(error, undefined);
}
},
),
);
// Routes
app.get(
"/api/auth/google",
passport.authenticate("google", {
scope: ["profile", "email"],
}),
);
app.get(
"/api/auth/google/callback",
passport.authenticate("google", { session: false }),
(req, res) => {
// Generate JWT
const tokens = generateTokens(req.user.id, req.user.email, req.user.role);
// Redirect to frontend with token
res.redirect(
`${process.env.FRONTEND_URL}/auth/callback?token=${tokens.accessToken}`,
);
},
);
```
## Authorization Patterns
### Pattern 1: Role-Based Access Control (RBAC)
```typescript
enum Role {
USER = "user",
MODERATOR = "moderator",
ADMIN = "admin",
}
const roleHierarchy: Record<Role, Role[]> = {
[Role.ADMIN]: [Role.ADMIN, Role.MODERATOR, Role.USER],
[Role.MODERATOR]: [Role.MODERATOR, Role.USER],
[Role.USER]: [Role.USER],
};
function hasRole(userRole: Role, requiredRole: Role): boolean {
return roleHierarchy[userRole].includes(requiredRole);
}
// Middleware
function requireRole(...roles: Role[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "Not authenticated" });
}
if (!roles.some((role) => hasRole(req.user.role, role))) {
return res.status(403).json({ error: "Insufficient permissions" });
}
next();
};
}
// Usage
app.delete(
"/api/users/:id",
authenticate,
requireRole(Role.ADMIN),
async (req, res) => {
// Only admins can delete users
await db.users.delete(req.params.id);
res.json({ message: "User deleted" });
},
);
```
### Pattern 2: Permission-Based Access Control
```typescript
enum Permission {
READ_USERS = "read:users",
WRITE_USERS = "write:users",
DELETE_USERS = "delete:users",
READ_POSTS = "read:posts",
WRITE_POSTS = "write:posts",
}
const rolePermissions: Record<Role, Permission[]> = {
[Role.USER]: [Permission.READ_POSTS, Permission.WRITE_POSTS],
[Role.MODERATOR]: [
Permission.READ_POSTS,
Permission.WRITE_POSTS,
Permission.READ_USERS,
],
[Role.ADMIN]: Object.values(Permission),
};
function hasPermission(userRole: Role, permission: Permission): boolean {
return rolePermissions[userRole]?.includes(permission) ?? false;
}
function requirePermission(...permissions: Permission[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "Not authenticated" });
}
const hasAllPermissions = permissions.every((permission) =>
hasPermission(req.user.role, permission),
);
if (!hasAllPermissions) {
return res.status(403).json({ error: "Insufficient permissions" });
}
next();
};
}
// Usage
app.get(
"/api/users",
authenticate,
requirePermission(Permission.READ_USERS),
async (req, res) => {
const users = await db.users.findAll();
res.json({ users });
},
);
```
### Pattern 3: Resource Ownership
```typescript
// Check if user owns resource
async function requireOwnership(
resourceType: "post" | "comment",
resourceIdParam: string = "id",
) {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "Not authenticated" });
}
const resourceId = req.params[resourceIdParam];
// Admins can access anything
if (req.user.role === Role.ADMIN) {
return next();
}
// Check ownership
let resource;
if (resourceType === "post") {
resource = await db.posts.findById(resourceId);
} else if (resourceType === "comment") {
resource = await db.comments.findById(resourceId);
}
if (!resource) {
return res.status(404).json({ error: "Resource not found" });
}
if (resource.userId !== req.user.userId) {
return res.status(403).json({ error: "Not authorized" });
}
next();
};
}
// Usage
app.put(
"/api/posts/:id",
authenticate,
requireOwnership("post"),
async (req, res) => {
// User can only update their own posts
const post = await db.posts.update(req.params.id, req.body);
res.json({ post });
},
);
```
## Security Best Practices
### Pattern 1: Password Security
```typescript
import bcrypt from "bcrypt";
import { z } from "zod";
// Password validation schema
const passwordSchema = z
.string()
.min(12, "Password must be at least 12 characters")
.regex(/[A-Z]/, "Password must contain uppercase letter")
.regex(/[a-z]/, "Password must contain lowercase letter")
.regex(/[0-9]/, "Password must contain number")
.regex(/[^A-Za-z0-9]/, "Password must contain special character");
// Hash password
async function hashPassword(password: string): Promise<string> {
const saltRounds = 12; // 2^12 iterations
return bcrypt.hash(password, saltRounds);
}
// Verify password
async function verifyPassword(
password: string,
hash: string,
): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// Registration with password validation
app.post("/api/auth/register", async (req, res) => {
try {
const { email, password } = req.body;
// Validate password
passwordSchema.parse(password);
// Check if user exists
const existingUser = await db.users.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: "Email already registered" });
}
// Hash password
const passwordHash = await hashPassword(password);
// Create user
const user = await db.users.create({
email,
passwordHash,
});
// Generate tokens
const tokens = generateTokens(user.id, user.email, user.role);
res.status(201).json({
user: { id: user.id, email: user.email },
...tokens,
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: error.errors[0].message });
}
res.status(500).json({ error: "Registration failed" });
}
});
```
### Pattern 2: Rate Limiting
```typescript
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
// Login rate limiter
const loginLimiter = rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: "Too many login attempts, please try again later",
standardHeaders: true,
legacyHeaders: false,
});
// API rate limiter
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
standardHeaders: true,
});
// Apply to routes
app.post("/api/auth/login", loginLimiter, async (req, res) => {
// Login logic
});
app.use("/api/", apiLimiter);
```
## Best Practices
1. **Never Store Plain Passwords**: Always hash with bcrypt/argon2
2. **Use HTTPS**: Encrypt data in transit
3. **Short-Lived Access Tokens**: 15-30 minutes max
4. **Secure Cookies**: httpOnly, secure, sameSite flags
5. **Validate All Input**: Email format, password strength
6. **Rate Limit Auth Endpoints**: Prevent brute force attacks
7. **Implement CSRF Protection**: For session-based auth
8. **Rotate Secrets Regularly**: JWT secrets, session secrets
9. **Log Security Events**: Login attempts, failed auth
10. **Use MFA When Possible**: Extra security layer
## Common Pitfalls
- **Weak Passwords**: Enforce strong password policies
- **JWT in localStorage**: Vulnerable to XSS, use httpOnly cookies
- **No Token Expiration**: Tokens should expire
- **Client-Side Auth Checks Only**: Always validate server-side
- **Insecure Password Reset**: Use secure tokens with expiration
- **No Rate Limiting**: Vulnerable to brute force
- **Trusting Client Data**: Always validate on server

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/broken-authentication",
"installedAt": "2026-04-07T00:45:24.780Z"
}

View File

@@ -0,0 +1,480 @@
---
name: broken-authentication
description: "Identify and exploit authentication and session management vulnerabilities in web applications. Broken authentication consistently ranks in the OWASP Top 10 and can lead to account takeover, identity theft, and unauthorized access to sensitive systems."
risk: unknown
source: community
author: zebbern
date_added: "2026-02-27"
---
# Broken Authentication Testing
## Purpose
Identify and exploit authentication and session management vulnerabilities in web applications. Broken authentication consistently ranks in the OWASP Top 10 and can lead to account takeover, identity theft, and unauthorized access to sensitive systems. This skill covers testing methodologies for password policies, session handling, multi-factor authentication, and credential management.
## Prerequisites
### Required Knowledge
- HTTP protocol and session mechanisms
- Authentication types (SFA, 2FA, MFA)
- Cookie and token handling
- Common authentication frameworks
### Required Tools
- Burp Suite Professional or Community
- Hydra or similar brute-force tools
- Custom wordlists for credential testing
- Browser developer tools
### Required Access
- Target application URL
- Test account credentials
- Written authorization for testing
## Outputs and Deliverables
1. **Authentication Assessment Report** - Document all identified vulnerabilities
2. **Credential Testing Results** - Brute-force and dictionary attack outcomes
3. **Session Security Analysis** - Token randomness and timeout evaluation
4. **Remediation Recommendations** - Security hardening guidance
## Core Workflow
### Phase 1: Authentication Mechanism Analysis
Understand the application's authentication architecture:
```
# Identify authentication type
- Password-based (forms, basic auth, digest)
- Token-based (JWT, OAuth, API keys)
- Certificate-based (mutual TLS)
- Multi-factor (SMS, TOTP, hardware tokens)
# Map authentication endpoints
/login, /signin, /authenticate
/register, /signup
/forgot-password, /reset-password
/logout, /signout
/api/auth/*, /oauth/*
```
Capture and analyze authentication requests:
```http
POST /login HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
username=test&password=test123
```
### Phase 2: Password Policy Testing
Evaluate password requirements and enforcement:
```bash
# Test minimum length (a, ab, abcdefgh)
# Test complexity (password, password1, Password1!)
# Test common weak passwords (123456, password, qwerty, admin)
# Test username as password (admin/admin, test/test)
```
Document policy gaps: Minimum length <8, no complexity, common passwords allowed, username as password.
### Phase 3: Credential Enumeration
Test for username enumeration vulnerabilities:
```bash
# Compare responses for valid vs invalid usernames
# Invalid: "Invalid username" vs Valid: "Invalid password"
# Check timing differences, response codes, registration messages
```
# Password reset
"Email sent if account exists" (secure)
"No account with that email" (leaks info)
# API responses
{"error": "user_not_found"}
{"error": "invalid_password"}
```
### Phase 4: Brute Force Testing
Test account lockout and rate limiting:
```bash
# Using Hydra for form-based auth
hydra -l admin -P /usr/share/wordlists/rockyou.txt \
target.com http-post-form \
"/login:username=^USER^&password=^PASS^:Invalid credentials"
# Using Burp Intruder
1. Capture login request
2. Send to Intruder
3. Set payload positions on password field
4. Load wordlist
5. Start attack
6. Analyze response lengths/codes
```
Check for protections:
```bash
# Account lockout
- After how many attempts?
- Duration of lockout?
- Lockout notification?
# Rate limiting
- Requests per minute limit?
- IP-based or account-based?
- Bypass via headers (X-Forwarded-For)?
# CAPTCHA
- After failed attempts?
- Easily bypassable?
```
### Phase 5: Credential Stuffing
Test with known breached credentials:
```bash
# Credential stuffing differs from brute force
# Uses known email:password pairs from breaches
# Using Burp Intruder with Pitchfork attack
1. Set username and password as positions
2. Load email list as payload 1
3. Load password list as payload 2 (matched pairs)
4. Analyze for successful logins
# Detection evasion
- Slow request rate
- Rotate source IPs
- Randomize user agents
- Add delays between attempts
```
### Phase 6: Session Management Testing
Analyze session token security:
```bash
# Capture session cookie
Cookie: SESSIONID=abc123def456
# Test token characteristics
1. Entropy - Is it random enough?
2. Length - Sufficient length (128+ bits)?
3. Predictability - Sequential patterns?
4. Secure flags - HttpOnly, Secure, SameSite?
```
Session token analysis:
```python
#!/usr/bin/env python3
import requests
import hashlib
# Collect multiple session tokens
tokens = []
for i in range(100):
response = requests.get("https://target.com/login")
token = response.cookies.get("SESSIONID")
tokens.append(token)
# Analyze for patterns
# Check for sequential increments
# Calculate entropy
# Look for timestamp components
```
### Phase 7: Session Fixation Testing
Test if session is regenerated after authentication:
```bash
# Step 1: Get session before login
GET /login HTTP/1.1
Response: Set-Cookie: SESSIONID=abc123
# Step 2: Login with same session
POST /login HTTP/1.1
Cookie: SESSIONID=abc123
username=valid&password=valid
# Step 3: Check if session changed
# VULNERABLE if SESSIONID remains abc123
# SECURE if new session assigned after login
```
Attack scenario:
```bash
# Attacker workflow:
1. Attacker visits site, gets session: SESSIONID=attacker_session
2. Attacker sends link to victim with fixed session:
https://target.com/login?SESSIONID=attacker_session
3. Victim logs in with attacker's session
4. Attacker now has authenticated session
```
### Phase 8: Session Timeout Testing
Verify session expiration policies:
```bash
# Test idle timeout
1. Login and note session cookie
2. Wait without activity (15, 30, 60 minutes)
3. Attempt to use session
4. Check if session is still valid
# Test absolute timeout
1. Login and continuously use session
2. Check if forced logout after set period (8 hours, 24 hours)
# Test logout functionality
1. Login and note session
2. Click logout
3. Attempt to reuse old session cookie
4. Session should be invalidated server-side
```
### Phase 9: Multi-Factor Authentication Testing
Assess MFA implementation security:
```bash
# OTP brute force
- 4-digit OTP = 10,000 combinations
- 6-digit OTP = 1,000,000 combinations
- Test rate limiting on OTP endpoint
# OTP bypass techniques
- Skip MFA step by direct URL access
- Modify response to indicate MFA passed
- Null/empty OTP submission
- Previous valid OTP reuse
# API Version Downgrade Attack (crAPI example)
# If /api/v3/check-otp has rate limiting, try older versions:
POST /api/v2/check-otp
{"otp": "1234"}
# Older API versions may lack security controls
# Using Burp for OTP testing
1. Capture OTP verification request
2. Send to Intruder
3. Set OTP field as payload position
4. Use numbers payload (0000-9999)
5. Check for successful bypass
```
Test MFA enrollment:
```bash
# Forced enrollment
- Can MFA be skipped during setup?
- Can backup codes be accessed without verification?
# Recovery process
- Can MFA be disabled via email alone?
- Social engineering potential?
```
### Phase 10: Password Reset Testing
Analyze password reset security:
```bash
# Token security
1. Request password reset
2. Capture reset link
3. Analyze token:
- Length and randomness
- Expiration time
- Single-use enforcement
- Account binding
# Token manipulation
https://target.com/reset?token=abc123&user=victim
# Try changing user parameter while using valid token
# Host header injection
POST /forgot-password HTTP/1.1
Host: attacker.com
email=victim@email.com
# Reset email may contain attacker's domain
```
## Quick Reference
### Common Vulnerability Types
| Vulnerability | Risk | Test Method |
|--------------|------|-------------|
| Weak passwords | High | Policy testing, dictionary attack |
| No lockout | High | Brute force testing |
| Username enumeration | Medium | Differential response analysis |
| Session fixation | High | Pre/post-login session comparison |
| Weak session tokens | High | Entropy analysis |
| No session timeout | Medium | Long-duration session testing |
| Insecure password reset | High | Token analysis, workflow bypass |
| MFA bypass | Critical | Direct access, response manipulation |
### Credential Testing Payloads
```bash
# Default credentials
admin:admin
admin:password
admin:123456
root:root
test:test
user:user
# Common passwords
123456
password
12345678
qwerty
abc123
password1
admin123
# Breached credential databases
- Have I Been Pwned dataset
- SecLists passwords
- Custom targeted lists
```
### Session Cookie Flags
| Flag | Purpose | Vulnerability if Missing |
|------|---------|------------------------|
| HttpOnly | Prevent JS access | XSS can steal session |
| Secure | HTTPS only | Sent over HTTP |
| SameSite | CSRF protection | Cross-site requests allowed |
| Path | URL scope | Broader exposure |
| Domain | Domain scope | Subdomain access |
| Expires | Lifetime | Persistent sessions |
### Rate Limiting Bypass Headers
```http
X-Forwarded-For: 127.0.0.1
X-Real-IP: 127.0.0.1
X-Originating-IP: 127.0.0.1
X-Client-IP: 127.0.0.1
X-Remote-IP: 127.0.0.1
True-Client-IP: 127.0.0.1
```
## Constraints and Limitations
### Legal Requirements
- Only test with explicit written authorization
- Avoid testing with real breached credentials
- Do not access actual user accounts
- Document all testing activities
### Technical Limitations
- CAPTCHA may prevent automated testing
- Rate limiting affects brute force timing
- MFA significantly increases attack difficulty
- Some vulnerabilities require victim interaction
### Scope Considerations
- Test accounts may behave differently than production
- Some features may be disabled in test environments
- Third-party authentication may be out of scope
- Production testing requires extra caution
## Examples
### Example 1: Account Lockout Bypass
**Scenario:** Test if account lockout can be bypassed
```bash
# Step 1: Identify lockout threshold
# Try 5 wrong passwords for admin account
# Result: "Account locked for 30 minutes"
# Step 2: Test bypass via IP rotation
# Use X-Forwarded-For header
POST /login HTTP/1.1
X-Forwarded-For: 192.168.1.1
username=admin&password=attempt1
# Increment IP for each attempt
X-Forwarded-For: 192.168.1.2
# Continue until successful or confirmed blocked
# Step 3: Test bypass via case manipulation
username=Admin (vs admin)
username=ADMIN
# Some systems treat these as different accounts
```
### Example 2: JWT Token Attack
**Scenario:** Exploit weak JWT implementation
```bash
# Step 1: Capture JWT token
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdCJ9.signature
# Step 2: Decode and analyze
# Header: {"alg":"HS256","typ":"JWT"}
# Payload: {"user":"test","role":"user"}
# Step 3: Try "none" algorithm attack
# Change header to: {"alg":"none","typ":"JWT"}
# Remove signature
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4ifQ.
# Step 4: Submit modified token
Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4ifQ.
```
### Example 3: Password Reset Token Exploitation
**Scenario:** Test password reset functionality
```bash
# Step 1: Request reset for test account
POST /forgot-password
email=test@example.com
# Step 2: Capture reset link
https://target.com/reset?token=a1b2c3d4e5f6
# Step 3: Test token properties
# Reuse: Try using same token twice
# Expiration: Wait 24+ hours and retry
# Modification: Change characters in token
# Step 4: Test for user parameter manipulation
https://target.com/reset?token=a1b2c3d4e5f6&email=admin@example.com
# Check if admin's password can be reset with test user's token
```
## Troubleshooting
| Issue | Solutions |
|-------|-----------|
| Brute force too slow | Identify rate limit scope; IP rotation; add delays; use targeted wordlists |
| Session analysis inconclusive | Collect 1000+ tokens; use statistical tools; check for timestamps; compare accounts |
| MFA cannot be bypassed | Document as secure; test backup/recovery mechanisms; check MFA fatigue; verify enrollment |
| Account lockout prevents testing | Request multiple test accounts; test threshold first; use slower timing |
## When to Use
This skill is applicable to execute the workflow or actions described in the overview.

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/bun-development",
"installedAt": "2026-04-07T00:45:24.781Z"
}

View File

@@ -0,0 +1,696 @@
---
name: bun-development
description: "Fast, modern JavaScript/TypeScript development with the Bun runtime, inspired by [oven-sh/bun](https://github.com/oven-sh/bun)."
risk: critical
source: community
date_added: "2026-02-27"
---
<!-- security-allowlist: curl-pipe-bash, irm-pipe-iex -->
# ⚡ Bun Development
> Fast, modern JavaScript/TypeScript development with the Bun runtime, inspired by [oven-sh/bun](https://github.com/oven-sh/bun).
## When to Use This Skill
Use this skill when:
- Starting new JS/TS projects with Bun
- Migrating from Node.js to Bun
- Optimizing development speed
- Using Bun's built-in tools (bundler, test runner)
- Troubleshooting Bun-specific issues
---
## 1. Getting Started
### 1.1 Installation
```bash
# macOS / Linux
curl -fsSL https://bun.sh/install | bash
# Windows
powershell -c "irm bun.sh/install.ps1 | iex"
# Homebrew
brew tap oven-sh/bun
brew install bun
# npm (if needed)
npm install -g bun
# Upgrade
bun upgrade
```
### 1.2 Why Bun?
| Feature | Bun | Node.js |
| :-------------- | :------------- | :-------------------------- |
| Startup time | ~25ms | ~100ms+ |
| Package install | 10-100x faster | Baseline |
| TypeScript | Native | Requires transpiler |
| JSX | Native | Requires transpiler |
| Test runner | Built-in | External (Jest, Vitest) |
| Bundler | Built-in | External (Webpack, esbuild) |
---
## 2. Project Setup
### 2.1 Create New Project
```bash
# Initialize project
bun init
# Creates:
# ├── package.json
# ├── tsconfig.json
# ├── index.ts
# └── README.md
# With specific template
bun create <template> <project-name>
# Examples
bun create react my-app # React app
bun create next my-app # Next.js app
bun create vite my-app # Vite app
bun create elysia my-api # Elysia API
```
### 2.2 package.json
```json
{
"name": "my-bun-project",
"version": "1.0.0",
"module": "index.ts",
"type": "module",
"scripts": {
"dev": "bun run --watch index.ts",
"start": "bun run index.ts",
"test": "bun test",
"build": "bun build ./index.ts --outdir ./dist",
"lint": "bunx eslint ."
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
}
}
```
### 2.3 tsconfig.json (Bun-optimized)
```json
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"types": ["bun-types"]
}
}
```
---
## 3. Package Management
### 3.1 Installing Packages
```bash
# Install from package.json
bun install # or 'bun i'
# Add dependencies
bun add express # Regular dependency
bun add -d typescript # Dev dependency
bun add -D @types/node # Dev dependency (alias)
bun add --optional pkg # Optional dependency
# From specific registry
bun add lodash --registry https://registry.npmmirror.com
# Install specific version
bun add react@18.2.0
bun add react@latest
bun add react@next
# From git
bun add github:user/repo
bun add git+https://github.com/user/repo.git
```
### 3.2 Removing & Updating
```bash
# Remove package
bun remove lodash
# Update packages
bun update # Update all
bun update lodash # Update specific
bun update --latest # Update to latest (ignore ranges)
# Check outdated
bun outdated
```
### 3.3 bunx (npx equivalent)
```bash
# Execute package binaries
bunx prettier --write .
bunx tsc --init
bunx create-react-app my-app
# With specific version
bunx -p typescript@4.9 tsc --version
# Run without installing
bunx cowsay "Hello from Bun!"
```
### 3.4 Lockfile
```bash
# bun.lockb is a binary lockfile (faster parsing)
# To generate text lockfile for debugging:
bun install --yarn # Creates yarn.lock
# Trust existing lockfile
bun install --frozen-lockfile
```
---
## 4. Running Code
### 4.1 Basic Execution
```bash
# Run TypeScript directly (no build step!)
bun run index.ts
# Run JavaScript
bun run index.js
# Run with arguments
bun run server.ts --port 3000
# Run package.json script
bun run dev
bun run build
# Short form (for scripts)
bun dev
bun build
```
### 4.2 Watch Mode
```bash
# Auto-restart on file changes
bun --watch run index.ts
# With hot reloading
bun --hot run server.ts
```
### 4.3 Environment Variables
```typescript
// .env file is loaded automatically!
// Access environment variables
const apiKey = Bun.env.API_KEY;
const port = Bun.env.PORT ?? "3000";
// Or use process.env (Node.js compatible)
const dbUrl = process.env.DATABASE_URL;
```
```bash
# Run with specific env file
bun --env-file=.env.production run index.ts
```
---
## 5. Built-in APIs
### 5.1 File System (Bun.file)
```typescript
// Read file
const file = Bun.file("./data.json");
const text = await file.text();
const json = await file.json();
const buffer = await file.arrayBuffer();
// File info
console.log(file.size); // bytes
console.log(file.type); // MIME type
// Write file
await Bun.write("./output.txt", "Hello, Bun!");
await Bun.write("./data.json", JSON.stringify({ foo: "bar" }));
// Stream large files
const reader = file.stream();
for await (const chunk of reader) {
console.log(chunk);
}
```
### 5.2 HTTP Server (Bun.serve)
```typescript
const server = Bun.serve({
port: 3000,
fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/") {
return new Response("Hello World!");
}
if (url.pathname === "/api/users") {
return Response.json([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
}
return new Response("Not Found", { status: 404 });
},
error(error) {
return new Response(`Error: ${error.message}`, { status: 500 });
},
});
console.log(`Server running at http://localhost:${server.port}`);
```
### 5.3 WebSocket Server
```typescript
const server = Bun.serve({
port: 3000,
fetch(req, server) {
// Upgrade to WebSocket
if (server.upgrade(req)) {
return; // Upgraded
}
return new Response("Upgrade failed", { status: 500 });
},
websocket: {
open(ws) {
console.log("Client connected");
ws.send("Welcome!");
},
message(ws, message) {
console.log(`Received: ${message}`);
ws.send(`Echo: ${message}`);
},
close(ws) {
console.log("Client disconnected");
},
},
});
```
### 5.4 SQLite (Bun.sql)
```typescript
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");
// Create table
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE
)
`);
// Insert
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
insert.run("Alice", "alice@example.com");
// Query
const query = db.prepare("SELECT * FROM users WHERE name = ?");
const user = query.get("Alice");
console.log(user); // { id: 1, name: "Alice", email: "alice@example.com" }
// Query all
const allUsers = db.query("SELECT * FROM users").all();
```
### 5.5 Password Hashing
```typescript
// Hash password
const password = "super-secret";
const hash = await Bun.password.hash(password);
// Verify password
const isValid = await Bun.password.verify(password, hash);
console.log(isValid); // true
// With algorithm options
const bcryptHash = await Bun.password.hash(password, {
algorithm: "bcrypt",
cost: 12,
});
```
---
## 6. Testing
### 6.1 Basic Tests
```typescript
// math.test.ts
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
describe("Math operations", () => {
it("adds two numbers", () => {
expect(1 + 1).toBe(2);
});
it("subtracts two numbers", () => {
expect(5 - 3).toBe(2);
});
});
```
### 6.2 Running Tests
```bash
# Run all tests
bun test
# Run specific file
bun test math.test.ts
# Run matching pattern
bun test --grep "adds"
# Watch mode
bun test --watch
# With coverage
bun test --coverage
# Timeout
bun test --timeout 5000
```
### 6.3 Matchers
```typescript
import { expect, test } from "bun:test";
test("matchers", () => {
// Equality
expect(1).toBe(1);
expect({ a: 1 }).toEqual({ a: 1 });
expect([1, 2]).toContain(1);
// Comparisons
expect(10).toBeGreaterThan(5);
expect(5).toBeLessThanOrEqual(5);
// Truthiness
expect(true).toBeTruthy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// Strings
expect("hello").toMatch(/ell/);
expect("hello").toContain("ell");
// Arrays
expect([1, 2, 3]).toHaveLength(3);
// Exceptions
expect(() => {
throw new Error("fail");
}).toThrow("fail");
// Async
await expect(Promise.resolve(1)).resolves.toBe(1);
await expect(Promise.reject("err")).rejects.toBe("err");
});
```
### 6.4 Mocking
```typescript
import { mock, spyOn } from "bun:test";
// Mock function
const mockFn = mock((x: number) => x * 2);
mockFn(5);
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith(5);
expect(mockFn.mock.results[0].value).toBe(10);
// Spy on method
const obj = {
method: () => "original",
};
const spy = spyOn(obj, "method").mockReturnValue("mocked");
expect(obj.method()).toBe("mocked");
expect(spy).toHaveBeenCalled();
```
---
## 7. Bundling
### 7.1 Basic Build
```bash
# Bundle for production
bun build ./src/index.ts --outdir ./dist
# With options
bun build ./src/index.ts \
--outdir ./dist \
--target browser \
--minify \
--sourcemap
```
### 7.2 Build API
```typescript
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
target: "browser", // or "bun", "node"
minify: true,
sourcemap: "external",
splitting: true,
format: "esm",
// External packages (not bundled)
external: ["react", "react-dom"],
// Define globals
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
// Naming
naming: {
entry: "[name].[hash].js",
chunk: "chunks/[name].[hash].js",
asset: "assets/[name].[hash][ext]",
},
});
if (!result.success) {
console.error(result.logs);
}
```
### 7.3 Compile to Executable
```bash
# Create standalone executable
bun build ./src/cli.ts --compile --outfile myapp
# Cross-compile
bun build ./src/cli.ts --compile --target=bun-linux-x64 --outfile myapp-linux
bun build ./src/cli.ts --compile --target=bun-darwin-arm64 --outfile myapp-mac
# With embedded assets
bun build ./src/cli.ts --compile --outfile myapp --embed ./assets
```
---
## 8. Migration from Node.js
### 8.1 Compatibility
```typescript
// Most Node.js APIs work out of the box
import fs from "fs";
import path from "path";
import crypto from "crypto";
// process is global
console.log(process.cwd());
console.log(process.env.HOME);
// Buffer is global
const buf = Buffer.from("hello");
// __dirname and __filename work
console.log(__dirname);
console.log(__filename);
```
### 8.2 Common Migration Steps
```bash
# 1. Install Bun
curl -fsSL https://bun.sh/install | bash
# 2. Replace package manager
rm -rf node_modules package-lock.json
bun install
# 3. Update scripts in package.json
# "start": "node index.js" → "start": "bun run index.ts"
# "test": "jest" → "test": "bun test"
# 4. Add Bun types
bun add -d @types/bun
```
### 8.3 Differences from Node.js
```typescript
// ❌ Node.js specific (may not work)
require("module") // Use import instead
require.resolve("pkg") // Use import.meta.resolve
__non_webpack_require__ // Not supported
// ✅ Bun equivalents
import pkg from "pkg";
const resolved = import.meta.resolve("pkg");
Bun.resolveSync("pkg", process.cwd());
// ❌ These globals differ
process.hrtime() // Use Bun.nanoseconds()
setImmediate() // Use queueMicrotask()
// ✅ Bun-specific features
const file = Bun.file("./data.txt"); // Fast file API
Bun.serve({ port: 3000, fetch: ... }); // Fast HTTP server
Bun.password.hash(password); // Built-in hashing
```
---
## 9. Performance Tips
### 9.1 Use Bun-native APIs
```typescript
// Slow (Node.js compat)
import fs from "fs/promises";
const content = await fs.readFile("./data.txt", "utf-8");
// Fast (Bun-native)
const file = Bun.file("./data.txt");
const content = await file.text();
```
### 9.2 Use Bun.serve for HTTP
```typescript
// Don't: Express/Fastify (overhead)
import express from "express";
const app = express();
// Do: Bun.serve (native, 4-10x faster)
Bun.serve({
fetch(req) {
return new Response("Hello!");
},
});
// Or use Elysia (Bun-optimized framework)
import { Elysia } from "elysia";
new Elysia().get("/", () => "Hello!").listen(3000);
```
### 9.3 Bundle for Production
```bash
# Always bundle and minify for production
bun build ./src/index.ts --outdir ./dist --minify --target node
# Then run the bundle
bun run ./dist/index.js
```
---
## Quick Reference
| Task | Command |
| :----------- | :----------------------------------------- |
| Init project | `bun init` |
| Install deps | `bun install` |
| Add package | `bun add <pkg>` |
| Run script | `bun run <script>` |
| Run file | `bun run file.ts` |
| Watch mode | `bun --watch run file.ts` |
| Run tests | `bun test` |
| Build | `bun build ./src/index.ts --outdir ./dist` |
| Execute pkg | `bunx <pkg>` |
---
## Resources
- [Bun Documentation](https://bun.sh/docs)
- [Bun GitHub](https://github.com/oven-sh/bun)
- [Elysia Framework](https://elysiajs.com/)
- [Bun Discord](https://bun.sh/discord)

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/drizzle-orm-expert",
"installedAt": "2026-04-07T00:45:24.781Z"
}

View File

@@ -0,0 +1,363 @@
---
name: drizzle-orm-expert
description: "Expert in Drizzle ORM for TypeScript — schema design, relational queries, migrations, and serverless database integration. Use when building type-safe database layers with Drizzle."
risk: safe
source: community
date_added: "2026-03-04"
---
# Drizzle ORM Expert
You are a production-grade Drizzle ORM expert. You help developers build type-safe, performant database layers using Drizzle ORM with TypeScript. You know schema design, the relational query API, Drizzle Kit migrations, and integrations with Next.js, tRPC, and serverless databases (Neon, PlanetScale, Turso, Supabase).
## When to Use This Skill
- Use when the user asks to set up Drizzle ORM in a new or existing project
- Use when designing database schemas with Drizzle's TypeScript-first approach
- Use when writing complex relational queries (joins, subqueries, aggregations)
- Use when setting up or troubleshooting Drizzle Kit migrations
- Use when integrating Drizzle with Next.js App Router, tRPC, or Hono
- Use when optimizing database performance (prepared statements, batching, connection pooling)
- Use when migrating from Prisma, TypeORM, or Knex to Drizzle
## Core Concepts
### Why Drizzle
Drizzle ORM is a TypeScript-first ORM that generates zero runtime overhead. Unlike Prisma (which uses a query engine binary), Drizzle compiles to raw SQL — making it ideal for edge runtimes and serverless. Key advantages:
- **SQL-like API**: If you know SQL, you know Drizzle
- **Zero dependencies**: Tiny bundle, works in Cloudflare Workers, Vercel Edge, Deno
- **Full type inference**: Schema → types → queries are all connected at compile time
- **Relational Query API**: Prisma-like nested includes without N+1 problems
## Schema Design Patterns
### Table Definitions
```typescript
// db/schema.ts
import { pgTable, text, integer, timestamp, boolean, uuid, pgEnum } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
// Enums
export const roleEnum = pgEnum("role", ["admin", "user", "moderator"]);
// Users table
export const users = pgTable("users", {
id: uuid("id").defaultRandom().primaryKey(),
email: text("email").notNull().unique(),
name: text("name").notNull(),
role: roleEnum("role").default("user").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
// Posts table with foreign key
export const posts = pgTable("posts", {
id: uuid("id").defaultRandom().primaryKey(),
title: text("title").notNull(),
content: text("content"),
published: boolean("published").default(false).notNull(),
authorId: uuid("author_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
```
### Relations
```typescript
// db/relations.ts
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
```
### Type Inference
```typescript
// Infer types directly from your schema — no separate type files needed
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";
export type User = InferSelectModel<typeof users>;
export type NewUser = InferInsertModel<typeof users>;
export type Post = InferSelectModel<typeof posts>;
export type NewPost = InferInsertModel<typeof posts>;
```
## Query Patterns
### Select Queries (SQL-like API)
```typescript
import { eq, and, like, desc, count, sql } from "drizzle-orm";
// Basic select
const allUsers = await db.select().from(users);
// Filtered with conditions
const admins = await db.select().from(users).where(eq(users.role, "admin"));
// Partial select (only specific columns)
const emails = await db.select({ email: users.email }).from(users);
// Join query
const postsWithAuthors = await db
.select({
title: posts.title,
authorName: users.name,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(10);
// Aggregation
const postCounts = await db
.select({
authorId: posts.authorId,
postCount: count(posts.id),
})
.from(posts)
.groupBy(posts.authorId);
```
### Relational Queries (Prisma-like API)
```typescript
// Nested includes — Drizzle resolves in a single query
const usersWithPosts = await db.query.users.findMany({
with: {
posts: {
where: eq(posts.published, true),
orderBy: [desc(posts.createdAt)],
limit: 5,
},
},
});
// Find one with nested data
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
with: { posts: true },
});
```
### Insert, Update, Delete
```typescript
// Insert with returning
const [newUser] = await db
.insert(users)
.values({ email: "dev@example.com", name: "Dev" })
.returning();
// Batch insert
await db.insert(posts).values([
{ title: "Post 1", authorId: newUser.id },
{ title: "Post 2", authorId: newUser.id },
]);
// Update
await db.update(users).set({ name: "Updated" }).where(eq(users.id, userId));
// Delete
await db.delete(posts).where(eq(posts.authorId, userId));
```
### Transactions
```typescript
const result = await db.transaction(async (tx) => {
const [user] = await tx.insert(users).values({ email, name }).returning();
await tx.insert(posts).values({ title: "Welcome Post", authorId: user.id });
return user;
});
```
## Migration Workflow (Drizzle Kit)
### Configuration
```typescript
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
```
### Commands
```bash
# Generate migration SQL from schema changes
npx drizzle-kit generate
# Push schema directly to database (development only — skips migration files)
npx drizzle-kit push
# Run pending migrations (production)
npx drizzle-kit migrate
# Open Drizzle Studio (GUI database browser)
npx drizzle-kit studio
```
## Database Client Setup
### PostgreSQL (Neon Serverless)
```typescript
// db/index.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
```
### SQLite (Turso/LibSQL)
```typescript
import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
import * as schema from "./schema";
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN,
});
export const db = drizzle(client, { schema });
```
### MySQL (PlanetScale)
```typescript
import { drizzle } from "drizzle-orm/planetscale-serverless";
import { Client } from "@planetscale/database";
import * as schema from "./schema";
const client = new Client({ url: process.env.DATABASE_URL! });
export const db = drizzle(client, { schema });
```
## Performance Optimization
### Prepared Statements
```typescript
// Prepare once, execute many times
const getUserById = db.query.users
.findFirst({
where: eq(users.id, sql.placeholder("id")),
})
.prepare("get_user_by_id");
// Execute with parameters
const user = await getUserById.execute({ id: "abc-123" });
```
### Batch Operations
```typescript
// Use db.batch() for multiple independent queries in one round-trip
const [allUsers, recentPosts] = await db.batch([
db.select().from(users),
db.select().from(posts).orderBy(desc(posts.createdAt)).limit(10),
]);
```
### Indexing in Schema
```typescript
import { index, uniqueIndex } from "drizzle-orm/pg-core";
export const posts = pgTable(
"posts",
{
id: uuid("id").defaultRandom().primaryKey(),
title: text("title").notNull(),
authorId: uuid("author_id").references(() => users.id).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [
index("posts_author_idx").on(table.authorId),
index("posts_created_idx").on(table.createdAt),
]
);
```
## Next.js Integration
### Server Component Usage
```typescript
// app/users/page.tsx (React Server Component)
import { db } from "@/db";
import { users } from "@/db/schema";
export default async function UsersPage() {
const allUsers = await db.select().from(users);
return (
<ul>
{allUsers.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
```
### Server Action
```typescript
// app/actions.ts
"use server";
import { db } from "@/db";
import { users } from "@/db/schema";
export async function createUser(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
await db.insert(users).values({ name, email });
}
```
## Best Practices
-**Do:** Keep all schema definitions in a single `db/schema.ts` or split by domain (`db/schema/users.ts`, `db/schema/posts.ts`)
-**Do:** Use `InferSelectModel` and `InferInsertModel` for type safety instead of manual interfaces
-**Do:** Use the relational query API (`db.query.*`) for nested data to avoid N+1 problems
-**Do:** Use prepared statements for frequently executed queries in production
-**Do:** Use `drizzle-kit generate` + `migrate` in production (never `push`)
-**Do:** Pass `{ schema }` to `drizzle()` to enable the relational query API
-**Don't:** Use `drizzle-kit push` in production — it can cause data loss
-**Don't:** Write raw SQL when the Drizzle query builder supports the operation
-**Don't:** Forget to define `relations()` if you want to use `db.query.*` with `with`
-**Don't:** Create a new database connection per request in serverless — use connection pooling
## Troubleshooting
**Problem:** `db.query.tableName` is undefined
**Solution:** Pass all schema objects (including relations) to `drizzle()`: `drizzle(client, { schema })`
**Problem:** Migration conflicts after schema changes
**Solution:** Run `npx drizzle-kit generate` to create a new migration, then `npx drizzle-kit migrate`
**Problem:** Type errors on `.returning()` with MySQL
**Solution:** MySQL does not support `RETURNING`. Use `.execute()` and read `insertId` from the result instead.

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/grill-me",
"installedAt": "2026-04-07T00:45:24.781Z"
}

View File

@@ -0,0 +1,10 @@
---
name: grill-me
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
---
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
Ask the questions one at a time.
If a question can be answered by exploring the codebase, explore the codebase instead.

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/nextjs-app-router-patterns",
"installedAt": "2026-04-07T00:45:24.782Z"
}

View File

@@ -0,0 +1,537 @@
---
name: nextjs-app-router-patterns
description: Master Next.js 14+ App Router with Server Components, streaming, parallel routes, and advanced data fetching. Use when building Next.js applications, implementing SSR/SSG, or optimizing React Server Components.
---
# Next.js App Router Patterns
Comprehensive patterns for Next.js 14+ App Router architecture, Server Components, and modern full-stack React development.
## When to Use This Skill
- Building new Next.js applications with App Router
- Migrating from Pages Router to App Router
- Implementing Server Components and streaming
- Setting up parallel and intercepting routes
- Optimizing data fetching and caching
- Building full-stack features with Server Actions
## Core Concepts
### 1. Rendering Modes
| Mode | Where | When to Use |
| --------------------- | ------------ | ----------------------------------------- |
| **Server Components** | Server only | Data fetching, heavy computation, secrets |
| **Client Components** | Browser | Interactivity, hooks, browser APIs |
| **Static** | Build time | Content that rarely changes |
| **Dynamic** | Request time | Personalized or real-time data |
| **Streaming** | Progressive | Large pages, slow data sources |
### 2. File Conventions
```
app/
├── layout.tsx # Shared UI wrapper
├── page.tsx # Route UI
├── loading.tsx # Loading UI (Suspense)
├── error.tsx # Error boundary
├── not-found.tsx # 404 UI
├── route.ts # API endpoint
├── template.tsx # Re-mounted layout
├── default.tsx # Parallel route fallback
└── opengraph-image.tsx # OG image generation
```
## Quick Start
```typescript
// app/layout.tsx
import { Inter } from 'next/font/google'
import { Providers } from './providers'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: { default: 'My App', template: '%s | My App' },
description: 'Built with Next.js App Router',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
)
}
// app/page.tsx - Server Component by default
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }, // ISR: revalidate every hour
})
return res.json()
}
export default async function HomePage() {
const products = await getProducts()
return (
<main>
<h1>Products</h1>
<ProductGrid products={products} />
</main>
)
}
```
## Patterns
### Pattern 1: Server Components with Data Fetching
```typescript
// app/products/page.tsx
import { Suspense } from 'react'
import { ProductList, ProductListSkeleton } from '@/components/products'
import { FilterSidebar } from '@/components/filters'
interface SearchParams {
category?: string
sort?: 'price' | 'name' | 'date'
page?: string
}
export default async function ProductsPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}) {
const params = await searchParams
return (
<div className="flex gap-8">
<FilterSidebar />
<Suspense
key={JSON.stringify(params)}
fallback={<ProductListSkeleton />}
>
<ProductList
category={params.category}
sort={params.sort}
page={Number(params.page) || 1}
/>
</Suspense>
</div>
)
}
// components/products/ProductList.tsx - Server Component
async function getProducts(filters: ProductFilters) {
const res = await fetch(
`${process.env.API_URL}/products?${new URLSearchParams(filters)}`,
{ next: { tags: ['products'] } }
)
if (!res.ok) throw new Error('Failed to fetch products')
return res.json()
}
export async function ProductList({ category, sort, page }: ProductFilters) {
const { products, totalPages } = await getProducts({ category, sort, page })
return (
<div>
<div className="grid grid-cols-3 gap-4">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
<Pagination currentPage={page} totalPages={totalPages} />
</div>
)
}
```
### Pattern 2: Client Components with 'use client'
```typescript
// components/products/AddToCartButton.tsx
'use client'
import { useState, useTransition } from 'react'
import { addToCart } from '@/app/actions/cart'
export function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition()
const [error, setError] = useState<string | null>(null)
const handleClick = () => {
setError(null)
startTransition(async () => {
const result = await addToCart(productId)
if (result.error) {
setError(result.error)
}
})
}
return (
<div>
<button
onClick={handleClick}
disabled={isPending}
className="btn-primary"
>
{isPending ? 'Adding...' : 'Add to Cart'}
</button>
{error && <p className="text-red-500 text-sm">{error}</p>}
</div>
)
}
```
### Pattern 3: Server Actions
```typescript
// app/actions/cart.ts
"use server";
import { revalidateTag } from "next/cache";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export async function addToCart(productId: string) {
const cookieStore = await cookies();
const sessionId = cookieStore.get("session")?.value;
if (!sessionId) {
redirect("/login");
}
try {
await db.cart.upsert({
where: { sessionId_productId: { sessionId, productId } },
update: { quantity: { increment: 1 } },
create: { sessionId, productId, quantity: 1 },
});
revalidateTag("cart");
return { success: true };
} catch (error) {
return { error: "Failed to add item to cart" };
}
}
export async function checkout(formData: FormData) {
const address = formData.get("address") as string;
const payment = formData.get("payment") as string;
// Validate
if (!address || !payment) {
return { error: "Missing required fields" };
}
// Process order
const order = await processOrder({ address, payment });
// Redirect to confirmation
redirect(`/orders/${order.id}/confirmation`);
}
```
### Pattern 4: Parallel Routes
```typescript
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<div className="dashboard-grid">
<main>{children}</main>
<aside className="analytics-panel">{analytics}</aside>
<aside className="team-panel">{team}</aside>
</div>
)
}
// app/dashboard/@analytics/page.tsx
export default async function AnalyticsSlot() {
const stats = await getAnalytics()
return <AnalyticsChart data={stats} />
}
// app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
return <ChartSkeleton />
}
// app/dashboard/@team/page.tsx
export default async function TeamSlot() {
const members = await getTeamMembers()
return <TeamList members={members} />
}
```
### Pattern 5: Intercepting Routes (Modal Pattern)
```typescript
// File structure for photo modal
// app/
// ├── @modal/
// │ ├── (.)photos/[id]/page.tsx # Intercept
// │ └── default.tsx
// ├── photos/
// │ └── [id]/page.tsx # Full page
// └── layout.tsx
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/Modal'
import { PhotoDetail } from '@/components/PhotoDetail'
export default async function PhotoModal({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const photo = await getPhoto(id)
return (
<Modal>
<PhotoDetail photo={photo} />
</Modal>
)
}
// app/photos/[id]/page.tsx - Full page version
export default async function PhotoPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const photo = await getPhoto(id)
return (
<div className="photo-page">
<PhotoDetail photo={photo} />
<RelatedPhotos photoId={id} />
</div>
)
}
// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
)
}
```
### Pattern 6: Streaming with Suspense
```typescript
// app/product/[id]/page.tsx
import { Suspense } from 'react'
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// This data loads first (blocking)
const product = await getProduct(id)
return (
<div>
{/* Immediate render */}
<ProductHeader product={product} />
{/* Stream in reviews */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={id} />
</Suspense>
{/* Stream in recommendations */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations productId={id} />
</Suspense>
</div>
)
}
// These components fetch their own data
async function Reviews({ productId }: { productId: string }) {
const reviews = await getReviews(productId) // Slow API
return <ReviewList reviews={reviews} />
}
async function Recommendations({ productId }: { productId: string }) {
const products = await getRecommendations(productId) // ML-based, slow
return <ProductCarousel products={products} />
}
```
### Pattern 7: Route Handlers (API Routes)
```typescript
// app/api/products/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const category = searchParams.get("category");
const products = await db.product.findMany({
where: category ? { category } : undefined,
take: 20,
});
return NextResponse.json(products);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const product = await db.product.create({
data: body,
});
return NextResponse.json(product, { status: 201 });
}
// app/api/products/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
if (!product) {
return NextResponse.json({ error: "Product not found" }, { status: 404 });
}
return NextResponse.json(product);
}
```
### Pattern 8: Metadata and SEO
```typescript
// app/products/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
type Props = {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const product = await getProduct(slug)
if (!product) return {}
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [{ url: product.image, width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
title: product.name,
description: product.description,
images: [product.image],
},
}
}
export async function generateStaticParams() {
const products = await db.product.findMany({ select: { slug: true } })
return products.map((p) => ({ slug: p.slug }))
}
export default async function ProductPage({ params }: Props) {
const { slug } = await params
const product = await getProduct(slug)
if (!product) notFound()
return <ProductDetail product={product} />
}
```
## Caching Strategies
### Data Cache
```typescript
// No cache (always fresh)
fetch(url, { cache: "no-store" });
// Cache forever (static)
fetch(url, { cache: "force-cache" });
// ISR - revalidate after 60 seconds
fetch(url, { next: { revalidate: 60 } });
// Tag-based invalidation
fetch(url, { next: { tags: ["products"] } });
// Invalidate via Server Action
("use server");
import { revalidateTag, revalidatePath } from "next/cache";
export async function updateProduct(id: string, data: ProductData) {
await db.product.update({ where: { id }, data });
revalidateTag("products");
revalidatePath("/products");
}
```
## Best Practices
### Do's
- **Start with Server Components** - Add 'use client' only when needed
- **Colocate data fetching** - Fetch data where it's used
- **Use Suspense boundaries** - Enable streaming for slow data
- **Leverage parallel routes** - Independent loading states
- **Use Server Actions** - For mutations with progressive enhancement
### Don'ts
- **Don't pass serializable data** - Server → Client boundary limitations
- **Don't use hooks in Server Components** - No useState, useEffect
- **Don't fetch in Client Components** - Use Server Components or React Query
- **Don't over-nest layouts** - Each layout adds to the component tree
- **Don't ignore loading states** - Always provide loading.tsx or Suspense

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/nextjs-best-practices",
"installedAt": "2026-04-07T00:45:24.782Z"
}

View File

@@ -0,0 +1,208 @@
---
name: nextjs-best-practices
description: "Next.js App Router principles. Server Components, data fetching, routing patterns."
risk: unknown
source: community
date_added: "2026-02-27"
---
# Next.js Best Practices
> Principles for Next.js App Router development.
---
## 1. Server vs Client Components
### Decision Tree
```
Does it need...?
├── useState, useEffect, event handlers
│ └── Client Component ('use client')
├── Direct data fetching, no interactivity
│ └── Server Component (default)
└── Both?
└── Split: Server parent + Client child
```
### By Default
| Type | Use |
|------|-----|
| **Server** | Data fetching, layout, static content |
| **Client** | Forms, buttons, interactive UI |
---
## 2. Data Fetching Patterns
### Fetch Strategy
| Pattern | Use |
|---------|-----|
| **Default** | Static (cached at build) |
| **Revalidate** | ISR (time-based refresh) |
| **No-store** | Dynamic (every request) |
### Data Flow
| Source | Pattern |
|--------|---------|
| Database | Server Component fetch |
| API | fetch with caching |
| User input | Client state + server action |
---
## 3. Routing Principles
### File Conventions
| File | Purpose |
|------|---------|
| `page.tsx` | Route UI |
| `layout.tsx` | Shared layout |
| `loading.tsx` | Loading state |
| `error.tsx` | Error boundary |
| `not-found.tsx` | 404 page |
### Route Organization
| Pattern | Use |
|---------|-----|
| Route groups `(name)` | Organize without URL |
| Parallel routes `@slot` | Multiple same-level pages |
| Intercepting `(.)` | Modal overlays |
---
## 4. API Routes
### Route Handlers
| Method | Use |
|--------|-----|
| GET | Read data |
| POST | Create data |
| PUT/PATCH | Update data |
| DELETE | Remove data |
### Best Practices
- Validate input with Zod
- Return proper status codes
- Handle errors gracefully
- Use Edge runtime when possible
---
## 5. Performance Principles
### Image Optimization
- Use next/image component
- Set priority for above-fold
- Provide blur placeholder
- Use responsive sizes
### Bundle Optimization
- Dynamic imports for heavy components
- Route-based code splitting (automatic)
- Analyze with bundle analyzer
---
## 6. Metadata
### Static vs Dynamic
| Type | Use |
|------|-----|
| Static export | Fixed metadata |
| generateMetadata | Dynamic per-route |
### Essential Tags
- title (50-60 chars)
- description (150-160 chars)
- Open Graph images
- Canonical URL
---
## 7. Caching Strategy
### Cache Layers
| Layer | Control |
|-------|---------|
| Request | fetch options |
| Data | revalidate/tags |
| Full route | route config |
### Revalidation
| Method | Use |
|--------|-----|
| Time-based | `revalidate: 60` |
| On-demand | `revalidatePath/Tag` |
| No cache | `no-store` |
---
## 8. Server Actions
### Use Cases
- Form submissions
- Data mutations
- Revalidation triggers
### Best Practices
- Mark with 'use server'
- Validate all inputs
- Return typed responses
- Handle errors
---
## 9. Anti-Patterns
| ❌ Don't | ✅ Do |
|----------|-------|
| 'use client' everywhere | Server by default |
| Fetch in client components | Fetch in server |
| Skip loading states | Use loading.tsx |
| Ignore error boundaries | Use error.tsx |
| Large client bundles | Dynamic imports |
---
## 10. Project Structure
```
app/
├── (marketing)/ # Route group
│ └── page.tsx
├── (dashboard)/
│ ├── layout.tsx # Dashboard layout
│ └── page.tsx
├── api/
│ └── [resource]/
│ └── route.ts
└── components/
└── ui/
```
---
> **Remember:** Server Components are the default for a reason. Start there, add client only when needed.
## When to Use
This skill is applicable to execute the workflow or actions described in the overview.

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/nextjs-developer",
"installedAt": "2026-04-07T00:45:24.782Z"
}

View File

@@ -0,0 +1,143 @@
---
name: nextjs-developer
description: "Use when building Next.js 14+ applications with App Router, server components, or server actions. Invoke to configure route handlers, implement middleware, set up API routes, add streaming SSR, write generateMetadata for SEO, scaffold loading.tsx/error.tsx boundaries, or deploy to Vercel. Triggers on: Next.js, Next.js 14, App Router, RSC, use server, Server Components, Server Actions, React Server Components, generateMetadata, loading.tsx, Next.js deployment, Vercel, Next.js performance."
license: MIT
metadata:
author: https://github.com/Jeffallan
version: "1.1.0"
domain: frontend
triggers: Next.js, Next.js 14, App Router, Server Components, Server Actions, React Server Components, Next.js deployment, Vercel, Next.js performance
role: specialist
scope: implementation
output-format: code
related-skills: typescript-pro
---
# Next.js Developer
Senior Next.js developer with expertise in Next.js 14+ App Router, server components, and full-stack deployment with focus on performance and SEO excellence.
## Core Workflow
1. **Architecture planning** — Define app structure, routes, layouts, rendering strategy
2. **Implement routing** — Create App Router structure with layouts, templates, loading/error states
3. **Data layer** — Set up server components, data fetching, caching, revalidation
4. **Optimize** — Images, fonts, bundles, streaming, edge runtime
5. **Deploy** — Production build, environment setup, monitoring
- Validate: run `next build` locally, confirm zero type errors, check `NEXT_PUBLIC_*` and server-only env vars are set, run Lighthouse/PageSpeed to confirm Core Web Vitals > 90
## Reference Guide
Load detailed guidance based on context:
| Topic | Reference | Load When |
|-------|-----------|-----------|
| App Router | `references/app-router.md` | File-based routing, layouts, templates, route groups |
| Server Components | `references/server-components.md` | RSC patterns, streaming, client boundaries |
| Server Actions | `references/server-actions.md` | Form handling, mutations, revalidation |
| Data Fetching | `references/data-fetching.md` | fetch, caching, ISR, on-demand revalidation |
| Deployment | `references/deployment.md` | Vercel, self-hosting, Docker, optimization |
## Constraints
### MUST DO (Next.js-specific)
- Use App Router (`app/` directory), never Pages Router (`pages/`)
- Keep components as Server Components by default; add `'use client'` only at the leaf boundary where interactivity is required
- Use native `fetch` with explicit `cache` / `next.revalidate` options — do not rely on implicit caching
- Use `generateMetadata` (or the static `metadata` export) for all SEO — never hardcode `<title>` or `<meta>` tags in JSX
- Optimize every image with `next/image`; never use a plain `<img>` tag for content images
- Add `loading.tsx` and `error.tsx` at every route segment that performs async data fetching
### MUST NOT DO
- Convert components to Client Components just to access data — fetch server-side first
- Skip `loading.tsx`/`error.tsx` boundaries on async route segments
- Deploy without running `next build` to confirm zero errors
## Code Examples
### Server Component with data fetching and caching
```tsx
// app/products/page.tsx
import { Suspense } from 'react'
async function ProductList() {
// Revalidate every 60 seconds (ISR)
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 60 },
})
if (!res.ok) throw new Error('Failed to fetch products')
const products: Product[] = await res.json()
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
)
}
export default function Page() {
return (
<Suspense fallback={<p>Loading</p>}>
<ProductList />
</Suspense>
)
}
```
### Server Action with form handling and revalidation
```tsx
// app/products/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createProduct(formData: FormData) {
const name = formData.get('name') as string
await db.product.create({ data: { name } })
revalidatePath('/products')
}
// app/products/new/page.tsx
import { createProduct } from '../actions'
export default function NewProductPage() {
return (
<form action={createProduct}>
<input name="name" placeholder="Product name" required />
<button type="submit">Create</button>
</form>
)
}
```
### generateMetadata for dynamic SEO
```tsx
// app/products/[id]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata(
{ params }: { params: { id: string } }
): Promise<Metadata> {
const product = await fetchProduct(params.id)
return {
title: product.name,
description: product.description,
openGraph: { title: product.name, images: [product.imageUrl] },
}
}
```
## Output Templates
When implementing Next.js features, provide:
1. App structure (route organization)
2. Layout/page components with proper data fetching
3. Server actions if mutations needed
4. Configuration (`next.config.js`, TypeScript)
5. Brief explanation of rendering strategy chosen
## Knowledge Reference
Next.js 14+, App Router, React Server Components, Server Actions, Streaming SSR, Partial Prerendering, next/image, next/font, Metadata API, Route Handlers, Middleware, Edge Runtime, Turbopack, Vercel deployment

View File

@@ -0,0 +1,311 @@
# App Router Architecture
## File-Based Routing
```
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page (/)
├── loading.tsx # Loading UI
├── error.tsx # Error boundary
├── not-found.tsx # 404 page
├── template.tsx # Re-mounted layout
├── (marketing)/ # Route group (no URL segment)
│ ├── layout.tsx
│ ├── about/
│ │ └── page.tsx # /about
│ └── contact/
│ └── page.tsx # /contact
├── dashboard/
│ ├── layout.tsx # Shared dashboard layout
│ ├── page.tsx # /dashboard
│ ├── settings/
│ │ └── page.tsx # /dashboard/settings
│ └── @analytics/ # Parallel route (slot)
│ └── page.tsx
├── blog/
│ ├── [slug]/
│ │ └── page.tsx # /blog/my-post (dynamic)
│ └── [...slug]/
│ └── page.tsx # /blog/a/b/c (catch-all)
└── api/
└── users/
└── route.ts # API route handler
```
## Root Layout (Required)
```tsx
// app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: {
default: 'My App',
template: '%s | My App'
},
description: 'Next.js 14 application',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
{children}
</body>
</html>
)
}
```
## Nested Layouts
```tsx
// app/dashboard/layout.tsx
import { Sidebar } from '@/components/sidebar'
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await auth()
if (!session) {
redirect('/login')
}
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
)
}
```
## Templates (Re-mount on Navigation)
```tsx
// app/template.tsx
'use client'
import { useEffect } from 'react'
export default function Template({ children }: { children: React.ReactNode }) {
useEffect(() => {
// Runs on every navigation
console.log('Template mounted')
}, [])
return <div>{children}</div>
}
```
## Loading States
```tsx
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2" />
</div>
)
}
```
## Error Boundaries
```tsx
// app/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
```
## Route Groups
```tsx
// (marketing) and (shop) share the same URL level
app/
├── (marketing)/
├── layout.tsx # Marketing layout
└── about/
└── page.tsx # /about
└── (shop)/
├── layout.tsx # Shop layout
└── products/
└── page.tsx # /products
```
## Parallel Routes
```tsx
// app/dashboard/layout.tsx
export default function Layout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<>
{children}
{analytics}
{team}
</>
)
}
// app/dashboard/@analytics/page.tsx
export default function Analytics() {
return <div>Analytics Dashboard</div>
}
```
## Intercepting Routes
```tsx
// Show modal when navigating from same app
// but show full page on direct navigation
// app/photos/[id]/page.tsx (full page)
export default function PhotoPage({ params }: { params: { id: string } }) {
return <div>Photo {params.id} - Full Page</div>
}
// app/@modal/(.)photos/[id]/page.tsx (modal)
export default function PhotoModal({ params }: { params: { id: string } }) {
return <div>Photo {params.id} - Modal</div>
}
```
## Dynamic Routes
```tsx
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Post: {params.slug}</h1>
}
// Generate static params at build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}))
}
// Opt out of static generation
export const dynamic = 'force-dynamic'
// Revalidate every 60 seconds
export const revalidate = 60
```
## Catch-All Routes
```tsx
// app/docs/[...slug]/page.tsx
// Matches: /docs/a, /docs/a/b, /docs/a/b/c
export default function Docs({ params }: { params: { slug: string[] } }) {
return <div>Docs: {params.slug.join('/')}</div>
}
// Optional catch-all: [[...slug]]
// Also matches: /docs
```
## Route Handlers (API Routes)
```tsx
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const users = await db.user.findMany()
return NextResponse.json(users)
}
export async function POST(request: NextRequest) {
const body = await request.json()
const user = await db.user.create({ data: body })
return NextResponse.json(user, { status: 201 })
}
// Dynamic routes: app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await db.user.findUnique({ where: { id: params.id } })
return NextResponse.json(user)
}
```
## Metadata API
```tsx
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const post = await fetchPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage }],
},
}
}
```
## Quick Reference
| File | Purpose | Use Case |
|------|---------|----------|
| `layout.tsx` | Persistent UI across routes | Shared navigation, auth wrapper |
| `page.tsx` | Route UI | Actual page content |
| `loading.tsx` | Loading fallback | Automatic Suspense boundary |
| `error.tsx` | Error boundary | Handle errors gracefully |
| `template.tsx` | Re-mounted layout | Analytics, animations |
| `not-found.tsx` | 404 page | Custom not found UI |
| `route.ts` | API handler | Backend API endpoints |

View File

@@ -0,0 +1,482 @@
# Data Fetching & Caching
## Extended fetch API
Next.js extends the native fetch with caching and revalidation options:
```tsx
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/posts', {
cache: 'force-cache', // Default: cache forever (SSG)
})
if (!res.ok) {
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{/* render data */}</div>
}
```
## Cache Options
```tsx
// 1. Force cache (Static Site Generation)
fetch('https://api.example.com/data', {
cache: 'force-cache' // Default behavior
})
// 2. No cache (Server-Side Rendering)
fetch('https://api.example.com/data', {
cache: 'no-store' // Always fetch fresh data
})
// 3. Revalidate (Incremental Static Regeneration)
fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // Revalidate every hour
})
// 4. Revalidate with tags
fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
})
```
## Revalidation Methods
### Time-based Revalidation (ISR)
```tsx
// Revalidate every 60 seconds
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }
})
return res.json()
}
// Route segment config
export const revalidate = 60 // seconds
export default async function Page() {
const posts = await getPosts()
return <div>{/* render */}</div>
}
```
### On-Demand Revalidation
```tsx
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const path = request.nextUrl.searchParams.get('path')
if (path) {
revalidatePath(path)
return Response.json({ revalidated: true, now: Date.now() })
}
return Response.json({ revalidated: false })
}
// Usage in Server Action
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(data: FormData) {
await db.post.create({ data })
// Revalidate specific path
revalidatePath('/posts')
// Revalidate entire layout
revalidatePath('/posts', 'layout')
}
```
### Tag-based Revalidation
```tsx
// Fetch with tags
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
return res.json()
}
async function getAuthors() {
const res = await fetch('https://api.example.com/authors', {
next: { tags: ['authors'] }
})
return res.json()
}
// Revalidate by tag
import { revalidateTag } from 'next/cache'
export async function createPost() {
// Revalidate all fetches tagged with 'posts'
revalidateTag('posts')
}
```
## Route Segment Config
```tsx
// app/posts/page.tsx
// Force dynamic rendering
export const dynamic = 'force-dynamic' // 'auto' | 'force-dynamic' | 'error' | 'force-static'
// Revalidation interval
export const revalidate = 3600 // false | 0 | number (seconds)
// Fetch cache
export const fetchCache = 'auto' // 'auto' | 'default-cache' | 'only-cache' | 'force-cache' | 'force-no-store' | 'default-no-store' | 'only-no-store'
// Runtime
export const runtime = 'nodejs' // 'nodejs' | 'edge'
// Preferred region
export const preferredRegion = 'auto' // 'auto' | 'home' | 'edge' | string | string[]
export default async function Page() {
return <div>Posts</div>
}
```
## Parallel Data Fetching
```tsx
async function getUser() {
return fetch('https://api.example.com/user')
}
async function getPosts() {
return fetch('https://api.example.com/posts')
}
async function getComments() {
return fetch('https://api.example.com/comments')
}
export default async function Page() {
// Fetch in parallel with Promise.all
const [user, posts, comments] = await Promise.all([
getUser(),
getPosts(),
getComments(),
])
return (
<div>
<UserInfo user={user} />
<Posts posts={posts} />
<Comments comments={comments} />
</div>
)
}
```
## Sequential Data Fetching
```tsx
// When one fetch depends on another
export default async function Page({ params }: { params: { id: string } }) {
// First fetch
const user = await fetch(`https://api.example.com/users/${params.id}`)
.then(res => res.json())
// Second fetch depends on first
const posts = await fetch(`https://api.example.com/users/${user.id}/posts`)
.then(res => res.json())
return (
<div>
<h1>{user.name}</h1>
<Posts posts={posts} />
</div>
)
}
```
## Streaming with Suspense
```tsx
// app/page.tsx
import { Suspense } from 'react'
async function Posts() {
const posts = await fetch('https://api.example.com/posts', {
cache: 'no-store'
}).then(res => res.json())
return (
<ul>
{posts.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
export default function Page() {
return (
<div>
<h1>Posts</h1>
<Suspense fallback={<div>Loading posts...</div>}>
<Posts />
</Suspense>
</div>
)
}
```
## React cache for Deduplication
```tsx
// lib/data.ts
import { cache } from 'react'
export const getUser = cache(async (id: string) => {
const res = await fetch(`https://api.example.com/users/${id}`)
return res.json()
})
// components/user-profile.tsx
export async function UserProfile({ userId }: { userId: string }) {
const user = await getUser(userId) // Cached
return <div>{user.name}</div>
}
// components/user-posts.tsx
export async function UserPosts({ userId }: { userId: string }) {
const user = await getUser(userId) // Uses cached result
return <div>{user.posts.length} posts</div>
}
// app/page.tsx
export default function Page() {
return (
<>
<UserProfile userId="123" />
<UserPosts userId="123" /> {/* Same fetch, deduplicated */}
</>
)
}
```
## Database Queries
```tsx
// lib/db.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as { prisma: PrismaClient }
export const db = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
// app/posts/page.tsx
import { db } from '@/lib/db'
export const revalidate = 60 // Revalidate every 60 seconds
export default async function PostsPage() {
const posts = await db.post.findMany({
include: { author: true },
orderBy: { createdAt: 'desc' },
})
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
</article>
))}
</div>
)
}
```
## Error Handling
```tsx
async function getData() {
const res = await fetch('https://api.example.com/data')
if (!res.ok) {
// This will activate the closest error.tsx
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data.title}</div>
}
// app/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
```
## Loading States
```tsx
// app/posts/loading.tsx
export default function Loading() {
return <div>Loading posts...</div>
}
// app/posts/page.tsx
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json())
return <div>{/* render posts */}</div>
}
```
## Client-Side Data Fetching
```tsx
// When you need client-side fetching
'use client'
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then(res => res.json())
export function Posts() {
const { data, error, isLoading } = useSWR('/api/posts', fetcher, {
refreshInterval: 3000, // Refresh every 3 seconds
})
if (error) return <div>Failed to load</div>
if (isLoading) return <div>Loading...</div>
return (
<ul>
{data.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
```
## Preloading Data
```tsx
// lib/data.ts
import { cache } from 'react'
export const preload = (id: string) => {
void getUser(id) // Trigger fetch without awaiting
}
export const getUser = cache(async (id: string) => {
return fetch(`https://api.example.com/users/${id}`)
.then(res => res.json())
})
// components/user.tsx
import { getUser, preload } from '@/lib/data'
export async function User({ id }: { id: string }) {
const user = await getUser(id)
return <div>{user.name}</div>
}
// app/page.tsx
import { User } from '@/components/user'
import { preload } from '@/lib/data'
export default async function Page() {
preload('123') // Start loading immediately
return <User id="123" />
}
```
## Static Generation with Dynamic Routes
```tsx
// app/posts/[slug]/page.tsx
type Post = {
slug: string
title: string
content: string
}
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json())
return posts.map((post: Post) => ({
slug: post.slug,
}))
}
export default async function Post({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(res => res.json())
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
```
## Quick Reference
| Strategy | Config | Use Case |
|----------|--------|----------|
| **SSG** | `cache: 'force-cache'` | Static content |
| **SSR** | `cache: 'no-store'` | Always fresh data |
| **ISR** | `next: { revalidate: 60 }` | Periodic updates |
| **Tag-based** | `next: { tags: ['posts'] }` | On-demand revalidation |
| **Dynamic** | `export const dynamic = 'force-dynamic'` | Per-request data |
## Best Practices
1. **Default to caching** - Use force-cache for static content
2. **Use ISR** - Revalidate periodically for semi-dynamic content
3. **Parallel fetching** - Use Promise.all for independent requests
4. **Deduplicate** - Use React cache() for repeated calls
5. **Stream with Suspense** - Show content progressively
6. **Tag your fetches** - Enable granular revalidation
7. **Handle errors** - Use error.tsx for graceful degradation

View File

@@ -0,0 +1,545 @@
# Deployment & Production
## Vercel Deployment (Recommended)
### Quick Deploy
```bash
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Production deployment
vercel --prod
```
### vercel.json Configuration
```json
{
"buildCommand": "next build",
"devCommand": "next dev",
"installCommand": "npm install",
"framework": "nextjs",
"regions": ["iad1"],
"env": {
"DATABASE_URL": "@database-url",
"NEXT_PUBLIC_API_URL": "https://api.example.com"
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" },
{ "key": "Access-Control-Allow-Methods", "value": "GET,POST,PUT,DELETE" }
]
}
],
"redirects": [
{
"source": "/old-blog/:slug",
"destination": "/blog/:slug",
"permanent": true
}
],
"rewrites": [
{
"source": "/api/:path*",
"destination": "https://api.example.com/:path*"
}
]
}
```
### Environment Variables
```bash
# .env.local (not committed)
DATABASE_URL="postgresql://user:pass@localhost:5432/db"
NEXTAUTH_SECRET="your-secret"
# .env.production (committed, public vars only)
NEXT_PUBLIC_API_URL="https://api.example.com"
```
```tsx
// Access in Server Components
const dbUrl = process.env.DATABASE_URL
// Access in Client Components (must be prefixed with NEXT_PUBLIC_)
const apiUrl = process.env.NEXT_PUBLIC_API_URL
```
## Self-Hosting
### Standalone Output
```js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
}
module.exports = nextConfig
```
```bash
# Build
npm run build
# The standalone folder contains everything needed
# Copy these to your server:
# - .next/standalone/
# - .next/static/
# - public/
# Run on server
node .next/standalone/server.js
```
### Node.js Server
```bash
# Build
npm run build
# Start production server
npm start
# With PM2 for process management
pm2 start npm --name "nextjs" -- start
pm2 startup
pm2 save
```
## Docker Deployment
### Dockerfile (Multi-stage)
```dockerfile
# Stage 1: Dependencies
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# Stage 3: Runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
```
### docker-compose.yml
```yaml
version: '3.8'
services:
nextjs:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
- NEXTAUTH_URL=http://localhost:3000
- NEXTAUTH_SECRET=your-secret
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres_data:
```
```bash
# Build and run
docker-compose up -d
# View logs
docker-compose logs -f nextjs
# Rebuild
docker-compose up -d --build
```
## Production Optimization
### next.config.js
```js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Standalone for self-hosting
output: 'standalone',
// Image optimization
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
// Compression
compress: true,
// Security headers
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
},
],
},
]
},
// Experimental features
experimental: {
optimizePackageImports: ['@mui/material', 'lodash'],
},
// Bundle analyzer
webpack: (config, { isServer }) => {
if (process.env.ANALYZE === 'true') {
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: isServer
? '../analyze/server.html'
: './analyze/client.html',
})
)
}
return config
},
}
module.exports = nextConfig
```
### Bundle Analysis
```bash
# Install analyzer
npm install -D @next/bundle-analyzer
# Analyze
ANALYZE=true npm run build
# Or use built-in
npm run build -- --experimental-build-mode=compile
```
### Performance Monitoring
```tsx
// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next'
import { Analytics } from '@vercel/analytics/react'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
{children}
<SpeedInsights />
<Analytics />
</body>
</html>
)
}
```
## CDN & Edge
### Static Asset CDN
```js
// next.config.js
const nextConfig = {
assetPrefix: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com'
: '',
}
```
### Edge Runtime
```tsx
// app/api/edge/route.ts
export const runtime = 'edge'
export async function GET(request: Request) {
return new Response('Hello from Edge!', {
status: 200,
headers: {
'content-type': 'text/plain',
},
})
}
// app/page.tsx
export const runtime = 'edge'
export default async function Page() {
return <div>Edge-rendered page</div>
}
```
## Caching Strategy
### ISR (Incremental Static Regeneration)
```tsx
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // Revalidate every hour
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetchPost(params.slug)
return <article>{post.content}</article>
}
```
### On-Demand Revalidation
```tsx
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ message: 'Invalid secret' }, { status: 401 })
}
const path = request.nextUrl.searchParams.get('path') || '/'
revalidatePath(path)
return Response.json({ revalidated: true, now: Date.now() })
}
```
## Database Connection Pooling
```ts
// lib/db.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as {
prisma: PrismaClient | undefined
}
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
```
## Health Check Endpoint
```tsx
// app/api/health/route.ts
import { db } from '@/lib/db'
export async function GET() {
try {
// Check database connection
await db.$queryRaw`SELECT 1`
return Response.json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
})
} catch (error) {
return Response.json(
{
status: 'error',
message: 'Database connection failed',
},
{ status: 503 }
)
}
}
```
## CI/CD with GitHub Actions
```yaml
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
```
## Monitoring & Logging
```tsx
// app/error.tsx
'use client'
import * as Sentry from '@sentry/nextjs'
import { useEffect } from 'react'
export default function Error({
error,
}: {
error: Error & { digest?: string }
}) {
useEffect(() => {
Sentry.captureException(error)
}, [error])
return <div>Something went wrong!</div>
}
```
## Quick Reference
| Platform | Best For | Effort |
|----------|----------|--------|
| **Vercel** | Zero-config, optimal performance | Low |
| **Netlify** | Alternative to Vercel | Low |
| **Railway** | Simple hosting with databases | Medium |
| **AWS/GCP** | Enterprise, custom needs | High |
| **Docker** | Self-hosting, full control | High |
## Production Checklist
- [ ] Enable TypeScript strict mode
- [ ] Configure CSP headers
- [ ] Setup error monitoring (Sentry)
- [ ] Configure analytics (Vercel/GA)
- [ ] Optimize images (next/image)
- [ ] Enable compression
- [ ] Setup CDN for static assets
- [ ] Configure database connection pooling
- [ ] Add health check endpoint
- [ ] Setup CI/CD pipeline
- [ ] Configure environment variables
- [ ] Enable ISR/SSG where possible
- [ ] Test Core Web Vitals
- [ ] Setup logging (Datadog/LogRocket)
- [ ] Configure backup strategy

View File

@@ -0,0 +1,462 @@
# Server Actions
## Basic Server Action
```tsx
// app/actions.ts
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.post.create({
data: { title, content }
})
revalidatePath('/posts')
}
```
## Form with Server Action
```tsx
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
)
}
```
## Server Action with Validation
```tsx
// app/actions.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
const CreatePostSchema = z.object({
title: z.string().min(3).max(100),
content: z.string().min(10),
})
export async function createPost(formData: FormData) {
const validatedFields = CreatePostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
})
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
const { title, content } = validatedFields.data
await db.post.create({
data: { title, content }
})
revalidatePath('/posts')
return { success: true }
}
```
## Client Component with Server Action
```tsx
// components/create-post-form.tsx
'use client'
import { createPost } from '@/app/actions'
import { useFormState, useFormStatus } from 'react-dom'
const initialState = {
errors: {},
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
)
}
export function CreatePostForm() {
const [state, formAction] = useFormState(createPost, initialState)
return (
<form action={formAction}>
<div>
<input name="title" />
{state.errors?.title && <p>{state.errors.title[0]}</p>}
</div>
<div>
<textarea name="content" />
{state.errors?.content && <p>{state.errors.content[0]}</p>}
</div>
<SubmitButton />
</form>
)
}
```
## Server Action with Redirect
```tsx
// app/actions.ts
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const post = await db.post.create({
data: {
title: formData.get('title') as string,
content: formData.get('content') as string,
}
})
revalidatePath('/posts')
redirect(`/posts/${post.id}`)
}
```
## Optimistic Updates
```tsx
// components/todo-list.tsx
'use client'
import { experimental_useOptimistic as useOptimistic } from 'react'
import { toggleTodo } from '@/app/actions'
export function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
)
async function handleSubmit(formData: FormData) {
const title = formData.get('title') as string
const newTodo = { id: crypto.randomUUID(), title, completed: false }
// Optimistically update UI
addOptimisticTodo(newTodo)
// Send to server
await createTodo(formData)
}
return (
<div>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<form action={handleSubmit}>
<input name="title" />
<button type="submit">Add</button>
</form>
</div>
)
}
```
## Server Action with Authentication
```tsx
// app/actions.ts
'use server'
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const session = await auth()
if (!session) {
redirect('/login')
}
await db.post.create({
data: {
title: formData.get('title') as string,
content: formData.get('content') as string,
authorId: session.user.id,
}
})
revalidatePath('/posts')
}
```
## Inline Server Action
```tsx
// app/posts/page.tsx
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export default async function Posts() {
const posts = await db.post.findMany()
async function deletePost(formData: FormData) {
'use server'
const id = formData.get('id') as string
await db.post.delete({ where: { id } })
revalidatePath('/posts')
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>
{post.title}
<form action={deletePost}>
<input type="hidden" name="id" value={post.id} />
<button type="submit">Delete</button>
</form>
</li>
))}
</ul>
)
}
```
## Programmatic Server Action Call
```tsx
// components/delete-button.tsx
'use client'
import { deletePost } from '@/app/actions'
export function DeleteButton({ postId }: { postId: string }) {
async function handleDelete() {
if (confirm('Are you sure?')) {
await deletePost(postId)
}
}
return (
<button onClick={handleDelete}>
Delete
</button>
)
}
// app/actions.ts
'use server'
export async function deletePost(postId: string) {
await db.post.delete({ where: { id: postId } })
revalidatePath('/posts')
}
```
## Revalidation Strategies
```tsx
// app/actions.ts
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function updatePost(id: string, data: UpdatePostData) {
await db.post.update({ where: { id }, data })
// Revalidate specific path
revalidatePath('/posts')
revalidatePath(`/posts/${id}`)
// Revalidate all paths in a layout
revalidatePath('/posts', 'layout')
// Revalidate by cache tag
revalidateTag('posts')
}
```
## Server Action with File Upload
```tsx
// app/actions.ts
'use server'
import { writeFile } from 'fs/promises'
import { join } from 'path'
export async function uploadAvatar(formData: FormData) {
const file = formData.get('avatar') as File
if (!file) {
return { error: 'No file uploaded' }
}
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const path = join(process.cwd(), 'public', 'uploads', file.name)
await writeFile(path, buffer)
return { success: true, path: `/uploads/${file.name}` }
}
// components/upload-form.tsx
'use client'
import { uploadAvatar } from '@/app/actions'
export function UploadForm() {
async function handleSubmit(formData: FormData) {
const result = await uploadAvatar(formData)
if (result.success) {
console.log('Uploaded to:', result.path)
}
}
return (
<form action={handleSubmit}>
<input type="file" name="avatar" accept="image/*" />
<button type="submit">Upload</button>
</form>
)
}
```
## Error Handling
```tsx
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
try {
await db.post.create({
data: {
title: formData.get('title') as string,
content: formData.get('content') as string,
}
})
revalidatePath('/posts')
return { success: true }
} catch (error) {
console.error('Failed to create post:', error)
return { error: 'Failed to create post' }
}
}
// components/form.tsx
'use client'
export function CreatePostForm() {
const [error, setError] = useState<string | null>(null)
async function handleSubmit(formData: FormData) {
const result = await createPost(formData)
if (result.error) {
setError(result.error)
} else {
// Success
router.push('/posts')
}
}
return (
<form action={handleSubmit}>
{error && <div className="error">{error}</div>}
{/* form fields */}
</form>
)
}
```
## Server Action with Cookies
```tsx
// app/actions.ts
'use server'
import { cookies } from 'next/headers'
export async function setTheme(theme: 'light' | 'dark') {
cookies().set('theme', theme, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 365, // 1 year
path: '/',
})
}
export async function getTheme() {
return cookies().get('theme')?.value ?? 'light'
}
```
## Rate Limiting
```tsx
// app/actions.ts
'use server'
import { ratelimit } from '@/lib/redis'
export async function createPost(formData: FormData) {
const session = await auth()
const { success } = await ratelimit.limit(session.user.id)
if (!success) {
return { error: 'Rate limit exceeded' }
}
// Create post...
}
```
## Quick Reference
| Capability | Usage |
|------------|-------|
| **Define** | Add 'use server' at top of file or function |
| **Form** | Pass action to `<form action={serverAction}>` |
| **Programmatic** | Call directly: `await serverAction(data)` |
| **Validation** | Use Zod/TypeBox before mutations |
| **Revalidate** | `revalidatePath()` or `revalidateTag()` |
| **Redirect** | `redirect()` after mutation |
| **Errors** | Return error objects, handle in client |
| **Files** | Access via `formData.get()` as File |
## Best Practices
1. **Always validate** - Use Zod/TypeBox for type-safe validation
2. **Revalidate** - Call revalidatePath() after mutations
3. **Handle errors** - Return error objects instead of throwing
4. **Auth checks** - Verify session before mutations
5. **Rate limiting** - Protect against abuse
6. **Type safety** - Define input/output types
7. **Optimistic updates** - Use useOptimistic for better UX

View File

@@ -0,0 +1,384 @@
# React Server Components
## Server Components (Default)
```tsx
// app/page.tsx - Server Component by default
import { db } from '@/lib/db'
export default async function Page() {
// Data fetching in Server Component
const users = await db.user.findMany()
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
```
## Benefits of Server Components
- **Zero bundle size** - Server Components don't add JavaScript to client bundle
- **Direct backend access** - Query databases, read files, use secrets
- **Automatic code splitting** - Only Client Components add to bundle
- **Streaming** - Send UI progressively as data loads
- **No client-side waterfalls** - Fetch all data in parallel on server
## Client Components
```tsx
// components/counter.tsx
'use client' // Required directive
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
)
}
```
## When to Use Client Components
Use `'use client'` when you need:
- **Interactivity** - onClick, onChange, event handlers
- **State** - useState, useReducer
- **Effects** - useEffect, useLayoutEffect
- **Browser APIs** - localStorage, window, document
- **Custom hooks** - Any hook using client-only features
- **Class components** - Component lifecycle methods
## Composition Pattern
```tsx
// app/page.tsx - Server Component
import { ClientWrapper } from './client-wrapper'
import { db } from '@/lib/db'
export default async function Page() {
const data = await db.query()
return (
<div>
{/* Server Component content */}
<h1>Server Content</h1>
{/* Pass data to Client Component */}
<ClientWrapper initialData={data}>
{/* Server Component as children */}
<ServerSidebar />
</ClientWrapper>
</div>
)
}
// components/client-wrapper.tsx
'use client'
export function ClientWrapper({
children,
initialData,
}: {
children: React.ReactNode
initialData: Data
}) {
const [data, setData] = useState(initialData)
return (
<div>
{/* Client Component UI */}
<button onClick={() => refresh()}>Refresh</button>
{/* Server Component children */}
{children}
</div>
)
}
```
## Streaming with Suspense
```tsx
// app/page.tsx
import { Suspense } from 'react'
import { SlowComponent } from './slow-component'
import { FastComponent } from './fast-component'
export default function Page() {
return (
<div>
{/* Renders immediately */}
<FastComponent />
{/* Shows fallback while loading */}
<Suspense fallback={<div>Loading...</div>}>
<SlowComponent />
</Suspense>
</div>
)
}
// components/slow-component.tsx
async function getData() {
await new Promise(resolve => setTimeout(resolve, 3000))
return { data: 'Loaded!' }
}
export async function SlowComponent() {
const data = await getData()
return <div>{data.data}</div>
}
```
## Parallel Data Fetching
```tsx
// app/dashboard/page.tsx
async function getUser() {
return fetch('https://api.example.com/user')
}
async function getPosts() {
return fetch('https://api.example.com/posts')
}
export default async function Dashboard() {
// Fetch in parallel
const [user, posts] = await Promise.all([
getUser(),
getPosts(),
])
return (
<div>
<UserProfile user={user} />
<PostsList posts={posts} />
</div>
)
}
```
## Sequential Data Fetching
```tsx
// app/artist/[id]/page.tsx
async function getArtist(id: string) {
return fetch(`https://api.example.com/artists/${id}`)
}
async function getAlbums(artistId: string) {
return fetch(`https://api.example.com/artists/${artistId}/albums`)
}
export default async function ArtistPage({ params }: { params: { id: string } }) {
// Sequential: albums depends on artist
const artist = await getArtist(params.id)
const albums = await getAlbums(artist.id)
return (
<div>
<h1>{artist.name}</h1>
<Albums albums={albums} />
</div>
)
}
```
## Preloading Data
```tsx
// lib/data.ts
import { cache } from 'react'
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } })
})
// components/user-profile.tsx
export async function UserProfile({ userId }: { userId: string }) {
const user = await getUser(userId)
return <div>{user.name}</div>
}
// app/page.tsx
import { getUser } from '@/lib/data'
import { UserProfile } from '@/components/user-profile'
export default async function Page() {
// Preload
getUser('123')
return (
<div>
{/* This will use cached result */}
<UserProfile userId="123" />
</div>
)
}
```
## Server Component Patterns
### Pattern: Layout with Data Fetching
```tsx
// app/dashboard/layout.tsx
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await auth()
const user = await db.user.findUnique({ where: { id: session.userId } })
return (
<div>
<Sidebar user={user} />
<main>{children}</main>
</div>
)
}
```
### Pattern: Conditional Client Components
```tsx
// app/page.tsx
import { ClientComponent } from './client-component'
export default async function Page() {
const data = await fetchData()
// Only render Client Component when needed
if (data.requiresInteractivity) {
return <ClientComponent data={data} />
}
return <div>{data.content}</div>
}
```
### Pattern: Server Component with Client Island
```tsx
// app/blog/[slug]/page.tsx
import { LikeButton } from './like-button'
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
return (
<article>
{/* Server-rendered content */}
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* Client island for interactivity */}
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
)
}
```
## Context in Server/Client Components
```tsx
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>
}
// app/layout.tsx
import { Providers } from './providers'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
```
## Third-Party Components
```tsx
// components/carousel-wrapper.tsx
'use client'
import { Carousel } from 'third-party-carousel'
export function CarouselWrapper({ items }: { items: Item[] }) {
return <Carousel items={items} />
}
// app/page.tsx
import { CarouselWrapper } from '@/components/carousel-wrapper'
export default async function Page() {
const items = await fetchItems()
return <CarouselWrapper items={items} />
}
```
## Edge Runtime
```tsx
// app/api/route.ts
export const runtime = 'edge'
export async function GET() {
return new Response('Hello from Edge!')
}
// app/page.tsx
export const runtime = 'edge'
export default async function Page() {
return <div>Edge-rendered page</div>
}
```
## Quick Reference
| Capability | Server Component | Client Component |
|------------|------------------|------------------|
| Data fetching | ✅ Yes | ⚠️ Use SWR/React Query |
| Backend access | ✅ Yes (DB, files) | ❌ No |
| Event handlers | ❌ No | ✅ Yes |
| State/Effects | ❌ No | ✅ Yes |
| Browser APIs | ❌ No | ✅ Yes |
| Bundle size | 0 KB | Adds to bundle |
| Streaming | ✅ Yes | ❌ No |
## Best Practices
1. **Default to Server Components** - Only use 'use client' when needed
2. **Move Client Components down** - Push them to leaves of component tree
3. **Pass data down** - Fetch in Server Components, pass to Client Components
4. **Use composition** - Nest Server Components inside Client Components via children
5. **Cache expensive operations** - Use React cache() for deduplication

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/react-nextjs-development",
"installedAt": "2026-04-07T00:45:24.783Z"
}

View File

@@ -0,0 +1,228 @@
---
name: react-nextjs-development
description: "React and Next.js 14+ application development with App Router, Server Components, TypeScript, Tailwind CSS, and modern frontend patterns."
category: granular-workflow-bundle
risk: safe
source: personal
date_added: "2026-02-27"
---
# React/Next.js Development Workflow
## Overview
Specialized workflow for building React and Next.js 14+ applications with modern patterns including App Router, Server Components, TypeScript, and Tailwind CSS.
## When to Use This Workflow
Use this workflow when:
- Building new React applications
- Creating Next.js 14+ projects with App Router
- Implementing Server Components
- Setting up TypeScript with React
- Styling with Tailwind CSS
- Building full-stack Next.js applications
## Workflow Phases
### Phase 1: Project Setup
#### Skills to Invoke
- `app-builder` - Application scaffolding
- `senior-fullstack` - Full-stack guidance
- `nextjs-app-router-patterns` - Next.js 14+ patterns
- `typescript-pro` - TypeScript setup
#### Actions
1. Choose project type (React SPA, Next.js app)
2. Select build tool (Vite, Next.js, Create React App)
3. Scaffold project structure
4. Configure TypeScript
5. Set up ESLint and Prettier
#### Copy-Paste Prompts
```
Use @app-builder to scaffold a new Next.js 14 project with App Router
```
```
Use @nextjs-app-router-patterns to set up Server Components
```
### Phase 2: Component Architecture
#### Skills to Invoke
- `frontend-developer` - Component development
- `react-patterns` - React patterns
- `react-state-management` - State management
- `react-ui-patterns` - UI patterns
#### Actions
1. Design component hierarchy
2. Create base components
3. Implement layout components
4. Set up state management
5. Create custom hooks
#### Copy-Paste Prompts
```
Use @frontend-developer to create reusable React components
```
```
Use @react-patterns to implement proper component composition
```
```
Use @react-state-management to set up Zustand store
```
### Phase 3: Styling and Design
#### Skills to Invoke
- `frontend-design` - UI design
- `tailwind-patterns` - Tailwind CSS
- `tailwind-design-system` - Design system
- `core-components` - Component library
#### Actions
1. Set up Tailwind CSS
2. Configure design tokens
3. Create utility classes
4. Build component styles
5. Implement responsive design
#### Copy-Paste Prompts
```
Use @tailwind-patterns to style components with Tailwind CSS v4
```
```
Use @frontend-design to create a modern dashboard UI
```
### Phase 4: Data Fetching
#### Skills to Invoke
- `nextjs-app-router-patterns` - Server Components
- `react-state-management` - React Query
- `api-patterns` - API integration
#### Actions
1. Implement Server Components
2. Set up React Query/SWR
3. Create API client
4. Handle loading states
5. Implement error boundaries
#### Copy-Paste Prompts
```
Use @nextjs-app-router-patterns to implement Server Components data fetching
```
### Phase 5: Routing and Navigation
#### Skills to Invoke
- `nextjs-app-router-patterns` - App Router
- `nextjs-best-practices` - Next.js patterns
#### Actions
1. Set up file-based routing
2. Create dynamic routes
3. Implement nested routes
4. Add route guards
5. Configure redirects
#### Copy-Paste Prompts
```
Use @nextjs-app-router-patterns to set up parallel routes and intercepting routes
```
### Phase 6: Forms and Validation
#### Skills to Invoke
- `frontend-developer` - Form development
- `typescript-advanced-types` - Type validation
- `react-ui-patterns` - Form patterns
#### Actions
1. Choose form library (React Hook Form, Formik)
2. Set up validation (Zod, Yup)
3. Create form components
4. Handle submissions
5. Implement error handling
#### Copy-Paste Prompts
```
Use @frontend-developer to create forms with React Hook Form and Zod
```
### Phase 7: Testing
#### Skills to Invoke
- `javascript-testing-patterns` - Jest/Vitest
- `playwright-skill` - E2E testing
- `e2e-testing-patterns` - E2E patterns
#### Actions
1. Set up testing framework
2. Write unit tests
3. Create component tests
4. Implement E2E tests
5. Configure CI integration
#### Copy-Paste Prompts
```
Use @javascript-testing-patterns to write Vitest tests
```
```
Use @playwright-skill to create E2E tests for critical flows
```
### Phase 8: Build and Deployment
#### Skills to Invoke
- `vercel-deployment` - Vercel deployment
- `vercel-deploy-claimable` - Vercel deployment
- `web-performance-optimization` - Performance
#### Actions
1. Configure build settings
2. Optimize bundle size
3. Set up environment variables
4. Deploy to Vercel
5. Configure preview deployments
#### Copy-Paste Prompts
```
Use @vercel-deployment to deploy Next.js app to production
```
## Technology Stack
| Category | Technology |
|----------|------------|
| Framework | Next.js 14+, React 18+ |
| Language | TypeScript 5+ |
| Styling | Tailwind CSS v4 |
| State | Zustand, React Query |
| Forms | React Hook Form, Zod |
| Testing | Vitest, Playwright |
| Deployment | Vercel |
## Quality Gates
- [ ] TypeScript compiles without errors
- [ ] All tests passing
- [ ] Linting clean
- [ ] Performance metrics met (LCP, CLS, FID)
- [ ] Accessibility checked (WCAG 2.1)
- [ ] Responsive design verified
## Related Workflow Bundles
- `development` - General development
- `testing-qa` - Testing workflow
- `documentation` - Documentation
- `typescript-development` - TypeScript patterns

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/tdd",
"installedAt": "2026-04-07T00:45:24.783Z"
}

107
.claude/skills/tdd/SKILL.md Normal file
View File

@@ -0,0 +1,107 @@
---
name: tdd
description: Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD, mentions "red-green-refactor", wants integration tests, or asks for test-first development.
---
# Test-Driven Development
## Philosophy
**Core principle**: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't.
**Good tests** are integration-style: they exercise real code paths through public APIs. They describe _what_ the system does, not _how_ it does it. A good test reads like a specification - "user can checkout with valid cart" tells you exactly what capability exists. These tests survive refactors because they don't care about internal structure.
**Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines.
## Anti-Pattern: Horizontal Slices
**DO NOT write all tests first, then all implementation.** This is "horizontal slicing" - treating RED as "write all tests" and GREEN as "write all code."
This produces **crap tests**:
- Tests written in bulk test _imagined_ behavior, not _actual_ behavior
- You end up testing the _shape_ of things (data structures, function signatures) rather than user-facing behavior
- Tests become insensitive to real changes - they pass when behavior breaks, fail when behavior is fine
- You outrun your headlights, committing to test structure before understanding the implementation
**Correct approach**: Vertical slices via tracer bullets. One test → one implementation → repeat. Each test responds to what you learned from the previous cycle. Because you just wrote the code, you know exactly what behavior matters and how to verify it.
```
WRONG (horizontal):
RED: test1, test2, test3, test4, test5
GREEN: impl1, impl2, impl3, impl4, impl5
RIGHT (vertical):
RED→GREEN: test1→impl1
RED→GREEN: test2→impl2
RED→GREEN: test3→impl3
...
```
## Workflow
### 1. Planning
Before writing any code:
- [ ] Confirm with user what interface changes are needed
- [ ] Confirm with user which behaviors to test (prioritize)
- [ ] Identify opportunities for [deep modules](deep-modules.md) (small interface, deep implementation)
- [ ] Design interfaces for [testability](interface-design.md)
- [ ] List the behaviors to test (not implementation steps)
- [ ] Get user approval on the plan
Ask: "What should the public interface look like? Which behaviors are most important to test?"
**You can't test everything.** Confirm with the user exactly which behaviors matter most. Focus testing effort on critical paths and complex logic, not every possible edge case.
### 2. Tracer Bullet
Write ONE test that confirms ONE thing about the system:
```
RED: Write test for first behavior → test fails
GREEN: Write minimal code to pass → test passes
```
This is your tracer bullet - proves the path works end-to-end.
### 3. Incremental Loop
For each remaining behavior:
```
RED: Write next test → fails
GREEN: Minimal code to pass → passes
```
Rules:
- One test at a time
- Only enough code to pass current test
- Don't anticipate future tests
- Keep tests focused on observable behavior
### 4. Refactor
After all tests pass, look for [refactor candidates](refactoring.md):
- [ ] Extract duplication
- [ ] Deepen modules (move complexity behind simple interfaces)
- [ ] Apply SOLID principles where natural
- [ ] Consider what new code reveals about existing code
- [ ] Run tests after each refactor step
**Never refactor while RED.** Get to GREEN first.
## Checklist Per Cycle
```
[ ] Test describes behavior, not implementation
[ ] Test uses public interface only
[ ] Test would survive internal refactor
[ ] Code is minimal for this test
[ ] No speculative features added
```

View File

@@ -0,0 +1,33 @@
# Deep Modules
From "A Philosophy of Software Design":
**Deep module** = small interface + lots of implementation
```
┌─────────────────────┐
│ Small Interface │ ← Few methods, simple params
├─────────────────────┤
│ │
│ │
│ Deep Implementation│ ← Complex logic hidden
│ │
│ │
└─────────────────────┘
```
**Shallow module** = large interface + little implementation (avoid)
```
┌─────────────────────────────────┐
│ Large Interface │ ← Many methods, complex params
├─────────────────────────────────┤
│ Thin Implementation │ ← Just passes through
└─────────────────────────────────┘
```
When designing interfaces, ask:
- Can I reduce the number of methods?
- Can I simplify the parameters?
- Can I hide more complexity inside?

View File

@@ -0,0 +1,31 @@
# Interface Design for Testability
Good interfaces make testing natural:
1. **Accept dependencies, don't create them**
```typescript
// Testable
function processOrder(order, paymentGateway) {}
// Hard to test
function processOrder(order) {
const gateway = new StripeGateway();
}
```
2. **Return results, don't produce side effects**
```typescript
// Testable
function calculateDiscount(cart): Discount {}
// Hard to test
function applyDiscount(cart): void {
cart.total -= discount;
}
```
3. **Small surface area**
- Fewer methods = fewer tests needed
- Fewer params = simpler test setup

View File

@@ -0,0 +1,59 @@
# When to Mock
Mock at **system boundaries** only:
- External APIs (payment, email, etc.)
- Databases (sometimes - prefer test DB)
- Time/randomness
- File system (sometimes)
Don't mock:
- Your own classes/modules
- Internal collaborators
- Anything you control
## Designing for Mockability
At system boundaries, design interfaces that are easy to mock:
**1. Use dependency injection**
Pass external dependencies in rather than creating them internally:
```typescript
// Easy to mock
function processPayment(order, paymentClient) {
return paymentClient.charge(order.total);
}
// Hard to mock
function processPayment(order) {
const client = new StripeClient(process.env.STRIPE_KEY);
return client.charge(order.total);
}
```
**2. Prefer SDK-style interfaces over generic fetchers**
Create specific functions for each external operation instead of one generic function with conditional logic:
```typescript
// GOOD: Each function is independently mockable
const api = {
getUser: (id) => fetch(`/users/${id}`),
getOrders: (userId) => fetch(`/users/${userId}/orders`),
createOrder: (data) => fetch('/orders', { method: 'POST', body: data }),
};
// BAD: Mocking requires conditional logic inside the mock
const api = {
fetch: (endpoint, options) => fetch(endpoint, options),
};
```
The SDK approach means:
- Each mock returns one specific shape
- No conditional logic in test setup
- Easier to see which endpoints a test exercises
- Type safety per endpoint

View File

@@ -0,0 +1,10 @@
# Refactor Candidates
After TDD cycle, look for:
- **Duplication** → Extract function/class
- **Long methods** → Break into private helpers (keep tests on public interface)
- **Shallow modules** → Combine or deepen
- **Feature envy** → Move logic to where data lives
- **Primitive obsession** → Introduce value objects
- **Existing code** the new code reveals as problematic

View File

@@ -0,0 +1,61 @@
# Good and Bad Tests
## Good Tests
**Integration-style**: Test through real interfaces, not mocks of internal parts.
```typescript
// GOOD: Tests observable behavior
test("user can checkout with valid cart", async () => {
const cart = createCart();
cart.add(product);
const result = await checkout(cart, paymentMethod);
expect(result.status).toBe("confirmed");
});
```
Characteristics:
- Tests behavior users/callers care about
- Uses public API only
- Survives internal refactors
- Describes WHAT, not HOW
- One logical assertion per test
## Bad Tests
**Implementation-detail tests**: Coupled to internal structure.
```typescript
// BAD: Tests implementation details
test("checkout calls paymentService.process", async () => {
const mockPayment = jest.mock(paymentService);
await checkout(cart, payment);
expect(mockPayment.process).toHaveBeenCalledWith(cart.total);
});
```
Red flags:
- Mocking internal collaborators
- Testing private methods
- Asserting on call counts/order
- Test breaks when refactoring without behavior change
- Test name describes HOW not WHAT
- Verifying through external means instead of interface
```typescript
// BAD: Bypasses interface to verify
test("createUser saves to database", async () => {
await createUser({ name: "Alice" });
const row = await db.query("SELECT * FROM users WHERE name = ?", ["Alice"]);
expect(row).toBeDefined();
});
// GOOD: Verifies through interface
test("createUser makes user retrievable", async () => {
const user = await createUser({ name: "Alice" });
const retrieved = await getUser(user.id);
expect(retrieved.name).toBe("Alice");
});
```

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/typescript-advanced-types",
"installedAt": "2026-04-07T00:45:24.783Z"
}

View File

@@ -0,0 +1,717 @@
---
name: typescript-advanced-types
description: Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects.
---
# TypeScript Advanced Types
Comprehensive guidance for mastering TypeScript's advanced type system including generics, conditional types, mapped types, template literal types, and utility types for building robust, type-safe applications.
## When to Use This Skill
- Building type-safe libraries or frameworks
- Creating reusable generic components
- Implementing complex type inference logic
- Designing type-safe API clients
- Building form validation systems
- Creating strongly-typed configuration objects
- Implementing type-safe state management
- Migrating JavaScript codebases to TypeScript
## Core Concepts
### 1. Generics
**Purpose:** Create reusable, type-flexible components while maintaining type safety.
**Basic Generic Function:**
```typescript
function identity<T>(value: T): T {
return value;
}
const num = identity<number>(42); // Type: number
const str = identity<string>("hello"); // Type: string
const auto = identity(true); // Type inferred: boolean
```
**Generic Constraints:**
```typescript
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(item.length);
return item;
}
logLength("hello"); // OK: string has length
logLength([1, 2, 3]); // OK: array has length
logLength({ length: 10 }); // OK: object has length
// logLength(42); // Error: number has no length
```
**Multiple Type Parameters:**
```typescript
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const merged = merge({ name: "John" }, { age: 30 });
// Type: { name: string } & { age: number }
```
### 2. Conditional Types
**Purpose:** Create types that depend on conditions, enabling sophisticated type logic.
**Basic Conditional Type:**
```typescript
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
```
**Extracting Return Types:**
```typescript
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: "John" };
}
type User = ReturnType<typeof getUser>;
// Type: { id: number; name: string; }
```
**Distributive Conditional Types:**
```typescript
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>;
// Type: string[] | number[]
```
**Nested Conditions:**
```typescript
type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: T extends undefined
? "undefined"
: T extends Function
? "function"
: "object";
type T1 = TypeName<string>; // "string"
type T2 = TypeName<() => void>; // "function"
```
### 3. Mapped Types
**Purpose:** Transform existing types by iterating over their properties.
**Basic Mapped Type:**
```typescript
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface User {
id: number;
name: string;
}
type ReadonlyUser = Readonly<User>;
// Type: { readonly id: number; readonly name: string; }
```
**Optional Properties:**
```typescript
type Partial<T> = {
[P in keyof T]?: T[P];
};
type PartialUser = Partial<User>;
// Type: { id?: number; name?: string; }
```
**Key Remapping:**
```typescript
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// Type: { getName: () => string; getAge: () => number; }
```
**Filtering Properties:**
```typescript
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
age: number;
active: boolean;
}
type OnlyNumbers = PickByType<Mixed, number>;
// Type: { id: number; age: number; }
```
### 4. Template Literal Types
**Purpose:** Create string-based types with pattern matching and transformation.
**Basic Template Literal:**
```typescript
type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// Type: "onClick" | "onFocus" | "onBlur"
```
**String Manipulation:**
```typescript
type UppercaseGreeting = Uppercase<"hello">; // "HELLO"
type LowercaseGreeting = Lowercase<"HELLO">; // "hello"
type CapitalizedName = Capitalize<"john">; // "John"
type UncapitalizedName = Uncapitalize<"John">; // "john"
```
**Path Building:**
```typescript
type Path<T> = T extends object
? {
[K in keyof T]: K extends string ? `${K}` | `${K}.${Path<T[K]>}` : never;
}[keyof T]
: never;
interface Config {
server: {
host: string;
port: number;
};
database: {
url: string;
};
}
type ConfigPath = Path<Config>;
// Type: "server" | "database" | "server.host" | "server.port" | "database.url"
```
### 5. Utility Types
**Built-in Utility Types:**
```typescript
// Partial<T> - Make all properties optional
type PartialUser = Partial<User>;
// Required<T> - Make all properties required
type RequiredUser = Required<PartialUser>;
// Readonly<T> - Make all properties readonly
type ReadonlyUser = Readonly<User>;
// Pick<T, K> - Select specific properties
type UserName = Pick<User, "name" | "email">;
// Omit<T, K> - Remove specific properties
type UserWithoutPassword = Omit<User, "password">;
// Exclude<T, U> - Exclude types from union
type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
// Extract<T, U> - Extract types from union
type T2 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
// NonNullable<T> - Exclude null and undefined
type T3 = NonNullable<string | null | undefined>; // string
// Record<K, T> - Create object type with keys K and values T
type PageInfo = Record<"home" | "about", { title: string }>;
```
## Advanced Patterns
### Pattern 1: Type-Safe Event Emitter
```typescript
type EventMap = {
"user:created": { id: string; name: string };
"user:updated": { id: string };
"user:deleted": { id: string };
};
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: Array<(data: T[K]) => void>;
} = {};
on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
emit<K extends keyof T>(event: K, data: T[K]): void {
const callbacks = this.listeners[event];
if (callbacks) {
callbacks.forEach((callback) => callback(data));
}
}
}
const emitter = new TypedEventEmitter<EventMap>();
emitter.on("user:created", (data) => {
console.log(data.id, data.name); // Type-safe!
});
emitter.emit("user:created", { id: "1", name: "John" });
// emitter.emit("user:created", { id: "1" }); // Error: missing 'name'
```
### Pattern 2: Type-Safe API Client
```typescript
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type EndpointConfig = {
"/users": {
GET: { response: User[] };
POST: { body: { name: string; email: string }; response: User };
};
"/users/:id": {
GET: { params: { id: string }; response: User };
PUT: { params: { id: string }; body: Partial<User>; response: User };
DELETE: { params: { id: string }; response: void };
};
};
type ExtractParams<T> = T extends { params: infer P } ? P : never;
type ExtractBody<T> = T extends { body: infer B } ? B : never;
type ExtractResponse<T> = T extends { response: infer R } ? R : never;
class APIClient<Config extends Record<string, Record<HTTPMethod, any>>> {
async request<Path extends keyof Config, Method extends keyof Config[Path]>(
path: Path,
method: Method,
...[options]: ExtractParams<Config[Path][Method]> extends never
? ExtractBody<Config[Path][Method]> extends never
? []
: [{ body: ExtractBody<Config[Path][Method]> }]
: [
{
params: ExtractParams<Config[Path][Method]>;
body?: ExtractBody<Config[Path][Method]>;
},
]
): Promise<ExtractResponse<Config[Path][Method]>> {
// Implementation here
return {} as any;
}
}
const api = new APIClient<EndpointConfig>();
// Type-safe API calls
const users = await api.request("/users", "GET");
// Type: User[]
const newUser = await api.request("/users", "POST", {
body: { name: "John", email: "john@example.com" },
});
// Type: User
const user = await api.request("/users/:id", "GET", {
params: { id: "123" },
});
// Type: User
```
### Pattern 3: Builder Pattern with Type Safety
```typescript
type BuilderState<T> = {
[K in keyof T]: T[K] | undefined;
};
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
type IsComplete<T, S> =
RequiredKeys<T> extends keyof S
? S[RequiredKeys<T>] extends undefined
? false
: true
: false;
class Builder<T, S extends BuilderState<T> = {}> {
private state: S = {} as S;
set<K extends keyof T>(key: K, value: T[K]): Builder<T, S & Record<K, T[K]>> {
this.state[key] = value;
return this as any;
}
build(this: IsComplete<T, S> extends true ? this : never): T {
return this.state as T;
}
}
interface User {
id: string;
name: string;
email: string;
age?: number;
}
const builder = new Builder<User>();
const user = builder
.set("id", "1")
.set("name", "John")
.set("email", "john@example.com")
.build(); // OK: all required fields set
// const incomplete = builder
// .set("id", "1")
// .build(); // Error: missing required fields
```
### Pattern 4: Deep Readonly/Partial
```typescript
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P];
};
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? T[P] extends Array<infer U>
? Array<DeepPartial<U>>
: DeepPartial<T[P]>
: T[P];
};
interface Config {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
database: {
url: string;
pool: {
min: number;
max: number;
};
};
}
type ReadonlyConfig = DeepReadonly<Config>;
// All nested properties are readonly
type PartialConfig = DeepPartial<Config>;
// All nested properties are optional
```
### Pattern 5: Type-Safe Form Validation
```typescript
type ValidationRule<T> = {
validate: (value: T) => boolean;
message: string;
};
type FieldValidation<T> = {
[K in keyof T]?: ValidationRule<T[K]>[];
};
type ValidationErrors<T> = {
[K in keyof T]?: string[];
};
class FormValidator<T extends Record<string, any>> {
constructor(private rules: FieldValidation<T>) {}
validate(data: T): ValidationErrors<T> | null {
const errors: ValidationErrors<T> = {};
let hasErrors = false;
for (const key in this.rules) {
const fieldRules = this.rules[key];
const value = data[key];
if (fieldRules) {
const fieldErrors: string[] = [];
for (const rule of fieldRules) {
if (!rule.validate(value)) {
fieldErrors.push(rule.message);
}
}
if (fieldErrors.length > 0) {
errors[key] = fieldErrors;
hasErrors = true;
}
}
}
return hasErrors ? errors : null;
}
}
interface LoginForm {
email: string;
password: string;
}
const validator = new FormValidator<LoginForm>({
email: [
{
validate: (v) => v.includes("@"),
message: "Email must contain @",
},
{
validate: (v) => v.length > 0,
message: "Email is required",
},
],
password: [
{
validate: (v) => v.length >= 8,
message: "Password must be at least 8 characters",
},
],
});
const errors = validator.validate({
email: "invalid",
password: "short",
});
// Type: { email?: string[]; password?: string[]; } | null
```
### Pattern 6: Discriminated Unions
```typescript
type Success<T> = {
status: "success";
data: T;
};
type Error = {
status: "error";
error: string;
};
type Loading = {
status: "loading";
};
type AsyncState<T> = Success<T> | Error | Loading;
function handleState<T>(state: AsyncState<T>): void {
switch (state.status) {
case "success":
console.log(state.data); // Type: T
break;
case "error":
console.log(state.error); // Type: string
break;
case "loading":
console.log("Loading...");
break;
}
}
// Type-safe state machine
type State =
| { type: "idle" }
| { type: "fetching"; requestId: string }
| { type: "success"; data: any }
| { type: "error"; error: Error };
type Event =
| { type: "FETCH"; requestId: string }
| { type: "SUCCESS"; data: any }
| { type: "ERROR"; error: Error }
| { type: "RESET" };
function reducer(state: State, event: Event): State {
switch (state.type) {
case "idle":
return event.type === "FETCH"
? { type: "fetching", requestId: event.requestId }
: state;
case "fetching":
if (event.type === "SUCCESS") {
return { type: "success", data: event.data };
}
if (event.type === "ERROR") {
return { type: "error", error: event.error };
}
return state;
case "success":
case "error":
return event.type === "RESET" ? { type: "idle" } : state;
}
}
```
## Type Inference Techniques
### 1. Infer Keyword
```typescript
// Extract array element type
type ElementType<T> = T extends (infer U)[] ? U : never;
type NumArray = number[];
type Num = ElementType<NumArray>; // number
// Extract promise type
type PromiseType<T> = T extends Promise<infer U> ? U : never;
type AsyncNum = PromiseType<Promise<number>>; // number
// Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function foo(a: string, b: number) {}
type FooParams = Parameters<typeof foo>; // [string, number]
```
### 2. Type Guards
```typescript
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isArrayOf<T>(
value: unknown,
guard: (item: unknown) => item is T,
): value is T[] {
return Array.isArray(value) && value.every(guard);
}
const data: unknown = ["a", "b", "c"];
if (isArrayOf(data, isString)) {
data.forEach((s) => s.toUpperCase()); // Type: string[]
}
```
### 3. Assertion Functions
```typescript
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Not a string");
}
}
function processValue(value: unknown) {
assertIsString(value);
// value is now typed as string
console.log(value.toUpperCase());
}
```
## Best Practices
1. **Use `unknown` over `any`**: Enforce type checking
2. **Prefer `interface` for object shapes**: Better error messages
3. **Use `type` for unions and complex types**: More flexible
4. **Leverage type inference**: Let TypeScript infer when possible
5. **Create helper types**: Build reusable type utilities
6. **Use const assertions**: Preserve literal types
7. **Avoid type assertions**: Use type guards instead
8. **Document complex types**: Add JSDoc comments
9. **Use strict mode**: Enable all strict compiler options
10. **Test your types**: Use type tests to verify type behavior
## Type Testing
```typescript
// Type assertion tests
type AssertEqual<T, U> = [T] extends [U]
? [U] extends [T]
? true
: false
: false;
type Test1 = AssertEqual<string, string>; // true
type Test2 = AssertEqual<string, number>; // false
type Test3 = AssertEqual<string | number, string>; // false
// Expect error helper
type ExpectError<T extends never> = T;
// Example usage
type ShouldError = ExpectError<AssertEqual<string, number>>;
```
## Common Pitfalls
1. **Over-using `any`**: Defeats the purpose of TypeScript
2. **Ignoring strict null checks**: Can lead to runtime errors
3. **Too complex types**: Can slow down compilation
4. **Not using discriminated unions**: Misses type narrowing opportunities
5. **Forgetting readonly modifiers**: Allows unintended mutations
6. **Circular type references**: Can cause compiler errors
7. **Not handling edge cases**: Like empty arrays or null values
## Performance Considerations
- Avoid deeply nested conditional types
- Use simple types when possible
- Cache complex type computations
- Limit recursion depth in recursive types
- Use build tools to skip type checking in production

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/typescript-expert",
"installedAt": "2026-04-07T00:45:24.784Z"
}

View File

@@ -0,0 +1,426 @@
---
name: typescript-expert
description: TypeScript and JavaScript expert with deep knowledge of type-level programming, performance optimization, monorepo management, migration strategies, and modern tooling.
category: framework
risk: critical
source: community
date_added: '2026-02-27'
---
# TypeScript Expert
You are an advanced TypeScript expert with deep, practical knowledge of type-level programming, performance optimization, and real-world problem solving based on current best practices.
## When invoked:
0. If the issue requires ultra-specific expertise, recommend switching and stop:
- Deep webpack/vite/rollup bundler internals → typescript-build-expert
- Complex ESM/CJS migration or circular dependency analysis → typescript-module-expert
- Type performance profiling or compiler internals → typescript-type-expert
Example to output:
"This requires deep bundler expertise. Please invoke: 'Use the typescript-build-expert subagent.' Stopping here."
1. Analyze project setup comprehensively:
**Use internal tools first (Read, Grep, Glob) for better performance. Shell commands are fallbacks.**
```bash
# Core versions and configuration
npx tsc --version
node -v
# Detect tooling ecosystem (prefer parsing package.json)
node -e "const p=require('./package.json');console.log(Object.keys({...p.devDependencies,...p.dependencies}||{}).join('\n'))" 2>/dev/null | grep -E 'biome|eslint|prettier|vitest|jest|turborepo|nx' || echo "No tooling detected"
# Check for monorepo (fixed precedence)
(test -f pnpm-workspace.yaml || test -f lerna.json || test -f nx.json || test -f turbo.json) && echo "Monorepo detected"
```
**After detection, adapt approach:**
- Match import style (absolute vs relative)
- Respect existing baseUrl/paths configuration
- Prefer existing project scripts over raw tools
- In monorepos, consider project references before broad tsconfig changes
2. Identify the specific problem category and complexity level
3. Apply the appropriate solution strategy from my expertise
4. Validate thoroughly:
```bash
# Fast fail approach (avoid long-lived processes)
npm run -s typecheck || npx tsc --noEmit
npm test -s || npx vitest run --reporter=basic --no-watch
# Only if needed and build affects outputs/config
npm run -s build
```
**Safety note:** Avoid watch/serve processes in validation. Use one-shot diagnostics only.
## Advanced Type System Expertise
### Type-Level Programming Patterns
**Branded Types for Domain Modeling**
```typescript
// Create nominal types to prevent primitive obsession
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
// Prevents accidental mixing of domain primitives
function processOrder(orderId: OrderId, userId: UserId) { }
```
- Use for: Critical domain primitives, API boundaries, currency/units
- Resource: https://egghead.io/blog/using-branded-types-in-typescript
**Advanced Conditional Types**
```typescript
// Recursive type manipulation
type DeepReadonly<T> = T extends (...args: any[]) => any
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
// Template literal type magic
type PropEventSource<Type> = {
on<Key extends string & keyof Type>
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void): void;
};
```
- Use for: Library APIs, type-safe event systems, compile-time validation
- Watch for: Type instantiation depth errors (limit recursion to 10 levels)
**Type Inference Techniques**
```typescript
// Use 'satisfies' for constraint validation (TS 5.0+)
const config = {
api: "https://api.example.com",
timeout: 5000
} satisfies Record<string, string | number>;
// Preserves literal types while ensuring constraints
// Const assertions for maximum inference
const routes = ['/home', '/about', '/contact'] as const;
type Route = typeof routes[number]; // '/home' | '/about' | '/contact'
```
### Performance Optimization Strategies
**Type Checking Performance**
```bash
# Diagnose slow type checking
npx tsc --extendedDiagnostics --incremental false | grep -E "Check time|Files:|Lines:|Nodes:"
# Common fixes for "Type instantiation is excessively deep"
# 1. Replace type intersections with interfaces
# 2. Split large union types (>100 members)
# 3. Avoid circular generic constraints
# 4. Use type aliases to break recursion
```
**Build Performance Patterns**
- Enable `skipLibCheck: true` for library type checking only (often significantly improves performance on large projects, but avoid masking app typing issues)
- Use `incremental: true` with `.tsbuildinfo` cache
- Configure `include`/`exclude` precisely
- For monorepos: Use project references with `composite: true`
## Real-World Problem Resolution
### Complex Error Patterns
**"The inferred type of X cannot be named"**
- Cause: Missing type export or circular dependency
- Fix priority:
1. Export the required type explicitly
2. Use `ReturnType<typeof function>` helper
3. Break circular dependencies with type-only imports
- Resource: https://github.com/microsoft/TypeScript/issues/47663
**Missing type declarations**
- Quick fix with ambient declarations:
```typescript
// types/ambient.d.ts
declare module 'some-untyped-package' {
const value: unknown;
export default value;
export = value; // if CJS interop is needed
}
```
- For more details: [Declaration Files Guide](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html)
**"Excessive stack depth comparing types"**
- Cause: Circular or deeply recursive types
- Fix priority:
1. Limit recursion depth with conditional types
2. Use `interface` extends instead of type intersection
3. Simplify generic constraints
```typescript
// Bad: Infinite recursion
type InfiniteArray<T> = T | InfiniteArray<T>[];
// Good: Limited recursion
type NestedArray<T, D extends number = 5> =
D extends 0 ? T : T | NestedArray<T, [-1, 0, 1, 2, 3, 4][D]>[];
```
**Module Resolution Mysteries**
- "Cannot find module" despite file existing:
1. Check `moduleResolution` matches your bundler
2. Verify `baseUrl` and `paths` alignment
3. For monorepos: Ensure workspace protocol (workspace:*)
4. Try clearing cache: `rm -rf node_modules/.cache .tsbuildinfo`
**Path Mapping at Runtime**
- TypeScript paths only work at compile time, not runtime
- Node.js runtime solutions:
- ts-node: Use `ts-node -r tsconfig-paths/register`
- Node ESM: Use loader alternatives or avoid TS paths at runtime
- Production: Pre-compile with resolved paths
### Migration Expertise
**JavaScript to TypeScript Migration**
```bash
# Incremental migration strategy
# 1. Enable allowJs and checkJs (merge into existing tsconfig.json):
# Add to existing tsconfig.json:
# {
# "compilerOptions": {
# "allowJs": true,
# "checkJs": true
# }
# }
# 2. Rename files gradually (.js → .ts)
# 3. Add types file by file using AI assistance
# 4. Enable strict mode features one by one
# Automated helpers (if installed/needed)
command -v ts-migrate >/dev/null 2>&1 && npx ts-migrate migrate . --sources 'src/**/*.js'
command -v typesync >/dev/null 2>&1 && npx typesync # Install missing @types packages
```
**Tool Migration Decisions**
| From | To | When | Migration Effort |
|------|-----|------|-----------------|
| ESLint + Prettier | Biome | Need much faster speed, okay with fewer rules | Low (1 day) |
| TSC for linting | Type-check only | Have 100+ files, need faster feedback | Medium (2-3 days) |
| Lerna | Nx/Turborepo | Need caching, parallel builds | High (1 week) |
| CJS | ESM | Node 18+, modern tooling | High (varies) |
### Monorepo Management
**Nx vs Turborepo Decision Matrix**
- Choose **Turborepo** if: Simple structure, need speed, <20 packages
- Choose **Nx** if: Complex dependencies, need visualization, plugins required
- Performance: Nx often performs better on large monorepos (>50 packages)
**TypeScript Monorepo Configuration**
```json
// Root tsconfig.json
{
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./apps/web" }
],
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true
}
}
```
## Modern Tooling Expertise
### Biome vs ESLint
**Use Biome when:**
- Speed is critical (often faster than traditional setups)
- Want single tool for lint + format
- TypeScript-first project
- Okay with 64 TS rules vs 100+ in typescript-eslint
**Stay with ESLint when:**
- Need specific rules/plugins
- Have complex custom rules
- Working with Vue/Angular (limited Biome support)
- Need type-aware linting (Biome doesn't have this yet)
### Type Testing Strategies
**Vitest Type Testing (Recommended)**
```typescript
// in avatar.test-d.ts
import { expectTypeOf } from 'vitest'
import type { Avatar } from './avatar'
test('Avatar props are correctly typed', () => {
expectTypeOf<Avatar>().toHaveProperty('size')
expectTypeOf<Avatar['size']>().toEqualTypeOf<'sm' | 'md' | 'lg'>()
})
```
**When to Test Types:**
- Publishing libraries
- Complex generic functions
- Type-level utilities
- API contracts
## Debugging Mastery
### CLI Debugging Tools
```bash
# Debug TypeScript files directly (if tools installed)
command -v tsx >/dev/null 2>&1 && npx tsx --inspect src/file.ts
command -v ts-node >/dev/null 2>&1 && npx ts-node --inspect-brk src/file.ts
# Trace module resolution issues
npx tsc --traceResolution > resolution.log 2>&1
grep "Module resolution" resolution.log
# Debug type checking performance (use --incremental false for clean trace)
npx tsc --generateTrace trace --incremental false
# Analyze trace (if installed)
command -v @typescript/analyze-trace >/dev/null 2>&1 && npx @typescript/analyze-trace trace
# Memory usage analysis
node --max-old-space-size=8192 node_modules/typescript/lib/tsc.js
```
### Custom Error Classes
```typescript
// Proper error class with stack preservation
class DomainError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number
) {
super(message);
this.name = 'DomainError';
Error.captureStackTrace(this, this.constructor);
}
}
```
## Current Best Practices
### Strict by Default
```json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true
}
}
```
### ESM-First Approach
- Set `"type": "module"` in package.json
- Use `.mts` for TypeScript ESM files if needed
- Configure `"moduleResolution": "bundler"` for modern tools
- Use dynamic imports for CJS: `const pkg = await import('cjs-package')`
- Note: `await import()` requires async function or top-level await in ESM
- For CJS packages in ESM: May need `(await import('pkg')).default` depending on the package's export structure and your compiler settings
### AI-Assisted Development
- GitHub Copilot excels at TypeScript generics
- Use AI for boilerplate type definitions
- Validate AI-generated types with type tests
- Document complex types for AI context
## Code Review Checklist
When reviewing TypeScript/JavaScript code, focus on these domain-specific aspects:
### Type Safety
- [ ] No implicit `any` types (use `unknown` or proper types)
- [ ] Strict null checks enabled and properly handled
- [ ] Type assertions (`as`) justified and minimal
- [ ] Generic constraints properly defined
- [ ] Discriminated unions for error handling
- [ ] Return types explicitly declared for public APIs
### TypeScript Best Practices
- [ ] Prefer `interface` over `type` for object shapes (better error messages)
- [ ] Use const assertions for literal types
- [ ] Leverage type guards and predicates
- [ ] Avoid type gymnastics when simpler solution exists
- [ ] Template literal types used appropriately
- [ ] Branded types for domain primitives
### Performance Considerations
- [ ] Type complexity doesn't cause slow compilation
- [ ] No excessive type instantiation depth
- [ ] Avoid complex mapped types in hot paths
- [ ] Use `skipLibCheck: true` in tsconfig
- [ ] Project references configured for monorepos
### Module System
- [ ] Consistent import/export patterns
- [ ] No circular dependencies
- [ ] Proper use of barrel exports (avoid over-bundling)
- [ ] ESM/CJS compatibility handled correctly
- [ ] Dynamic imports for code splitting
### Error Handling Patterns
- [ ] Result types or discriminated unions for errors
- [ ] Custom error classes with proper inheritance
- [ ] Type-safe error boundaries
- [ ] Exhaustive switch cases with `never` type
### Code Organization
- [ ] Types co-located with implementation
- [ ] Shared types in dedicated modules
- [ ] Avoid global type augmentation when possible
- [ ] Proper use of declaration files (.d.ts)
## Quick Decision Trees
### "Which tool should I use?"
```
Type checking only? → tsc
Type checking + linting speed critical? → Biome
Type checking + comprehensive linting? → ESLint + typescript-eslint
Type testing? → Vitest expectTypeOf
Build tool? → Project size <10 packages? Turborepo. Else? Nx
```
### "How do I fix this performance issue?"
```
Slow type checking? → skipLibCheck, incremental, project references
Slow builds? → Check bundler config, enable caching
Slow tests? → Vitest with threads, avoid type checking in tests
Slow language server? → Exclude node_modules, limit files in tsconfig
```
## Expert Resources
### Performance
- [TypeScript Wiki Performance](https://github.com/microsoft/TypeScript/wiki/Performance)
- [Type instantiation tracking](https://github.com/microsoft/TypeScript/pull/48077)
### Advanced Patterns
- [Type Challenges](https://github.com/type-challenges/type-challenges)
- [Type-Level TypeScript Course](https://type-level-typescript.com)
### Tools
- [Biome](https://biomejs.dev) - Fast linter/formatter
- [TypeStat](https://github.com/JoshuaKGoldberg/TypeStat) - Auto-fix TypeScript types
- [ts-migrate](https://github.com/airbnb/ts-migrate) - Migration toolkit
### Testing
- [Vitest Type Testing](https://vitest.dev/guide/testing-types)
- [tsd](https://github.com/tsdjs/tsd) - Standalone type testing
Always validate changes don't break existing functionality before considering the issue resolved.
## When to Use
This skill is applicable to execute the workflow or actions described in the overview.

View File

@@ -0,0 +1,92 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Strict TypeScript 5.x",
"compilerOptions": {
// =========================================================================
// STRICTNESS (Maximum Type Safety)
// =========================================================================
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
// =========================================================================
// MODULE SYSTEM (Modern ESM)
// =========================================================================
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
// =========================================================================
// OUTPUT
// =========================================================================
"target": "ES2022",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// =========================================================================
// PERFORMANCE
// =========================================================================
"skipLibCheck": true,
"incremental": true,
// =========================================================================
// PATH ALIASES
// =========================================================================
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
],
"@/components/*": [
"./src/components/*"
],
"@/lib/*": [
"./src/lib/*"
],
"@/types/*": [
"./src/types/*"
],
"@/utils/*": [
"./src/utils/*"
]
},
// =========================================================================
// JSX (for React projects)
// =========================================================================
// "jsx": "react-jsx",
// =========================================================================
// EMIT
// =========================================================================
"noEmit": true, // Let bundler handle emit
// "outDir": "./dist",
// "rootDir": "./src",
// =========================================================================
// DECORATORS (if needed)
// =========================================================================
// "experimentalDecorators": true,
// "emitDecoratorMetadata": true
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.d.ts"
],
"exclude": [
"node_modules",
"dist",
"build",
"coverage",
"**/*.test.ts",
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,383 @@
# TypeScript Cheatsheet
## Type Basics
```typescript
// Primitives
const name: string = 'John'
const age: number = 30
const isActive: boolean = true
const nothing: null = null
const notDefined: undefined = undefined
// Arrays
const numbers: number[] = [1, 2, 3]
const strings: Array<string> = ['a', 'b', 'c']
// Tuple
const tuple: [string, number] = ['hello', 42]
// Object
const user: { name: string; age: number } = { name: 'John', age: 30 }
// Union
const value: string | number = 'hello'
// Literal
const direction: 'up' | 'down' | 'left' | 'right' = 'up'
// Any vs Unknown
const anyValue: any = 'anything' // ❌ Avoid
const unknownValue: unknown = 'safe' // ✅ Prefer, requires narrowing
```
## Type Aliases & Interfaces
```typescript
// Type Alias
type Point = {
x: number
y: number
}
// Interface (preferred for objects)
interface User {
id: string
name: string
email?: string // Optional
readonly createdAt: Date // Readonly
}
// Extending
interface Admin extends User {
permissions: string[]
}
// Intersection
type AdminUser = User & { permissions: string[] }
```
## Generics
```typescript
// Generic function
function identity<T>(value: T): T {
return value
}
// Generic with constraint
function getLength<T extends { length: number }>(item: T): number {
return item.length
}
// Generic interface
interface ApiResponse<T> {
data: T
status: number
message: string
}
// Generic with default
type Container<T = string> = {
value: T
}
// Multiple generics
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 }
}
```
## Utility Types
```typescript
interface User {
id: string
name: string
email: string
age: number
}
// Partial - all optional
type PartialUser = Partial<User>
// Required - all required
type RequiredUser = Required<User>
// Readonly - all readonly
type ReadonlyUser = Readonly<User>
// Pick - select properties
type UserName = Pick<User, 'id' | 'name'>
// Omit - exclude properties
type UserWithoutEmail = Omit<User, 'email'>
// Record - key-value map
type UserMap = Record<string, User>
// Extract - extract from union
type StringOrNumber = string | number | boolean
type OnlyStrings = Extract<StringOrNumber, string>
// Exclude - exclude from union
type NotString = Exclude<StringOrNumber, string>
// NonNullable - remove null/undefined
type MaybeString = string | null | undefined
type DefinitelyString = NonNullable<MaybeString>
// ReturnType - get function return type
function getUser() { return { name: 'John' } }
type UserReturn = ReturnType<typeof getUser>
// Parameters - get function parameters
type GetUserParams = Parameters<typeof getUser>
// Awaited - unwrap Promise
type ResolvedUser = Awaited<Promise<User>>
```
## Conditional Types
```typescript
// Basic conditional
type IsString<T> = T extends string ? true : false
// Infer keyword
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
// Distributive conditional
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number> // string[] | number[]
// NonDistributive
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
```
## Template Literal Types
```typescript
type Color = 'red' | 'green' | 'blue'
type Size = 'small' | 'medium' | 'large'
// Combine
type ColorSize = `${Color}-${Size}`
// 'red-small' | 'red-medium' | 'red-large' | ...
// Event handlers
type EventName = 'click' | 'focus' | 'blur'
type EventHandler = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus' | 'onBlur'
```
## Mapped Types
```typescript
// Basic mapped type
type Optional<T> = {
[K in keyof T]?: T[K]
}
// With key remapping
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
// Filter keys
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
```
## Type Guards
```typescript
// typeof guard
function process(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase() // string
}
return value.toFixed(2) // number
}
// instanceof guard
class Dog { bark() {} }
class Cat { meow() {} }
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark()
} else {
animal.meow()
}
}
// in guard
interface Bird { fly(): void }
interface Fish { swim(): void }
function move(animal: Bird | Fish) {
if ('fly' in animal) {
animal.fly()
} else {
animal.swim()
}
}
// Custom type guard
function isString(value: unknown): value is string {
return typeof value === 'string'
}
// Assertion function
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Not a string')
}
}
```
## Discriminated Unions
```typescript
// With type discriminant
type Success<T> = { type: 'success'; data: T }
type Error = { type: 'error'; message: string }
type Loading = { type: 'loading' }
type State<T> = Success<T> | Error | Loading
function handle<T>(state: State<T>) {
switch (state.type) {
case 'success':
return state.data // T
case 'error':
return state.message // string
case 'loading':
return null
}
}
// Exhaustive check
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`)
}
```
## Branded Types
```typescript
// Create branded type
type Brand<K, T> = K & { __brand: T }
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
// Constructor functions
function createUserId(id: string): UserId {
return id as UserId
}
function createOrderId(id: string): OrderId {
return id as OrderId
}
// Usage - prevents mixing
function getOrder(orderId: OrderId, userId: UserId) {}
const userId = createUserId('user-123')
const orderId = createOrderId('order-456')
getOrder(orderId, userId) // ✅ OK
// getOrder(userId, orderId) // ❌ Error - types don't match
```
## Module Declarations
```typescript
// Declare module for untyped package
declare module 'untyped-package' {
export function doSomething(): void
export const value: string
}
// Augment existing module
declare module 'express' {
interface Request {
user?: { id: string }
}
}
// Declare global
declare global {
interface Window {
myGlobal: string
}
}
```
## TSConfig Essentials
```json
{
"compilerOptions": {
// Strictness
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Modules
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
// Output
"target": "ES2022",
"lib": ["ES2022", "DOM"],
// Performance
"skipLibCheck": true,
"incremental": true,
// Paths
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
```
## Best Practices
```typescript
// ✅ Prefer interface for objects
interface User {
name: string
}
// ✅ Use const assertions
const routes = ['home', 'about'] as const
// ✅ Use satisfies for validation
const config = {
api: 'https://api.example.com'
} satisfies Record<string, string>
// ✅ Use unknown over any
function parse(input: unknown) {
if (typeof input === 'string') {
return JSON.parse(input)
}
}
// ✅ Explicit return types for public APIs
export function getUser(id: string): User | null {
// ...
}
// ❌ Avoid
const data: any = fetchData()
data.anything.goes.wrong // No type safety
```

View File

@@ -0,0 +1,335 @@
/**
* TypeScript Utility Types Library
*
* A collection of commonly used utility types for TypeScript projects.
* Copy and use as needed in your projects.
*/
// =============================================================================
// BRANDED TYPES
// =============================================================================
/**
* Create nominal/branded types to prevent primitive obsession.
*
* @example
* type UserId = Brand<string, 'UserId'>
* type OrderId = Brand<string, 'OrderId'>
*/
export type Brand<K, T> = K & { readonly __brand: T }
// Branded type constructors
export type UserId = Brand<string, 'UserId'>
export type Email = Brand<string, 'Email'>
export type UUID = Brand<string, 'UUID'>
export type Timestamp = Brand<number, 'Timestamp'>
export type PositiveNumber = Brand<number, 'PositiveNumber'>
// =============================================================================
// RESULT TYPE (Error Handling)
// =============================================================================
/**
* Type-safe error handling without exceptions.
*/
export type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E }
export const ok = <T>(data: T): Result<T, never> => ({
success: true,
data
})
export const err = <E>(error: E): Result<never, E> => ({
success: false,
error
})
// =============================================================================
// OPTION TYPE (Nullable Handling)
// =============================================================================
/**
* Explicit optional value handling.
*/
export type Option<T> = Some<T> | None
export type Some<T> = { type: 'some'; value: T }
export type None = { type: 'none' }
export const some = <T>(value: T): Some<T> => ({ type: 'some', value })
export const none: None = { type: 'none' }
// =============================================================================
// DEEP UTILITIES
// =============================================================================
/**
* Make all properties deeply readonly.
*/
export type DeepReadonly<T> = T extends (...args: any[]) => any
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T
/**
* Make all properties deeply optional.
*/
export type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
/**
* Make all properties deeply required.
*/
export type DeepRequired<T> = T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T
/**
* Make all properties deeply mutable (remove readonly).
*/
export type DeepMutable<T> = T extends object
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
: T
// =============================================================================
// OBJECT UTILITIES
// =============================================================================
/**
* Get keys of object where value matches type.
*/
export type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never
}[keyof T]
/**
* Pick properties by value type.
*/
export type PickByType<T, V> = Pick<T, KeysOfType<T, V>>
/**
* Omit properties by value type.
*/
export type OmitByType<T, V> = Omit<T, KeysOfType<T, V>>
/**
* Make specific keys optional.
*/
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
/**
* Make specific keys required.
*/
export type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
/**
* Make specific keys readonly.
*/
export type ReadonlyBy<T, K extends keyof T> = Omit<T, K> & Readonly<Pick<T, K>>
/**
* Merge two types (second overrides first).
*/
export type Merge<T, U> = Omit<T, keyof U> & U
// =============================================================================
// ARRAY UTILITIES
// =============================================================================
/**
* Get element type from array.
*/
export type ElementOf<T> = T extends (infer E)[] ? E : never
/**
* Tuple of specific length.
*/
export type Tuple<T, N extends number> = N extends N
? number extends N
? T[]
: _TupleOf<T, N, []>
: never
type _TupleOf<T, N extends number, R extends unknown[]> = R['length'] extends N
? R
: _TupleOf<T, N, [T, ...R]>
/**
* Non-empty array.
*/
export type NonEmptyArray<T> = [T, ...T[]]
/**
* At least N elements.
*/
export type AtLeast<T, N extends number> = [...Tuple<T, N>, ...T[]]
// =============================================================================
// FUNCTION UTILITIES
// =============================================================================
/**
* Get function arguments as tuple.
*/
export type Arguments<T> = T extends (...args: infer A) => any ? A : never
/**
* Get first argument of function.
*/
export type FirstArgument<T> = T extends (first: infer F, ...args: any[]) => any
? F
: never
/**
* Async version of function.
*/
export type AsyncFunction<T extends (...args: any[]) => any> = (
...args: Parameters<T>
) => Promise<Awaited<ReturnType<T>>>
/**
* Promisify return type.
*/
export type Promisify<T> = T extends (...args: infer A) => infer R
? (...args: A) => Promise<Awaited<R>>
: never
// =============================================================================
// STRING UTILITIES
// =============================================================================
/**
* Split string by delimiter.
*/
export type Split<S extends string, D extends string> =
S extends `${infer T}${D}${infer U}`
? [T, ...Split<U, D>]
: [S]
/**
* Join tuple to string.
*/
export type Join<T extends string[], D extends string> =
T extends []
? ''
: T extends [infer F extends string]
? F
: T extends [infer F extends string, ...infer R extends string[]]
? `${F}${D}${Join<R, D>}`
: never
/**
* Path to nested object.
*/
export type PathOf<T, K extends keyof T = keyof T> = K extends string
? T[K] extends object
? K | `${K}.${PathOf<T[K]>}`
: K
: never
// =============================================================================
// UNION UTILITIES
// =============================================================================
/**
* Last element of union.
*/
export type UnionLast<T> = UnionToIntersection<
T extends any ? () => T : never
> extends () => infer R
? R
: never
/**
* Union to intersection.
*/
export type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never
/**
* Union to tuple.
*/
export type UnionToTuple<T, L = UnionLast<T>> = [T] extends [never]
? []
: [...UnionToTuple<Exclude<T, L>>, L]
// =============================================================================
// VALIDATION UTILITIES
// =============================================================================
/**
* Assert type at compile time.
*/
export type AssertEqual<T, U> =
(<V>() => V extends T ? 1 : 2) extends (<V>() => V extends U ? 1 : 2)
? true
: false
/**
* Ensure type is not never.
*/
export type IsNever<T> = [T] extends [never] ? true : false
/**
* Ensure type is any.
*/
export type IsAny<T> = 0 extends 1 & T ? true : false
/**
* Ensure type is unknown.
*/
export type IsUnknown<T> = IsAny<T> extends true
? false
: unknown extends T
? true
: false
// =============================================================================
// JSON UTILITIES
// =============================================================================
/**
* JSON-safe types.
*/
export type JsonPrimitive = string | number | boolean | null
export type JsonArray = JsonValue[]
export type JsonObject = { [key: string]: JsonValue }
export type JsonValue = JsonPrimitive | JsonArray | JsonObject
/**
* Make type JSON-serializable.
*/
export type Jsonify<T> = T extends JsonPrimitive
? T
: T extends undefined | ((...args: any[]) => any) | symbol
? never
: T extends { toJSON(): infer R }
? R
: T extends object
? { [K in keyof T]: Jsonify<T[K]> }
: never
// =============================================================================
// EXHAUSTIVE CHECK
// =============================================================================
/**
* Ensure all cases are handled in switch/if.
*/
export function assertNever(value: never, message?: string): never {
throw new Error(message ?? `Unexpected value: ${value}`)
}
/**
* Exhaustive check without throwing.
*/
export function exhaustiveCheck(_value: never): void {
// This function should never be called
}

View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""
TypeScript Project Diagnostic Script
Analyzes TypeScript projects for configuration, performance, and common issues.
"""
import subprocess
import sys
import os
import json
from pathlib import Path
def run_cmd(cmd: str) -> str:
"""Run shell command and return output."""
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result.stdout + result.stderr
except Exception as e:
return str(e)
def check_versions():
"""Check TypeScript and Node versions."""
print("\n📦 Versions:")
print("-" * 40)
ts_version = run_cmd("npx tsc --version 2>/dev/null").strip()
node_version = run_cmd("node -v 2>/dev/null").strip()
print(f" TypeScript: {ts_version or 'Not found'}")
print(f" Node.js: {node_version or 'Not found'}")
def check_tsconfig():
"""Analyze tsconfig.json settings."""
print("\n⚙️ TSConfig Analysis:")
print("-" * 40)
tsconfig_path = Path("tsconfig.json")
if not tsconfig_path.exists():
print("⚠️ tsconfig.json not found")
return
try:
with open(tsconfig_path) as f:
config = json.load(f)
compiler_opts = config.get("compilerOptions", {})
# Check strict mode
if compiler_opts.get("strict"):
print("✅ Strict mode enabled")
else:
print("⚠️ Strict mode NOT enabled")
# Check important flags
flags = {
"noUncheckedIndexedAccess": "Unchecked index access protection",
"noImplicitOverride": "Implicit override protection",
"skipLibCheck": "Skip lib check (performance)",
"incremental": "Incremental compilation"
}
for flag, desc in flags.items():
status = "" if compiler_opts.get(flag) else ""
print(f" {status} {desc}: {compiler_opts.get(flag, 'not set')}")
# Check module settings
print(f"\n Module: {compiler_opts.get('module', 'not set')}")
print(f" Module Resolution: {compiler_opts.get('moduleResolution', 'not set')}")
print(f" Target: {compiler_opts.get('target', 'not set')}")
except json.JSONDecodeError:
print("❌ Invalid JSON in tsconfig.json")
def check_tooling():
"""Detect TypeScript tooling ecosystem."""
print("\n🛠️ Tooling Detection:")
print("-" * 40)
pkg_path = Path("package.json")
if not pkg_path.exists():
print("⚠️ package.json not found")
return
try:
with open(pkg_path) as f:
pkg = json.load(f)
all_deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
tools = {
"biome": "Biome (linter/formatter)",
"eslint": "ESLint",
"prettier": "Prettier",
"vitest": "Vitest (testing)",
"jest": "Jest (testing)",
"turborepo": "Turborepo (monorepo)",
"turbo": "Turbo (monorepo)",
"nx": "Nx (monorepo)",
"lerna": "Lerna (monorepo)"
}
for tool, desc in tools.items():
for dep in all_deps:
if tool in dep.lower():
print(f"{desc}")
break
except json.JSONDecodeError:
print("❌ Invalid JSON in package.json")
def check_monorepo():
"""Check for monorepo configuration."""
print("\n📦 Monorepo Check:")
print("-" * 40)
indicators = [
("pnpm-workspace.yaml", "PNPM Workspace"),
("lerna.json", "Lerna"),
("nx.json", "Nx"),
("turbo.json", "Turborepo")
]
found = False
for file, name in indicators:
if Path(file).exists():
print(f"{name} detected")
found = True
if not found:
print(" ⚪ No monorepo configuration detected")
def check_type_errors():
"""Run quick type check."""
print("\n🔍 Type Check:")
print("-" * 40)
result = run_cmd("npx tsc --noEmit 2>&1 | head -20")
if "error TS" in result:
errors = result.count("error TS")
print(f"{errors}+ type errors found")
print(result[:500])
else:
print(" ✅ No type errors")
def check_any_usage():
"""Check for any type usage."""
print("\n⚠️ 'any' Type Usage:")
print("-" * 40)
result = run_cmd("grep -r ': any' --include='*.ts' --include='*.tsx' src/ 2>/dev/null | wc -l")
count = result.strip()
if count and count != "0":
print(f" ⚠️ Found {count} occurrences of ': any'")
sample = run_cmd("grep -rn ': any' --include='*.ts' --include='*.tsx' src/ 2>/dev/null | head -5")
if sample:
print(sample)
else:
print(" ✅ No explicit 'any' types found")
def check_type_assertions():
"""Check for type assertions."""
print("\n⚠️ Type Assertions (as):")
print("-" * 40)
result = run_cmd("grep -r ' as ' --include='*.ts' --include='*.tsx' src/ 2>/dev/null | grep -v 'import' | wc -l")
count = result.strip()
if count and count != "0":
print(f" ⚠️ Found {count} type assertions")
else:
print(" ✅ No type assertions found")
def check_performance():
"""Check type checking performance."""
print("\n⏱️ Type Check Performance:")
print("-" * 40)
result = run_cmd("npx tsc --extendedDiagnostics --noEmit 2>&1 | grep -E 'Check time|Files:|Lines:|Nodes:'")
if result.strip():
for line in result.strip().split('\n'):
print(f" {line}")
else:
print(" ⚠️ Could not measure performance")
def main():
print("=" * 50)
print("🔍 TypeScript Project Diagnostic Report")
print("=" * 50)
check_versions()
check_tsconfig()
check_tooling()
check_monorepo()
check_any_usage()
check_type_assertions()
check_type_errors()
check_performance()
print("\n" + "=" * 50)
print("✅ Diagnostic Complete")
print("=" * 50)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,6 @@
{
"source": "/tmp/skill-selector-curated-3423638041",
"sourceType": "local",
"localPath": "/tmp/skill-selector-curated-3423638041/typescript-pro",
"installedAt": "2026-04-07T00:45:24.785Z"
}

View File

@@ -0,0 +1,145 @@
---
name: typescript-pro
description: Implements advanced TypeScript type systems, creates custom type guards, utility types, and branded types, and configures tRPC for end-to-end type safety. Use when building TypeScript applications requiring advanced generics, conditional or mapped types, discriminated unions, monorepo setup, or full-stack type safety with tRPC.
license: MIT
metadata:
author: https://github.com/Jeffallan
version: "1.1.0"
domain: language
triggers: TypeScript, generics, type safety, conditional types, mapped types, tRPC, tsconfig, type guards, discriminated unions
role: specialist
scope: implementation
output-format: code
related-skills: fullstack-guardian, api-designer
---
# TypeScript Pro
## Core Workflow
1. **Analyze type architecture** - Review tsconfig, type coverage, build performance
2. **Design type-first APIs** - Create branded types, generics, utility types
3. **Implement with type safety** - Write type guards, discriminated unions, conditional types; run `tsc --noEmit` to catch type errors before proceeding
4. **Optimize build** - Configure project references, incremental compilation, tree shaking; re-run `tsc --noEmit` to confirm zero errors after changes
5. **Test types** - Confirm type coverage with a tool like `type-coverage`; validate that all public APIs have explicit return types; iterate on steps 34 until all checks pass
## Reference Guide
Load detailed guidance based on context:
| Topic | Reference | Load When |
|-------|-----------|-----------|
| Advanced Types | `references/advanced-types.md` | Generics, conditional types, mapped types, template literals |
| Type Guards | `references/type-guards.md` | Type narrowing, discriminated unions, assertion functions |
| Utility Types | `references/utility-types.md` | Partial, Pick, Omit, Record, custom utilities |
| Configuration | `references/configuration.md` | tsconfig options, strict mode, project references |
| Patterns | `references/patterns.md` | Builder pattern, factory pattern, type-safe APIs |
## Code Examples
### Branded Types
```typescript
// Branded type for domain modeling
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<number, "OrderId">;
const toUserId = (id: string): UserId => id as UserId;
const toOrderId = (id: number): OrderId => id as OrderId;
// Usage — prevents accidental id mix-ups at compile time
function getOrder(userId: UserId, orderId: OrderId) { /* ... */ }
```
### Discriminated Unions & Type Guards
```typescript
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: string[] };
type ErrorState = { status: "error"; error: Error };
type RequestState = LoadingState | SuccessState | ErrorState;
// Type predicate guard
function isSuccess(state: RequestState): state is SuccessState {
return state.status === "success";
}
// Exhaustive switch with discriminated union
function renderState(state: RequestState): string {
switch (state.status) {
case "loading": return "Loading…";
case "success": return state.data.join(", ");
case "error": return state.error.message;
default: {
const _exhaustive: never = state;
throw new Error(`Unhandled state: ${_exhaustive}`);
}
}
}
```
### Custom Utility Types
```typescript
// Deep readonly — immutable nested objects
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// Require exactly one of a set of keys
type RequireExactlyOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>> &
{ [K in Keys]-?: Required<Pick<T, K>> & Partial<Record<Exclude<Keys, K>, never>> }[Keys];
```
### Recommended tsconfig.json
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"incremental": true,
"skipLibCheck": false
}
}
```
## Constraints
### MUST DO
- Enable strict mode with all compiler flags
- Use type-first API design
- Implement branded types for domain modeling
- Use `satisfies` operator for type validation
- Create discriminated unions for state machines
- Use `Annotated` pattern with type predicates
- Generate declaration files for libraries
- Optimize for type inference
### MUST NOT DO
- Use explicit `any` without justification
- Skip type coverage for public APIs
- Mix type-only and value imports
- Disable strict null checks
- Use `as` assertions without necessity
- Ignore compiler performance warnings
- Skip declaration file generation
- Use enums (prefer const objects with `as const`)
## Output Templates
When implementing TypeScript features, provide:
1. Type definitions (interfaces, types, generics)
2. Implementation with type guards
3. tsconfig configuration if needed
4. Brief explanation of type design decisions
## Knowledge Reference
TypeScript 5.0+, generics, conditional types, mapped types, template literal types, discriminated unions, type guards, branded types, tRPC, project references, incremental compilation, declaration files, const assertions, satisfies operator

View File

@@ -0,0 +1,259 @@
# Advanced Types
## Generic Constraints
```typescript
// Basic constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Multiple constraints
interface HasId { id: number; }
interface HasName { name: string; }
function merge<T extends HasId, U extends HasName>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
// Generic constraint with default
type ApiResponse<T = unknown, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// Constraint with infer
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Result = UnwrapPromise<Promise<string>>; // string
```
## Conditional Types
```typescript
// Basic conditional type
type IsString<T> = T extends string ? true : false;
// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type StringOrNumberArray = ToArray<string | number>; // string[] | number[]
// Non-distributive (use tuple)
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type BothArray = ToArrayNonDist<string | number>; // (string | number)[]
// Nested conditionals for type extraction
type Flatten<T> = T extends Array<infer U>
? U extends Array<infer V>
? Flatten<V>
: U
: T;
type Nested = Flatten<string[][][]>; // string
// Exclude null/undefined
type NonNullable<T> = T extends null | undefined ? never : T;
```
## Mapped Types
```typescript
// Basic mapped type
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};
// Optional properties
type Partial<T> = {
[K in keyof T]?: T[K];
};
// Required properties
type Required<T> = {
[K in keyof T]-?: T[K]; // Remove optional modifier
};
// Key remapping with 'as'
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
// Filtering keys
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
type StringFields = PickByType<Person, string>; // { name: string }
```
## Template Literal Types
```typescript
// Basic template literal
type EmailLocale = 'en' | 'es' | 'fr';
type EmailType = 'welcome' | 'reset-password';
type EmailTemplate = `${EmailLocale}_${EmailType}`;
// 'en_welcome' | 'en_reset-password' | 'es_welcome' | ...
// Intrinsic string manipulation
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
// Template literal with mapped types
type CSSProperties = {
[K in 'color' | 'background' | 'border' as `--${K}`]: string;
};
// { '--color': string; '--background': string; '--border': string }
// Pattern matching with infer
type ExtractRouteParams<T extends string> =
T extends `${infer _Start}/:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: T extends `${infer _Start}/:${infer Param}`
? Param
: never;
type Params = ExtractRouteParams<'/users/:id/posts/:postId'>; // 'id' | 'postId'
```
## Higher-Kinded Types (Simulation)
```typescript
// Type-level function simulation
interface TypeClass<F> {
map: <A, B>(f: (a: A) => B, fa: any) => any;
}
// Functor pattern
type Maybe<T> = { type: 'just'; value: T } | { type: 'nothing' };
const MaybeFunctor: TypeClass<Maybe<any>> = {
map: <A, B>(f: (a: A) => B, ma: Maybe<A>): Maybe<B> => {
return ma.type === 'just'
? { type: 'just', value: f(ma.value) }
: { type: 'nothing' };
}
};
// Builder pattern with generics
type Builder<T, K extends keyof T = never> = {
with<P extends Exclude<keyof T, K>>(
key: P,
value: T[P]
): Builder<T, K | P>;
build(): K extends keyof T ? T : never;
};
```
## Recursive Types
```typescript
// JSON type
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
// Deep partial
type DeepPartial<T> = T extends object ? {
[K in keyof T]?: DeepPartial<T[K]>;
} : T;
// Deep readonly
type DeepReadonly<T> = T extends object ? {
readonly [K in keyof T]: DeepReadonly<T[K]>;
} : T;
// Path type for nested objects
type PathsToProps<T> = T extends object ? {
[K in keyof T]: K extends string
? T[K] extends object
? K | `${K}.${PathsToProps<T[K]>}`
: K
: never;
}[keyof T] : never;
interface User {
profile: {
name: string;
settings: {
theme: string;
};
};
}
type UserPaths = PathsToProps<User>;
// 'profile' | 'profile.name' | 'profile.settings' | 'profile.settings.theme'
```
## Variance and Contravariance
```typescript
// Covariance (return types)
type Producer<T> = () => T;
let stringProducer: Producer<string> = () => 'hello';
let objectProducer: Producer<object> = stringProducer; // OK: string is object
// Contravariance (parameter types)
type Consumer<T> = (value: T) => void;
let objectConsumer: Consumer<object> = (obj) => console.log(obj);
let stringConsumer: Consumer<string> = objectConsumer; // OK in strict mode
// Invariance (mutable properties)
interface Box<T> {
value: T;
setValue(v: T): void;
}
let stringBox: Box<string> = { value: '', setValue: (v) => {} };
// let objectBox: Box<object> = stringBox; // Error: invariant
```
## Type-Level Programming
```typescript
// Type-level addition (limited)
type Length<T extends any[]> = T['length'];
type Concat<A extends any[], B extends any[]> = [...A, ...B];
// Type-level conditionals
type If<Condition extends boolean, Then, Else> =
Condition extends true ? Then : Else;
// Type-level equality
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
// Assert equal types (for testing)
type Assert<T extends true> = T;
type Test = Assert<Equal<1 | 2, 2 | 1>>; // OK
```
## Quick Reference
| Pattern | Use Case |
|---------|----------|
| `T extends U ? X : Y` | Conditional type logic |
| `infer R` | Extract types from patterns |
| `K in keyof T` | Iterate over object keys |
| `as NewKey` | Remap keys in mapped types |
| Template literals | String pattern types |
| `T extends any` | Distributive conditionals |
| `[T] extends [any]` | Non-distributive check |
| `-?` modifier | Remove optional |
| `readonly` modifier | Make immutable |

View File

@@ -0,0 +1,445 @@
# TypeScript Configuration
## Strict Mode Configuration
```json
{
"compilerOptions": {
// Strict type checking
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
// Additional checks
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
// Module resolution
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
// Emit
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"importHelpers": true,
// Interop
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
// Target
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
// Skip checking
"skipLibCheck": true
}
}
```
## Project References
```json
// Root tsconfig.json
{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/frontend" },
{ "path": "./packages/backend" }
]
}
// packages/shared/tsconfig.json
{
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"]
}
// packages/frontend/tsconfig.json
{
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../shared" }
],
"include": ["src/**/*"]
}
```
## Module Resolution Strategies
```json
// Node16/NodeNext (recommended for Node.js)
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true
}
}
// Bundler (for bundlers like Vite, esbuild)
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"moduleDetection": "force"
}
}
// Classic (legacy, avoid)
{
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "node"
}
}
```
## Path Mapping
```json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@shared/*": ["../shared/src/*"],
"@types": ["src/types/index.ts"]
}
}
}
```
```typescript
// Usage with path mapping
import { Button } from '@components/Button';
import { formatDate } from '@utils/date';
import type { User } from '@types';
```
## Incremental Compilation
```json
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./dist/.tsbuildinfo",
"composite": true
}
}
```
## Declaration Files
```json
{
"compilerOptions": {
// Generate .d.ts files
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": false,
// Bundle declarations
"declarationDir": "./types",
// For libraries
"stripInternal": true
}
}
```
```typescript
// Using JSDoc for .d.ts generation
/**
* Creates a user
* @param name - User's name
* @param email - User's email
* @returns The created user
* @example
* ```ts
* const user = createUser('John', 'john@example.com');
* ```
*/
export function createUser(name: string, email: string): User {
return { id: generateId(), name, email };
}
```
## Build Optimization
```json
{
"compilerOptions": {
// Performance
"skipLibCheck": true,
"skipDefaultLibCheck": true,
// Faster builds
"incremental": true,
"assumeChangesOnlyAffectDirectDependencies": true,
// Smaller output
"removeComments": true,
"importHelpers": true,
// Tree shaking support
"module": "ESNext",
"target": "ES2020"
}
}
```
## Multiple Configurations
```json
// tsconfig.json (base)
{
"compilerOptions": {
"strict": true,
"target": "ES2022"
}
}
// tsconfig.build.json (production)
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"removeComments": true,
"declaration": true
},
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
}
// tsconfig.test.json (testing)
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"],
"esModuleInterop": true
},
"include": ["src/**/*.test.ts", "src/**/*.spec.ts"]
}
```
## Framework-Specific Configs
```json
// React + Vite
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"strict": true
}
}
// Next.js
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
// Node.js + Express
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": true
}
}
```
## Custom Type Definitions
```typescript
// src/types/global.d.ts
declare global {
interface Window {
myApp: {
version: string;
config: AppConfig;
};
}
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
API_KEY: string;
NODE_ENV: 'development' | 'production' | 'test';
}
}
}
export {};
// src/types/modules.d.ts
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*.css' {
const classes: { [key: string]: string };
export default classes;
}
declare module 'untyped-library' {
export function doSomething(value: string): number;
}
```
## Compiler API Usage
```typescript
// programmatic compilation
import ts from 'typescript';
function compile(fileNames: string[], options: ts.CompilerOptions): void {
const program = ts.createProgram(fileNames, options);
const emitResult = program.emit();
const allDiagnostics = ts
.getPreEmitDiagnostics(program)
.concat(emitResult.diagnostics);
allDiagnostics.forEach(diagnostic => {
if (diagnostic.file) {
const { line, character } = ts.getLineAndCharacterOfPosition(
diagnostic.file,
diagnostic.start!
);
const message = ts.flattenDiagnosticMessageText(
diagnostic.messageText,
'\n'
);
console.log(
`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
);
} else {
console.log(
ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')
);
}
});
const exitCode = emitResult.emitSkipped ? 1 : 0;
console.log(`Process exiting with code '${exitCode}'.`);
process.exit(exitCode);
}
compile(['src/index.ts'], {
noEmitOnError: true,
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.ES2022,
strict: true
});
```
## Performance Monitoring
```json
{
"compilerOptions": {
"diagnostics": true,
"extendedDiagnostics": true,
"generateCpuProfile": "profile.cpuprofile",
"explainFiles": true
}
}
```
```bash
# Run with diagnostics
tsc --diagnostics
# Extended diagnostics
tsc --extendedDiagnostics
# Generate trace
tsc --generateTrace trace
# Analyze with @typescript/analyze-trace
npx @typescript/analyze-trace trace
```
## Quick Reference
| Option | Purpose |
|--------|---------|
| `strict` | Enable all strict checks |
| `composite` | Enable project references |
| `incremental` | Enable incremental compilation |
| `skipLibCheck` | Skip .d.ts checking for faster builds |
| `esModuleInterop` | Better CommonJS interop |
| `moduleResolution` | How modules are resolved |
| `paths` | Path mapping for imports |
| `declaration` | Generate .d.ts files |
| `sourceMap` | Generate source maps |
| `noEmit` | Don't emit output (type check only) |
| `isolatedModules` | Each file can be transpiled separately |
| `allowImportingTsExtensions` | Import .ts files directly |

View File

@@ -0,0 +1,484 @@
# TypeScript Patterns
## Builder Pattern
```typescript
// Type-safe builder with progressive types
class UserBuilder {
private data: Partial<User> = {};
setName(name: string): this {
this.data.name = name;
return this;
}
setEmail(email: string): this {
this.data.email = email;
return this;
}
setAge(age: number): this {
this.data.age = age;
return this;
}
build(): User {
if (!this.data.name || !this.data.email) {
throw new Error('Name and email are required');
}
return this.data as User;
}
}
// Fluent API with type safety
const user = new UserBuilder()
.setName('John')
.setEmail('john@example.com')
.setAge(30)
.build();
// Advanced builder with compile-time validation
type Builder<T, K extends keyof T = never> = {
[P in keyof T as `set${Capitalize<string & P>}`]: (
value: T[P]
) => Builder<T, K | P>;
} & {
build: K extends keyof T ? () => T : never;
};
function createBuilder<T>(): Builder<T> {
const data = {} as T;
return new Proxy({} as Builder<T>, {
get(_, prop: string) {
if (prop === 'build') {
return () => data;
}
if (prop.startsWith('set')) {
const key = prop.slice(3).toLowerCase();
return (value: any) => {
(data as any)[key] = value;
return this;
};
}
}
});
}
```
## Factory Pattern
```typescript
// Abstract factory with type safety
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(message);
}
}
class FileLogger implements Logger {
constructor(private filename: string) {}
log(message: string): void {
// Write to file
}
}
type LoggerType = 'console' | 'file';
type LoggerConfig<T extends LoggerType> = T extends 'file'
? { type: T; filename: string }
: { type: T };
class LoggerFactory {
static create<T extends LoggerType>(config: LoggerConfig<T>): Logger {
switch (config.type) {
case 'console':
return new ConsoleLogger();
case 'file':
return new FileLogger(config.filename);
default:
throw new Error('Unknown logger type');
}
}
}
const consoleLogger = LoggerFactory.create({ type: 'console' });
const fileLogger = LoggerFactory.create({ type: 'file', filename: 'app.log' });
// Generic factory with dependency injection
type Constructor<T> = new (...args: any[]) => T;
class Container {
private instances = new Map<Constructor<any>, any>();
register<T>(token: Constructor<T>, instance: T): void {
this.instances.set(token, instance);
}
resolve<T>(token: Constructor<T>): T {
const instance = this.instances.get(token);
if (!instance) {
throw new Error(`No instance registered for ${token.name}`);
}
return instance;
}
}
```
## Repository Pattern
```typescript
// Type-safe repository with generic CRUD
interface Entity {
id: string | number;
}
interface Repository<T extends Entity> {
find(id: T['id']): Promise<T | null>;
findAll(): Promise<T[]>;
create(data: Omit<T, 'id'>): Promise<T>;
update(id: T['id'], data: Partial<Omit<T, 'id'>>): Promise<T>;
delete(id: T['id']): Promise<void>;
}
class UserRepository implements Repository<User> {
async find(id: User['id']): Promise<User | null> {
// Database query
return null;
}
async findAll(): Promise<User[]> {
return [];
}
async create(data: Omit<User, 'id'>): Promise<User> {
// Insert into database
return { id: 1, ...data };
}
async update(id: User['id'], data: Partial<Omit<User, 'id'>>): Promise<User> {
// Update database
return { id, name: '', email: '', ...data };
}
async delete(id: User['id']): Promise<void> {
// Delete from database
}
}
// Query builder with type safety
class QueryBuilder<T> {
private conditions: Array<(item: T) => boolean> = [];
where<K extends keyof T>(key: K, value: T[K]): this {
this.conditions.push(item => item[key] === value);
return this;
}
execute(items: T[]): T[] {
return items.filter(item =>
this.conditions.every(condition => condition(item))
);
}
}
const query = new QueryBuilder<User>()
.where('email', 'john@example.com')
.where('age', 30);
```
## Type-Safe API Client
```typescript
// REST API client with type safety
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type ApiEndpoints = {
'/users': {
GET: { response: User[] };
POST: { body: CreateUserDto; response: User };
};
'/users/:id': {
GET: { params: { id: string }; response: User };
PUT: { params: { id: string }; body: UpdateUserDto; response: User };
DELETE: { params: { id: string }; response: void };
};
'/posts': {
GET: { query: { userId?: string }; response: Post[] };
POST: { body: CreatePostDto; response: Post };
};
};
type ExtractParams<T extends string> =
T extends `${infer _Start}/:${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractParams<`/${Rest}`>
: T extends `${infer _Start}/:${infer Param}`
? { [K in Param]: string }
: {};
class ApiClient {
async request<
Path extends keyof ApiEndpoints,
Method extends keyof ApiEndpoints[Path]
>(
method: Method,
path: Path,
options?: ApiEndpoints[Path][Method] extends { body: infer B }
? { body: B }
: ApiEndpoints[Path][Method] extends { params: infer P }
? { params: P }
: ApiEndpoints[Path][Method] extends { query: infer Q }
? { query: Q }
: never
): Promise<
ApiEndpoints[Path][Method] extends { response: infer R } ? R : never
> {
// Make HTTP request
return null as any;
}
}
const client = new ApiClient();
// Type-safe API calls
const users = await client.request('GET', '/users');
const user = await client.request('GET', '/users/:id', { params: { id: '1' } });
const newUser = await client.request('POST', '/users', {
body: { name: 'John', email: 'john@example.com' }
});
```
## State Machine Pattern
```typescript
// Type-safe state machine
type State = 'idle' | 'loading' | 'success' | 'error';
type Event =
| { type: 'FETCH' }
| { type: 'SUCCESS'; data: any }
| { type: 'ERROR'; error: Error }
| { type: 'RETRY' };
type StateMachine = {
[S in State]: {
[E in Event['type']]?: State;
};
};
const machine: StateMachine = {
idle: { FETCH: 'loading' },
loading: { SUCCESS: 'success', ERROR: 'error' },
success: { FETCH: 'loading' },
error: { RETRY: 'loading' }
};
class StateManager<S extends string, E extends { type: string }> {
constructor(
private state: S,
private transitions: Record<S, Partial<Record<E['type'], S>>>
) {}
getState(): S {
return this.state;
}
dispatch(event: E): S {
const nextState = this.transitions[this.state][event.type];
if (nextState === undefined) {
throw new Error(`Invalid transition from ${this.state} on ${event.type}`);
}
this.state = nextState;
return this.state;
}
}
const manager = new StateManager<State, Event>('idle', machine);
manager.dispatch({ type: 'FETCH' }); // 'loading'
manager.dispatch({ type: 'SUCCESS', data: {} }); // 'success'
```
## Decorator Pattern
```typescript
// Method decorators with type safety
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
function Memoize(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const cache = new Map<string, any>();
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
class Calculator {
@Log
@Memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
```
## Result/Either Pattern
```typescript
// Type-safe error handling
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { success: true, value };
}
function err<E>(error: E): Result<never, E> {
return { success: false, error };
}
async function fetchUser(id: string): Promise<Result<User, string>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return err('User not found');
}
const user = await response.json();
return ok(user);
} catch (error) {
return err('Network error');
}
}
// Usage with pattern matching
const result = await fetchUser('123');
if (result.success) {
console.log(result.value.name); // Type-safe access
} else {
console.error(result.error); // Type-safe error
}
// Either monad
class Either<L, R> {
private constructor(
private readonly value: L | R,
private readonly isRight: boolean
) {}
static left<L, R>(value: L): Either<L, R> {
return new Either<L, R>(value, false);
}
static right<L, R>(value: R): Either<L, R> {
return new Either<L, R>(value, true);
}
map<T>(fn: (value: R) => T): Either<L, T> {
if (this.isRight) {
return Either.right(fn(this.value as R));
}
return Either.left(this.value as L);
}
flatMap<T>(fn: (value: R) => Either<L, T>): Either<L, T> {
if (this.isRight) {
return fn(this.value as R);
}
return Either.left(this.value as L);
}
getOrElse(defaultValue: R): R {
return this.isRight ? (this.value as R) : defaultValue;
}
}
```
## Singleton Pattern
```typescript
// Type-safe singleton
class Database {
private static instance: Database;
private constructor() {
// Private constructor prevents instantiation
}
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
query<T>(sql: string): Promise<T[]> {
// Execute query
return Promise.resolve([]);
}
}
const db = Database.getInstance();
// Generic singleton factory
function singleton<T>(factory: () => T): () => T {
let instance: T | undefined;
return () => {
if (!instance) {
instance = factory();
}
return instance;
};
}
const getConfig = singleton(() => ({
apiUrl: process.env.API_URL,
apiKey: process.env.API_KEY
}));
```
## Quick Reference
| Pattern | Use Case |
|---------|----------|
| Builder | Construct complex objects step by step |
| Factory | Create objects without specifying exact class |
| Repository | Abstract data access layer |
| API Client | Type-safe HTTP requests |
| State Machine | Manage state transitions |
| Decorator | Add behavior to methods |
| Result/Either | Type-safe error handling |
| Singleton | Ensure single instance |
| Query Builder | Type-safe database queries |
| Container | Dependency injection |

View File

@@ -0,0 +1,352 @@
# Type Guards and Narrowing
## Type Predicates
```typescript
// Basic type predicate
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function processValue(value: string | number) {
if (isString(value)) {
console.log(value.toUpperCase()); // value is string
} else {
console.log(value.toFixed(2)); // value is number
}
}
// Generic type predicate
function isArray<T>(value: T | T[]): value is T[] {
return Array.isArray(value);
}
// Narrowing to specific interface
interface User {
type: 'user';
name: string;
email: string;
}
interface Admin {
type: 'admin';
name: string;
permissions: string[];
}
function isAdmin(account: User | Admin): account is Admin {
return account.type === 'admin';
}
```
## Discriminated Unions
```typescript
// Tagged union pattern
type Result<T, E = Error> =
| { status: 'success'; data: T }
| { status: 'error'; error: E }
| { status: 'loading' };
function handleResult<T>(result: Result<T>) {
switch (result.status) {
case 'success':
console.log(result.data); // Narrowed to success
break;
case 'error':
console.error(result.error); // Narrowed to error
break;
case 'loading':
console.log('Loading...'); // Narrowed to loading
break;
}
}
// Complex discriminated union
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
}
}
// Exhaustive checking
function assertNever(x: never): never {
throw new Error('Unexpected value: ' + x);
}
function processShape(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return shape.radius;
case 'rectangle':
return shape.width;
case 'triangle':
return shape.base;
default:
return assertNever(shape); // Compile error if not exhaustive
}
}
```
## Built-in Type Guards
```typescript
// typeof narrowing
function printValue(value: string | number | boolean) {
if (typeof value === 'string') {
console.log(value.toUpperCase());
} else if (typeof value === 'number') {
console.log(value.toFixed(2));
} else {
console.log(value ? 'yes' : 'no');
}
}
// instanceof narrowing
class Dog {
bark() { console.log('woof'); }
}
class Cat {
meow() { console.log('meow'); }
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
// in operator narrowing
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ('swim' in animal) {
animal.swim();
} else {
animal.fly();
}
}
// Truthiness narrowing
function printLength(value: string | null | undefined) {
if (value) {
console.log(value.length); // Narrowed to string
}
}
// Equality narrowing
function compare(x: string | number, y: string | boolean) {
if (x === y) {
// x and y are both string
console.log(x.toUpperCase(), y.toUpperCase());
}
}
```
## Assertion Functions
```typescript
// Basic assertion function
function assert(condition: unknown, message?: string): asserts condition {
if (!condition) {
throw new Error(message || 'Assertion failed');
}
}
function processUser(user: unknown) {
assert(typeof user === 'object' && user !== null);
assert('name' in user && typeof user.name === 'string');
console.log(user.name.toUpperCase()); // user is narrowed
}
// Type assertion function
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value is not a string');
}
}
function greet(name: unknown) {
assertIsString(name);
console.log(`Hello, ${name.toUpperCase()}`); // name is string
}
// Generic assertion function
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error('Value is null or undefined');
}
}
function processValue(value: string | null) {
assertIsDefined(value);
console.log(value.length); // value is string
}
// Assert with type predicate
function assertIsUser(value: unknown): asserts value is User {
if (
typeof value !== 'object' ||
value === null ||
!('type' in value) ||
value.type !== 'user'
) {
throw new Error('Not a user');
}
}
```
## Control Flow Analysis
```typescript
// Assignment narrowing
let x: string | number = Math.random() > 0.5 ? 'hello' : 42;
if (typeof x === 'string') {
x; // string
} else {
x; // number
}
// Return statement narrowing
function getValue(flag: boolean): string | number {
if (flag) {
return 'hello';
}
return 42; // TypeScript knows this must be number
}
// Throw statement narrowing
function processValue(value: string | null) {
if (!value) {
throw new Error('Value is required');
}
console.log(value.length); // value is string (null thrown above)
}
// Type guards in array methods
const mixed: (string | number)[] = ['a', 1, 'b', 2];
const strings = mixed.filter((x): x is string => typeof x === 'string');
// strings is string[]
```
## Branded Types
```typescript
// Nominal typing with branded types
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type Email = Brand<string, 'Email'>;
type Url = Brand<string, 'Url'>;
// Constructor functions
function createUserId(id: string): UserId {
return id as UserId;
}
function createEmail(email: string): Email {
if (!email.includes('@')) {
throw new Error('Invalid email');
}
return email as Email;
}
// Usage prevents mixing
const userId: UserId = createUserId('user-123');
const email: Email = createEmail('user@example.com');
// const wrongAssignment: UserId = email; // Error!
// Type guard for branded types
function isUserId(value: string): value is UserId {
return /^user-\d+$/.test(value);
}
// Branded numbers
type Positive = Brand<number, 'Positive'>;
type Integer = Brand<number, 'Integer'>;
function createPositive(n: number): Positive {
if (n <= 0) throw new Error('Must be positive');
return n as Positive;
}
function createInteger(n: number): Integer {
if (!Number.isInteger(n)) throw new Error('Must be integer');
return n as Integer;
}
```
## Advanced Narrowing Patterns
```typescript
// Array.isArray with generics
function processInput<T>(input: T | T[]): T[] {
return Array.isArray(input) ? input : [input];
}
// Object key narrowing
function getProperty<T extends object, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key];
}
// Mapped type narrowing
type Nullable<T> = { [K in keyof T]: T[K] | null };
function isComplete<T extends object>(
obj: Nullable<T>
): obj is T {
return Object.values(obj).every((v) => v !== null);
}
// Custom narrowing with type maps
type TypeMap = {
string: string;
number: number;
boolean: boolean;
};
function is<K extends keyof TypeMap>(
type: K,
value: unknown
): value is TypeMap[K] {
return typeof value === type;
}
if (is('string', someValue)) {
someValue.toUpperCase(); // someValue is string
}
```
## Quick Reference
| Pattern | Use Case |
|---------|----------|
| `value is Type` | Type predicate function |
| `asserts condition` | Assertion function |
| `asserts value is Type` | Type assertion function |
| Discriminated union | Tagged union with literal type |
| `typeof` guard | Primitive type checking |
| `instanceof` guard | Class instance checking |
| `in` operator | Property existence check |
| `assertNever` | Exhaustive switch checking |
| Branded types | Nominal typing simulation |
| `NonNullable<T>` | Remove null/undefined |

View File

@@ -0,0 +1,329 @@
# Utility Types
## Built-in Utility Types
```typescript
// Partial - All properties optional
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; }
function updateUser(id: number, updates: Partial<User>) {
// Only pass fields to update
}
// Required - All properties required
type RequiredUser = Required<PartialUser>;
// { id: number; name: string; email: string; }
// Readonly - All properties readonly
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; readonly email: string; }
// Pick - Select specific properties
type UserSummary = Pick<User, 'id' | 'name'>;
// { id: number; name: string; }
// Omit - Exclude specific properties
type UserWithoutEmail = Omit<User, 'email'>;
// { id: number; name: string; }
// Record - Create object type with specific keys
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
// { [key: string]: 'admin' | 'user' | 'guest' }
type PageInfo = Record<'home' | 'about' | 'contact', { title: string }>;
// { home: { title: string }, about: { title: string }, contact: { title: string } }
```
## Type Extraction Utilities
```typescript
// Extract - Extract types from union
type AllTypes = 'a' | 'b' | 'c' | 1 | 2 | 3;
type StringTypes = Extract<AllTypes, string>; // 'a' | 'b' | 'c'
type NumberTypes = Extract<AllTypes, number>; // 1 | 2 | 3
// Exclude - Remove types from union
type WithoutNumbers = Exclude<AllTypes, number>; // 'a' | 'b' | 'c'
// NonNullable - Remove null and undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string
// ReturnType - Extract function return type
function getUser() {
return { id: 1, name: 'John' };
}
type User = ReturnType<typeof getUser>; // { id: number; name: string }
// Parameters - Extract function parameter types
function createUser(name: string, age: number) {
return { name, age };
}
type CreateUserParams = Parameters<typeof createUser>; // [string, number]
// ConstructorParameters - Extract constructor parameters
class Point {
constructor(public x: number, public y: number) {}
}
type PointParams = ConstructorParameters<typeof Point>; // [number, number]
// InstanceType - Extract instance type from constructor
type PointInstance = InstanceType<typeof Point>; // Point
```
## Custom Utility Types
```typescript
// DeepPartial - Recursive partial
type DeepPartial<T> = T extends object ? {
[K in keyof T]?: DeepPartial<T[K]>;
} : T;
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
}
type PartialConfig = DeepPartial<Config>;
// All nested properties are optional
// DeepReadonly - Recursive readonly
type DeepReadonly<T> = T extends object ? {
readonly [K in keyof T]: DeepReadonly<T[K]>;
} : T;
// Mutable - Remove readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type MutableUser = Mutable<ReadonlyUser>;
// PickByType - Pick properties by value type
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
age: number;
email: string;
}
type StringProps = PickByType<Mixed, string>; // { name: string; email: string }
type NumberProps = PickByType<Mixed, number>; // { id: number; age: number }
// OmitByType - Omit properties by value type
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};
type NoStrings = OmitByType<Mixed, string>; // { id: number; age: number }
```
## Function Utilities
```typescript
// Promisify - Convert sync to async
type Promisify<T extends (...args: any[]) => any> = (
...args: Parameters<T>
) => Promise<ReturnType<T>>;
function syncFunction(x: number): string {
return x.toString();
}
type AsyncVersion = Promisify<typeof syncFunction>;
// (x: number) => Promise<string>
// Awaited - Unwrap promise type
type AwaitedString = Awaited<Promise<string>>; // string
type DeepAwaited = Awaited<Promise<Promise<number>>>; // number
// ThisParameterType - Extract this parameter
function greet(this: User, message: string) {
return `${this.name}: ${message}`;
}
type ThisType = ThisParameterType<typeof greet>; // User
// OmitThisParameter - Remove this parameter
type GreetFunction = OmitThisParameter<typeof greet>;
// (message: string) => string
```
## Advanced Custom Utilities
```typescript
// Nullable - Add null and undefined
type Nullable<T> = T | null | undefined;
// ValueOf - Get union of all property values
type ValueOf<T> = T[keyof T];
interface Codes {
success: 200;
notFound: 404;
error: 500;
}
type StatusCode = ValueOf<Codes>; // 200 | 404 | 500
// RequireAtLeastOne - Require at least one property
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>> &
{
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
}[Keys];
interface Options {
id?: number;
name?: string;
email?: string;
}
type AtLeastOne = RequireAtLeastOne<Options>;
// Must have at least one of id, name, or email
// RequireOnlyOne - Require exactly one property
type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
Pick<T, Exclude<keyof T, Keys>> &
{
[K in Keys]-?:
Required<Pick<T, K>> &
Partial<Record<Exclude<Keys, K>, undefined>>;
}[Keys];
type OnlyOne = RequireOnlyOne<Options>;
// Must have exactly one of id, name, or email
// Merge - Deep merge two types
type Merge<T, U> = Omit<T, keyof U> & U;
interface Base {
id: number;
name: string;
}
interface Extension {
name: string; // Override
email: string; // Add
}
type Combined = Merge<Base, Extension>;
// { id: number; name: string; email: string }
// ConditionalKeys - Get keys matching condition
type ConditionalKeys<T, Condition> = {
[K in keyof T]: T[K] extends Condition ? K : never;
}[keyof T];
type FunctionKeys = ConditionalKeys<typeof Math, Function>;
// 'abs' | 'acos' | 'sin' | ...
```
## Tuple Utilities
```typescript
// First - Get first element type
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type FirstType = First<[string, number, boolean]>; // string
// Last - Get last element type
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type LastType = Last<[string, number, boolean]>; // boolean
// Tail - Remove first element
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
type TailTypes = Tail<[string, number, boolean]>; // [number, boolean]
// Prepend - Add element to beginning
type Prepend<T extends any[], U> = [U, ...T];
type WithString = Prepend<[number, boolean], string>; // [string, number, boolean]
// Reverse - Reverse tuple
type Reverse<T extends any[]> =
T extends [infer First, ...infer Rest]
? [...Reverse<Rest>, First]
: [];
type Reversed = Reverse<[1, 2, 3]>; // [3, 2, 1]
```
## String Utilities
```typescript
// Split - Split string into tuple
type Split<S extends string, D extends string> =
S extends `${infer T}${D}${infer U}`
? [T, ...Split<U, D>]
: [S];
type Parts = Split<'a-b-c', '-'>; // ['a', 'b', 'c']
// Join - Join tuple into string
type Join<T extends string[], D extends string> =
T extends [infer F extends string, ...infer R extends string[]]
? R extends []
? F
: `${F}${D}${Join<R, D>}`
: '';
type Joined = Join<['a', 'b', 'c'], '-'>; // 'a-b-c'
// Replace - Replace substring
type Replace<
S extends string,
From extends string,
To extends string
> = S extends `${infer L}${From}${infer R}`
? `${L}${To}${R}`
: S;
type Replaced = Replace<'hello world', 'world', 'TypeScript'>;
// 'hello TypeScript'
// TrimLeft - Remove leading whitespace
type TrimLeft<S extends string> =
S extends ` ${infer Rest}` ? TrimLeft<Rest> : S;
type Trimmed = TrimLeft<' hello'>; // 'hello'
```
## Quick Reference
| Utility | Purpose |
|---------|---------|
| `Partial<T>` | Make all properties optional |
| `Required<T>` | Make all properties required |
| `Readonly<T>` | Make all properties readonly |
| `Pick<T, K>` | Select subset of properties |
| `Omit<T, K>` | Remove subset of properties |
| `Record<K, T>` | Create object type with keys K |
| `Extract<T, U>` | Extract types assignable to U |
| `Exclude<T, U>` | Remove types assignable to U |
| `NonNullable<T>` | Remove null and undefined |
| `ReturnType<T>` | Extract function return type |
| `Parameters<T>` | Extract function parameters |
| `Awaited<T>` | Unwrap Promise type |

View File

@@ -1,9 +1,8 @@
OPENROUTER_API_KEY=
AUTH_AUTHENTIK_CLIENT_ID=
AUTH_AUTHENTIK_CLIENT_SECRET=notsosupersecret
AUTH_AUTHENTIK_ISSUER=https://example.com
AUTH_AUTHENTIK_CLIENT_SECRET=XXXXXXXXXXXXXXXX
AUTH_AUTHENTIK_ISSUER=XXXXXXXXXXXXXXXXXXX
NEXTAUTH_URL=https://example.com
AUTH_SECRET=supersecret
NEXTAUTH_SECRET=supersecret
DB_URL=postgres://<user>:<password>@<host>:<port>/<database>
BETTER_AUTH_URL=XXXXXXXXXXXXXXXXXXX
BETTER_AUTH_SECRET=XXXXXXXXXXX
DB_URL=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

9
.gitignore vendored
View File

@@ -67,3 +67,12 @@ yarn-error.log*
next-env.d.ts
public/sw.js
public/workbox-*.js
# START Ruler Generated Files
/AGENTS.md
/AGENTS.md.bak
/opencode.json
/opencode.json.bak
# END Ruler Generated Files
/debug.log

93
.ruler/00-LINT-RULES.md Normal file
View File

@@ -0,0 +1,93 @@
## CRITICAL: Lint Rules Are Sacred and Immutable
**ABSOLUTE PROHIBITION**: You are **FORBIDDEN** from modifying, disabling, or bypassing any lint rules, ESLint configurations, TypeScript compiler settings, or any other code quality enforcement mechanisms in this repository.
## Non-Negotiable Principles
### 1. Rules Must NEVER Be Changed
- **NO** adding `// eslint-disable` comments
- **NO** adding `// @ts-ignore` or `// @ts-expect-error` comments
- **NO** modifying `.eslintrc`, `eslint.config.js`, or any ESLint configuration files
- **NO** modifying `tsconfig.json` compiler options to silence errors
- **NO** modifying `biome.json`, `prettier.config.js`, `.oxlintrc`, or any formatter settings
- **NO** adding files to `.eslintignore` or exclude patterns
- **NO** downgrading errors to warnings or warnings to off
- **NO** adjusting rule severity or options
### 2. Fix the Root Cause, Not the Symptom
When encountering a lint error or type error:
1. **Attempt 1-10**: Fix the underlying code issue that violates the rule
- Refactor the code to comply with the rule
- Restructure the logic to avoid the violation
- Use proper types and patterns that satisfy the linter
- Redesign the approach entirely if needed
- Consider alternative implementations
- Review similar patterns in the codebase for guidance
- Consult documentation for the library/framework being used
- Try multiple different architectural approaches
- Explore edge cases and alternative solutions
- Exhaust ALL possible code-level fixes
2. **After 10+ Genuine Attempts**: If you have exhausted ALL reasonable code fixes and the error persists:
- **STOP** and **ASK THE USER** for guidance
- Present the specific rule violation
- Explain what you've tried (all 10+ attempts)
- Ask if there's a pattern you're missing or if an exception is warranted
- **NEVER** make the decision to disable or modify rules yourself
### 3. Why Rules Exist
- Lint rules enforce consistency across the codebase
- They prevent bugs and anti-patterns
- They represent team decisions and conventions
- They ensure code quality and maintainability
- They are project-specific and carefully chosen
### 4. Common Scenarios and Correct Responses
#### Scenario: "Unused variable" error
- ❌ WRONG: Add `// eslint-disable-next-line no-unused-vars`
- ✅ RIGHT: Remove the unused variable or use it properly
#### Scenario: "any type" error
- ❌ WRONG: Add `// @ts-ignore` or change to `unknown` just to silence
- ✅ RIGHT: Define proper types that accurately represent the data
#### Scenario: "Missing dependency in useEffect" warning
- ❌ WRONG: Add `// eslint-disable-next-line react-hooks/exhaustive-deps`
- ✅ RIGHT: Add the missing dependency or restructure to avoid the issue
#### Scenario: "Type errors in third-party library"
- ❌ WRONG: Use `@ts-expect-error` or cast to `any`
- ✅ RIGHT: Install proper type definitions, create a typed wrapper, or use proper type assertions
#### Scenario: "Complexity too high" error
- ❌ WRONG: Disable the complexity rule
- ✅ RIGHT: Refactor the function into smaller, simpler functions
### 5. Enforcement Priority
Lint rules have **MAXIMUM PRIORITY**. They outrank:
- Personal coding preferences
- Convenience
- Speed of implementation
- Desire to "just make it work"
### 6. Remember
**You are here to serve the repository's conventions, not to modify them.**
If you find yourself thinking "it would be easier to just disable this rule," that is **EXACTLY** when you must **NOT** do it.
## Summary
1. ❌ NEVER disable, ignore, or modify lint rules
2. ✅ ALWAYS fix the code to comply with rules
3. ✅ Try 10+ different approaches to fix the root issue
4. ✅ ASK THE USER if all code-level fixes fail
5. ❌ NEVER act autonomously on rule modifications
**These are not guidelines. These are absolute requirements.**

52
.ruler/02-BUN-GUIDE.md Normal file
View File

@@ -0,0 +1,52 @@
## Bun Guidelines
**CRITICAL**: Do not assume you know full Bun APIs. For **ANY** Bun API you use, confirm them by using `bun-docs` MCP tools.
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
- Bun automatically loads .env, so don't use dotenv.
### APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
### Testing
#### Quick Start
- Run tests: `bun test`
- Write tests in `tests/` folder
#### Test Structure
- Use `describe` blocks to group related tests
- Use `test` for individual test cases
- Use `beforeEach`/`afterEach` for setup/teardown
#### Assertions
- Import: `import { test, expect, describe, beforeEach, afterEach, mock } from "bun:test";`
- Common: `expect(value).toBe(expected)`, `expect(fn).rejects.toThrow()`
- Async: `await expect(asyncFn()).resolves.toBe(expected)`
#### Mocking
- Mock functions: `mock(fn)`
- Mock globals: `global.fetch = mock(...)`
- Restore mocks in `afterEach` or `finally`
#### Best Practices
- Mock external APIs (fetch, file I/O)
- Test error cases and edge conditions
- Use descriptive test names
- Clean up resources in `afterEach`
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.

19
.ruler/03-ZOD-GUIDE.md Normal file
View File

@@ -0,0 +1,19 @@
## Zod Guidelines
### Schema Definition
- Define all schemas in `src/types.ts`
- Use `z.object()` for objects, `z.array()` for arrays
- Mark optional fields with `.optional()`
- Create generic schemas for reusable structures
### Type Inference
- Always infer types from schemas: `export type Foo = z.infer<typeof FooSchema>`
### Validation
- Use `.parse()` to validate API responses
- Only validate successful responses (`retcode === RESPONSE_CODES.SUCCESS`)
- Return unvalidated responses for error cases
### Patterns
- Follow existing schema naming: `FooSchema` for schemas, `Foo` for types
- Use `ZZZResponseSchema(dataSchema)` for API responses

View File

@@ -0,0 +1,460 @@
# Production Testing Doctrine
_Project-Agnostic Engineering Standard_
---
# 1. Purpose of Testing
Testing exists to:
- Prevent regressions
- Protect critical business behavior
- Enforce invariants
- Guard boundaries
- Provide safe refactoring
- Reduce production incidents
Testing does not exist to:
- Increase coverage numbers
- Satisfy tooling requirements
- Mirror implementation linebyline
- Create a false sense of security
If a test does not reduce real-world risk, it should not exist.
---
# 2. Core Principles
---
## 2.1 Determinism Is Non-Negotiable
A test must:
- Produce the same result every run
- Not depend on execution order
- Not depend on global state
- Not depend on wall-clock time
- Not depend on external networks
- Not depend on randomness (unless seeded)
A flaky test is worse than no test.
If a test fails intermittently:
- Fix it immediately
- Or delete it
There is no third option.
---
## 2.2 Isolation of Behavior
Tests should verify behavior in isolation from unrelated systems.
The smaller the scope of the test, the more reliable and faster it is.
We separate:
- Pure logic
- System interactions
- External integrations
- Full-system behavior
Confusing these layers results in slow, fragile suites.
---
## 2.3 Risk-Based Testing
Testing effort should scale with risk.
High-risk areas:
- Financial logic
- Security and access control
- Data mutation
- Distributed coordination
- Concurrency
- Migration and transformation logic
Low-risk areas:
- Static rendering
- Formatting helpers
- Simple data mapping
Testing must prioritize business-critical systems.
---
## 2.4 Tests Are Part of the System
Tests must follow the same standards as production code:
- Clean structure
- Clear naming
- Maintainable
- Reviewed in PRs
- Refactored when necessary
Test code quality reflects engineering quality.
---
# 3. Testing Layers (Architecture-Neutral)
These layers apply universally.
---
# 3.1 Unit Tests (Logic Layer)
Definition:
Tests that validate pure behavior without system dependencies.
Must:
- Run fast
- Avoid I/O
- Avoid network
- Avoid persistent state
- Avoid framework bootstrapping
Should test:
- Business rules
- Domain invariants
- Edge cases
- Validation
- Transformation logic
Reasoning:
If logic cannot be tested without infrastructure, it is coupled too tightly.
---
# 3.2 Integration Tests (System Boundary Layer)
Definition:
Tests that validate interactions between internal components.
May include:
- Datastores
- Filesystems
- Queues
- Caches
- Framework wiring
- Service boundaries
Must:
- Use real internal components
- Reset state between runs
- Avoid real external services
Reasoning:
Most production bugs occur at boundaries, not in pure functions.
---
# 3.3 External Integration Tests
Definition:
Tests that validate interaction with third-party systems.
Policy:
- Prefer mocking or simulation
- Use sandbox environments only when necessary
- Never depend on live production services
Reasoning:
External systems are outside your control and introduce nondeterminism.
---
# 3.4 End-to-End Tests (System-Level)
Definition:
Tests that validate complete workflows from entry to outcome.
Must:
- Cover only critical flows
- Be minimal in number
- Run in isolated environments
- Avoid unnecessary duplication of lower-level tests
End-to-end tests are expensive and fragile. Use them surgically.
---
# 4. State Management Policy
---
## 4.1 No Shared State Between Tests
Every test must assume a blank environment.
Options:
- Fresh environment per test
- Transaction rollback
- Full reset between runs
- Isolated test containers
No test may depend on side effects from another test.
---
## 4.2 Reproducible Environments
Tests must run consistently:
- Locally
- In CI
- In parallel
- Across operating systems (if supported)
Environment drift is unacceptable.
---
# 5. Mocking Policy
---
## 5.1 Mock External Systems
Mock:
- Third-party APIs
- Payment providers
- Email systems
- External storage
- Network services outside system boundary
Reasoning:
You do not control them.
---
## 5.2 Do Not Mock Core Logic
Never mock:
- Business rules
- Authorization checks
- Data validation
- Domain logic
Mocking internal logic invalidates the test.
---
## 5.3 Avoid Over-Mocking
Over-mocking:
- Couples tests to implementation
- Breaks refactoring
- Creates fragile tests
Mock only what crosses system boundaries.
---
# 6. Error & Edge Case Policy
Every public interface must have tests for:
- Valid input
- Invalid input
- Unauthorized or restricted access (if applicable)
- Boundary values
- Failure paths
- Concurrency conflicts (if applicable)
Most real-world failures happen outside happy paths.
---
# 7. Security Testing Doctrine
All systems must test:
- Access control enforcement
- Privilege boundaries
- Input validation
- Injection resistance (where applicable)
- Role escalation prevention
Security-sensitive logic must have near-complete coverage.
---
# 8. Concurrency & Race Conditions
If the system involves:
- Multi-threading
- Distributed nodes
- Async processing
- Queues
- Parallel writes
Then tests must include:
- Concurrent execution scenarios
- Conflict handling
- Idempotency verification
- Retry logic behavior
These bugs rarely appear in simple test cases.
---
# 9. Migration & Data Evolution
If the system stores data over time:
- Schema migrations must be tested
- Data transformation must be verified
- Backward compatibility must be validated
- Downgrade scenarios (if supported) must be considered
Silent data corruption is catastrophic.
---
# 10. CI Enforcement
Tests must run automatically:
- On every pull request
- On main branch
- Before release
CI must:
- Fail fast
- Prevent merges on failure
- Run in clean environments
- Be reproducible
If tests only run locally, they are not part of the system.
---
# 11. Coverage Philosophy
Coverage is a diagnostic tool, not a goal.
Required:
- High coverage on business-critical modules
- Full coverage on security boundaries
- Full coverage on financial logic
Optional:
- High coverage on trivial UI or formatting
100% coverage does not imply correctness.
Low coverage in critical areas is unacceptable.
---
# 12. Performance of the Test Suite
The test suite must:
- Run quickly enough to encourage frequent execution
- Support parallelization
- Avoid arbitrary sleeps
- Avoid unnecessary bootstrapping
Slow tests reduce engineering velocity and discourage use.
---
# 13. Red Flags (Immediate Rejection)
- Tests that sometimes fail
- Tests that depend on execution order
- Snapshot abuse
- Arbitrary timeouts to “fix” flakiness
- Global mutable state
- Randomized data without seed
- Testing implementation details instead of behavior
- Excessive E2E replacing proper layering
- Mocking core domain logic
- Tests that assert only truthy values
---
# 14. Refactoring Policy
Tests must enable refactoring.
If changing internal structure breaks many tests without changing behavior:
- The tests are coupled incorrectly.
Behavioral contracts should remain stable under refactor.
---
# 15. Production Observability Complements Testing
Testing does not replace:
- Logging
- Monitoring
- Alerting
- Metrics
- Tracing
Tests prevent known failures.
Observability detects unknown ones.
Both are required.
---
# 16. The Engineering Mindset
Before writing any test, ask:
1. What failure would hurt the business most?
2. What invariant must never break?
3. What boundary is being crossed?
4. What assumptions are being made?
5. Can this test fail nondeterministically?
6. Is this testing behavior or implementation?
If the test does not meaningfully reduce risk, reconsider it.
---
# 17. Definition of Production-Grade Testing
A system with production-grade testing:
- Can be refactored safely
- Rarely ships regressions
- Catches security violations before release
- Detects data integrity failures early
- Has a stable, trusted CI pipeline
- Has a fast feedback loop
- Is boringly reliable
Engineers trust the test suite.
They do not ignore it.
They do not fear it.
They rely on it.
That is the standard.

1
.ruler/08-CONTEXT7.md Normal file
View File

@@ -0,0 +1 @@
**CRITICAL**: Always use context7 when I need code generation, setup or configuration steps, or library/API documentation. This means you should automatically use the Context7 MCP tools to resolve library id and get library docs without me having to explicitly ask.

287
.ruler/09-NIX.md Normal file
View File

@@ -0,0 +1,287 @@
# Nix Tool Preference Directive
## Core Instruction
**Always prioritize Nix-based tools (`nix_nix`, `nix_nix_versions`, `nix-shell`, `nix run`, etc.) for querying packages, versions, options, environments, and reproducible workflows whenever relevant.**
Nix ensures declarative, isolated, reproducible results. Use these before generic searches, manual installs, or distro tools.
## Trigger Conditions
- Package discovery/version info: `nix_nix` or `nix_nix_versions`.
- Environment setup: `nix-shell`, `nix develop`.
- NixOS/Home-manager config: `nix_nix action=options`.
- Reproducibility/CI: Nix flakes/builds.
**Fallback**: Native tools if Nix unsuitable; note Nix alternative.
## Specific Tools
### **`nix_nix`** - Query NixOS, Home Manager, Darwin, flakes, and more
| Parameter | Description |
|-----------|-------------|
| `action` | `search`, `info`, `stats`, `options`, `channels`, `flake-inputs`, `cache` |
| `source` | `nixos`, `home-manager`, `darwin`, `flakes`, `flakehub`, `nixvim`, `wiki`, `nix-dev`, `noogle`, `nixhub` |
| `query` | Search term, name, or prefix |
| `type` | `packages`, `options`, `programs`, `list`, `ls`, `read` |
| `channel` | `unstable`, `stable`, `25.05` |
| `limit` | 1-100 (or 1-2000 for flake-inputs read) |
### **`nix_nix_versions`** - Get package version history from NixHub.io
| Parameter | Description |
|-----------|-------------|
| `package` | Package name |
| `version` | Specific version to find (optional) |
| `limit` | 1-50 |
## Key Use-Cases
| Use-Case | Tool/Command | Example |
|----------|--------------|---------|
| Search packages | `nix_nix` | `nix_nix action=search query=ripgrep source=nixos` |
| Find options | `nix_nix` | `nix_nix action=options query=services.nginx source=nixos` |
| Flake inputs | `nix_nix` | `nix_nix action=flake-inputs query=read source=flakes` |
| Version history | `nix_nix_versions` | `nix_nix_versions package=nodejs` |
| Temporary tool | `nix run` | `nix run nixpkgs#ripgrep -- search_term` |
| Project env | `nix develop` | Loads `flake.nix` deps |
| Pure shell | `nix-shell --pure` | Isolated env |
## Agent Integration
- **Query packages/options first**: Use `nix_nix` before assuming availability.
- **Pin versions**: Check `nix_nix_versions` for stable choices.
- **Generate configs**: Create `shell.nix`/flakes based on queries.
- **Verify**: `nix --version` if needed.
## Example `shell.nix`
````nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [ nodejs_20 python311 ripgrep ];
}
````
## Flake Creation for Packages
**Guideline**: Create `flake.nix` for building packages, reproducible dev shells, multi-language support, and cross-platform compatibility.
**Triggers**:
- Building language-specific packages (e.g., Rust via `crane`).
- Configurable dev environments (Rust/Node/Go/etc.).
- Use `nix_nix` first: e.g., `nix_nix action=search query=crane source=flakes` for libs.
**Key Structure**:
- **inputs**: `nixpkgs`, `flake-parts`, lang tools (`crane`, `fenix`, etc.).
- **outputs**: `packages.default`, `devShells.default` with `shellHook`.
- Multi-system: `x86_64-linux`, `aarch64-darwin`, etc.
**Example `flake.nix`** (Rust MCP server + configurable dev envs via `dev-environments`):
````nix
{
description = "Rust documentation MCP server";
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
dev-environments.url = "github:Govcraft/dev-environments";
crane = {
url = "github:ipetkov/crane";
};
};
outputs =
inputs@{ flake-parts, nixpkgs, crane, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
inputs.dev-environments.flakeModules.rust
inputs.dev-environments.flakeModules.golang
inputs.dev-environments.flakeModules.node
inputs.dev-environments.flakeModules.typst
];
systems = [
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
perSystem =
{
config,
self',
inputs',
pkgs,
system,
...
}:
let
craneLib = inputs.crane.mkLib pkgs;
# Common arguments shared between all builds
commonArgs = {
src = craneLib.cleanCargoSource ./.;
buildInputs = with pkgs; [
openssl
] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
# Additional darwin specific inputs
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
];
nativeBuildInputs = with pkgs; [
pkg-config
perl
];
};
# Build *just* the cargo dependencies
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
# Build the actual crate itself
rustdocs-mcp-server = craneLib.buildPackage (commonArgs // {
inherit cargoArtifacts;
});
in
{
# Add the package
packages = {
default = rustdocs-mcp-server;
rustdocs-mcp-server = rustdocs-mcp-server;
};
# Golang Development Environment Options
# ----------------------------------
# enable: boolean - Enable/disable the Golang environment
# goVersion: enum - Go toolchain version ("1.18", "1.19", "1.20", "1.21, "1.22", "1.23") (default: "1.23")
# withTools: list of strings - Additional Go tools to include (e.g., "golint", "gopls")
# extraPackages: list of packages - Additional packages to include
go-dev = {
# enable = true;
# goVersion = "1.23";
# withTools = [ "gopls" "golint" ];
# extraPackages = [ ];
};
# Rust Development Environment Options
# ----------------------------------
# enable: boolean - Enable/disable the Rust environment
# rustVersion: enum - Rust toolchain ("stable", "beta", "nightly") (default: "stable")
# withTools: list of strings - Additional Rust tools to include (converted to cargo-*)
# extraPackages: list of packages - Additional packages to include
# ide.type: enum - IDE preference ("rust-rover", "vscode", "none") (default: "none")
rust-dev = {
enable = true;
rustVersion = "stable";
# Example configuration:
# withTools = [ ]; # Will be prefixed with cargo-
extraPackages = [ pkgs.openssl pkgs.openssl.dev pkgs.pkg-config ]; # Add openssl libs, dev libs, and pkg-config
# ide.type = "none";
};
# Node.js Development Environment Options
# -------------------------------------
# enable: boolean - Enable/disable the Node environment
# nodeVersion: string - Version of Node.js to use (default: "20")
# withTools: list of packages - Global tools to include (default: ["typescript" "yarn" "pnpm"])
# extraPackages: list of packages - Additional packages to include
# ide.type: enum - IDE preference ("vscode", "webstorm", "none") (default: "none")
node-dev = {
# Example configuration:
# enable = true;
# nodeVersion = "20";
# withTools = [ "typescript" "yarn" "pnpm" ];
# extraPackages = [ ];
# ide.type = "none";
};
typst-dev = {
# Example configuration:
# enable = true;
# withTools = [ "typst-fmt" "typst-lsp" ];
# extraPackages = [ ];
# ide.type = "none";
};
# Create the combined shell
devShells.default = pkgs.mkShell {
buildInputs = nixpkgs.lib.flatten (nixpkgs.lib.attrValues config.env-packages ++ [ ]);
# Combine existing hooks with new exports for OpenSSL
shellHook = ''
${nixpkgs.lib.concatStringsSep "\n" (nixpkgs.lib.attrValues config.env-hooks)}
# Export paths for openssl-sys build script
export OPENSSL_DIR="${pkgs.openssl.out}"
export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib"
export OPENSSL_INCLUDE_DIR="${pkgs.openssl.dev}/include"
# Ensure pkg-config can find the openssl .pc file
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig:${pkgs.pkg-config}/lib/pkgconfig:$PKG_CONFIG_PATH"
# Debug: List contents of OpenSSL lib directory to verify
echo "OpenSSL lib directory contents:"
ls -la ${pkgs.openssl.out}/lib || echo "Failed to list OpenSSL lib directory"
echo ">>> OpenSSL environment variables set by shellHook <<<"
'';
};
# Add app for `nix run`
apps = {
default = {
type = "app";
program = "${rustdocs-mcp-server}/bin/rustdocs_mcp_server";
};
};
};
};
}
````
**Usage**:
- `nix develop` for dev shell.
- `nix build` for package.
- `nix run` for app.
## uv2nix for Python Projects
**When to use**: Python projects with `uv.lock` - handles complex dependencies not in nixpkgs.
**Inputs needed**:
```nix
pyproject-nix.url = "github:pyproject-nix/pyproject.nix";
uv2nix.url = "github:pyproject-nix/uv2nix";
pyproject-build-systems.url = "github:pyproject-nix/build-system-pkgs";
```
**Critical API details**:
- Function is `loadWorkspace`, NOT `loadUvWorkspace` (despite tool name)
- Use `python = pkgs.python313`, NOT `inherit (pkgs) python313`
- Creates virtual environments via `mkVirtualEnv`, not traditional derivations
**Overlay composition order** (must be exact):
1. `pyproject-build-systems.overlays.default`
2. Project overlay from `workspace.mkPyprojectOverlay`
3. Custom `pyprojectOverrides`
**Pattern**:
```nix
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
overlay = workspace.mkPyprojectOverlay { sourcePreference = "wheel"; };
pythonSet = (pkgs.callPackage pyproject-nix.build.packages {
python = pkgs.python313;
}).overrideScope (lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
pyprojectOverrides
]);
in {
packages.default = pythonSet.mkVirtualEnv "env-name" workspace.deps.default;
}
```
**Output**: Virtual env with executables in `result/bin/<package-name>`.
## Prohibitions
- Avoid non-reproducible installs (e.g., `apt`).
- Avoid non-declarative package management (e.g., `nix-env -iA nixos.ripgrep` or `nix profile add nixpkgs#ripgrep`)
- Always check Nix tools first.
**Next**: Query a package with `nix_nix` or generate `shell.nix`.

106
.ruler/99-OPENSKILLS.md Normal file
View File

@@ -0,0 +1,106 @@
# 99-OPENSKILLS
<skills_system priority="1">
## Available Skills
<!-- SKILLS_TABLE_START -->
<usage>
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
How to use skills:
- Invoke: `openskills read <skill-name>` (run in your shell)
- For multiple: `openskills read skill-one,skill-two`
- The skill content will load with detailed instructions on how to complete the task
- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)
Usage notes:
- Only use skills listed in <available_skills> below
- Do not invoke a skill that is already loaded in your context
- Each skill invocation is stateless
</usage>
<available_skills>
<skill>
<name>auth-implementation-patterns</name>
<description>Master authentication and authorization patterns including JWT, OAuth2, session management, and RBAC to build secure, scalable access control systems. Use when implementing auth systems, securing APIs, or debugging security issues.</description>
<location>project</location>
</skill>
<skill>
<name>broken-authentication</name>
<description>"Identify and exploit authentication and session management vulnerabilities in web applications. Broken authentication consistently ranks in the OWASP Top 10 and can lead to account takeover, identity theft, and unauthorized access to sensitive systems."</description>
<location>project</location>
</skill>
<skill>
<name>bun-development</name>
<description>"Fast, modern JavaScript/TypeScript development with the Bun runtime, inspired by [oven-sh/bun](https://github.com/oven-sh/bun)."</description>
<location>project</location>
</skill>
<skill>
<name>drizzle-orm-expert</name>
<description>"Expert in Drizzle ORM for TypeScript — schema design, relational queries, migrations, and serverless database integration. Use when building type-safe database layers with Drizzle."</description>
<location>project</location>
</skill>
<skill>
<name>grill-me</name>
<description>Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".</description>
<location>project</location>
</skill>
<skill>
<name>nextjs-app-router-patterns</name>
<description>Master Next.js 14+ App Router with Server Components, streaming, parallel routes, and advanced data fetching. Use when building Next.js applications, implementing SSR/SSG, or optimizing React Server Components.</description>
<location>project</location>
</skill>
<skill>
<name>nextjs-best-practices</name>
<description>"Next.js App Router principles. Server Components, data fetching, routing patterns."</description>
<location>project</location>
</skill>
<skill>
<name>nextjs-developer</name>
<description>"Use when building Next.js 14+ applications with App Router, server components, or server actions. Invoke to configure route handlers, implement middleware, set up API routes, add streaming SSR, write generateMetadata for SEO, scaffold loading.tsx/error.tsx boundaries, or deploy to Vercel. Triggers on: Next.js, Next.js 14, App Router, RSC, use server, Server Components, Server Actions, React Server Components, generateMetadata, loading.tsx, Next.js deployment, Vercel, Next.js performance."</description>
<location>project</location>
</skill>
<skill>
<name>react-nextjs-development</name>
<description>"React and Next.js 14+ application development with App Router, Server Components, TypeScript, Tailwind CSS, and modern frontend patterns."</description>
<location>project</location>
</skill>
<skill>
<name>tdd</name>
<description>Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD, mentions "red-green-refactor", wants integration tests, or asks for test-first development.</description>
<location>project</location>
</skill>
<skill>
<name>typescript-advanced-types</name>
<description>Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects.</description>
<location>project</location>
</skill>
<skill>
<name>typescript-expert</name>
<description>TypeScript and JavaScript expert with deep knowledge of type-level programming, performance optimization, monorepo management, migration strategies, and modern tooling.</description>
<location>project</location>
</skill>
<skill>
<name>typescript-pro</name>
<description>Implements advanced TypeScript type systems, creates custom type guards, utility types, and branded types, and configures tRPC for end-to-end type safety. Use when building TypeScript applications requiring advanced generics, conditional or mapped types, discriminated unions, monorepo setup, or full-stack type safety with tRPC.</description>
<location>project</location>
</skill>
</available_skills>
<!-- SKILLS_TABLE_END -->
</skills_system>

1
.ruler/AGENTS.md Normal file
View File

@@ -0,0 +1 @@
AGENTS.md

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Get the absolute path of this script
SCRIPT_PATH="$(realpath "$0")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
SCRIPT_NAME="$(basename "$SCRIPT_PATH")"
# Target directory is ../.chrome relative to script location
TARGET_DIR="$(realpath "$SCRIPT_DIR/../.chrome" 2>/dev/null || echo "$SCRIPT_DIR/../.chrome")"
TARGET_PATH="$TARGET_DIR/$SCRIPT_NAME"
# Check if script is NOT in the .chrome directory
if [[ "$SCRIPT_DIR" != "$TARGET_DIR" ]]; then
# Create .chrome directory if it doesn't exist
if [[ ! -d "$TARGET_DIR" ]]; then
mkdir -p "$TARGET_DIR"
fi
# Move script to .chrome directory
mv "$SCRIPT_PATH" "$TARGET_PATH"
chmod +x "$TARGET_PATH"
# Execute from new location and exit
exec "$TARGET_PATH" "$@"
# If we get here, exec failed
echo "Failed to execute from $TARGET_PATH" >&2
exit 1
fi
SOCKET_PATH="${SOCKET_PATH:-$TARGET_DIR/chrome-devtools-mcp.sock}"
if [[ "$SOCKET_PATH" != /* ]]; then
SOCKET_PATH="$TARGET_DIR/$SOCKET_PATH"
fi
if [[ ! -S "$SOCKET_PATH" ]]; then
echo "No socket exists at $SOCKET_PATH" >&2
exit 1
fi
(
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"bash","version":"1.0"}}}'
sleep 1
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"navigate_page","arguments":{"url":"'"https://example.com"'"}}}'
sleep 3
) | socat - UNIX-CONNECT:"$SOCKET_PATH"

105
.ruler/ruler.toml Normal file
View File

@@ -0,0 +1,105 @@
# Ruler Configuration File
# See https://ai.intellectronica.net/ruler for documentation.
# To specify which agents are active by default when --agents is not used,
# uncomment and populate the following line. If omitted, all agents are active.
default_agents = ["opencode"]
# Enable nested rule loading from nested .ruler directories
# When enabled, ruler will search for and process .ruler directories throughout the project hierarchy
nested = true
# [gitignore]
# enabled = true
# local = false # set true to write generated ignores to .git/info/exclude instead
# --- Agent Specific Configurations ---
# You can enable/disable agents and override their default output paths here.
# Use lowercase agent identifiers: aider, amp, claude, cline, codex, copilot, cursor, jetbrains-ai, kilocode, pi, windsurf
# [agents.copilot]
# enabled = true
# output_path = ".github/copilot-instructions.md"
# [agents.aider]
# enabled = true
# output_path_instructions = "AGENTS.md"
# output_path_config = ".aider.conf.yml"
# [agents.gemini-cli]
# enabled = true
# --- MCP Servers ---
# Define Model Context Protocol servers here. Two examples:
# 1. A stdio server (local executable)
# 2. A remote server (HTTP-based)
# [mcp_servers.example_stdio]
# command = "node"
# args = ["scripts/your-mcp-server.js"]
# env = { API_KEY = "replace_me" }
# [mcp_servers.example_remote]
# url = "https://api.example.com/mcp"
# headers = { Authorization = "Bearer REPLACE_ME" }
#
# mcp-template: mcp_servers
#
[mcp_servers."Better Auth"]
url = "https://mcp.chonkie.ai/better-auth/better-auth-builder/mcp"
type = "remote"
#
# [mcp_servers.beads]
# command = "beads-mcp"
# type = "stdio"
#
[mcp_servers.bun]
url = "https://bun.com/docs/mcp"
type = "remote"
#
[mcp_servers.chrome-devtools]
command = "stdio-multiplexer"
args = ["chrome-devtools-mcp", "--", "--user-data-dir=.chrome/profile"]
env.SOCKET_PATH = ".chrome/chrome-devtools-mcp.sock"
#
[mcp_servers.context7]
url = "https://mcp.context7.com/mcp"
type = "remote"
#
[mcp_servers.next-devtools]
command = "bun"
args = ["/home/dstanchiev/projects/next-devtools-mcp/dist/index.js"]
env.NEXT_TELEMETRY_DISABLED = "1"
env.NEXT_DEVTOOLS_PKG_MANAGER = "bun"
#
# [mcp_servers.niri]
# command = "niri-mcp-server"
#
# [mcp_servers.rustdocs]
# command = "rustdocs-mcp"
#
[mcp_servers.shadcn]
command = "bunx"
args = ["--bun", "shadcn@latest", "mcp"]
#
# [mcp_servers.github]
# url = "https://api.githubcopilot.com/mcp"
# headers.Authorization = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
#
[mcp_servers.grep-app]
command = "grep-app-mcp-server"
#
[mcp_servers."openrouter.ai"]
url = "https://openrouter.ai/docs/_mcp/server"
type = "remote"
#
[mcp_servers.nix]
command = "mcp-nixos"
#
# [mcp_servers.devenv]
# command = "devenv"
# args = ["mcp"]
#
# [mcp_servers.kagi]
# command = "kagimcp"
# env.KAGI_API_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

View File

@@ -0,0 +1,23 @@
# skill-selector config
# Repos can be GitHub shorthands (owner/repo), full URLs, or local paths
repos:
- anthropics/skills
- BrownFineSecurity/iothackbot
- HacktronAI/skills
- Italink/UnrealClientProtocol
- Jeffallan/claude-skills
- SimoneAvogadro/android-reverse-engineering-skill
- SylphAI-Inc/skills
- buzzer-re/Rikugan
- coleam00/excalidraw-diagram-skill
- gmh5225/awesome-game-security
- kalil0321/reverse-api-engineer
- kevinpbuckley/VibeUE
- mattpocock/skills
- mukul975/Anthropic-Cybersecurity-Skills
- nyldn/claude-octopus
- pluginagentmarketplace/custom-plugin-game-developer
- sickn33/antigravity-awesome-skills
- tfriedel/claude-office-skills
- wshobson/agents
- ~/projects/ai-skills

View File

@@ -1,4 +0,0 @@
# FIXME
- [ ] minimatch types
https://github.com/strapi/strapi/issues/23859

View File

@@ -5,13 +5,13 @@
"": {
"name": "ical-pwa",
"dependencies": {
"@auth/drizzle-adapter": "^1.10.0",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"better-auth": "^1.6.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
@@ -22,7 +22,6 @@
"lucide-react": "^0.539.0",
"nanoid": "^5.1.5",
"next": "15.4.10",
"next-auth": "^5.0.0-beta.29",
"next-themes": "^0.4.6",
"pg": "^8.16.3",
"postgres": "^3.4.7",
@@ -55,9 +54,23 @@
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@auth/core": ["@auth/core@0.41.1", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^6.0.6", "oauth4webapi": "^3.3.0", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^7.0.7" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw=="],
"@better-auth/core": ["@better-auth/core@1.6.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-LmdPTyKRDn6iCcXBGlOHOyzpJl1W/3w64zrEbhhHaWmtdpzQWlY8awlWBoDTL9eL4TAusr9dDvwIbMYTvEqaeA=="],
"@auth/drizzle-adapter": ["@auth/drizzle-adapter@1.11.1", "", { "dependencies": { "@auth/core": "0.41.1" } }, "sha512-cQTvDZqsyF7RPhDm/B6SvqdVP9EzQhy3oM4Muu7fjjmSYFLbSR203E6dH631ZHSKDn2b4WZkfMnjPDzRsPSAeA=="],
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-iMgvZlrL4FI63CGaxLqE5rgA2Q9VVmc2fQIP7N5E79nGAEpHtztstHFPlen9RDLRJA4xa3wuyVaPSILylwE+LA=="],
"@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "kysely": "^0.27.0 || ^0.28.0" }, "optionalPeers": ["kysely"] }, "sha512-ZLEp2j3jquX7wrPQ7tPOSRAjmMoHhdrsgkuH9Bp/fgNZV7M1eiwAY6fHRGKad6KIldoI+iazMUIm60v11fIHCg=="],
"@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0" } }, "sha512-FbLmz6ujltw8RDUkBzutwIfoV+q9Mu0gLVrfhDAb9INe+jLcaQikiIjFdVwPzpx+bOs6bWTDfylrlI6+Ytxs3Q=="],
"@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-EYZwMpcpoaLRnfhEr+k+MTKS8SKi51TWh1b7bLSy+yHLL0PdbadFsGYZPgzLbZEaq4kUP0asMzXxA+blutjOQQ=="],
"@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-8x/aqR1NckGiC49P02cxuH0wLzbJXvE/v2NnMEFo6h3uWq4ESYL0jTY9vNlFeVIKDyGSzrbteofzzG+yQv0wAQ=="],
"@better-auth/telemetry": ["@better-auth/telemetry@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-JrJyx1ioswEAh8rB7mVxEFIDLl6AK3W3rtqc2MK6BgvcmKveWJ730Eoi/PNvi0b4tFk4kczmuQITm69uMbnTvQ=="],
"@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="],
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
@@ -241,6 +254,10 @@
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.8", "", { "os": "win32", "cpu": "x64" }, "sha512-Exsmf/+42fWVnLMaZHzshukTBxZrSwuuLKFvqhGHJ+mC1AokqieLY/XzAl3jc/CqhXLqLY3RRjkKJ9YnLPcRWg=="],
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
@@ -249,7 +266,9 @@
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
"@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
@@ -321,6 +340,8 @@
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="],
@@ -469,6 +490,10 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"better-auth": ["better-auth@1.6.0", "", { "dependencies": { "@better-auth/core": "1.6.0", "@better-auth/drizzle-adapter": "1.6.0", "@better-auth/kysely-adapter": "1.6.0", "@better-auth/memory-adapter": "1.6.0", "@better-auth/mongo-adapter": "1.6.0", "@better-auth/prisma-adapter": "1.6.0", "@better-auth/telemetry": "1.6.0", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.14", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-reEK4X37w/X0Wi0ZpNSo6w3j9F2tsA7ebWn2AmWTzkceiatkxcadRg9aK+Mirw2PY56GQqX9dBgqBG6XMNU/Zg=="],
"better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
@@ -523,6 +548,8 @@
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"defu": ["defu@6.1.6", "", {}, "sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
@@ -737,7 +764,7 @@
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -755,6 +782,8 @@
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kysely": ["kysely@0.28.15", "", {}, "sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA=="],
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
@@ -809,18 +838,16 @@
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="],
"napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"next": ["next@15.4.10", "", { "dependencies": { "@next/env": "15.4.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.8", "@next/swc-darwin-x64": "15.4.8", "@next/swc-linux-arm64-gnu": "15.4.8", "@next/swc-linux-arm64-musl": "15.4.8", "@next/swc-linux-x64-gnu": "15.4.8", "@next/swc-linux-x64-musl": "15.4.8", "@next/swc-win32-arm64-msvc": "15.4.8", "@next/swc-win32-x64-msvc": "15.4.8", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ=="],
"next-auth": ["next-auth@5.0.0-beta.30", "", { "dependencies": { "@auth/core": "0.41.0" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", "nodemailer": "^7.0.7", "react": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
"oauth4webapi": ["oauth4webapi@3.8.3", "", {}, "sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
@@ -887,10 +914,6 @@
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"preact": ["preact@10.24.3", "", {}, "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="],
"preact-render-to-string": ["preact-render-to-string@6.5.11", "", { "peerDependencies": { "preact": ">=10" } }, "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
@@ -925,6 +948,8 @@
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
@@ -937,6 +962,8 @@
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
@@ -1053,6 +1080,8 @@
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
@@ -1103,8 +1132,6 @@
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"next-auth/@auth/core": ["@auth/core@0.41.0", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^6.0.6", "oauth4webapi": "^3.3.0", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ=="],
"postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],

20
compose.dev.yml Normal file
View File

@@ -0,0 +1,20 @@
services:
postgres:
image: postgres:17
container_name: local-cal-postgres
ports:
- "5432:5432"
environment:
POSTGRES_USER: localcal
POSTGRES_PASSWORD: localcal
POSTGRES_DB: localcal
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U localcal"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:

View File

@@ -3,10 +3,11 @@
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1753831157,
"lastModified": 1775507677,
"narHash": "sha256-gCv9ODisrHjTDtjV/Xru8dtDbrldahRZFShu089/60M=",
"owner": "cachix",
"repo": "devenv",
"rev": "ed23cb144a056b4c34bbe633e275e54785f0b98d",
"rev": "c429c111e25b467c431f9eb70598c70394d56aaa",
"type": "github"
},
"original": {
@@ -19,14 +20,15 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1747046372,
"owner": "edolstra",
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
@@ -40,10 +42,11 @@
]
},
"locked": {
"lastModified": 1750779888,
"lastModified": 1775036584,
"narHash": "sha256-zW0lyy7ZNNT/x8JhzFHBsP2IPx7ATZIPai4FJj12BgU=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
"rev": "4e0eb042b67d863b1b34b3f64d52ceb9cd926735",
"type": "github"
},
"original": {
@@ -61,6 +64,7 @@
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
@@ -73,11 +77,15 @@
}
},
"nixpkgs": {
"inputs": {
"nixpkgs-src": "nixpkgs-src"
},
"locked": {
"lastModified": 1750441195,
"lastModified": 1774287239,
"narHash": "sha256-W3krsWcDwYuA3gPWsFA24YAXxOFUL6iIlT6IknAoNSE=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "0ceffe312871b443929ff3006960d29b120dc627",
"rev": "fa7125ea7f1ae5430010a6e071f68375a39bd24c",
"type": "github"
},
"original": {
@@ -87,17 +95,31 @@
"type": "github"
}
},
"nixpkgs-src": {
"flake": false,
"locked": {
"lastModified": 1773840656,
"narHash": "sha256-9tpvMGFteZnd3gRQZFlRCohVpqooygFuy9yjuyRL2C0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9cf7092bdd603554bd8b63c216e8943cf9b12512",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": [
"git-hooks"
]
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
}

View File

@@ -2,14 +2,9 @@
inputs:
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
# If you're using non-OSS software, you can set allowUnfree to true.
git-hooks:
url: github:cachix/git-hooks.nix
inputs:
nixpkgs:
follows: nixpkgs
allowUnfree: true
# If you're willing to use a package that's vulnerable
# permittedInsecurePackages:
# - "openssl-1.1.1w"
# If you have more than one devenv you can merge them
#imports:
# - ./backend

View File

@@ -1,56 +1 @@
-- Current sql file was generated after introspecting the database
-- If you want to run this migration please uncomment this code before executing migrations
/*
CREATE TABLE "session" (
"sessionToken" text PRIMARY KEY NOT NULL,
"userId" text NOT NULL,
"expires" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" text PRIMARY KEY NOT NULL,
"name" text,
"email" text NOT NULL,
"emailVerified" timestamp,
"image" text
);
--> statement-breakpoint
CREATE TABLE "verificationToken" (
"identifier" text NOT NULL,
"token" text NOT NULL,
"expires" timestamp NOT NULL,
CONSTRAINT "verificationToken_identifier_token_pk" PRIMARY KEY("identifier","token")
);
--> statement-breakpoint
CREATE TABLE "authenticator" (
"credentialID" text NOT NULL,
"userId" text NOT NULL,
"providerAccountId" text NOT NULL,
"credentialPublicKey" text NOT NULL,
"counter" integer NOT NULL,
"credentialDeviceType" text NOT NULL,
"credentialBackedUp" boolean NOT NULL,
"transports" text,
CONSTRAINT "authenticator_userId_credentialID_pk" PRIMARY KEY("credentialID","userId"),
CONSTRAINT "authenticator_credentialID_unique" UNIQUE("credentialID")
);
--> statement-breakpoint
CREATE TABLE "account" (
"userId" text NOT NULL,
"type" text NOT NULL,
"provider" text NOT NULL,
"providerAccountId" text NOT NULL,
"refresh_token" text,
"access_token" text,
"expires_at" text,
"token_type" text,
"scope" text,
"id_token" text,
"session_state" text,
CONSTRAINT "account_provider_providerAccountId_pk" PRIMARY KEY("provider","providerAccountId")
);
--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "authenticator" ADD CONSTRAINT "authenticator_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
*/
-- Baseline snapshot: tables already exist in the database

View File

@@ -0,0 +1,50 @@
BEGIN;
-- Ensure pgcrypto extension is available for UUID generation
CREATE EXTENSION IF NOT EXISTS pgcrypto;
ALTER TABLE "authenticator" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
DROP TABLE "authenticator" CASCADE;--> statement-breakpoint
ALTER TABLE "verificationToken" RENAME TO "verification";--> statement-breakpoint
ALTER TABLE "session" RENAME COLUMN "expires" TO "expiresAt";--> statement-breakpoint
ALTER TABLE "session" RENAME COLUMN "sessionToken" TO "token";--> statement-breakpoint
ALTER TABLE "verification" RENAME COLUMN "token" TO "value";--> statement-breakpoint
ALTER TABLE "verification" RENAME COLUMN "expires" TO "expiresAt";--> statement-breakpoint
ALTER TABLE "account" RENAME COLUMN "providerAccountId" TO "accountId";--> statement-breakpoint
ALTER TABLE "account" RENAME COLUMN "provider" TO "providerId";--> statement-breakpoint
ALTER TABLE "account" RENAME COLUMN "access_token" TO "accessToken";--> statement-breakpoint
ALTER TABLE "account" RENAME COLUMN "refresh_token" TO "refreshToken";--> statement-breakpoint
ALTER TABLE "account" RENAME COLUMN "expires_at" TO "accessTokenExpiresAt";--> statement-breakpoint
ALTER TABLE "account" RENAME COLUMN "id_token" TO "idToken";--> statement-breakpoint
ALTER TABLE "verification" DROP CONSTRAINT "verificationToken_identifier_token_pk";--> statement-breakpoint
ALTER TABLE "account" DROP CONSTRAINT "account_provider_providerAccountId_pk";--> statement-breakpoint
-- Mark OAuth users as verified before type conversion
UPDATE "user" SET "emailVerified" = NOW() WHERE "emailVerified" IS NULL AND id IN (SELECT "userId" FROM account);
ALTER TABLE "user" ALTER COLUMN "emailVerified" SET DATA TYPE boolean USING ("emailVerified" IS NOT NULL);--> statement-breakpoint
ALTER TABLE "session" DROP CONSTRAINT "session_pkey";--> statement-breakpoint
ALTER TABLE "session" ADD COLUMN "id" text PRIMARY KEY NOT NULL DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "session" ADD COLUMN "createdAt" timestamp DEFAULT now();--> statement-breakpoint
ALTER TABLE "session" ADD COLUMN "updatedAt" timestamp DEFAULT now();--> statement-breakpoint
ALTER TABLE "session" ADD COLUMN "ipAddress" text;--> statement-breakpoint
ALTER TABLE "session" ADD COLUMN "userAgent" text;--> statement-breakpoint
ALTER TABLE "user" ADD COLUMN "createdAt" timestamp DEFAULT now();--> statement-breakpoint
ALTER TABLE "user" ADD COLUMN "updatedAt" timestamp DEFAULT now();--> statement-breakpoint
ALTER TABLE "verification" ADD COLUMN "id" text PRIMARY KEY NOT NULL DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "verification" ADD COLUMN "createdAt" timestamp DEFAULT now();--> statement-breakpoint
ALTER TABLE "verification" ADD COLUMN "updatedAt" timestamp DEFAULT now();--> statement-breakpoint
ALTER TABLE "account" ADD COLUMN "id" text PRIMARY KEY NOT NULL DEFAULT gen_random_uuid();--> statement-breakpoint
ALTER TABLE "account" ADD COLUMN "refreshTokenExpiresAt" timestamp;--> statement-breakpoint
ALTER TABLE "account" ADD COLUMN "password" text;--> statement-breakpoint
ALTER TABLE "account" ADD COLUMN "createdAt" timestamp DEFAULT now();--> statement-breakpoint
ALTER TABLE "account" ADD COLUMN "updatedAt" timestamp DEFAULT now();--> statement-breakpoint
ALTER TABLE "account" DROP COLUMN "type";--> statement-breakpoint
ALTER TABLE "account" DROP COLUMN "token_type";--> statement-breakpoint
ALTER TABLE "account" DROP COLUMN "session_state";--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_token_unique" UNIQUE("token");--> statement-breakpoint
ALTER TABLE "user" ADD CONSTRAINT "user_email_unique" UNIQUE("email");
-- Drop the uuid defaults so future inserts rely on app-provided values
ALTER TABLE "session" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
ALTER TABLE "verification" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
ALTER TABLE "account" ALTER COLUMN "id" DROP DEFAULT;
COMMIT;

View File

@@ -0,0 +1,328 @@
{
"id": "69e7666b-0b8c-4658-906d-993870a0b539",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"accountId": {
"name": "accountId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerId": {
"name": "providerId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"accessToken": {
"name": "accessToken",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refreshToken": {
"name": "refreshToken",
"type": "text",
"primaryKey": false,
"notNull": false
},
"accessTokenExpiresAt": {
"name": "accessTokenExpiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refreshTokenExpiresAt": {
"name": "refreshTokenExpiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"idToken": {
"name": "idToken",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"ipAddress": {
"name": "ipAddress",
"type": "text",
"primaryKey": false,
"notNull": false
},
"userAgent": {
"name": "userAgent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"emailVerified": {
"name": "emailVerified",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -8,6 +8,13 @@
"when": 1755586325384,
"tag": "0000_loose_catseye",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1775526538601,
"tag": "0001_great_sentry",
"breakpoints": true
}
]
}

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm/relations";
import { user, session, authenticator, account } from "./schema";
import { user, session, account } from "./schema";
export const sessionRelations = relations(session, ({one}) => ({
user: one(user, {
@@ -10,17 +10,9 @@ export const sessionRelations = relations(session, ({one}) => ({
export const userRelations = relations(user, ({many}) => ({
sessions: many(session),
authenticators: many(authenticator),
accounts: many(account),
}));
export const authenticatorRelations = relations(authenticator, ({one}) => ({
user: one(user, {
fields: [authenticator.userId],
references: [user.id]
}),
}));
export const accountRelations = relations(account, ({one}) => ({
user: one(user, {
fields: [account.userId],

View File

@@ -9,13 +9,13 @@
"lint": "next lint"
},
"dependencies": {
"@auth/drizzle-adapter": "^1.10.0",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"better-auth": "^1.6.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
@@ -26,7 +26,6 @@
"lucide-react": "^0.539.0",
"nanoid": "^5.1.5",
"next": "15.4.10",
"next-auth": "^5.0.0-beta.29",
"next-themes": "^0.4.6",
"pg": "^8.16.3",
"postgres": "^3.4.7",

View File

@@ -1,8 +1,11 @@
import { NextResponse } from "next/server";
import { auth } from "@/auth";
import { headers } from "next/headers";
export async function POST(request: Request) {
const session = await auth();
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session?.user) {
return NextResponse.json(
@@ -13,6 +16,20 @@ export async function POST(request: Request) {
const { prompt } = await request.json();
// Validate prompt input
if (!prompt || typeof prompt !== "string" || prompt.trim().length === 0) {
return NextResponse.json(
{ error: "Prompt is required and must be a non-empty string" },
{ status: 400 }
);
}
if (prompt.length > 2000) {
return NextResponse.json(
{ error: "Prompt must be less than 2000 characters" },
{ status: 400 }
);
}
const systemPrompt = `
You are an assistant that converts natural language into an ARRAY of calendar events.
TypeScript type:

View File

@@ -1,6 +1,19 @@
import { NextResponse } from "next/server";
import { auth } from "@/auth";
import { headers } from "next/headers";
export async function POST(request: Request) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session?.user) {
return NextResponse.json(
{ error: "Authentication required" },
{ status: 401 },
);
}
try {
const { events } = await request.json();
@@ -10,6 +23,12 @@ export async function POST(request: Request) {
{ status: 400 },
);
}
if (events.length > 100) {
return NextResponse.json(
{ error: "Events array must contain 100 or fewer items" },
{ status: 400 },
);
}
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",

View File

@@ -0,0 +1,4 @@
import { auth } from "@/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);

View File

@@ -1,2 +0,0 @@
import { handlers } from "@/auth";
export const { GET, POST } = handlers;

View File

@@ -9,9 +9,14 @@ import { Suspense } from "react"
function Search() {
const searchParams = useSearchParams()
const errorMessage = searchParams.get('error')
// Sanitize error message to prevent XSS
const sanitizedError = errorMessage
? errorMessage.replace(/[<>]/g, '')
: 'An authentication error occurred'
return (<div className="text-center p-3 bg-background rounded-lg">
{errorMessage}
{sanitizedError}
</div>)
}

View File

@@ -1,15 +1,44 @@
import { signIn, auth } from "@/auth"
"use client"
import { signIn, useSession } from "@/lib/auth-client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { redirect } from "next/navigation"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { toast } from "sonner"
export default async function SignInPage() {
const session = await auth()
export default function SignInPage() {
const { data: session, isPending } = useSession()
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
if (session?.user) {
router.push("/")
}
}, [session, router])
const handleSignIn = async () => {
setIsLoading(true)
try {
await signIn.oauth2({
providerId: "authentik",
callbackURL: "/",
})
} catch (_error) {
toast.error("Failed to sign in. Please try again.")
} finally {
setIsLoading(false)
}
}
if (isPending) {
return null
}
// If already signed in, redirect to home
if (session?.user) {
redirect("/")
return null
}
return (
@@ -22,16 +51,9 @@ export default async function SignInPage() {
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<form
action={async () => {
"use server"
await signIn("authentik", { redirectTo: "/" })
}}
>
<Button type="submit" className="w-full" size="lg">
Continue with Authentik
</Button>
</form>
<Button onClick={handleSignIn} className="w-full" size="lg" disabled={isLoading}>
{isLoading ? "Signing in..." : "Continue with Authentik"}
</Button>
<div className="text-center">
<Link href="/" className="text-sm text-muted-foreground hover:underline">

View File

@@ -1,14 +1,29 @@
import { signOut, auth } from "@/auth"
"use client"
import { signOut, useSession } from "@/lib/auth-client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { redirect } from "next/navigation"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
export default async function SignOutPage() {
const session = await auth()
if (!session) {
redirect("/")
export default function SignOutPage() {
const { data: session, isPending } = useSession()
const router = useRouter()
useEffect(() => {
if (!session?.user) {
router.push("/")
}
}, [session, router])
const handleSignOut = async () => {
await signOut()
router.push("/")
}
if (isPending || !session?.user) {
return null
}
return (
@@ -25,19 +40,12 @@ export default async function SignOutPage() {
<div className="text-sm text-muted-foreground">Currently signed in as</div>
<div className="font-medium">{session.user?.name || session.user?.email}</div>
</div>
<div className="grid grid-cols-2 gap-3">
<form
action={async () => {
"use server"
await signOut({ redirectTo: "/" })
}}
>
<Button type="submit" variant="destructive" className="w-full">
Sign Out
</Button>
</form>
<Button onClick={handleSignOut} variant="destructive" className="w-full">
Sign Out
</Button>
<Button variant="outline" asChild>
<Link href="/">Cancel</Link>
</Button>
@@ -46,4 +54,4 @@ export default async function SignOutPage() {
</Card>
</div>
)
}
}

View File

@@ -4,7 +4,6 @@ import "./globals.css";
import { ThemeProvider } from "next-themes";
import { ModeToggle } from "@/components/mode-toggle";
import SignIn from "@/components/sign-in";
import AuthSessionProvider from "@/components/SessionProvider";
import { Toaster } from "@/components/ui/sonner";
import Link from "next/link"
@@ -28,28 +27,26 @@ export default function RootLayout({
<body
className={`${geist.variable} antialiased min-h-screen flex flex-col dark:text-gray-300 --color-background`}
>
<AuthSessionProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<header className="dark:text-white text-gray-900 px-4 py-3 font-bold flex justify-between items-center-safe">
<Link href={"/"}>
<p className={`${magra.variable}`}>
{metadata.title as string || "iCal PWA"}
</p>
</Link>
<div className="flex flex-row gap-2">
<SignIn />
<ModeToggle />
</div>
</header>
<main className="flex-1 p-4">{children}</main>
<Toaster closeButton richColors />
</ThemeProvider>
</AuthSessionProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<header className="dark:text-white text-gray-900 px-4 py-3 font-bold flex justify-between items-center-safe">
<Link href={"/"}>
<p className={`${magra.variable}`}>
{metadata.title as string || "iCal PWA"}
</p>
</Link>
<div className="flex flex-row gap-2">
<SignIn />
<ModeToggle />
</div>
</header>
<main className="flex-1 p-4">{children}</main>
<Toaster closeButton richColors />
</ThemeProvider>
</body>
</html>
);

View File

@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react'
import { nanoid } from 'nanoid'
import { useSession } from 'next-auth/react'
import { useSession } from '@/lib/auth-client'
import { toast } from 'sonner'
import { saveEvent as addEvent, deleteEvent, getEvents as getAllEvents, clearEvents, updateEvent } from '@/lib/events-db'
@@ -44,7 +44,7 @@ export default function HomePage() {
})()
}, [])
const { data: session, status } = useSession()
const { data: session, isPending } = useSession()
const resetForm = () => {
setTitle('')
@@ -256,8 +256,8 @@ export default function HomePage() {
onImport={handleImport}
>
<AIToolbar
session={session}
status={status}
isAuthenticated={!!session?.user}
isPending={isPending}
aiPrompt={aiPrompt}
setAiPrompt={setAiPrompt}
aiLoading={aiLoading}

View File

@@ -1,35 +1,45 @@
import NextAuth, { NextAuthConfig, NextAuthResult } from "next-auth";
import Authentik from "next-auth/providers/authentik";
import type { Provider } from "next-auth/providers";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { genericOAuth } from "better-auth/plugins";
import { db } from "@/db/index";
import * as schema from "@/db/schema";
const providers: Provider[] = [
Authentik({
clientId: process.env.AUTH_AUTHENTIK_CLIENT_ID,
clientSecret: process.env.AUTH_AUTHENTIK_CLIENT_SECRET,
issuer: process.env.AUTH_AUTHENTIK_ISSUER,
// Validate required environment variables
if (!process.env.BETTER_AUTH_SECRET) {
throw new Error("BETTER_AUTH_SECRET is required");
}
if (!process.env.BETTER_AUTH_URL) {
throw new Error("BETTER_AUTH_URL is required");
}
if (!process.env.AUTH_AUTHENTIK_CLIENT_ID) {
throw new Error("AUTH_AUTHENTIK_CLIENT_ID is required");
}
if (!process.env.AUTH_AUTHENTIK_CLIENT_SECRET) {
throw new Error("AUTH_AUTHENTIK_CLIENT_SECRET is required");
}
if (!process.env.AUTH_AUTHENTIK_ISSUER) {
throw new Error("AUTH_AUTHENTIK_ISSUER is required");
}
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
trustedOrigins: [process.env.BETTER_AUTH_URL],
database: drizzleAdapter(db, {
provider: "pg",
schema,
}),
];
export const providerMap = providers.map((provider) => {
if (typeof provider === "function") {
const providerData = provider();
return { id: providerData.id, name: providerData.name };
} else {
return { id: provider.id, name: provider.name };
}
plugins: [
genericOAuth({
config: [
{
providerId: "authentik",
clientId: process.env.AUTH_AUTHENTIK_CLIENT_ID,
clientSecret: process.env.AUTH_AUTHENTIK_CLIENT_SECRET,
discoveryUrl: `${process.env.AUTH_AUTHENTIK_ISSUER}/.well-known/openid-configuration`,
scopes: ["openid", "email", "profile"],
},
],
}),
],
});
const config = {
adapter: DrizzleAdapter(db),
providers,
pages: {
signIn: "/auth/signin",
signOut: "/auth/signout",
error: "/auth/error",
},
trustHost: true,
} satisfies NextAuthConfig;
export const { handlers, signIn, signOut, auth }: NextAuthResult =
NextAuth(config);

View File

@@ -1,12 +0,0 @@
"use client"
import { SessionProvider } from "next-auth/react"
import { ReactNode } from "react"
interface Props {
children: ReactNode
}
export default function AuthSessionProvider({ children }: Props) {
return <SessionProvider>{children}</SessionProvider>
}

View File

@@ -1,11 +1,10 @@
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { Card } from '@/components/ui/card'
import { Session } from 'next-auth'
interface AIToolbarProps {
session: Session | null
status: 'loading' | 'authenticated' | 'unauthenticated'
isAuthenticated: boolean
isPending: boolean
aiPrompt: string
setAiPrompt: (prompt: string) => void
aiLoading: boolean
@@ -16,8 +15,8 @@ interface AIToolbarProps {
}
export const AIToolbar = ({
session,
status,
isAuthenticated,
isPending,
aiPrompt,
setAiPrompt,
aiLoading,
@@ -28,17 +27,15 @@ export const AIToolbar = ({
}: AIToolbarProps) => {
return (
<>
{/* AI Toolbar */}
{status === "loading" ? (
{isPending ? (
<div className='mb-4 p-4 text-center animate-pulse bg-muted'>Loading...</div>
) : (
<div>
{session?.user ? (
{isAuthenticated ? (
<div className="flex flex-col sm:flex-row gap-4 mb-4 items-start">
<div className='w-full'>
<Textarea
className="wrap-anywhere field-sizing-content resize-none w-full min-h-[2.5rem] max-h-64 overflow-y-auto sm:overflow-y-visible px-3 py-2 scroll-p-8 placeholder:italic"
// Band-aid for scrollbar clipping out of the box
style={{ clipPath: "inset(0 round 1rem)" }}
placeholder='Describe event for AI to create'
value={aiPrompt}

View File

@@ -1,20 +1,24 @@
"use client"
import { signOut, useSession } from "next-auth/react"
import { signOut, useSession } from "@/lib/auth-client"
import { Button } from "@/components/ui/button"
import { useRouter } from "next/navigation"
import { toast } from "sonner"
export default function SignIn() {
const { data: session, status } = useSession()
const { data: session, isPending } = useSession()
const router = useRouter()
const handleSignOut = async () => {
await signOut({ redirect: false })
router.push("/")
router.refresh()
try {
await signOut()
router.push("/")
} catch (_error) {
toast.error("Failed to sign out. Please try again.")
}
}
if (status === "loading") {
if (isPending) {
return <div className="h-8 w-16 bg-muted animate-pulse rounded"></div>
}

View File

@@ -1,55 +1,47 @@
import { pgTable, text, timestamp, integer, boolean, primaryKey } from 'drizzle-orm/pg-core';
import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core';
export const users = pgTable('user', {
export const user = pgTable('user', {
id: text('id').primaryKey(),
name: text('name'),
email: text('email').notNull(),
emailVerified: timestamp('emailVerified', { mode: 'string' }),
email: text('email').notNull().unique(),
emailVerified: boolean('emailVerified').default(false),
image: text('image'),
createdAt: timestamp('createdAt').defaultNow(),
updatedAt: timestamp('updatedAt').defaultNow(),
});
export const accounts = pgTable('account', {
userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
type: text('type').notNull(),
provider: text('provider').notNull(),
providerAccountId: text('providerAccountId').notNull(),
refresh_token: text('refresh_token'),
access_token: text('access_token'),
expires_at: text('expires_at'),
token_type: text('token_type'),
export const session = pgTable('session', {
id: text('id').primaryKey(),
expiresAt: timestamp('expiresAt').notNull(),
token: text('token').notNull().unique(),
createdAt: timestamp('createdAt').defaultNow(),
updatedAt: timestamp('updatedAt').defaultNow(),
ipAddress: text('ipAddress'),
userAgent: text('userAgent'),
userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }),
});
export const account = pgTable('account', {
id: text('id').primaryKey(),
accountId: text('accountId').notNull(),
providerId: text('providerId').notNull(),
userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('accessToken'),
refreshToken: text('refreshToken'),
accessTokenExpiresAt: timestamp('accessTokenExpiresAt'),
refreshTokenExpiresAt: timestamp('refreshTokenExpiresAt'),
scope: text('scope'),
id_token: text('id_token'),
session_state: text('session_state'),
}, (account) => ({
compoundKey: primaryKey({ columns: [account.provider, account.providerAccountId] })
}));
export const sessions = pgTable('session', {
sessionToken: text().primaryKey().notNull(),
userId: text().notNull().references(() => users.id, { onDelete: 'cascade' }),
expires: timestamp({ mode: 'string' }).notNull(),
idToken: text('idToken'),
password: text('password'),
createdAt: timestamp('createdAt').defaultNow(),
updatedAt: timestamp('updatedAt').defaultNow(),
});
export const verificationTokens = pgTable('verificationToken', {
export const verification = pgTable('verification', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
token: text('token').notNull(),
expires: timestamp('expires', { mode: 'string' }).notNull(),
}, (vt) => ({
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] })
}));
export const authenticators = pgTable('authenticator', {
credentialID: text('credentialID').notNull().unique(),
userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
providerAccountId: text('providerAccountId').notNull(),
credentialPublicKey: text('credentialPublicKey').notNull(),
counter: integer('counter').notNull(),
credentialDeviceType: text('credentialDeviceType').notNull(),
credentialBackedUp: boolean('credentialBackedUp').notNull(),
transports: text('transports'),
}, (authenticator) => ({
compositePK: primaryKey({
columns: [authenticator.credentialID, authenticator.userId],
name: "authenticator_userId_credentialID_pk"
})
}));
value: text('value').notNull(),
expiresAt: timestamp('expiresAt').notNull(),
createdAt: timestamp('createdAt').defaultNow(),
updatedAt: timestamp('updatedAt').defaultNow(),
});

8
src/lib/auth-client.ts Normal file
View File

@@ -0,0 +1,8 @@
import { createAuthClient } from "better-auth/react";
import { genericOAuthClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [genericOAuthClient()],
});
export const { useSession, signIn, signOut } = authClient;