From 51dedc6b366c78191aed446ff2b5478038011aea Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Wed, 22 Oct 2025 15:02:48 -0400 Subject: [PATCH 01/17] relaxing env validation locally --- packages/app/control/.env.example | 4 ++ packages/app/control/src/env.ts | 18 +++----- packages/app/control/src/instrumentation.ts | 13 ++++++ packages/app/control/src/logger.ts | 45 +++++++++++-------- .../control/src/services/email/lib/client.ts | 4 +- .../control/src/services/email/lib/send.ts | 15 ++++++- .../app/control/src/services/stripe/client.ts | 8 ++-- .../src/services/stripe/create-link/lib.ts | 4 ++ .../stripe/webhook/construct-event.ts | 4 ++ 9 files changed, 80 insertions(+), 35 deletions(-) diff --git a/packages/app/control/.env.example b/packages/app/control/.env.example index adb5a1743..8dabb238d 100644 --- a/packages/app/control/.env.example +++ b/packages/app/control/.env.example @@ -1,3 +1,7 @@ +# ---------- +# Environment +# ---------- +VERCEL_ENV="local" # ---------- # Application diff --git a/packages/app/control/src/env.ts b/packages/app/control/src/env.ts index 247102fc1..a6905a43a 100644 --- a/packages/app/control/src/env.ts +++ b/packages/app/control/src/env.ts @@ -56,21 +56,15 @@ export const env = createEnv({ // email - AUTH_RESEND_KEY: !IS_INTEGRATION_TEST - ? z.string() - : z.string().default('auth-resend-key-change-in-production'), - AUTH_RESEND_FROM_EMAIL: !IS_INTEGRATION_TEST - ? z.email() - : z.string().default('john@doe.com'), - RESEND_FLOW_CONTROL_KEY: IS_STRICT - ? z.string() - : z.string().default('resend-flow-control-key'), + AUTH_RESEND_KEY: IS_STRICT ? z.string() : z.string().optional(), + AUTH_RESEND_FROM_EMAIL: IS_STRICT ? z.email() : z.string().optional(), + RESEND_FLOW_CONTROL_KEY: IS_STRICT ? z.string() : z.string().optional(), // stripe - STRIPE_SECRET_KEY: z.string(), - STRIPE_PUBLISHABLE_KEY: z.string(), - STRIPE_WEBHOOK_SECRET: z.string(), + STRIPE_SECRET_KEY: IS_STRICT ? z.string() : z.string().optional(), + STRIPE_PUBLISHABLE_KEY: IS_STRICT ? z.string() : z.string().optional(), + STRIPE_WEBHOOK_SECRET: IS_STRICT ? z.string() : z.string().optional(), WEBHOOK_URL: IS_STRICT ? z.url() : z.url().default('http://localhost:3000/stripe/webhook'), diff --git a/packages/app/control/src/instrumentation.ts b/packages/app/control/src/instrumentation.ts index 7c19c87df..c9e4d2aa4 100644 --- a/packages/app/control/src/instrumentation.ts +++ b/packages/app/control/src/instrumentation.ts @@ -7,7 +7,20 @@ const SIGNOZ_INGESTION_KEY = env.SIGNOZ_INGESTION_KEY; const OTEL_EXPORTER_OTLP_ENDPOINT = env.OTEL_EXPORTER_OTLP_ENDPOINT; const SIGNOZ_SERVICE_NAME = env.SIGNOZ_SERVICE_NAME; +// Check if telemetry config is properly set +const isTelemetryConfigured = + OTEL_EXPORTER_OTLP_ENDPOINT && + OTEL_EXPORTER_OTLP_ENDPOINT !== 'undefined' && + SIGNOZ_SERVICE_NAME && + SIGNOZ_SERVICE_NAME !== 'undefined'; + export function register() { + // Skip telemetry setup if config is missing or invalid + if (!isTelemetryConfigured) { + console.log('[Instrumentation] Skipping telemetry setup (config missing)'); + return; + } + // --- Traces --- registerOTel({ serviceName: SIGNOZ_SERVICE_NAME, diff --git a/packages/app/control/src/logger.ts b/packages/app/control/src/logger.ts index 350b33dcd..f7ad45396 100644 --- a/packages/app/control/src/logger.ts +++ b/packages/app/control/src/logger.ts @@ -125,38 +125,47 @@ class ConsoleLogProcessor implements LogRecordProcessor { // --- Setup function --- export function setupLoggerProvider() { - const logExporter = new OTLPLogExporter({ - url: `${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs`, - headers: SIGNOZ_INGESTION_KEY - ? { - 'signoz-access-token': SIGNOZ_INGESTION_KEY, - } - : {}, - }); + // Check if telemetry config is properly set + const isTelemetryConfigured = + OTEL_EXPORTER_OTLP_ENDPOINT && + OTEL_EXPORTER_OTLP_ENDPOINT !== 'undefined' && + SIGNOZ_SERVICE_NAME && + SIGNOZ_SERVICE_NAME !== 'undefined'; const resource = new Resource({ - 'service.name': SIGNOZ_SERVICE_NAME, + 'service.name': SIGNOZ_SERVICE_NAME || 'echo-control-local', 'service.environment': NODE_ENV, }); const loggerProvider = new LoggerProvider({ resource }); - const batchProcessor = new BatchLogRecordProcessor(logExporter); - // Add the OTLP processor with trace context injection - loggerProvider.addLogRecordProcessor( - new TraceContextLogProcessor(batchProcessor) - ); - - // Add console processor for local development and debugging - // You can control this with an environment variable if needed + // Only set up OTLP exporter if telemetry is properly configured + if (isTelemetryConfigured) { + const logExporter = new OTLPLogExporter({ + url: `${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs`, + headers: SIGNOZ_INGESTION_KEY + ? { + 'signoz-access-token': SIGNOZ_INGESTION_KEY, + } + : {}, + }); + + const batchProcessor = new BatchLogRecordProcessor(logExporter); + + // Add the OTLP processor with trace context injection + loggerProvider.addLogRecordProcessor( + new TraceContextLogProcessor(batchProcessor) + ); + } + // Always add console processor for local development and debugging loggerProvider.addLogRecordProcessor( new TraceContextLogProcessor(new ConsoleLogProcessor()) ); logs.setGlobalLoggerProvider(loggerProvider); - return logs.getLogger(SIGNOZ_SERVICE_NAME); // handy to export directly + return logs.getLogger(SIGNOZ_SERVICE_NAME || 'echo-control-local'); } export const logger = setupLoggerProvider(); diff --git a/packages/app/control/src/services/email/lib/client.ts b/packages/app/control/src/services/email/lib/client.ts index c3a968aff..96a7d8d02 100644 --- a/packages/app/control/src/services/email/lib/client.ts +++ b/packages/app/control/src/services/email/lib/client.ts @@ -2,4 +2,6 @@ import { Resend } from 'resend'; import { env } from '@/env'; -export const emailClient = new Resend(env.AUTH_RESEND_KEY); +export const emailClient = env.AUTH_RESEND_KEY + ? new Resend(env.AUTH_RESEND_KEY) + : null; diff --git a/packages/app/control/src/services/email/lib/send.ts b/packages/app/control/src/services/email/lib/send.ts index cda1bfc36..cc73ea589 100644 --- a/packages/app/control/src/services/email/lib/send.ts +++ b/packages/app/control/src/services/email/lib/send.ts @@ -12,6 +12,18 @@ export function sendEmailWithRetry( options?: CreateEmailRequestOptions, config?: Partial ) { + // Skip email sending if not in production environment + if (!emailClient || !env.AUTH_RESEND_FROM_EMAIL) { + console.log( + '[Email Skipped - Not in Production] Would have sent email:', + payload + ); + return Promise.resolve({ + data: { id: 'skipped-not-in-production' }, + error: null, + } as ResendResult<{ id: string }>); + } + const idempotencyKey = options?.idempotencyKey ?? randomUUID(); const stableOptions: CreateEmailRequestOptions = { ...options, @@ -19,10 +31,11 @@ export function sendEmailWithRetry( }; const fromEmail = env.AUTH_RESEND_FROM_EMAIL; + const client = emailClient; // TypeScript refinement return resendRetry( () => - emailClient.emails.send( + client.emails.send( { ...payload, from: `Sam Ragsdale <${fromEmail}>` as const, diff --git a/packages/app/control/src/services/stripe/client.ts b/packages/app/control/src/services/stripe/client.ts index 542b5cba6..c19273a8e 100644 --- a/packages/app/control/src/services/stripe/client.ts +++ b/packages/app/control/src/services/stripe/client.ts @@ -2,6 +2,8 @@ import Stripe from 'stripe'; import { env } from '@/env'; -export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { - apiVersion: '2025-05-28.basil', -}); +export const stripe = env.STRIPE_SECRET_KEY + ? new Stripe(env.STRIPE_SECRET_KEY, { + apiVersion: '2025-05-28.basil', + }) + : null; diff --git a/packages/app/control/src/services/stripe/create-link/lib.ts b/packages/app/control/src/services/stripe/create-link/lib.ts index 7a06d0f13..d5956ef21 100644 --- a/packages/app/control/src/services/stripe/create-link/lib.ts +++ b/packages/app/control/src/services/stripe/create-link/lib.ts @@ -22,6 +22,10 @@ export const createPaymentLink = async ( userId: string, parameters: z.infer ) => { + if (!stripe) { + throw new Error('Stripe is not configured for this environment'); + } + const { amount, name, description, successUrl, metadata } = createPaymentLinkSchema.parse(parameters); diff --git a/packages/app/control/src/services/stripe/webhook/construct-event.ts b/packages/app/control/src/services/stripe/webhook/construct-event.ts index 949d4e373..9f75aa456 100644 --- a/packages/app/control/src/services/stripe/webhook/construct-event.ts +++ b/packages/app/control/src/services/stripe/webhook/construct-event.ts @@ -6,6 +6,10 @@ import { logger } from '@/logger'; import type { NextRequest } from 'next/server'; export const constructStripeEvent = (request: NextRequest, body: string) => { + if (!stripe || !env.STRIPE_WEBHOOK_SECRET) { + throw new Error('Stripe is not configured for this environment'); + } + const signature = request.headers.get('stripe-signature'); if (!signature) { From 832a467346e3210523742de6afff18c4e2d13d7f Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Wed, 22 Oct 2025 15:05:51 -0400 Subject: [PATCH 02/17] formatting --- .../server/src/services/fund-repo/fundRepoService.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/app/server/src/services/fund-repo/fundRepoService.ts b/packages/app/server/src/services/fund-repo/fundRepoService.ts index 9fd3acf0f..6368fac47 100644 --- a/packages/app/server/src/services/fund-repo/fundRepoService.ts +++ b/packages/app/server/src/services/fund-repo/fundRepoService.ts @@ -112,11 +112,7 @@ export async function fundRepo( } } - - -export async function safeFundRepo( - amount: number, -): Promise { +export async function safeFundRepo(amount: number): Promise { try { const repoId = process.env.MERIT_REPO_ID; if (!repoId) { @@ -124,6 +120,8 @@ export async function safeFundRepo( } await fundRepo(amount, Number(repoId)); } catch (error) { - logger.error(`Error in safe funding repo: ${error instanceof Error ? error.message : 'Unknown error'} | Amount: ${amount}`); + logger.error( + `Error in safe funding repo: ${error instanceof Error ? error.message : 'Unknown error'} | Amount: ${amount}` + ); } -} \ No newline at end of file +} From 30ecdb3964546a2a1a2905ed3a57f53f668b5ad2 Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Wed, 22 Oct 2025 15:28:29 -0400 Subject: [PATCH 03/17] resend lint error --- packages/app/control/src/services/email/queue.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/app/control/src/services/email/queue.ts b/packages/app/control/src/services/email/queue.ts index c278c98b3..5655a61da 100644 --- a/packages/app/control/src/services/email/queue.ts +++ b/packages/app/control/src/services/email/queue.ts @@ -33,11 +33,13 @@ export const queueJob = async (body: z.infer) => { type: 'email', job: body, }, - flowControl: { - key: env.RESEND_FLOW_CONTROL_KEY, - rate: 2, - period: '1m', - }, + ...(env.RESEND_FLOW_CONTROL_KEY && { + flowControl: { + key: env.RESEND_FLOW_CONTROL_KEY, + rate: 2, + period: '1m', + }, + }), }); }; From 1ebf7c929d595387383c2bebeb14c9f2304c5a84 Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Thu, 23 Oct 2025 11:06:35 -0400 Subject: [PATCH 04/17] test user locally --- .../src/app/(auth)/(control)/login/page.tsx | 29 +++++++++++++++++++ packages/app/control/src/auth/config.ts | 17 ++++++++++- .../app/control/src/services/email/queue.ts | 5 ++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/app/control/src/app/(auth)/(control)/login/page.tsx b/packages/app/control/src/app/(auth)/(control)/login/page.tsx index f1a1effb4..26836d165 100644 --- a/packages/app/control/src/app/(auth)/(control)/login/page.tsx +++ b/packages/app/control/src/app/(auth)/(control)/login/page.tsx @@ -13,6 +13,9 @@ import { oauthProviders } from '@/auth/providers'; import { cn } from '@/lib/utils'; import type { Route } from 'next'; +import { env } from '@/env'; + +const IS_LOCAL_MODE = env.NODE_ENV === 'development'; export default async function SignInPage({ searchParams, @@ -120,6 +123,32 @@ export default async function SignInPage({ + {IS_LOCAL_MODE && ( + <> +
+ + dev only + +
+
{ + 'use server'; + await signIn('test-user-1', { + redirectTo: redirectTo || '/', + }); + }} + className="w-full" + > + +
+ + )} ); } diff --git a/packages/app/control/src/auth/config.ts b/packages/app/control/src/auth/config.ts index ab51f30d0..eab941c10 100644 --- a/packages/app/control/src/auth/config.ts +++ b/packages/app/control/src/auth/config.ts @@ -27,8 +27,23 @@ declare module 'next-auth/jwt' { const IS_TEST_MODE = env.INTEGRATION_TEST_MODE; +const IS_LOCAL_MODE = env.NODE_ENV === 'development'; + +// Determine which providers to use based on environment +const getProviders = () => { + if (IS_TEST_MODE) { + return testProviders; + } + + if (IS_LOCAL_MODE) { + return [...testProviders, ...oauthProviders]; + } + + return oauthProviders; +}; + export const authConfig = { - providers: IS_TEST_MODE ? testProviders : oauthProviders, + providers: getProviders(), // Only allow skipCSRFCheck in test mode skipCSRFCheck: IS_TEST_MODE ? skipCSRFCheck : undefined, pages: { diff --git a/packages/app/control/src/services/email/queue.ts b/packages/app/control/src/services/email/queue.ts index c278c98b3..6081066b1 100644 --- a/packages/app/control/src/services/email/queue.ts +++ b/packages/app/control/src/services/email/queue.ts @@ -27,6 +27,11 @@ export const emailJobSchema = z.discriminatedUnion('campaign', [ ]); export const queueJob = async (body: z.infer) => { + // Skip queueing in local development mode + if (env.NODE_ENV === 'development' || !env.RESEND_FLOW_CONTROL_KEY) { + return; + } + await queueClient.publishJSON({ url: `${env.NEXT_PUBLIC_APP_URL}/api/jobs`, body: { From f425d59fb2e426c592b9cc220bac1adddf9233f5 Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Thu, 23 Oct 2025 11:08:25 -0400 Subject: [PATCH 05/17] test user clean up --- packages/app/control/src/services/email/queue.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/app/control/src/services/email/queue.ts b/packages/app/control/src/services/email/queue.ts index d62f7a26f..6081066b1 100644 --- a/packages/app/control/src/services/email/queue.ts +++ b/packages/app/control/src/services/email/queue.ts @@ -38,13 +38,11 @@ export const queueJob = async (body: z.infer) => { type: 'email', job: body, }, - ...(env.RESEND_FLOW_CONTROL_KEY && { - flowControl: { - key: env.RESEND_FLOW_CONTROL_KEY, - rate: 2, - period: '1m', - }, - }), + flowControl: { + key: env.RESEND_FLOW_CONTROL_KEY, + rate: 2, + period: '1m', + }, }); }; From 379a75ffac847eba069e92d341305817931e907d Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Thu, 23 Oct 2025 11:17:28 -0400 Subject: [PATCH 06/17] cleaning up env --- packages/app/control/.env.example | 3 +- packages/app/control/README.md | 29 ++++++----------- .../src/app/(auth)/(control)/login/page.tsx | 2 +- packages/app/control/src/auth/providers.ts | 31 +++++++++++++++++++ 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/packages/app/control/.env.example b/packages/app/control/.env.example index 8dabb238d..fdde53e31 100644 --- a/packages/app/control/.env.example +++ b/packages/app/control/.env.example @@ -1,7 +1,8 @@ # ---------- # Environment # ---------- -VERCEL_ENV="local" + +SKIP_ENV_VALIDATION=1 # Remove in production! # ---------- # Application diff --git a/packages/app/control/README.md b/packages/app/control/README.md index c507343d7..446a27a46 100644 --- a/packages/app/control/README.md +++ b/packages/app/control/README.md @@ -73,39 +73,28 @@ A comprehensive Next.js application for managing Echo applications, API keys, an # Example: DATABASE_URL="postgresql://username:password@localhost:5469/echo_control" ``` -4. **Run database migrations**: +4. ** Create auth secret**: + + ```bash + pnpm dlx auth secret + ``` + +5. **Run database migrations**: ```bash npx prisma generate npx prisma db push ``` -5. **Start the development server**: +6. **Start the development server**: ```bash pnpm run dev ``` -6. **Open the application**: +7. **Open the application**: Visit [http://localhost:3000](http://localhost:3000) -## Environment Variables - -Create a `.env` file with the following variables: - -```env -# Database -DATABASE_URL="postgresql://username:password@localhost:5469/echo_control" - -# Stripe (Mocked) -STRIPE_SECRET_KEY="mock_stripe_secret_key" -STRIPE_PUBLISHABLE_KEY="mock_stripe_publishable_key" -STRIPE_WEBHOOK_SECRET="mock_webhook_secret" - -# Application -NEXTAUTH_URL="http://localhost:3000" -API_KEY_PREFIX="echo_" -``` ## Features Overview diff --git a/packages/app/control/src/app/(auth)/(control)/login/page.tsx b/packages/app/control/src/app/(auth)/(control)/login/page.tsx index 26836d165..5601a6b1d 100644 --- a/packages/app/control/src/app/(auth)/(control)/login/page.tsx +++ b/packages/app/control/src/app/(auth)/(control)/login/page.tsx @@ -133,7 +133,7 @@ export default async function SignInPage({
{ 'use server'; - await signIn('test-user-1', { + await signIn('local-user', { redirectTo: redirectTo || '/', }); }} diff --git a/packages/app/control/src/auth/providers.ts b/packages/app/control/src/auth/providers.ts index cbbd3dafb..302eb54aa 100644 --- a/packages/app/control/src/auth/providers.ts +++ b/packages/app/control/src/auth/providers.ts @@ -124,6 +124,37 @@ export const testProviders: Provider[] = [ image: 'http://echo.merit.systems/logo/light.svg', }); + return { + id: user.id, + name: user.name, + email: user.email, + image: user.image, + }; + }, + }), + Credentials({ + id: 'local-user', + name: 'Local User', + credentials: {}, + authorize: async () => { + const existingUser = await getUserByEmail('local@example.com'); + + if (existingUser) { + return { + id: existingUser.id, + name: existingUser.name, + email: existingUser.email, + image: existingUser.image, + }; + } + + const user = await createUser({ + id: 'ffffffff-ffff-ffff-ffff-ffffffffffff', + name: 'Local User', + email: 'local@example.com', + image: 'http://echo.merit.systems/logo/light.svg', + }); + return { id: user.id, name: user.name, From a2d0dde99a9456af26b7bda9b186f9706b76febd Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Thu, 23 Oct 2025 11:26:07 -0400 Subject: [PATCH 07/17] contributing.md --- CONTRIBUTING.md | 519 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 519 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..aed1a636c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,519 @@ +# Contributing to Echo + +Thank you for your interest in contributing to Echo! We're building user-pays AI infrastructure that helps developers monetize their AI applications without fronting costs. Every contribution helps make AI development more accessible and sustainable. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Bounties](#bounties) +- [Ways to Contribute](#ways-to-contribute) +- [Getting Started](#getting-started) +- [Development Workflow](#development-workflow) +- [Project Structure](#project-structure) +- [Coding Standards](#coding-standards) +- [Commit Message Guidelines](#commit-message-guidelines) +- [Pull Request Process](#pull-request-process) +- [Testing](#testing) +- [Documentation](#documentation) +- [Community and Support](#community-and-support) + +## Code of Conduct + +By participating in this project, you agree to be respectful, inclusive, and constructive. We aim to create a welcoming environment for all contributors regardless of background or experience level. + +## Bounties + +**Get paid to contribute to Echo!** + +We offer bounties for specific features, bug fixes, and improvements. This is a great way to get started with open source contributions while earning rewards for your work. + +[**View available bounties →**](https://terminal.merit.systems/Merit-Systems/echo/bounties) + +To claim a bounty: +1. Check the [bounties page](https://terminal.merit.systems/Merit-Systems/echo/bounties) for available tasks +2. Comment on the bounty to express interest +3. Submit your work via pull request +4. Receive payment once your PR is merged + +New bounties are added regularly, so check back often! + +## Ways to Contribute + +### 🐛 Report Bugs + +If you find a bug, please [open an issue](https://github.com/Merit-Systems/echo/issues/new) with: +- A clear, descriptive title +- Steps to reproduce the issue +- Expected vs. actual behavior +- Your environment (OS, Node version, browser, etc.) +- Screenshots or code snippets if applicable + +### 💡 Suggest Features + +Have an idea? We'd love to hear it! [Open a feature request](https://github.com/Merit-Systems/echo/issues/new) with: +- A clear description of the feature +- Use cases and examples +- Why this would benefit Echo users +- Any implementation ideas you might have + +### 📝 Improve Documentation + +Documentation improvements are always welcome! This includes: +- Fixing typos or unclear explanations +- Adding examples or tutorials +- Improving API documentation +- Translating documentation + +### 🔧 Submit Code Changes + +Ready to code? Check our [open issues](https://github.com/Merit-Systems/echo/issues) for ideas, or propose your own changes. + +## Getting Started + +### Prerequisites + +- **Node.js**: 18.0.0 or higher +- **pnpm**: 10.0.0 or higher +- **PostgreSQL**: Required for running Echo Control locally +- **Git**: For version control + +### Initial Setup + +1. **Fork and clone the repository** + + ```bash + git clone https://github.com/YOUR_USERNAME/echo.git + cd echo + ``` + +2. **Install dependencies** + + ```bash + pnpm install + ``` + +3. **Set up environment variables** + + For Echo Control: + ```bash + cd packages/app/control + cp .env.example .env + # Edit .env with your configuration + ``` + + For Echo Server: + ```bash + cd packages/app/server + cp .env.example .env + # Edit .env with your configuration + ``` + +4. **Set up the database** (for Echo Control) + + ```bash + cd packages/app/control + ./setup-db.sh + # Or manually: + npx prisma generate + npx prisma db push + ``` + +5. **Start development servers** + + From the root directory: + ```bash + pnpm dev + ``` + + This starts both Echo Control (localhost:3000) and Echo Server simultaneously. + +## Development Workflow + +### Creating a Branch + +Use descriptive branch names with prefixes: + +```bash +git checkout -b feature/add-anthropic-support +git checkout -b fix/balance-calculation-error +git checkout -b docs/improve-quickstart +git checkout -b refactor/simplify-auth-flow +``` + +### Making Changes + +1. Make your changes in focused, logical commits +2. Write or update tests for your changes +3. Ensure all tests pass: `pnpm test:all` +4. Run linting: `pnpm lint` +5. Check types: `pnpm type-check` +6. Format code: `pnpm format` + +### Testing Your Changes + +```bash +# Run all tests +pnpm test:all + +# Run unit tests only +pnpm test:unit + +# Run integration tests +pnpm test:integration + +# Test in a specific package +pnpm --filter @merit-systems/echo-react-sdk test +``` + +## Project Structure + +Echo is a monorepo organized as follows: + +``` +echo/ +├── packages/ +│ ├── app/ +│ │ ├── control/ # Echo Control Plane (Next.js app) +│ │ └── server/ # Echo Server (Express proxy) +│ ├── sdk/ +│ │ ├── ts/ # Core TypeScript SDK +│ │ ├── react/ # React SDK +│ │ ├── next/ # Next.js SDK +│ │ ├── aix402/ # AI SDK 402 payment protocol +│ │ └── auth-js-provider/# Auth.js provider +│ └── tests/ # Integration and smoke tests +├── templates/ # Starter templates +└── docs/ # Documentation +``` + +### Key Packages + +- **Echo Control** (`packages/app/control`): User-facing dashboard, authentication, billing +- **Echo Server** (`packages/app/server`): Proxy server for LLM requests with metering +- **Echo TS SDK** (`packages/sdk/ts`): Foundation for all framework-specific SDKs +- **Echo React SDK** (`packages/sdk/react`): React hooks and components +- **Echo Next SDK** (`packages/sdk/next`): Next.js App Router integration + +## Coding Standards + +### TypeScript + +- Use TypeScript for all new code +- Prefer explicit types over `any` +- Use interfaces for public APIs, types for internal structures +- Enable strict mode in `tsconfig.json` + +### Imports + +- **Always use absolute imports** with the `@` syntax, not relative imports +- Use kebab-case for TypeScript files +- Group imports: external packages → internal packages → local files + +```typescript +// ✅ Good +import { useState } from 'react'; +import { generateText } from 'ai'; + +import { useEchoAuth } from '@/hooks/use-echo-auth'; +import { Button } from '@/components/button'; + +// ❌ Bad +import { Button } from '../../../components/button'; +``` + +### React Components + +- Use functional components with hooks +- Prefer named exports for components +- Use TypeScript for prop types +- Follow kebab-case for component filenames + +```typescript +// user-avatar.tsx +export function UserAvatar({ user }: UserAvatarProps) { + // Implementation +} +``` + +### Naming Conventions + +- **Files**: kebab-case (`user-profile.tsx`, `api-client.ts`) +- **Components**: PascalCase (`UserProfile`, `ApiKeyList`) +- **Functions/Variables**: camelCase (`getUserBalance`, `apiKey`) +- **Constants**: SCREAMING_SNAKE_CASE (`API_KEY_PREFIX`, `MAX_RETRIES`) +- **Types/Interfaces**: PascalCase (`UserProfile`, `ApiResponse`) + +### Feature Flags + +When working with feature flags: + +- Use as few places as possible to reduce undefined behavior +- Store flag names in an enum (TypeScript) or const object (JavaScript) +- Use SCREAMING_SNAKE_CASE for flag names +- Gate flag-dependent code with validation checks + +```typescript +// ✅ Good +enum FeatureFlags { + ANTHROPIC_SUPPORT = 'anthropic_support', + USAGE_ANALYTICS = 'usage_analytics', +} + +if (flags[FeatureFlags.ANTHROPIC_SUPPORT] === true) { + // Feature-specific code +} +``` + +### Custom Properties (Analytics) + +- Store event/property names in enums or const objects when used in 2+ places +- Follow existing naming conventions (consult maintainers if unsure) +- Never change existing event names without discussion (breaks analytics) + +### Code Quality + +- Follow DRY (Don't Repeat Yourself) principles where appropriate +- Write self-documenting code with clear variable names +- Add comments for complex logic or non-obvious decisions +- Keep functions small and focused on a single responsibility +- Handle errors appropriately with meaningful messages + +## Commit Message Guidelines + +We follow [Conventional Commits](https://www.conventionalcommits.org/) for clear, semantic commit history. + +### Format + +``` +(): + + + +