ultra-dex 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -10
- package/assets/cursor-rules/18-webhooks.mdc +52 -0
- package/assets/cursor-rules/19-cron-jobs.mdc +61 -0
- package/assets/cursor-rules/20-rate-limiting.mdc +59 -0
- package/assets/cursor-rules/21-logging.mdc +58 -0
- package/assets/cursor-rules/22-caching.mdc +70 -0
- package/assets/cursor-rules/23-forms.mdc +67 -0
- package/assets/cursor-rules/24-file-upload.mdc +78 -0
- package/assets/cursor-rules/25-websockets.mdc +100 -0
- package/bin/ultra-dex.js +40 -2
- package/lib/commands/agents.js +185 -4
- package/lib/commands/banner.js +1 -1
- package/lib/commands/cloud.js +780 -0
- package/lib/commands/exec.js +434 -0
- package/lib/commands/github.js +475 -0
- package/lib/commands/monitoring.js +207 -0
- package/lib/commands/run.js +39 -5
- package/lib/commands/scaffold.js +25 -4
- package/lib/commands/search.js +477 -0
- package/lib/commands/serve.js +1 -1
- package/lib/commands/state.js +1 -1
- package/lib/commands/swarm.js +78 -34
- package/lib/commands/sync.js +189 -0
- package/lib/mcp/client.js +521 -0
- package/lib/mcp/graph.js +14 -5
- package/lib/mcp/server.js +1 -1
- package/lib/mcp/tools.js +47 -7
- package/lib/providers/agent-sdk.js +630 -0
- package/lib/providers/anthropic-agents.js +580 -0
- package/lib/providers/base.js +36 -9
- package/lib/providers/claude.js +146 -89
- package/lib/providers/index.js +6 -1
- package/lib/providers/langchain.js +315 -0
- package/lib/providers/openai-assistants.js +484 -0
- package/lib/quality/scanner.js +47 -24
- package/lib/swarm/index.js +83 -29
- package/lib/swarm/protocol.js +48 -5
- package/lib/swarm/tiers.js +41 -7
- package/lib/utils/browser.js +373 -0
- package/lib/utils/config-manager.js +446 -0
- package/lib/utils/error-recovery.js +473 -0
- package/lib/utils/graph.js +25 -6
- package/lib/utils/interactive-mode.js +417 -0
- package/lib/utils/monitoring.js +413 -0
- package/lib/utils/version-display.js +1 -1
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -2,23 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
> Scaffold Ultra-Dex projects from the command line, now with **AI-powered plan generation** and **God Mode** autonomous agents.
|
|
4
4
|
|
|
5
|
-
## What's New in v3.
|
|
5
|
+
## What's New in v3.4.0 (Survival Mode)
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
+
# 🧠 Auto-sync CONTEXT.md from codebase (eliminates manual updates)
|
|
9
|
+
npx ultra-dex sync --brain
|
|
10
|
+
|
|
11
|
+
# 🐳 Execute code in Docker sandbox
|
|
12
|
+
npx ultra-dex exec "console.log('safe!')" --lang javascript
|
|
13
|
+
|
|
14
|
+
# 🔍 Semantic search with vector embeddings
|
|
15
|
+
npx ultra-dex search "user authentication" --index
|
|
16
|
+
|
|
17
|
+
# 🐙 GitHub integration (issues → tasks, auto-PR)
|
|
18
|
+
npx ultra-dex github sync
|
|
19
|
+
npx ultra-dex github pr --from-swarm
|
|
20
|
+
|
|
8
21
|
# 🤖 Autonomous agent swarms with parallel execution
|
|
9
22
|
npx ultra-dex swarm "Build user authentication" --parallel
|
|
10
23
|
|
|
11
24
|
# 🔄 Start the Active Kernel (MCP + WebSocket + Dashboard)
|
|
12
25
|
npx ultra-dex serve
|
|
13
26
|
|
|
14
|
-
#
|
|
15
|
-
npx ultra-dex
|
|
16
|
-
|
|
17
|
-
# 🖥️ Live Dashboard with Brain Visualization
|
|
18
|
-
npx ultra-dex dashboard
|
|
19
|
-
|
|
20
|
-
# 🛠️ Self-Healing CI/CD Monitor
|
|
21
|
-
npx ultra-dex ci-monitor --port 3003
|
|
27
|
+
# 📊 Health monitoring and metrics
|
|
28
|
+
npx ultra-dex health
|
|
29
|
+
npx ultra-dex metrics
|
|
22
30
|
```
|
|
23
31
|
|
|
24
32
|
## First 10 Minutes
|
|
@@ -156,10 +164,37 @@ npx ultra-dex check
|
|
|
156
164
|
ANTHROPIC_API_KEY=sk-ant-... # Claude (recommended for complex tasks)
|
|
157
165
|
OPENAI_API_KEY=sk-... # OpenAI
|
|
158
166
|
GOOGLE_AI_KEY=... # Gemini
|
|
167
|
+
OLLAMA_HOST=http://localhost:11434 # Ollama (local AI)
|
|
159
168
|
ULTRA_DEX_DEFAULT_PROVIDER=claude # Default AI provider
|
|
160
169
|
```
|
|
161
170
|
|
|
162
|
-
##
|
|
171
|
+
## Local AI with Ollama
|
|
172
|
+
|
|
173
|
+
Ultra-Dex supports Ollama for fully local, private AI operations:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# 1. Install Ollama (https://ollama.ai)
|
|
177
|
+
curl -fsSL https://ollama.ai/install.sh | sh
|
|
178
|
+
|
|
179
|
+
# 2. Pull a model
|
|
180
|
+
ollama pull llama3.2
|
|
181
|
+
ollama pull codellama
|
|
182
|
+
|
|
183
|
+
# 3. Use with Ultra-Dex
|
|
184
|
+
npx ultra-dex generate "Your idea" --provider ollama --model codellama
|
|
185
|
+
|
|
186
|
+
# For agent swarms with local AI
|
|
187
|
+
npx ultra-dex swarm "Build user auth" --provider ollama
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Supported Ollama Models:**
|
|
191
|
+
- `llama3.2` - General tasks
|
|
192
|
+
- `codellama` - Code generation (recommended)
|
|
193
|
+
- `mistral` - Fast, efficient
|
|
194
|
+
- `mixtral` - Best quality for local
|
|
195
|
+
- Any model available via `ollama list`
|
|
196
|
+
|
|
197
|
+
## All Commands (44+)
|
|
163
198
|
|
|
164
199
|
| Command | Description |
|
|
165
200
|
|---------|-------------|
|
|
@@ -201,6 +236,13 @@ ULTRA_DEX_DEFAULT_PROVIDER=claude # Default AI provider
|
|
|
201
236
|
| `pack` | Bundle agents and rules |
|
|
202
237
|
| `run` | Execute agent task |
|
|
203
238
|
| `diff` | Compare plan vs implemented code |
|
|
239
|
+
| `exec` | **NEW** Execute code in Docker sandbox |
|
|
240
|
+
| `search` | **NEW** Semantic codebase search |
|
|
241
|
+
| `github` | **NEW** GitHub sync (issues, PRs) |
|
|
242
|
+
| `cloud` | **NEW** Team collaboration server |
|
|
243
|
+
| `metrics` | **NEW** Show CLI metrics |
|
|
244
|
+
| `health` | **NEW** System health check |
|
|
245
|
+
| `debug` | **NEW** Debug information |
|
|
204
246
|
|
|
205
247
|
## Links
|
|
206
248
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Webhook Implementation Rules
|
|
2
|
+
|
|
3
|
+
## Webhook Endpoint Design
|
|
4
|
+
- Use POST method for incoming webhooks
|
|
5
|
+
- Validate webhook signatures (HMAC-SHA256)
|
|
6
|
+
- Return 200 immediately, process async
|
|
7
|
+
- Implement idempotency with event IDs
|
|
8
|
+
|
|
9
|
+
## Security
|
|
10
|
+
```typescript
|
|
11
|
+
// Verify webhook signature
|
|
12
|
+
async function verifyWebhook(req: Request, secret: string) {
|
|
13
|
+
const signature = req.headers.get('x-signature-256');
|
|
14
|
+
const body = await req.text();
|
|
15
|
+
const expected = crypto
|
|
16
|
+
.createHmac('sha256', secret)
|
|
17
|
+
.update(body)
|
|
18
|
+
.digest('hex');
|
|
19
|
+
return crypto.timingSafeEqual(
|
|
20
|
+
Buffer.from(signature || ''),
|
|
21
|
+
Buffer.from(`sha256=${expected}`)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Retry Strategy
|
|
27
|
+
- Implement exponential backoff (1s, 2s, 4s, 8s, 16s)
|
|
28
|
+
- Max 5 retry attempts
|
|
29
|
+
- Store failed webhooks for manual retry
|
|
30
|
+
- Log all webhook attempts with correlation IDs
|
|
31
|
+
|
|
32
|
+
## Event Processing
|
|
33
|
+
```typescript
|
|
34
|
+
// Idempotent webhook handler
|
|
35
|
+
async function handleWebhook(event: WebhookEvent) {
|
|
36
|
+
const existing = await db.webhookEvent.findUnique({
|
|
37
|
+
where: { eventId: event.id }
|
|
38
|
+
});
|
|
39
|
+
if (existing) return { status: 'duplicate' };
|
|
40
|
+
|
|
41
|
+
await db.$transaction([
|
|
42
|
+
db.webhookEvent.create({ data: { eventId: event.id } }),
|
|
43
|
+
processEvent(event)
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Outbound Webhooks
|
|
49
|
+
- Queue outbound webhooks with job processor
|
|
50
|
+
- Include timestamp and signature in headers
|
|
51
|
+
- Support webhook URL validation before saving
|
|
52
|
+
- Implement circuit breaker for failing endpoints
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Cron & Scheduled Jobs Rules
|
|
2
|
+
|
|
3
|
+
## Job Scheduling Patterns
|
|
4
|
+
- Use cron expressions for recurring jobs
|
|
5
|
+
- Store job definitions in database, not code
|
|
6
|
+
- Implement job locking for distributed systems
|
|
7
|
+
- Track job execution history
|
|
8
|
+
|
|
9
|
+
## Implementation
|
|
10
|
+
```typescript
|
|
11
|
+
// Next.js API route with Vercel Cron
|
|
12
|
+
// vercel.json: { "crons": [{ "path": "/api/cron/daily", "schedule": "0 0 * * *" }] }
|
|
13
|
+
|
|
14
|
+
export async function GET(req: Request) {
|
|
15
|
+
// Verify cron secret
|
|
16
|
+
const authHeader = req.headers.get('authorization');
|
|
17
|
+
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
18
|
+
return new Response('Unauthorized', { status: 401 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Execute with timeout protection
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timeout = setTimeout(() => controller.abort(), 55000);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await runDailyTasks({ signal: controller.signal });
|
|
27
|
+
return Response.json({ success: true });
|
|
28
|
+
} finally {
|
|
29
|
+
clearTimeout(timeout);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Job Locking
|
|
35
|
+
```typescript
|
|
36
|
+
// Prevent concurrent execution
|
|
37
|
+
async function withJobLock(jobName: string, fn: () => Promise<void>) {
|
|
38
|
+
const lock = await db.jobLock.create({
|
|
39
|
+
data: {
|
|
40
|
+
name: jobName,
|
|
41
|
+
lockedAt: new Date(),
|
|
42
|
+
expiresAt: new Date(Date.now() + 60000)
|
|
43
|
+
}
|
|
44
|
+
}).catch(() => null);
|
|
45
|
+
|
|
46
|
+
if (!lock) return { skipped: true };
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await fn();
|
|
50
|
+
return { success: true };
|
|
51
|
+
} finally {
|
|
52
|
+
await db.jobLock.delete({ where: { name: jobName } });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Monitoring
|
|
58
|
+
- Log job start/end with duration
|
|
59
|
+
- Alert on job failures or timeouts
|
|
60
|
+
- Track job queue depth
|
|
61
|
+
- Implement dead letter queue for failed jobs
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Rate Limiting Rules
|
|
2
|
+
|
|
3
|
+
## Strategy Selection
|
|
4
|
+
- Token bucket for API endpoints
|
|
5
|
+
- Sliding window for auth attempts
|
|
6
|
+
- Fixed window for simple limits
|
|
7
|
+
- Leaky bucket for smoothing bursts
|
|
8
|
+
|
|
9
|
+
## Implementation
|
|
10
|
+
```typescript
|
|
11
|
+
// Redis-based rate limiter
|
|
12
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
13
|
+
import { Redis } from '@upstash/redis';
|
|
14
|
+
|
|
15
|
+
const ratelimit = new Ratelimit({
|
|
16
|
+
redis: Redis.fromEnv(),
|
|
17
|
+
limiter: Ratelimit.slidingWindow(10, '10 s'),
|
|
18
|
+
analytics: true,
|
|
19
|
+
prefix: 'api',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Middleware
|
|
23
|
+
export async function rateLimitMiddleware(req: Request) {
|
|
24
|
+
const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1';
|
|
25
|
+
const { success, limit, reset, remaining } = await ratelimit.limit(ip);
|
|
26
|
+
|
|
27
|
+
if (!success) {
|
|
28
|
+
return new Response('Rate limit exceeded', {
|
|
29
|
+
status: 429,
|
|
30
|
+
headers: {
|
|
31
|
+
'X-RateLimit-Limit': limit.toString(),
|
|
32
|
+
'X-RateLimit-Remaining': remaining.toString(),
|
|
33
|
+
'X-RateLimit-Reset': reset.toString(),
|
|
34
|
+
'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Tiered Limits
|
|
42
|
+
```typescript
|
|
43
|
+
const RATE_LIMITS = {
|
|
44
|
+
free: { requests: 100, window: '1 h' },
|
|
45
|
+
pro: { requests: 1000, window: '1 h' },
|
|
46
|
+
enterprise: { requests: 10000, window: '1 h' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
async function getRateLimit(userId: string) {
|
|
50
|
+
const user = await getUser(userId);
|
|
51
|
+
return RATE_LIMITS[user.plan];
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Headers to Return
|
|
56
|
+
- `X-RateLimit-Limit`: Total allowed requests
|
|
57
|
+
- `X-RateLimit-Remaining`: Requests left in window
|
|
58
|
+
- `X-RateLimit-Reset`: Unix timestamp when limit resets
|
|
59
|
+
- `Retry-After`: Seconds until retry (on 429)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Structured Logging Rules
|
|
2
|
+
|
|
3
|
+
## Log Levels
|
|
4
|
+
- ERROR: System failures, requires immediate action
|
|
5
|
+
- WARN: Unexpected but recoverable situations
|
|
6
|
+
- INFO: Business events, request lifecycle
|
|
7
|
+
- DEBUG: Detailed diagnostic information
|
|
8
|
+
|
|
9
|
+
## Structured Format
|
|
10
|
+
```typescript
|
|
11
|
+
import pino from 'pino';
|
|
12
|
+
|
|
13
|
+
const logger = pino({
|
|
14
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
15
|
+
formatters: {
|
|
16
|
+
level: (label) => ({ level: label }),
|
|
17
|
+
},
|
|
18
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
19
|
+
base: {
|
|
20
|
+
service: 'ultra-dex',
|
|
21
|
+
version: process.env.VERSION,
|
|
22
|
+
env: process.env.NODE_ENV,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Usage
|
|
27
|
+
logger.info({ userId, action: 'login' }, 'User logged in');
|
|
28
|
+
logger.error({ err, requestId }, 'Payment failed');
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Request Context
|
|
32
|
+
```typescript
|
|
33
|
+
// Add request ID to all logs
|
|
34
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
35
|
+
|
|
36
|
+
const requestContext = new AsyncLocalStorage<{ requestId: string }>();
|
|
37
|
+
|
|
38
|
+
export function withRequestId(req: Request, fn: () => Promise<Response>) {
|
|
39
|
+
const requestId = req.headers.get('x-request-id') || crypto.randomUUID();
|
|
40
|
+
return requestContext.run({ requestId }, fn);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getRequestId() {
|
|
44
|
+
return requestContext.getStore()?.requestId;
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## What to Log
|
|
49
|
+
- Request: method, path, user, duration, status
|
|
50
|
+
- Errors: stack, context, correlation ID
|
|
51
|
+
- Business: user actions, state changes, payments
|
|
52
|
+
- Never log: passwords, tokens, PII, credit cards
|
|
53
|
+
|
|
54
|
+
## Log Aggregation
|
|
55
|
+
- Use JSON format for machine parsing
|
|
56
|
+
- Send to centralized service (Datadog, Logtail, Axiom)
|
|
57
|
+
- Set up alerts for error rate spikes
|
|
58
|
+
- Implement log sampling for high-volume endpoints
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Caching Strategy Rules
|
|
2
|
+
|
|
3
|
+
## Cache Layers
|
|
4
|
+
1. Browser cache (Cache-Control headers)
|
|
5
|
+
2. CDN/Edge cache (Vercel, Cloudflare)
|
|
6
|
+
3. Application cache (Redis, in-memory)
|
|
7
|
+
4. Database cache (query cache, materialized views)
|
|
8
|
+
|
|
9
|
+
## HTTP Caching
|
|
10
|
+
```typescript
|
|
11
|
+
// Next.js cache headers
|
|
12
|
+
export async function GET() {
|
|
13
|
+
const data = await fetchData();
|
|
14
|
+
|
|
15
|
+
return Response.json(data, {
|
|
16
|
+
headers: {
|
|
17
|
+
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Static pages
|
|
23
|
+
export const revalidate = 3600; // ISR: revalidate every hour
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Redis Caching
|
|
27
|
+
```typescript
|
|
28
|
+
import { Redis } from '@upstash/redis';
|
|
29
|
+
|
|
30
|
+
const redis = Redis.fromEnv();
|
|
31
|
+
|
|
32
|
+
async function getCached<T>(
|
|
33
|
+
key: string,
|
|
34
|
+
fetcher: () => Promise<T>,
|
|
35
|
+
ttl = 3600
|
|
36
|
+
): Promise<T> {
|
|
37
|
+
const cached = await redis.get<T>(key);
|
|
38
|
+
if (cached) return cached;
|
|
39
|
+
|
|
40
|
+
const fresh = await fetcher();
|
|
41
|
+
await redis.set(key, fresh, { ex: ttl });
|
|
42
|
+
return fresh;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Usage
|
|
46
|
+
const user = await getCached(
|
|
47
|
+
`user:${userId}`,
|
|
48
|
+
() => db.user.findUnique({ where: { id: userId } }),
|
|
49
|
+
300
|
|
50
|
+
);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Cache Invalidation
|
|
54
|
+
```typescript
|
|
55
|
+
// Event-driven invalidation
|
|
56
|
+
async function updateUser(userId: string, data: UpdateData) {
|
|
57
|
+
await db.user.update({ where: { id: userId }, data });
|
|
58
|
+
|
|
59
|
+
// Invalidate related caches
|
|
60
|
+
await redis.del(`user:${userId}`);
|
|
61
|
+
await redis.del(`user:${userId}:profile`);
|
|
62
|
+
await redis.del(`team:${data.teamId}:members`);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Cache Patterns
|
|
67
|
+
- Cache-aside: App manages cache
|
|
68
|
+
- Write-through: Write to cache + DB
|
|
69
|
+
- Write-behind: Write to cache, async DB
|
|
70
|
+
- Read-through: Cache fetches on miss
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Form Handling Rules
|
|
2
|
+
|
|
3
|
+
## Server Actions (Next.js 15)
|
|
4
|
+
```typescript
|
|
5
|
+
'use server';
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { revalidatePath } from 'next/cache';
|
|
9
|
+
|
|
10
|
+
const schema = z.object({
|
|
11
|
+
email: z.string().email(),
|
|
12
|
+
name: z.string().min(2).max(100),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export async function createUser(formData: FormData) {
|
|
16
|
+
const parsed = schema.safeParse({
|
|
17
|
+
email: formData.get('email'),
|
|
18
|
+
name: formData.get('name'),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!parsed.success) {
|
|
22
|
+
return { error: parsed.error.flatten().fieldErrors };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await db.user.create({ data: parsed.data });
|
|
26
|
+
revalidatePath('/users');
|
|
27
|
+
return { success: true };
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Client Form Component
|
|
32
|
+
```tsx
|
|
33
|
+
'use client';
|
|
34
|
+
|
|
35
|
+
import { useActionState } from 'react';
|
|
36
|
+
import { createUser } from './actions';
|
|
37
|
+
|
|
38
|
+
export function CreateUserForm() {
|
|
39
|
+
const [state, formAction, pending] = useActionState(createUser, null);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<form action={formAction}>
|
|
43
|
+
<input name="email" type="email" required />
|
|
44
|
+
{state?.error?.email && <p className="error">{state.error.email}</p>}
|
|
45
|
+
|
|
46
|
+
<input name="name" required />
|
|
47
|
+
{state?.error?.name && <p className="error">{state.error.name}</p>}
|
|
48
|
+
|
|
49
|
+
<button disabled={pending}>
|
|
50
|
+
{pending ? 'Creating...' : 'Create User'}
|
|
51
|
+
</button>
|
|
52
|
+
</form>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Validation
|
|
58
|
+
- Client: HTML5 validation + real-time feedback
|
|
59
|
+
- Server: Always re-validate with Zod
|
|
60
|
+
- Sanitize: Strip HTML, trim whitespace
|
|
61
|
+
- Rate limit: Form submissions per IP
|
|
62
|
+
|
|
63
|
+
## File Uploads
|
|
64
|
+
- Use presigned URLs for large files
|
|
65
|
+
- Validate file type on both client and server
|
|
66
|
+
- Set maximum file size limits
|
|
67
|
+
- Scan uploads for malware in production
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# File Upload Rules
|
|
2
|
+
|
|
3
|
+
## Presigned URL Pattern
|
|
4
|
+
```typescript
|
|
5
|
+
// Server action to get upload URL
|
|
6
|
+
'use server';
|
|
7
|
+
|
|
8
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
9
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
10
|
+
|
|
11
|
+
const s3 = new S3Client({ region: process.env.AWS_REGION });
|
|
12
|
+
|
|
13
|
+
export async function getUploadUrl(filename: string, contentType: string) {
|
|
14
|
+
const key = `uploads/${crypto.randomUUID()}/${filename}`;
|
|
15
|
+
|
|
16
|
+
const command = new PutObjectCommand({
|
|
17
|
+
Bucket: process.env.S3_BUCKET,
|
|
18
|
+
Key: key,
|
|
19
|
+
ContentType: contentType,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
|
|
23
|
+
return { url, key };
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Client Upload
|
|
28
|
+
```typescript
|
|
29
|
+
async function uploadFile(file: File) {
|
|
30
|
+
// 1. Get presigned URL
|
|
31
|
+
const { url, key } = await getUploadUrl(file.name, file.type);
|
|
32
|
+
|
|
33
|
+
// 2. Upload directly to S3
|
|
34
|
+
await fetch(url, {
|
|
35
|
+
method: 'PUT',
|
|
36
|
+
body: file,
|
|
37
|
+
headers: { 'Content-Type': file.type },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 3. Save reference to database
|
|
41
|
+
await saveFileReference(key);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Validation
|
|
46
|
+
```typescript
|
|
47
|
+
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
|
|
48
|
+
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
|
49
|
+
|
|
50
|
+
function validateFile(file: File) {
|
|
51
|
+
if (!ALLOWED_TYPES.includes(file.type)) {
|
|
52
|
+
throw new Error('Invalid file type');
|
|
53
|
+
}
|
|
54
|
+
if (file.size > MAX_SIZE) {
|
|
55
|
+
throw new Error('File too large');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Image Processing
|
|
61
|
+
```typescript
|
|
62
|
+
// Process uploaded images
|
|
63
|
+
import sharp from 'sharp';
|
|
64
|
+
|
|
65
|
+
async function processImage(buffer: Buffer) {
|
|
66
|
+
return sharp(buffer)
|
|
67
|
+
.resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
|
|
68
|
+
.webp({ quality: 80 })
|
|
69
|
+
.toBuffer();
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Security
|
|
74
|
+
- Validate MIME type server-side (don't trust Content-Type)
|
|
75
|
+
- Generate random filenames to prevent enumeration
|
|
76
|
+
- Serve uploads from separate domain/CDN
|
|
77
|
+
- Set Content-Disposition: attachment for downloads
|
|
78
|
+
- Scan files with antivirus in production
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# WebSocket & Real-time Rules
|
|
2
|
+
|
|
3
|
+
## Connection Management
|
|
4
|
+
```typescript
|
|
5
|
+
// Server setup with ws
|
|
6
|
+
import { WebSocketServer } from 'ws';
|
|
7
|
+
|
|
8
|
+
const wss = new WebSocketServer({ port: 8080 });
|
|
9
|
+
const clients = new Map<string, WebSocket>();
|
|
10
|
+
|
|
11
|
+
wss.on('connection', (ws, req) => {
|
|
12
|
+
const userId = authenticateFromHeaders(req);
|
|
13
|
+
if (!userId) return ws.close(4001, 'Unauthorized');
|
|
14
|
+
|
|
15
|
+
clients.set(userId, ws);
|
|
16
|
+
|
|
17
|
+
ws.on('message', (data) => handleMessage(userId, data));
|
|
18
|
+
ws.on('close', () => clients.delete(userId));
|
|
19
|
+
ws.on('error', (err) => console.error('WS error:', err));
|
|
20
|
+
|
|
21
|
+
// Heartbeat
|
|
22
|
+
ws.isAlive = true;
|
|
23
|
+
ws.on('pong', () => { ws.isAlive = true; });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Ping interval
|
|
27
|
+
setInterval(() => {
|
|
28
|
+
wss.clients.forEach((ws) => {
|
|
29
|
+
if (!ws.isAlive) return ws.terminate();
|
|
30
|
+
ws.isAlive = false;
|
|
31
|
+
ws.ping();
|
|
32
|
+
});
|
|
33
|
+
}, 30000);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Message Protocol
|
|
37
|
+
```typescript
|
|
38
|
+
interface WSMessage {
|
|
39
|
+
type: string;
|
|
40
|
+
payload: unknown;
|
|
41
|
+
timestamp: number;
|
|
42
|
+
correlationId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function handleMessage(userId: string, data: Buffer) {
|
|
46
|
+
const message: WSMessage = JSON.parse(data.toString());
|
|
47
|
+
|
|
48
|
+
switch (message.type) {
|
|
49
|
+
case 'subscribe':
|
|
50
|
+
subscribeToChannel(userId, message.payload.channel);
|
|
51
|
+
break;
|
|
52
|
+
case 'unsubscribe':
|
|
53
|
+
unsubscribeFromChannel(userId, message.payload.channel);
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
sendError(userId, 'Unknown message type');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Broadcasting
|
|
62
|
+
```typescript
|
|
63
|
+
// Pub/sub with Redis for horizontal scaling
|
|
64
|
+
import { Redis } from 'ioredis';
|
|
65
|
+
|
|
66
|
+
const pub = new Redis(process.env.REDIS_URL);
|
|
67
|
+
const sub = new Redis(process.env.REDIS_URL);
|
|
68
|
+
|
|
69
|
+
sub.subscribe('notifications');
|
|
70
|
+
sub.on('message', (channel, message) => {
|
|
71
|
+
const { userIds, data } = JSON.parse(message);
|
|
72
|
+
userIds.forEach((userId: string) => {
|
|
73
|
+
clients.get(userId)?.send(JSON.stringify(data));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export function broadcast(userIds: string[], data: unknown) {
|
|
78
|
+
pub.publish('notifications', JSON.stringify({ userIds, data }));
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Client Reconnection
|
|
83
|
+
```typescript
|
|
84
|
+
function createReconnectingSocket(url: string) {
|
|
85
|
+
let ws: WebSocket;
|
|
86
|
+
let reconnectDelay = 1000;
|
|
87
|
+
|
|
88
|
+
function connect() {
|
|
89
|
+
ws = new WebSocket(url);
|
|
90
|
+
ws.onopen = () => { reconnectDelay = 1000; };
|
|
91
|
+
ws.onclose = () => {
|
|
92
|
+
setTimeout(connect, reconnectDelay);
|
|
93
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
connect();
|
|
98
|
+
return { send: (data: unknown) => ws.send(JSON.stringify(data)) };
|
|
99
|
+
}
|
|
100
|
+
```
|