diff --git a/.env.deploy.example b/.env.deploy.example index 9463356..d4dab8d 100644 --- a/.env.deploy.example +++ b/.env.deploy.example @@ -1,18 +1,11 @@ -# Operator-only file: holds the credentials the deploy + migrate scripts need. +# Operator-only file: holds the credentials the deploy scripts need. # Copy to .env.deploy and fill in. NEVER commit .env.deploy. -# Telegram (must match values set as Vercel env vars) +# Telegram (must match values set as Vercel env vars). +# Used by: scripts/register-webhook.js, scripts/set-bot-description.js TELEGRAM_BOT_TOKEN= TELEGRAM_WEBHOOK_SECRET= -# Where Telegram should send webhook updates after Phase 6 cutover. +# Where Telegram should send webhook updates. # Format: https:///api/webhook -# Variable name kept as WORKER_URL for register-webhook.js compatibility. WORKER_URL=https://store-scraper-bot.vercel.app/api/webhook - -# Upstash Redis credentials — required by migrate-atlas-to-upstash.js. -# If the Vercel Marketplace Upstash integration is set up, the bot also -# accepts KV_REST_API_URL / KV_REST_API_TOKEN as fallbacks (see upstash.js). -UPSTASH_REDIS_REST_URL= -UPSTASH_REDIS_REST_TOKEN= -KEY_PREFIX=store-scraper-bot: diff --git a/.env.example b/.env.example index 4878d05..b6b5e6f 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,3 @@ CRON_SECRET=generate_another_random_string_at_least_32_chars ADMIN_IDS=123456789,987654321 APP_CACHE_SECONDS=600 NUM_DAYS_WARNING_NOT_UPDATED=30 - -# One-shot migration only (Phase 5: legacy Atlas → Upstash). Remove after migration done. -MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/dbname diff --git a/README.md b/README.md index c3a37d0..0ad9d92 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,10 @@ Vercel env vars: | `UPSTASH_REDIS_REST_TOKEN` | Upstash REST token (or `KV_REST_API_TOKEN` fallback) | | `KEY_PREFIX` | Namespace for all Redis keys (default `store-scraper-bot:`) | | `CRON_SECRET` | ≥32 chars random; required by Vercel Cron handler | -| `ENV` | `DEVELOPMENT` or `PRODUCTION` | -| `SOURCE_COMMIT` | Optional; shown on startup | | `APP_CACHE_SECONDS` | Cache TTL for upstream API responses (default 600) | -| `NUM_DAYS_WARNING_NOT_UPDATED` | Threshold for daily warning (default 30) | -| `SCHEDULE_CHECK_APP_TIME` | Cron expression in Vietnam timezone (default `0 7 * * *`) | +| `NUM_DAYS_WARNING_NOT_UPDATED` | Default warning threshold in days (default 30; per-group override via `/setdayswarning`) | -Operator-only `.env.deploy` (used by `npm run register`) — see `.env.deploy.example`. +Operator-only `.env.deploy` (used by `npm run register` + `npm run describe`) — see `.env.deploy.example`. ## Run diff --git a/scripts/check-secret-leaks.js b/scripts/check-secret-leaks.js index bbc2327..64fdf99 100644 --- a/scripts/check-secret-leaks.js +++ b/scripts/check-secret-leaks.js @@ -4,8 +4,15 @@ import { readdirSync, readFileSync, statSync } from 'node:fs'; import { join } from 'node:path'; -const SECRETS = ['MONGODB_URI', 'TELEGRAM_BOT_TOKEN', 'TELEGRAM_WEBHOOK_SECRET', 'ADMIN_IDS']; -const ROOTS = ['src', 'scripts']; +const SECRETS = [ + 'TELEGRAM_BOT_TOKEN', + 'TELEGRAM_WEBHOOK_SECRET', + 'UPSTASH_REDIS_REST_TOKEN', + 'KV_REST_API_TOKEN', + 'CRON_SECRET', + 'ADMIN_IDS', +]; +const ROOTS = ['src', 'scripts', 'api']; function* walk(dir) { let entries; diff --git a/scripts/list-upstash-keys.js b/scripts/list-upstash-keys.js new file mode 100644 index 0000000..54a694a --- /dev/null +++ b/scripts/list-upstash-keys.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node +// One-shot inspector: lists every Redis key the bot can see. +// Run via: node --env-file=.env.deploy scripts/list-upstash-keys.js +// Read-only. + +import { Redis } from '@upstash/redis'; + +const url = process.env.UPSTASH_REDIS_REST_URL ?? process.env.KV_REST_API_URL; +const token = process.env.UPSTASH_REDIS_REST_TOKEN ?? process.env.KV_REST_API_TOKEN; +const prefix = process.env.KEY_PREFIX ?? 'store-scraper-bot:'; + +if (!url || !token) { + console.error('Upstash credentials not found in env'); + process.exit(1); +} + +const redis = new Redis({ url, token }); + +let cursor = 0; +const keys = []; +do { + const [next, batch] = await redis.scan(cursor, { match: '*', count: 100 }); + cursor = Number(next); + keys.push(...batch); +} while (cursor !== 0); + +keys.sort(); + +const inPrefix = keys.filter((k) => k.startsWith(prefix)); +const orphan = keys.filter((k) => !k.startsWith(prefix)); + +console.log(`KEY_PREFIX: ${prefix}`); +console.log(`Total keys: ${keys.length}`); +console.log(`In-prefix: ${inPrefix.length}`); +console.log(`Orphan: ${orphan.length}`); +console.log(); +console.log('In-prefix keys:'); +for (const k of inPrefix) console.log(` ${k}`); +if (orphan.length) { + console.log(); + console.log('Orphan keys (NOT under KEY_PREFIX):'); + for (const k of orphan) console.log(` ${k}`); +}