From dde2fc241dbcf8d2c50ae29efdb6b3245772c384 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Apr 2026 12:53:51 +0200 Subject: [PATCH] docs: add design specs for image URL fetching, auth, and MCP server Three independent feature specs covering: - API endpoint for fetching images from URLs with local storage - Public-read/authenticated-write auth with sessions and API keys - Built-in MCP server for Claude Code/Desktop integration Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-04-03-authentication-design.md | 133 +++++++++++++++ .../2026-04-03-image-url-fetching-design.md | 87 ++++++++++ .../specs/2026-04-03-mcp-server-design.md | 158 ++++++++++++++++++ 3 files changed, 378 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-03-authentication-design.md create mode 100644 docs/superpowers/specs/2026-04-03-image-url-fetching-design.md create mode 100644 docs/superpowers/specs/2026-04-03-mcp-server-design.md diff --git a/docs/superpowers/specs/2026-04-03-authentication-design.md b/docs/superpowers/specs/2026-04-03-authentication-design.md new file mode 100644 index 0000000..6efa3d7 --- /dev/null +++ b/docs/superpowers/specs/2026-04-03-authentication-design.md @@ -0,0 +1,133 @@ +# Authentication — Design Spec + +## Overview + +Add authentication to GearBox with a public-read, authenticated-write model. Web UI uses cookie-based sessions. Programmatic access (MCP server, scripts) uses API keys. Single-user app — one admin account, created on first setup. + +## Database Schema + +### `users` table + +```typescript +export const users = sqliteTable("users", { + id: integer("id").primaryKey({ autoIncrement: true }), + username: text("username").notNull().unique(), + passwordHash: text("password_hash").notNull(), + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), +}); +``` + +### `sessions` table + +```typescript +export const sessions = sqliteTable("sessions", { + id: text("id").primaryKey(), // random token + userId: integer("user_id").notNull().references(() => users.id), + expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), +}); +``` + +### `apiKeys` table + +```typescript +export const apiKeys = sqliteTable("api_keys", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull(), + keyHash: text("key_hash").notNull(), + keyPrefix: text("key_prefix").notNull(), // first 8 chars for identification + createdAt: integer("created_at", { mode: "timestamp" }).notNull(), +}); +``` + +## Password Hashing + +Use `Bun.password.hash()` and `Bun.password.verify()` with argon2 (Bun's default). No external dependencies needed. + +## Auth Middleware + +Hono middleware applied to all write endpoints (POST, PUT, DELETE): + +1. Check for `X-API-Key` header — if present, hash and compare against `api_keys` table +2. Check for session cookie (`gearbox_session`) — if present, look up in `sessions` table, verify not expired +3. If neither is valid, return `401 Unauthorized` + +GET endpoints remain public — no middleware applied. + +**Exempt from auth:** `/api/auth/login`, `/api/auth/setup`, and `/api/auth/me` (GET) are not protected by the write middleware. + +**Before setup:** If no user account exists yet, write endpoints return `403` with `{ error: "setup_required" }` so the frontend can prompt account creation. + +## API Endpoints + +### Auth routes (`/api/auth`) + +- `POST /api/auth/login` — accepts `{ username, password }`, creates session, sets cookie +- `POST /api/auth/logout` — clears session cookie, deletes session record +- `GET /api/auth/me` — returns current user info if authenticated, or `null` +- `POST /api/auth/setup` — initial account creation (only works if no users exist) +- `PUT /api/auth/password` — change password (requires current password) + +### API key routes (`/api/auth/keys`) — all authenticated + +- `GET /api/auth/keys` — list API keys (name, prefix, createdAt — never the full key) +- `POST /api/auth/keys` — create new key, returns the full key once +- `DELETE /api/auth/keys/:id` — revoke a key + +## Session Management + +- Session token: 32-byte random hex string +- Cookie: `gearbox_session`, httpOnly, sameSite=lax, path=/ +- Session expiry: 30 days, refreshed on each authenticated request +- Sessions stored in SQLite — simple cleanup of expired rows + +## Frontend Changes + +### Login button (top-right, Gitea-style) + +- When not logged in: "Sign in" button in the header/navbar +- When logged in: username display with dropdown (logout, settings link) + +### Login page + +- Route: `/login` +- Simple form: username + password + submit +- Redirects back to previous page on success +- Error message on invalid credentials + +### Initial setup + +- If `GET /api/auth/me` returns `null` and no users exist, show a setup prompt +- Setup form: create username + password +- Only shown once — after account creation, normal login flow applies + +### Conditional UI + +- Add/edit/delete buttons: hidden when not authenticated +- Forms (ItemForm, CandidateForm, etc.): only accessible when authenticated +- The app is fully browseable without login — you just can't modify anything + +### Auth state + +- `useAuth` hook using React Query: calls `GET /api/auth/me` +- Returns `{ user, isAuthenticated, login, logout }` +- Cached and invalidated on login/logout + +### Settings page additions + +- API key management section: list, create, revoke +- Change password form + +## Testing + +- Auth service tests: login, logout, session creation/validation, password change +- API key tests: create, verify, revoke +- Middleware tests: write endpoints reject without auth, read endpoints work without auth +- Setup flow test: first-user creation + +## Security Considerations + +- Passwords hashed with argon2 via Bun built-in +- Session tokens are cryptographically random +- API keys hashed before storage (only shown once on creation) +- httpOnly cookies prevent XSS access to session +- No CORS changes needed (single-origin app) diff --git a/docs/superpowers/specs/2026-04-03-image-url-fetching-design.md b/docs/superpowers/specs/2026-04-03-image-url-fetching-design.md new file mode 100644 index 0000000..b474c0f --- /dev/null +++ b/docs/superpowers/specs/2026-04-03-image-url-fetching-design.md @@ -0,0 +1,87 @@ +# Image URL Fetching — Design Spec + +## Overview + +Add the ability to fetch images from external URLs via the API, download them to local storage, and preserve the original source URL as metadata. API-only feature — no frontend changes. + +## New Endpoint + +### `POST /api/images/from-url` + +**Request:** + +```json +{ "url": "https://example.com/photo.jpg" } +``` + +**Validation (Zod):** + +- `url` — valid URL string, required + +**Server-side behavior:** + +1. Fetch the URL with a 10-second timeout +2. Check response `Content-Type` is one of: `image/jpeg`, `image/png`, `image/webp` +3. Check `Content-Length` does not exceed 5MB (match existing upload limit) +4. Stream response body to `uploads/` directory using existing naming: `${Date.now()}-${randomUUID()}.${ext}` +5. If any check fails, return 400 with descriptive error + +**Response:** + +```json +{ "filename": "1712160000000-abc123.jpg", "sourceUrl": "https://example.com/photo.jpg" } +``` + +Callers can use `filename` for `imageFilename` and `sourceUrl` for `imageSourceUrl` when creating/updating items or candidates. + +**Error responses:** + +- `400` — invalid URL, unsupported content type, file too large, fetch failed +- `500` — server error during download/save + +## Schema Changes + +### `items` table + +Add column: + +``` +imageSourceUrl: text("image_source_url") // nullable +``` + +### `threadCandidates` table + +Add column: + +``` +imageSourceUrl: text("image_source_url") // nullable +``` + +### Zod schemas + +Add `imageSourceUrl: z.string().url().optional()` to: + +- `createItemSchema` +- `updateItemSchema` +- `createCandidateSchema` +- `updateCandidateSchema` + +### Types + +Types are inferred from Zod schemas and Drizzle tables — no manual updates needed. + +## Existing Behavior Unchanged + +- `POST /api/images` (file upload) remains as-is +- All existing image display, cleanup, and serving logic unchanged +- `imageFilename` continues to work identically + +## Test Helper Updates + +Add `image_source_url TEXT` column to the `items` and `thread_candidates` CREATE TABLE statements in `tests/helpers/db.ts`. + +## Testing + +- Service test: fetch from a valid URL, verify file saved and filename returned +- Route test: POST to `/api/images/from-url` with valid/invalid URLs +- Validation tests: wrong content type, oversized image, invalid URL format, timeout diff --git a/docs/superpowers/specs/2026-04-03-mcp-server-design.md b/docs/superpowers/specs/2026-04-03-mcp-server-design.md new file mode 100644 index 0000000..b21ee35 --- /dev/null +++ b/docs/superpowers/specs/2026-04-03-mcp-server-design.md @@ -0,0 +1,158 @@ +# MCP Server — Design Spec + +## Overview + +Built-in MCP server running inside the GearBox Hono process, exposed via SSE transport at `/mcp`. Provides tools for managing the full gear collection with workflow guidance emphasizing research threads. Authenticates via API key. + +## Transport & Configuration + +- **Transport:** SSE or Streamable HTTP at `/mcp` (use whichever the MCP SDK supports best at implementation time — the newer spec favors Streamable HTTP) +- **Enabled by default**, disable with `GEARBOX_MCP=false` env var +- **Authentication:** API key passed in MCP client config, sent as `X-API-Key` header +- **SDK:** `@modelcontextprotocol/sdk` TypeScript package + +## Integration with Hono + +The MCP server mounts as a route on the existing Hono app: + +```typescript +// src/server/index.ts +if (process.env.GEARBOX_MCP !== "false") { + app.route("/mcp", mcpRoutes); +} +``` + +The MCP route handler bridges SSE transport to the MCP server instance, which calls GearBox services directly (not via HTTP) for efficiency. + +## Tools + +All tools include descriptive names and descriptions that guide Claude toward the research thread workflow. + +### Item Tools + +| Tool | Description | +|------|-------------| +| `list_items` | List all items in the gear collection. Optionally filter by category. | +| `get_item` | Get details of a specific item by ID. | +| `create_item` | Add a new item to the gear collection. Use this for items you've already decided on. For items you're still researching, use create_thread instead. | +| `update_item` | Update an existing item's details. | +| `delete_item` | Remove an item from the collection. | + +### Category Tools + +| Tool | Description | +|------|-------------| +| `list_categories` | List all gear categories. | +| `create_category` | Create a new category for organizing gear. | + +### Thread Tools (Primary Workflow) + +| Tool | Description | +|------|-------------| +| `list_threads` | List all research threads. Threads are the recommended way to evaluate gear purchases — create a thread, add candidates, compare them, then resolve to pick a winner. | +| `get_thread` | Get a thread with all its candidates and comparison data. | +| `create_thread` | Start a new research thread for evaluating a gear purchase. This is the preferred workflow: create a thread describing what you need, add candidate products, compare specs/weight/price, then resolve when you've decided. | +| `resolve_thread` | Resolve a thread by picking the winning candidate. This adds the winner to your collection as a new item and marks the thread as resolved. | + +### Candidate Tools + +| Tool | Description | +|------|-------------| +| `add_candidate` | Add a candidate product to a research thread. Include weight, price, pros, cons, and optionally an image URL. | +| `update_candidate` | Update a candidate's details — weight, price, pros, cons, etc. | +| `remove_candidate` | Remove a candidate from a research thread. | + +### Setup Tools + +| Tool | Description | +|------|-------------| +| `list_setups` | List all gear setups (named configurations of items). | +| `get_setup` | Get a setup with all its items, total weight, and total cost. | +| `create_setup` | Create a new gear setup. | +| `update_setup` | Update a setup's items or details. | + +### Image Tools + +| Tool | Description | +|------|-------------| +| `upload_image_from_url` | Fetch an image from a URL and attach it to an item or candidate. | + +## Resources + +### `gearbox://collection-summary` + +Provides an overview of the current gear collection: + +- Total items, total weight, total cost +- Items per category +- Active research threads +- Number of setups + +This resource gives Claude context about the collection state before making tool calls. + +## Workflow Guidance + +The MCP server's tool descriptions are crafted to guide Claude toward the research thread pattern: + +1. When the user asks about buying gear, Claude should prefer `create_thread` over `create_item` +2. Candidates are added to threads for comparison before committing +3. `resolve_thread` is the way to finalize a purchase decision +4. Direct `create_item` is for items already owned or decided on + +This guidance lives in the tool descriptions themselves — no separate system prompt needed. The `collection-summary` resource helps Claude understand what's already in the collection. + +## Implementation Structure + +``` +src/server/mcp/ + index.ts — MCP server setup, tool/resource registration + tools/ + items.ts — Item tool handlers + categories.ts — Category tool handlers + threads.ts — Thread + candidate tool handlers + setups.ts — Setup tool handlers + images.ts — Image tool handlers + resources/ + collection.ts — Collection summary resource +``` + +## Client Configuration + +### Claude Code (`.claude/settings.json`) + +```json +{ + "mcpServers": { + "gearbox": { + "type": "sse", + "url": "http://localhost:3000/mcp", + "headers": { + "X-API-Key": "" + } + } + } +} +``` + +### Claude Desktop + +```json +{ + "mcpServers": { + "gearbox": { + "type": "sse", + "url": "http://localhost:3000/mcp", + "headers": { + "X-API-Key": "" + } + } + } +} +``` + +## Testing + +- Tool handler tests: each tool with valid/invalid inputs +- Auth test: requests without API key are rejected +- Resource test: collection summary returns accurate data +- Integration test: create thread -> add candidates -> resolve -> verify item created