commit 3a28426bf417f3d82206a804775c4e84732f41f7 Author: wehub-resource-sync Date: Mon Jul 13 12:23:40 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.agents/docs/deployment.md b/.agents/docs/deployment.md new file mode 100644 index 0000000..ef83530 --- /dev/null +++ b/.agents/docs/deployment.md @@ -0,0 +1,169 @@ +# InsForge Deployment - Agent Documentation + +## Overview + +Deploy frontend applications to InsForge with the `create-deployment` MCP tool. The tool uploads the source directory through InsForge's direct deployment flow, starts a Vercel-backed production build, and records the result in `deployments.runs`. + +Use the source directory, not a pre-built artifact directory. The backend validates the manifest, streams each file to the provider, verifies file size and SHA-1 digest, then starts the deployment after all files are uploaded. + +The REST API still supports the legacy source-zip flow for older clients. Do not use it from agents unless the direct MCP tool is unavailable. + +## Deploy With MCP + +Call `create-deployment` with: + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `sourceDirectory` | Yes | Absolute path to the app source directory, for example `/Users/me/project/frontend`. | +| `projectSettings.buildCommand` | No | Build command, for example `npm run build`. Omit to use provider defaults. | +| `projectSettings.outputDirectory` | No | Static output directory, for example `dist` or `build`. | +| `projectSettings.installCommand` | No | Install command, for example `npm install` or `pnpm install`. | +| `projectSettings.devCommand` | No | Development command metadata for framework-aware builds. | +| `projectSettings.rootDirectory` | No | Root directory inside the uploaded source tree. | +| `envVars` | No | Array of `{ "key": "...", "value": "..." }` variables to create or update before build. | +| `meta` | No | String key-value metadata for provider deployment creation. | + +Example: + +```json +{ + "sourceDirectory": "/Users/me/project/frontend", + "projectSettings": { + "buildCommand": "npm run build", + "outputDirectory": "dist" + }, + "envVars": [ + { + "key": "VITE_INSFORGE_BASE_URL", + "value": "https://your-project.region.insforge.app" + }, + { + "key": "VITE_INSFORGE_ANON_KEY", + "value": "your-anon-key" + } + ], + "meta": { + "source": "agent" + } +} +``` + +Important: + +- `sourceDirectory` must be an absolute path. +- Upload app source files, not only `dist`, unless the project is intentionally a plain static site. +- Include framework files needed by Vercel, such as `package.json`, lock file, framework config, and `vercel.json` when needed. +- Prefix browser-exposed variables correctly, for example `VITE_` for Vite and `NEXT_PUBLIC_` for Next.js. +- Do not put service-role keys, admin tokens, or private provider keys in browser-exposed variables. +- Tailwind projects should stay on Tailwind CSS 3.4 unless the app already supports v4. + +## What The Tool Does + +The current deployment path maps to these backend steps: + +1. Register a direct upload manifest in `POST /api/deployments/direct`. +2. Upload each file with `PUT /api/deployments/:id/files/:fileId/content`. +3. Start the provider build with `POST /api/deployments/:id/start`. +4. Poll or sync status until the run reaches `READY`, `ERROR`, or `CANCELED`. + +The direct file upload endpoint requires `Content-Type: application/octet-stream`. The backend rejects missing files, size mismatches, SHA mismatches, invalid relative paths, and attempts to start before every file has `uploaded_at`. + +## Check Deployment Status + +Use the Dashboard, MCP SQL tools, or the REST API. For SQL: + +```sql +SELECT id, provider_deployment_id, status, url, metadata, created_at, updated_at +FROM deployments.runs +ORDER BY created_at DESC +LIMIT 5; +``` + +To inspect an interrupted direct upload: + +```sql +SELECT file_path, size_bytes, uploaded_at +FROM deployments.files +WHERE deployment_id = '' +ORDER BY file_path; +``` + +If a deployment has a provider ID but the status looks stale, sync it through the API: + +```http +POST /api/deployments//sync +``` + +## Status Values + +| Status | Description | +|--------|-------------| +| `WAITING` | Run exists and is waiting for source uploads. | +| `UPLOADING` | Source files are uploading or provider deployment creation is in progress. | +| `QUEUED` | Provider accepted the deployment and queued the build. | +| `BUILDING` | Provider is building the app. | +| `READY` | Deployment completed. The `url` column contains the live URL. | +| `ERROR` | Upload, build, provider, or webhook processing failed. | +| `CANCELED` | Deployment was canceled. | + +## Get The Live URL + +After status is `READY`, read the URL: + +```sql +SELECT url +FROM deployments.runs +WHERE id = ''; +``` + +The Dashboard may show a friendlier domain when a custom `.insforge.site` slug or user-owned domain is configured. + +## Environment Variables + +You can pass `envVars` during deployment or manage them from the Dashboard. Deployment env vars are Vercel encrypted environment variables, not database rows. + +Rules: + +- Lists show keys and metadata only. Values are hidden unless a single variable is fetched for editing. +- Duplicate keys in one create-or-update request are rejected. +- Values are applied to `production`, `preview`, and `development` targets. +- Use public prefixes only for values that browser code may read. + +## SPA Routing + +For React, Vue, Svelte, or other single-page apps that need client-side routing, add `vercel.json` at the app root: + +```json +{ + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} +``` + +Do not add this rewrite to frameworks that already own routing, such as Next.js. + +## Common Failures + +| Symptom | Check | +|---------|-------| +| Deployment service is not configured | Self-hosted backend needs `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, and `VERCEL_PROJECT_ID`. | +| Upload fails with content type error | Direct file upload must use `application/octet-stream`. | +| Upload fails with size or SHA mismatch | Recompute the manifest from the exact bytes being uploaded. | +| Start fails before provider build | Query `deployments.files` and confirm every row has `uploaded_at`. | +| Build cannot find scripts | Confirm `package.json`, lock file, root directory, and build command. | +| SPA route returns 404 | Add the `vercel.json` rewrite for static SPA apps. | +| Provider rate limit | Retry the failed upload/start step with a fresh request. | + +## Quick Reference + +| Task | Preferred tool | +|------|----------------| +| Deploy app | `create-deployment` MCP tool | +| Check latest runs | Dashboard or `SELECT * FROM deployments.runs ORDER BY created_at DESC` | +| Inspect direct file upload state | `SELECT * FROM deployments.files WHERE deployment_id = '...'` | +| Manage env vars | Dashboard Sites -> Environment Variables | +| Manage domains | Dashboard Sites -> Domains | diff --git a/.agents/docs/insforge-instructions-sdk.md b/.agents/docs/insforge-instructions-sdk.md new file mode 100644 index 0000000..186bbf5 --- /dev/null +++ b/.agents/docs/insforge-instructions-sdk.md @@ -0,0 +1,131 @@ +# InsForge SDK Documentation - Overview + +## What is InsForge? + +Backend-as-a-service (BaaS) platform providing: + +- **Database**: PostgreSQL with PostgREST API +- **Authentication**: Email/password + OAuth (Google, GitHub) +- **Storage**: File upload/download +- **AI**: OpenRouter key provisioning and model catalog for direct OpenAI-compatible integrations +- **Functions**: Serverless function deployment +- **Realtime**: WebSocket pub/sub (database + client events) + +## Installation + +The following is a step-by-step guide to installing and using the InsForge TypeScript SDK for Web applications. If you are building other types of applications, please refer to: +- [Swift SDK documentation](/sdks/swift/overview) for iOS, macOS, tvOS, and watchOS applications. +- [Kotlin SDK documentation](/sdks/kotlin/overview) for Android applications. +- [REST API documentation](/sdks/rest/overview) for direct HTTP API access. + +### 🚨 CRITICAL: Follow these steps in order + +### Step 1: Download Template + +Use the `download-template` MCP tool to create a new project with your backend URL and anon key pre-configured. + +### Step 2: Install SDK + +```bash +npm install @insforge/sdk@latest +``` + +### Step 3: Create SDK Client + +You must create a client instance using `createClient()` with your base URL and anon key: + +```javascript +import { createClient } from '@insforge/sdk'; + +const client = createClient({ + baseUrl: 'https://your-app.region.insforge.app', // Your InsForge backend URL + anonKey: 'your-anon-key-here' // Get this from backend metadata +}); + +``` + +**API BASE URL**: Your API base URL is `https://your-app.region.insforge.app`. + +## Getting Detailed Documentation + +### 🚨 CRITICAL: Always Fetch Documentation Before Writing Code + +InsForge provides official SDKs and REST APIs, use them to interact with InsForge services from your application code. + +- [TypeScript SDK](/sdks/typescript/overview) - JavaScript/TypeScript +- [Swift SDK](/sdks/swift/overview) - iOS, macOS, tvOS, and watchOS +- [Kotlin SDK](/sdks/kotlin/overview) - Android and Kotlin Multiplatform +- [REST API](/sdks/rest/overview) - Direct HTTP API access + +Before writing or editing any InsForge integration code, you **MUST** call the `fetch-docs` or `fetch-sdk-docs` MCP tool to get the latest SDK documentation. This ensures you have accurate, up-to-date implementation patterns. + +### Use the InsForge `fetch-docs` MCP tool to get specific SDK documentation: + +Available documentation types: + +- `"instructions"` - Essential backend setup (START HERE) +- `"real-time"` - Real-time pub/sub (database + client events) via WebSockets +- `"db-sdk-typescript"` - Database operations with TypeScript SDK +- **Authentication** - Choose based on implementation: + - `"auth-sdk-typescript"` - TypeScript SDK methods for custom auth flows + - `"auth-components-react"` - Pre-built auth UI for React+Vite (single-page app) + - `"auth-components-react-router"` - Pre-built auth UI for React(Vite+React Router) (multi-page app) + - `"auth-components-nextjs"` - Pre-built auth UI for Next.js (SSR app) +- `"storage-sdk"` - File storage operations +- `"functions-sdk"` - Serverless functions invocation +- `"ai-integration-sdk"` - AI integration with the provisioned OpenRouter key and OpenAI SDK +- `"deployment"` - Deploy frontend applications via MCP tool +- `"payments"` - Stripe Checkout, Billing Portal, webhook projections, and fulfillment patterns + +These docs are mostly for the TypeScript SDK. For other languages, you can also use the `fetch-sdk-docs` MCP tool to get specific documentation. + +### Use the InsForge `fetch-sdk-docs` MCP tool to get specific SDK documentation + +You can fetch SDK documentation using the `fetch-sdk-docs` MCP tool with a specific feature type and language. + +Available feature types: +- `db` - Database operations +- `storage` - File storage operations +- `functions` - Serverless functions invocation +- `auth` - User authentication +- `ai` - AI integration with the provisioned OpenRouter key and OpenAI SDK +- `realtime` - Real-time pub/sub (database + client events) via WebSockets +- `payments` - Stripe Checkout and Billing Portal with webhook-based fulfillment + +Available languages: +- `typescript` - JavaScript/TypeScript SDK +- `swift` - Swift SDK (for iOS, macOS, tvOS, and watchOS) +- `kotlin` - Kotlin SDK (for Android and JVM applications) +- `rest-api` - REST API + +Payments currently has TypeScript SDK docs only. Use the Payments API reference for non-TypeScript clients. + +## When to Use SDK vs MCP Tools + +### Always SDK for Application Logic: + +- Authentication (register, login, logout, profiles) +- Database CRUD (select, insert, update, delete) +- Storage operations (upload, download files) +- AI integration via the provisioned OpenRouter key with the OpenAI SDK or OpenRouter HTTP API +- Serverless function invocation +- Payments checkout and customer portal session creation + +### Use MCP Tools for Infrastructure: + +- Project scaffolding (`download-template`) - Download starter templates with InsForge integration +- Backend setup and metadata (`get-backend-metadata`) +- Database schema management (`run-raw-sql`, `get-table-schema`) +- Storage bucket creation (`create-bucket`, `list-buckets`, `delete-bucket`) +- Serverless function deployment (`create-function`, `update-function`, `delete-function`) +- Frontend deployment (`create-deployment`) - Deploy frontend apps to InsForge hosting + +## Important Notes + +- For auth: use `auth-sdk` for custom UI, or framework-specific components for pre-built UI +- SDK returns `{data, error}` structure for all operations +- Database inserts require array format: `[{...}]` +- Serverless functions have one endpoint and do not support nested route paths +- Storage: Upload files to buckets, store URLs in database +- AI integrations should call OpenRouter directly with `baseURL: "https://openrouter.ai/api/v1"` and a server-side `OPENROUTER_API_KEY` +- **EXTRA IMPORTANT**: Use Tailwind CSS 3.4 (do not upgrade to v4). Lock these dependencies in `package.json` diff --git a/.agents/docs/payments-razorpay.md b/.agents/docs/payments-razorpay.md new file mode 100644 index 0000000..14bdd11 --- /dev/null +++ b/.agents/docs/payments-razorpay.md @@ -0,0 +1,297 @@ +# InsForge Razorpay Payments - Agent Documentation + +## Use Razorpay For + +- Razorpay Orders with Razorpay Checkout for one-time payments. +- Razorpay Subscriptions with Razorpay Checkout authorization. +- Razorpay Items and Plans. +- Manual Razorpay webhook setup. +- Cancel, pause, and resume subscription management through backend routes. + +Do not use Stripe Checkout, Stripe Prices, or Billing Portal concepts in a Razorpay flow. Razorpay Checkout runs in the app with `checkout.js`; it does not return a hosted Checkout URL. + +## Before Coding + +1. Use `environment: "test"` unless the user explicitly approves live Razorpay changes. +2. Confirm both Key ID and Key Secret are configured for the target environment. +3. Confirm Items and Plans exist in that same environment for subscription flows. +4. Confirm the Razorpay webhook is manually configured in the Razorpay Dashboard with a public HTTPS URL. +5. Treat Checkout callback verification as immediate authenticity only. Durable fulfillment must come from webhooks. +6. For one-time products, prefer creating Razorpay Items even though Orders can be amount-only. Items keep the catalog visible after sync; Orders are payment attempts. + +## Webhook Setup + +Razorpay webhooks are manual. Generate or view the webhook URL and secret in Dashboard -> Payments -> Settings -> Webhooks. + +Use these backend routes for admin tooling: + +```http +GET /api/payments/razorpay/test/webhook +POST /api/payments/razorpay/test/webhook/rotate-secret +``` + +Recommended active events are the events InsForge handles: + +- `payment.authorized` +- `payment.captured` +- `payment.failed` +- `order.paid` +- `invoice.paid` +- `invoice.expired` +- `refund.created` +- `refund.processed` +- `refund.failed` +- `subscription.created` +- `subscription.activated` +- `subscription.charged` +- `subscription.updated` +- `subscription.cancelled` +- `subscription.paused` +- `subscription.resumed` +- `subscription.halted` +- `subscription.completed` +- `subscription.expired` + +Razorpay can only deliver webhooks to a public HTTPS URL. + +## One-Time Order + +Create an app-owned pending order first. Then create the Razorpay Order with the provider-scoped SDK: + +```typescript +const { data, error } = await insforge.payments.razorpay.createOrder('test', { + amount: 50000, + currency: 'INR', + receipt: 'order_123', + subject: { type: 'team', id: 'team_123' }, + customerEmail: 'buyer@example.com', + notes: { order_id: 'order_123' } +}); + +if (error) throw error; +``` + +If a fulfillment trigger reads `notes.order_id`, pass `notes: { order_id: ... }` when creating the Order or Subscription. + +Open Razorpay Checkout in the frontend with `data.checkoutOptions`. After Checkout returns `razorpay_order_id`, `razorpay_payment_id`, and `razorpay_signature`, verify through the SDK: + +```typescript +await insforge.payments.razorpay.verifyOrder('test', { + orderId: response.razorpay_order_id, + paymentId: response.razorpay_payment_id, + signature: response.razorpay_signature +}); +``` + +Only treat verification as proof that the client return was authentic. Fulfillment should still come from verified Razorpay webhook events. + +## Subscription + +Razorpay subscriptions use Plans, not Stripe Prices. A Plan is a recurring definition around a Razorpay Item. + +Create or sync the plan first, then create the subscription through the provider-scoped SDK: + +```typescript +const { data, error } = await insforge.payments.razorpay.createSubscription('test', { + planId: 'plan_123', + totalCount: 12, + subject: { type: 'team', id: 'team_123' }, + customerEmail: 'buyer@example.com' +}); + +if (error) throw error; +``` + +Open Razorpay Checkout with `data.checkoutOptions.subscription_id`. After Checkout returns the subscription payment signature values, verify through the SDK: + +```typescript +await insforge.payments.razorpay.verifySubscription('test', { + subscriptionId: response.razorpay_subscription_id, + paymentId: response.razorpay_payment_id, + signature: response.razorpay_signature +}); +``` + +Manage the subscription through provider-scoped SDK helpers: + +```typescript +await insforge.payments.razorpay.cancelSubscription('test', 'sub_123', { + cancelAtCycleEnd: false +}); + +await insforge.payments.razorpay.pauseSubscription('test', 'sub_123'); +await insforge.payments.razorpay.resumeSubscription('test', 'sub_123'); +``` + +Razorpay subscription creation evaluates the caller's `INSERT` policy on `payments.razorpay_subscriptions`. Cancel, pause, and resume evaluate `UPDATE` policies. PostgreSQL also applies `SELECT` policies to rows returned by `INSERT/UPDATE ... RETURNING`, so add matching `SELECT` visibility for the same billing subject when a policy probe needs to return the row. Do not let users submit arbitrary subjects unless the app checks that they can manage that billing subject. + +## Fulfillment + +Do not mark orders paid or grant subscription access from Checkout callback verification alone. Use verified Razorpay webhook events for durable fulfillment. Do not attach fulfillment triggers to provider mirror tables such as `payments.razorpay_subscriptions`. + +```sql +CREATE OR REPLACE FUNCTION public.fulfill_razorpay_order() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.provider = 'razorpay' + AND NEW.event_type IN ('payment.captured', 'order.paid', 'invoice.paid') + AND NEW.processing_status = 'processed' + AND COALESCE( + NEW.payload -> 'payload' -> 'payment' -> 'entity' -> 'notes' ->> 'order_id', + NEW.payload -> 'payload' -> 'invoice' -> 'entity' -> 'notes' ->> 'order_id' + ) IS NOT NULL THEN + UPDATE public.orders + SET status = 'paid', + paid_at = COALESCE(NEW.processed_at, NOW()) + WHERE id::text = COALESCE( + NEW.payload -> 'payload' -> 'payment' -> 'entity' -> 'notes' ->> 'order_id', + NEW.payload -> 'payload' -> 'invoice' -> 'entity' -> 'notes' ->> 'order_id' + ) + AND status = 'pending'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +CREATE TRIGGER fulfill_razorpay_order_from_webhook + AFTER INSERT OR UPDATE ON payments.webhook_events + FOR EACH ROW + EXECUTE FUNCTION public.fulfill_razorpay_order(); +``` + +For subscriptions, resolve the billing subject from the subscription entity's `notes` in the event payload — InsForge stamps `insforge_subject_type` and `insforge_subject_id` into notes at subscription creation (and creates the `payments.customer_mappings` row at the same time, so the mapping is also a safe fallback): + +```sql +CREATE OR REPLACE FUNCTION public.grant_razorpay_subscription_access() +RETURNS TRIGGER AS $$ +DECLARE + v_subject_type TEXT; + v_subject_id TEXT; +BEGIN + IF NEW.provider = 'razorpay' + AND NEW.event_type = 'subscription.charged' + AND NEW.processing_status = 'processed' THEN + v_subject_type := NEW.payload -> 'payload' -> 'subscription' -> 'entity' + -> 'notes' ->> 'insforge_subject_type'; + v_subject_id := NEW.payload -> 'payload' -> 'subscription' -> 'entity' + -> 'notes' ->> 'insforge_subject_id'; + + IF v_subject_id IS NULL THEN + SELECT m.subject_type, m.subject_id + INTO v_subject_type, v_subject_id + FROM payments.customer_mappings m + WHERE m.provider = NEW.provider + AND m.environment = NEW.environment + AND m.provider_customer_id = NEW.payload -> 'payload' -> 'subscription' + -> 'entity' ->> 'customer_id'; + END IF; + + IF v_subject_id IS NULL THEN + RAISE WARNING 'Razorpay event % has no resolvable billing subject', NEW.provider_event_id; + RETURN NEW; + END IF; + + -- Branch on the subject type sent at creation; team_id is a UUID here, + -- so the type check also guards the cast. + IF v_subject_type = 'team' THEN + INSERT INTO public.team_entitlements (team_id, plan, active, updated_at) + VALUES (v_subject_id::uuid, 'pro', true, NOW()) + ON CONFLICT (team_id) DO UPDATE SET + plan = EXCLUDED.plan, + active = true, + updated_at = NOW(); + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +CREATE TRIGGER grant_razorpay_subscription_access_from_webhook + AFTER INSERT OR UPDATE ON payments.webhook_events + FOR EACH ROW + EXECUTE FUNCTION public.grant_razorpay_subscription_access(); +``` + +Handle revocation the same way from `subscription.cancelled`, `subscription.halted`, and `subscription.expired`. + +Adapt the payload lookup to the app schema and event shape. Protect app-owned billing tables with RLS. Use `payments.transactions` for dashboard/reporting only. + +## Security + +- Add RLS or server-side membership checks before exposing order or subscription flows for shared subjects. +- Consider RLS on `payments.razorpay_orders` and `payments.razorpay_subscriptions`. +- Do not expose `payments.customers`, `payments.transactions`, or `payments.razorpay_subscriptions` directly to end users. +- Do not write provider-managed payments tables directly. Use the Payments API, Razorpay webhooks, or app-owned trigger targets. +- Notes keys starting with `insforge_` are reserved. + +## Debugging + +Check recent Razorpay order attempts: + +```sql +SELECT id, environment, status, subject_type, subject_id, + order_id, receipt, amount, currency, verified_payment_id, + last_error, created_at, updated_at +FROM payments.razorpay_orders +ORDER BY created_at DESC +LIMIT 20; +``` + +Check recent Razorpay subscriptions: + +```sql +SELECT environment, subscription_id, plan_id, customer_id, status, + subject_type, subject_id, authorization_payment_id, + current_start, current_end, created_at, updated_at +FROM payments.razorpay_subscriptions +ORDER BY created_at DESC +LIMIT 20; +``` + +Check customer mappings: + +```sql +SELECT provider, environment, subject_type, subject_id, provider_customer_id, created_at, updated_at +FROM payments.customer_mappings +WHERE provider = 'razorpay' +ORDER BY updated_at DESC +LIMIT 20; +``` + +Check Razorpay transactions: + +```sql +SELECT provider, environment, type, status, subject_type, subject_id, + provider_object_type, provider_object_id, amount, currency, + paid_at, failed_at, refunded_at, created_at +FROM payments.transactions +WHERE provider = 'razorpay' +ORDER BY created_at DESC +LIMIT 20; +``` + +Check webhook failures: + +```sql +SELECT provider, environment, provider_event_id, event_type, processing_status, + attempt_count, last_error, received_at, processed_at +FROM payments.webhook_events +WHERE provider = 'razorpay' + AND processing_status IN ('failed', 'pending') +ORDER BY received_at DESC +LIMIT 20; +``` + +## Common Failures + +| Symptom | Check | +|---------|-------| +| Order creation fails | Verify Key ID and Key Secret for the environment and confirm amount is in the smallest currency unit. | +| Checkout does not open | Confirm `https://checkout.razorpay.com/v1/checkout.js` is loaded and `checkoutOptions.key` is present. | +| Signature verification fails | Pass the exact order or subscription ID, payment ID, and signature returned by Razorpay Checkout. | +| Webhook signature is invalid | Confirm the Razorpay Dashboard webhook secret matches Dashboard -> Payments -> Settings -> Webhooks for the same `test` or `live` environment, and that the URL ends with `/api/webhooks/razorpay/{environment}`. | +| Subscription creation fails | Verify the Plan exists in the same Razorpay environment and has a valid Item. | +| Payment shows in Razorpay but not InsForge | Check manual webhook setup and `payments.webhook_events`. | +| User can start a subscription for another team | Add RLS or server-side membership checks for the billing subject. | diff --git a/.agents/docs/payments-stripe.md b/.agents/docs/payments-stripe.md new file mode 100644 index 0000000..1da16f1 --- /dev/null +++ b/.agents/docs/payments-stripe.md @@ -0,0 +1,287 @@ +# InsForge Stripe Payments - Agent Documentation + +## Use Stripe For + +- Stripe Checkout for one-time payments. +- Stripe Checkout for subscriptions. +- Stripe Billing Portal links for existing customers. +- Stripe Products and Prices. +- Stripe-managed subscription and invoice lifecycle. + +Do not build raw card collection UI. Do not use Razorpay concepts such as Orders, Items, or Plans in a Stripe flow. + +## Before Coding + +1. Use `environment: "test"` unless the user explicitly approves live Stripe changes. +2. Confirm the Stripe secret key is configured for the target environment. +3. Confirm Product and Price IDs exist in that same environment. +4. Confirm the backend can receive Stripe webhooks. InsForge manages the Stripe webhook endpoint when the backend is reachable. +5. Treat Checkout success URLs as UX redirects only. Fulfillment must come from webhooks. + +Project admins configure Stripe in Dashboard -> Payments -> Settings or with the CLI: + +```bash +npx @insforge/cli payments stripe status +npx @insforge/cli payments stripe config set --environment test sk_test_xxx +npx @insforge/cli payments stripe sync --environment test +npx @insforge/cli payments stripe webhooks configure --environment test +``` + +## Runtime Setup + +Use the TypeScript SDK from application code: + +```typescript +import { createClient } from '@insforge/sdk'; + +const insforge = createClient({ + baseUrl: 'https://your-project.insforge.app', + anonKey: 'your-anon-key' +}); +``` + +Checkout requires an InsForge user token. Guest one-time checkout can use an anonymous InsForge token. API keys are not a replacement because the backend needs user context for `payments.stripe_checkout_sessions`. + +## One-Time Checkout + +Create an app-owned pending order first, then start Checkout: + +```typescript +const { data: order, error: orderError } = await insforge + .from('orders') + .insert([{ user_id: user.id, status: 'pending' }]) + .select() + .single(); + +if (orderError) throw orderError; + +const { data, error } = await insforge.payments.stripe.createCheckoutSession('test', { + mode: 'payment', + lineItems: [{ priceId: 'price_123', quantity: 1 }], + successUrl: `${window.location.origin}/orders/${order.id}`, + cancelUrl: `${window.location.origin}/pricing`, + customerEmail: user.email, + metadata: { order_id: order.id }, + idempotencyKey: `order:${order.id}` +}); + +if (error) throw error; +if (data?.checkoutSession.url) { + window.location.assign(data.checkoutSession.url); +} +``` + +For anonymous one-time purchases, omit `subject` and pass `customerEmail` when available. + +## Subscription Checkout + +Subscriptions require a billing subject. Pick a stable app owner such as user, team, organization, workspace, tenant, or group. + +```typescript +const { data, error } = await insforge.payments.stripe.createCheckoutSession('test', { + mode: 'subscription', + subject: { type: 'team', id: teamId }, + lineItems: [{ priceId: 'price_monthly_123', quantity: 1 }], + successUrl: `${window.location.origin}/billing/success`, + cancelUrl: `${window.location.origin}/billing`, + customerEmail: user.email, + idempotencyKey: `team:${teamId}:pro-monthly` +}); + +if (error) throw error; +if (data?.checkoutSession.url) { + window.location.assign(data.checkoutSession.url); +} +``` + +Do not let users submit arbitrary `subject.type` and `subject.id` values unless the app checks they can manage that billing subject. + +## Customer Portal + +Use Billing Portal after Checkout has created a customer mapping for the subject. + +```typescript +const { data, error } = await insforge.payments.stripe.createCustomerPortalSession('test', { + subject: { type: 'team', id: teamId }, + returnUrl: `${window.location.origin}/billing` +}); + +if (error) { + if ('statusCode' in error && error.statusCode === 404) { + return; + } + + throw error; +} + +if (data?.customerPortalSession.url) { + window.location.assign(data.customerPortalSession.url); +} +``` + +Portal creation requires an authenticated user and an existing `payments.customer_mappings` row for the subject. + +## Fulfillment + +Create triggers from verified Stripe webhook events into app-owned tables. + +Webhook events are verified and processed independently. InsForge commits all rows derived from an event before marking that event `processed`, but there is no ordering guarantee across events: Stripe can deliver `invoice.paid` before `checkout.session.completed`, so rows created by another event (such as `payments.customer_mappings`) may not exist yet when your trigger fires. Resolve the billing subject from the event payload first and use `payments.customer_mappings` as a fallback, exactly as the examples below do. Never let fulfillment skip silently: log or dead-letter events you cannot resolve. + +### One-time payments + +```sql +CREATE OR REPLACE FUNCTION public.fulfill_paid_order() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.provider = 'stripe' + AND NEW.event_type = 'checkout.session.completed' + AND NEW.processing_status = 'processed' + AND (NEW.payload -> 'data' -> 'object' -> 'metadata' ->> 'order_id') IS NOT NULL THEN + UPDATE public.orders + SET status = 'paid', + paid_at = COALESCE(NEW.processed_at, NOW()) + WHERE id::text = NEW.payload -> 'data' -> 'object' -> 'metadata' ->> 'order_id' + AND status = 'pending'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +CREATE TRIGGER fulfill_paid_order_from_stripe_webhook + AFTER INSERT OR UPDATE ON payments.webhook_events + FOR EACH ROW + EXECUTE FUNCTION public.fulfill_paid_order(); +``` + +### Subscriptions + +Subscription events do not carry your app's `metadata`. Resolve the billing subject from the subscription metadata embedded in the event payload — InsForge stamps `insforge_subject_type` and `insforge_subject_id` at checkout, and Stripe snapshots it onto subscription-generated invoices as `parent.subscription_details.metadata`. Check `invoice.metadata` next, then fall back to `payments.customer_mappings` (the same order InsForge uses internally): + +```sql +CREATE OR REPLACE FUNCTION public.grant_subscription_access() +RETURNS TRIGGER AS $$ +DECLARE + v_subject_type TEXT; + v_subject_id TEXT; +BEGIN + IF NEW.provider = 'stripe' + AND NEW.event_type = 'invoice.paid' + AND NEW.processing_status = 'processed' THEN + v_subject_type := COALESCE( + NEW.payload -> 'data' -> 'object' -> 'parent' + -> 'subscription_details' -> 'metadata' ->> 'insforge_subject_type', + NEW.payload -> 'data' -> 'object' -> 'metadata' ->> 'insforge_subject_type' + ); + v_subject_id := COALESCE( + NEW.payload -> 'data' -> 'object' -> 'parent' + -> 'subscription_details' -> 'metadata' ->> 'insforge_subject_id', + NEW.payload -> 'data' -> 'object' -> 'metadata' ->> 'insforge_subject_id' + ); + + IF v_subject_id IS NULL THEN + SELECT m.subject_type, m.subject_id + INTO v_subject_type, v_subject_id + FROM payments.customer_mappings m + WHERE m.provider = NEW.provider + AND m.environment = NEW.environment + AND m.provider_customer_id = NEW.payload -> 'data' -> 'object' ->> 'customer'; + END IF; + + IF v_subject_id IS NULL THEN + RAISE WARNING 'Stripe event % has no resolvable billing subject', NEW.provider_event_id; + RETURN NEW; + END IF; + + -- Branch on the subject type sent at checkout; team_id is a UUID here, + -- so the type check also guards the cast. + IF v_subject_type = 'team' THEN + INSERT INTO public.team_entitlements (team_id, plan, active, updated_at) + VALUES (v_subject_id::uuid, 'pro', true, NOW()) + ON CONFLICT (team_id) DO UPDATE SET + plan = EXCLUDED.plan, + active = true, + updated_at = NOW(); + END IF; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +CREATE TRIGGER grant_subscription_access_from_stripe_webhook + AFTER INSERT OR UPDATE ON payments.webhook_events + FOR EACH ROW + EXECUTE FUNCTION public.grant_subscription_access(); +``` + +Adjust the trigger target to the app-owned entitlement table for the billing subject type used at checkout. Handle revocation the same way from `customer.subscription.deleted` and `customer.subscription.updated` events (`payload -> 'data' -> 'object' -> 'metadata'` holds the subject keys on subscription events). + +Use `payments.transactions` for dashboard/reporting only. + +## Security + +- Add RLS or server-side membership checks before exposing checkout or portal flows for shared subjects. +- Consider RLS on `payments.stripe_checkout_sessions` and `payments.stripe_customer_portal_sessions`. +- PostgreSQL applies `SELECT` policies to rows returned by `INSERT ... RETURNING` and idempotent retry lookups. If checkout creation is denied even though an `INSERT` policy exists, add matching `SELECT` visibility for the same billing subject and idempotency key. +- Do not expose `payments.customers`, `payments.transactions`, `payments.stripe_subscriptions`, or `payments.stripe_subscription_items` directly to end users. +- Do not write Stripe-managed payments tables directly. Use the Payments API, Stripe webhooks, or app-owned trigger targets. +- Metadata keys starting with `insforge_` are reserved. + +## Debugging + +Check recent checkout attempts: + +```sql +SELECT id, environment, mode, status, payment_status, subject_type, subject_id, + checkout_session_id, customer_id, subscription_id, + last_error, created_at, updated_at +FROM payments.stripe_checkout_sessions +ORDER BY created_at DESC +LIMIT 20; +``` + +Check customer mappings: + +```sql +SELECT provider, environment, subject_type, subject_id, provider_customer_id, created_at, updated_at +FROM payments.customer_mappings +WHERE provider = 'stripe' +ORDER BY updated_at DESC +LIMIT 20; +``` + +Check Stripe transactions: + +```sql +SELECT provider, environment, type, status, subject_type, subject_id, + provider_object_type, provider_object_id, amount, currency, + paid_at, failed_at, refunded_at, created_at +FROM payments.transactions +WHERE provider = 'stripe' +ORDER BY created_at DESC +LIMIT 20; +``` + +Check webhook failures: + +```sql +SELECT provider, environment, provider_event_id, event_type, processing_status, + attempt_count, last_error, received_at, processed_at +FROM payments.webhook_events +WHERE provider = 'stripe' + AND processing_status IN ('failed', 'pending') +ORDER BY received_at DESC +LIMIT 20; +``` + +## Common Failures + +| Symptom | Check | +|---------|-------| +| Checkout returns Stripe key not configured | Configure the correct `test` or `live` Stripe key. | +| Checkout uses the wrong price | Verify the Price ID belongs to the selected environment. | +| Duplicate checkout attempts | Use a stable `idempotencyKey` based on the order, cart, or billing subject. | +| Portal returns not found | The subject has no Stripe customer mapping yet. Have the customer complete Checkout first. | +| Payment shows in Stripe but not InsForge | Check Stripe webhook configuration and `payments.webhook_events`. | +| User can start checkout for another team | Add RLS or server-side membership checks for the billing subject. | diff --git a/.agents/docs/payments.md b/.agents/docs/payments.md new file mode 100644 index 0000000..35a10e4 --- /dev/null +++ b/.agents/docs/payments.md @@ -0,0 +1,30 @@ +# InsForge Payments - Agent Documentation + +Use a provider-specific guide before coding. Stripe and Razorpay have different payment models, runtime APIs, table names, and webhook setup requirements. + +## Provider guides + +- [Stripe payments](./payments-stripe.md): Stripe Checkout, Billing Portal, Products, Prices, Subscriptions, managed webhooks. +- [Razorpay payments](./payments-razorpay.md): Razorpay Orders, Checkout script, Items, Plans, Subscriptions, manual webhooks. + +## Shared rules + +1. Use `environment: "test"` unless the user explicitly approves live payment changes. +2. Never put provider secret keys in frontend code or browser-exposed deployment variables. +3. Do not fulfill from Stripe success URLs or Razorpay Checkout callbacks alone. +4. Fulfillment should run from verified rows in `payments.webhook_events`. +5. Keep user-facing order, credit, and entitlement state in app-owned tables with app-owned RLS. +6. Use `payments.transactions` for dashboard and reporting queries, not as the primary fulfillment contract. +7. Webhook events are processed independently with no cross-event ordering guarantee. Rows derived from an event are committed before that event is marked `processed`, but rows derived from other events (such as `payments.customer_mappings` or provider mirrors) may not exist yet. Fulfillment triggers must resolve billing subjects from the event payload first and treat lookups into rows owned by other events as fallbacks. + +## Shared tables + +| Table | Use | +|-------|-----| +| `payments.provider_connections` | Provider/environment connection, sync, and webhook setup status. | +| `payments.customer_mappings` | Billing subject to provider customer ID mapping. | +| `payments.customers` | Admin/customer mirror for dashboard visibility. | +| `payments.webhook_events` | Verified provider event ledger. Build durable fulfillment triggers here. | +| `payments.transactions` | Dashboard/reporting projection for successful, failed, pending, and refunded transactions. | + +Legacy `payments.payment_history` rows are migrated into `payments.transactions`, but triggers on `payment_history` are not migrated automatically. Recreate fulfillment triggers on `payments.webhook_events`. diff --git a/.agents/docs/real-time.md b/.agents/docs/real-time.md new file mode 100644 index 0000000..61e5c25 --- /dev/null +++ b/.agents/docs/real-time.md @@ -0,0 +1,268 @@ +# InsForge Realtime - Agent Documentation + +## Use Realtime For + +- Live client updates from database changes. +- Client-to-client broadcasts on named channels. +- Ephemeral presence on subscribed channels. +- Webhook fan-out for messages published to a channel. + +Realtime delivers events to subscribed clients and configured webhook URLs. If server-side code must perform work after a database change, such as sending email, writing another table, or calling an API, put that work in an Edge Function and invoke it from a database trigger. + +## Mental Model + +1. Create channel patterns in `realtime.channels`. +2. Publish messages into `realtime.messages`. +3. Postgres triggers `pg_notify('realtime_message', message_id)`. +4. The backend loads the message, checks the channel is enabled, and delivers it to Socket.IO subscribers plus channel webhooks. +5. Delivery stats are written back to the message row. + +Channel patterns use SQL `LIKE`: `order:%` matches `order:123`. Use `:` as the separator and `%` as the wildcard. Do not use `_`. + +## Backend Setup + +### 1. Create Channel Patterns + +```sql +INSERT INTO realtime.channels (pattern, description, enabled) +VALUES + ('orders', 'Global order events', true), + ('order:%', 'Per-order events', true), + ('chat:%', 'Chat room events', true); +``` + +You can also create channels in the Dashboard Realtime page. + +### 2. Publish Database Changes + +Create a trigger on the app table you want to watch. In the trigger function, call `realtime.publish(channel, event, payload)` to choose the channel, event name, and payload. + +```sql +CREATE OR REPLACE FUNCTION notify_order_status() +RETURNS TRIGGER AS $$ +BEGIN + PERFORM realtime.publish( + 'order:' || NEW.id::text, + 'status_changed', + jsonb_build_object( + 'id', NEW.id, + 'status', NEW.status, + 'updatedAt', NEW.updated_at + ) + ); + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +CREATE TRIGGER order_status_realtime + AFTER UPDATE OF status ON orders + FOR EACH ROW + WHEN (OLD.status IS DISTINCT FROM NEW.status) + EXECUTE FUNCTION notify_order_status(); +``` + +For delete events, use `OLD` instead of `NEW`. + +### 3. Add Access Control When Needed + +Realtime is open by default. `anon` and `authenticated` can subscribe to enabled channels and publish to channels they joined. + +To restrict access: + +```sql +ALTER TABLE realtime.channels ENABLE ROW LEVEL SECURITY; +ALTER TABLE realtime.messages ENABLE ROW LEVEL SECURITY; +``` + +Subscribe is controlled by `SELECT` policies on `realtime.channels`: + +```sql +CREATE POLICY "users_subscribe_own_orders" +ON realtime.channels +FOR SELECT +TO authenticated +USING ( + pattern = 'order:%' + AND EXISTS ( + SELECT 1 + FROM orders + WHERE id = NULLIF(split_part(realtime.channel_name(), ':', 2), '')::uuid + AND user_id = auth.uid() + ) +); +``` + +Publish is controlled by `INSERT` policies on `realtime.messages`: + +```sql +CREATE POLICY "members_publish_chat" +ON realtime.messages +FOR INSERT +TO authenticated +WITH CHECK ( + channel_name LIKE 'chat:%' + AND EXISTS ( + SELECT 1 + FROM chat_members + WHERE room_id = NULLIF(split_part(channel_name, ':', 2), '')::uuid + AND user_id = auth.uid() + ) +); +``` + +Use `realtime.channel_name()` in subscribe policies because `realtime.channels` stores patterns, while the client requests a resolved channel such as `order:123`. + +## Frontend SDK Pattern + +```typescript +import { createClient } from '@insforge/sdk'; + +const insforge = createClient({ + baseUrl: 'https://your-project.insforge.app', + anonKey: 'your-anon-key' +}); + +insforge.realtime.on('error', ({ channel, code, message }) => { + console.error(channel, code, message); +}); + +await insforge.realtime.connect(); + +const response = await insforge.realtime.subscribe(`order:${orderId}`); + +if (!response.ok) { + throw new Error(response.error.message); +} + +insforge.realtime.on('status_changed', (payload) => { + console.log(payload.status); + console.log(payload.meta.messageId); +}); +``` + +### SDK Methods + +| Task | Method | +|------|--------| +| Connect | `await insforge.realtime.connect()` | +| Subscribe | `await insforge.realtime.subscribe(channel)` | +| Publish | `await insforge.realtime.publish(channel, event, payload)` | +| Listen | `insforge.realtime.on(event, callback)` | +| Listen once | `insforge.realtime.once(event, callback)` | +| Remove listener | `insforge.realtime.off(event, callback)` | +| Unsubscribe | `insforge.realtime.unsubscribe(channel)` | +| Disconnect | `insforge.realtime.disconnect()` | +| List local subscriptions | `insforge.realtime.getSubscribedChannels()` | + +Client publish requires a successful subscription to the same channel first. + +## Raw Socket.IO Contract + +Use this only when the SDK is not available. + +```typescript +import { io } from 'socket.io-client'; + +const socket = io('https://your-project.insforge.app', { + auth: { + token: '' + } +}); + +socket.emit('realtime:subscribe', { channel: 'chat:room-1' }, (response) => { + if (!response.ok) { + console.error(response.error); + } +}); + +socket.on('new_message', (message) => { + console.log(message); +}); + +socket.emit('realtime:publish', { + channel: 'chat:room-1', + event: 'new_message', + payload: { text: 'Hello' } +}); +``` + +Events: + +| Event | Direction | +|-------|-----------| +| `realtime:subscribe` | Client to server | +| `realtime:unsubscribe` | Client to server | +| `realtime:publish` | Client to server | +| Custom event name | Server to client | +| `presence:join` | Server to client | +| `presence:leave` | Server to client | +| `realtime:error` | Server to client | + +## Presence + +`subscribe()` returns: + +```json +{ + "ok": true, + "channel": "chat:room-1", + "presence": { + "members": [ + { + "type": "user", + "presenceId": "user-id", + "joinedAt": "2026-04-25T17:00:00.000Z" + } + ] + } +} +``` + +Listen for `presence:join` and `presence:leave` to keep local online state current. Presence is in-memory online state, not durable membership. + +## Webhooks + +Channel `webhookUrls` receive the message payload as the request body. + +Headers: + +| Header | Meaning | +|--------|---------| +| `X-InsForge-Event` | Event name | +| `X-InsForge-Channel` | Resolved channel name | +| `X-InsForge-Message-Id` | Message UUID | + +Webhook delivery counts appear in message history as `whAudienceCount` and `whDeliveredCount`. + +## Message Retention + +`realtime.config.retention_days` controls cleanup: + +- `NULL`: keep messages indefinitely. +- Positive integer: delete messages older than that many days. + +The cleanup job runs daily through pg_cron. + +## Dashboard And REST Checks + +Use Dashboard Realtime pages to verify: + +- Channels: patterns, enabled state, webhooks. +- Messages: payloads, sender type, WebSocket audience, webhook delivery. +- Permissions: RLS policies for subscribe and publish. +- Settings: message retention. + +REST endpoints: + +| Endpoint | Purpose | +|----------|---------| +| `GET /api/realtime/channels` | List channels | +| `POST /api/realtime/channels` | Create channel | +| `PUT /api/realtime/channels/{id}` | Update channel | +| `DELETE /api/realtime/channels/{id}` | Delete channel | +| `GET /api/realtime/messages` | List message history | +| `DELETE /api/realtime/messages` | Clear all message history | +| `GET /api/realtime/messages/stats` | Message stats | +| `GET /api/realtime/permissions` | Realtime RLS policies | +| `GET /api/realtime/config` | Retention config | +| `PATCH /api/realtime/config` | Update retention config | diff --git a/.agents/skills/insforge-dev/SKILL.md b/.agents/skills/insforge-dev/SKILL.md new file mode 100644 index 0000000..6df9eb3 --- /dev/null +++ b/.agents/skills/insforge-dev/SKILL.md @@ -0,0 +1,73 @@ +--- +name: insforge-dev +description: Use this skill set when contributing to the InsForge monorepo itself. This is for InsForge maintainers and contributors editing the platform, the shared dashboard package, the self-hosting shell, the UI library, shared schemas, tests, or docs. +--- + +# InsForge Dev + +Use this skill set for work inside the InsForge repository. + +Then use the narrowest package skill that matches the task: + +- `backend` +- `dashboard` +- `ui` +- `shared-schemas` +- `docs` + +Use the cross-repo release workflow skill when preparing an OSS PR: + +- `e2e-testing` + +## Core Rules + +1. Identify the package boundary before editing. + - `backend/`: API, auth, database, providers, realtime, schedules + - `packages/dashboard/`: publishable dashboard package that supports `self-hosting` and `cloud-hosting` modes + - `frontend/`: local React + Vite shell that mounts `packages/dashboard/` in `self-hosting` mode + - `packages/shared-schemas/`: cross-package contracts + - `packages/ui/`: reusable design-system primitives + - `docs/`: product and agent-facing documentation + +2. Put code in the narrowest correct layer. + - Contract change: `packages/shared-schemas/` first, then consumers. + - Backend behavior: route -> service -> provider/infra. + - Shared dashboard behavior, routes, features, exports, and host contracts: `packages/dashboard/`. + - Self-hosting-only bootstrap, env wiring, and shell styling: `frontend/`. + - Reusable primitive: `packages/ui/` first. + +3. Preserve repo conventions. + - Backend TS source uses ESM-style `.js` import specifiers. + - Backend success responses usually return raw JSON, not `{ data }`. + - Backend validation commonly uses shared Zod schemas plus `AppError`. + - Dashboard data access goes through `apiClient` and React Query. + - Dashboard frontend tests are split into Vitest unit tests, Vitest component tests, and Playwright UI smoke tests. + - Shared payloads belong in `@insforge/shared-schemas`. + - Never use the TypeScript `any` type. Prefer precise types, schema-derived types, `unknown`, or generics. + +4. Do not confuse repo development with app development on InsForge. + - This repo contains the platform, the publishable dashboard package, and a local shell for self-hosting mode. + - Keep guidance focused on maintaining InsForge itself. + +## Finish Rules + +- Run the smallest validation that gives confidence for the change. +- Use repo-level checks like `npm run lint`, `npm run build`, `npm run typecheck`, and `npm test` when the change crosses package boundaries — each is wired through Turborepo (`turbo run `) and covers every workspace, including `packages/dashboard/` and `packages/shared-schemas/`. +- For dashboard UI behavior changes, choose the lowest useful frontend test layer from the `dashboard` skill and run that command before reporting back. +- Use the package-specific validation steps in the child skill when the work is isolated to one package. +- When reporting back, state what changed, what you validated, and what you could not validate. + +## Pre-PR Checklist (Mandatory Before Pushing to a PR) + +Before opening a PR or pushing new commits to an existing PR branch, run **all** of the following from the repo root and do not proceed while any of them fail on files your change touches: + +1. `npx turbo run typecheck` — must pass across all packages. +2. `npx turbo run lint` — must pass. If the failure is pre-existing in `main` and unrelated to your change, scope it: + - Run `npx eslint ` and confirm your files are clean. + - Call out the pre-existing debt in the PR body so reviewers know it is not yours. + - Auto-fixable prettier/eslint errors in your own diff must be fixed (`npx eslint --fix ` or `npm run format`). +3. `npx turbo run test` (or the package-specific test command) — all tests must pass, including any new tests you added for the change. +4. `npx turbo run build` if routing, config, schemas, or cross-package exports changed. +5. Use `e2e-testing` to run the deterministic cross-repo E2E gate before opening, updating, or submitting the InsForge OSS PR. + +Never push with failing checks on files you touched, even if CI would catch them later. CI failures slow reviewers down and the lint fix almost always takes less than a minute locally. diff --git a/.agents/skills/insforge-dev/backend/SKILL.md b/.agents/skills/insforge-dev/backend/SKILL.md new file mode 100644 index 0000000..ea69fc0 --- /dev/null +++ b/.agents/skills/insforge-dev/backend/SKILL.md @@ -0,0 +1,74 @@ +--- +name: backend +description: Use this skill when contributing to InsForge's backend package. This is for maintainers editing backend routes, services, providers, auth, database logic (including RLS-enforced surfaces like storage and realtime), schedules, or backend tests in the InsForge monorepo. +--- + +# InsForge Dev Backend + +Use this skill for `backend/` work in the InsForge repository. + +## Scope + +- `backend/src/api/**` +- `backend/src/services/**` +- `backend/src/providers/**` +- `backend/src/infra/**` +- `backend/tests/**` + +## Working Rules + +1. Keep the route -> service -> provider/infra split intact. + - Routes handle auth, parsing, validation, and delegation. + - Services own business logic and orchestration. + - Providers and infra wrap external systems or lower-level integrations. + - Service layer code should be the only layer that interacts with the core PostgreSQL database. + - Do not put direct database access in routes. + - Do not bypass services when reading from or writing to Postgres. + +2. Follow backend conventions. + - Use ESM-style `.js` import specifiers in TypeScript source. + - InsForge's core database is PostgreSQL. + - InsForge currently runs as a single-instance server, so be careful about introducing logic that assumes distributed coordination, cross-instance locking, or background worker separation. + - Reuse shared schemas from `@insforge/shared-schemas` when contracts cross packages. + - Use `safeParse` plus `AppError` for invalid input. + - Return successful results through `successResponse`. + - Preserve existing auth middleware patterns such as `verifyAdmin`, `verifyUser`, and `verifyApiKey`. + - Never use the TypeScript `any` type. Prefer precise interfaces, schema-derived types, `unknown`, or constrained generics. + - For schema changes, write a new migration file instead of editing database structure manually. + - Put schema changes under `backend/src/infra/database/migrations/`. + +3. Write idempotent migrations. Every SQL migration must be safe to re-run. + - Use `CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`. + - Never use bare `ALTER TABLE ... RENAME TO` — it fails if the target name already exists. Wrap renames in a `DO` block that checks `information_schema.tables` for both source and target. + - Always `DROP TRIGGER IF EXISTS` before `CREATE TRIGGER`. + - Guard data migrations and `DROP COLUMN` behind `information_schema.columns` checks when the column may already be gone. + - Use `ON CONFLICT` or `WHERE NOT EXISTS` for seed `INSERT` statements. + +4. Preserve existing behavior around mutation flows. + - Keep audit logging when surrounding routes already log state changes. + - Keep error handling flowing through shared middleware. + - Do not introduce a new response envelope unless the existing feature already uses one. + - For critical flows with multiple dependent database writes, use an explicit transactional process so the whole operation succeeds or fails together. + - Be especially careful with transactions around auth, secrets, billing-like usage updates, schema changes, and any flow that would leave the system inconsistent if partially applied. + +5. Use Postgres Row Level Security, not app-side filters, for tables accessed via authenticated end-user routes (anything where `req.user` reaches the service layer). RLS-enforced services such as storage, realtime, and payments should use `withUserContext`. Tables accessed only by admin or service-internal paths (audit logs, billing aggregations) don't need RLS. Do not write `WHERE user_id = $1` filters in services; let RLS evaluate `auth.jwt() ->> 'sub'` against the row. + - Plumb identity through `withUserContext(pool, ctx, fn, settings?)` from `services/database/user-context.service.ts`. It opens a transaction, sets `SET LOCAL ROLE` plus the canonical `request.jwt.claims` JSON GUC via `set_config`, applies optional transaction-local settings such as `realtime.channel_name`, runs `fn`, commits on success or rolls back on error, and resets role in `finally` so policies see the calling user via `auth.jwt() ->> 'sub'`. + - Keep `UserContext` user-only and defined in `api/middlewares/auth.ts`: `{ id, role, email? }` (`id` is always present at the API level). API keys and admin bypass flags do not belong inside `UserContext`. + - Routes that issue out-of-band URLs (S3 presigned redirects, signed download links, anything the client redeems against a service that won't re-evaluate RLS) must do an explicit RLS-scoped existence check before handing the URL out — RLS does not fire when the client redeems the URL directly. See `StorageService.objectIsVisible` as the template. + - Migrations that enable RLS on an existing populated table must auto-install a sensible default policy set so the upgrade does not silently break existing rows. See migration 036's `IF EXISTS (SELECT 1 FROM ) THEN END IF` pattern. + - When adding a new RLS-enforced table: enable RLS, `GRANT` table-level CRUD to `authenticated`, and write per-operation policies (SELECT, INSERT, UPDATE, DELETE). Public-bucket-style anonymous bypasses live at the route layer before calling the RLS helper, not in policies. + - Normal raw SQL and custom migrations execute as `project_admin`. It has service-key row visibility, but PostgreSQL grants and ownership still limit object access and DDL. + +6. Always write unit tests for new code. + - Every new feature, migration, service, or bug fix should have accompanying unit tests. + - For migrations, write tests that validate SQL structure and idempotency guards (see `tests/unit/redirect-url-whitelist-migration.test.ts` for the pattern). + - For services, test business logic and error cases. + - For RLS-gated services, mock the pool/client and pin the SQL sequence (see `tests/unit/user-context.service.test.ts` and `tests/unit/storage-object-is-visible.test.ts`). + - Run the full test suite before submitting work: `cd backend && npm test`. + +## Validation + +- `cd backend && npm test` +- `cd backend && npm run build` + +For contract changes, also validate `packages/shared-schemas/` and any affected dashboard consumers. diff --git a/.agents/skills/insforge-dev/dashboard/SKILL.md b/.agents/skills/insforge-dev/dashboard/SKILL.md new file mode 100644 index 0000000..89e9e6d --- /dev/null +++ b/.agents/skills/insforge-dev/dashboard/SKILL.md @@ -0,0 +1,107 @@ +--- +name: dashboard +description: Use this skill when contributing to InsForge's shared dashboard package. This is for maintainers editing `packages/dashboard`, which ships in `self-hosting` and `cloud-hosting` modes, and the local `frontend/` shell used for `self-hosting` in this repo. +--- + +# InsForge Dev Dashboard + +Use this skill for dashboard work in the InsForge repository. + +## Scope + +- `packages/dashboard/src/**` +- `packages/dashboard/package.json` +- `packages/dashboard/README.md` +- `packages/dashboard/*.config.*` +- `frontend/src/**` +- `frontend/package.json` + +## Working Rules + +1. Respect the shared-package versus host-app boundary. + - This dashboard is built with React and TypeScript. + - `packages/dashboard/` is the source of truth for the dashboard product. + - The package must support both `self-hosting` and `cloud-hosting` modes. + - Keep self-hosting-only bootstrap, local env defaults, and shell styling in `frontend/`. + - Do not let `packages/dashboard/` depend on `frontend/`. + - If both modes need a capability, define it in the package API first. + +2. Preserve dashboard data-flow conventions. + - Follow the flow `service -> hook -> UI`. + - Use `apiClient` for HTTP calls so auth refresh and error handling stay consistent. + - Put request logic in services, data fetching and mutation state in hooks, and rendering/orchestration in UI components and pages. + - Reuse existing contexts, host abstractions, and hooks before creating new global state. + +3. Reuse the existing component layers. + - Use `@insforge/ui` for generic primitives. + - Use shared dashboard components when the pattern is already present. + - Keep reusable dashboard UI in `packages/dashboard/`. + - Only add UI to `frontend/` when it is specific to the local self-hosting shell. + - Keep package styles scoped to the dashboard container. + +4. Keep the package surface aligned with shared contracts. + - Import cross-package types and Zod-derived shapes from `@insforge/shared-schemas`. + - When backend payloads change, update the related services, hooks, UI, and exported types together. + - Keep `packages/dashboard/src/index.ts` and `packages/dashboard/src/types` aligned with the public package API. + - Never use the TypeScript `any` type. Prefer precise prop, state, API, and hook result types. + +## Frontend Testing + +Use the lowest test layer that covers the risk. Add one focused regression test for bug fixes when practical. + +| Change type | Test layer | Location | Command | +| --- | --- | --- | --- | +| Pure helper, parser, formatter, state reducer | Unit | `packages/dashboard/src/**/__tests__/*.test.ts` | `npm --workspace @insforge/dashboard run test:unit` | +| React component behavior, forms, dialogs, conditional rendering | Component | `packages/dashboard/src/**/__tests__/*.test.tsx` | `npm --workspace @insforge/dashboard run test:component` | +| Routing, auth redirects, host-mode integration, browser-only behavior | UI smoke | `packages/dashboard/tests/ui/*.spec.ts` | `npm --workspace @insforge/dashboard run test:ui` | + +Conventions: + +- `npm --workspace @insforge/dashboard run test` runs the full Vitest suite: unit plus component. +- GitHub Actions splits dashboard checks into unit, component, and UI jobs in `.github/workflows/frontend-tests.yml`. Update the workflow when adding or renaming test scripts. +- Unit tests should avoid React rendering and network mocking. Test data transformations directly. +- Component tests use Testing Library with `packages/dashboard/src/test/setup.ts`. Mock hooks, services, and host context at the package boundary; assert user-visible behavior and callback effects, not implementation details or CSS class strings. +- UI smoke tests use Playwright against the local `frontend/` shell. Mock backend API routes in `packages/dashboard/tests/ui/fixtures/`; every mocked route must either fulfill, fallback, or abort. Do not leave requests pending after a test branch. +- Prefer one clear test per user-observable behavior over broad snapshot tests. Avoid brittle assertions tied to copy that is not part of the behavior being protected. + +## Local debug: viewing cloud-hosting-only UI in self-hosting + +**Use when** previewing UI gated on `useIsCloudHostingMode()`, `isInsForgeCloudProject()`, or a PostHog feature flag (e.g. the CTest dashboard variant, `dashboard-v3-experiment === 'c_test'`, the CLI connect panel) while running the local `frontend/` self-hosting shell. + +The lowest-friction approach is to **temporarily hardcode** the three gates below to `true`/the new branch, then restart the Vite dev server. These edits bypass real host/project detection and MUST be fully reverted before committing — landing them breaks both self-hosting and cloud-hosting users. + +### Hardcodes + +1. `packages/dashboard/src/lib/config/DashboardHostContext.tsx` — `useIsCloudHostingMode()` → `return true;` (was `useDashboardHost().mode === 'cloud-hosting'`). +2. `packages/dashboard/src/lib/utils/utils.ts` — `isInsForgeCloudProject()` → `return true;` (was the `.insforge.app` hostname check). +3. If the UI is also feature-flag-gated, hardcode the consumer. For CTest: `AppRoutes.tsx` → `const DashboardHomePage = CTestDashboardPage;` and, if relevant, the matching branch in `AppLayout.tsx` for ``. + +Mark every hardcode with a trailing `// LOCAL DEBUG: ` comment so revert is a mechanical search. + +### Revert checklist — run all before committing + +1. `git grep -n "LOCAL DEBUG" packages/dashboard/src/` returns zero matches. +2. Each gate is restored to its **original expression**, not just an equivalent value (the `mode === 'cloud-hosting'` comparison, the hostname check, the `getFeatureFlag(...)` call must all be back). +3. Any imports deleted during debug (commonly `DashboardPage`, `getFeatureFlag`, `ConnectDialog`) are restored. +4. `cd packages/dashboard && npm run lint && npm run typecheck` both pass. +5. `git diff` of the four files above shows only intended changes — no `return true;`, no missing imports. + +### Rationalizations to reject + +| Excuse | Reality | +|--------|---------| +| "I'll revert in a follow-up PR." | Follow-up = a window where prod is broken. Revert now. | +| "The original check was effectively the same." | If it were, you wouldn't have needed the hardcode. Restore the expression, not a value-equivalent. | +| "Lint passed, so the deleted import doesn't matter." | Lint passed because the import was deleted; on revert the original code needs it back. | +| "I'll ship the env-var override instead." | No env-var override is wired in the code. Don't invent one on the commit path — restore the original. | + +## Validation + +- `cd packages/dashboard && npm run test:unit` when changing pure helpers or reducers +- `cd packages/dashboard && npm run test:component` when changing React component behavior +- `cd packages/dashboard && npm run test:ui` when changing routing, auth, host-mode integration, or browser-only behavior +- `cd packages/dashboard && npm run typecheck` +- `cd packages/dashboard && npm run build` +- `cd frontend && npm run build` when the local self-hosting shell changes + +For shared contract changes, also validate `packages/shared-schemas/` and the affected backend surface. diff --git a/.agents/skills/insforge-dev/docs/SKILL.md b/.agents/skills/insforge-dev/docs/SKILL.md new file mode 100644 index 0000000..7e5e1eb --- /dev/null +++ b/.agents/skills/insforge-dev/docs/SKILL.md @@ -0,0 +1,47 @@ +--- +name: docs +description: Use this skill when contributing to InsForge's product documentation in this repository. This is for maintainers editing public docs in `docs/core-concepts`, agent docs in `.agents/docs`, SDK integration guides in `docs/sdks`, and OpenAPI specs in `openapi`. +--- + +# InsForge Dev Docs + +Use this skill for `docs/core-concepts/`, `.agents/docs/`, `docs/sdks/`, and `openapi/` in the InsForge repository. + +The documentation in this repo is primarily product documentation for InsForge users and agents integrating with InsForge. This skill is for InsForge engineers maintaining that public documentation surface. + +## Scope + +- `docs/core-concepts/**` +- `.agents/docs/**` +- `docs/sdks/**` +- `openapi/**` + +## Working Rules + +1. Put each document in the correct documentation surface. + - Human-friendly docs published on the public doc site belong in `docs/core-concepts/` and related public doc folders. + - For implementation-heavy public docs, prefer an `architecture.md` file inside the relevant `docs/core-concepts//` folder. + - Agent-only instructions belong in `.agents/docs/`. + - SDK integration guides for each framework belong in `docs/sdks/`. + - OpenAPI contract changes belong in the matching files under `openapi/`. + +2. Match the writing style to the audience. + - Public docs should be human-friendly and explain the implementation clearly. + - Keep public docs human-sounding: avoid AI-writing tells such as em dashes, rule-of-three lists, "not just X but Y" parallelism, inflated significance, vague attribution, and AI vocabulary (delve, leverage, underscore, seamless, robust). See the `doc-author` skill's `INSFORGE.md` overlay, "Sound human, not AI-generated". + - `architecture.md` pages in `docs/core-concepts/` should explain how the feature works in detail. + - Agent docs in `.agents/docs/` should be instruction-first and execution-oriented. + - Agent docs should avoid explanatory filler and focus on the exact steps an agent should follow to complete the work. + +3. Prevent documentation drift on every implementation change. + - Before changing implementation, check the current user-facing docs for that feature. + - After changing implementation, update the relevant Markdown docs and the relevant OpenAPI YAML files in the same pass. + - Do not treat OpenAPI and Markdown as separate optional follow-ups when the feature contract or behavior changed. + - If a change affects agent workflows, update the corresponding file in `.agents/docs/`. + - If a change affects public product understanding, update the corresponding file in `docs/core-concepts/`, including `architecture.md` when implementation details changed. + - If a change affects SDK integration guidance, update the corresponding framework guide in `docs/sdks/`. + +## Validation + +- Re-read every documented command, path, route, and payload for correctness. +- Cross-check OpenAPI YAML and Markdown docs against the implemented behavior before finishing. +- Mention anything you could not verify directly. diff --git a/.agents/skills/insforge-dev/e2e-testing/SKILL.md b/.agents/skills/insforge-dev/e2e-testing/SKILL.md new file mode 100644 index 0000000..3a7b651 --- /dev/null +++ b/.agents/skills/insforge-dev/e2e-testing/SKILL.md @@ -0,0 +1,142 @@ +--- +name: e2e-testing +description: Use this skill when an InsForge maintainer has finished an OSS repo change and is ready to open, update, or submit the InsForge PR. Runs the release-quality deterministic E2E gate by building a package.json-derived InsForge test image tag, deciding whether sibling agent-e2e fixture coverage must change, dispatching the Deterministic Fixture E2E workflow, waiting for results, and triaging failures before PR submission. +--- + +# InsForge E2E Testing Gate + +Use this skill after local implementation and normal InsForge pre-PR checks pass, and before opening, updating, or submitting the InsForge OSS PR. + +This is an additional release-quality gate. It does not replace local `typecheck`, `lint`, `test`, or `build` validation from the parent `insforge-dev` skill. + +## Repositories + +- InsForge OSS repo: current workspace. +- E2E repo: remote GitHub repository `InsForge/agent-e2e`. + +Use the remote `InsForge/agent-e2e` repository for workflow dispatch and read-only workflow checks. Do not rely on a developer-specific local checkout path. Create or use a local checkout only when the deterministic fixture tests must be edited. + +## Build The Test Tag + +1. Read the root InsForge `package.json` version. +2. Increment only the patch number. +3. Build a tag in this form: + +```text +v..- +``` + +Example: root version `2.2.3` and feature `storage returning rls` becomes `v2.2.4-storage-returning-rls`. + +Use the `package.json` version as the only source of truth for the base version. Ignore higher existing test tags when calculating the base version. + +Slug rules: + +- Prefer the issue key, PR topic, or branch topic. +- Lowercase all letters. +- Replace runs of non-alphanumeric characters with one hyphen. +- Trim leading and trailing hyphens. +- Keep it short enough to scan in GitHub Actions and image tags. + +## Build The InsForge Image + +Dispatch the InsForge `Build and Push Docker Image` workflow with the test tag: + +```bash +gh workflow run "Build and Push Docker Image" --repo InsForge/InsForge --ref -f test_tag= +``` + +Then wait for the matching run to complete: + +```bash +gh run list --repo InsForge/InsForge --workflow "Build and Push Docker Image" --limit 10 +gh run watch --repo InsForge/InsForge +``` + +Do not start the cross-repo E2E workflow until the image build succeeds. + +## Decide Whether `agent-e2e` Must Change + +Inspect the InsForge diff and compare it with deterministic fixture coverage in `InsForge/agent-e2e`. + +For read-only checks, prefer remote GitHub access such as `gh api`, `gh repo view`, or remote file reads. Use a local checkout only when editing fixture files or when remote inspection is not enough to understand coverage. + +Update `agent-e2e` when the InsForge change adds, removes, or changes behavior that the deterministic fixture should assert, including: + +- API contract or validation behavior. +- Auth, permissions, RLS, storage, realtime, functions, schedules, AI, SDK, or CLI-visible behavior. +- Any regression that local tests cover but the release gate should also protect across the deployed runtime. + +Do not update `agent-e2e` for internal refactors, docs-only changes, local test-only changes, or behavior already covered by the deterministic fixture with no assertion change needed. + +Ignore `Support Desk Agent E2E (Exploratory)`. It is not part of this gate. + +## If No E2E Test Update Is Needed + +Run the deterministic fixture workflow from `agent-e2e` main: + +```bash +gh workflow run "Deterministic Fixture E2E" --repo InsForge/agent-e2e --ref main -f insforge_tag= +``` + +Find and watch the run: + +```bash +gh run list --repo InsForge/agent-e2e --workflow "Deterministic Fixture E2E" --limit 10 +gh run watch --repo InsForge/agent-e2e +``` + +## If E2E Tests Need An Update + +Work in a local checkout of the remote `InsForge/agent-e2e` repo only for the fixture update. + +1. Use an existing clean checkout or clone `https://github.com/InsForge/agent-e2e.git` into an isolated workspace. +2. Fetch `origin` and start from `origin/main`. +3. Create a branch named `codex/`. +4. Update only the deterministic fixture validators, fixtures, app assertions, and docs needed for the InsForge behavior change. +5. Run the smallest local validation that gives confidence: + +```bash +npm run typecheck +npm run lint +npm run fixture:e2e:dry +``` + +Run broader validation when the changed fixture area supports it. + +6. Open an `agent-e2e` PR for the fixture update. +7. Dispatch the deterministic fixture workflow from the `agent-e2e` branch that contains the fixture update: + +```bash +gh workflow run "Deterministic Fixture E2E" --repo InsForge/agent-e2e --ref -f insforge_tag= +``` + +8. Watch the run before proceeding with the InsForge PR. + +## Interpret Results + +If the deterministic fixture workflow passes: + +- Link the run in the InsForge PR body or final PR notes. +- If an `agent-e2e` PR was required, link that PR too. +- Proceed with opening, updating, or submitting the InsForge OSS PR. + +If the deterministic fixture workflow fails: + +1. Inspect the failed job logs and uploaded artifact. +2. Identify whether the failure is caused by the InsForge implementation, the new or existing deterministic fixture, a transient infrastructure problem, or an unrelated existing failure. +3. Fix the correct branch: + - InsForge implementation bug: update the InsForge branch, rebuild the test image with the same tag or a new short retry tag, then rerun E2E. + - Fixture bug or missing assertion update: update the `agent-e2e` branch, rerun local validation, then rerun E2E from that branch. + - Transient infrastructure issue: rerun once after noting the evidence. +4. Do not submit the InsForge PR until the deterministic result is clear or the user explicitly accepts the risk. + +## Report Back + +When finished, report: + +- Test tag used. +- InsForge image workflow run result. +- Whether `agent-e2e` changed. +- Deterministic Fixture E2E run result. +- Links to the InsForge PR, `agent-e2e` PR if any, and workflow runs. diff --git a/.agents/skills/insforge-dev/shared-schemas/SKILL.md b/.agents/skills/insforge-dev/shared-schemas/SKILL.md new file mode 100644 index 0000000..84bc60d --- /dev/null +++ b/.agents/skills/insforge-dev/shared-schemas/SKILL.md @@ -0,0 +1,43 @@ +--- +name: shared-schemas +description: Use this skill when contributing to InsForge's shared schema package. This is for maintainers editing published Zod contracts, exported types, and shared API payload definitions consumed by InsForge packages in this repo and other InsForge tooling. +--- + +# InsForge Dev Shared Schemas + +Use this skill for `packages/shared-schemas/` work in the InsForge repository. + +## Scope + +- `packages/shared-schemas/src/**` +- any code in this repo that consumes the changed contracts +- downstream InsForge tooling that depends on the published `@insforge/shared-schemas` package, including consumers not present in this repository + +## Working Rules + +1. Treat `packages/shared-schemas/` as the source of truth for cross-package payloads. + - If a request, response, or domain shape is shared across InsForge surfaces, define it here. + - Do not duplicate the same contract in package-local files. + - Remember that this package is not only for the repo's backend and dashboard packages. It is also consumed by other InsForge tooling, including MCP and SDK code that may live outside this repository. + +2. Keep schemas organized by domain. + - Follow the existing `*.schema.ts` and `*-api.schema.ts` split when it fits the current package pattern. + - Keep `packages/shared-schemas/src/index.ts` aligned with the intended public API. + - Treat exported names and schema shapes as a public contract surface, not just an internal refactor target. + +3. Use schema changes as a synchronization trigger. + - Update backend validation and response usage. + - Update shared dashboard services, hooks, and UI assumptions in `packages/dashboard/`. + - Check for import sites across `packages/*`, `frontend/`, and `backend/` before finishing. + - Call out likely downstream impact on MCP, SDK, or other external InsForge consumers when a change alters exported names, schema semantics, or payload shape. + - Be conservative with breaking changes. If a breaking contract change is necessary, make it explicit in the handoff. + - Never use the TypeScript `any` type. Shared contracts should stay explicit and trustworthy across package boundaries. + +## Validation + +- `cd packages/shared-schemas && npm run build` +- `cd backend && npx tsc --noEmit` +- `cd packages/dashboard && npm run typecheck` + +Run package-specific tests as needed where behavior changed. +If external consumers such as MCP or SDK cannot be validated from this repo, say that clearly instead of implying they were covered. diff --git a/.agents/skills/insforge-dev/ui/SKILL.md b/.agents/skills/insforge-dev/ui/SKILL.md new file mode 100644 index 0000000..885fd5c --- /dev/null +++ b/.agents/skills/insforge-dev/ui/SKILL.md @@ -0,0 +1,42 @@ +--- +name: ui +description: Use this skill when contributing to InsForge's reusable UI package. This is for maintainers editing design-system primitives, exports, styles, and package-level component behavior in the InsForge monorepo. +--- + +# InsForge Dev UI + +Use this skill for `packages/ui/` work in the InsForge repository. + +## Scope + +- `packages/ui/src/components/**` +- `packages/ui/src/lib/**` +- `packages/ui/src/index.ts` +- `packages/ui/src/styles.css` + +## Working Rules + +1. Put only reusable primitives here. + - If the component is generic across dashboard features or other InsForge apps, it belongs in `packages/ui/`. + - If it is tightly coupled to one dashboard workflow but should ship to both OSS and cloud hosts, keep it in `packages/dashboard/`. + - If it is only for the self-hosting host app, keep it in `frontend/`. + +2. Preserve the package's implementation style. + - Use `class-variance-authority` for variants when appropriate. + - Use the shared `cn()` helper for class merging. + - Follow the existing Radix-wrapper and typed-export patterns. + +3. Keep the public surface in sync. + - Export new public components from `packages/ui/src/index.ts`. + - Avoid adding internal-only abstractions to the package surface unless they are meant to be consumed. + - Never use the TypeScript `any` type. Keep component props and exported helpers strictly typed. + +4. Validate downstream impact. + - The shared dashboard package consumes this package directly, so UI changes can break `packages/dashboard/` even if `packages/ui/` itself builds cleanly. + +## Validation + +- `cd packages/ui && npm run build` +- `cd packages/ui && npm run typecheck` + +Also validate `packages/dashboard/` when the changed component is used in the dashboard, and validate `frontend/` if the host app integration or CSS entrypoints changed. diff --git a/.archive/docs/deprecated/insforge-auth-api.md b/.archive/docs/deprecated/insforge-auth-api.md new file mode 100644 index 0000000..83f0579 --- /dev/null +++ b/.archive/docs/deprecated/insforge-auth-api.md @@ -0,0 +1,213 @@ +# Insforge OSS Authentication API Documentation + +## Overview + +Insforge uses JWT tokens and API keys for authentication. Store tokens in localStorage after login. +**All requests**: Use `Authorization: Bearer ` header (for both JWT tokens and API keys) + +## Base URL +`http://localhost:7130` + +## User Authentication + +### Register New User +**POST** `/api/auth/users` + +Body: `{"email": "user@example.com", "password": "password", "name": "User Name"}` + +Returns: `{"accessToken": "...", "user": {"id": "...", "email": "...", "name": "...", "emailVerified": false, "createdAt": "...", "updatedAt": "..."}}` + +**Note:** This creates an entry in the `users` table with the same `id` for profile data + +### Login User +**POST** `/api/auth/sessions` + +Body: `{"email": "user@example.com", "password": "password"}` + +Returns: `{"accessToken": "...", "user": {"id": "...", "email": "...", "name": "...", "emailVerified": false, "createdAt": "...", "updatedAt": "..."}}` + +### Get Current User +**GET** `/api/auth/sessions/current` + +Headers: `Authorization: Bearer ` + +Returns: `{"user": {"id": "...", "email": "...", "role": "authenticated"}}` + +**Note**: Returns LIMITED fields (id, email, role). For user profile data (nickname, avatar, bio, etc.), query `/api/database/records/users?id=eq.` + +**Common errors:** +- `401` with `"code": "MISSING_AUTHORIZATION_HEADER"` → No token provided +- `401` with `"code": "INVALID_TOKEN"` → Token expired or invalid + +## Admin Authentication + +### Admin Login +**POST** `/api/auth/admin/sessions` + +Request: +```json +{ + "username": "admin", + "password": "change-this-password" +} +``` + +Response: +```json +{ + "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "projectAdmin": { + "subject": "local:admin", + "username": "admin" + } +} +``` + +```bash +# Mac/Linux +curl -X POST http://localhost:7130/api/auth/admin/sessions \ + -H 'Content-Type: application/json' \ + -d '{"username":"admin","password":"change-this-password"}' + +# Windows PowerShell (use curl.exe) +curl.exe -X POST http://localhost:7130/api/auth/admin/sessions \ + -H "Content-Type: application/json" \ + -d '{\"username\":\"admin\",\"password\":\"change-this-password\"}' +``` + +## Error Response Format + +All error responses follow this format: +```json +{ + "code": "ERROR_CODE", + "message": "Human-readable error message" +} +``` + +Example error: +```json +{ + "code": "INVALID_EMAIL", + "message": "Please provide a valid email" +} +``` + +## OAuth Support + +Insforge supports Google and GitHub OAuth when configured with environment variables. + +OAuth workflow: +1. End user initiates OAuth login and receives authorization URL from backend +2. After successful user authorization, Google/GitHub redirects to backend callback +3. Backend generates JWT token and redirects to the application page + +Prerequisites: +1. Create Google or GitHub OAuth Application and obtain Client ID and Client Secret +2. Configure each platform's Client ID/Client Secret in InsForge backend (via Environment Variables) + +### OAuth Endpoints + +#### Get OAuth URL (Google/GitHub) +**GET** `/api/auth/oauth/:provider` + +Parameters: +- `provider`: "google" or "github" in the URL path +- Query params: `?redirect_uri=http://localhost:3000/dashboard` + +Returns: `{"authUrl": "https://accounts.google.com/..."}` - URL to redirect user to provider's OAuth page. + +```bash +# Mac/Linux +curl -X GET "http://localhost:7130/api/auth/oauth/google?redirect_uri=http://localhost:3000/dashboard" + +# Windows PowerShell (use curl.exe) +curl.exe -X GET "http://localhost:7130/api/auth/oauth/google?redirect_uri=http://localhost:3000/dashboard" +``` + +Example response: +```json +{ + "authUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=..." +} +``` + +#### OAuth Callback +The OAuth provider will redirect to: +- Google: `http://localhost:7130/api/auth/oauth/google/callback` +- GitHub: `http://localhost:7130/api/auth/oauth/github/callback` + +After processing, backend redirects to your specified `redirect_uri` with JWT token in URL parameters: +- `access_token` - JWT authentication token +- `user_id` - User's unique ID +- `email` - User's email address +- `name` - User's display name + +## User Profile Table + +### Users Table +The `users` table stores **user profile data**: +- **✅ READ** via: `GET /api/database/records/users` +- **✅ WRITE** via: `PATCH /api/database/records/users?id=eq.` +- **✅ Foreign keys allowed** - reference `users.id` +- **IMPORTANT**: Add columns to this table for profile data instead of creating separate profile tables + +**Schema:** +- `id` - User ID (UUID, primary key) +- `nickname` - Display name (text, nullable) +- `avatar_url` - Profile picture URL (text, nullable) +- `bio` - User biography (text, nullable) +- `birthday` - Birth date (date, nullable) +- `created_at` - Account creation timestamp +- `updated_at` - Last update timestamp + +**Note:** Email and name from auth are returned by Auth API, not stored in users table + +Example - Create table with user reference: +```json +{ + "table_name": "posts", + "columns": [ + { + "name": "title", + "type": "string", + "nullable": false, + "is_unique": false + }, + { + "name": "user_id", + "type": "string", + "nullable": false, + "is_unique": false, + "foreign_key": { + "reference_table": "users", + "reference_column": "id", + "on_delete": "CASCADE", + "on_update": "CASCADE" + } + } + ] +} +``` + +### Available Tables +- **users** - User profile data (read/write access) + - Use this table for foreign key references + - Update profiles with PATCH requests + +## Headers Summary + +| API Type | Header Required | +|----------|----------------| +| Auth endpoints | None | +| Database/Storage | `Authorization: Bearer ` | +| MCP testing only | `x-api-key: ` | + +## Critical Notes + +1. `/api/auth/sessions/current` returns `{"user": {...}}` - nested, not root level +2. `/api/auth/sessions/current` only has: id, email, role (limited fields) +3. Full user profile: `GET /api/database/records/users?id=eq.` +4. POST to database requires `[{...}]` array format always +5. Auth endpoints (register/login): no headers needed +6. Protected endpoints: `Authorization: Bearer ` diff --git a/.archive/docs/deprecated/insforge-auth-sdk.md b/.archive/docs/deprecated/insforge-auth-sdk.md new file mode 100644 index 0000000..161f54e --- /dev/null +++ b/.archive/docs/deprecated/insforge-auth-sdk.md @@ -0,0 +1,100 @@ +# InsForge Auth SDK + +## Setup +```javascript +import { createClient } from '@insforge/sdk'; +const client = createClient({ baseUrl: 'http://localhost:7130' }); +``` + +## Methods + +### signUp +```javascript +await client.auth.signUp({ email, password, name? }) +// Returns: { data: { accessToken, user }, error } +// user: { id, email, name, emailVerified, createdAt, updatedAt } +// Token auto-stored +``` + +### signInWithPassword +```javascript +await client.auth.signInWithPassword({ email, password }) +// Returns: { data: { accessToken, user }, error } +// Token auto-stored +``` + +### signInWithOAuth +```javascript +const { data, error } = await client.auth.signInWithOAuth({ + provider: 'google'|'github', + redirectTo: window.location.origin, + skipBrowserRedirect: true +}) +// Returns: { data: { url, provider }, error } + +// Manual redirect required +if (data?.url) { + window.location.href = data.url +} + +// ⚠️ IMPORTANT: No callback handling needed! +// After OAuth, user returns to redirectTo URL already authenticated +// The SDK automatically: +// - Handles the OAuth callback +// - Stores the JWT token +// - Makes user available via getCurrentUser() + +// ❌ DON'T DO THIS (not needed): +// const accessToken = urlParams.get('access_token') +// const userId = urlParams.get('user_id') + +// ✅ DO THIS INSTEAD (after redirect back): +const { data: userData } = await client.auth.getCurrentUser() +``` + +### getCurrentUser +```javascript +await client.auth.getCurrentUser() +// Returns: { data: { user: { id, email, role }, profile: {...} }, error } +// Makes API call to validate token and fetch profile +``` + +### getCurrentSession +```javascript +await client.auth.getCurrentSession() +// Returns: { data: { session: { accessToken, user } }, error } +// From localStorage, no API call +``` + +### getProfile +```javascript +await client.auth.getProfile(userId) +// Returns: { data: { id, nickname, bio, ... }, error } +// Returns single object, not array! +``` + +### setProfile +```javascript +await client.auth.setProfile({ nickname, bio, avatar_url }) +// Returns: { data: { id, nickname, bio, ... }, error } +// Returns single object, not array! +``` + +### signOut +```javascript +await client.auth.signOut() +// Returns: { error } +// Clears token from storage +``` + +## Error Codes +- `INVALID_EMAIL` +- `WEAK_PASSWORD` +- `USER_ALREADY_EXISTS` +- `INVALID_CREDENTIALS` +- `INVALID_TOKEN` + +## Notes +- Tokens stored in localStorage (browser) or memory (Node.js) +- All requests after login automatically include token +- User profile data in `users` table, not auth \ No newline at end of file diff --git a/.archive/docs/deprecated/insforge-db-api.md b/.archive/docs/deprecated/insforge-db-api.md new file mode 100644 index 0000000..e7e0ca9 --- /dev/null +++ b/.archive/docs/deprecated/insforge-db-api.md @@ -0,0 +1,359 @@ +# Insforge OSS Database API Documentation + +## API Basics + +**Base URL:** `http://localhost:7130` + +**Note:** Avoid special characters (!,$,`,\) in curl command data - they can cause bash interpretation issues. Use simple text for testing. + +**Authentication Requirements:** +- **READ operations (GET):** No authentication required - public access by default +- **WRITE operations (POST/PATCH/DELETE):** Requires `Authorization: Bearer ` header (JWT token or API key for MCP testing) + +**Important: How Authentication Works** +1. Login returns a **JWT access token** - e.g., `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...` +2. Just use: `Authorization: Bearer ` in your requests +3. API keys (starting with `ik_`) can also be used as Bearer tokens for testing +**Critical:** Always call `get-backend-metadata` first to understand current database structure +**Critical:** POST body must be arrays `[{...}]`, query filters `?field=eq.value`, add header `Prefer: return=representation` to return created data - follows PostgREST design (not traditional REST) + +## Table Operations (Use MCP Tools) + +### Available MCP Tools + +1. **get-backend-metadata** - Get current database structure (always start here) +2. **create-table** - Create new table with explicit schema +3. **update-table-schema** - Alter existing table schema +4. **delete-table** - Remove table completely +5. **get-table-schema** - Get specific table structure + +### Column Types +- `string` - Text data +- `integer` - Whole numbers +- `float` - Decimal numbers +- `boolean` - True/false values +- `datetime` - Date and time +- `json` - JSON objects +- `uuid` - Unique identifiers + +## Record Operations (Use REST API) + +### Base URL +`/api/database/records/:tableName` + +### Query Records +**GET** `/api/database/records/:tableName` + +Query parameters: +- `limit` - Maximum records (default: 100) +- `offset` - Skip records for pagination +- `order` - Sort by field (e.g., `createdAt.desc`) +- PostgREST filters: `field=eq.value`, `field=gt.value`, etc. + +Response: Array of records with auto-generated `id`, `created_at`, `updated_at` fields + +Example: +```bash +# Windows PowerShell: use curl.exe +curl -X GET "http://localhost:7130/api/database/records/posts?limit=10" +``` + +### Create Records +**POST** `/api/database/records/:tableName` + +**AUTHENTICATION REQUIRED** - Must include `Authorization: Bearer ` + +**CRITICAL**: Request body MUST be an array, even for single records! + +**⚠️ IMPORTANT: Default Response Behavior** +- **By default, POST requests return an empty array `[]`** +- **To get the created records in the response, you MUST include the header:** + ``` + Prefer: return=representation + ``` +- **Without this header, you get no data back, just an empty array!** + +Send array of records: +```json +[ + { + "field1": "value1", + "field2": "value2" + } +] +``` + +For a single record, still wrap in array: +```json +[ + { + "name": "John Doe", + "email": "john@example.com" + } +] +``` + +Response format (WITHOUT `Prefer` header - default): +```json +[] +``` + +Response format (WITH `Prefer: return=representation` header): +```json +[ + { + "id": "248373e1-0aea-45ce-8844-5ef259203749", + "name": "John Doe", + "email": "john@example.com", + "createdAt": "2025-07-18T05:37:24.338Z", + "updatedAt": "2025-07-18T05:37:24.338Z" + } +] +``` + +Example: +```bash +# Mac/Linux +curl -X POST http://localhost:7130/api/database/records/comments \ + -H 'Authorization: Bearer ' \ + -H 'Content-Type: application/json' \ + -H 'Prefer: return=representation' \ + -d '[{"user_id": "from-localStorage", "post_id": "post-uuid", "content": "Great"}]' + +# Windows PowerShell (use curl.exe) - different quotes needed for nested JSON +curl.exe -X POST http://localhost:7130/api/database/records/comments \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -H "Prefer: return=representation" \ + -d '[{\"user_id\": \"from-localStorage\", \"post_id\": \"post-uuid\", \"content\": \"Great\"}]' +``` + +### Update Record +**PATCH** `/api/database/records/:tableName?id=eq.uuid` + +**AUTHENTICATION REQUIRED** + +**⚠️ IMPORTANT: PATCH Limitations** +- **PostgREST does NOT support SQL expressions** like `count + 1` +- You must fetch the current value and calculate in your code: + +```javascript +// ❌ WRONG - This will NOT work +await api.patch(`/api/database/records/posts?id=eq.${postId}`, { + comments_count: 'comments_count + 1' // PostgREST doesn't evaluate expressions! +}); + +// ✅ CORRECT - Fetch and calculate +const post = await api.get(`/api/database/records/posts?id=eq.${postId}`); +await api.patch(`/api/database/records/posts?id=eq.${postId}`, { + comments_count: post.data[0].comments_count + 1 +}); +``` + +**Default Response Behavior** +- **By default, PATCH requests return an empty array `[]`** +- **To get the updated record in the response, you MUST include the header:** + ``` + Prefer: return=representation + ``` + +Send fields to update: +```json +{ + "field1": "new_value" +} +``` + +Response format (WITHOUT `Prefer: return=representation` header - default): +```json +"" +``` + +Response format (WITH `Prefer: return=representation` header): +```json +[ + { + "id": "123e4567-e89b-12d3-a456-426614174000", + "field1": "new_value", + "createdAt": "2025-01-01T00:00:00Z", + "updatedAt": "2025-01-21T11:00:00Z" + } +] +``` + +### Delete Record +**DELETE** `/api/database/records/:tableName?id=eq.uuid` + +**AUTHENTICATION REQUIRED** + +**⚠️ IMPORTANT: Delete Behavior** +- **Without `Prefer: return=representation`**: Returns `204 No Content` (no body) +- **With `Prefer: return=representation`**: Returns `200 OK` with: + - `[{...}]` - Array containing deleted record(s) if found + - `[]` - Empty array if record didn't exist +- **DELETE is idempotent**: No error if record doesn't exist + +Response format (WITHOUT `Prefer` header - default): +``` +204 No Content (no body) +``` + +Response format (WITH `Prefer: return=representation` header): +```json +// If record existed and was deleted: +[ + { + "id": "123e4567-e89b-12d3-a456-426614174000", + "name": "Deleted User", + "createdAt": "2025-01-01T00:00:00Z", + "updatedAt": "2025-01-21T11:00:00Z" + } +] + +// If record didn't exist (already deleted or never existed): +[] +``` + +## Error Response Format + +All error responses follow this format: +```json +{ + "error": "ERROR_CODE", + "message": "Human-readable error message", + "statusCode": 400, + "nextActions": "Suggested action to resolve the error" +} +``` + +Example error: +```json +{ + "error": "TABLE_NOT_FOUND", + "message": "Table 'nonexistent' does not exist", + "statusCode": 404, + "nextActions": "Check table name and try again" +} +``` + +## Pagination + +For paginated results, use the `Range` header: +```bash +# Windows PowerShell: use curl.exe +curl "http://localhost:7130/api/database/records/posts" \ + -H "Range: 0-9" \ + -H "Prefer: count=exact" +``` + +Response includes `Content-Range` header: +``` +Content-Range: 0-9/100 # Shows items 0-9 out of 100 total +``` + +Without `Prefer: count=exact`, you get: `Content-Range: 0-9/*` (no total count) + +## 🚨 Working with User Data + +**The `users` table stores user profiles:** +- **✅ READ**: `GET /api/database/records/users` - Get user profiles +- **✅ WRITE**: `PATCH /api/database/records/users?id=eq.` - Update profiles + +**Schema:** +- `id` - User ID (UUID, references auth system) +- `nickname` - Display name (text, nullable) +- `avatar_url` - Profile picture URL (text, nullable) +- `bio` - User biography (text, nullable) +- `birthday` - Birth date (date, nullable) +- `created_at` - Profile creation timestamp +- `updated_at` - Last update timestamp + +**Important:** +- User accounts (email, password) are managed via Auth API only +- The `users` table is automatically created when a user registers +- Use `users.id` for foreign key references in your tables + +**Creating tables with user references:** +```json +{ + "table_name": "posts", + "columns": [ + {"name": "user_id", "type": "string", "nullable": false, + "foreign_key": {"reference_table": "users", "reference_column": "id", + "on_delete": "CASCADE", "on_update": "CASCADE"}}, + {"name": "content", "type": "string", "nullable": false} + ] +} +``` + +## 🚨 Critical: Always Include user_id + +**Every user-related table MUST include user_id field from localStorage:** + +```javascript +// Frontend: Get user_id from localStorage after login +const userId = localStorage.getItem('user_id'); +``` + +```bash +# ❌ WRONG - Missing user_id +curl -X POST http://localhost:7130/api/database/records/comments \ + -H "Authorization: Bearer TOKEN" \ + -d '[{"content": "Great post"}]' + +# ✅ CORRECT - Includes user_id +# Mac/Linux +curl -X POST http://localhost:7130/api/database/records/comments \ + -H 'Authorization: Bearer TOKEN' \ + -H 'Prefer: return=representation' \ + -d '[{"content": "Great post", "user_id": "user-uuid-from-localStorage"}]' + +# Windows PowerShell (use curl.exe) - different quotes needed for nested JSON +curl.exe -X POST http://localhost:7130/api/database/records/comments \ + -H "Authorization: Bearer TOKEN" \ + -H "Prefer: return=representation" \ + -d '[{\"content\": \"Great post\", \"user_id\": \"user-uuid-from-localStorage\"}]' +``` + +**Required for all user-related operations:** +- Creating posts, comments, likes, follows +- Any table with a `user_id` foreign key +- Without it, your INSERT will fail with missing field error + +## Important Rules + +1. **Authentication Summary**: + | Operation | Auth Required | Header | + |-----------|--------------|--------| + | GET (read) | ❌ No | None needed | + | POST (create) | ✅ Yes | `Authorization: Bearer ` | + | PATCH (update) | ✅ Yes | `Authorization: Bearer ` | + | DELETE | ✅ Yes | `Authorization: Bearer ` | + +2. **Auto-Generated Fields** + - `id` - UUID primary key (auto-generated) + - `createdAt` - Timestamp (auto-set) + - `updatedAt` - Timestamp (auto-updated) + +2. **System Tables** + - Tables prefixed with `_` are system tables (protected) + - User profiles stored in `users` table (read/write allowed) + - Account management only through Auth API (register/login) + +3. **Common PostgREST Errors**: + ```json + {"code": "42501", "message": "permission denied for table comments"} + // Means: User not authenticated for write operation + + {"code": "PGRST301", "message": "JWSError (CompactDecodeError Invalid number of parts: Expected 3 parts; got 1)"} + // Means: Invalid or expired token - user needs to login again + ``` + +4. **Remember** + - READ operations are public (no auth needed) + - WRITE operations require token from login + - POST needs array `[{...}]` even for single record + - Add `Prefer: return=representation` to see created/updated data + - PATCH cannot use SQL expressions - calculate in JavaScript + - Tokens from login work directly as Bearer tokens + - Always include `user_id` in user-related tables \ No newline at end of file diff --git a/.archive/docs/deprecated/insforge-db-sdk.md b/.archive/docs/deprecated/insforge-db-sdk.md new file mode 100644 index 0000000..bd11faf --- /dev/null +++ b/.archive/docs/deprecated/insforge-db-sdk.md @@ -0,0 +1,140 @@ +# InsForge Database SDK + +## Setup +```javascript +import { createClient } from '@insforge/sdk'; +const client = createClient({ baseUrl: 'http://localhost:7130' }); +``` + +## Query Builder + +### from(table) +```javascript +client.database.from('posts') // Returns QueryBuilder +``` + +## Operations + +### select +```javascript +.select() // All columns +.select('id, title') // Specific columns +.select('*, user:user_id(name, email)') // Foreign key expansion +``` + +### insert +```javascript +.insert({ title: 'Hello' }).select() // Single record with data returned +.insert([{...}, {...}]).select() // Multiple records with data returned +// Note: Always sends array to API +// Without .select(), returns null data +``` + +### update +```javascript +.update({ title: 'Updated' }).eq('id', '123').select() +// Must chain with filter and .select() to return data +``` + +### delete +```javascript +.delete().eq('id', '123').select() +// Must chain with filter and .select() to return data +``` + +### upsert +```javascript +.upsert({ id: '123', title: 'New or Update' }).select() +// Updates if exists, inserts if not +// Use .select() to return data +``` + +## Filters + +```javascript +.eq('col', value) // column = value +.neq('col', value) // column != value +.gt('col', value) // column > value +.gte('col', value) // column >= value +.lt('col', value) // column < value +.lte('col', value) // column <= value +.like('col', '%pat%') // LIKE pattern +.ilike('col', '%pat%') // ILIKE pattern +.is('col', null) // IS NULL +.in('col', [1,2,3]) // IN array + +.or('status.eq.active,status.eq.pending') // OR condition +.and('price.gte.100,price.lte.500') // Explicit AND +.not('deleted', 'is.true') // NOT condition +``` + +### OR Examples +```javascript +// Simple OR +.or('status.eq.active,status.eq.pending') +// WHERE status = 'active' OR status = 'pending' + +// OR with other filters (implicit AND) +.eq('user_id', '123') +.or('status.eq.draft,status.eq.published') +// WHERE user_id = '123' AND (status = 'draft' OR status = 'published') + +// Complex OR with NOT +.or('age.lt.18,age.gt.65,not.is_active.is.true') +// WHERE age < 18 OR age > 65 OR NOT is_active +``` + +## Modifiers + +```javascript +.order('col') // ASC +.order('col', { ascending: false }) // DESC +.limit(10) +.offset(20) +.range(0, 9) // Headers: Range: 0-9 +.single() // Return object not array +.count('exact') // Include total count +``` + +## Execute + +```javascript +// Methods are thenable +const { data, error } = await client.database + .from('posts') + .select() + .eq('user_id', '123') + .limit(10); + +// data: array or null +// error: { message, statusCode, code } or null +``` + +## Foreign Key Expansion + +```javascript +// PostgREST syntax +.select('*, user:user_id(name, email)') + +// Response: +{ + id: '123', + title: 'Post', + user_id: '456', + user: { name: 'John', email: 'john@example.com' } +} +``` + +## Users Table + +```javascript +// Profile data (not auth) +await client.database.from('users').select().eq('id', userId).single() +await client.database.from('users').update({ nickname, avatar_url }).eq('id', userId).select() +``` + +## Notes +- Uses PostgREST under the hood +- POST requires array format `[{...}]` +- All methods return QueryBuilder for chaining +- Execute returns `{ data, error }` \ No newline at end of file diff --git a/.archive/docs/deprecated/insforge-debug-sdk.md b/.archive/docs/deprecated/insforge-debug-sdk.md new file mode 100644 index 0000000..6ea5767 --- /dev/null +++ b/.archive/docs/deprecated/insforge-debug-sdk.md @@ -0,0 +1,157 @@ +# InsForge Debug SDK + +## Common SDK Errors + +### Auth Errors +```javascript +// Token not stored +{ code: 'MISSING_AUTHORIZATION_HEADER' } +// Fix: User needs to login + +// Token expired +{ code: 'INVALID_TOKEN' } +// Fix: client.auth.signInWithPassword() + +// Invalid credentials +{ code: 'INVALID_CREDENTIALS' } +// Fix: Check email/password +``` + +### Database Errors +```javascript +// Table doesn't exist +{ statusCode: 404, message: 'Table not found' } +// Fix: Use MCP tool to create table + +// Missing required field +{ code: '23502', message: 'null value in column "user_id"' } +// Fix: Include all required fields + +// Foreign key violation +{ code: '23503', message: 'violates foreign key constraint' } +// Fix: Referenced record must exist + +// Permission denied +{ code: '42501', message: 'permission denied for table' } +// Fix: User must be authenticated +``` + +## Debug Techniques + +### 1. Check Authentication +```javascript +const { data, error } = await client.auth.getCurrentUser(); +if (error) { + console.log('Not authenticated:', error.message); + // Need to login first +} +``` + +### 2. Verify Token Storage +```javascript +const { data } = await client.auth.getCurrentSession(); +console.log('Stored token:', data?.session?.accessToken ? 'Present' : 'Missing'); +``` + +### 3. Test Database Connection +```javascript +// Simple query to test connection +const { error } = await client.database.from('users').select().limit(1); +if (error) { + console.log('Database issue:', error); +} +``` + +### 4. Enable Network Inspection +```javascript +// Browser: Open DevTools > Network tab +// Check request headers for Authorization: Bearer token +// Check response for error details +``` + +### 5. Test With Raw HTTP +```javascript +// When SDK fails, test endpoint directly +const response = await fetch('http://localhost:7130/api/database/records/posts', { + headers: { + 'Authorization': 'Bearer ' + token + } +}); +const data = await response.json(); +console.log('Raw response:', data); +``` + +## Common Issues + +### Empty Response +```javascript +// INSERT/UPDATE/DELETE return empty without Prefer header +// SDK handles this automatically, but check if using raw API +``` + +### Array Format +```javascript +// POST requires array +// ❌ Wrong: { title: 'Test' } +// ✅ Right: [{ title: 'Test' }] +// SDK handles this automatically +``` + +### Token Not Persisting +```javascript +// Check storage adapter +const client = createClient({ + baseUrl: 'http://localhost:7130', +}); +``` + +### CORS Issues +```javascript +// Backend allows all origins by default +// If modified, check backend CORS settings +``` + +## Test Utilities + +### Full Test Suite +```javascript +async function testSDK() { + const email = `test-${Date.now()}@example.com`; + + // Test auth + console.log('Testing auth...'); + const { error: signUpError } = await client.auth.signUp({ email, password: 'test123' }); + if (signUpError) return console.error('SignUp failed:', signUpError); + + // Test current user + const { data: user, error: userError } = await client.auth.getCurrentUser(); + if (userError) return console.error('GetUser failed:', userError); + console.log('✅ Auth working, user:', user.user.id); + + // Test database + console.log('Testing database...'); + const { data: posts, error: dbError } = await client.database.from('posts').select().limit(1); + if (dbError) return console.error('Database failed:', dbError); + console.log('✅ Database working, posts:', posts.length); + + // Test insert + const { error: insertError } = await client.database + .from('posts') + .insert({ title: 'Test', user_id: user.user.id }) + .select(); + if (insertError) return console.error('Insert failed:', insertError); + console.log('✅ Insert working'); +} + +testSDK().catch(console.error); +``` + +## Backend Verification + +```javascript +// Check backend is running +fetch('http://localhost:7130/api/health') + .then(r => r.json()) + .then(d => console.log('Backend:', d)) + .catch(e => console.error('Backend not running')); +``` \ No newline at end of file diff --git a/.archive/docs/deprecated/insforge-debug.md b/.archive/docs/deprecated/insforge-debug.md new file mode 100644 index 0000000..c748678 --- /dev/null +++ b/.archive/docs/deprecated/insforge-debug.md @@ -0,0 +1,65 @@ +# Insforge Debug Guide + +## When Your API Code Fails + +**Start here** → `get-instructions` and `get-backend-metadata` (understand system state) +**Read docs** → `get-db-api`, `get-auth-api`, `get-storage-api` (read ALL of them) +**Check table** → `get-table-schema` with your table name +**Test endpoint** → Use curl with exact API format from docs + +## Critical Rule: Read Documentation First + +Before debugging, you MUST read all documentation to understand how the API works. + +## Common API Issues + +**Table created but API fails** → Check field names match schema exactly +**Array required** → PostgREST requires POST requests as arrays `[{...}]` +**Foreign key error** → Parent record must exist before child +**Permission denied** → Write operations need `Authorization: Bearer ` +**JWSError** → JWT token expired or invalid - user needs to login again +**PATCH increment fails** → PostgREST doesn't support SQL expressions like `count + 1` + +## Debug Workflow + +1. **Always** call `get-backend-metadata` first +2. Read the relevant API documentation completely +3. Check your table schema matches your API calls +4. Test with curl using exact format from docs +5. Verify response matches documentation + +### Example Debug Tests + +```bash +# Test GET endpoint +# Windows PowerShell: use curl.exe +curl -X GET http://localhost:7130/api/database/records/your_table \ + -H "Authorization: Bearer YOUR_TOKEN" | jq . + +# Test POST with array format +# Mac/Linux +curl -X POST http://localhost:7130/api/database/records/your_table \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer YOUR_TOKEN' \ + -H 'Prefer: return=representation' \ + -d '[{"field": "value"}]' | jq . + +# Windows PowerShell (use curl.exe) - different quotes for nested JSON +curl.exe -X POST http://localhost:7130/api/database/records/your_table \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Prefer: return=representation" \ + -d '[{\"field\": \"value\"}]' | jq . +``` + +## Key Rules + +- Backend runs on port 7130 +- **READ operations**: No authentication required +- **WRITE operations**: Need `Authorization: Bearer ` header +- POST requests must be arrays `[{...}]` +- System tables (prefixed with _) need special APIs +- No escaped characters in JSON +- Login/register returns JWT tokens directly in `accessToken` field + +**Remember**: MCP creates the structure, but you must follow API documentation exactly to use it. \ No newline at end of file diff --git a/.archive/docs/deprecated/insforge-instructions.md b/.archive/docs/deprecated/insforge-instructions.md new file mode 100644 index 0000000..ab82af7 --- /dev/null +++ b/.archive/docs/deprecated/insforge-instructions.md @@ -0,0 +1,124 @@ +# Insforge OSS Instructions + +## What Insforge OSS Does + +Backend-as-a-service with database, authentication, and file storage. + +**Key Concept**: InsForge replaces your traditional backend - implement business logic by calling database operations directly. Instead of building API endpoints, use our database API as your application's backend. + +## 🚨 Project Setup + +**Create your app in a NEW directory, not inside `insforge/`** + +The `insforge/` directory is the BaaS platform. Your app should live elsewhere: +``` +~/projects/ +├── insforge/ # ← BaaS platform (don't work here) +└── my-app/ # ← Your new app (work here) +``` + +## When to Use Tools + +**MUST DO FIRST** → Download project rules: `download-project-rules` +**Start here** → `get-backend-metadata` (shows current database state) +**Need docs** → `get-db-api`, `get-auth-api`, or `get-storage-api` +**Create table** → `create-table` with explicit schema +**Work with data** → Use database API endpoints directly + +## Critical Rule: +**MUST DO FIRST** → Call`download-project-rules` to download project rules + +## Critical Rule: Check Metadata First + +Before ANY database operation, call `get-backend-metadata` to get the current database state. + +## Standard Workflow + +1. **Always** call `get-backend-metadata` first +2. Check `get-instructions` if unfamiliar with the system +3. Create tables with `create-table` if needed +4. Use database API to insert/query/update/delete records +5. Call `get-backend-metadata` again to verify changes + +## Key Rules + +- Frequently check `get-instructions` and `get-backend-metadata` +- Always define explicit table schemas (no assumptions) +- Every table gets auto ID, created_at, updated_at fields +- **Database operations require**: JWT token (Authorization: Bearer header) +- **API keys are for MCP testing** (use tokens for production) +- File uploads work automatically with multipart/form-data + +## Authentication Requirements + +### Database Operations Need Authentication Token: +1. **JWT Token**: `Authorization: Bearer your-jwt-token` - Authenticates the user + +**Important Note about API Keys:** +- The `x-api-key` header is ONLY used for MCP (Model Context Protocol) testing +- Production applications should NEVER use API keys +- Always use JWT tokens from user/admin authentication for real applications + +Without the Bearer token, you'll get "permission denied" errors when trying to insert, update, or delete records. + +### Getting Authentication: +```bash +# Works on both Windows and Unix (Windows PowerShell: use curl.exe) +# 1. First login to get JWT token +curl -X POST http://localhost:7130/api/auth/admin/sessions \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"admin\",\"password\":\"your-password\"}" + +# Response includes token: {"accessToken": "eyJ...", "projectAdmin": {...}} + +# Works on both Windows and Unix (Windows PowerShell: use curl.exe) +# 2. Use the auth token for database operations +curl -X POST http://localhost:7130/api/database/records/products \ + -H "Authorization: Bearer eyJ..." \ + -H "Content-Type: application/json" \ + -d "[{\"name\": \"Product\", \"price\": 99.99}]" +``` + +## Example: Comment Upvoting Feature + +- Check current tables: `get-backend-metadata` +- Create comment_votes table: `create-table` with user_id, comment_id, vote_type fields +- Frontend upvote action: `POST /api/database/records/comment_votes` with vote data +- Frontend display scores: `GET /api/database/records/comment_votes?comment_id=eq.123` to count votes +- No separate backend needed - frontend calls InsForge database API directly + + +## Critical Rule: Test API Endpoints with curl + +After creating or modifying any API endpoint, always test it with curl to verify it works correctly. + +**Note:** Avoid special characters (!,$,`,\) in curl command data - they can cause bash interpretation issues. Use simple text for testing: + +```bash +# Works on both Windows and Unix (Windows PowerShell: use curl.exe) +# Example: Test creating a record (requires JWT token) +curl -X POST http://localhost:7130/api/database/records/posts \ + -H "x-api-key: your-api-key" \ + -H "Authorization: Bearer your-jwt-token" \ + -H "Content-Type: application/json" \ + -d '[{\"title\": \"Test Post\", \"content\": \"Test content\"}]' + +# Works on both Windows and Unix (Windows PowerShell: use curl.exe) +# Example: Test querying records (requires both API key and JWT token) +curl http://localhost:7130/api/database/records/posts?id=eq.123 \ + -H "x-api-key: your-api-key" \ + -H "Authorization: Bearer your-jwt-token" + +# Works on both Windows and Unix (Windows PowerShell: use curl.exe) +# Example: Test authentication +curl -X POST http://localhost:7130/api/auth/users \ + -H "Content-Type: application/json" \ + -d '{\"email\": \"test@example.com\", \"password\": \"testpass123\"}' +``` + +Always include: +- **Both headers for database operations**: x-api-key AND Authorization: Bearer token +- Correct HTTP method (GET, POST, PATCH, DELETE) +- Valid JSON payload for POST/PATCH requests (remember: POST requires array format `[{...}]`) +- Query parameters for filtering GET requests +- Prefer: return=representation header if you want to see the created/updated records diff --git a/.archive/docs/deprecated/insforge-project.md b/.archive/docs/deprecated/insforge-project.md new file mode 100644 index 0000000..235d917 --- /dev/null +++ b/.archive/docs/deprecated/insforge-project.md @@ -0,0 +1,118 @@ +--- +description: Insforge AI Development Rules - Essential guidelines for BaaS platform development +globs: +alwaysApply: true +--- + +# Insforge Development Rules + +## Core Identity +You are an exceptional software developer using Insforge Backend to assist building the product. Make it visually stunning, content-rich, professional-grade UIs. + +## 🔴 MANDATORY: cURL Test at EVERY Step + +**For AI Agents: Test with cURL repeatedly throughout development:** + +1. **Before coding** → Test endpoint exists, check response format +2. **During coding** → Test when confused about any API behavior +3. **After coding** → Test complete user journey end-to-end +4. **When debugging** → Test to see actual vs expected responses + +```bash +# Mac/Linux +curl -X POST http://localhost:7130/api/[endpoint] \ + -H 'Content-Type: application/json' \ + -d '[{"key": "value"}]' | jq . + +# Windows PowerShell (use curl.exe) - different quotes for nested JSON +curl.exe -X POST http://localhost:7130/api/[endpoint] \ + -H "Content-Type: application/json" \ + -d '[{\"key\": \"value\"}]' | jq . +``` + +**You WILL get it wrong without testing. Test early, test often.** + +## Critical Architecture Points + +When in doubt, read instructions documents again. + +## 🚨 CRUD Operations - PostgREST NOT RESTful +### PostgREST Database API Behavior + +**Critical PostgREST Rules:** + +1. **POST requires array**: `[{...}]` even for single record +2. **Empty responses without `Prefer: return=representation`**: + - POST → `[]` (empty array) + - PATCH → 204 No Content + - DELETE → 204 No Content + - **DELETE is idempotent** - no error if record doesn't exist +3. **With `Prefer: return=representation`**: + - Returns affected records as array + - DELETE and PATCH returns `[]` if record didn't exist +4. **Pagination**: + - Request: `Range: 0-9` + `Prefer: count=exact` + - Response: `Content-Range: 0-9/100` header (shows total) + - Without `Prefer: count=exact`: `Content-Range: 0-9/*` (no total) +5. **Query syntax**: `?field=operator.value` + - `?id=eq.123` (equals) + - `?age=gt.30` (greater than) + - `?name=like.*john*` (pattern match) + +## Auth Operations: + +### 🚨 IMPORTANT: Correct Auth Endpoints +- **Register**: `POST /api/auth/users` - Create new user account +- **Login**: `POST /api/auth/sessions` - Authenticate existing user +- **Admin Login**: `POST /api/auth/admin/sessions` - Admin authentication +- **Current User**: `GET /api/auth/sessions/current` - Get authenticated user info +- `/api/auth/sessions/current` returns `{"user": {...}}` - nested structure +- Store JWT tokens and include as `Authorization: Bearer {accessToken}` header +- **Note**: Login/register returns `{accessToken, user}` with JWT token + +### Regular API Response Format + +**⚠️ IMPORTANT: Frontend Error Handling** +- **PARSE** backend responses and display user-friendly messages +- **DO NOT** show raw API responses directly to users +- **TRANSFORM** error details into readable, actionable messages + +- Success: Data directly (object/array) +- Error: `{error, message, statusCode}` +- Empty POST/PATCH/DELETE: Add `Prefer: return=representation` + +### 🚨 Storage API Rules +- **Upload Methods**: + - **PUT** `/api/storage/buckets/{bucket}/objects/{filename}` - Upload with specific key + - **POST** `/api/storage/buckets/{bucket}/objects` - Upload with auto-generated key +- **Authentication**: Upload operations require `Authorization: Bearer {accessToken}` +- **Generate Unique Filenames**: Use POST for auto-generated keys to prevent overwrites +- **Multipart Form**: Use FormData for file uploads +- **URL Format**: Storage returns **ABSOLUTE URLs** (e.g., `http://localhost:7130/api/storage/buckets/{bucket}/objects/{filename}`) + - **IMPORTANT**: URLs are complete and ready to use - no need to prepend host or path + - Use the URL directly in `` or fetch requests + +## 🔥 Test EVERY Endpoint + +**Backend runs on port 7130** + +Always test with cURL before UI integration: +- Include `Authorization: Bearer {accessToken}` for auth +- Add `Prefer: return=representation` to see created data +- Windows PowerShell: use curl.exe + +```bash +# Mac/Linux +curl -X POST http://localhost:7130/api/database/records/posts \ + -H 'Authorization: Bearer TOKEN' \ + -H 'Content-Type: application/json' \ + -H 'Prefer: return=representation' \ + -d '[{"user_id": "from-localStorage", "caption": "Test"}]' + +# Windows PowerShell (use curl.exe) - different quotes for nested JSON +curl.exe -X POST http://localhost:7130/api/database/records/posts \ + -H "Authorization: Bearer TOKEN" \ + -H "Content-Type: application/json" \ + -H "Prefer: return=representation" \ + -d '[{\"user_id\": \"from-localStorage\", \"caption\": \"Test\"}]' +``` \ No newline at end of file diff --git a/.archive/docs/deprecated/insforge-storage-api.md b/.archive/docs/deprecated/insforge-storage-api.md new file mode 100644 index 0000000..1a0e606 --- /dev/null +++ b/.archive/docs/deprecated/insforge-storage-api.md @@ -0,0 +1,279 @@ +# Insforge OSS Storage API Documentation + +## API Basics + +**Base URL:** `http://localhost:7130` +**Authentication:** +- **Upload operations (PUT/POST/DELETE):** Requires `Authorization: Bearer ` header +- **Download from public buckets:** No authentication required +- **List/manage buckets:** Requires authentication +- **API keys are for MCP testing** (use tokens for production) +**System:** Bucket-based storage with public/private access control + +### 🔴 CRITICAL: URL Format +**Storage returns ABSOLUTE URLs** that are ready to use directly: +- Response `url` field contains complete URL: `http://localhost:7130/api/storage/buckets/{bucket}/objects/{filename}` +- **DO NOT prepend host or modify the URL** - use it exactly as returned +- URLs work directly in ``, `
) THEN END IF` pattern. + - When adding a new RLS-enforced table: enable RLS, `GRANT` table-level CRUD to `authenticated`, and write per-operation policies (SELECT, INSERT, UPDATE, DELETE). Public-bucket-style anonymous bypasses live at the route layer before calling the RLS helper, not in policies. + - Normal raw SQL and custom migrations execute as `project_admin`. It has service-key row visibility, but PostgreSQL grants and ownership still limit object access and DDL. + +6. Always write unit tests for new code. + - Every new feature, migration, service, or bug fix should have accompanying unit tests. + - For migrations, write tests that validate SQL structure and idempotency guards (see `tests/unit/redirect-url-whitelist-migration.test.ts` for the pattern). + - For services, test business logic and error cases. + - For RLS-gated services, mock the pool/client and pin the SQL sequence (see `tests/unit/user-context.service.test.ts` and `tests/unit/storage-object-is-visible.test.ts`). + - Run the full test suite before submitting work: `cd backend && npm test`. + +## Validation + +- `cd backend && npm test` +- `cd backend && npm run build` + +For contract changes, also validate `packages/shared-schemas/` and any affected dashboard consumers. diff --git a/.claude/skills/insforge-dev/dashboard/SKILL.md b/.claude/skills/insforge-dev/dashboard/SKILL.md new file mode 100644 index 0000000..89e9e6d --- /dev/null +++ b/.claude/skills/insforge-dev/dashboard/SKILL.md @@ -0,0 +1,107 @@ +--- +name: dashboard +description: Use this skill when contributing to InsForge's shared dashboard package. This is for maintainers editing `packages/dashboard`, which ships in `self-hosting` and `cloud-hosting` modes, and the local `frontend/` shell used for `self-hosting` in this repo. +--- + +# InsForge Dev Dashboard + +Use this skill for dashboard work in the InsForge repository. + +## Scope + +- `packages/dashboard/src/**` +- `packages/dashboard/package.json` +- `packages/dashboard/README.md` +- `packages/dashboard/*.config.*` +- `frontend/src/**` +- `frontend/package.json` + +## Working Rules + +1. Respect the shared-package versus host-app boundary. + - This dashboard is built with React and TypeScript. + - `packages/dashboard/` is the source of truth for the dashboard product. + - The package must support both `self-hosting` and `cloud-hosting` modes. + - Keep self-hosting-only bootstrap, local env defaults, and shell styling in `frontend/`. + - Do not let `packages/dashboard/` depend on `frontend/`. + - If both modes need a capability, define it in the package API first. + +2. Preserve dashboard data-flow conventions. + - Follow the flow `service -> hook -> UI`. + - Use `apiClient` for HTTP calls so auth refresh and error handling stay consistent. + - Put request logic in services, data fetching and mutation state in hooks, and rendering/orchestration in UI components and pages. + - Reuse existing contexts, host abstractions, and hooks before creating new global state. + +3. Reuse the existing component layers. + - Use `@insforge/ui` for generic primitives. + - Use shared dashboard components when the pattern is already present. + - Keep reusable dashboard UI in `packages/dashboard/`. + - Only add UI to `frontend/` when it is specific to the local self-hosting shell. + - Keep package styles scoped to the dashboard container. + +4. Keep the package surface aligned with shared contracts. + - Import cross-package types and Zod-derived shapes from `@insforge/shared-schemas`. + - When backend payloads change, update the related services, hooks, UI, and exported types together. + - Keep `packages/dashboard/src/index.ts` and `packages/dashboard/src/types` aligned with the public package API. + - Never use the TypeScript `any` type. Prefer precise prop, state, API, and hook result types. + +## Frontend Testing + +Use the lowest test layer that covers the risk. Add one focused regression test for bug fixes when practical. + +| Change type | Test layer | Location | Command | +| --- | --- | --- | --- | +| Pure helper, parser, formatter, state reducer | Unit | `packages/dashboard/src/**/__tests__/*.test.ts` | `npm --workspace @insforge/dashboard run test:unit` | +| React component behavior, forms, dialogs, conditional rendering | Component | `packages/dashboard/src/**/__tests__/*.test.tsx` | `npm --workspace @insforge/dashboard run test:component` | +| Routing, auth redirects, host-mode integration, browser-only behavior | UI smoke | `packages/dashboard/tests/ui/*.spec.ts` | `npm --workspace @insforge/dashboard run test:ui` | + +Conventions: + +- `npm --workspace @insforge/dashboard run test` runs the full Vitest suite: unit plus component. +- GitHub Actions splits dashboard checks into unit, component, and UI jobs in `.github/workflows/frontend-tests.yml`. Update the workflow when adding or renaming test scripts. +- Unit tests should avoid React rendering and network mocking. Test data transformations directly. +- Component tests use Testing Library with `packages/dashboard/src/test/setup.ts`. Mock hooks, services, and host context at the package boundary; assert user-visible behavior and callback effects, not implementation details or CSS class strings. +- UI smoke tests use Playwright against the local `frontend/` shell. Mock backend API routes in `packages/dashboard/tests/ui/fixtures/`; every mocked route must either fulfill, fallback, or abort. Do not leave requests pending after a test branch. +- Prefer one clear test per user-observable behavior over broad snapshot tests. Avoid brittle assertions tied to copy that is not part of the behavior being protected. + +## Local debug: viewing cloud-hosting-only UI in self-hosting + +**Use when** previewing UI gated on `useIsCloudHostingMode()`, `isInsForgeCloudProject()`, or a PostHog feature flag (e.g. the CTest dashboard variant, `dashboard-v3-experiment === 'c_test'`, the CLI connect panel) while running the local `frontend/` self-hosting shell. + +The lowest-friction approach is to **temporarily hardcode** the three gates below to `true`/the new branch, then restart the Vite dev server. These edits bypass real host/project detection and MUST be fully reverted before committing — landing them breaks both self-hosting and cloud-hosting users. + +### Hardcodes + +1. `packages/dashboard/src/lib/config/DashboardHostContext.tsx` — `useIsCloudHostingMode()` → `return true;` (was `useDashboardHost().mode === 'cloud-hosting'`). +2. `packages/dashboard/src/lib/utils/utils.ts` — `isInsForgeCloudProject()` → `return true;` (was the `.insforge.app` hostname check). +3. If the UI is also feature-flag-gated, hardcode the consumer. For CTest: `AppRoutes.tsx` → `const DashboardHomePage = CTestDashboardPage;` and, if relevant, the matching branch in `AppLayout.tsx` for ``. + +Mark every hardcode with a trailing `// LOCAL DEBUG: ` comment so revert is a mechanical search. + +### Revert checklist — run all before committing + +1. `git grep -n "LOCAL DEBUG" packages/dashboard/src/` returns zero matches. +2. Each gate is restored to its **original expression**, not just an equivalent value (the `mode === 'cloud-hosting'` comparison, the hostname check, the `getFeatureFlag(...)` call must all be back). +3. Any imports deleted during debug (commonly `DashboardPage`, `getFeatureFlag`, `ConnectDialog`) are restored. +4. `cd packages/dashboard && npm run lint && npm run typecheck` both pass. +5. `git diff` of the four files above shows only intended changes — no `return true;`, no missing imports. + +### Rationalizations to reject + +| Excuse | Reality | +|--------|---------| +| "I'll revert in a follow-up PR." | Follow-up = a window where prod is broken. Revert now. | +| "The original check was effectively the same." | If it were, you wouldn't have needed the hardcode. Restore the expression, not a value-equivalent. | +| "Lint passed, so the deleted import doesn't matter." | Lint passed because the import was deleted; on revert the original code needs it back. | +| "I'll ship the env-var override instead." | No env-var override is wired in the code. Don't invent one on the commit path — restore the original. | + +## Validation + +- `cd packages/dashboard && npm run test:unit` when changing pure helpers or reducers +- `cd packages/dashboard && npm run test:component` when changing React component behavior +- `cd packages/dashboard && npm run test:ui` when changing routing, auth, host-mode integration, or browser-only behavior +- `cd packages/dashboard && npm run typecheck` +- `cd packages/dashboard && npm run build` +- `cd frontend && npm run build` when the local self-hosting shell changes + +For shared contract changes, also validate `packages/shared-schemas/` and the affected backend surface. diff --git a/.claude/skills/insforge-dev/docs/SKILL.md b/.claude/skills/insforge-dev/docs/SKILL.md new file mode 100644 index 0000000..7e5e1eb --- /dev/null +++ b/.claude/skills/insforge-dev/docs/SKILL.md @@ -0,0 +1,47 @@ +--- +name: docs +description: Use this skill when contributing to InsForge's product documentation in this repository. This is for maintainers editing public docs in `docs/core-concepts`, agent docs in `.agents/docs`, SDK integration guides in `docs/sdks`, and OpenAPI specs in `openapi`. +--- + +# InsForge Dev Docs + +Use this skill for `docs/core-concepts/`, `.agents/docs/`, `docs/sdks/`, and `openapi/` in the InsForge repository. + +The documentation in this repo is primarily product documentation for InsForge users and agents integrating with InsForge. This skill is for InsForge engineers maintaining that public documentation surface. + +## Scope + +- `docs/core-concepts/**` +- `.agents/docs/**` +- `docs/sdks/**` +- `openapi/**` + +## Working Rules + +1. Put each document in the correct documentation surface. + - Human-friendly docs published on the public doc site belong in `docs/core-concepts/` and related public doc folders. + - For implementation-heavy public docs, prefer an `architecture.md` file inside the relevant `docs/core-concepts//` folder. + - Agent-only instructions belong in `.agents/docs/`. + - SDK integration guides for each framework belong in `docs/sdks/`. + - OpenAPI contract changes belong in the matching files under `openapi/`. + +2. Match the writing style to the audience. + - Public docs should be human-friendly and explain the implementation clearly. + - Keep public docs human-sounding: avoid AI-writing tells such as em dashes, rule-of-three lists, "not just X but Y" parallelism, inflated significance, vague attribution, and AI vocabulary (delve, leverage, underscore, seamless, robust). See the `doc-author` skill's `INSFORGE.md` overlay, "Sound human, not AI-generated". + - `architecture.md` pages in `docs/core-concepts/` should explain how the feature works in detail. + - Agent docs in `.agents/docs/` should be instruction-first and execution-oriented. + - Agent docs should avoid explanatory filler and focus on the exact steps an agent should follow to complete the work. + +3. Prevent documentation drift on every implementation change. + - Before changing implementation, check the current user-facing docs for that feature. + - After changing implementation, update the relevant Markdown docs and the relevant OpenAPI YAML files in the same pass. + - Do not treat OpenAPI and Markdown as separate optional follow-ups when the feature contract or behavior changed. + - If a change affects agent workflows, update the corresponding file in `.agents/docs/`. + - If a change affects public product understanding, update the corresponding file in `docs/core-concepts/`, including `architecture.md` when implementation details changed. + - If a change affects SDK integration guidance, update the corresponding framework guide in `docs/sdks/`. + +## Validation + +- Re-read every documented command, path, route, and payload for correctness. +- Cross-check OpenAPI YAML and Markdown docs against the implemented behavior before finishing. +- Mention anything you could not verify directly. diff --git a/.claude/skills/insforge-dev/e2e-testing/SKILL.md b/.claude/skills/insforge-dev/e2e-testing/SKILL.md new file mode 100644 index 0000000..3a7b651 --- /dev/null +++ b/.claude/skills/insforge-dev/e2e-testing/SKILL.md @@ -0,0 +1,142 @@ +--- +name: e2e-testing +description: Use this skill when an InsForge maintainer has finished an OSS repo change and is ready to open, update, or submit the InsForge PR. Runs the release-quality deterministic E2E gate by building a package.json-derived InsForge test image tag, deciding whether sibling agent-e2e fixture coverage must change, dispatching the Deterministic Fixture E2E workflow, waiting for results, and triaging failures before PR submission. +--- + +# InsForge E2E Testing Gate + +Use this skill after local implementation and normal InsForge pre-PR checks pass, and before opening, updating, or submitting the InsForge OSS PR. + +This is an additional release-quality gate. It does not replace local `typecheck`, `lint`, `test`, or `build` validation from the parent `insforge-dev` skill. + +## Repositories + +- InsForge OSS repo: current workspace. +- E2E repo: remote GitHub repository `InsForge/agent-e2e`. + +Use the remote `InsForge/agent-e2e` repository for workflow dispatch and read-only workflow checks. Do not rely on a developer-specific local checkout path. Create or use a local checkout only when the deterministic fixture tests must be edited. + +## Build The Test Tag + +1. Read the root InsForge `package.json` version. +2. Increment only the patch number. +3. Build a tag in this form: + +```text +v..- +``` + +Example: root version `2.2.3` and feature `storage returning rls` becomes `v2.2.4-storage-returning-rls`. + +Use the `package.json` version as the only source of truth for the base version. Ignore higher existing test tags when calculating the base version. + +Slug rules: + +- Prefer the issue key, PR topic, or branch topic. +- Lowercase all letters. +- Replace runs of non-alphanumeric characters with one hyphen. +- Trim leading and trailing hyphens. +- Keep it short enough to scan in GitHub Actions and image tags. + +## Build The InsForge Image + +Dispatch the InsForge `Build and Push Docker Image` workflow with the test tag: + +```bash +gh workflow run "Build and Push Docker Image" --repo InsForge/InsForge --ref -f test_tag= +``` + +Then wait for the matching run to complete: + +```bash +gh run list --repo InsForge/InsForge --workflow "Build and Push Docker Image" --limit 10 +gh run watch --repo InsForge/InsForge +``` + +Do not start the cross-repo E2E workflow until the image build succeeds. + +## Decide Whether `agent-e2e` Must Change + +Inspect the InsForge diff and compare it with deterministic fixture coverage in `InsForge/agent-e2e`. + +For read-only checks, prefer remote GitHub access such as `gh api`, `gh repo view`, or remote file reads. Use a local checkout only when editing fixture files or when remote inspection is not enough to understand coverage. + +Update `agent-e2e` when the InsForge change adds, removes, or changes behavior that the deterministic fixture should assert, including: + +- API contract or validation behavior. +- Auth, permissions, RLS, storage, realtime, functions, schedules, AI, SDK, or CLI-visible behavior. +- Any regression that local tests cover but the release gate should also protect across the deployed runtime. + +Do not update `agent-e2e` for internal refactors, docs-only changes, local test-only changes, or behavior already covered by the deterministic fixture with no assertion change needed. + +Ignore `Support Desk Agent E2E (Exploratory)`. It is not part of this gate. + +## If No E2E Test Update Is Needed + +Run the deterministic fixture workflow from `agent-e2e` main: + +```bash +gh workflow run "Deterministic Fixture E2E" --repo InsForge/agent-e2e --ref main -f insforge_tag= +``` + +Find and watch the run: + +```bash +gh run list --repo InsForge/agent-e2e --workflow "Deterministic Fixture E2E" --limit 10 +gh run watch --repo InsForge/agent-e2e +``` + +## If E2E Tests Need An Update + +Work in a local checkout of the remote `InsForge/agent-e2e` repo only for the fixture update. + +1. Use an existing clean checkout or clone `https://github.com/InsForge/agent-e2e.git` into an isolated workspace. +2. Fetch `origin` and start from `origin/main`. +3. Create a branch named `codex/`. +4. Update only the deterministic fixture validators, fixtures, app assertions, and docs needed for the InsForge behavior change. +5. Run the smallest local validation that gives confidence: + +```bash +npm run typecheck +npm run lint +npm run fixture:e2e:dry +``` + +Run broader validation when the changed fixture area supports it. + +6. Open an `agent-e2e` PR for the fixture update. +7. Dispatch the deterministic fixture workflow from the `agent-e2e` branch that contains the fixture update: + +```bash +gh workflow run "Deterministic Fixture E2E" --repo InsForge/agent-e2e --ref -f insforge_tag= +``` + +8. Watch the run before proceeding with the InsForge PR. + +## Interpret Results + +If the deterministic fixture workflow passes: + +- Link the run in the InsForge PR body or final PR notes. +- If an `agent-e2e` PR was required, link that PR too. +- Proceed with opening, updating, or submitting the InsForge OSS PR. + +If the deterministic fixture workflow fails: + +1. Inspect the failed job logs and uploaded artifact. +2. Identify whether the failure is caused by the InsForge implementation, the new or existing deterministic fixture, a transient infrastructure problem, or an unrelated existing failure. +3. Fix the correct branch: + - InsForge implementation bug: update the InsForge branch, rebuild the test image with the same tag or a new short retry tag, then rerun E2E. + - Fixture bug or missing assertion update: update the `agent-e2e` branch, rerun local validation, then rerun E2E from that branch. + - Transient infrastructure issue: rerun once after noting the evidence. +4. Do not submit the InsForge PR until the deterministic result is clear or the user explicitly accepts the risk. + +## Report Back + +When finished, report: + +- Test tag used. +- InsForge image workflow run result. +- Whether `agent-e2e` changed. +- Deterministic Fixture E2E run result. +- Links to the InsForge PR, `agent-e2e` PR if any, and workflow runs. diff --git a/.claude/skills/insforge-dev/shared-schemas/SKILL.md b/.claude/skills/insforge-dev/shared-schemas/SKILL.md new file mode 100644 index 0000000..84bc60d --- /dev/null +++ b/.claude/skills/insforge-dev/shared-schemas/SKILL.md @@ -0,0 +1,43 @@ +--- +name: shared-schemas +description: Use this skill when contributing to InsForge's shared schema package. This is for maintainers editing published Zod contracts, exported types, and shared API payload definitions consumed by InsForge packages in this repo and other InsForge tooling. +--- + +# InsForge Dev Shared Schemas + +Use this skill for `packages/shared-schemas/` work in the InsForge repository. + +## Scope + +- `packages/shared-schemas/src/**` +- any code in this repo that consumes the changed contracts +- downstream InsForge tooling that depends on the published `@insforge/shared-schemas` package, including consumers not present in this repository + +## Working Rules + +1. Treat `packages/shared-schemas/` as the source of truth for cross-package payloads. + - If a request, response, or domain shape is shared across InsForge surfaces, define it here. + - Do not duplicate the same contract in package-local files. + - Remember that this package is not only for the repo's backend and dashboard packages. It is also consumed by other InsForge tooling, including MCP and SDK code that may live outside this repository. + +2. Keep schemas organized by domain. + - Follow the existing `*.schema.ts` and `*-api.schema.ts` split when it fits the current package pattern. + - Keep `packages/shared-schemas/src/index.ts` aligned with the intended public API. + - Treat exported names and schema shapes as a public contract surface, not just an internal refactor target. + +3. Use schema changes as a synchronization trigger. + - Update backend validation and response usage. + - Update shared dashboard services, hooks, and UI assumptions in `packages/dashboard/`. + - Check for import sites across `packages/*`, `frontend/`, and `backend/` before finishing. + - Call out likely downstream impact on MCP, SDK, or other external InsForge consumers when a change alters exported names, schema semantics, or payload shape. + - Be conservative with breaking changes. If a breaking contract change is necessary, make it explicit in the handoff. + - Never use the TypeScript `any` type. Shared contracts should stay explicit and trustworthy across package boundaries. + +## Validation + +- `cd packages/shared-schemas && npm run build` +- `cd backend && npx tsc --noEmit` +- `cd packages/dashboard && npm run typecheck` + +Run package-specific tests as needed where behavior changed. +If external consumers such as MCP or SDK cannot be validated from this repo, say that clearly instead of implying they were covered. diff --git a/.claude/skills/insforge-dev/ui/SKILL.md b/.claude/skills/insforge-dev/ui/SKILL.md new file mode 100644 index 0000000..885fd5c --- /dev/null +++ b/.claude/skills/insforge-dev/ui/SKILL.md @@ -0,0 +1,42 @@ +--- +name: ui +description: Use this skill when contributing to InsForge's reusable UI package. This is for maintainers editing design-system primitives, exports, styles, and package-level component behavior in the InsForge monorepo. +--- + +# InsForge Dev UI + +Use this skill for `packages/ui/` work in the InsForge repository. + +## Scope + +- `packages/ui/src/components/**` +- `packages/ui/src/lib/**` +- `packages/ui/src/index.ts` +- `packages/ui/src/styles.css` + +## Working Rules + +1. Put only reusable primitives here. + - If the component is generic across dashboard features or other InsForge apps, it belongs in `packages/ui/`. + - If it is tightly coupled to one dashboard workflow but should ship to both OSS and cloud hosts, keep it in `packages/dashboard/`. + - If it is only for the self-hosting host app, keep it in `frontend/`. + +2. Preserve the package's implementation style. + - Use `class-variance-authority` for variants when appropriate. + - Use the shared `cn()` helper for class merging. + - Follow the existing Radix-wrapper and typed-export patterns. + +3. Keep the public surface in sync. + - Export new public components from `packages/ui/src/index.ts`. + - Avoid adding internal-only abstractions to the package surface unless they are meant to be consumed. + - Never use the TypeScript `any` type. Keep component props and exported helpers strictly typed. + +4. Validate downstream impact. + - The shared dashboard package consumes this package directly, so UI changes can break `packages/dashboard/` even if `packages/ui/` itself builds cleanly. + +## Validation + +- `cd packages/ui && npm run build` +- `cd packages/ui && npm run typecheck` + +Also validate `packages/dashboard/` when the changed component is used in the dashboard, and validate `frontend/` if the host app integration or CSS entrypoints changed. diff --git a/.codex/skills/insforge-dev/SKILL.md b/.codex/skills/insforge-dev/SKILL.md new file mode 100644 index 0000000..6df9eb3 --- /dev/null +++ b/.codex/skills/insforge-dev/SKILL.md @@ -0,0 +1,73 @@ +--- +name: insforge-dev +description: Use this skill set when contributing to the InsForge monorepo itself. This is for InsForge maintainers and contributors editing the platform, the shared dashboard package, the self-hosting shell, the UI library, shared schemas, tests, or docs. +--- + +# InsForge Dev + +Use this skill set for work inside the InsForge repository. + +Then use the narrowest package skill that matches the task: + +- `backend` +- `dashboard` +- `ui` +- `shared-schemas` +- `docs` + +Use the cross-repo release workflow skill when preparing an OSS PR: + +- `e2e-testing` + +## Core Rules + +1. Identify the package boundary before editing. + - `backend/`: API, auth, database, providers, realtime, schedules + - `packages/dashboard/`: publishable dashboard package that supports `self-hosting` and `cloud-hosting` modes + - `frontend/`: local React + Vite shell that mounts `packages/dashboard/` in `self-hosting` mode + - `packages/shared-schemas/`: cross-package contracts + - `packages/ui/`: reusable design-system primitives + - `docs/`: product and agent-facing documentation + +2. Put code in the narrowest correct layer. + - Contract change: `packages/shared-schemas/` first, then consumers. + - Backend behavior: route -> service -> provider/infra. + - Shared dashboard behavior, routes, features, exports, and host contracts: `packages/dashboard/`. + - Self-hosting-only bootstrap, env wiring, and shell styling: `frontend/`. + - Reusable primitive: `packages/ui/` first. + +3. Preserve repo conventions. + - Backend TS source uses ESM-style `.js` import specifiers. + - Backend success responses usually return raw JSON, not `{ data }`. + - Backend validation commonly uses shared Zod schemas plus `AppError`. + - Dashboard data access goes through `apiClient` and React Query. + - Dashboard frontend tests are split into Vitest unit tests, Vitest component tests, and Playwright UI smoke tests. + - Shared payloads belong in `@insforge/shared-schemas`. + - Never use the TypeScript `any` type. Prefer precise types, schema-derived types, `unknown`, or generics. + +4. Do not confuse repo development with app development on InsForge. + - This repo contains the platform, the publishable dashboard package, and a local shell for self-hosting mode. + - Keep guidance focused on maintaining InsForge itself. + +## Finish Rules + +- Run the smallest validation that gives confidence for the change. +- Use repo-level checks like `npm run lint`, `npm run build`, `npm run typecheck`, and `npm test` when the change crosses package boundaries — each is wired through Turborepo (`turbo run `) and covers every workspace, including `packages/dashboard/` and `packages/shared-schemas/`. +- For dashboard UI behavior changes, choose the lowest useful frontend test layer from the `dashboard` skill and run that command before reporting back. +- Use the package-specific validation steps in the child skill when the work is isolated to one package. +- When reporting back, state what changed, what you validated, and what you could not validate. + +## Pre-PR Checklist (Mandatory Before Pushing to a PR) + +Before opening a PR or pushing new commits to an existing PR branch, run **all** of the following from the repo root and do not proceed while any of them fail on files your change touches: + +1. `npx turbo run typecheck` — must pass across all packages. +2. `npx turbo run lint` — must pass. If the failure is pre-existing in `main` and unrelated to your change, scope it: + - Run `npx eslint ` and confirm your files are clean. + - Call out the pre-existing debt in the PR body so reviewers know it is not yours. + - Auto-fixable prettier/eslint errors in your own diff must be fixed (`npx eslint --fix ` or `npm run format`). +3. `npx turbo run test` (or the package-specific test command) — all tests must pass, including any new tests you added for the change. +4. `npx turbo run build` if routing, config, schemas, or cross-package exports changed. +5. Use `e2e-testing` to run the deterministic cross-repo E2E gate before opening, updating, or submitting the InsForge OSS PR. + +Never push with failing checks on files you touched, even if CI would catch them later. CI failures slow reviewers down and the lint fix almost always takes less than a minute locally. diff --git a/.codex/skills/insforge-dev/backend/SKILL.md b/.codex/skills/insforge-dev/backend/SKILL.md new file mode 100644 index 0000000..ea69fc0 --- /dev/null +++ b/.codex/skills/insforge-dev/backend/SKILL.md @@ -0,0 +1,74 @@ +--- +name: backend +description: Use this skill when contributing to InsForge's backend package. This is for maintainers editing backend routes, services, providers, auth, database logic (including RLS-enforced surfaces like storage and realtime), schedules, or backend tests in the InsForge monorepo. +--- + +# InsForge Dev Backend + +Use this skill for `backend/` work in the InsForge repository. + +## Scope + +- `backend/src/api/**` +- `backend/src/services/**` +- `backend/src/providers/**` +- `backend/src/infra/**` +- `backend/tests/**` + +## Working Rules + +1. Keep the route -> service -> provider/infra split intact. + - Routes handle auth, parsing, validation, and delegation. + - Services own business logic and orchestration. + - Providers and infra wrap external systems or lower-level integrations. + - Service layer code should be the only layer that interacts with the core PostgreSQL database. + - Do not put direct database access in routes. + - Do not bypass services when reading from or writing to Postgres. + +2. Follow backend conventions. + - Use ESM-style `.js` import specifiers in TypeScript source. + - InsForge's core database is PostgreSQL. + - InsForge currently runs as a single-instance server, so be careful about introducing logic that assumes distributed coordination, cross-instance locking, or background worker separation. + - Reuse shared schemas from `@insforge/shared-schemas` when contracts cross packages. + - Use `safeParse` plus `AppError` for invalid input. + - Return successful results through `successResponse`. + - Preserve existing auth middleware patterns such as `verifyAdmin`, `verifyUser`, and `verifyApiKey`. + - Never use the TypeScript `any` type. Prefer precise interfaces, schema-derived types, `unknown`, or constrained generics. + - For schema changes, write a new migration file instead of editing database structure manually. + - Put schema changes under `backend/src/infra/database/migrations/`. + +3. Write idempotent migrations. Every SQL migration must be safe to re-run. + - Use `CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS`, `ADD COLUMN IF NOT EXISTS`. + - Never use bare `ALTER TABLE ... RENAME TO` — it fails if the target name already exists. Wrap renames in a `DO` block that checks `information_schema.tables` for both source and target. + - Always `DROP TRIGGER IF EXISTS` before `CREATE TRIGGER`. + - Guard data migrations and `DROP COLUMN` behind `information_schema.columns` checks when the column may already be gone. + - Use `ON CONFLICT` or `WHERE NOT EXISTS` for seed `INSERT` statements. + +4. Preserve existing behavior around mutation flows. + - Keep audit logging when surrounding routes already log state changes. + - Keep error handling flowing through shared middleware. + - Do not introduce a new response envelope unless the existing feature already uses one. + - For critical flows with multiple dependent database writes, use an explicit transactional process so the whole operation succeeds or fails together. + - Be especially careful with transactions around auth, secrets, billing-like usage updates, schema changes, and any flow that would leave the system inconsistent if partially applied. + +5. Use Postgres Row Level Security, not app-side filters, for tables accessed via authenticated end-user routes (anything where `req.user` reaches the service layer). RLS-enforced services such as storage, realtime, and payments should use `withUserContext`. Tables accessed only by admin or service-internal paths (audit logs, billing aggregations) don't need RLS. Do not write `WHERE user_id = $1` filters in services; let RLS evaluate `auth.jwt() ->> 'sub'` against the row. + - Plumb identity through `withUserContext(pool, ctx, fn, settings?)` from `services/database/user-context.service.ts`. It opens a transaction, sets `SET LOCAL ROLE` plus the canonical `request.jwt.claims` JSON GUC via `set_config`, applies optional transaction-local settings such as `realtime.channel_name`, runs `fn`, commits on success or rolls back on error, and resets role in `finally` so policies see the calling user via `auth.jwt() ->> 'sub'`. + - Keep `UserContext` user-only and defined in `api/middlewares/auth.ts`: `{ id, role, email? }` (`id` is always present at the API level). API keys and admin bypass flags do not belong inside `UserContext`. + - Routes that issue out-of-band URLs (S3 presigned redirects, signed download links, anything the client redeems against a service that won't re-evaluate RLS) must do an explicit RLS-scoped existence check before handing the URL out — RLS does not fire when the client redeems the URL directly. See `StorageService.objectIsVisible` as the template. + - Migrations that enable RLS on an existing populated table must auto-install a sensible default policy set so the upgrade does not silently break existing rows. See migration 036's `IF EXISTS (SELECT 1 FROM
) THEN END IF` pattern. + - When adding a new RLS-enforced table: enable RLS, `GRANT` table-level CRUD to `authenticated`, and write per-operation policies (SELECT, INSERT, UPDATE, DELETE). Public-bucket-style anonymous bypasses live at the route layer before calling the RLS helper, not in policies. + - Normal raw SQL and custom migrations execute as `project_admin`. It has service-key row visibility, but PostgreSQL grants and ownership still limit object access and DDL. + +6. Always write unit tests for new code. + - Every new feature, migration, service, or bug fix should have accompanying unit tests. + - For migrations, write tests that validate SQL structure and idempotency guards (see `tests/unit/redirect-url-whitelist-migration.test.ts` for the pattern). + - For services, test business logic and error cases. + - For RLS-gated services, mock the pool/client and pin the SQL sequence (see `tests/unit/user-context.service.test.ts` and `tests/unit/storage-object-is-visible.test.ts`). + - Run the full test suite before submitting work: `cd backend && npm test`. + +## Validation + +- `cd backend && npm test` +- `cd backend && npm run build` + +For contract changes, also validate `packages/shared-schemas/` and any affected dashboard consumers. diff --git a/.codex/skills/insforge-dev/dashboard/SKILL.md b/.codex/skills/insforge-dev/dashboard/SKILL.md new file mode 100644 index 0000000..89e9e6d --- /dev/null +++ b/.codex/skills/insforge-dev/dashboard/SKILL.md @@ -0,0 +1,107 @@ +--- +name: dashboard +description: Use this skill when contributing to InsForge's shared dashboard package. This is for maintainers editing `packages/dashboard`, which ships in `self-hosting` and `cloud-hosting` modes, and the local `frontend/` shell used for `self-hosting` in this repo. +--- + +# InsForge Dev Dashboard + +Use this skill for dashboard work in the InsForge repository. + +## Scope + +- `packages/dashboard/src/**` +- `packages/dashboard/package.json` +- `packages/dashboard/README.md` +- `packages/dashboard/*.config.*` +- `frontend/src/**` +- `frontend/package.json` + +## Working Rules + +1. Respect the shared-package versus host-app boundary. + - This dashboard is built with React and TypeScript. + - `packages/dashboard/` is the source of truth for the dashboard product. + - The package must support both `self-hosting` and `cloud-hosting` modes. + - Keep self-hosting-only bootstrap, local env defaults, and shell styling in `frontend/`. + - Do not let `packages/dashboard/` depend on `frontend/`. + - If both modes need a capability, define it in the package API first. + +2. Preserve dashboard data-flow conventions. + - Follow the flow `service -> hook -> UI`. + - Use `apiClient` for HTTP calls so auth refresh and error handling stay consistent. + - Put request logic in services, data fetching and mutation state in hooks, and rendering/orchestration in UI components and pages. + - Reuse existing contexts, host abstractions, and hooks before creating new global state. + +3. Reuse the existing component layers. + - Use `@insforge/ui` for generic primitives. + - Use shared dashboard components when the pattern is already present. + - Keep reusable dashboard UI in `packages/dashboard/`. + - Only add UI to `frontend/` when it is specific to the local self-hosting shell. + - Keep package styles scoped to the dashboard container. + +4. Keep the package surface aligned with shared contracts. + - Import cross-package types and Zod-derived shapes from `@insforge/shared-schemas`. + - When backend payloads change, update the related services, hooks, UI, and exported types together. + - Keep `packages/dashboard/src/index.ts` and `packages/dashboard/src/types` aligned with the public package API. + - Never use the TypeScript `any` type. Prefer precise prop, state, API, and hook result types. + +## Frontend Testing + +Use the lowest test layer that covers the risk. Add one focused regression test for bug fixes when practical. + +| Change type | Test layer | Location | Command | +| --- | --- | --- | --- | +| Pure helper, parser, formatter, state reducer | Unit | `packages/dashboard/src/**/__tests__/*.test.ts` | `npm --workspace @insforge/dashboard run test:unit` | +| React component behavior, forms, dialogs, conditional rendering | Component | `packages/dashboard/src/**/__tests__/*.test.tsx` | `npm --workspace @insforge/dashboard run test:component` | +| Routing, auth redirects, host-mode integration, browser-only behavior | UI smoke | `packages/dashboard/tests/ui/*.spec.ts` | `npm --workspace @insforge/dashboard run test:ui` | + +Conventions: + +- `npm --workspace @insforge/dashboard run test` runs the full Vitest suite: unit plus component. +- GitHub Actions splits dashboard checks into unit, component, and UI jobs in `.github/workflows/frontend-tests.yml`. Update the workflow when adding or renaming test scripts. +- Unit tests should avoid React rendering and network mocking. Test data transformations directly. +- Component tests use Testing Library with `packages/dashboard/src/test/setup.ts`. Mock hooks, services, and host context at the package boundary; assert user-visible behavior and callback effects, not implementation details or CSS class strings. +- UI smoke tests use Playwright against the local `frontend/` shell. Mock backend API routes in `packages/dashboard/tests/ui/fixtures/`; every mocked route must either fulfill, fallback, or abort. Do not leave requests pending after a test branch. +- Prefer one clear test per user-observable behavior over broad snapshot tests. Avoid brittle assertions tied to copy that is not part of the behavior being protected. + +## Local debug: viewing cloud-hosting-only UI in self-hosting + +**Use when** previewing UI gated on `useIsCloudHostingMode()`, `isInsForgeCloudProject()`, or a PostHog feature flag (e.g. the CTest dashboard variant, `dashboard-v3-experiment === 'c_test'`, the CLI connect panel) while running the local `frontend/` self-hosting shell. + +The lowest-friction approach is to **temporarily hardcode** the three gates below to `true`/the new branch, then restart the Vite dev server. These edits bypass real host/project detection and MUST be fully reverted before committing — landing them breaks both self-hosting and cloud-hosting users. + +### Hardcodes + +1. `packages/dashboard/src/lib/config/DashboardHostContext.tsx` — `useIsCloudHostingMode()` → `return true;` (was `useDashboardHost().mode === 'cloud-hosting'`). +2. `packages/dashboard/src/lib/utils/utils.ts` — `isInsForgeCloudProject()` → `return true;` (was the `.insforge.app` hostname check). +3. If the UI is also feature-flag-gated, hardcode the consumer. For CTest: `AppRoutes.tsx` → `const DashboardHomePage = CTestDashboardPage;` and, if relevant, the matching branch in `AppLayout.tsx` for ``. + +Mark every hardcode with a trailing `// LOCAL DEBUG: ` comment so revert is a mechanical search. + +### Revert checklist — run all before committing + +1. `git grep -n "LOCAL DEBUG" packages/dashboard/src/` returns zero matches. +2. Each gate is restored to its **original expression**, not just an equivalent value (the `mode === 'cloud-hosting'` comparison, the hostname check, the `getFeatureFlag(...)` call must all be back). +3. Any imports deleted during debug (commonly `DashboardPage`, `getFeatureFlag`, `ConnectDialog`) are restored. +4. `cd packages/dashboard && npm run lint && npm run typecheck` both pass. +5. `git diff` of the four files above shows only intended changes — no `return true;`, no missing imports. + +### Rationalizations to reject + +| Excuse | Reality | +|--------|---------| +| "I'll revert in a follow-up PR." | Follow-up = a window where prod is broken. Revert now. | +| "The original check was effectively the same." | If it were, you wouldn't have needed the hardcode. Restore the expression, not a value-equivalent. | +| "Lint passed, so the deleted import doesn't matter." | Lint passed because the import was deleted; on revert the original code needs it back. | +| "I'll ship the env-var override instead." | No env-var override is wired in the code. Don't invent one on the commit path — restore the original. | + +## Validation + +- `cd packages/dashboard && npm run test:unit` when changing pure helpers or reducers +- `cd packages/dashboard && npm run test:component` when changing React component behavior +- `cd packages/dashboard && npm run test:ui` when changing routing, auth, host-mode integration, or browser-only behavior +- `cd packages/dashboard && npm run typecheck` +- `cd packages/dashboard && npm run build` +- `cd frontend && npm run build` when the local self-hosting shell changes + +For shared contract changes, also validate `packages/shared-schemas/` and the affected backend surface. diff --git a/.codex/skills/insforge-dev/docs/SKILL.md b/.codex/skills/insforge-dev/docs/SKILL.md new file mode 100644 index 0000000..7e5e1eb --- /dev/null +++ b/.codex/skills/insforge-dev/docs/SKILL.md @@ -0,0 +1,47 @@ +--- +name: docs +description: Use this skill when contributing to InsForge's product documentation in this repository. This is for maintainers editing public docs in `docs/core-concepts`, agent docs in `.agents/docs`, SDK integration guides in `docs/sdks`, and OpenAPI specs in `openapi`. +--- + +# InsForge Dev Docs + +Use this skill for `docs/core-concepts/`, `.agents/docs/`, `docs/sdks/`, and `openapi/` in the InsForge repository. + +The documentation in this repo is primarily product documentation for InsForge users and agents integrating with InsForge. This skill is for InsForge engineers maintaining that public documentation surface. + +## Scope + +- `docs/core-concepts/**` +- `.agents/docs/**` +- `docs/sdks/**` +- `openapi/**` + +## Working Rules + +1. Put each document in the correct documentation surface. + - Human-friendly docs published on the public doc site belong in `docs/core-concepts/` and related public doc folders. + - For implementation-heavy public docs, prefer an `architecture.md` file inside the relevant `docs/core-concepts//` folder. + - Agent-only instructions belong in `.agents/docs/`. + - SDK integration guides for each framework belong in `docs/sdks/`. + - OpenAPI contract changes belong in the matching files under `openapi/`. + +2. Match the writing style to the audience. + - Public docs should be human-friendly and explain the implementation clearly. + - Keep public docs human-sounding: avoid AI-writing tells such as em dashes, rule-of-three lists, "not just X but Y" parallelism, inflated significance, vague attribution, and AI vocabulary (delve, leverage, underscore, seamless, robust). See the `doc-author` skill's `INSFORGE.md` overlay, "Sound human, not AI-generated". + - `architecture.md` pages in `docs/core-concepts/` should explain how the feature works in detail. + - Agent docs in `.agents/docs/` should be instruction-first and execution-oriented. + - Agent docs should avoid explanatory filler and focus on the exact steps an agent should follow to complete the work. + +3. Prevent documentation drift on every implementation change. + - Before changing implementation, check the current user-facing docs for that feature. + - After changing implementation, update the relevant Markdown docs and the relevant OpenAPI YAML files in the same pass. + - Do not treat OpenAPI and Markdown as separate optional follow-ups when the feature contract or behavior changed. + - If a change affects agent workflows, update the corresponding file in `.agents/docs/`. + - If a change affects public product understanding, update the corresponding file in `docs/core-concepts/`, including `architecture.md` when implementation details changed. + - If a change affects SDK integration guidance, update the corresponding framework guide in `docs/sdks/`. + +## Validation + +- Re-read every documented command, path, route, and payload for correctness. +- Cross-check OpenAPI YAML and Markdown docs against the implemented behavior before finishing. +- Mention anything you could not verify directly. diff --git a/.codex/skills/insforge-dev/e2e-testing/SKILL.md b/.codex/skills/insforge-dev/e2e-testing/SKILL.md new file mode 100644 index 0000000..3a7b651 --- /dev/null +++ b/.codex/skills/insforge-dev/e2e-testing/SKILL.md @@ -0,0 +1,142 @@ +--- +name: e2e-testing +description: Use this skill when an InsForge maintainer has finished an OSS repo change and is ready to open, update, or submit the InsForge PR. Runs the release-quality deterministic E2E gate by building a package.json-derived InsForge test image tag, deciding whether sibling agent-e2e fixture coverage must change, dispatching the Deterministic Fixture E2E workflow, waiting for results, and triaging failures before PR submission. +--- + +# InsForge E2E Testing Gate + +Use this skill after local implementation and normal InsForge pre-PR checks pass, and before opening, updating, or submitting the InsForge OSS PR. + +This is an additional release-quality gate. It does not replace local `typecheck`, `lint`, `test`, or `build` validation from the parent `insforge-dev` skill. + +## Repositories + +- InsForge OSS repo: current workspace. +- E2E repo: remote GitHub repository `InsForge/agent-e2e`. + +Use the remote `InsForge/agent-e2e` repository for workflow dispatch and read-only workflow checks. Do not rely on a developer-specific local checkout path. Create or use a local checkout only when the deterministic fixture tests must be edited. + +## Build The Test Tag + +1. Read the root InsForge `package.json` version. +2. Increment only the patch number. +3. Build a tag in this form: + +```text +v..- +``` + +Example: root version `2.2.3` and feature `storage returning rls` becomes `v2.2.4-storage-returning-rls`. + +Use the `package.json` version as the only source of truth for the base version. Ignore higher existing test tags when calculating the base version. + +Slug rules: + +- Prefer the issue key, PR topic, or branch topic. +- Lowercase all letters. +- Replace runs of non-alphanumeric characters with one hyphen. +- Trim leading and trailing hyphens. +- Keep it short enough to scan in GitHub Actions and image tags. + +## Build The InsForge Image + +Dispatch the InsForge `Build and Push Docker Image` workflow with the test tag: + +```bash +gh workflow run "Build and Push Docker Image" --repo InsForge/InsForge --ref -f test_tag= +``` + +Then wait for the matching run to complete: + +```bash +gh run list --repo InsForge/InsForge --workflow "Build and Push Docker Image" --limit 10 +gh run watch --repo InsForge/InsForge +``` + +Do not start the cross-repo E2E workflow until the image build succeeds. + +## Decide Whether `agent-e2e` Must Change + +Inspect the InsForge diff and compare it with deterministic fixture coverage in `InsForge/agent-e2e`. + +For read-only checks, prefer remote GitHub access such as `gh api`, `gh repo view`, or remote file reads. Use a local checkout only when editing fixture files or when remote inspection is not enough to understand coverage. + +Update `agent-e2e` when the InsForge change adds, removes, or changes behavior that the deterministic fixture should assert, including: + +- API contract or validation behavior. +- Auth, permissions, RLS, storage, realtime, functions, schedules, AI, SDK, or CLI-visible behavior. +- Any regression that local tests cover but the release gate should also protect across the deployed runtime. + +Do not update `agent-e2e` for internal refactors, docs-only changes, local test-only changes, or behavior already covered by the deterministic fixture with no assertion change needed. + +Ignore `Support Desk Agent E2E (Exploratory)`. It is not part of this gate. + +## If No E2E Test Update Is Needed + +Run the deterministic fixture workflow from `agent-e2e` main: + +```bash +gh workflow run "Deterministic Fixture E2E" --repo InsForge/agent-e2e --ref main -f insforge_tag= +``` + +Find and watch the run: + +```bash +gh run list --repo InsForge/agent-e2e --workflow "Deterministic Fixture E2E" --limit 10 +gh run watch --repo InsForge/agent-e2e +``` + +## If E2E Tests Need An Update + +Work in a local checkout of the remote `InsForge/agent-e2e` repo only for the fixture update. + +1. Use an existing clean checkout or clone `https://github.com/InsForge/agent-e2e.git` into an isolated workspace. +2. Fetch `origin` and start from `origin/main`. +3. Create a branch named `codex/`. +4. Update only the deterministic fixture validators, fixtures, app assertions, and docs needed for the InsForge behavior change. +5. Run the smallest local validation that gives confidence: + +```bash +npm run typecheck +npm run lint +npm run fixture:e2e:dry +``` + +Run broader validation when the changed fixture area supports it. + +6. Open an `agent-e2e` PR for the fixture update. +7. Dispatch the deterministic fixture workflow from the `agent-e2e` branch that contains the fixture update: + +```bash +gh workflow run "Deterministic Fixture E2E" --repo InsForge/agent-e2e --ref -f insforge_tag= +``` + +8. Watch the run before proceeding with the InsForge PR. + +## Interpret Results + +If the deterministic fixture workflow passes: + +- Link the run in the InsForge PR body or final PR notes. +- If an `agent-e2e` PR was required, link that PR too. +- Proceed with opening, updating, or submitting the InsForge OSS PR. + +If the deterministic fixture workflow fails: + +1. Inspect the failed job logs and uploaded artifact. +2. Identify whether the failure is caused by the InsForge implementation, the new or existing deterministic fixture, a transient infrastructure problem, or an unrelated existing failure. +3. Fix the correct branch: + - InsForge implementation bug: update the InsForge branch, rebuild the test image with the same tag or a new short retry tag, then rerun E2E. + - Fixture bug or missing assertion update: update the `agent-e2e` branch, rerun local validation, then rerun E2E from that branch. + - Transient infrastructure issue: rerun once after noting the evidence. +4. Do not submit the InsForge PR until the deterministic result is clear or the user explicitly accepts the risk. + +## Report Back + +When finished, report: + +- Test tag used. +- InsForge image workflow run result. +- Whether `agent-e2e` changed. +- Deterministic Fixture E2E run result. +- Links to the InsForge PR, `agent-e2e` PR if any, and workflow runs. diff --git a/.codex/skills/insforge-dev/shared-schemas/SKILL.md b/.codex/skills/insforge-dev/shared-schemas/SKILL.md new file mode 100644 index 0000000..84bc60d --- /dev/null +++ b/.codex/skills/insforge-dev/shared-schemas/SKILL.md @@ -0,0 +1,43 @@ +--- +name: shared-schemas +description: Use this skill when contributing to InsForge's shared schema package. This is for maintainers editing published Zod contracts, exported types, and shared API payload definitions consumed by InsForge packages in this repo and other InsForge tooling. +--- + +# InsForge Dev Shared Schemas + +Use this skill for `packages/shared-schemas/` work in the InsForge repository. + +## Scope + +- `packages/shared-schemas/src/**` +- any code in this repo that consumes the changed contracts +- downstream InsForge tooling that depends on the published `@insforge/shared-schemas` package, including consumers not present in this repository + +## Working Rules + +1. Treat `packages/shared-schemas/` as the source of truth for cross-package payloads. + - If a request, response, or domain shape is shared across InsForge surfaces, define it here. + - Do not duplicate the same contract in package-local files. + - Remember that this package is not only for the repo's backend and dashboard packages. It is also consumed by other InsForge tooling, including MCP and SDK code that may live outside this repository. + +2. Keep schemas organized by domain. + - Follow the existing `*.schema.ts` and `*-api.schema.ts` split when it fits the current package pattern. + - Keep `packages/shared-schemas/src/index.ts` aligned with the intended public API. + - Treat exported names and schema shapes as a public contract surface, not just an internal refactor target. + +3. Use schema changes as a synchronization trigger. + - Update backend validation and response usage. + - Update shared dashboard services, hooks, and UI assumptions in `packages/dashboard/`. + - Check for import sites across `packages/*`, `frontend/`, and `backend/` before finishing. + - Call out likely downstream impact on MCP, SDK, or other external InsForge consumers when a change alters exported names, schema semantics, or payload shape. + - Be conservative with breaking changes. If a breaking contract change is necessary, make it explicit in the handoff. + - Never use the TypeScript `any` type. Shared contracts should stay explicit and trustworthy across package boundaries. + +## Validation + +- `cd packages/shared-schemas && npm run build` +- `cd backend && npx tsc --noEmit` +- `cd packages/dashboard && npm run typecheck` + +Run package-specific tests as needed where behavior changed. +If external consumers such as MCP or SDK cannot be validated from this repo, say that clearly instead of implying they were covered. diff --git a/.codex/skills/insforge-dev/ui/SKILL.md b/.codex/skills/insforge-dev/ui/SKILL.md new file mode 100644 index 0000000..885fd5c --- /dev/null +++ b/.codex/skills/insforge-dev/ui/SKILL.md @@ -0,0 +1,42 @@ +--- +name: ui +description: Use this skill when contributing to InsForge's reusable UI package. This is for maintainers editing design-system primitives, exports, styles, and package-level component behavior in the InsForge monorepo. +--- + +# InsForge Dev UI + +Use this skill for `packages/ui/` work in the InsForge repository. + +## Scope + +- `packages/ui/src/components/**` +- `packages/ui/src/lib/**` +- `packages/ui/src/index.ts` +- `packages/ui/src/styles.css` + +## Working Rules + +1. Put only reusable primitives here. + - If the component is generic across dashboard features or other InsForge apps, it belongs in `packages/ui/`. + - If it is tightly coupled to one dashboard workflow but should ship to both OSS and cloud hosts, keep it in `packages/dashboard/`. + - If it is only for the self-hosting host app, keep it in `frontend/`. + +2. Preserve the package's implementation style. + - Use `class-variance-authority` for variants when appropriate. + - Use the shared `cn()` helper for class merging. + - Follow the existing Radix-wrapper and typed-export patterns. + +3. Keep the public surface in sync. + - Export new public components from `packages/ui/src/index.ts`. + - Avoid adding internal-only abstractions to the package surface unless they are meant to be consumed. + - Never use the TypeScript `any` type. Keep component props and exported helpers strictly typed. + +4. Validate downstream impact. + - The shared dashboard package consumes this package directly, so UI changes can break `packages/dashboard/` even if `packages/ui/` itself builds cleanly. + +## Validation + +- `cd packages/ui && npm run build` +- `cd packages/ui && npm run typecheck` + +Also validate `packages/dashboard/` when the changed component is used in the dashboard, and validate `frontend/` if the host app integration or CSS entrypoints changed. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bc871f3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,64 @@ +# Dependencies +node_modules/ +frontend/node_modules/ +auth/node_modules/ +shared-schemas/node_modules/ +backend/node_modules/ + +# Build outputs +dist/ +frontend/dist/ +*.tsbuildinfo + +# Data and logs +data/ +*.log +npm-debug.log* + +# Environment files +.env +.env.local +.env.*.local + +# Development files +.git/ +.gitignore +*.md +docs/** +!docs/sdks/** +!docs/snippets/** +!.agents/docs/** +tests/ +*.test.ts +*.test.tsx +coverage/ +.nyc_output/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store + +# Docker files (prevent recursion) +Dockerfile +docker-compose*.yml +.dockerignore + +# Development configs +vitest.config.ts +# tsconfig.json files are needed for build - DO NOT IGNORE +# frontend/tsconfig.json is needed for @ alias resolution - DO NOT IGNORE +# frontend/vite.config.ts is needed for build - DO NOT IGNORE +.eslintrc.json +frontend/.eslintrc.cjs + +# CI/CD +.github/ +.gitlab-ci.yml + +# Temporary files +tmp/ +temp/ +*.tmp \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..39fca97 --- /dev/null +++ b/.env.example @@ -0,0 +1,315 @@ +# ============================================================================= +# InsForge Environment Configuration +# ============================================================================= +# Copy this file to .env and fill in your values: +# cp .env.example .env +# +# Security Notes: +# - Never commit .env to version control +# - Use strong, unique secrets in production +# - Rotate secrets regularly +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Server Configuration +# ----------------------------------------------------------------------------- +PORT=7130 + +# Max body size for JSON payloads (default: 100mb) +# High default ensures "out-of-the-box" reliability for large metadata/storage requests. +# Users can decrease this for hardened security on low-resource environments. +MAX_JSON_BODY_SIZE=100mb + +# Max body size for URL-encoded payloads (default: 10mb) +MAX_URLENCODED_BODY_SIZE=10mb + +# ----------------------------------------------------------------------------- +# PostgreSQL Configuration +# ----------------------------------------------------------------------------- +# These are optional - defaults are shown below +# In production, use strong passwords and consider external database services +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=insforge + +# ----------------------------------------------------------------------------- +# Ports (Configurable) +# ----------------------------------------------------------------------------- + +# PostgreSQL +POSTGRES_PORT=5432 + +# PostgREST API +POSTGREST_PORT=5430 + +# Main application ports +APP_PORT=7130 +AUTH_PORT=7131 +UI_PORT=7132 + +# Deno serverless runtime +DENO_PORT=7133 + +# API Base URLs - Update if running on different host/port +API_BASE_URL=http://localhost:7130 +VITE_API_BASE_URL=http://localhost:7130 + +# ----------------------------------------------------------------------------- +# Authentication & Security +# ----------------------------------------------------------------------------- +# JWT_SECRET: Must be at least 32 characters. Use a secure random generator! +# Example: openssl rand -base64 32 +JWT_SECRET=your-secret-key-here-must-be-32-char-or-above + +# Root admin credentials - CHANGE THESE IN PRODUCTION! +ROOT_ADMIN_USERNAME=admin +ROOT_ADMIN_PASSWORD=change-this-password + +# Encryption key for secrets and database encryption +# IMPORTANT: Set this to a separate 32+ character secret from JWT_SECRET. +# If not set, JWT_SECRET is used as fallback — but rotating JWT_SECRET will +# permanently corrupt all stored secrets (API keys, OAuth tokens, etc.). +# Generate with: openssl rand -base64 32 +ENCRYPTION_KEY= + +# API Key for authenticated requests +# Must start with 'ik_' prefix and be at least 32 characters +# Optional - will be auto-generated if not provided +ACCESS_API_KEY=ik_your-api-key-here-32-chars-minimum + +# Anon Key for public client requests (safe to embed in frontends) +# Must start with 'anon_' prefix +# Optional - will be auto-generated if not provided +ACCESS_ANON_KEY=anon_your-anon-key-here + +# Cloud API Host (Optional) +# Only needed if using cloud features +CLOUD_API_HOST=https://api.insforge.dev + +# ----------------------------------------------------------------------------- +# Deployment Configuration (Optional) +# ----------------------------------------------------------------------------- +# Required for self-hosted site deployments and custom domains. +# Legacy deployments also require AWS_S3_BUCKET to be configured. +VERCEL_TOKEN= +VERCEL_TEAM_ID= +VERCEL_PROJECT_ID= + +# ----------------------------------------------------------------------------- +# AWS Config Bucket Configuration (Optional) +# ----------------------------------------------------------------------------- +# Used for loading remote configuration files from S3 +# If not provided, defaults will be used +AWS_CONFIG_BUCKET=insforge-config +AWS_CONFIG_REGION=us-east-2 + + + +# ----------------------------------------------------------------------------- +# Storage Configuration +# ----------------------------------------------------------------------------- +# Storage options: +# - Local filesystem: Leave AWS_S3_BUCKET empty (default) +# - AWS S3: Set AWS_S3_BUCKET, AWS_REGION, and AWS credentials +# - S3-compatible (Wasabi, MinIO, etc.): Set S3_* variables +# +# Local storage directory (default: ./insforge-storage relative to cwd) +# In Docker this is set to /insforge-storage automatically via the Dockerfile. +# PaaS deployments (Zeabur, Render, etc.) should set this to match the +# persistent volume mount path. +# STORAGE_DIR=./insforge-storage +# +# AWS credentials (used by CloudWatch logging and as fallback for storage) +AWS_ACCESS_KEY_ID= +AWS_REGION= +AWS_S3_BUCKET= +AWS_SECRET_ACCESS_KEY= + +# Max upload file size in bytes (default: 52428800 = 50MB) +# Examples: 10485760 = 10MB, 104857600 = 100MB +MAX_FILE_SIZE= + +# S3-compatible storage credentials (for Wasabi, MinIO, R2, etc.) +# These take precedence over AWS_* variables if set +S3_ACCESS_KEY_ID= +S3_ENDPOINT_URL= +S3_SECRET_ACCESS_KEY= +# Path-style addressing (default true, required by MinIO). Set to false for +# providers that require virtual-hosted-style (Tencent COS, Aliyun OSS). +S3_FORCE_PATH_STYLE=true + +# CloudFront signed URLs (Optional, AWS S3 only) +# When AWS_CLOUDFRONT_URL is set, storage downloads are served through +# CloudFront with signed URLs instead of S3 presigned URLs. Ignored when +# S3_ENDPOINT_URL is set (CloudFront does not front S3-compatible providers). +# Setup: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html +# +# AWS_CLOUDFRONT_URL: CloudFront distribution domain (e.g. https://d1234abcd.cloudfront.net) +# AWS_CLOUDFRONT_KEY_PAIR_ID: Trusted key group key-pair ID (e.g. K2JCJMDEHXQW5F) +# AWS_CLOUDFRONT_PRIVATE_KEY: PEM private key. Use \n for newlines when kept on one line. +# If URL is set but key pair or private key is missing, downloads fall back to S3 presigned URLs. +AWS_CLOUDFRONT_URL= +AWS_CLOUDFRONT_KEY_PAIR_ID= +AWS_CLOUDFRONT_PRIVATE_KEY= + +# ----------------------------------------------------------------------------- +# Logging Configuration +# ----------------------------------------------------------------------------- +# CloudWatch Logging: Set AWS_REGION + AWS credentials above +# File-based Logging: Configure LOGS_DIR below (used when AWS credentials not provided) +# Directory where log files will be stored (defaults to ./logs) +LOGS_DIR= + +# ----------------------------------------------------------------------------- +# Anonymous Telemetry +# ----------------------------------------------------------------------------- +# InsForge sends anonymous self-host usage events so maintainers can understand +# active deployments, version adoption, and deployment success. It never sends +# secrets, environment variables, logs, domains, file paths, project names, or +# database contents. +# +# Disable all telemetry: +# INSFORGE_TELEMETRY_DISABLED=1 + + +# ----------------------------------------------------------------------------- +# AI/LLM Configuration +# ----------------------------------------------------------------------------- +# OpenRouter API - Get your API key from https://openrouter.ai/keys +# Used for AI-powered features and model gateway +OPENROUTER_API_KEY= + +# Maximum output tokens per chat completion request (default: 16384). +# Increase this for models with larger output windows (e.g. 32768 for 32K-output models). +# Must be a positive integer. Invalid values are silently ignored and the default is used. +MAX_COMPLETION_TOKENS=16384 + +# ----------------------------------------------------------------------------- +# Stripe Payments Configuration (Optional) +# ----------------------------------------------------------------------------- +# Developer-owned Stripe secret keys. Test key is used for implementation and +# validation. Live key is used only for explicit go-live flows. +STRIPE_LIVE_SECRET_KEY= +STRIPE_TEST_SECRET_KEY= + +# ----------------------------------------------------------------------------- +# Razorpay Payments Configuration (Optional) +# ----------------------------------------------------------------------------- +# Developer-owned Razorpay keys. Test keys are used for implementation and +# validation. Live keys are used only for explicit go-live flows. +RAZORPAY_LIVE_KEY_ID= +RAZORPAY_LIVE_KEY_SECRET= +RAZORPAY_TEST_KEY_ID= +RAZORPAY_TEST_KEY_SECRET= + +# ----------------------------------------------------------------------------- +# Analytics Configuration +# ----------------------------------------------------------------------------- +# PostHog - Only needed for local development +# Get your key from https://posthog.com/settings/project +VITE_PUBLIC_POSTHOG_KEY= + +# ----------------------------------------------------------------------------- +# OAuth Configuration (Optional) +# ----------------------------------------------------------------------------- +# Enable social login by configuring one or more providers below. +# Each provider requires registering an application in their developer console. +# +# Google OAuth +# Console: https://console.cloud.google.com/ +# Redirect URI: http://localhost:7130/auth/google/callback +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +# GitHub OAuth +# Console: https://github.com/settings/developers +# Redirect URI: http://localhost:7130/auth/github/callback +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +# Microsoft OAuth (Azure AD) +# Console: https://portal.azure.com/ +# Redirect URI: http://localhost:7130/auth/microsoft/callback +MICROSOFT_CLIENT_ID= +MICROSOFT_CLIENT_SECRET= + +# Discord OAuth +# Console: https://discord.com/developers/applications +# Redirect URI: http://localhost:7130/auth/discord/callback +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= + +# LinkedIn OAuth +# Console: https://www.linkedin.com/developers/apps +# Redirect URI: http://localhost:7130/auth/linkedin/callback +LINKEDIN_CLIENT_ID= +LINKEDIN_CLIENT_SECRET= + +# X (Twitter) OAuth +# Console: https://developer.twitter.com/en/portal/dashboard +# Redirect URI: http://localhost:7130/auth/x/callback +X_CLIENT_ID= +X_CLIENT_SECRET= + +# Apple OAuth (Sign in with Apple) +# Console: https://developer.apple.com/account/resources/identifiers/list +# APPLE_CLIENT_ID: Your Services ID (e.g., com.yourapp.service) +# APPLE_CLIENT_SECRET: JSON string with teamId, keyId, and privateKey +# Format: {"teamId":"XXX","keyId":"YYY","privateKey":"-----BEGIN PRIVATE KEY-----\n..."} +# Redirect URI: http://localhost:7130/auth/apple/callback +APPLE_CLIENT_ID= +APPLE_CLIENT_SECRET= + +# ----------------------------------------------------------------------------- +# Multi-tenant Cloud Configuration +# ----------------------------------------------------------------------------- +# These are only used for the cloud-hosted solution +# Leave empty for self-hosted deployments +DEPLOYMENT_ID= +PROJECT_ID= +APP_KEY= + +# ----------------------------------------------------------------------------- +# Serverless Functions Configuration +# ----------------------------------------------------------------------------- +# Deno Deploy - For hosting serverless edge functions +# Get your token from: https://dash.deno.com/account#access-tokens +# Get your org ID from: https://dash.deno.com/ +DENO_DEPLOY_TOKEN= +DENO_DEPLOY_ORG_ID= + +# ─── Compute Services (Fly.io) ────────────────────────────────────────────── +# Deploy Docker containers with persistent URLs. +# Full setup + architecture: https://docs.insforge.dev/core-concepts/compute/architecture +# +# Setup — both FLY_API_TOKEN and FLY_ORG are required. FLY_ORG must be YOUR +# Fly org slug; leaving it blank or as "insforge" will fail with an opaque +# Fly auth error because that's our internal org. Compute auto-enables when +# both are set. +# 1. Create a Fly.io account: https://fly.io +# 2. Find your org slug: fly orgs list +# 3. Create an API token scoped to that org: fly tokens create org -o +# 4. Set FLY_API_TOKEN= and FLY_ORG= +# +# Optional: Set COMPUTE_DOMAIN only if you own a wildcard domain pointed at +# Fly (e.g. *.compute.yourdomain.com CNAME'd to fly.dev). Leave unset to use +# Fly's own .fly.dev hostname, which works out of the box. + +FLY_API_TOKEN= +FLY_ORG= +COMPUTE_DOMAIN= + +# Cloud-mode compute (provisioned automatically when PROJECT_ID + CLOUD_API_HOST +# are set by insforge-cloud). Self-host users with FLY_API_TOKEN always take +# priority and do not need to set PROJECT_ID. + +# ============================================================================= +# End of Configuration +# ============================================================================= +# After configuring this file: +# 1. Run: docker compose up -d +# 2. Check logs: docker compose logs -f +# 3. Access the dashboard at: http://localhost:7131 +# +# For more information, visit: https://insforge.dev/docs +# ============================================================================= diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..9a7500c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [InsForge] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..1dac8f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,36 @@ +name: 🐛 Bug Report +description: Report a bug to help us improve +title: "[Bug]: " +labels: ["bug", "needs-triage"] +assignees: [] +body: + - type: markdown + attributes: + value: | + Thanks for reporting a bug! Fill out what you can - we'll figure out the rest together. + + - type: textarea + id: what-happened + attributes: + label: What's the bug? + description: Describe what went wrong + placeholder: Tell us what happened... + validations: + required: true + + - type: textarea + id: steps-reproduce + attributes: + label: How to reproduce + description: Steps to trigger the bug (if you know them) + placeholder: | + 1. Do this... + 2. Then this... + 3. Bug happens + + - type: textarea + id: environment + attributes: + label: Environment (optional) + description: What are you running? (OS, browser, version, etc.) + placeholder: e.g., macOS, Chrome, Docker, Node 20... diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..651ef99 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: 📚 Documentation + url: https://docs.insforge.dev + about: Check our documentation for common questions and setup guides + - name: 💬 Discussions + url: https://github.com/InsForge/InsForge/discussions + about: Ask questions and discuss ideas with the community + - name: 🆘 Discord Support + url: https://discord.com/invite/MPxwj5xVvW + about: Get help with installation, configuration, and usage diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..0e34454 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,26 @@ +name: ✨ Feature Request +description: Suggest an idea or enhancement +title: "[Feature]: " +labels: ["enhancement", "needs-triage"] +assignees: [] +body: + - type: markdown + attributes: + value: | + Have an idea? Share it! We'd love to hear what you're thinking. + + - type: textarea + id: feature-description + attributes: + label: What feature do you want? + description: Describe your idea + placeholder: I'd like to be able to... + validations: + required: true + + - type: textarea + id: why + attributes: + label: Why do you need this? + description: What problem does it solve? + placeholder: This would help me... diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..f1ffc50 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +## Summary + + + +## How did you test this change? + + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..68d4eed --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,147 @@ +# Project Overview + +InsForge is an open-source Backend-as-a-Service (BaaS) platform designed specifically for AI agents. It provides a comprehensive solution for managing backend services including authentication, database operations, storage, and serverless functions, all accessible through REST APIs with PostgreSQL as the primary database. + +## Architecture + +This is a monorepo containing: +- **Backend**: Node.js with Express.js, providing RESTful APIs +- **Frontend**: React with Vite, offering an admin dashboard +- **Functions**: Serverless function runtime using Deno +- **Shared-schemas**: Common TypeScript schemas shared across modules + +## Folder Structure + +- `/backend`: Node.js backend server + - `/src/api`: API routes and middleware + - `/src/controllers`: Business logic controllers + - `/src/core`: Core services (auth, database, storage, metadata) + - `/src/types`: TypeScript type definitions + - `/src/utils`: Utility functions and helpers +- `/frontend`: React dashboard application + - `/src/components`: Reusable UI components + - `/src/features`: Feature-specific modules (auth, database, storage, logs) + - `/src/lib`: Shared libraries, hooks, and utilities +- `/functions`: Serverless function runtime +- `/shared-schemas`: Shared TypeScript schemas +- `/docker-init`: Docker initialization scripts +- `/openapi`: API documentation in OpenAPI format +- `/tests`: Comprehensive test suites + +## Libraries and Frameworks + +### Backend +- Express.js for REST API framework +- PostgreSQL with pg driver for database +- Better-auth for authentication +- AWS SDK for S3-compatible storage +- Zod for schema validation +- JWT for token management +- TypeScript for type safety + +### Frontend +- React 19 with functional components +- Vite for build tooling +- TanStack Query for data fetching +- React Hook Form with Zod validation +- Tailwind CSS for styling +- Radix UI for accessible component primitives +- React Router for navigation +- TypeScript for type safety + +## Coding Standards + +### General +- Use TypeScript for all code files +- Prefer descriptive, unabbreviated variable and function names +- Follow consistent file naming: kebab-case for files, PascalCase for components +- Maintain clear folder structure with feature-based organization +- Use ES modules (import/export syntax) +- Implement proper error handling with try-catch blocks +- Add appropriate logging for debugging + +### Backend Specific +- Use async/await for asynchronous operations +- Implement proper middleware for authentication and error handling +- Follow RESTful conventions for API endpoints +- Use Zod schemas for request/response validation +- Return consistent API responses using utility functions +- Implement proper database transaction handling + +### Frontend Specific +- Use functional components with hooks exclusively +- Implement proper TypeScript interfaces for all props +- Use React Hook Form for form handling +- Follow component composition patterns +- Implement proper loading and error states +- Use TanStack Query for server state management +- Keep components focused and single-purpose + +#### Naming Conventions +- **Variables, parameters, properties, and functions**: Use camelCase + - Examples: `userName`, `getUserData()`, `isLoading` +- **React components and TypeScript types/interfaces**: Use PascalCase + - Examples: `UserProfileDialog`, `interface User`, `type TableSchema` +- **Constants and Enum members**: Use UPPER_CASE + - Examples: `const API_BASE_URL`, `enum Status { ACTIVE, INACTIVE }` +- **File naming**: + - Components: `PascalCase.tsx` (e.g., `DataGrid.tsx`) + - Hooks: `use` + `PascalCase.ts` (e.g., `useAuth.ts`) + - Services: `camelCase.service.ts` (e.g., `auth.service.ts`) + - Utils: `kebab-case.ts` or `camelCase.ts` (e.g., `database-utils.ts`) +- **No snake_case**: Avoid snake_case except in SQL queries and external APIs + +### Code Style +- Use single quotes for strings +- Include semicolons at the end of statements +- Use arrow functions for callbacks and inline functions +- Prefer const over let, avoid var +- Use template literals for string interpolation +- Maintain consistent indentation (2 spaces) +- Format code with Prettier configuration + +## UI Guidelines + +- Follow a clean, modern design with consistent spacing +- Use Radix UI primitives for accessibility +- Maintain consistent color scheme using CSS variables +- Use appropriate loading skeletons for data fetching +- Display clear error messages with actionable feedback +- Implement proper form validation with inline errors +- Use consistent icon set from lucide-react + +## Testing Standards + +- Implement integration tests for API endpoints +- Maintain test coverage above 70% +- Mock external dependencies appropriately + +## Security Considerations + +- Never commit secrets or API keys +- Use environment variables for configuration +- Implement proper authentication and authorization +- Validate all user inputs +- Sanitize data before database operations +- Use prepared statements for SQL queries +- Implement rate limiting for API endpoints +- Follow OWASP security best practices + +## Performance Guidelines + +- Implement proper database indexing +- Use pagination for large data sets +- Optimize React component re-renders +- Implement proper caching strategies +- Use lazy loading for code splitting +- Optimize bundle size with proper imports +- Monitor and log performance metrics + +## Documentation + +- Document all API endpoints with clear descriptions +- Include JSDoc comments for complex functions +- Maintain up-to-date README files +- Document environment variables and configuration +- Provide clear setup instructions +- Include examples for common use cases \ No newline at end of file diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 0000000..9d93e89 --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,202 @@ +name: Build and Push Docker Image + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + test_tag: + description: 'Test tag name (e.g., v0.1.1-test)' + required: false + default: 'v0.1.1-test' + +env: + GHCR_REGISTRY: ghcr.io + IMAGE_NAME: insforge-oss + +jobs: + build-amd64: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT }} + + - name: Extract metadata for GHCR + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.GHCR_REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + flavor: | + suffix=-amd64 + latest=false + tags: | + type=ref,event=tag + type=raw,value=${{ github.event.inputs.test_tag }},enable=${{ github.event_name == 'workflow_dispatch' }} + + - name: Build and push (amd64) + uses: docker/build-push-action@v7 + with: + context: . + target: runner + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=amd64 + cache-to: type=gha,mode=max,scope=amd64 + build-args: | + VERSION=${{ github.ref_name }} + BUILD_DATE=${{ github.event.head_commit.timestamp }} + COMMIT_SHA=${{ github.sha }} + VITE_PUBLIC_POSTHOG_KEY=${{ secrets.POSTHOG_KEY }} + + build-arm64: + runs-on: ubuntu-24.04-arm + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT }} + + - name: Extract metadata for GHCR + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.GHCR_REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + flavor: | + suffix=-arm64 + latest=false + tags: | + type=ref,event=tag + type=raw,value=${{ github.event.inputs.test_tag }},enable=${{ github.event_name == 'workflow_dispatch' }} + + - name: Build and push (arm64) + uses: docker/build-push-action@v7 + with: + context: . + target: runner + platforms: linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=arm64 + cache-to: type=gha,mode=max,scope=arm64 + build-args: | + VERSION=${{ github.ref_name }} + BUILD_DATE=${{ github.event.head_commit.timestamp }} + COMMIT_SHA=${{ github.sha }} + VITE_PUBLIC_POSTHOG_KEY=${{ secrets.POSTHOG_KEY }} + + merge-manifests: + needs: [build-amd64, build-arm64] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT }} + + - name: Extract metadata for GHCR + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.GHCR_REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + flavor: | + latest=false + tags: | + type=ref,event=tag + type=raw,value=${{ github.event.inputs.test_tag }},enable=${{ github.event_name == 'workflow_dispatch' }} + type=raw,value=latest,enable=${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') }} + + - name: Create and push multi-arch manifest + env: + GHCR_IMAGE_RAW: ${{ env.GHCR_REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + SOURCE_VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.test_tag || github.ref_name }} + run: | + GHCR_IMAGE=$(echo "${GHCR_IMAGE_RAW}" | tr '[:upper:]' '[:lower:]') + for TAG in $(echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n'); do + docker buildx imagetools create \ + --tag "${TAG}" \ + "${GHCR_IMAGE}:${SOURCE_VERSION}-amd64" \ + "${GHCR_IMAGE}:${SOURCE_VERSION}-arm64" + done + + push-to-ecr: + needs: merge-manifests + runs-on: ubuntu-latest + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') + permissions: + id-token: write + contents: read + + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ secrets.AWS_ECR_ROLE_ARN }} + aws-region: ${{ secrets.AWS_ECR_REGION }} + + - name: Login to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@v2 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT }} + + - name: Install crane + uses: imjasonh/setup-crane@6da1ae018866400525525ce74ff892880c099987 # v0.5 + + - name: Copy multi-arch image to ECR + env: + ECR_REGISTRY: ${{ steps.ecr-login.outputs.registry }} + VERSION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.test_tag || github.ref_name }} + GHCR_IMAGE_RAW: ${{ env.GHCR_REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} + ECR_IMAGE: ${{ steps.ecr-login.outputs.registry }}/${{ env.IMAGE_NAME }} + TAG_LATEST: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') }} + run: | + # Convert GHCR image name to lowercase (Docker requires lowercase) + GHCR_IMAGE=$(echo "${GHCR_IMAGE_RAW}" | tr '[:upper:]' '[:lower:]') + # Copy multi-arch image from GHCR to ECR (preserves all platforms) + crane copy ${GHCR_IMAGE}:${VERSION} ${ECR_IMAGE}:${VERSION} + if [[ "${TAG_LATEST}" == "true" ]]; then + crane copy ${GHCR_IMAGE}:${VERSION} ${ECR_IMAGE}:latest + fi diff --git a/.github/workflows/check-migrations.yml b/.github/workflows/check-migrations.yml new file mode 100644 index 0000000..ebcef81 --- /dev/null +++ b/.github/workflows/check-migrations.yml @@ -0,0 +1,29 @@ +name: Check Migrations + +on: + push: + branches: [ main ] + pull_request: + branches: [ '*' ] + # Required by the merge queue: the queue builds a temporary `merge_group` ref + # (PR rebased onto the latest main) and waits for this check to report on it. + # Without this trigger the check never runs for queued PRs and the queue stalls. + merge_group: + +jobs: + check-duplicate-numbers: + name: Check for duplicate migration numbers + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: Check for duplicate migration numbers + run: node scripts/check-migration-duplicates.js + working-directory: backend diff --git a/.github/workflows/ci-premerge-check.yml b/.github/workflows/ci-premerge-check.yml new file mode 100644 index 0000000..94751e8 --- /dev/null +++ b/.github/workflows/ci-premerge-check.yml @@ -0,0 +1,24 @@ +name: CI Pre-merge Check + +on: + pull_request: + branches: [ main, master, develop ] + push: + branches: [ main, master, develop ] + workflow_dispatch: # Allows manual triggering for testing + +jobs: + docker-build: + name: CI Pre-merge Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Build Docker image + uses: docker/build-push-action@v7 + with: + context: . + push: false + tags: insforge:test \ No newline at end of file diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..c7fffc6 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,65 @@ +name: E2E Tests + +on: + pull_request: + branches: [main, master, develop] + workflow_dispatch: + +jobs: + e2e: + name: E2E Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Create .env file + run: | + cat > .env << EOF + POSTGRES_USER=postgres + POSTGRES_PASSWORD=postgres + POSTGRES_DB=insforge + DATABASE_URL=postgresql://postgres:postgres@postgres:5432/insforge + JWT_SECRET=test-secret-key-for-ci-cd-pipeline + ROOT_ADMIN_USERNAME=admin + ROOT_ADMIN_PASSWORD=test-admin-password-for-ci + TEST_API_BASE=http://localhost:7130/api + INSFORGE_DISABLE_WRITE_RATE_LIMIT=1 + EOF + + - name: Start Docker Compose services + run: docker compose up -d + + - name: Wait for PostgreSQL to be ready + run: | + echo "Waiting for PostgreSQL..." + timeout 60 bash -c 'until docker compose exec -T postgres pg_isready -U postgres; do sleep 2; done' + + - name: Wait for InsForge backend to be ready + run: | + echo "Waiting for InsForge backend..." + timeout 180 bash -c 'until curl -f http://localhost:7130/api/health 2>/dev/null; do echo "Waiting..."; sleep 5; done' + echo "Backend is ready!" + + - name: Install test dependencies in container + run: docker compose exec -T insforge apk add --no-cache bash curl jq postgresql-client + + - name: Run E2E tests + run: docker compose exec -T insforge npm run test:e2e + + - name: Show container logs on failure + if: failure() + run: | + echo "=== PostgreSQL logs ===" + docker compose logs postgres + echo "" + echo "=== InsForge backend logs ===" + docker compose logs insforge + echo "" + echo "=== PostgREST logs ===" + docker compose logs postgrest + + - name: Cleanup + if: always() + run: docker compose down -v diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml new file mode 100644 index 0000000..f859d10 --- /dev/null +++ b/.github/workflows/frontend-tests.yml @@ -0,0 +1,128 @@ +name: Frontend Tests + +on: + pull_request: + branches: [main, master, develop] + workflow_dispatch: + +permissions: + contents: read + +jobs: + dashboard-unit-tests: + name: Dashboard Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run dashboard unit tests + run: npm --workspace @insforge/dashboard run test:unit + + dashboard-component-tests: + name: Dashboard Component Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run dashboard component tests + run: npm --workspace @insforge/dashboard run test:component + + ui-unit-tests: + name: UI Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run UI unit tests + run: npm --workspace @insforge/ui run test:unit + + ui-component-tests: + name: UI Component Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run UI component tests + run: npm --workspace @insforge/ui run test:component + + dashboard-e2e-tests: + name: Dashboard E2E Tests + runs-on: ubuntu-latest + needs: + - dashboard-unit-tests + - dashboard-component-tests + - ui-unit-tests + - ui-component-tests + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Install Playwright Chromium + run: npx playwright install --with-deps chromium + + - name: Run dashboard E2E tests + run: npm --workspace @insforge/dashboard run test:ui + + - name: Upload Playwright artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: dashboard-playwright-results + path: | + packages/dashboard/playwright-report/ + packages/dashboard/test-results/ + retention-days: 7 diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..e59e4d3 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,69 @@ +name: Integration Tests + +on: + pull_request: + branches: [main, master, develop] + workflow_dispatch: + +permissions: + contents: read + +jobs: + integration-tests: + name: Integration Tests + runs-on: ubuntu-latest + + services: + postgres: + image: ghcr.io/insforge/postgres:v15.13.2 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: insforge + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + + env: + PGHOST: 127.0.0.1 + PGPORT: 5432 + PGUSER: postgres + PGPASSWORD: postgres + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install Postgres client + run: | + sudo apt-get update + sudo apt-get install -y postgresql-client + + - name: Wait for Postgres + run: | + for i in {1..60}; do + if pg_isready -h 127.0.0.1 -p 5432 -U postgres; then + exit 0 + fi + sleep 2 + done + echo "postgres not ready in time" + exit 1 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run integration tests + run: cd backend && npm run test:integration diff --git a/.github/workflows/lint-and-format.yml b/.github/workflows/lint-and-format.yml new file mode 100644 index 0000000..9fad956 --- /dev/null +++ b/.github/workflows/lint-and-format.yml @@ -0,0 +1,49 @@ +name: Lint and Format Check + +on: + push: + branches: [ main ] + pull_request: + branches: [ '*' ] + +jobs: + lint-and-format: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Check insforge-dev skill copies are in sync + run: scripts/sync-skills.sh --check + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v45 + with: + files: | + **/*.{ts,tsx,js,jsx,mjs,cjs} + + - name: Run ESLint on changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: npx eslint ${{ steps.changed-files.outputs.all_changed_files }} + + - name: Get changed files for Prettier + id: changed-files-prettier + uses: tj-actions/changed-files@v45 + + - name: Check Prettier formatting on changed files + if: steps.changed-files-prettier.outputs.any_changed == 'true' + run: npx prettier --check --ignore-unknown ${{ steps.changed-files-prettier.outputs.all_changed_files }} + + - name: Run TypeScript type checking + run: npm run typecheck \ No newline at end of file diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..94b84d7 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,27 @@ +name: Unit Tests + +on: + pull_request: + branches: [main, master, develop] + workflow_dispatch: + +jobs: + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run backend unit tests + run: npm run test:backend diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..854798e --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +node_modules/ +dist/ +data/ +coverage/ +playwright-report/ +test-results/ +.env +.worktrees/ +.DS_Store +*.log +*.db +*.db-shm +*.db-wal +CLAUDE.md +.claude/* +!.claude/skills/ +.claude/skills/* +!.claude/skills/README.md +!.claude/skills/insforge-dev/ +!.claude/skills/insforge-dev/** +!.claude/skills/doc-author/ +!.claude/skills/doc-author/** +.agents/* +!.agents/skills/ +.agents/skills/* +!.agents/skills/insforge-dev/ +!.agents/skills/insforge-dev/** +!.agents/docs/ +!.agents/docs/** +deno.lock +better-auth/ +.mcp.json +backend/logs/* + +# Turborepo +.turbo +.gstack/ diff --git a/.internal/docs/audits/_audit-2026-04-18.md b/.internal/docs/audits/_audit-2026-04-18.md new file mode 100644 index 0000000..ef7de99 --- /dev/null +++ b/.internal/docs/audits/_audit-2026-04-18.md @@ -0,0 +1,44 @@ +# Docs audit — 2026-04-18 + +Audit of every `.mdx` file under `docs/` (excluding `docs/deprecated/` — +those are intentional preservation) against the rules in +`.claude/skills/doc-author/SKILL.md` (upstream Mintlify, MIT) and the +InsForge overlay at `.claude/skills/doc-author/INSFORGE.md`. + +Methodology: targeted `grep` passes for each rule category plus manual +cross-check of `docs/docs.json` navigation entries against the filesystem. +Fixed in this PR: the top five high-severity rows. Everything else is +filed as follow-up tickets per #1117. + +**Key:** severity is the operational impact if the issue ships. +`high` = broken nav or broken link a reader will hit; `med` = visible +style inconsistency with the overlay or an orphaned page; `low` = minor +copy nit. + +## Findings + +| # | Page | Issue | Severity | Recommended fix | +|---|------|-------|----------|-----------------| +| 1 | `docs/docs.json` (lines 125-127) | Nav points at `sdks/typescript/ui-components/react`, `sdks/typescript/ui-components/nextjs`, `sdks/typescript/ui-components/customization` — none of these `.mdx` files exist on disk, so the entire "UI Components" group breaks Mintlify nav rendering. | high | Remove the three dead entries (and the surrounding `{ "group": "UI Components", ... }` wrapper). Re-add when pages land via a future ticket. | +| 2 | `docs/sdks/typescript/overview.mdx` (lines 38-39) | Same dead targets as #1: body prose links to `/sdks/typescript/ui-components/react` and `/sdks/typescript/ui-components/nextjs`. Readers clicking from the SDK landing page hit 404. | high | Delete the two bullet links (or point them at the npm package pages). Skill §"Internal links": root-relative paths, and only to real pages. | +| 3 | `docs/changelog.mdx` (lines 41, 64, 80, 91, 150, 188, 204) | Seven broken internal links using the obsolete `/core-concepts/{ai,email,authentication,database}/sdk` and `/core-concepts/authentication/ui-components/react` routes. The SDK reference pages live at `/sdks/typescript/*` now. | high | Repoint each link to the current `/sdks/typescript/` page. For the `ui-components/react` link (line 150), drop it — no replacement page exists yet. | +| 4 | `docs/sdks/flutter/*.mdx` (all 6 pages: `overview`, `auth`, `database`, `storage`, `realtime`, `ai`) | Flutter SDK has six reference pages on disk but zero entries in `docs/docs.json` navigation → readers cannot discover them from the site nav. Skill §"What requires escalation": changes that affect navigation structure. Orphaned pages are worse than missing pages. | high | Add a `"Flutter"` group under the `"SDK & Examples"` tab in `docs/docs.json`, mirroring the Swift/Kotlin groups. Six pages in the order `overview → database → auth → storage → realtime → ai` (Flutter has no `functions.mdx`). | +| 5 | `docs/sdks/typescript/email.mdx` (line 6) | Experimental-feature callout uses `` — overlay INSFORGE.md §"Experimental features get `` at top" requires `` for experimental/private-preview status. `` is for neutral context; readers don't register it as a stability signal. Consistent with how `docs/core-concepts/email/architecture.mdx:6-8` and `docs/core-concepts/deployments/architecture.mdx:6-8` already treat experimental features. | high | Change the `` tag to ``, keeping body text identical. | +| 6 | `docs/partnership.mdx` (lines 76-91) | 16 lines begin with `- ✅` emoji-prefixed bullets. Skill §"What to avoid / Never use: Emoji in documentation." | med | Strip the ✅ emoji; plain bullets read as checklist items already. One-commit, zero structural change. | +| 7 | `docs/core-concepts/realtime/sdk.mdx` and `docs/core-concepts/storage/sdk.mdx` | Both pages exist on disk but are NOT in `docs/docs.json` navigation — they're orphaned. They also duplicate content that lives at `/sdks/typescript/realtime` and `/sdks/typescript/storage` (the canonical SDK-tab pages). Deprecated-API smell: the SDK-tab pages are canonical, the core-concepts pages are the older location. | med | Follow-up ticket: either delete both (canonical SDK docs live under `/sdks/typescript/*` per docs.json:110-141) or add them to nav. Audit recommends deletion; `changelog.mdx:106` still links to `/core-concepts/realtime/sdk` so a link rewrite is part of the same ticket. | +| 8 | `docs/sdks/rest/storage.mdx` (sections at lines 154, 196, 289) | Three H2 sections titled "Upload Object (Deprecated)", "Upload with Auto-Generated Key (Deprecated)", "Download Object (Deprecated)". The parenthetical label works, but a `` callout inside each section would make the status machine-readable (skill §"Callouts / `` — something potentially destructive"). | med | Follow-up ticket: prepend a `These endpoints are deprecated. Use [...] instead.` callout at the top of each of the three sections; do not remove the content (REST reference is intentionally complete). | +| 9 | `docs/sdks/typescript/database.mdx` (line 379) | Example imports `@insforge/react` with an inline comment `// or '@insforge/react-router'`. No `/sdks/typescript/ui-components/*` page exists (see #1) to explain when you'd pick which — the reader has nowhere to go. | med | Follow-up ticket: after UI-components pages are restored (#1), add a cross-link here. Standalone fix not useful. | +| 10 | `docs/quickstart.mdx` (lines 13-15) | Uses markdown numbered list `1. 2. 3.` for a three-action sequence inside "Step 1" (which is itself an H2). Skill §"Steps for sequential procedures: use the `` component". Impact is small — the page is already readable. | low | Deferred. Minimal-diff conversion is ~20 lines for a 5-line list; not worth this PR. Folded into catch-all follow-up. | +| 11 | `docs/sdks/typescript/email.mdx` (line 2) | Title is `"Emails SDK Reference"` (plural) but description says `"Send custom transactional emails with the InsForge SDK"` and the module name elsewhere is "Email" (singular; matches `docs.json:218` `"group": "Email"` and the architecture page `docs/core-concepts/email/architecture.mdx`). | low | Rename title to `"Email SDK Reference"` for consistency. Folded into catch-all follow-up. | +| 12 | Many pages | Most H2 headings use Title Case (`## Quick Start`, `## Framework-Specific Packages`, `## Learn More`). Skill §"Voice and structure / Sentence case for headings". This is a systemic repo convention, not a one-page bug. | low | No action. Folded into catch-all follow-up to consider as a separate discussion. Overlay does not override on this, but repo-wide changes are out of scope per ticket #1117. | + +## Summary + +- **High-severity rows:** 5 (fixed in this PR). +- **Medium-severity rows:** 4 (one follow-up ticket each). +- **Low-severity rows:** 3 (folded into a single catch-all ticket). + +## Fix commits in this PR + +One commit per fixed row, each citing the skill/overlay section it enforces. +See the `insforge/commander/1117` branch. diff --git a/.internal/docs/plans/2026-03-16-custom-smtp.md b/.internal/docs/plans/2026-03-16-custom-smtp.md new file mode 100644 index 0000000..00d6247 --- /dev/null +++ b/.internal/docs/plans/2026-03-16-custom-smtp.md @@ -0,0 +1,2037 @@ +# Custom SMTP & Email Templates Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Allow project admins to configure custom SMTP servers and edit email templates via the Auth Settings UI, replacing the default InsForge cloud email provider. + +**Architecture:** New `SmtpEmailProvider` implementing the existing `EmailProvider` interface, with config and templates stored in PostgreSQL. `EmailService` checks DB config per-call to route through SMTP or cloud. Frontend adds an "Email" tab to the Auth Settings dialog with two cards: SMTP settings and template editor. + +**Tech Stack:** nodemailer, PostgreSQL, React + react-hook-form, Zod, existing InsForge patterns (singleton services, EncryptionManager, verifyAdmin middleware) + +**Spec:** `docs/superpowers/specs/2026-03-16-custom-smtp-design.md` + +--- + +## File Map + +### New Files + +| File | Purpose | +|------|---------| +| `backend/src/infra/database/migrations/024_create-smtp-config-and-email-templates.sql` | Migration: smtp_configs + email_templates tables + seed defaults | +| `backend/src/providers/email/smtp.provider.ts` | SmtpEmailProvider implementing EmailProvider interface | +| `backend/src/services/email/smtp-config.service.ts` | CRUD service for SMTP config (singleton) | +| `backend/src/services/email/email-template.service.ts` | CRUD service for email templates (singleton) | +| `frontend/src/features/auth/services/smtp-config.service.ts` | Frontend API client for SMTP config | +| `frontend/src/features/auth/services/email-template.service.ts` | Frontend API client for email templates | +| `frontend/src/features/auth/hooks/useSmtpConfig.ts` | React Query hook for SMTP config | +| `frontend/src/features/auth/hooks/useEmailTemplates.ts` | React Query hook for email templates | +| `frontend/src/features/auth/components/SmtpSettingsCard.tsx` | SMTP config form card component | +| `frontend/src/features/auth/components/EmailTemplateCard.tsx` | Email template editor card component | + +### Modified Files + +| File | Change | +|------|--------| +| `shared-schemas/src/auth.schema.ts` | Add `smtpConfigSchema`, `emailTemplateSchema` | +| `shared-schemas/src/auth-api.schema.ts` | Add request/response schemas + type exports | +| `packages/shared-schemas/src/error-codes.schema.ts` | Add SMTP error codes | +| `backend/src/services/email/email.service.ts` | Route sends through SMTP when configured | +| `backend/src/api/routes/auth/index.routes.ts` | Add SMTP config + template CRUD routes | +| `frontend/src/features/auth/components/AuthSettingsMenuDialog.tsx` | Add "Email" tab with SMTP + template cards | +| `package.json` (backend workspace) | Add `nodemailer` + `@types/nodemailer` | + +--- + +## Task 1: Install nodemailer dependency + +**Files:** +- Modify: `backend/package.json` + +- [ ] **Step 1: Install nodemailer** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge/backend +npm install nodemailer +npm install -D @types/nodemailer +``` + +- [ ] **Step 2: Verify installation** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge/backend +node -e "require('nodemailer')" +``` + +Expected: No error output + +- [ ] **Step 3: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add backend/package.json backend/package-lock.json +git commit -m "chore: add nodemailer dependency for custom SMTP support" +``` + +--- + +## Task 2: Database migration — SMTP config and email templates tables + +**Files:** +- Create: `backend/src/infra/database/migrations/024_create-smtp-config-and-email-templates.sql` + +- [ ] **Step 1: Write the migration file** + +```sql +-- Migration: Create SMTP configuration and email templates tables +-- These tables support custom SMTP email delivery as an alternative to InsForge cloud + +-- ============================================================================ +-- SMTP Configuration (singleton) +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS auth.smtp_configs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + enabled BOOLEAN NOT NULL DEFAULT FALSE, + host TEXT NOT NULL DEFAULT '', + port INTEGER NOT NULL DEFAULT 465, + username TEXT NOT NULL DEFAULT '', + password_encrypted TEXT NOT NULL DEFAULT '', + sender_email TEXT NOT NULL DEFAULT '', + sender_name TEXT NOT NULL DEFAULT '', + min_interval_seconds INTEGER NOT NULL DEFAULT 60, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Singleton constraint: only one row allowed +CREATE UNIQUE INDEX IF NOT EXISTS smtp_configs_singleton_idx ON auth.smtp_configs ((1)); + +-- Insert default row (disabled) +INSERT INTO auth.smtp_configs (enabled) +VALUES (FALSE) +ON CONFLICT DO NOTHING; + +-- ============================================================================ +-- Email Templates +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS auth.email_templates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + template_type TEXT NOT NULL, + subject TEXT NOT NULL, + body_html TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT email_templates_type_unique UNIQUE (template_type) +); + +-- Seed default templates +INSERT INTO auth.email_templates (template_type, subject, body_html) VALUES +( + 'email-verification-code', + 'Verify your email', + '

Verify your email

Enter this code to verify your email address

{{ code }}

This code expires in 15 minutes

' +), +( + 'email-verification-link', + 'Verify your email', + '

Verify your email

Click the button below to verify your email address

Verify Email

This link expires in 24 hours

' +), +( + 'reset-password-code', + 'Reset your password', + '

Reset your password

Enter this code to reset your password

{{ code }}

This code expires in 15 minutes

' +), +( + 'reset-password-link', + 'Reset your password', + '

Reset your password

Click the button below to reset your password

Reset Password

This link expires in 24 hours

' +) +ON CONFLICT (template_type) DO NOTHING; +``` + +- [ ] **Step 2: Verify migration number is correct** + +```bash +ls /Users/gary/projects/insforge-repo/InsForge/backend/src/infra/database/migrations/ | tail -3 +``` + +Expected: `024_create-smtp-config-and-email-templates.sql` follows `023_ai-configs-soft-delete.sql` + +- [ ] **Step 3: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add backend/src/infra/database/migrations/024_create-smtp-config-and-email-templates.sql +git commit -m "feat(database): add smtp_configs and email_templates tables" +``` + +--- + +## Task 3: Shared schemas — SMTP config and email template Zod schemas + +**Files:** +- Modify: `shared-schemas/src/auth.schema.ts` (add after `authConfigSchema` at line 110) +- Modify: `shared-schemas/src/auth-api.schema.ts` (add request/response schemas + type exports) + +- [ ] **Step 1: Add entity schemas to `auth.schema.ts`** + +Add after `authConfigSchema` (line 110), before the token payload schema: + +```typescript +// SMTP configuration schema +export const smtpConfigSchema = z.object({ + id: z.string().uuid(), + enabled: z.boolean(), + host: z.string(), + port: z.number().int().min(1).max(65535), + username: z.string(), + hasPassword: z.boolean(), // Never expose actual password + senderEmail: z.string(), + senderName: z.string(), + minIntervalSeconds: z.number().int().min(0), + createdAt: z.string(), + updatedAt: z.string(), +}); + +// Email template schema +export const emailTemplateSchema = z.object({ + id: z.string().uuid(), + templateType: z.string(), + subject: z.string(), + bodyHtml: z.string(), + createdAt: z.string(), + updatedAt: z.string(), +}); +``` + +Add type exports at the end of the type exports section: + +```typescript +export type SmtpConfigSchema = z.infer; +export type EmailTemplateSchema = z.infer; +``` + +- [ ] **Step 2: Add API schemas to `auth-api.schema.ts`** + +Add a new section before the error response schema section (before line 376): + +```typescript +// ============================================================================ +// SMTP Configuration schemas +// ============================================================================ + +/** + * PUT /api/auth/smtp-config - Upsert SMTP configuration + */ +export const upsertSmtpConfigRequestSchema = z.object({ + enabled: z.boolean(), + host: z.string().min(1, 'SMTP host is required'), + port: z.number().int().min(1).max(65535), + username: z.string().min(1, 'SMTP username is required'), + password: z.string().min(1, 'SMTP password is required').optional(), + senderEmail: z.string().email('Invalid sender email'), + senderName: z.string().min(1, 'Sender name is required'), + minIntervalSeconds: z.number().int().min(0).default(60), +}); + +/** + * Response for GET /api/auth/smtp-config + */ +export const getSmtpConfigResponseSchema = smtpConfigSchema; + +// ============================================================================ +// Email Template schemas +// ============================================================================ + +/** + * PUT /api/auth/email-templates/:type - Update email template + */ +export const updateEmailTemplateRequestSchema = z.object({ + subject: z.string().min(1, 'Subject is required'), + bodyHtml: z.string().min(1, 'Template body is required'), +}); + +/** + * Response for GET /api/auth/email-templates + */ +export const listEmailTemplatesResponseSchema = z.object({ + data: z.array(emailTemplateSchema), +}); +``` + +Import `smtpConfigSchema` and `emailTemplateSchema` at the top of `auth-api.schema.ts` (line 11): + +```typescript +import { + emailSchema, + passwordSchema, + nameSchema, + userIdSchema, + userSchema, + profileSchema, + oAuthConfigSchema, + oAuthProvidersSchema, + authConfigSchema, + smtpConfigSchema, + emailTemplateSchema, +} from './auth.schema'; +``` + +Add type exports at the end of the type exports section: + +```typescript +export type UpsertSmtpConfigRequest = z.infer; +export type GetSmtpConfigResponse = z.infer; +export type UpdateEmailTemplateRequest = z.infer; +export type ListEmailTemplatesResponse = z.infer; +``` + +- [ ] **Step 3: Verify shared-schemas compile** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge/shared-schemas +npx tsc --noEmit +``` + +Expected: No errors + +- [ ] **Step 4: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add shared-schemas/src/auth.schema.ts shared-schemas/src/auth-api.schema.ts +git commit -m "feat(schemas): add SMTP config and email template Zod schemas" +``` + +--- + +## Task 4: Backend — Error constants and email type updates + +**Files:** +- Modify: `packages/shared-schemas/src/error-codes.schema.ts` + +- [ ] **Step 1: Add SMTP error codes to `packages/shared-schemas/src/error-codes.schema.ts`** + +Add these string literals to the email list that feeds `errorCodeSchema`: + +```typescript + 'EMAIL_SMTP_CONNECTION_FAILED', + 'EMAIL_SMTP_SEND_FAILED', + 'EMAIL_TEMPLATE_NOT_FOUND', +``` + +- [ ] **Step 2: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add packages/shared-schemas/src/error-codes.schema.ts +git commit -m "feat(types): add SMTP error codes and email template record type" +``` + +--- + +## Task 5: Backend — SMTP Config Service + +**Files:** +- Create: `backend/src/services/email/smtp-config.service.ts` + +- [ ] **Step 1: Write the SMTP Config Service** + +```typescript +import { Pool } from 'pg'; +import nodemailer from 'nodemailer'; +import { DatabaseManager } from '@/infra/database/database.manager.js'; +import { EncryptionManager } from '@/infra/security/encryption.manager.js'; +import { AppError } from '@/api/middlewares/error.js'; +import { ERROR_CODES } from '@insforge/shared-schemas'; +import logger from '@/utils/logger.js'; +import type { SmtpConfigSchema, UpsertSmtpConfigRequest } from '@insforge/shared-schemas'; + +export class SmtpConfigService { + private static instance: SmtpConfigService; + private pool: Pool | null = null; + + private constructor() { + logger.info('SmtpConfigService initialized'); + } + + public static getInstance(): SmtpConfigService { + if (!SmtpConfigService.instance) { + SmtpConfigService.instance = new SmtpConfigService(); + } + return SmtpConfigService.instance; + } + + private getPool(): Pool { + if (!this.pool) { + this.pool = DatabaseManager.getInstance().getPool(); + } + return this.pool; + } + + /** + * Get SMTP configuration (password masked) + */ + async getSmtpConfig(): Promise { + try { + const result = await this.getPool().query( + `SELECT + id, + enabled, + host, + port, + username, + password_encrypted IS NOT NULL AND password_encrypted != '' as "hasPassword", + sender_email as "senderEmail", + sender_name as "senderName", + min_interval_seconds as "minIntervalSeconds", + created_at as "createdAt", + updated_at as "updatedAt" + FROM auth.smtp_configs + LIMIT 1` + ); + + if (!result.rows.length) { + return { + id: '00000000-0000-0000-0000-000000000000', + enabled: false, + host: '', + port: 465, + username: '', + hasPassword: false, + senderEmail: '', + senderName: '', + minIntervalSeconds: 60, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + } + + return result.rows[0]; + } catch (error) { + logger.error('Failed to get SMTP config', { error }); + throw new AppError('Failed to get SMTP configuration', 500, ERROR_CODES.INTERNAL_ERROR); + } + } + + /** + * Get raw SMTP config with decrypted password (for internal use by SmtpEmailProvider) + */ + async getRawSmtpConfig(): Promise<{ + enabled: boolean; + host: string; + port: number; + username: string; + password: string; + senderEmail: string; + senderName: string; + minIntervalSeconds: number; + } | null> { + try { + const result = await this.getPool().query( + `SELECT + enabled, + host, + port, + username, + password_encrypted as "passwordEncrypted", + sender_email as "senderEmail", + sender_name as "senderName", + min_interval_seconds as "minIntervalSeconds" + FROM auth.smtp_configs + LIMIT 1` + ); + + if (!result.rows.length || !result.rows[0].enabled) { + return null; + } + + const row = result.rows[0]; + return { + enabled: row.enabled, + host: row.host, + port: row.port, + username: row.username, + password: row.passwordEncrypted ? EncryptionManager.decrypt(row.passwordEncrypted) : '', + senderEmail: row.senderEmail, + senderName: row.senderName, + minIntervalSeconds: row.minIntervalSeconds, + }; + } catch (error) { + logger.error('Failed to get raw SMTP config', { error }); + return null; + } + } + + /** + * Get decrypted password from existing config (for SMTP verification when password not re-submitted) + */ + private async getDecryptedPassword(): Promise { + const result = await this.getPool().query( + `SELECT password_encrypted FROM auth.smtp_configs LIMIT 1` + ); + if (!result.rows.length || !result.rows[0].password_encrypted) return ''; + return EncryptionManager.decrypt(result.rows[0].password_encrypted); + } + + /** + * Upsert SMTP configuration + */ + async upsertSmtpConfig(input: UpsertSmtpConfigRequest): Promise { + const client = await this.getPool().connect(); + try { + await client.query('BEGIN'); + + const existingResult = await client.query('SELECT id FROM auth.smtp_configs LIMIT 1 FOR UPDATE'); + + // Validate SMTP connection before persisting + if (input.enabled) { + const testPassword = input.password + || (existingResult.rows.length + ? await this.getDecryptedPassword() + : ''); + + const transporter = nodemailer.createTransport({ + host: input.host, + port: input.port, + secure: input.port === 465, + auth: { + user: input.username, + pass: testPassword, + }, + }); + + try { + await transporter.verify(); + } catch (verifyError) { + await client.query('ROLLBACK'); + throw new AppError( + `SMTP connection failed: ${verifyError instanceof Error ? verifyError.message : 'Unknown error'}`, + 400, + ERROR_CODES.EMAIL_SMTP_CONNECTION_FAILED + ); + } + } + + const encryptedPassword = input.password + ? EncryptionManager.encrypt(input.password) + : undefined; + + let result; + + if (existingResult.rows.length) { + // Update existing + const updates: string[] = []; + const values: (string | number | boolean)[] = []; + let paramCount = 1; + + updates.push(`enabled = $${paramCount++}`); + values.push(input.enabled); + + updates.push(`host = $${paramCount++}`); + values.push(input.host); + + updates.push(`port = $${paramCount++}`); + values.push(input.port); + + updates.push(`username = $${paramCount++}`); + values.push(input.username); + + if (encryptedPassword) { + updates.push(`password_encrypted = $${paramCount++}`); + values.push(encryptedPassword); + } + + updates.push(`sender_email = $${paramCount++}`); + values.push(input.senderEmail); + + updates.push(`sender_name = $${paramCount++}`); + values.push(input.senderName); + + updates.push(`min_interval_seconds = $${paramCount++}`); + values.push(input.minIntervalSeconds ?? 60); + + updates.push('updated_at = NOW()'); + + result = await client.query( + `UPDATE auth.smtp_configs + SET ${updates.join(', ')} + RETURNING + id, + enabled, + host, + port, + username, + password_encrypted IS NOT NULL AND password_encrypted != '' as "hasPassword", + sender_email as "senderEmail", + sender_name as "senderName", + min_interval_seconds as "minIntervalSeconds", + created_at as "createdAt", + updated_at as "updatedAt"`, + values + ); + } else { + // Insert new + result = await client.query( + `INSERT INTO auth.smtp_configs (enabled, host, port, username, password_encrypted, sender_email, sender_name, min_interval_seconds) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING + id, + enabled, + host, + port, + username, + password_encrypted IS NOT NULL AND password_encrypted != '' as "hasPassword", + sender_email as "senderEmail", + sender_name as "senderName", + min_interval_seconds as "minIntervalSeconds", + created_at as "createdAt", + updated_at as "updatedAt"`, + [ + input.enabled, + input.host, + input.port, + input.username, + encryptedPassword || '', + input.senderEmail, + input.senderName, + input.minIntervalSeconds ?? 60, + ] + ); + } + + await client.query('COMMIT'); + logger.info('SMTP config updated', { enabled: input.enabled }); + return result.rows[0]; + } catch (error) { + await client.query('ROLLBACK'); + logger.error('Failed to upsert SMTP config', { error }); + if (error instanceof AppError) throw error; + throw new AppError('Failed to update SMTP configuration', 500, ERROR_CODES.INTERNAL_ERROR); + } finally { + client.release(); + } + } +} +``` + +- [ ] **Step 2: Verify it compiles** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge/backend +npx tsc --noEmit +``` + +Expected: No errors related to smtp-config.service + +- [ ] **Step 3: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add backend/src/services/email/smtp-config.service.ts +git commit -m "feat(email): add SMTP config service with encrypted password storage" +``` + +--- + +## Task 6: Backend — Email Template Service + +**Files:** +- Create: `backend/src/services/email/email-template.service.ts` + +- [ ] **Step 1: Write the Email Template Service** + +```typescript +import { Pool } from 'pg'; +import { DatabaseManager } from '@/infra/database/database.manager.js'; +import { AppError } from '@/api/middlewares/error.js'; +import { ERROR_CODES } from '@insforge/shared-schemas'; +import logger from '@/utils/logger.js'; +import type { EmailTemplate } from '@/types/email.js'; +import type { EmailTemplateSchema, UpdateEmailTemplateRequest } from '@insforge/shared-schemas'; + +export class EmailTemplateService { + private static instance: EmailTemplateService; + private pool: Pool | null = null; + + private constructor() { + logger.info('EmailTemplateService initialized'); + } + + public static getInstance(): EmailTemplateService { + if (!EmailTemplateService.instance) { + EmailTemplateService.instance = new EmailTemplateService(); + } + return EmailTemplateService.instance; + } + + private getPool(): Pool { + if (!this.pool) { + this.pool = DatabaseManager.getInstance().getPool(); + } + return this.pool; + } + + /** + * Get all email templates + */ + async getTemplates(): Promise { + try { + const result = await this.getPool().query( + `SELECT + id, + template_type as "templateType", + subject, + body_html as "bodyHtml", + created_at as "createdAt", + updated_at as "updatedAt" + FROM auth.email_templates + ORDER BY template_type` + ); + return result.rows; + } catch (error) { + logger.error('Failed to get email templates', { error }); + throw new AppError('Failed to get email templates', 500, ERROR_CODES.INTERNAL_ERROR); + } + } + + /** + * Get a single email template by type + */ + async getTemplate(templateType: EmailTemplate): Promise { + try { + const result = await this.getPool().query( + `SELECT + id, + template_type as "templateType", + subject, + body_html as "bodyHtml", + created_at as "createdAt", + updated_at as "updatedAt" + FROM auth.email_templates + WHERE template_type = $1`, + [templateType] + ); + + if (!result.rows.length) { + throw new AppError( + `Email template not found: ${templateType}`, + 404, + ERROR_CODES.EMAIL_TEMPLATE_NOT_FOUND + ); + } + + return result.rows[0]; + } catch (error) { + if (error instanceof AppError) throw error; + logger.error('Failed to get email template', { templateType, error }); + throw new AppError('Failed to get email template', 500, ERROR_CODES.INTERNAL_ERROR); + } + } + + /** + * Update an email template + */ + async updateTemplate( + templateType: EmailTemplate, + input: UpdateEmailTemplateRequest + ): Promise { + try { + const result = await this.getPool().query( + `UPDATE auth.email_templates + SET subject = $1, body_html = $2, updated_at = NOW() + WHERE template_type = $3 + RETURNING + id, + template_type as "templateType", + subject, + body_html as "bodyHtml", + created_at as "createdAt", + updated_at as "updatedAt"`, + [input.subject, input.bodyHtml, templateType] + ); + + if (!result.rows.length) { + throw new AppError( + `Email template not found: ${templateType}`, + 404, + ERROR_CODES.EMAIL_TEMPLATE_NOT_FOUND + ); + } + + logger.info('Email template updated', { templateType }); + return result.rows[0]; + } catch (error) { + if (error instanceof AppError) throw error; + logger.error('Failed to update email template', { templateType, error }); + throw new AppError('Failed to update email template', 500, ERROR_CODES.INTERNAL_ERROR); + } + } +} +``` + +- [ ] **Step 2: Verify it compiles** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge/backend +npx tsc --noEmit +``` + +- [ ] **Step 3: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add backend/src/services/email/email-template.service.ts +git commit -m "feat(email): add email template CRUD service" +``` + +--- + +## Task 7: Backend — SMTP Email Provider + +**Files:** +- Create: `backend/src/providers/email/smtp.provider.ts` + +- [ ] **Step 1: Write the SMTP Email Provider** + +```typescript +import nodemailer from 'nodemailer'; +import type { Transporter } from 'nodemailer'; +import { EmailProvider } from './base.provider.js'; +import { SmtpConfigService } from '@/services/email/smtp-config.service.js'; +import { EmailTemplateService } from '@/services/email/email-template.service.js'; +import { AppError } from '@/api/middlewares/error.js'; +import { ERROR_CODES } from '@insforge/shared-schemas'; +import logger from '@/utils/logger.js'; +import type { EmailTemplate } from '@/types/email.js'; +import type { SendRawEmailRequest } from '@insforge/shared-schemas'; + +/** + * HTML-escape a string to prevent XSS in email templates + */ +function escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * SMTP email provider using nodemailer + */ +export class SmtpEmailProvider implements EmailProvider { + private smtpConfigService = SmtpConfigService.getInstance(); + private emailTemplateService = EmailTemplateService.getInstance(); + + supportsTemplates(): boolean { + return true; + } + + /** + * Create a nodemailer transporter from DB config + */ + private async createTransporter(): Promise<{ transporter: Transporter; senderEmail: string; senderName: string }> { + const config = await this.smtpConfigService.getRawSmtpConfig(); + + if (!config) { + throw new AppError( + 'SMTP is not configured or not enabled', + 500, + ERROR_CODES.EMAIL_SMTP_CONNECTION_FAILED + ); + } + + const transporter = nodemailer.createTransport({ + host: config.host, + port: config.port, + secure: config.port === 465, + auth: { + user: config.username, + pass: config.password, + }, + }); + + return { + transporter, + senderEmail: config.senderEmail, + senderName: config.senderName, + }; + } + + /** + * Render a template by replacing placeholders with values + */ + private renderTemplate( + html: string, + variables: Record + ): string { + let rendered = html; + for (const [key, value] of Object.entries(variables)) { + // Don't escape 'link' values — they are URLs used in href attributes + const safeValue = key === 'link' ? value : escapeHtml(value); + rendered = rendered.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'), safeValue); + } + return rendered; + } + + async sendWithTemplate( + email: string, + name: string, + template: EmailTemplate, + variables?: Record + ): Promise { + try { + const { transporter, senderEmail, senderName } = await this.createTransporter(); + const templateRecord = await this.emailTemplateService.getTemplate(template); + + const renderedSubject = this.renderTemplate( + templateRecord.subject, + { ...variables, email, app_name: name } + ); + const renderedBody = this.renderTemplate( + templateRecord.bodyHtml, + { ...variables, email, app_name: name } + ); + + await transporter.sendMail({ + from: `"${senderName}" <${senderEmail}>`, + to: email, + subject: renderedSubject, + html: renderedBody, + }); + + logger.info('Email sent via SMTP', { template, to: email }); + } catch (error) { + if (error instanceof AppError) throw error; + logger.error('Failed to send email via SMTP', { + template, + to: email, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw new AppError( + `Failed to send email via SMTP: ${error instanceof Error ? error.message : 'Unknown error'}`, + 500, + ERROR_CODES.EMAIL_SMTP_SEND_FAILED + ); + } + } + + async sendRaw(options: SendRawEmailRequest): Promise { + try { + const { transporter, senderEmail, senderName } = await this.createTransporter(); + + await transporter.sendMail({ + from: `"${senderName}" <${senderEmail}>`, + to: options.to, + subject: options.subject, + html: options.html, + cc: options.cc, + bcc: options.bcc, + replyTo: options.replyTo, + }); + + logger.info('Raw email sent via SMTP', { to: options.to }); + } catch (error) { + if (error instanceof AppError) throw error; + logger.error('Failed to send raw email via SMTP', { + to: options.to, + error: error instanceof Error ? error.message : 'Unknown error', + }); + throw new AppError( + `Failed to send email via SMTP: ${error instanceof Error ? error.message : 'Unknown error'}`, + 500, + ERROR_CODES.EMAIL_SMTP_SEND_FAILED + ); + } + } +} +``` + +- [ ] **Step 2: Verify it compiles** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge/backend +npx tsc --noEmit +``` + +- [ ] **Step 3: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add backend/src/providers/email/smtp.provider.ts +git commit -m "feat(email): add SMTP email provider with nodemailer" +``` + +--- + +## Task 8: Backend — Modify EmailService to route through SMTP + +**Files:** +- Modify: `backend/src/services/email/email.service.ts` + +- [ ] **Step 1: Update EmailService to check SMTP config per-call** + +Replace the entire file content with: + +```typescript +import { EmailProvider } from '@/providers/email/base.provider.js'; +import { CloudEmailProvider } from '@/providers/email/cloud.provider.js'; +import { SmtpEmailProvider } from '@/providers/email/smtp.provider.js'; +import { SmtpConfigService } from '@/services/email/smtp-config.service.js'; +import { EmailTemplate } from '@/types/email.js'; +import { SendRawEmailRequest } from '@insforge/shared-schemas'; +import logger from '@/utils/logger.js'; + +/** + * Email service that orchestrates different email providers + */ +export class EmailService { + private static instance: EmailService; + private cloudProvider: EmailProvider; + private smtpProvider: EmailProvider; + private smtpConfigService: SmtpConfigService; + + private constructor() { + this.cloudProvider = new CloudEmailProvider(); + this.smtpProvider = new SmtpEmailProvider(); + this.smtpConfigService = SmtpConfigService.getInstance(); + logger.info('EmailService initialized with cloud + SMTP providers'); + } + + public static getInstance(): EmailService { + if (!EmailService.instance) { + EmailService.instance = new EmailService(); + } + return EmailService.instance; + } + + /** + * Resolve which provider to use based on SMTP config + */ + private async resolveProvider(): Promise { + try { + const config = await this.smtpConfigService.getRawSmtpConfig(); + if (config && config.enabled) { + return this.smtpProvider; + } + } catch (error) { + logger.warn('Failed to check SMTP config, falling back to cloud provider', { error }); + } + return this.cloudProvider; + } + + /** + * Send email using predefined template + */ + public async sendWithTemplate( + email: string, + name: string, + template: EmailTemplate, + variables?: Record + ): Promise { + const provider = await this.resolveProvider(); + return provider.sendWithTemplate(email, name, template, variables); + } + + /** + * Send custom/raw email + */ + public async sendRaw(options: SendRawEmailRequest): Promise { + const provider = await this.resolveProvider(); + if (!provider.sendRaw) { + throw new Error('Current email provider does not support raw email sending'); + } + return provider.sendRaw(options); + } + + /** + * Check if current provider supports templates + */ + public supportsTemplates(): boolean { + return true; + } +} +``` + +- [ ] **Step 2: Verify it compiles** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge/backend +npx tsc --noEmit +``` + +- [ ] **Step 3: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add backend/src/services/email/email.service.ts +git commit -m "feat(email): route email sends through SMTP when configured" +``` + +--- + +## Task 9: Backend — API routes for SMTP config and email templates + +**Files:** +- Modify: `backend/src/api/routes/auth/index.routes.ts` + +- [ ] **Step 1: Add imports at the top of the file** + +Add these imports alongside existing ones: + +```typescript +import { SmtpConfigService } from '@/services/email/smtp-config.service.js'; +import { EmailTemplateService } from '@/services/email/email-template.service.js'; +import { + upsertSmtpConfigRequestSchema, + updateEmailTemplateRequestSchema, +} from '@insforge/shared-schemas'; +import type { EmailTemplate } from '@/types/email.js'; +``` + +Add service instances alongside existing ones (after `const auditService = ...`): + +```typescript +const smtpConfigService = SmtpConfigService.getInstance(); +const emailTemplateService = EmailTemplateService.getInstance(); +``` + +- [ ] **Step 2: Add SMTP config routes** + +Add before the `export default router;` line (line 874): + +```typescript +// ============================================================================ +// SMTP Configuration Routes +// ============================================================================ + +// GET /api/auth/smtp-config - Get SMTP configuration (admin only) +router.get('/smtp-config', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const config = await smtpConfigService.getSmtpConfig(); + successResponse(res, config); + } catch (error) { + next(error); + } +}); + +// PUT /api/auth/smtp-config - Update SMTP configuration (admin only) +router.put('/smtp-config', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const validationResult = upsertSmtpConfigRequestSchema.safeParse(req.body); + if (!validationResult.success) { + throw new AppError( + validationResult.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', '), + 400, + ERROR_CODES.INVALID_INPUT + ); + } + + const input = validationResult.data; + const config = await smtpConfigService.upsertSmtpConfig(input); + + await auditService.log({ + actor: req.user?.email || 'api-key', + action: 'UPDATE_SMTP_CONFIG', + module: 'EMAIL', + details: { enabled: input.enabled, host: input.host }, + ip_address: req.ip, + }); + + successResponse(res, config); + } catch (error) { + next(error); + } +}); + +// ============================================================================ +// Email Template Routes +// ============================================================================ + +// GET /api/auth/email-templates - Get all email templates (admin only) +router.get('/email-templates', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const templates = await emailTemplateService.getTemplates(); + successResponse(res, { data: templates }); + } catch (error) { + next(error); + } +}); + +// PUT /api/auth/email-templates/:type - Update email template (admin only) +router.put('/email-templates/:type', verifyAdmin, async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const templateType = req.params.type as EmailTemplate; + const validTypes: EmailTemplate[] = [ + 'email-verification-code', + 'email-verification-link', + 'reset-password-code', + 'reset-password-link', + ]; + + if (!validTypes.includes(templateType)) { + throw new AppError( + `Invalid template type: ${templateType}`, + 400, + ERROR_CODES.INVALID_INPUT + ); + } + + const validationResult = updateEmailTemplateRequestSchema.safeParse(req.body); + if (!validationResult.success) { + throw new AppError( + validationResult.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', '), + 400, + ERROR_CODES.INVALID_INPUT + ); + } + + const template = await emailTemplateService.updateTemplate(templateType, validationResult.data); + + await auditService.log({ + actor: req.user?.email || 'api-key', + action: 'UPDATE_EMAIL_TEMPLATE', + module: 'EMAIL', + details: { templateType }, + ip_address: req.ip, + }); + + successResponse(res, template); + } catch (error) { + next(error); + } +}); +``` + +- [ ] **Step 3: Verify it compiles** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge/backend +npx tsc --noEmit +``` + +- [ ] **Step 4: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add backend/src/api/routes/auth/index.routes.ts +git commit -m "feat(api): add SMTP config and email template CRUD routes" +``` + +--- + +## Task 10: Frontend — SMTP Config API service and hook + +**Files:** +- Create: `frontend/src/features/auth/services/smtp-config.service.ts` +- Create: `frontend/src/features/auth/hooks/useSmtpConfig.ts` + +- [ ] **Step 1: Write the SMTP config API service** + +```typescript +import { apiClient } from '@/lib/api/client'; +import type { SmtpConfigSchema, UpsertSmtpConfigRequest } from '@insforge/shared-schemas'; + +export class SmtpConfigService { + async getConfig(): Promise { + return apiClient.request('/auth/smtp-config'); + } + + async updateConfig(config: UpsertSmtpConfigRequest): Promise { + return apiClient.request('/auth/smtp-config', { + method: 'PUT', + body: JSON.stringify(config), + }); + } +} + +export const smtpConfigService = new SmtpConfigService(); +``` + +- [ ] **Step 2: Write the useSmtpConfig hook** + +```typescript +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useToast } from '@/lib/hooks/useToast'; +import { smtpConfigService } from '@/features/auth/services/smtp-config.service'; +import type { SmtpConfigSchema, UpsertSmtpConfigRequest } from '@insforge/shared-schemas'; + +export function useSmtpConfig() { + const queryClient = useQueryClient(); + const { showToast } = useToast(); + + const { + data: config, + isLoading, + error, + refetch, + } = useQuery({ + queryKey: ['smtp-config'], + queryFn: () => smtpConfigService.getConfig(), + }); + + const updateConfigMutation = useMutation({ + mutationFn: (config: UpsertSmtpConfigRequest) => smtpConfigService.updateConfig(config), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['smtp-config'] }); + showToast('SMTP configuration updated successfully', 'success'); + }, + onError: (error: Error) => { + showToast(error.message || 'Failed to update SMTP configuration', 'error'); + }, + }); + + return { + config, + isLoading, + isUpdating: updateConfigMutation.isPending, + error, + updateConfig: updateConfigMutation.mutate, + refetch, + }; +} +``` + +- [ ] **Step 3: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add frontend/src/features/auth/services/smtp-config.service.ts frontend/src/features/auth/hooks/useSmtpConfig.ts +git commit -m "feat(frontend): add SMTP config service and React Query hook" +``` + +--- + +## Task 11: Frontend — Email Template API service and hook + +**Files:** +- Create: `frontend/src/features/auth/services/email-template.service.ts` +- Create: `frontend/src/features/auth/hooks/useEmailTemplates.ts` + +- [ ] **Step 1: Write the email template API service** + +```typescript +import { apiClient } from '@/lib/api/client'; +import type { + EmailTemplateSchema, + ListEmailTemplatesResponse, + UpdateEmailTemplateRequest, +} from '@insforge/shared-schemas'; + +export class EmailTemplateService { + async getTemplates(): Promise { + return apiClient.request('/auth/email-templates'); + } + + async updateTemplate( + type: string, + data: UpdateEmailTemplateRequest + ): Promise { + return apiClient.request(`/auth/email-templates/${type}`, { + method: 'PUT', + body: JSON.stringify(data), + }); + } +} + +export const emailTemplateService = new EmailTemplateService(); +``` + +- [ ] **Step 2: Write the useEmailTemplates hook** + +```typescript +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useToast } from '@/lib/hooks/useToast'; +import { emailTemplateService } from '@/features/auth/services/email-template.service'; +import type { + ListEmailTemplatesResponse, + UpdateEmailTemplateRequest, +} from '@insforge/shared-schemas'; + +export function useEmailTemplates() { + const queryClient = useQueryClient(); + const { showToast } = useToast(); + + const { + data: templates, + isLoading, + error, + refetch, + } = useQuery({ + queryKey: ['email-templates'], + queryFn: () => emailTemplateService.getTemplates(), + }); + + const updateTemplateMutation = useMutation({ + mutationFn: ({ type, data }: { type: string; data: UpdateEmailTemplateRequest }) => + emailTemplateService.updateTemplate(type, data), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['email-templates'] }); + showToast('Email template updated successfully', 'success'); + }, + onError: (error: Error) => { + showToast(error.message || 'Failed to update email template', 'error'); + }, + }); + + return { + templates: templates?.data ?? [], + isLoading, + isUpdating: updateTemplateMutation.isPending, + error, + updateTemplate: updateTemplateMutation.mutate, + refetch, + }; +} +``` + +- [ ] **Step 3: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add frontend/src/features/auth/services/email-template.service.ts frontend/src/features/auth/hooks/useEmailTemplates.ts +git commit -m "feat(frontend): add email template service and React Query hook" +``` + +--- + +## Task 12: Frontend — SMTP Settings Card component + +**Files:** +- Create: `frontend/src/features/auth/components/SmtpSettingsCard.tsx` + +- [ ] **Step 1: Write the SmtpSettingsCard component** + +```tsx +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button, Input, Switch } from '@insforge/ui'; +import { + upsertSmtpConfigRequestSchema, + type SmtpConfigSchema, + type UpsertSmtpConfigRequest, +} from '@insforge/shared-schemas'; + +interface SmtpSettingsCardProps { + config: SmtpConfigSchema | undefined; + isLoading: boolean; + isUpdating: boolean; + onSave: (data: UpsertSmtpConfigRequest) => void; +} + +interface SettingRowProps { + label: string; + description?: string; + children: React.ReactNode; +} + +function SettingRow({ label, description, children }: SettingRowProps) { + return ( +
+
+
+

{label}

+
+ {description && ( +

+ {description} +

+ )} +
+
{children}
+
+ ); +} + +export function SmtpSettingsCard({ config, isLoading, isUpdating, onSave }: SmtpSettingsCardProps) { + const form = useForm({ + resolver: zodResolver(upsertSmtpConfigRequestSchema), + defaultValues: { + enabled: false, + host: '', + port: 465, + username: '', + password: undefined, + senderEmail: '', + senderName: '', + minIntervalSeconds: 60, + }, + }); + + const enabled = form.watch('enabled'); + + useEffect(() => { + if (config) { + form.reset({ + enabled: config.enabled, + host: config.host, + port: config.port, + username: config.username, + password: undefined, // Never pre-fill password + senderEmail: config.senderEmail, + senderName: config.senderName, + minIntervalSeconds: config.minIntervalSeconds, + }); + } + }, [config, form]); + + const handleSubmit = () => { + void form.handleSubmit((data) => { + onSave(data); + })(); + }; + + if (isLoading) { + return ( +
+ Loading SMTP configuration... +
+ ); + } + + return ( +
+
+

SMTP Provider Settings

+

+ Configure a custom SMTP server for sending emails. Your SMTP credentials will always be + encrypted in our database. +

+
+ + + form.setValue('enabled', value, { shouldDirty: true })} + /> + + +
+
+
+

Sender Details

+
+ + + + + + + +
+
+ +
+

SMTP Provider Settings

+
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+ + {form.formState.isDirty && ( +
+ + +
+ )} +
+ ); +} +``` + +- [ ] **Step 2: Verify it compiles** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge/frontend +npx tsc --noEmit +``` + +- [ ] **Step 3: Commit** + +```bash +cd /Users/gary/projects/insforge-repo/InsForge +git add frontend/src/features/auth/components/SmtpSettingsCard.tsx +git commit -m "feat(frontend): add SMTP settings card component" +``` + +--- + +## Task 13: Frontend — Email Template Card component + +**Files:** +- Create: `frontend/src/features/auth/components/EmailTemplateCard.tsx` + +- [ ] **Step 1: Write the EmailTemplateCard component** + +```tsx +import { useEffect, useState } from 'react'; +import { Button, Input, Select, SelectContent, SelectItem, SelectTrigger } from '@insforge/ui'; +import type { EmailTemplateSchema, UpdateEmailTemplateRequest } from '@insforge/shared-schemas'; + +interface EmailTemplateCardProps { + templates: EmailTemplateSchema[]; + isLoading: boolean; + isUpdating: boolean; + onSave: (params: { type: string; data: UpdateEmailTemplateRequest }) => void; +} + +const TEMPLATE_LABELS: Record = { + 'email-verification-code': 'Email Verification (Code)', + 'email-verification-link': 'Email Verification (Link)', + 'reset-password-code': 'Password Reset (Code)', + 'reset-password-link': 'Password Reset (Link)', +}; + +const TEMPLATE_PLACEHOLDERS: Record = { + 'email-verification-code': ['{{ code }}', '{{ email }}', '{{ app_name }}'], + 'email-verification-link': ['{{ link }}', '{{ email }}', '{{ app_name }}'], + 'reset-password-code': ['{{ code }}', '{{ email }}', '{{ app_name }}'], + 'reset-password-link': ['{{ link }}', '{{ email }}', '{{ app_name }}'], +}; + +type EditorTab = 'source' | 'preview'; + +export function EmailTemplateCard({ + templates, + isLoading, + isUpdating, + onSave, +}: EmailTemplateCardProps) { + const [selectedType, setSelectedType] = useState('email-verification-code'); + const [activeTab, setActiveTab] = useState('source'); + const [subject, setSubject] = useState(''); + const [bodyHtml, setBodyHtml] = useState(''); + const [isDirty, setIsDirty] = useState(false); + + const selectedTemplate = templates.find((t) => t.templateType === selectedType); + + useEffect(() => { + if (selectedTemplate) { + setSubject(selectedTemplate.subject); + setBodyHtml(selectedTemplate.bodyHtml); + setIsDirty(false); + } + }, [selectedTemplate]); + + const handleSubjectChange = (value: string) => { + setSubject(value); + setIsDirty(true); + }; + + const handleBodyChange = (value: string) => { + setBodyHtml(value); + setIsDirty(true); + }; + + const handleSave = () => { + onSave({ + type: selectedType, + data: { subject, bodyHtml }, + }); + setIsDirty(false); + }; + + const handleCancel = () => { + if (selectedTemplate) { + setSubject(selectedTemplate.subject); + setBodyHtml(selectedTemplate.bodyHtml); + setIsDirty(false); + } + }; + + if (isLoading) { + return ( +
+ Loading email templates... +
+ ); + } + + const placeholders = TEMPLATE_PLACEHOLDERS[selectedType] ?? []; + + return ( +
+
+

Email Templates

+

+ Customize the email content sent to users. Templates use placeholders that are replaced + with actual values when sent. +

+
+ +
+
+
+

Template

+
+
+ +
+
+ +
+
+

Subject

+
+
+ handleSubjectChange(e.target.value)} + placeholder="Email subject line" + /> +
+
+ +
+
+ + +
+ + {activeTab === 'source' ? ( +