npm package: @puzzle-section/sdk
Description: TypeScript SDK for the Puzzle Section Platform API
Author: Industrial Assets
Latest version: 1.0.4

# @puzzle-section/sdk

Official TypeScript SDK for the Puzzle Section Platform API.

The SDK provides access to:

- **Puzzle content** — daily puzzles, by type, by ID, solution validation
- **Admin tools** — puzzle and variant management, content moderation
- **Health monitoring** — API status and connectivity checks

User management and progress tracking are tenant responsibilities. Implement your own backend for user-specific features.

## Installation

```bash
npm install @puzzle-section/sdk
# or
yarn add @puzzle-section/sdk
# or
pnpm add @puzzle-section/sdk
```

**Requirements:** Node.js 18+, TypeScript 5.0+ (recommended), or any modern browser with ES2020+ support.

## Quick Start

```typescript
import { PuzzleSectionClient } from "@puzzle-section/sdk";

const client = new PuzzleSectionClient({
  apiKey: "ps_live_xxxxxxxxxxxx",
});

const { data: puzzles } = await client.puzzles.getDaily();
```

## Configuration

```typescript
const client = new PuzzleSectionClient({
  apiKey: "ps_live_xxxxxxxxxxxx", // Required — tenant API key
  baseUrl: "https://api.puzzlesection.app", // Optional — default shown
  timeout: 30000, // Optional — request timeout in ms
  retryCount: 3, // Optional — max retries on 5xx / 429
  fetch: customFetch, // Optional — custom fetch implementation
});
```

| Option       | Type         | Default                         | Description                                                |
| ------------ | ------------ | ------------------------------- | ---------------------------------------------------------- |
| `apiKey`     | string       | —                               | Tenant API key (required). Sent as `X-API-Key`.            |
| `baseUrl`    | string       | `https://api.puzzlesection.app` | API base URL. All requests go to `{baseUrl}/api/v1{path}`. |
| `timeout`    | number       | `30000`                         | Request timeout in milliseconds.                           |
| `retryCount` | number       | `3`                             | Max retries for 5xx and 429 responses.                     |
| `fetch`      | typeof fetch | `globalThis.fetch`              | Custom fetch for environments without native fetch.        |

## Response Envelope

Every SDK method returns `Promise<ResponseWithRateLimit<T>>`:

```typescript
interface ResponseWithRateLimit<T> {
  data: T;
  rateLimit: RateLimitInfo;
}

interface RateLimitInfo {
  limit: number; // Max requests per window
  remaining: number; // Requests remaining
  reset: number; // Unix timestamp when window resets
}

const { data, rateLimit } = await client.puzzles.getDaily();
console.log(rateLimit.remaining); // 999
```

## API Reference

### client.puzzles

```typescript
// All daily puzzles
const { data } = await client.puzzles.getDaily();
// data: Puzzle[]

// Filtered by type, difficulty, and date
const { data } = await client.puzzles.getDaily({
  date: '2026-03-05',               // YYYY-MM-DD (default: today)
  types: ['sudoku', 'kakuro'],      // PuzzleType[]
  difficulties: ['medium', 'hard'], // PuzzleDifficulty[]
});

// Fetch a single puzzle by UUID
const { data: puzzle } = await client.puzzles.getById('550e8400-e29b-...');

// Paginated list by type
const { data } = await client.puzzles.getByType('sudoku', {
  difficulty: 'hard',
  limit: 10,
  page: 1,
});
// data.data: Puzzle[], data.pagination: { page, limit, total, totalPages }

// Specific puzzle for a date and type
const { data } = await client.puzzles.getByDate('2026-03-05', 'sudoku', 'medium');

// All available puzzle types
const { data: types } = await client.puzzles.getTypes();

// Validate a solution
const { data } = await client.puzzles.validateSolution(
  '550e8400-...',
  { grid: [[5, 3, 4, 6, 7, 8, 9, 1, 2], ...] }
);
// data: { valid: boolean, errors?: string[] }
```

### client.health

```typescript
// Full health check
const { data } = await client.health.check();
// data: {
//   status: 'healthy' | 'degraded' | 'unhealthy',
//   version: '1.0.0',
//   timestamp: '2026-03-05T12:00:00Z',
//   checks: { database: 'up' | 'down', redis: 'up' | 'down' }
// }

// Simple connectivity ping
const { data } = await client.health.ping();
// data: { pong: true, timestamp: '2026-03-05T12:00:00Z' }
```

### client.admin

Admin methods are scoped to your tenant and are intended for puzzle content management — typically used by internal tools and the dashboard, not end-user applications.

```typescript
// List puzzles with filters
const { data } = await client.admin.listPuzzles({
  type: 'nonogram',
  status: 'draft',
  search: 'cat',
  page: 1,
  pageSize: 20,
});
// data: PaginatedResponse<AdminPuzzle>

// Create a puzzle
const { data: puzzle } = await client.admin.createPuzzle({
  title: 'Cat Nonogram',
  type: 'nonogram',
});

// Update puzzle metadata
const { data: updated } = await client.admin.updatePuzzle('puzzle-id', {
  title: 'Updated Title',
  status: 'published',
  event_tags: ['spring-2026'],
});

// Get a single puzzle
const { data: puzzle } = await client.admin.getPuzzle('puzzle-id');

// Lifecycle
await client.admin.publishPuzzle('puzzle-id');
await client.admin.unpublishPuzzle('puzzle-id');
await client.admin.deletePuzzle('puzzle-id');            // soft delete
await client.admin.restorePuzzle('puzzle-id');           // restore from bin
await client.admin.permanentlyDeletePuzzle('puzzle-id');
await client.admin.removeFromLibrary('puzzle-id');       // deactivate all variants

// List deleted puzzles
const { data } = await client.admin.listDeletedPuzzles();

// Create or update a puzzle variant
const { data: variant } = await client.admin.upsertVariant('puzzle-id', {
  difficulty: 'medium',
  enabled: true,
  width: 15,
  height: 15,
  grid_data: [[0, 1, 1, ...], ...],
  color_grid: [[0, 1, 2, ...], ...], // for color nonograms
  palette: ['#000', '#ff0000', '#0000ff'],
});

// Delete a variant
await client.admin.deleteVariant('puzzle-id', 'variant-id');

// Evaluate nonogram fun factor
const { data: result } = await client.admin.evaluateFunFactor({
  grid_data: [[0, 1, 1, ...], ...],
  difficulty: 'medium',
  width: 15,
  height: 15,
});
// result.funScore, result.meetsThreshold, result.recommendation, ...

// Filter an offensive word from a wordsearch puzzle
await client.admin.filterWordFromPuzzle('puzzle-id', {
  word: 'offensive',
  reason: 'inappropriate',
  addedByName: 'Content Moderator',
  addedByEmail: 'mod@example.com',
});
```

## Error Handling

The SDK throws typed error classes that extend `ApiError`. Use `instanceof` to branch:

```typescript
import {
  PuzzleSectionClient,
  ApiError,
  AuthenticationError,
  RateLimitError,
  NotFoundError,
  ValidationError,
  ServerError,
} from "@puzzle-section/sdk";

try {
  const { data } = await client.puzzles.getById("nonexistent");
} catch (error) {
  if (error instanceof RateLimitError) {
    // 429 — SDK retries automatically; this fires when retries are exhausted
    console.log(`Retry after ${error.retryAfter}s`);
    console.log(`Limit: ${error.limit}, Remaining: ${error.remaining}`);
  } else if (error instanceof AuthenticationError) {
    // 401 — invalid or missing API key
    console.error("Check your API key configuration");
  } else if (error instanceof NotFoundError) {
    // 404 — resource does not exist
    console.log(error.message);
  } else if (error instanceof ValidationError) {
    // 400 — request validation failed
    console.log(error.details); // Record<string, string[]>
  } else if (error instanceof ServerError) {
    // 500–504 — thrown after retries are exhausted
    console.error("Server error");
  } else if (error instanceof ApiError) {
    // Catch-all for any other API error
    console.error(
      `[${error.code}] ${error.message} (HTTP ${error.statusCode})`,
    );
  }
}
```

| Class                 | HTTP Status | Extra Properties                            |
| --------------------- | ----------- | ------------------------------------------- |
| `ApiError`            | any         | `message`, `code`, `statusCode`, `details?` |
| `AuthenticationError` | 401         | —                                           |
| `ValidationError`     | 400         | `details: Record<string, string[]>`         |
| `NotFoundError`       | 404         | —                                           |
| `RateLimitError`      | 429         | `retryAfter`, `limit`, `remaining`, `reset` |
| `ServerError`         | 500–504     | —                                           |

**Retry behaviour:**

- **5xx errors** are retried up to `retryCount` times with exponential backoff (2<sup>attempt</sup> seconds).
- **429 errors** are retried using the `Retry-After` header value when present.
- **4xx errors** (except 429) are not retried.
- **Timeouts** throw `ApiError` with code `TIMEOUT` (status 408).
- **Network failures** throw `ApiError` with code `NETWORK_ERROR` (status 0).

## TypeScript Types

All types are exported from the package:

```typescript
import type {
  // Puzzle types
  Puzzle,
  PuzzleType, // 'sudoku' | 'wordsearch' | 'crossword' | ... (14 types)
  PuzzleDifficulty, // 'easy' | 'medium' | 'hard' | 'expert'
  PuzzleData,
  PuzzleSolution,

  // Response types
  RateLimitInfo,
  HealthStatus,

  // Admin types
  AdminPuzzle,
  AdminPuzzleVariant,
  AdminPuzzleType,
  AdminPuzzleStatus,
  CreateAdminPuzzleRequest,
  UpdateAdminPuzzleRequest,
  UpsertPuzzleVariantRequest,
  FunFactorResult,

  // Config
  ClientConfig,
} from "@puzzle-section/sdk";
```

## Available Puzzle Types

- `sudoku` — Classic 9×9 Sudoku
- `kakuro` — Cross-sum puzzles
- `crossmath` — Arithmetic crossword
- `slitherlink` — Loop-drawing puzzle
- `hashi` — Bridge-building puzzle
- `breadcrumb` — Path-following puzzle
- `crossword` — Word crossword
- `wordsearch` — Find hidden words
- `wordladder` — Transform words step by step
- `wordpath` — Connect letters into a path
- `nonogram` — Picture logic (black and white)
- `colornonogram` — Picture logic (colour)
- `mosaic` — Fill-in picture puzzle
- `picturepath` — Draw a path through a picture

## Browser and Module Support

```typescript
// ES Modules (browser / Node.js)
import { PuzzleSectionClient } from "@puzzle-section/sdk";

// CommonJS (Node.js)
const { PuzzleSectionClient } = require("@puzzle-section/sdk");
```

## License

Licensed under the Apache License, Version 2.0. See LICENSE for details.

This SDK is developed in a private codebase and published as a package for public use. Official updates and releases are provided through this package.