npm package: @logscore/botcha
Description: LLM verification through logic-based challenges
Author: logscore
Latest version: 1.1.1

# Botcha

AI verification through logic-based challenges. Botcha generates complex ownership-transfer puzzles that are trivial for LLMs to solve but difficult for humans or scripts to solve in time.

## Installation

```bash
npm install @logscore/botcha
```

## LLM Quick Start

To get your coding gent up to speed as fast as possible, we recommend asking it to read `node_modules/@logscore/botcha/skills.md` and follow the instructions.

### Optional Storage Adapters

You can use any storage adapter that implements the `StorageAdapter` interface. By default, Botcha uses an in-memory storage adapter. FOr production, we recommend using Redis or PostgreSQL.

## Quick Start

```typescript
import { createBotcha } from "@logscore/botcha";

const botcha = createBotcha();

// Create a challenge
const { id, text } = await botcha.createChallenge();
console.log(text);
// Output: CUSTODY CHAIN: SILVER LOCKET
// Track possession of the silver locket through the following sequence.
//
// 1. Morgan left the silver locket in a secure container.
// 2. Quinn discovered the missing silver locket under a desk.
// ... (50 events)
//
// QUESTION: Who possesses the silver locket at the conclusion of event 23?

// Verify the answer
const isHuman = await botcha.verifyChallenge(id, "Riley");
console.log(isHuman); // true or false
```

## Configuration

### Basic Configuration

```typescript
import { createBotcha } from "@logscore/botcha";

const botcha = createBotcha({
  // Challenge generation options
  challengeConfig: {
    people: ["Alex", "Jordan", "Taylor"], // Custom names (default: 7 names)
    objects: ["red key", "blue orb"], // Custom objects (default: 4 objects)
    transactionCount: 30, // Number of events (default: 50)
    enableDistractors: true, // Add distractor notes (default: true)
  },

  // Session management
  expirationMs: 60000, // 60 seconds (default: 30 seconds)

  // Background sweeper (optional)
  sweeperIntervalMs: 60000, // Cleanup every 60 seconds
});
```

### Storage Adapters

#### In-Memory (Default)

```typescript
import { createBotcha } from "@logscore/botcha";

const botcha = createBotcha(); // Uses memory storage by default
```

#### Redis

```typescript
import { createBotcha, RedisStorage } from "@logscore/botcha";

const botcha = createBotcha({
  storage: new RedisStorage({
    url: "redis://localhost:6379",
    keyPrefix: "myapp:botcha:",
  }),
});
```

#### PostgreSQL

```typescript
import { createBotcha, PostgresStorage } from "@logscore/botcha";

const botcha = createBotcha({
  storage: new PostgresStorage({
    connectionString: process.env.DATABASE_URL,
  }),
});
```

#### SQLite

```typescript
import { createBotcha, SQLiteStorage } from "@logscore/botcha";

const botcha = createBotcha({
  storage: new SQLiteStorage({
    path: "./botcha.db", // Or ':memory:' for in-memory
  }),
});
```

#### MySQL

```typescript
import { createBotcha, MySQLStorage } from "@logscore/botcha";

const botcha = createBotcha({
  storage: new MySQLStorage({
    host: "localhost",
    port: 3306,
    user: "root",
    password: "password",
    database: "myapp",
  }),
});
```

### Custom Storage Adapter

```typescript
import { createBotcha, type StorageAdapter, type Session } from "@logscore/botcha";

class DynamoDBStorage implements StorageAdapter {
  async get(id: string): Promise<Session | null> {
    // Your implementation
  }

  async set(id: string, session: Session): Promise<void> {
    // Your implementation
  }

  async delete(id: string): Promise<boolean> {
    // Your implementation
  }

  async cleanupExpired(): Promise<number> {
    // Your implementation
  }
}

const botcha = createBotcha({
  storage: new DynamoDBStorage(),
});
```

## Error Handling

```typescript
import {
  Botcha,
  ChallengeNotFoundError,
  ChallengeExpiredError,
  InvalidAnswerError,
} from "@logscore/botcha";

const botcha = createBotcha();

try {
  const isHuman = await botcha.verifyChallenge(id, answer);
} catch (error) {
  if (error instanceof ChallengeNotFoundError) {
    // Challenge doesn't exist or already used
  } else if (error instanceof ChallengeExpiredError) {
    // Challenge timed out
  } else if (error instanceof InvalidAnswerError) {
    // Wrong answer
  }
}
```

## Framework Examples

### Express.js

```typescript
import express from "express";
import { createBotcha } from "@logscore/botcha";

const app = express();
const botcha = createBotcha();

app.post("/challenge", async (req, res) => {
  const { id, text } = await botcha.createChallenge();
  res.json({ id, text });
});

app.post("/verify", async (req, res) => {
  const { id, answer } = req.body;

  try {
    const isHuman = await botcha.verifyChallenge(id, answer);
    res.json({ isHuman });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});
```

### Next.js API Route

```typescript
import { createBotcha } from "@logscore/botcha";

const botcha = createBotcha();

export async function POST() {
  const { id, text } = await botcha.createChallenge();
  return Response.json({ id, text });
}
```

### Hono

```typescript
import { Hono } from "hono";
import { createBotcha } from "@logscore/botcha";

const app = new Hono();
const botcha = createBotcha();

app.post("/challenge", async (c) => {
  const { id, text } = await botcha.createChallenge();
  return c.json({ id, text });
});
```

## Cleanup Strategies

### Lazy Cleanup (Default)

Expired sessions are removed only when accessed. Zero background overhead.

```typescript
const botcha = createBotcha(); // Lazy cleanup only
```

### Background Sweeper

Periodic cleanup for high-traffic applications.

```typescript
const botcha = createBotcha({
  sweeperIntervalMs: 60000, // Cleanup every 60 seconds
});

// Stop sweeper when shutting down
botcha.stopSweeper();
```

### Manual Cleanup

```typescript
const deleted = await botcha.cleanupExpired();
console.log(`Cleaned up ${deleted} expired sessions`);
```

## Security

- **Timing-safe comparison:** Uses `crypto.timingSafeEqual` to prevent timing attacks
- **Short-lived sessions:** 30-second default expiration prevents replay attacks
- **Cryptographic RNG:** SHA-256 based deterministic RNG
- **One-time use:** Sessions are deleted after successful verification

## Database Schema

### PostgreSQL/SQLite/MySQL

```sql
CREATE TABLE botcha_sessions (
  id VARCHAR(21) PRIMARY KEY,
  answer VARCHAR(255) NOT NULL,
  seed VARCHAR(255) NOT NULL,
  challenge_type VARCHAR(50) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  expires_at TIMESTAMP NOT NULL
);

CREATE INDEX idx_expires ON botcha_sessions(expires_at);
```

### Redis

Uses hash data structure with automatic TTL:

```
HSET botcha:session:<id> answer <answer> seed <seed> challenge_type <type> expires_at <timestamp>
EXPIRE botcha:session:<id> <ttl_seconds>
```

## API Reference

### Botcha Class

#### Constructor

```typescript
createBotcha(config?: BotchaConfig)
```

#### Methods

- `createChallenge(): Promise<ChallengeResponse>` - Create a new challenge
- `verifyChallenge(id: string, answer: string): Promise<boolean>` - Verify an answer
- `getSession(id: string): Promise<Session | null>` - Get session info
- `deleteChallenge(id: string): Promise<boolean>` - Delete a challenge
- `cleanupExpired(): Promise<number>` - Manually cleanup expired sessions
- `stopSweeper(): void` - Stop background sweeper

### Types

```typescript
interface BotchaConfig {
  challengeType?: "ownership";
  challengeConfig?: OwnershipConfig;
  storage?: StorageAdapter;
  expirationMs?: number;
  sweeperIntervalMs?: number;
}

interface OwnershipConfig {
  people?: string[];
  objects?: string[];
  transactionCount?: number;
  questionEventMin?: number;
  questionEventMax?: number;
  enableDistractors?: boolean;
}

interface ChallengeResponse {
  id: string;
  text: string;
}

interface Session {
  id: string;
  answer: string;
  seed: string;
  challengeType: string;
  expiresAt: number;
}
```

## License

MIT