From 4277f11c486e80e720c2510ec2cc4c77489394ab Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Tue, 14 Apr 2026 15:28:53 +0700 Subject: [PATCH] docs: add CLAUDE.md and project documentation Add CLAUDE.md for AI assistant context. Create four new docs: deployment-guide.md (full deploy flow + secret rotation + rollback), code-standards.md (formatting, naming, module conventions, testing), codebase-summary.md (tech stack, modules, data flows, external APIs), development-roadmap.md (completed phases + planned work). --- CLAUDE.md | 69 ++++++++++++++++++++ docs/code-standards.md | 90 ++++++++++++++++++++++++++ docs/codebase-summary.md | 79 +++++++++++++++++++++++ docs/deployment-guide.md | 123 ++++++++++++++++++++++++++++++++++++ docs/development-roadmap.md | 44 +++++++++++++ 5 files changed, 405 insertions(+) create mode 100644 CLAUDE.md create mode 100644 docs/code-standards.md create mode 100644 docs/codebase-summary.md create mode 100644 docs/deployment-guide.md create mode 100644 docs/development-roadmap.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6c513a6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +npm run dev # local dev server (wrangler dev) at http://localhost:8787 +npm run lint # biome check src tests scripts +npm run format # biome format --write +npm test # vitest run (all tests) +npx vitest run tests/modules/trading/format.test.js # single test file +npx vitest run -t "formats with dot" # single test by name +npm run deploy # wrangler deploy + register webhook/commands with Telegram +npm run register:dry # preview setWebhook + setMyCommands payloads without calling Telegram +``` + +## Architecture + +grammY Telegram bot on Cloudflare Workers. Modules are plug-n-play: each module is a folder under `src/modules/` that exports `{ name, commands[], init? }`. A single `MODULES` env var controls which modules are loaded. + +**Request flow:** `POST /webhook` → grammY validates secret header → `getBot(env)` (memoized per warm instance) → `installDispatcher` builds registry on first call → `bot.command(name, handler)` for every command → handler runs. + +**Key abstractions:** +- `src/modules/registry.js` — loads modules from static import map (`src/modules/index.js`), validates commands, detects name conflicts across all visibility levels, builds four maps (public/protected/private/all). Memoized via `getCurrentRegistry()`. +- `src/db/create-store.js` — wraps Cloudflare KV with auto-prefixed keys per module (`moduleName:key`). Modules never touch `env.KV` directly. +- `scripts/register.js` — post-deploy script that imports the same registry to derive public commands, then calls Telegram `setWebhook` + `setMyCommands`. Uses `stub-kv.js` to satisfy KV binding without real IO. + +**Three command visibilities:** public (in Telegram `/` menu + `/help`), protected (in `/help` only), private (hidden easter eggs). All three are registered via `bot.command()` — visibility controls discoverability, not access. + +## Adding a Module + +1. Create `src/modules//index.js` with default export `{ name, commands, init? }` +2. Add one line to `src/modules/index.js` static import map +3. Add `` to `MODULES` in `wrangler.toml` `[vars]` +4. Full guide: `docs/adding-a-module.md` + +## Module Contract + +```js +{ + name: "mymod", // must match folder + import map key + init: async ({ db, env }) => { ... }, // optional — db is prefixed KVStore + commands: [{ + name: "mycmd", // ^[a-z0-9_]{1,32}$, no leading slash + visibility: "public", // "public" | "protected" | "private" + description: "Does a thing", // required for all visibilities + handler: async (ctx) => { ... }, // grammY context + }], +} +``` + +Command names must be globally unique across ALL modules and visibilities. Conflicts throw at load time. + +## Testing + +Pure-logic unit tests only — no workerd, no Telegram fixtures. Tests use fakes from `tests/fakes/` (fake-kv-namespace, fake-bot, fake-modules) injected via parameters, not `vi.mock`. + +For modules that call `fetch` (like trading/prices), stub `global.fetch` with `vi.fn()` in tests. + +## Code Style + +Biome enforces: 2-space indent, double quotes, semicolons, trailing commas, 100-char line width, sorted imports. Run `npm run format` before committing. Keep files under 200 lines — split into focused submodules when approaching the limit. + +## Environment + +- Secrets (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_WEBHOOK_SECRET`): set via `wrangler secret put`, mirrored in `.env.deploy` (gitignored) for register script +- `.dev.vars`: local dev secrets (gitignored), copy from `.dev.vars.example` +- Node >=20.6 required (for `--env-file` flag) diff --git a/docs/code-standards.md b/docs/code-standards.md new file mode 100644 index 0000000..9bf157c --- /dev/null +++ b/docs/code-standards.md @@ -0,0 +1,90 @@ +# Code Standards + +## Language & Runtime + +- **JavaScript** (ES modules, `"type": "module"` in package.json) +- **No TypeScript** — JSDoc typedefs for type contracts (see `kv-store-interface.js`, `registry.js`) +- **Cloudflare Workers runtime** — Web APIs only, no Node.js built-ins, no `nodejs_compat` +- **grammY** for Telegram bot framework + +## Formatting (Biome) + +Enforced by `npm run lint` / `npm run format`: + +- 2-space indent +- Double quotes +- Semicolons: always +- Trailing commas: all +- Line width: 100 characters +- Imports: auto-sorted by Biome + +Run `npm run format` before committing. + +## File Organization + +- **Max 200 lines per code file.** Split into focused submodules when approaching the limit. +- Module code lives in `src/modules//` — one folder per module. +- Shared utilities in `src/util/`. +- DB layer in `src/db/`. +- Tests mirror source structure: `tests/modules//`, `tests/db/`, `tests/util/`. + +## Naming Conventions + +- **Files:** lowercase, hyphens for multi-word (`stats-handler.js`, `fake-kv-namespace.js`) +- **Directories:** lowercase, single word preferred (`trading/`, `util/`) +- **Functions/variables:** camelCase +- **Constants:** UPPER_SNAKE_CASE for frozen config objects (`SYMBOLS`, `CURRENCIES`) +- **Command names:** lowercase + digits + underscore, 1-32 chars, no leading slash + +## Module Conventions + +Every module default export must have: + +```js +export default { + name: "modname", // === folder name === import map key + commands: [...], // validated at load time + init: async ({ db, env }) => { ... }, // optional +}; +``` + +- Store module-level `db` reference in a closure variable, set during `init` +- Never access `env.KV` directly — always use the prefixed `db` from `init` +- Handlers receive grammY `ctx` — use `ctx.match` for command arguments, `ctx.from.id` for user identity +- Reply with `ctx.reply(text)` — plain text or Telegram HTML + +## Error Handling + +- **Load-time failures** (bad module, command conflicts, missing env): throw immediately — fail loud at deploy, not at runtime. +- **Handler-level errors** (API failures, bad user input): catch and reply with user-friendly message. Never crash the handler — grammY logs unhandled rejections but the user sees nothing. +- **KV failures**: best-effort writes (wrap in try/catch), guard reads with `?.` and null coalescing. +- `getJSON` swallows corrupt JSON and returns null — modules must handle null gracefully. + +## Testing + +- **Framework:** Vitest +- **Style:** Pure-logic unit tests. No workerd, no Telegram integration, no network calls. +- **Fakes:** `tests/fakes/` provides `fake-kv-namespace.js`, `fake-bot.js`, `fake-modules.js`. Inject via parameters, not `vi.mock`. +- **External APIs:** Stub `global.fetch` with `vi.fn()` returning canned responses. +- **Coverage:** `npx vitest run --coverage` (v8 provider, text + HTML output). + +## Commit Messages + +Conventional commits: + +``` +feat: add paper trading module +fix: handle null price in sell handler +docs: update architecture for trading module +refactor: extract stats handler to separate file +test: add portfolio edge case tests +``` + +## Security + +- Secrets live in Cloudflare Workers secrets (runtime) and `.env.deploy` (local, gitignored). Never commit secrets. +- `.dev.vars` is gitignored — local dev only. +- grammY validates webhook secret on every update. No manual header parsing. +- Module KV prefixing is a code-review boundary, not a cryptographic one. +- Private commands are discoverability control, not access control. +- HTML output in `/help` uses `escapeHtml` to prevent injection. diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md new file mode 100644 index 0000000..42afd72 --- /dev/null +++ b/docs/codebase-summary.md @@ -0,0 +1,79 @@ +# Codebase Summary + +## Overview + +Telegram bot on Cloudflare Workers with a plug-n-play module system. grammY handles Telegram API; modules register commands with three visibility levels. Data stored in Cloudflare KV behind a prefixed `KVStore` interface. + +## Tech Stack + +| Layer | Technology | +|-------|-----------| +| Runtime | Cloudflare Workers (V8 isolates) | +| Bot framework | grammY 1.x | +| Storage | Cloudflare KV | +| Linter/Formatter | Biome | +| Tests | Vitest | +| Deploy | Wrangler CLI | + +## Active Modules + +| Module | Status | Commands | Description | +|--------|--------|----------|-------------| +| `util` | Complete | `/info`, `/help` | Bot info and command help renderer | +| `trading` | Complete | `/trade_topup`, `/trade_buy`, `/trade_sell`, `/trade_convert`, `/trade_stats` | Paper trading with crypto, VN stocks, forex, gold | +| `misc` | Stub | `/ping`, `/mstats`, `/fortytwo` | Health check + DB demo | +| `wordle` | Stub | `/wordle`, `/wstats`, `/konami` | Placeholder for word game | +| `loldle` | Stub | `/loldle`, `/ggwp` | Placeholder for LoL game | + +## Key Data Flows + +### Command Processing +``` +Telegram update → POST /webhook → grammY secret validation +→ getBot(env) → dispatcher routes /cmd → module handler +→ handler reads/writes KV via db.getJSON/putJSON +→ ctx.reply() → response to Telegram +``` + +### Trading Module Price Fetch +``` +User sends /trade_buy → handler calls getPrice(db, symbol) +→ getPrices(db) checks KV cache (key: "prices:latest") +→ if stale (>60s): fetch CoinGecko + TCBS + ER-API in parallel +→ merge results, cache in KV → return price in VND +``` + +### Deploy Pipeline +``` +npm run deploy → wrangler deploy (upload to CF) +→ scripts/register.js → buildRegistry with stub KV +→ POST setWebhook + POST setMyCommands to Telegram API +``` + +## External Dependencies + +| Dependency | Purpose | Version | +|-----------|---------|---------| +| `grammy` | Telegram Bot API framework | ^1.30.0 | +| `@biomejs/biome` | Linting + formatting (dev) | ^1.9.0 | +| `vitest` | Test runner (dev) | ^2.1.0 | +| `wrangler` | Cloudflare Workers CLI (dev) | ^3.90.0 | + +## External APIs (Trading Module) + +| API | Purpose | Auth | Rate Limit | +|-----|---------|------|-----------| +| CoinGecko `/api/v3/simple/price` | Crypto + gold prices in VND | None | 30 calls/min (free) | +| TCBS `/stock-insight/v1/stock/bars-long-term` | Vietnam stock close prices | None | Unofficial | +| open.er-api.com `/v6/latest/USD` | USD/VND forex rate | None | 1,500/month (free) | + +## Test Coverage + +110 tests across 11 test files: + +| Area | Tests | What's Covered | +|------|-------|---------------| +| DB layer | 19 | KV store, prefixing, JSON helpers, pagination | +| Module framework | 33 | Registry, dispatcher, validators, help renderer | +| Utilities | 4 | HTML escaping | +| Trading module | 54 | Symbols, formatters, portfolio CRUD, all 5 command handlers | diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md new file mode 100644 index 0000000..1765e3b --- /dev/null +++ b/docs/deployment-guide.md @@ -0,0 +1,123 @@ +# Deployment Guide + +## Prerequisites + +- Node.js ≥ 20.6 +- Cloudflare account with Workers + KV enabled +- Telegram bot token from [@BotFather](https://t.me/BotFather) +- `wrangler` CLI authenticated: `npx wrangler login` + +## Environment Setup + +### 1. Cloudflare KV Namespaces + +```bash +npx wrangler kv namespace create miti99bot-kv +npx wrangler kv namespace create miti99bot-kv --preview +``` + +Paste returned IDs into `wrangler.toml`: + +```toml +[[kv_namespaces]] +binding = "KV" +id = "" +preview_id = "" +``` + +### 2. Worker Secrets + +```bash +npx wrangler secret put TELEGRAM_BOT_TOKEN +npx wrangler secret put TELEGRAM_WEBHOOK_SECRET +``` + +`TELEGRAM_WEBHOOK_SECRET` — any high-entropy string (e.g. `openssl rand -hex 32`). grammY validates it on every webhook update via `X-Telegram-Bot-Api-Secret-Token`. + +### 3. Local Dev Config + +```bash +cp .dev.vars.example .dev.vars # for wrangler dev +cp .env.deploy.example .env.deploy # for register script +``` + +Both are gitignored. Fill in matching token + secret values. + +`.env.deploy` also needs: +- `WORKER_URL` — the `*.workers.dev` URL (known after first deploy) +- `MODULES` — comma-separated, must match `wrangler.toml` `[vars].MODULES` + +## Deploy + +### First Time + +```bash +npx wrangler deploy # learn the *.workers.dev URL +# paste URL into .env.deploy as WORKER_URL +npm run register:dry # preview payloads +npm run deploy # deploy + register webhook + commands +``` + +### Subsequent Deploys + +```bash +npm run deploy +``` + +This runs `wrangler deploy` then `scripts/register.js` (setWebhook + setMyCommands). + +### What the Register Script Does + +`scripts/register.js` imports the same registry the Worker uses, builds it with a stub KV to derive public commands, then calls two Telegram APIs: + +1. `setWebhook` — points Telegram at `WORKER_URL/webhook` with the secret token +2. `setMyCommands` — pushes public command list to Telegram's `/` menu + +### Dry Run + +```bash +npm run register:dry +``` + +Prints both payloads (webhook secret redacted) without calling Telegram. + +## Secret Rotation + +1. Generate new secret: `openssl rand -hex 32` +2. Update Cloudflare: `npx wrangler secret put TELEGRAM_WEBHOOK_SECRET` +3. Update `.env.deploy` with same value +4. Redeploy: `npm run deploy` (register step re-calls setWebhook with new secret) + +Both values MUST match — mismatch causes 401 on every webhook. + +## Adding/Removing Modules + +When changing the active module list: + +1. Update `MODULES` in `wrangler.toml` `[vars]` +2. Update `MODULES` in `.env.deploy` +3. If adding: ensure module exists in `src/modules/index.js` import map +4. `npm run deploy` + +Removing a module from `MODULES` makes it inert — its KV data remains but nothing loads it. + +## Rollback + +Cloudflare Workers supports instant rollback via the dashboard or: + +```bash +npx wrangler rollback +``` + +To disable a specific module without rollback, remove its name from `MODULES` and redeploy. + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| 401 on all webhooks | Secret mismatch between CF secret and `.env.deploy` | Re-set both to same value, redeploy | +| Bot doesn't respond to commands | `MODULES` missing the module name | Add to both `wrangler.toml` and `.env.deploy` | +| `command conflict` at deploy | Two modules register same command name | Rename one command | +| `missing env: X` from register | `.env.deploy` incomplete | Add missing variable | +| `--env-file` not recognized | Node < 20.6 | Upgrade Node | +| `/help` missing a module | Module has no public or protected commands | Add at least one non-private command | diff --git a/docs/development-roadmap.md b/docs/development-roadmap.md new file mode 100644 index 0000000..b23d2a6 --- /dev/null +++ b/docs/development-roadmap.md @@ -0,0 +1,44 @@ +# Development Roadmap + +## Current State: v0.1.0 + +Core framework complete. One fully implemented module (trading), three stubs (wordle, loldle, misc). 110 passing tests. Deployed to Cloudflare Workers. + +## Completed + +### Phase 1: Bot Framework (v0.1.0) +- [x] Cloudflare Workers entry point with webhook validation +- [x] grammY bot factory with memoized cold-start handling +- [x] Plug-n-play module system with static import map +- [x] Three-tier command visibility (public/protected/private) +- [x] Unified conflict detection across all modules +- [x] KVStore interface with auto-prefixed per-module namespacing +- [x] Post-deploy register script (setWebhook + setMyCommands) +- [x] `/info` and `/help` commands +- [x] 56 unit tests covering all framework seams + +### Phase 2: Trading Module (v0.1.0) +- [x] Paper trading with 5 commands (topup/buy/sell/convert/stats) +- [x] Real-time prices: CoinGecko (crypto+gold), TCBS (VN stocks), ER-API (forex) +- [x] 60-second price caching in KV with stale fallback +- [x] Per-user portfolio storage with VND as base currency +- [x] P&L tracking (total invested vs current portfolio value) +- [x] 54 unit tests for symbols, formatters, portfolio CRUD, handlers + +## Planned + +### Phase 3: Game Modules +- [ ] Wordle implementation (currently stub) +- [ ] Loldle implementation (currently stub) +- [ ] Game state persistence in KV + +### Phase 4: Infrastructure +- [ ] CI pipeline (GitHub Actions: lint + test on PR) +- [ ] KV namespace IDs in wrangler.toml (currently REPLACE_ME placeholders) +- [ ] Per-module rate limiting consideration + +### Future Considerations +- Internationalization (per-module if needed) +- More trading assets (expand symbol registry) +- Trading history / transaction log +- Group chat features