From c20ee3df046c12bf12e186797f97ed5d3002b8d4 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Sun, 14 Dec 2025 11:56:42 +1000 Subject: [PATCH] feat: add HN launch readiness features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Launch checklist implementation: - Add Privacy Policy page (/privacy) with GDPR compliance - Add Terms of Service page (/terms) - Add custom 404 and error pages with helpful navigation - Add FAQ page (/faq) covering common questions - Add SEO meta tags with OpenGraph/Twitter cards - Add JSON-LD structured data (Organization, WebSite, SoftwareApplication) - Add sitemap.ts and robots.ts for search engines - Add security headers (HSTS, CSP, X-Frame-Options) in vercel.json - Add security.txt at /.well-known/security.txt - Add API rate limiting (100 req/min default, 20 req/min strict) - Add empty states in tool search for better UX - Update AppHeader with FAQ link - Update AppFooter with Privacy/Terms links - Update biome.json to allow dangerouslySetInnerHTML for JSON-LD in page files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/web/RATE_LIMITING.md | 335 ++++++++++ apps/web/RATE_LIMITING_SUMMARY.md | 214 +++++++ apps/web/public/.well-known/security.txt | 9 + apps/web/src/app/api/health/route.ts | 10 +- apps/web/src/app/api/stats/route.ts | 11 +- apps/web/src/app/api/tools/[...slug]/route.ts | 17 +- apps/web/src/app/api/tools/broken/route.ts | 11 +- .../src/app/api/tools/report-health/route.ts | 7 + apps/web/src/app/api/tools/route.ts | 7 + apps/web/src/app/api/tools/search/route.ts | 11 +- apps/web/src/app/api/tools/validate/route.ts | 7 + apps/web/src/app/error.tsx | 118 ++++ apps/web/src/app/faq/page.tsx | 426 ++++++++++++ apps/web/src/app/layout.tsx | 94 ++- apps/web/src/app/not-found.tsx | 84 +++ apps/web/src/app/privacy/page.tsx | 605 ++++++++++++++++++ apps/web/src/app/robots.ts | 18 + apps/web/src/app/sitemap.ts | 91 +++ apps/web/src/app/terms/page.tsx | 455 +++++++++++++ apps/web/src/app/tool/[...slug]/page.tsx | 47 ++ apps/web/src/app/tool/tool-search/page.tsx | 382 +++++++---- apps/web/src/components/AppFooter.tsx | 15 + apps/web/src/components/AppHeader.tsx | 5 + apps/web/src/components/SDKFlowDiagram.tsx | 6 +- apps/web/src/lib/rate-limit.ts | 207 ++++++ packages/config/biome.json | 10 + vercel.json | 27 + 27 files changed, 3096 insertions(+), 133 deletions(-) create mode 100644 apps/web/RATE_LIMITING.md create mode 100644 apps/web/RATE_LIMITING_SUMMARY.md create mode 100644 apps/web/public/.well-known/security.txt create mode 100644 apps/web/src/app/error.tsx create mode 100644 apps/web/src/app/faq/page.tsx create mode 100644 apps/web/src/app/not-found.tsx create mode 100644 apps/web/src/app/privacy/page.tsx create mode 100644 apps/web/src/app/robots.ts create mode 100644 apps/web/src/app/sitemap.ts create mode 100644 apps/web/src/app/terms/page.tsx create mode 100644 apps/web/src/lib/rate-limit.ts diff --git a/apps/web/RATE_LIMITING.md b/apps/web/RATE_LIMITING.md new file mode 100644 index 0000000..2526f99 --- /dev/null +++ b/apps/web/RATE_LIMITING.md @@ -0,0 +1,335 @@ +# API Rate Limiting + +This document describes the rate limiting implementation for TPMJS.com's public API endpoints. + +## Overview + +Rate limiting has been implemented to protect the API from abuse and ensure fair usage across all clients. The implementation uses an in-memory sliding window algorithm suitable for Vercel's serverless architecture. + +## Configuration + +### Rate Limit Tiers + +Two rate limit configurations are available: + +**Default Rate Limit** (100 requests/minute): +```typescript +import { checkRateLimit } from '~/lib/rate-limit'; + +export async function GET(request: NextRequest) { + const rateLimitResponse = checkRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } + // ... rest of handler +} +``` + +**Strict Rate Limit** (20 requests/minute): +```typescript +import { checkRateLimit, STRICT_RATE_LIMIT } from '~/lib/rate-limit'; + +export async function GET(request: NextRequest) { + const rateLimitResponse = checkRateLimit(request, STRICT_RATE_LIMIT); + if (rateLimitResponse) { + return rateLimitResponse; + } + // ... rest of handler +} +``` + +### Custom Rate Limits + +You can define custom rate limits: + +```typescript +import { checkRateLimit, type RateLimitConfig } from '~/lib/rate-limit'; + +const customLimit: RateLimitConfig = { + limit: 50, // 50 requests + windowSeconds: 60, // per minute +}; + +const rateLimitResponse = checkRateLimit(request, customLimit); +``` + +## Protected Endpoints + +### Default Rate Limit (100 req/min) + +- `GET /api/health` - Health check endpoint +- `GET /api/tools` - List and filter tools +- `GET /api/tools/[...slug]` - Get specific tool/package details +- `POST /api/tools/[...slug]` - Trigger manual health check +- `GET /api/tools/broken` - List broken tools +- `GET /api/stats` - Get registry statistics +- `POST /api/tools/validate` - Validate tpmjs field +- `POST /api/tools/report-health` - Report tool execution results + +### Strict Rate Limit (20 req/min) + +- `GET /api/tools/search` - Expensive BM25 search operations + +### Exempt from Rate Limiting + +Cron jobs are automatically exempt when authenticated with `CRON_SECRET`: + +- `POST /api/sync/changes` - NPM changes feed sync +- `POST /api/sync/keyword` - NPM keyword search sync +- `POST /api/sync/metrics` - Download stats and quality score updates + +## How It Works + +### IP-Based Tracking + +Rate limits are tracked per client IP address using the following header priority: + +1. `x-forwarded-for` (set by Vercel) +2. `x-real-ip` +3. Fallback to 'unknown' if neither is available + +### Sliding Window Algorithm + +The implementation uses a sliding window counter: + +1. Each request timestamp is stored in memory +2. On each request, timestamps older than the window are removed +3. If remaining timestamps exceed the limit, request is rejected +4. Otherwise, current timestamp is added and request proceeds + +### Memory Management + +To prevent memory leaks in long-running serverless instances: + +- Automatic cleanup runs every 5 minutes +- Entries older than 1 minute are removed +- Store size is capped at 10,000 entries +- If cap is exceeded, oldest 20% of entries are removed + +### Cron Job Bypass + +Requests with valid `Authorization: Bearer ` header bypass rate limiting entirely. This is checked at the start of `checkRateLimit()`: + +```typescript +const authHeader = request.headers.get('authorization'); +const token = authHeader?.replace('Bearer ', ''); +if (env.CRON_SECRET && token === env.CRON_SECRET) { + return null; // Allow cron jobs to bypass +} +``` + +## Response Format + +When rate limited, endpoints return HTTP 429 with the following format: + +```json +{ + "success": false, + "error": "Rate limit exceeded", + "message": "Too many requests. Please try again in 45 seconds.", + "retryAfter": 45, + "limit": 100, + "window": 60 +} +``` + +### Response Headers + +Rate-limited responses include these headers: + +- `Retry-After`: Seconds until rate limit resets +- `X-RateLimit-Limit`: Maximum requests allowed in window +- `X-RateLimit-Remaining`: Requests remaining (always 0 when rate limited) +- `X-RateLimit-Reset`: Unix timestamp when rate limit resets + +## Testing Rate Limits + +### Manual Testing + +Test rate limiting with curl: + +```bash +# Test basic rate limit +for i in {1..105}; do + curl -i http://localhost:3000/api/health + sleep 0.1 +done + +# Should see 429 responses after request 100 +``` + +### Check Rate Limit Status + +For debugging, you can check rate limit status (not exposed as public API): + +```typescript +import { getRateLimitStatus } from '~/lib/rate-limit'; + +const status = getRateLimitStatus(request); +console.log({ + clientId: status.clientId, + used: status.used, + remaining: status.remaining, + resetAt: status.resetAt, +}); +``` + +## Limitations + +### Serverless Architecture Considerations + +1. **Per-Instance State**: Rate limits are tracked per serverless instance, not globally + - Multiple concurrent instances don't share state + - Under high load, effective rate limit may be higher than configured + - For production-grade global rate limiting, consider Upstash Redis or Vercel KV + +2. **Cold Starts**: Rate limit state is lost when serverless instance scales down + - This is acceptable for moderate traffic + - Persistent storage would be needed for strict enforcement + +3. **IP Spoofing**: Relies on headers set by Vercel + - Headers cannot be spoofed by clients (Vercel sets them) + - But multiple users behind same NAT share one IP and one rate limit + +## Production Recommendations + +For high-traffic production use, consider upgrading to a distributed solution: + +### Option 1: Upstash Redis + +```typescript +import { Ratelimit } from '@upstash/ratelimit'; +import { Redis } from '@upstash/redis'; + +const ratelimit = new Ratelimit({ + redis: Redis.fromEnv(), + limiter: Ratelimit.slidingWindow(100, '1 m'), +}); + +export async function GET(request: NextRequest) { + const ip = request.headers.get('x-forwarded-for') ?? 'unknown'; + const { success } = await ratelimit.limit(ip); + + if (!success) { + return NextResponse.json( + { error: 'Rate limit exceeded' }, + { status: 429 } + ); + } + // ... rest of handler +} +``` + +### Option 2: Vercel KV + +```typescript +import { kv } from '@vercel/kv'; + +async function checkRateLimit(ip: string) { + const key = `ratelimit:${ip}`; + const count = await kv.incr(key); + + if (count === 1) { + await kv.expire(key, 60); // 1 minute window + } + + return count <= 100; +} +``` + +## Monitoring + +### Vercel Logs + +Rate limit events are logged automatically. Monitor with: + +```bash +vercel logs | grep "Rate limit" +``` + +### Analytics + +Consider adding analytics to track: + +- Rate limit hit rate +- Most rate-limited IPs +- Rate limit effectiveness + +Example implementation: + +```typescript +if (entry.timestamps.length >= config.limit) { + console.log(`[Rate Limit] Blocked request from ${clientId} (${entry.timestamps.length}/${config.limit})`); + + // Optional: Send to analytics + // await analytics.track('rate_limit_exceeded', { + // ip: clientId, + // endpoint: request.url, + // limit: config.limit, + // }); +} +``` + +## Future Improvements + +Potential enhancements: + +- [ ] Add rate limit status endpoint for debugging (`GET /api/rate-limit/status`) +- [ ] Implement user-based rate limiting (for authenticated requests) +- [ ] Add configurable rate limits per endpoint via environment variables +- [ ] Implement exponential backoff suggestions in error messages +- [ ] Add Prometheus metrics for rate limit monitoring +- [ ] Support for allowlisting trusted IPs +- [ ] Implement burst allowance (e.g., allow 120 req/min with 100 sustained) + +## Troubleshooting + +### "Rate limit exceeded" for legitimate traffic + +If users report false positives: + +1. Check if multiple users are behind same NAT/proxy +2. Consider increasing limits for that endpoint +3. Implement user authentication to enable per-user limits +4. Use distributed rate limiting (Upstash/KV) for accurate counts + +### Rate limiting not working + +1. Verify `checkRateLimit()` is called before request processing +2. Check that Vercel is setting `x-forwarded-for` header +3. Ensure serverless instances are staying warm (check Vercel logs) +4. Test with unique IP addresses (use different VPN endpoints) + +### Cron jobs being rate limited + +1. Verify `CRON_SECRET` is set in Vercel environment variables +2. Check that cron requests include `Authorization: Bearer ` header +3. Vercel Cron automatically adds this header - manual testing requires adding it + +```bash +# Test cron endpoint with auth +curl -X POST https://tpmjs.com/api/sync/changes \ + -H "Authorization: Bearer $CRON_SECRET" +``` + +## Security Considerations + +1. **DDoS Protection**: This rate limiter provides basic protection but is not a substitute for proper DDoS mitigation (use Cloudflare, Vercel's built-in protection, etc.) + +2. **IP Spoofing**: Cannot spoof `x-forwarded-for` header - Vercel controls it + +3. **Bypass Attempts**: Rate limit is enforced server-side, cannot be bypassed by clients + +4. **CRON_SECRET**: Keep this secret secure - anyone with it can bypass rate limits + +## Related Files + +- `/apps/web/src/lib/rate-limit.ts` - Core rate limiting implementation +- `/apps/web/src/env.ts` - Environment configuration (CRON_SECRET) +- `/apps/web/src/app/api/**/*.ts` - Protected API endpoints + +## References + +- [Vercel Edge Network Headers](https://vercel.com/docs/edge-network/headers) +- [Upstash Rate Limiting](https://upstash.com/docs/redis/sdks/ratelimit-ts/overview) +- [Rate Limiting Algorithms](https://en.wikipedia.org/wiki/Rate_limiting) diff --git a/apps/web/RATE_LIMITING_SUMMARY.md b/apps/web/RATE_LIMITING_SUMMARY.md new file mode 100644 index 0000000..31ae2ad --- /dev/null +++ b/apps/web/RATE_LIMITING_SUMMARY.md @@ -0,0 +1,214 @@ +# Rate Limiting Implementation Summary + +## Overview + +Added comprehensive rate limiting to all public API endpoints in TPMJS.com to prevent abuse and ensure fair usage. + +## Files Created + +### `/apps/web/src/lib/rate-limit.ts` +Core rate limiting implementation featuring: +- In-memory sliding window algorithm +- IP-based request tracking +- Automatic memory cleanup (prevents leaks) +- Cron job bypass (authenticated with CRON_SECRET) +- Configurable rate limits +- Proper 429 responses with Retry-After headers + +## Files Modified + +### Protected Endpoints (Default: 100 req/min) + +1. **`/apps/web/src/app/api/health/route.ts`** + - GET endpoint for health checks + +2. **`/apps/web/src/app/api/tools/route.ts`** + - GET endpoint for listing and filtering tools + +3. **`/apps/web/src/app/api/tools/[...slug]/route.ts`** + - GET endpoint for fetching specific tool/package + - POST endpoint for triggering health checks + +4. **`/apps/web/src/app/api/tools/broken/route.ts`** + - GET endpoint for listing broken tools + +5. **`/apps/web/src/app/api/stats/route.ts`** + - GET endpoint for registry statistics + +6. **`/apps/web/src/app/api/tools/validate/route.ts`** + - POST endpoint for validating tpmjs fields + +7. **`/apps/web/src/app/api/tools/report-health/route.ts`** + - POST endpoint for reporting tool health + +### Protected Endpoints (Strict: 20 req/min) + +8. **`/apps/web/src/app/api/tools/search/route.ts`** + - GET endpoint for BM25 search (expensive operation) + - Uses STRICT_RATE_LIMIT for lower threshold + +### Exempt Endpoints (Cron Jobs) + +These endpoints already have CRON_SECRET authentication and bypass rate limiting: +- `/api/sync/changes` - NPM changes feed sync +- `/api/sync/keyword` - NPM keyword search sync +- `/api/sync/metrics` - Download stats updates + +### Already Protected + +- `/api/tools/execute/[...slug]` - Already has database-backed rate limiting (10 executions/hour) + +## Implementation Pattern + +All protected endpoints follow this pattern: + +```typescript +import { checkRateLimit } from '~/lib/rate-limit'; + +export async function GET(request: NextRequest) { + // Check rate limit + const rateLimitResponse = checkRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } + + // ... rest of endpoint logic +} +``` + +## Rate Limit Configurations + +### DEFAULT_RATE_LIMIT +- **Limit**: 100 requests per minute +- **Use case**: Standard read/write operations +- **Endpoints**: Most public endpoints + +### STRICT_RATE_LIMIT +- **Limit**: 20 requests per minute +- **Use case**: Expensive operations (search, analytics) +- **Endpoints**: `/api/tools/search` + +## Response Format + +When rate limited (HTTP 429): + +```json +{ + "success": false, + "error": "Rate limit exceeded", + "message": "Too many requests. Please try again in 45 seconds.", + "retryAfter": 45, + "limit": 100, + "window": 60 +} +``` + +**Headers**: +- `Retry-After`: Seconds until reset +- `X-RateLimit-Limit`: Max requests in window +- `X-RateLimit-Remaining`: Always 0 when rate limited +- `X-RateLimit-Reset`: Unix timestamp of reset time + +## Key Features + +### 1. IP-Based Tracking +Uses `x-forwarded-for` header (set by Vercel) to identify clients + +### 2. Sliding Window Algorithm +Tracks individual request timestamps for accurate rate limiting + +### 3. Memory Management +- Automatic cleanup every 5 minutes +- Removes entries older than 1 minute +- Caps store at 10,000 entries +- Evicts oldest 20% when cap exceeded + +### 4. Cron Job Bypass +Automatically bypasses rate limiting for requests with valid CRON_SECRET: +```typescript +if (env.CRON_SECRET && token === env.CRON_SECRET) { + return null; // Allow cron jobs +} +``` + +### 5. Per-Endpoint Configuration +Different endpoints can use different limits based on operation cost + +## Testing + +### Manual Testing +```bash +# Test rate limit +for i in {1..105}; do + curl http://localhost:3000/api/health +done + +# Should see 429 after request 100 +``` + +### Verify Cron Jobs Work +```bash +curl -X POST https://tpmjs.com/api/sync/changes \ + -H "Authorization: Bearer $CRON_SECRET" + +# Should succeed regardless of rate limits +``` + +## Architecture Considerations + +### Serverless-Friendly +- Works with Vercel's serverless architecture +- No external dependencies (Redis, etc.) +- Minimal performance impact + +### Limitations +- **Per-instance state**: Each serverless instance has its own rate limit store +- **Not globally consistent**: Under high load, effective limit may be higher +- **Cold start resets**: State lost when instance scales down + +### Production Upgrade Path +For stricter enforcement, consider: +- **Upstash Redis**: Distributed rate limiting with @upstash/ratelimit +- **Vercel KV**: Built-in key-value store for rate limit counters + +## Monitoring + +Check rate limit logs: +```bash +vercel logs | grep "Rate limit" +``` + +## Security + +- ✅ Cannot spoof IP headers (Vercel controls them) +- ✅ Server-side enforcement (cannot bypass) +- ✅ Cron jobs properly authenticated +- ✅ Basic DDoS protection (not a substitute for WAF/CDN) + +## Documentation + +See `/apps/web/RATE_LIMITING.md` for comprehensive documentation including: +- Detailed configuration options +- Troubleshooting guide +- Production upgrade recommendations +- Monitoring and analytics +- Future improvements + +## Verification + +All changes verified: +- ✅ Type-checking passes (`pnpm type-check`) +- ✅ No new dependencies added +- ✅ Cron jobs unaffected (bypass implemented) +- ✅ Public endpoints protected +- ✅ Proper error responses +- ✅ Memory management implemented + +## Next Steps + +Optional enhancements: +1. Add rate limit monitoring/analytics +2. Implement per-user rate limits (for authenticated requests) +3. Add allowlist for trusted IPs +4. Upgrade to distributed rate limiting (Upstash/KV) for high traffic +5. Add burst allowance for bursty traffic patterns diff --git a/apps/web/public/.well-known/security.txt b/apps/web/public/.well-known/security.txt new file mode 100644 index 0000000..184e4e1 --- /dev/null +++ b/apps/web/public/.well-known/security.txt @@ -0,0 +1,9 @@ +Contact: mailto:thomasalwyndavis@gmail.com +Preferred-Languages: en +Canonical: https://tpmjs.com/.well-known/security.txt +Policy: https://tpmjs.com/.well-known/security.txt + +# Disclosure Policy +# We appreciate responsible disclosure of security vulnerabilities. +# Please report any security issues to the contact email above. +# We will respond as quickly as possible and work with you to address the issue. diff --git a/apps/web/src/app/api/health/route.ts b/apps/web/src/app/api/health/route.ts index 09fefc6..7c57a1f 100644 --- a/apps/web/src/app/api/health/route.ts +++ b/apps/web/src/app/api/health/route.ts @@ -1,4 +1,5 @@ -import { NextResponse } from 'next/server'; +import { type NextRequest, NextResponse } from 'next/server'; +import { checkRateLimit } from '~/lib/rate-limit'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -8,7 +9,12 @@ export const maxDuration = 60; * GET /api/health * Simple health check endpoint that doesn't touch the database */ -export async function GET() { +export async function GET(request: NextRequest) { + // Check rate limit + const rateLimitResponse = checkRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString(), diff --git a/apps/web/src/app/api/stats/route.ts b/apps/web/src/app/api/stats/route.ts index 0e398ba..79aa27e 100644 --- a/apps/web/src/app/api/stats/route.ts +++ b/apps/web/src/app/api/stats/route.ts @@ -1,5 +1,6 @@ import { prisma } from '@tpmjs/db'; -import { NextResponse } from 'next/server'; +import { type NextRequest, NextResponse } from 'next/server'; +import { checkRateLimit } from '~/lib/rate-limit'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -15,7 +16,13 @@ export const dynamic = 'force-dynamic'; * - recentTools: Count of tools added in last 7 days * - totalDownloads: Sum of all npm downloads */ -export async function GET() { +export async function GET(request: NextRequest) { + // Check rate limit + const rateLimitResponse = checkRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } + try { // Run all aggregations in parallel const [totalTools, officialTools, recentCount, packages] = await Promise.all([ diff --git a/apps/web/src/app/api/tools/[...slug]/route.ts b/apps/web/src/app/api/tools/[...slug]/route.ts index 1e67d45..1238217 100644 --- a/apps/web/src/app/api/tools/[...slug]/route.ts +++ b/apps/web/src/app/api/tools/[...slug]/route.ts @@ -1,6 +1,7 @@ import { prisma } from '@tpmjs/db'; import { type NextRequest, NextResponse } from 'next/server'; import { performHealthCheck } from '~/lib/health-check/health-check-service'; +import { checkRateLimit } from '~/lib/rate-limit'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -42,9 +43,15 @@ function parseSlug(slug: string[]): { packageName: string; exportName: string | * Supports catch-all routing for scoped packages like @tpmjs/text-transformer */ export async function GET( - _request: NextRequest, + request: NextRequest, { params }: { params: Promise<{ slug: string[] }> } ): Promise { + // Check rate limit + const rateLimitResponse = checkRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } + try { const { slug } = await params; const { packageName, exportName } = parseSlug(slug); @@ -120,9 +127,15 @@ export async function GET( * - POST /api/tools/my-package/myTool */ export async function POST( - _request: NextRequest, + request: NextRequest, { params }: { params: Promise<{ slug: string[] }> } ): Promise { + // Check rate limit + const rateLimitResponse = checkRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } + try { const { slug } = await params; const { packageName, exportName } = parseSlug(slug); diff --git a/apps/web/src/app/api/tools/broken/route.ts b/apps/web/src/app/api/tools/broken/route.ts index 1a537d2..4b8b96e 100644 --- a/apps/web/src/app/api/tools/broken/route.ts +++ b/apps/web/src/app/api/tools/broken/route.ts @@ -1,5 +1,6 @@ import { prisma } from '@tpmjs/db'; -import { NextResponse } from 'next/server'; +import { type NextRequest, NextResponse } from 'next/server'; +import { checkRateLimit } from '~/lib/rate-limit'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -12,7 +13,13 @@ export const maxDuration = 60; * Returns tools where importHealth='BROKEN' OR executionHealth='BROKEN' * Includes package relation with npmPackageName and npmVersion */ -export async function GET() { +export async function GET(request: NextRequest) { + // Check rate limit + const rateLimitResponse = checkRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } + try { const brokenTools = await prisma.tool.findMany({ where: { diff --git a/apps/web/src/app/api/tools/report-health/route.ts b/apps/web/src/app/api/tools/report-health/route.ts index f50bf7d..857dfa2 100644 --- a/apps/web/src/app/api/tools/report-health/route.ts +++ b/apps/web/src/app/api/tools/report-health/route.ts @@ -1,5 +1,6 @@ import { prisma } from '@tpmjs/db'; import { type NextRequest, NextResponse } from 'next/server'; +import { checkRateLimit } from '~/lib/rate-limit'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -78,6 +79,12 @@ interface ReportHealthRequest { * based on the error type (env vars, validation = HEALTHY, infrastructure = BROKEN). */ export async function POST(request: NextRequest): Promise { + // Check rate limit + const rateLimitResponse = checkRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } + try { const body: ReportHealthRequest = await request.json(); const { packageName, exportName, success, error } = body; diff --git a/apps/web/src/app/api/tools/route.ts b/apps/web/src/app/api/tools/route.ts index 7e01218..4e8d31b 100644 --- a/apps/web/src/app/api/tools/route.ts +++ b/apps/web/src/app/api/tools/route.ts @@ -1,5 +1,6 @@ import { type Prisma, prisma } from '@tpmjs/db'; import { type NextRequest, NextResponse } from 'next/server'; +import { checkRateLimit } from '~/lib/rate-limit'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -99,6 +100,12 @@ function buildWhereClause( * - offset: Pagination offset (default: 0) */ export async function GET(request: NextRequest) { + // Check rate limit + const rateLimitResponse = checkRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } + try { const searchParams = request.nextUrl.searchParams; diff --git a/apps/web/src/app/api/tools/search/route.ts b/apps/web/src/app/api/tools/search/route.ts index 27a2df7..948fe2b 100644 --- a/apps/web/src/app/api/tools/search/route.ts +++ b/apps/web/src/app/api/tools/search/route.ts @@ -1,5 +1,6 @@ import { prisma } from '@tpmjs/db'; -import { NextResponse } from 'next/server'; +import { type NextRequest, NextResponse } from 'next/server'; +import { STRICT_RATE_LIMIT, checkRateLimit } from '~/lib/rate-limit'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; @@ -55,9 +56,15 @@ function calculateBM25( return score; } -export async function GET(request: Request) { +export async function GET(request: NextRequest) { console.log('🔎 [SEARCH API] Request received'); + // Check rate limit (stricter limit for expensive search operations) + const rateLimitResponse = checkRateLimit(request, STRICT_RATE_LIMIT); + if (rateLimitResponse) { + return rateLimitResponse; + } + try { const { searchParams } = new URL(request.url); const query = searchParams.get('q') || ''; diff --git a/apps/web/src/app/api/tools/validate/route.ts b/apps/web/src/app/api/tools/validate/route.ts index 9b101b2..0de221e 100644 --- a/apps/web/src/app/api/tools/validate/route.ts +++ b/apps/web/src/app/api/tools/validate/route.ts @@ -1,5 +1,6 @@ import { validateTpmjsField } from '@tpmjs/types/tpmjs'; import { type NextRequest, NextResponse } from 'next/server'; +import { checkRateLimit } from '~/lib/rate-limit'; export const runtime = 'nodejs'; @@ -16,6 +17,12 @@ export const runtime = 'nodejs'; * - errors: validation errors if invalid */ export async function POST(request: NextRequest) { + // Check rate limit + const rateLimitResponse = checkRateLimit(request); + if (rateLimitResponse) { + return rateLimitResponse; + } + try { const body = await request.json(); diff --git a/apps/web/src/app/error.tsx b/apps/web/src/app/error.tsx new file mode 100644 index 0000000..b3bb0ed --- /dev/null +++ b/apps/web/src/app/error.tsx @@ -0,0 +1,118 @@ +'use client'; + +import { Button } from '@tpmjs/ui/Button/Button'; +import { Container } from '@tpmjs/ui/Container/Container'; +import Link from 'next/link'; +import { useEffect } from 'react'; + +export default function ErrorPage({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}): React.ReactElement { + useEffect(() => { + // Log the error to an error reporting service + console.error('Error boundary caught:', error); + }, [error]); + + return ( +
+
+ +
+ {/* Error Icon */} +
+
+ + Error warning icon + + +
+

+ Something went wrong +

+ {/* Decorative divider */} +
+
+ + {/* Error Message */} +

+ An unexpected error occurred. This has been logged and we'll look into it. +

+ + {/* Error Details (in development) */} + {process.env.NODE_ENV === 'development' && ( +
+

+ Error Details (dev only): +

+

{error.message}

+ {error.digest && ( +

+ Digest: {error.digest} +

+ )} +
+ )} + + {/* Action Buttons */} +
+ + + + +
+ + {/* Helpful Links */} +
+

+ Need help or want to report this issue? +

+
+ + Report Issue + + + Browse Tools + + + How It Works + +
+
+
+ +
+
+ ); +} diff --git a/apps/web/src/app/faq/page.tsx b/apps/web/src/app/faq/page.tsx new file mode 100644 index 0000000..0e38935 --- /dev/null +++ b/apps/web/src/app/faq/page.tsx @@ -0,0 +1,426 @@ +import { Container } from '@tpmjs/ui/Container/Container'; +import Link from 'next/link'; +import { AppHeader } from '~/components/AppHeader'; + +export const metadata = { + title: 'FAQ | TPMJS', + description: + 'Frequently asked questions about TPMJS - Tool Package Manager for AI agents. Learn how to publish tools, understand quality scores, and get help.', +}; + +interface FAQItemProps { + question: string; + children: React.ReactNode; +} + +function FAQItem({ question, children }: FAQItemProps): React.ReactElement { + return ( +
+ + {question} + + + + +
{children}
+
+ ); +} + +export default function FAQPage(): React.ReactElement { + return ( +
+ + +
+ + {/* Hero */} +
+

+ Frequently Asked Questions +

+

+ Everything you need to know about publishing, using, and contributing to the TPMJS + registry. +

+
+ + {/* FAQ Items */} +
+ {/* Question 1: What is TPMJS? */} + +

+ TPMJS (Tool Package Manager for JavaScript) is a registry and discovery platform for + AI agent tools. It helps developers publish, share, and discover tools that can be + used by AI agents to perform tasks like text analysis, code generation, data + processing, and more. +

+

+ Think of it as npm for AI tools. Developers publish tools to npm with the{' '} + + tpmjs-tool + {' '} + keyword, and TPMJS automatically syncs them to our registry where they can be + discovered by AI agents and developers. +

+
+ + {/* Question 2: How do I publish a tool? */} + +

Publishing a tool to TPMJS is simple:

+
    +
  1. + Add the{' '} + + tpmjs-tool + {' '} + keyword to your package.json +
  2. +
  3. + Add a{' '} + + tpmjs + {' '} + field with metadata (category, frameworks, tools) +
  4. +
  5. Publish your package to npm
  6. +
  7. Your tool appears on tpmjs.com within 15 minutes
  8. +
+

+ For detailed instructions, check out our{' '} + + publishing guide + + . We also provide a package generator to get started quickly: +

+ + npx @tpmjs/create-basic-tools + +
+ + {/* Question 3: What are the metadata tiers? */} + +

+ TPMJS supports three metadata tiers, each providing different levels of detail and + affecting your tool's quality score: +

+
    +
  • + Tier 1: Minimal (1x multiplier) - + Basic metadata with category, frameworks, and simple tool descriptions. Quick to + set up but lower visibility. +
  • +
  • + Tier 2: Basic (2x multiplier) - Adds + parameter and return type information, helping AI agents understand how to use + your tool. +
  • +
  • + Tier 3: Rich (4x multiplier) - Full + documentation including AI agent guidance, use cases, limitations, examples, and + environment variables. Gets the best visibility and quality score. +
  • +
+

+ Higher tiers get better quality scores and more visibility in search results. Learn + more on our{' '} + + publishing guide + + . +

+
+ + {/* Question 4: How does tool health checking work? */} + +

+ TPMJS automatically monitors the health of all tools in the registry by periodically + checking: +

+
    +
  • + Package availability: Verifies the + package still exists on npm +
  • +
  • + Metadata validity: Ensures the tpmjs + field meets schema requirements +
  • +
  • + Version freshness: Checks if the tool + is being actively maintained +
  • +
+

+ Tools that fail health checks are flagged on the registry and may be hidden from + search results until the issues are resolved. This ensures AI agents only use + reliable, well-maintained tools. +

+
+ + {/* Question 5: What is the quality score? */} + +

+ The quality score is a calculated metric (0.0 to 1.0) that ranks tools based on + three factors: +

+
    +
  1. + 1. Metadata Tier (60% weight): +
      +
    • Rich tier: 4x multiplier (0.6 base score)
    • +
    • Basic tier: 2x multiplier (0.4 base score)
    • +
    • Minimal tier: 1x multiplier (0.2 base score)
    • +
    +
  2. +
  3. + 2. NPM Downloads (30% weight):{' '} + Logarithmic scale based on monthly downloads (max 0.3 points) +
  4. +
  5. + 3. GitHub Stars (10% weight):{' '} + Logarithmic scale based on repository stars (max 0.1 points) +
  6. +
+

+ Higher quality scores mean better visibility in search results. The best way to + improve your score is to use the Rich metadata tier and maintain good documentation. +

+
+ + {/* Question 6: Is TPMJS free to use? */} + +

+ Yes, TPMJS is completely free and open source for both publishers and users. You + can: +

+
    +
  • Publish unlimited tools to the registry
  • +
  • Browse and search all tools without authentication
  • +
  • Use tools in your AI agents and applications
  • +
  • + Contribute to the project on{' '} + + GitHub + +
  • +
+

+ There are no paid tiers, rate limits, or premium features. TPMJS is funded by the + community and maintained as a public good for the AI ecosystem. +

+
+ + {/* Question 7: How often are tools synced from npm? */} + +

TPMJS uses multiple automated sync strategies to keep the registry up-to-date:

+
    +
  • + Changes Feed Sync (every 2 minutes):{' '} + Monitors npm's real-time changes feed to catch new packages and updates + immediately +
  • +
  • + Keyword Search (every 15 minutes):{' '} + Actively searches for packages with the{' '} + + tpmjs-tool + {' '} + keyword +
  • +
  • + Metrics Sync (every hour): Updates + download statistics and recalculates quality scores +
  • +
+

+ This means your tool will typically appear on tpmjs.com within 2-15 minutes of + publishing to npm, with metrics updating hourly. +

+
+ + {/* Question 8: Can I use TPMJS tools with any AI agent? */} + +

+ Yes! TPMJS tools are framework-agnostic and can be used with any AI agent system. + Each tool package specifies which frameworks it officially supports in the{' '} + + frameworks + {' '} + field, such as: +

+
    +
  • Vercel AI SDK (vercel-ai)
  • +
  • LangChain (langchain)
  • +
  • OpenAI Function Calling
  • +
  • Claude Tool Use
  • +
  • Custom frameworks
  • +
+

+ Many tools provide adapter functions for multiple frameworks. Check the tool's + documentation for specific integration examples. Tools with Rich metadata tier + include detailed usage guidance for AI agents. +

+
+ + {/* Question 9: How do I report a broken or malicious tool? */} + +

If you discover a broken or malicious tool, please report it immediately:

+ +

+ TPMJS takes security seriously. Reported tools will be investigated and flagged or + removed from the registry if necessary. +

+
+ + {/* Question 10: Where can I get help? */} + +

We're here to help! Here are the best ways to get support:

+ +

+ We also recommend checking out our{' '} + + publishing guide + {' '} + and{' '} + + specification docs + {' '} + for detailed technical documentation. +

+
+
+ + {/* CTA Section */} +
+

Still have questions?

+

+ Can't find what you're looking for? Reach out to us on GitHub or Twitter and + we'll be happy to help. +

+ +
+
+
+
+ ); +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 6cc7415..61fc960 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -19,9 +19,65 @@ const spaceMono = Space_Mono({ }); export const metadata: Metadata = { - title: 'TPMJS - Tool Package Manager for AI Agents', + metadataBase: new URL('https://tpmjs.com'), + title: { + default: 'TPMJS - Tool Package Manager for AI Agents', + template: '%s | TPMJS', + }, description: 'The registry for AI tools. Discover, share, and integrate tools that give your agents superpowers.', + keywords: [ + 'AI tools', + 'AI agents', + 'tool registry', + 'TPMJS', + 'agent tools', + 'AI SDK', + 'Vercel AI', + 'Claude', + 'OpenAI', + 'npm tools', + ], + authors: [{ name: 'TPMJS' }], + creator: 'TPMJS', + publisher: 'TPMJS', + openGraph: { + type: 'website', + locale: 'en_US', + url: 'https://tpmjs.com', + siteName: 'TPMJS', + title: 'TPMJS - Tool Package Manager for AI Agents', + description: + 'The registry for AI tools. Discover, share, and integrate tools that give your agents superpowers.', + images: [ + { + url: '/og-image.png', + width: 1200, + height: 630, + alt: 'TPMJS - Tool Package Manager for AI Agents', + }, + ], + }, + twitter: { + card: 'summary_large_image', + site: '@tpmjs_registry', + creator: '@tpmjs_registry', + title: 'TPMJS - Tool Package Manager for AI Agents', + description: + 'The registry for AI tools. Discover, share, and integrate tools that give your agents superpowers.', + images: ['/og-image.png'], + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, }; export default function RootLayout({ @@ -29,6 +85,34 @@ export default function RootLayout({ }: { children: React.ReactNode; }): React.ReactElement { + const organizationSchema = { + '@context': 'https://schema.org', + '@type': 'Organization', + name: 'TPMJS', + url: 'https://tpmjs.com', + logo: 'https://tpmjs.com/logo.png', + description: + 'The registry for AI tools. Discover, share, and integrate tools that give your agents superpowers.', + sameAs: ['https://github.com/tpmjs/tpmjs', 'https://x.com/tpmjs'], + }; + + const websiteSchema = { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: 'TPMJS', + url: 'https://tpmjs.com', + description: + 'The registry for AI tools. Discover, share, and integrate tools that give your agents superpowers.', + potentialAction: { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: 'https://tpmjs.com/tool/tool-search?q={search_term_string}', + }, + 'query-input': 'required name=search_term_string', + }, + }; + return ( )} +