Claude Code Plugins

Community-maintained marketplace

Feedback

agentic-jumpstart-security

@webdevcody/agentic-jumpstart
12
0

Security best practices for TanStack Start applications with React 19, Drizzle ORM, PostgreSQL, Stripe, OAuth, and AWS S3/R2. Use when writing secure code, implementing authentication, handling payments, protecting API endpoints, validating input, preventing XSS/CSRF/SQL injection, or when the user mentions security, vulnerabilities, or authentication.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name agentic-jumpstart-security
description Security best practices for TanStack Start applications with React 19, Drizzle ORM, PostgreSQL, Stripe, OAuth, and AWS S3/R2. Use when writing secure code, implementing authentication, handling payments, protecting API endpoints, validating input, preventing XSS/CSRF/SQL injection, or when the user mentions security, vulnerabilities, or authentication.

Security Best Practices

Authentication & Authorization

Session Management with Arctic OAuth

// Always use the validateRequest utility for authentication
import { validateRequest } from "~/utils/auth";

const { user, session } = await validateRequest();
if (!user) {
  throw redirect({ to: "/unauthenticated" });
}

Middleware Protection Patterns

Always protect server functions with appropriate middleware:

import { createServerFn } from "@tanstack/react-start";
import { authenticatedMiddleware, adminMiddleware, unauthenticatedMiddleware } from "~/lib/auth";

// For authenticated-only endpoints
export const protectedFn = createServerFn()
  .middleware([authenticatedMiddleware])
  .handler(async ({ context }) => {
    // context.userId is guaranteed to exist
  });

// For admin-only endpoints
export const adminOnlyFn = createServerFn()
  .middleware([adminMiddleware])
  .handler(async ({ context }) => {
    // Only admins can access
  });

// For public endpoints that may have user context
export const publicFn = createServerFn()
  .middleware([unauthenticatedMiddleware])
  .handler(async ({ context }) => {
    // context.userId may be undefined
  });

Route Protection

import { assertIsAdminFn } from "~/fn/auth";

export const Route = createFileRoute("/admin/settings")({
  beforeLoad: () => assertIsAdminFn(),
  component: AdminSettingsPage,
});

Input Validation with Zod

Always validate all input with Zod schemas:

import { z } from "zod";

export const updateUserFn = createServerFn()
  .middleware([authenticatedMiddleware])
  .inputValidator(
    z.object({
      name: z.string().min(1).max(100),
      email: z.string().email(),
      bio: z.string().max(500).optional(),
    })
  )
  .handler(async ({ data, context }) => {
    // data is fully validated and typed
    return updateUserUseCase(context.userId, data);
  });

Common Validation Patterns

// ID validation
const idSchema = z.number().int().positive();

// Slug validation
const slugSchema = z.string().regex(/^[a-z0-9-]+$/).min(1).max(100);

// Pagination
const paginationSchema = z.object({
  page: z.number().int().min(1).default(1),
  limit: z.number().int().min(1).max(100).default(20),
});

// File upload validation
const fileSchema = z.object({
  name: z.string().max(255),
  size: z.number().max(500 * 1024 * 1024), // 500MB max
  type: z.string().refine((t) => ALLOWED_MIME_TYPES.includes(t)),
});

SQL Injection Prevention

Use Drizzle ORM Parameterized Queries

Drizzle ORM automatically parameterizes queries. Never use raw string concatenation:

// GOOD: Parameterized query
const user = await database
  .select()
  .from(users)
  .where(eq(users.email, email))
  .limit(1);

// GOOD: Using SQL template literals for raw queries
import { sql } from "drizzle-orm";
const result = await database.execute(
  sql`SELECT * FROM users WHERE email = ${email}`
);

// BAD: Never do this
// const result = await database.execute(`SELECT * FROM users WHERE email = '${email}'`);

XSS Prevention

HTML Sanitization

Always sanitize user-generated HTML content:

import sanitizeHtml from "sanitize-html";

const sanitizedContent = sanitizeHtml(userContent, {
  allowedTags: ["b", "i", "em", "strong", "a", "p", "br", "ul", "ol", "li"],
  allowedAttributes: {
    a: ["href", "target"],
  },
  allowedSchemes: ["http", "https"],
});

React Escaping

React automatically escapes content, but be careful with:

// DANGEROUS: Only use when content is trusted/sanitized
<div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />

// SAFE: React auto-escapes
<div>{userContent}</div>

Stripe Payment Security

Webhook Verification

Always verify Stripe webhooks:

import Stripe from "stripe";

const stripe = new Stripe(env.STRIPE_SECRET_KEY);

export async function handleStripeWebhook(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("stripe-signature");

  if (!signature) {
    throw new Error("Missing stripe signature");
  }

  // Verify the webhook signature
  const event = stripe.webhooks.constructEvent(
    body,
    signature,
    env.STRIPE_WEBHOOK_ENDPOINT_SECRET
  );

  // Process the verified event
  switch (event.type) {
    case "checkout.session.completed":
      await handleCheckoutComplete(event.data.object);
      break;
    // Handle other events...
  }
}

Sensitive Data Handling

// Never log full card numbers or sensitive payment data
console.log("Payment processed for customer:", customerId);
// NOT: console.log("Card:", cardNumber);

// Use Stripe's client-side library for card collection
// Never handle raw card data on your server

File Upload Security

Presigned URL Pattern

import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { PutObjectCommand } from "@aws-sdk/client-s3";

export async function getUploadUrl(
  userId: number,
  fileName: string,
  contentType: string
) {
  // Validate content type
  const allowedTypes = ["image/jpeg", "image/png", "video/mp4"];
  if (!allowedTypes.includes(contentType)) {
    throw new Error("Invalid file type");
  }

  // Generate unique key with user ID for ownership
  const key = `uploads/${userId}/${Date.now()}-${fileName}`;

  const command = new PutObjectCommand({
    Bucket: env.R2_BUCKET_NAME,
    Key: key,
    ContentType: contentType,
  });

  const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
  return { url, key };
}

File Validation

const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
const ALLOWED_VIDEO_TYPES = ["video/mp4", "video/webm"];
const ALLOWED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp"];

function validateFileUpload(file: { size: number; type: string }) {
  if (file.size > MAX_FILE_SIZE) {
    throw new Error("File too large");
  }

  const allAllowedTypes = [...ALLOWED_VIDEO_TYPES, ...ALLOWED_IMAGE_TYPES];
  if (!allAllowedTypes.includes(file.type)) {
    throw new Error("Invalid file type");
  }
}

Environment Variables

Secure Configuration

// src/utils/env.ts - Centralized environment validation
import { z } from "zod";

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
  STRIPE_WEBHOOK_ENDPOINT_SECRET: z.string().startsWith("whsec_"),
  GOOGLE_CLIENT_ID: z.string(),
  GOOGLE_CLIENT_SECRET: z.string(),
  R2_ACCESS_KEY_ID: z.string(),
  R2_SECRET_ACCESS_KEY: z.string(),
  BASE_URL: z.string().url(),
});

export const env = envSchema.parse(process.env);

Gitignore Pattern

Ensure sensitive files are never committed:

.env
.env.local
.env.production
*.pem
*.key

Cookie Security

import { setCookie } from "vinxi/http";

setCookie("session", sessionId, {
  httpOnly: true,        // Prevent XSS access
  secure: true,          // HTTPS only in production
  sameSite: "lax",       // CSRF protection
  maxAge: 60 * 60 * 24 * 30, // 30 days
  path: "/",
});

Error Handling

Never Expose Internal Errors

// Use PublicError for user-facing errors
export class PublicError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "PublicError";
  }
}

// In handlers, catch and sanitize errors
try {
  await riskyOperation();
} catch (error) {
  if (error instanceof PublicError) {
    throw error; // Safe to expose
  }
  console.error("Internal error:", error);
  throw new PublicError("An unexpected error occurred");
}

Rate Limiting

Consider implementing rate limiting for sensitive endpoints:

// Track attempts by IP or user
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();

function checkRateLimit(key: string, maxAttempts: number, windowMs: number) {
  const now = Date.now();
  const record = rateLimitMap.get(key);

  if (!record || record.resetAt < now) {
    rateLimitMap.set(key, { count: 1, resetAt: now + windowMs });
    return true;
  }

  if (record.count >= maxAttempts) {
    throw new PublicError("Too many requests. Please try again later.");
  }

  record.count++;
  return true;
}

Security Checklist

  • All server functions use appropriate middleware
  • All input is validated with Zod schemas
  • User-generated HTML is sanitized before rendering
  • Stripe webhooks are signature-verified
  • File uploads validate type and size
  • Environment variables are validated at startup
  • Cookies use httpOnly, secure, and sameSite flags
  • Internal errors are not exposed to users
  • Sensitive routes are protected with beforeLoad
  • Database queries use parameterized queries (Drizzle ORM)