mirror of
https://github.com/tiennm99/claude-status-webhook.git
synced 2026-04-17 11:20:30 +00:00
fix: security and robustness improvements, add project docs
- Hash inputs in timingSafeEqual to prevent length leak side-channel - Add quote escaping to escapeHtml for defense in depth - Normalize chatId to Number in parseKvKey for type consistency - Log Retry-After header on 429 rate limit responses - Slim README to focused overview, move details to docs/ - Add docs/: system-architecture, setup-guide, feature-decisions - Add documentation section and README guidelines to CLAUDE.md
This commit is contained in:
13
CLAUDE.md
13
CLAUDE.md
@@ -66,3 +66,16 @@ Bot stores `message_thread_id` from the topic where `/start` was sent. Notificat
|
||||
|
||||
- `claude_status` — KV namespace
|
||||
- `claude-status` — Queue producer/consumer (batch size 30, max retries 3)
|
||||
|
||||
## Documentation
|
||||
|
||||
Detailed docs live in `docs/`:
|
||||
- `docs/setup-guide.md` — Prerequisites, deployment, local dev
|
||||
- `docs/system-architecture.md` — Entry points, data flow, KV schema, queue, security
|
||||
- `docs/feature-decisions.md` — Evaluated features and rationale for decisions
|
||||
|
||||
## README Guidelines
|
||||
|
||||
Keep `README.md` clean and focused: project intro, features, commands, quick start, and links to docs.
|
||||
Move detailed setup, architecture, and decision records to `docs/`. Do not bloat the README with
|
||||
step-by-step instructions or implementation details.
|
||||
|
||||
143
README.md
143
README.md
@@ -1,17 +1,17 @@
|
||||
# claude-status-webhook
|
||||
|
||||
Telegram bot that forwards [Claude Status](https://status.claude.com/) updates to subscribed users. Whenever an incident or component status change occurs on Claude's status page, subscribers receive a Telegram notification instantly.
|
||||
Telegram bot that forwards [Claude Status](https://status.claude.com/) updates to subscribed users. Instant notifications when Claude goes down — incidents, component changes, and resolutions.
|
||||
|
||||
Hosted on [Cloudflare Workers](https://workers.cloudflare.com/) with KV for storage and Queues for reliable message fan-out. Fully serverless, fully free tier.
|
||||
Fully serverless on [Cloudflare Workers](https://workers.cloudflare.com/) (free tier). Zero infrastructure to manage.
|
||||
|
||||
## Features
|
||||
|
||||
- **Incident notifications** — new incidents, updates, and resolutions with impact severity
|
||||
- **Real-time incident alerts** — new incidents, updates, and resolutions with impact severity
|
||||
- **Component status changes** — e.g., API goes from Operational → Degraded Performance
|
||||
- **Per-user subscription preferences** — subscribe to incidents only, components only, or both
|
||||
- **Component-specific filtering** — subscribe to specific components (e.g., API only)
|
||||
- **Supergroup topic support** — send `/start` in a specific topic and notifications go to that topic
|
||||
- **On-demand status check** — `/status` fetches live data from status.claude.com
|
||||
- **Subscription preferences** — incidents only, components only, or both
|
||||
- **Component filtering** — subscribe to specific components (e.g., API only)
|
||||
- **Supergroup topic support** — notifications routed to the topic where `/start` was sent
|
||||
- **Live status check** — `/status` fetches current data from status.claude.com
|
||||
- **Self-healing** — automatically removes subscribers who block the bot
|
||||
|
||||
## Bot Commands
|
||||
@@ -19,28 +19,15 @@ Hosted on [Cloudflare Workers](https://workers.cloudflare.com/) with KV for stor
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/help` | Detailed command guide with examples |
|
||||
| `/start` | Subscribe to notifications (default: incidents + components) |
|
||||
| `/start` | Subscribe to notifications |
|
||||
| `/stop` | Unsubscribe from notifications |
|
||||
| `/status` | Show current status with overall indicator and all components |
|
||||
| `/status <name>` | Show status of a specific component (fuzzy match) |
|
||||
| `/subscribe incident` | Receive incident notifications only |
|
||||
| `/subscribe component` | Receive component update notifications only |
|
||||
| `/subscribe component <name>` | Filter to a specific component (e.g., `/subscribe component api`) |
|
||||
| `/subscribe component all` | Clear component filter (receive all) |
|
||||
| `/subscribe all` | Receive both incidents and components (default) |
|
||||
| `/history` | Show 5 most recent incidents with impact and links |
|
||||
| `/history <count>` | Show up to 10 recent incidents |
|
||||
| `/uptime` | Component health overview with last status change time |
|
||||
| `/status [name]` | Current system status (optionally for a specific component) |
|
||||
| `/subscribe <type>` | Set preferences: `incident`, `component`, or `all` |
|
||||
| `/subscribe component <name>` | Filter to a specific component |
|
||||
| `/history [count]` | Recent incidents (default 5, max 10) |
|
||||
| `/uptime` | Component health overview |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/) (v18+)
|
||||
- A [Cloudflare account](https://dash.cloudflare.com/sign-up) (free tier is sufficient)
|
||||
- A [Telegram bot token](https://core.telegram.org/bots#how-do-i-create-a-bot) from [@BotFather](https://t.me/BotFather)
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Clone and install
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tiennm99/claude-status-webhook.git
|
||||
@@ -48,103 +35,15 @@ cd claude-status-webhook
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Authenticate with Cloudflare
|
||||
See [Setup Guide](docs/setup-guide.md) for full deployment instructions.
|
||||
|
||||
```bash
|
||||
npx wrangler login
|
||||
```
|
||||
## Documentation
|
||||
|
||||
### 3. Create KV namespace and Queue
|
||||
|
||||
```bash
|
||||
npx wrangler kv namespace create claude-status
|
||||
npx wrangler queues create claude-status
|
||||
```
|
||||
|
||||
Copy the KV namespace ID from the output and update `wrangler.jsonc`:
|
||||
|
||||
```jsonc
|
||||
"kv_namespaces": [
|
||||
{ "binding": "claude_status", "id": "YOUR_KV_NAMESPACE_ID" }
|
||||
]
|
||||
```
|
||||
|
||||
### 4. Set secrets
|
||||
|
||||
```bash
|
||||
npx wrangler secret put BOT_TOKEN
|
||||
# Paste your Telegram bot token
|
||||
|
||||
npx wrangler secret put WEBHOOK_SECRET
|
||||
# Choose a random secret string for the Statuspage webhook URL
|
||||
```
|
||||
|
||||
### 5. Deploy
|
||||
|
||||
```bash
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
Note the worker URL from the output (e.g., `https://claude-status-webhook.<your-subdomain>.workers.dev`).
|
||||
|
||||
### 6. Set up Telegram bot
|
||||
|
||||
Run the setup script to register bot commands and set the Telegram webhook:
|
||||
|
||||
```bash
|
||||
node scripts/setup-bot.js
|
||||
```
|
||||
|
||||
It will prompt for your bot token and worker URL. You should see `{"ok":true}` for both webhook and commands.
|
||||
|
||||
### 7. Configure Statuspage webhook
|
||||
|
||||
1. Go to [status.claude.com](https://status.claude.com/)
|
||||
2. Click **Subscribe to Updates** → **Webhook**
|
||||
3. Enter the webhook URL: `<WORKER_URL>/webhook/status/<WEBHOOK_SECRET>`
|
||||
|
||||
Replace `<WEBHOOK_SECRET>` with the secret you set in step 4.
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This starts a local dev server with wrangler that emulates KV and Queues locally. You can test the Statuspage webhook with curl:
|
||||
|
||||
```bash
|
||||
# Test incident webhook
|
||||
curl -X POST http://localhost:8787/webhook/status/your-test-secret \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"meta": { "event_type": "incident.created" },
|
||||
"incident": {
|
||||
"name": "API Degraded Performance",
|
||||
"status": "investigating",
|
||||
"impact": "major",
|
||||
"shortlink": "https://stspg.io/xxx",
|
||||
"incident_updates": [{ "body": "We are investigating the issue.", "status": "investigating" }]
|
||||
}
|
||||
}'
|
||||
|
||||
# Test component webhook
|
||||
curl -X POST http://localhost:8787/webhook/status/your-test-secret \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"meta": { "event_type": "component.updated" },
|
||||
"component": { "name": "API", "status": "degraded_performance" },
|
||||
"component_update": { "old_status": "operational", "new_status": "degraded_performance" }
|
||||
}'
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Runtime**: [Cloudflare Workers](https://workers.cloudflare.com/)
|
||||
- **Storage**: [Cloudflare KV](https://developers.cloudflare.com/kv/)
|
||||
- **Queue**: [Cloudflare Queues](https://developers.cloudflare.com/queues/)
|
||||
- **HTTP framework**: [Hono](https://hono.dev/)
|
||||
- **Telegram framework**: [grammY](https://grammy.dev/)
|
||||
| Doc | Description |
|
||||
|-----|-------------|
|
||||
| [Setup Guide](docs/setup-guide.md) | Prerequisites, deployment, Telegram & Statuspage configuration |
|
||||
| [System Architecture](docs/system-architecture.md) | Entry points, data flow, KV schema, queue processing |
|
||||
| [Feature Decisions](docs/feature-decisions.md) | Evaluated features and rationale for decisions |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
65
docs/feature-decisions.md
Normal file
65
docs/feature-decisions.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Feature Decisions
|
||||
|
||||
Suggested features evaluated during code review (April 2026). Each was declined with rationale documented here for future reference.
|
||||
|
||||
## Declined Features
|
||||
|
||||
### 1. Digest / Quiet Mode
|
||||
|
||||
**Idea**: Batch notifications into a daily summary instead of instant alerts.
|
||||
|
||||
**Decision**: Skip. The bot's core purpose is real-time incident notification. Users subscribe specifically to know immediately when Claude goes down. A digest defeats the primary use case — if Claude API is having a major outage, a daily summary hours later is useless.
|
||||
|
||||
### 2. Inline Keyboard for /subscribe
|
||||
|
||||
**Idea**: Replace text commands with clickable buttons using grammY's inline keyboard support.
|
||||
|
||||
**Decision**: Skip. The bot targets a technical audience (developers monitoring Claude API). Text commands are sufficient and simpler to maintain. Inline keyboards add UI state management complexity (callback queries, button expiry) without meaningful UX gain for this user base.
|
||||
|
||||
### 3. Mute Command (/mute \<duration>)
|
||||
|
||||
**Idea**: Temporarily pause notifications without unsubscribing (e.g., `/mute 2h`).
|
||||
|
||||
**Decision**: Skip. Same reasoning as digest mode — this is a real-time alerting bot. If users want silence, `/stop` and `/start` are simple enough. Adding mute requires duration parsing, TTL tracking in KV, and filtering logic during fan-out — complexity not justified for a simple bot.
|
||||
|
||||
### 4. Status Change Deduplication
|
||||
|
||||
**Idea**: If a component flaps (operational → degraded → operational in 2 minutes), debounce into one message.
|
||||
|
||||
**Decision**: Skip. Would require stateful tracking of recent events (timestamps per component in KV) and delayed sending via scheduled workers or Durable Objects. Adds significant complexity. Statuspage already debounces on their end to some extent. Users prefer knowing every state change for a service they depend on.
|
||||
|
||||
### 5. Scheduled Status Digest
|
||||
|
||||
**Idea**: CF Workers `scheduled` cron trigger sends a daily "all clear" or summary to subscribers.
|
||||
|
||||
**Decision**: Skip. A "nothing happened" message daily is noise. Users can run `/status` on demand. Adding cron introduces a new entry point, scheduling logic, and message formatting for a low-value feature.
|
||||
|
||||
### 6. Admin Commands (/stats)
|
||||
|
||||
**Idea**: `/stats` to show subscriber count, recent webhook events (useful for bot operator).
|
||||
|
||||
**Decision**: Skip. The bot has a small, known user base. Subscriber count can be checked via Cloudflare KV dashboard. Adding admin auth (allowlisted chat IDs) and stats tracking adds code for a rarely-used feature.
|
||||
|
||||
### 7. Multi-Language Support
|
||||
|
||||
**Idea**: At minimum English/Vietnamese support.
|
||||
|
||||
**Decision**: Skip. Status messages from Statuspage come in English. Bot messages are short and technical. Internationalization adds string management overhead disproportionate to the bot's scope.
|
||||
|
||||
### 8. Webhook HMAC Signature Verification
|
||||
|
||||
**Idea**: Verify Statuspage webhook payloads using HMAC signatures as a second auth layer beyond URL secret.
|
||||
|
||||
**Decision**: Skip. Atlassian Statuspage does not provide HMAC signature headers for webhook deliveries. The URL secret is the only auth mechanism they support. If they add signature support in the future, this should be revisited.
|
||||
|
||||
### 9. Proactive Rate Limit Tracking
|
||||
|
||||
**Idea**: Track per-chat message counts to stay within Telegram's rate limits proactively.
|
||||
|
||||
**Decision**: Skip. Current reactive approach (retry on 429, CF Queues backoff) is sufficient for the subscriber scale. Proactive tracking requires per-chat counters in KV with TTL, sliding window logic, and adds read/write overhead to every message send. Worth revisiting only if 429 errors become frequent at scale.
|
||||
|
||||
### 10. Web Dashboard
|
||||
|
||||
**Idea**: Replace the `/` health check with a status page showing subscriber count and recent webhook events.
|
||||
|
||||
**Decision**: Skip. The bot is the product, not a web app. A dashboard requires frontend code, event logging to KV, and maintenance for a feature only the operator would see. Cloudflare dashboard + Telegram already provide sufficient visibility.
|
||||
117
docs/setup-guide.md
Normal file
117
docs/setup-guide.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Setup Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/) (v18+)
|
||||
- A [Cloudflare account](https://dash.cloudflare.com/sign-up) (free tier is sufficient)
|
||||
- A [Telegram bot token](https://core.telegram.org/bots#how-do-i-create-a-bot) from [@BotFather](https://t.me/BotFather)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|-----------|
|
||||
| Runtime | [Cloudflare Workers](https://workers.cloudflare.com/) |
|
||||
| Storage | [Cloudflare KV](https://developers.cloudflare.com/kv/) |
|
||||
| Queue | [Cloudflare Queues](https://developers.cloudflare.com/queues/) |
|
||||
| HTTP framework | [Hono](https://hono.dev/) |
|
||||
| Telegram framework | [grammY](https://grammy.dev/) |
|
||||
|
||||
## Deployment
|
||||
|
||||
### 1. Clone and install
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tiennm99/claude-status-webhook.git
|
||||
cd claude-status-webhook
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Authenticate with Cloudflare
|
||||
|
||||
```bash
|
||||
npx wrangler login
|
||||
```
|
||||
|
||||
### 3. Create KV namespace and Queue
|
||||
|
||||
```bash
|
||||
npx wrangler kv namespace create claude-status
|
||||
npx wrangler queues create claude-status
|
||||
```
|
||||
|
||||
Copy the KV namespace ID from the output and update `wrangler.jsonc`:
|
||||
|
||||
```jsonc
|
||||
"kv_namespaces": [
|
||||
{ "binding": "claude_status", "id": "YOUR_KV_NAMESPACE_ID" }
|
||||
]
|
||||
```
|
||||
|
||||
### 4. Set secrets
|
||||
|
||||
```bash
|
||||
npx wrangler secret put BOT_TOKEN
|
||||
# Paste your Telegram bot token
|
||||
|
||||
npx wrangler secret put WEBHOOK_SECRET
|
||||
# Choose a random secret string for the Statuspage webhook URL
|
||||
```
|
||||
|
||||
### 5. Deploy
|
||||
|
||||
```bash
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
Note the worker URL from the output (e.g., `https://claude-status-webhook.<your-subdomain>.workers.dev`).
|
||||
|
||||
### 6. Set up Telegram bot
|
||||
|
||||
Run the setup script to register bot commands and set the Telegram webhook:
|
||||
|
||||
```bash
|
||||
node scripts/setup-bot.js
|
||||
```
|
||||
|
||||
It will prompt for your bot token and worker URL. You should see `{"ok":true}` for both webhook and commands.
|
||||
|
||||
### 7. Configure Statuspage webhook
|
||||
|
||||
1. Go to [status.claude.com](https://status.claude.com/)
|
||||
2. Click **Subscribe to Updates** → **Webhook**
|
||||
3. Enter the webhook URL: `<WORKER_URL>/webhook/status/<WEBHOOK_SECRET>`
|
||||
|
||||
Replace `<WEBHOOK_SECRET>` with the secret you set in step 4.
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This starts a local dev server with wrangler that emulates KV and Queues locally. Test with curl:
|
||||
|
||||
```bash
|
||||
# Test incident webhook
|
||||
curl -X POST http://localhost:8787/webhook/status/your-test-secret \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"meta": { "event_type": "incident.created" },
|
||||
"incident": {
|
||||
"name": "API Degraded Performance",
|
||||
"status": "investigating",
|
||||
"impact": "major",
|
||||
"shortlink": "https://stspg.io/xxx",
|
||||
"incident_updates": [{ "body": "We are investigating the issue.", "status": "investigating" }]
|
||||
}
|
||||
}'
|
||||
|
||||
# Test component webhook
|
||||
curl -X POST http://localhost:8787/webhook/status/your-test-secret \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"meta": { "event_type": "component.updated" },
|
||||
"component": { "name": "API", "status": "degraded_performance" },
|
||||
"component_update": { "old_status": "operational", "new_status": "degraded_performance" }
|
||||
}'
|
||||
```
|
||||
113
docs/system-architecture.md
Normal file
113
docs/system-architecture.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# System Architecture
|
||||
|
||||
Telegram bot forwarding [status.claude.com](https://status.claude.com/) (Atlassian Statuspage) webhook notifications to subscribed users. Hosted on Cloudflare Workers.
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
Statuspage ──POST──► CF Worker (fetch) ──► CF Queue ──► CF Worker (queue) ──► Telegram API
|
||||
│ │
|
||||
Telegram ──POST──► Bot commands ▼
|
||||
│ User receives message
|
||||
▼
|
||||
Cloudflare KV
|
||||
```
|
||||
|
||||
## Entry Points
|
||||
|
||||
The worker exports two handlers from `src/index.js`:
|
||||
|
||||
| Handler | Trigger | Purpose |
|
||||
|---------|---------|---------|
|
||||
| `fetch` | HTTP request | Hono.js router — serves health check, Telegram webhook, Statuspage webhook |
|
||||
| `queue` | CF Queue batch | Processes queued messages, sends each to Telegram API |
|
||||
|
||||
## HTTP Routes
|
||||
|
||||
| Method | Path | File | Purpose |
|
||||
|--------|------|------|---------|
|
||||
| GET | `/` | `index.js` | Health check |
|
||||
| POST | `/webhook/telegram` | `bot-commands.js` | grammY `webhookCallback` — processes bot commands |
|
||||
| POST | `/webhook/status/:secret` | `statuspage-webhook.js` | Receives Statuspage webhooks, validates URL secret |
|
||||
|
||||
A middleware in `index.js` normalizes double slashes in URL paths (Statuspage occasionally sends `//webhook/...`).
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Statuspage → Subscribers
|
||||
|
||||
1. **Receive**: Statuspage POSTs incident/component event to `/webhook/status/:secret`
|
||||
2. **Validate**: URL secret verified via timing-safe comparison (SHA-256 hashed, `crypto-utils.js`)
|
||||
3. **Parse**: Extract event type (`incident.*` or `component.*`), format HTML message
|
||||
4. **Filter**: Query KV for subscribers matching event type + component filter (`kv-store.js`)
|
||||
5. **Enqueue**: Batch messages into CF Queue (100 per `sendBatch` call)
|
||||
6. **Deliver**: Queue consumer sends each message to Telegram API (`queue-consumer.js`)
|
||||
7. **Self-heal**: On 403/400 (bot blocked/chat deleted), subscriber auto-removed from KV
|
||||
|
||||
### User → Bot
|
||||
|
||||
1. Telegram POSTs update to `/webhook/telegram`
|
||||
2. grammY framework routes to command handler
|
||||
3. Command reads/writes KV as needed
|
||||
4. Bot replies directly via grammY context
|
||||
|
||||
## Source Files
|
||||
|
||||
| File | Lines | Responsibility |
|
||||
|------|-------|---------------|
|
||||
| `index.js` | ~30 | Hono router, path normalization middleware, export handlers |
|
||||
| `bot-commands.js` | ~145 | `/start`, `/stop`, `/subscribe` — subscription management |
|
||||
| `bot-info-commands.js` | ~125 | `/help`, `/status`, `/history`, `/uptime` — read-only info |
|
||||
| `statuspage-webhook.js` | ~85 | Webhook validation, event parsing, subscriber fan-out |
|
||||
| `queue-consumer.js` | ~65 | Batch message delivery, retry/removal logic |
|
||||
| `kv-store.js` | ~140 | KV key management, subscriber CRUD, filtered listing |
|
||||
| `status-fetcher.js` | ~120 | Statuspage API client, HTML formatters, status helpers |
|
||||
| `crypto-utils.js` | ~12 | Timing-safe string comparison (SHA-256 hashed) |
|
||||
| `telegram-api.js` | ~9 | Telegram Bot API URL builder |
|
||||
|
||||
## KV Storage
|
||||
|
||||
Namespace binding: `claude_status`
|
||||
|
||||
### Key Format
|
||||
|
||||
- `sub:{chatId}` — direct chat or group (no topic)
|
||||
- `sub:{chatId}:{threadId}` — supergroup topic (`threadId` can be `0` for General topic)
|
||||
|
||||
### Value
|
||||
|
||||
```json
|
||||
{ "types": ["incident", "component"], "components": ["API"] }
|
||||
```
|
||||
|
||||
- `types`: which event categories to receive
|
||||
- `components`: component name filter (empty array = all components)
|
||||
|
||||
### Metadata
|
||||
|
||||
Same shape as value — stored as KV key metadata so `getSubscribersByType()` uses only `kv.list()` (reads metadata from listing) instead of individual `kv.get()` calls. This is O(1) list calls vs O(N) get calls.
|
||||
|
||||
## Queue Processing
|
||||
|
||||
Binding: `claude-status` queue
|
||||
|
||||
- **Batch size**: 30 messages per consumer invocation
|
||||
- **Max retries**: 3 (configured in `wrangler.jsonc`)
|
||||
- **429 handling**: `msg.retry()` with CF Queues backoff; `Retry-After` header logged
|
||||
- **403/400 handling**: subscriber removed from KV, message acknowledged
|
||||
- **Network errors**: `msg.retry()` for transient failures
|
||||
|
||||
## Security
|
||||
|
||||
- **Statuspage webhook auth**: URL path secret validated with timing-safe SHA-256 comparison
|
||||
- **Telegram webhook**: Registered via `setup-bot.js` — Telegram only sends to the registered URL
|
||||
- **No secrets in code**: `BOT_TOKEN` and `WEBHOOK_SECRET` stored as Cloudflare secrets
|
||||
- **HTML injection**: All user/external strings passed through `escapeHtml()` before Telegram HTML rendering
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `hono` | HTTP routing framework (lightweight, CF Workers native) |
|
||||
| `grammy` | Telegram Bot API framework (webhook mode) |
|
||||
| `wrangler` | CF Workers CLI (dev/deploy, dev dependency) |
|
||||
@@ -1,10 +1,13 @@
|
||||
/**
|
||||
* Timing-safe string comparison using Web Crypto API
|
||||
* Timing-safe string comparison using Web Crypto API.
|
||||
* Hashes both inputs first so the comparison is always fixed-length (32 bytes),
|
||||
* preventing attackers from probing the secret length via timing side-channels.
|
||||
*/
|
||||
export async function timingSafeEqual(a, b) {
|
||||
const encoder = new TextEncoder();
|
||||
const bufA = encoder.encode(a);
|
||||
const bufB = encoder.encode(b);
|
||||
if (bufA.byteLength !== bufB.byteLength) return false;
|
||||
return crypto.subtle.timingSafeEqual(bufA, bufB);
|
||||
const [hashA, hashB] = await Promise.all([
|
||||
crypto.subtle.digest("SHA-256", encoder.encode(a)),
|
||||
crypto.subtle.digest("SHA-256", encoder.encode(b)),
|
||||
]);
|
||||
return crypto.subtle.timingSafeEqual(hashA, hashB);
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ function parseKvKey(kvKey) {
|
||||
const possibleThread = raw.slice(lastColon + 1);
|
||||
if (/^\d+$/.test(possibleThread)) {
|
||||
return {
|
||||
chatId: raw.slice(0, lastColon),
|
||||
chatId: Number(raw.slice(0, lastColon)),
|
||||
threadId: parseInt(possibleThread, 10),
|
||||
};
|
||||
}
|
||||
return { chatId: raw, threadId: null };
|
||||
return { chatId: Number(raw), threadId: null };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,7 +42,8 @@ export async function handleQueue(batch, env) {
|
||||
removed++;
|
||||
msg.ack();
|
||||
} else if (res.status === 429) {
|
||||
console.log("Queue: rate limited, retrying");
|
||||
const retryAfter = res.headers.get("Retry-After");
|
||||
console.log(`Queue: rate limited for ${chatId}, Retry-After: ${retryAfter ?? "unknown"}`);
|
||||
retried++;
|
||||
msg.retry();
|
||||
} else {
|
||||
|
||||
@@ -64,7 +64,7 @@ export function humanizeStatus(status) {
|
||||
* Escape HTML special chars for Telegram's HTML parse mode
|
||||
*/
|
||||
export function escapeHtml(s) {
|
||||
return s?.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") ?? "";
|
||||
return s?.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """) ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user