mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-04 11:47:37 +00:00
config(git,claude,ssh): templated gitconfig; claude settings/commands/hooks/skills; ssh config
gitconfig: name/email from chezmoi data, gh credential helper by PATH lookup (portable to darwin). claude: no credentials, history, projects, sessions, or plugin caches. ssh config as private_ (0600). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3c691e8dc5
commit
20891f3910
476 files changed
+59086
-7
No files matched your search
@@ -0,0 +1,346 @@
|
||||
# Cloudflare Workers Best Practices
|
||||
|
||||
High-level guidance for Workers that invoke Durable Objects.
|
||||
|
||||
## Wrangler Configuration
|
||||
|
||||
### wrangler.jsonc (Recommended)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "my-worker",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2024-12-01",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{ "name": "CHAT_ROOM", "class_name": "ChatRoom" },
|
||||
{ "name": "USER_SESSION", "class_name": "UserSession" }
|
||||
]
|
||||
},
|
||||
|
||||
"migrations": [
|
||||
{ "tag": "v1", "new_sqlite_classes": ["ChatRoom", "UserSession"] }
|
||||
],
|
||||
|
||||
// Environment variables
|
||||
"vars": {
|
||||
"ENVIRONMENT": "production"
|
||||
},
|
||||
|
||||
// KV namespaces
|
||||
"kv_namespaces": [
|
||||
{ "binding": "CONFIG", "id": "abc123" }
|
||||
],
|
||||
|
||||
// R2 buckets
|
||||
"r2_buckets": [
|
||||
{ "binding": "UPLOADS", "bucket_name": "my-uploads" }
|
||||
],
|
||||
|
||||
// D1 databases
|
||||
"d1_databases": [
|
||||
{ "binding": "DB", "database_id": "xyz789" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### wrangler.toml (Alternative)
|
||||
|
||||
```toml
|
||||
name = "my-worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-12-01"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
[[durable_objects.bindings]]
|
||||
name = "CHAT_ROOM"
|
||||
class_name = "ChatRoom"
|
||||
|
||||
[[migrations]]
|
||||
tag = "v1"
|
||||
new_sqlite_classes = ["ChatRoom"]
|
||||
|
||||
[vars]
|
||||
ENVIRONMENT = "production"
|
||||
```
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
### Environment Interface
|
||||
|
||||
```typescript
|
||||
// src/types.ts
|
||||
import { ChatRoom } from "./durable-objects/chat-room";
|
||||
import { UserSession } from "./durable-objects/user-session";
|
||||
|
||||
export interface Env {
|
||||
// Durable Objects
|
||||
CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
|
||||
USER_SESSION: DurableObjectNamespace<UserSession>;
|
||||
|
||||
// KV
|
||||
CONFIG: KVNamespace;
|
||||
|
||||
// R2
|
||||
UPLOADS: R2Bucket;
|
||||
|
||||
// D1
|
||||
DB: D1Database;
|
||||
|
||||
// Environment variables
|
||||
ENVIRONMENT: string;
|
||||
API_KEY: string; // From secrets
|
||||
}
|
||||
```
|
||||
|
||||
### Export Durable Object Classes
|
||||
|
||||
```typescript
|
||||
// src/index.ts
|
||||
export { ChatRoom } from "./durable-objects/chat-room";
|
||||
export { UserSession } from "./durable-objects/user-session";
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
// Worker handler
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Worker Handler Pattern
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
|
||||
try {
|
||||
// Route to appropriate handler
|
||||
if (url.pathname.startsWith("/api/rooms")) {
|
||||
return handleRooms(request, env);
|
||||
}
|
||||
if (url.pathname.startsWith("/api/users")) {
|
||||
return handleUsers(request, env);
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
} catch (error) {
|
||||
console.error("Request failed:", error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
async function handleRooms(request: Request, env: Env): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const roomId = url.searchParams.get("room");
|
||||
|
||||
if (!roomId) {
|
||||
return Response.json({ error: "Missing room parameter" }, { status: 400 });
|
||||
}
|
||||
|
||||
const stub = env.CHAT_ROOM.getByName(roomId);
|
||||
|
||||
if (request.method === "POST") {
|
||||
const body = await request.json<{ userId: string; message: string }>();
|
||||
const result = await stub.sendMessage(body.userId, body.message);
|
||||
return Response.json(result);
|
||||
}
|
||||
|
||||
const messages = await stub.getMessages();
|
||||
return Response.json(messages);
|
||||
}
|
||||
```
|
||||
|
||||
## Request Validation
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
|
||||
const SendMessageSchema = z.object({
|
||||
userId: z.string().min(1),
|
||||
message: z.string().min(1).max(1000),
|
||||
});
|
||||
|
||||
async function handleSendMessage(request: Request, env: Env): Promise<Response> {
|
||||
const body = await request.json();
|
||||
const result = SendMessageSchema.safeParse(body);
|
||||
|
||||
if (!result.success) {
|
||||
return Response.json(
|
||||
{ error: "Validation failed", details: result.error.issues },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const stub = env.CHAT_ROOM.getByName(result.data.userId);
|
||||
const message = await stub.sendMessage(result.data.userId, result.data.message);
|
||||
return Response.json(message);
|
||||
}
|
||||
```
|
||||
|
||||
## Observability & Logging
|
||||
|
||||
### Structured Logging
|
||||
|
||||
```typescript
|
||||
function log(level: "info" | "warn" | "error", message: string, data?: Record<string, unknown>) {
|
||||
console.log(JSON.stringify({
|
||||
level,
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
...data,
|
||||
}));
|
||||
}
|
||||
|
||||
// Usage
|
||||
log("info", "Request received", { path: url.pathname, method: request.method });
|
||||
log("error", "DO call failed", { roomId, error: String(error) });
|
||||
```
|
||||
|
||||
### Request Tracing
|
||||
|
||||
```typescript
|
||||
async function handleRequest(request: Request, env: Env): Promise<Response> {
|
||||
const requestId = crypto.randomUUID();
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const response = await processRequest(request, env);
|
||||
|
||||
log("info", "Request completed", {
|
||||
requestId,
|
||||
duration: Date.now() - startTime,
|
||||
status: response.status,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
log("error", "Request failed", {
|
||||
requestId,
|
||||
duration: Date.now() - startTime,
|
||||
error: String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tail Workers (Production)
|
||||
|
||||
For production logging, use Tail Workers to forward logs:
|
||||
|
||||
```jsonc
|
||||
// wrangler.jsonc
|
||||
{
|
||||
"tail_consumers": [
|
||||
{ "service": "log-collector" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Graceful DO Errors
|
||||
|
||||
```typescript
|
||||
async function callDO(stub: DurableObjectStub<ChatRoom>, method: string): Promise<Response> {
|
||||
try {
|
||||
const result = await stub.getMessages();
|
||||
return Response.json(result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
// DO threw an error
|
||||
log("error", "DO operation failed", { error: error.message });
|
||||
return Response.json(
|
||||
{ error: "Service temporarily unavailable" },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Timeout Handling
|
||||
|
||||
```typescript
|
||||
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
const timeout = new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Timeout")), ms)
|
||||
);
|
||||
return Promise.race([promise, timeout]);
|
||||
}
|
||||
|
||||
// Usage
|
||||
const result = await withTimeout(stub.processData(data), 5000);
|
||||
```
|
||||
|
||||
## CORS Handling
|
||||
|
||||
```typescript
|
||||
function corsHeaders(): HeadersInit {
|
||||
return {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
if (request.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders() });
|
||||
}
|
||||
|
||||
const response = await handleRequest(request, env);
|
||||
|
||||
// Add CORS headers to response
|
||||
const newHeaders = new Headers(response.headers);
|
||||
Object.entries(corsHeaders()).forEach(([k, v]) => newHeaders.set(k, v));
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers: newHeaders,
|
||||
});
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Secrets Management
|
||||
|
||||
Set secrets via wrangler CLI (not in config files):
|
||||
|
||||
```bash
|
||||
wrangler secret put API_KEY
|
||||
wrangler secret put DATABASE_URL
|
||||
```
|
||||
|
||||
Access in code:
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const apiKey = env.API_KEY; // From secret
|
||||
// ...
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Local development
|
||||
wrangler dev
|
||||
|
||||
# Deploy
|
||||
wrangler deploy
|
||||
|
||||
# Tail logs
|
||||
wrangler tail
|
||||
|
||||
# List DOs
|
||||
wrangler d1 execute DB --command "SELECT * FROM _cf_DO"
|
||||
```
|
||||
Reference in new issue
Block a user