mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: make Telegram optional and unify user authentication with owner ID
- Make TELEGRAM_BOT_TOKEN and ALLOWED_CHAT_IDS optional environment variables - Add telegramEnabled flag to conditionally initialize the bot on startup - Introduce persistent owner ID for unified user identity across web and Telegram auth - Update Telegram bot to accept configuration in constructor instead of using global config - Handle empty allowlist by showing chat ID prompt on /start command - Use owner ID instead of Telegram user ID for API authentication - Add conditional Telegram support checks in auth routes with clear error messages - Update documentation to explain optional Telegram configuration and binding workflow - Rename telegramUserId to userId in auth middleware for clarity
This commit is contained in:
@@ -11,14 +11,18 @@ Run Claude Code / Codex / Coding Agent sessions locally and control them remotel
|
||||
2. Start the server on a machine you control:
|
||||
|
||||
```bash
|
||||
export TELEGRAM_BOT_TOKEN="..."
|
||||
export ALLOWED_CHAT_IDS="12345678"
|
||||
export CLI_API_TOKEN="shared-secret"
|
||||
export WEBAPP_URL="https://your-domain.example" # required for Telegram Mini App
|
||||
export TELEGRAM_BOT_TOKEN="..."
|
||||
export ALLOWED_CHAT_IDS="12345678"
|
||||
|
||||
hapi server
|
||||
```
|
||||
|
||||
If you only want the web app + CLI, you can skip TELEGRAM_BOT_TOKEN and ALLOWED_CHAT_IDS.
|
||||
To enable Telegram later, set TELEGRAM_BOT_TOKEN and WEBAPP_URL, start the server, send `/start`
|
||||
to the bot to get your chat ID, set ALLOWED_CHAT_IDS, and restart the server.
|
||||
|
||||
3. If the server has no public IP, expose it over HTTPS:
|
||||
- Cloudflare Tunnel docs: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/
|
||||
- Tailscale docs: https://tailscale.com/kb/
|
||||
|
||||
+9
-3
@@ -18,13 +18,15 @@ Telegram bot + HTTP API + realtime updates for hapi.
|
||||
|
||||
## Configuration
|
||||
Required:
|
||||
- `CLI_API_TOKEN` - shared secret used by CLI and web login.
|
||||
|
||||
Optional (Telegram):
|
||||
- `TELEGRAM_BOT_TOKEN` - token from @BotFather.
|
||||
- `ALLOWED_CHAT_IDS` - comma-separated chat IDs allowed to use the bot.
|
||||
- `CLI_API_TOKEN` - shared secret used by CLI and web login.
|
||||
- `WEBAPP_URL` - public HTTPS URL for Telegram Mini App access.
|
||||
|
||||
Optional:
|
||||
- `WEBAPP_PORT` - HTTP port (default: 3006).
|
||||
- `WEBAPP_URL` - public URL for Telegram Mini App button.
|
||||
- `CORS_ORIGINS` - comma-separated origins, or `*`.
|
||||
- `HAPI_HOME` - data directory (default: ~/.hapi).
|
||||
- `DB_PATH` - SQLite database path.
|
||||
@@ -40,6 +42,10 @@ export WEBAPP_URL="https://your-domain.example"
|
||||
hapi server
|
||||
```
|
||||
|
||||
If you only need web + CLI, you can omit TELEGRAM_BOT_TOKEN and ALLOWED_CHAT_IDS.
|
||||
To enable Telegram, set TELEGRAM_BOT_TOKEN and WEBAPP_URL, start the server, send `/start`
|
||||
to the bot to get your chat ID, set ALLOWED_CHAT_IDS, and restart the server.
|
||||
|
||||
From source:
|
||||
```bash
|
||||
bun install
|
||||
@@ -69,7 +75,7 @@ The server is the hub for direct-connect mode. It accepts CLI connections over S
|
||||
|
||||
## Security model
|
||||
Access is controlled by:
|
||||
- Telegram chat ID allowlist.
|
||||
- Telegram chat ID allowlist (when Telegram is enabled).
|
||||
- `CLI_API_TOKEN` shared secret for CLI and browser access.
|
||||
|
||||
Transport security depends on HTTPS in front of the server.
|
||||
|
||||
+16
-18
@@ -2,9 +2,11 @@
|
||||
* Configuration for hapi-server (Direct Connect)
|
||||
*
|
||||
* Required environment variables:
|
||||
* - CLI_API_TOKEN: Shared secret for hapi CLI authentication
|
||||
*
|
||||
* Optional Telegram environment variables:
|
||||
* - TELEGRAM_BOT_TOKEN: Telegram Bot API token from @BotFather
|
||||
* - ALLOWED_CHAT_IDS: Comma-separated list of allowed Telegram chat IDs
|
||||
* - CLI_API_TOKEN: Shared secret for hapi CLI authentication
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
@@ -13,11 +15,14 @@ import { join } from 'node:path'
|
||||
|
||||
class Configuration {
|
||||
/** Telegram Bot API token */
|
||||
public readonly telegramBotToken: string
|
||||
public readonly telegramBotToken: string | null
|
||||
|
||||
/** List of allowed Telegram chat IDs (security whitelist) */
|
||||
public readonly allowedChatIds: number[]
|
||||
|
||||
/** Telegram bot enabled status (token present) */
|
||||
public readonly telegramEnabled: boolean
|
||||
|
||||
/** CLI auth token (shared secret) */
|
||||
public readonly cliApiToken: string
|
||||
|
||||
@@ -37,26 +42,19 @@ class Configuration {
|
||||
public readonly corsOrigins: string[]
|
||||
|
||||
constructor() {
|
||||
// Required: Telegram Bot Token
|
||||
// Optional: Telegram Bot Token
|
||||
const botToken = process.env.TELEGRAM_BOT_TOKEN
|
||||
if (!botToken) {
|
||||
throw new Error('TELEGRAM_BOT_TOKEN environment variable is required')
|
||||
}
|
||||
this.telegramBotToken = botToken
|
||||
this.telegramBotToken = botToken ?? null
|
||||
this.telegramEnabled = Boolean(this.telegramBotToken)
|
||||
|
||||
// Required: Allowed Chat IDs
|
||||
// Optional: Allowed Chat IDs
|
||||
const chatIdsStr = process.env.ALLOWED_CHAT_IDS
|
||||
if (!chatIdsStr) {
|
||||
throw new Error('ALLOWED_CHAT_IDS environment variable is required (comma-separated list)')
|
||||
}
|
||||
this.allowedChatIds = chatIdsStr
|
||||
.split(',')
|
||||
.map(id => parseInt(id.trim(), 10))
|
||||
.filter(id => !isNaN(id))
|
||||
|
||||
if (this.allowedChatIds.length === 0) {
|
||||
throw new Error('ALLOWED_CHAT_IDS must contain at least one valid chat ID')
|
||||
}
|
||||
? chatIdsStr
|
||||
.split(',')
|
||||
.map(id => parseInt(id.trim(), 10))
|
||||
.filter(id => !isNaN(id))
|
||||
: []
|
||||
|
||||
// Required: CLI API token (shared secret)
|
||||
const cliApiToken = process.env.CLI_API_TOKEN
|
||||
|
||||
+29
-15
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* HAPI Telegram Bot - Main Entry Point
|
||||
* HAPI Server - Main Entry Point
|
||||
*
|
||||
* This is a Telegram Bot client for HAPI that provides:
|
||||
* - Session list and detail views
|
||||
* - Message viewing and sending
|
||||
* - Permission approval workflows
|
||||
* - Session control (abort, mode switching, model selection)
|
||||
* - New session creation on remote machines
|
||||
* Provides:
|
||||
* - Web app + HTTP API
|
||||
* - Socket.IO for CLI connections
|
||||
* - SSE updates for the web UI
|
||||
* - Optional Telegram bot for notifications and Mini App entrypoint
|
||||
*/
|
||||
|
||||
import { getConfiguration } from './configuration'
|
||||
@@ -26,12 +25,18 @@ let webServer: BunServer<WebSocketData> | null = null
|
||||
let sseManager: SSEManager | null = null
|
||||
|
||||
async function main() {
|
||||
console.log('HAPI Bot starting...')
|
||||
console.log('HAPI Server starting...')
|
||||
|
||||
// Load configuration (will throw if required env vars missing)
|
||||
const config = getConfiguration()
|
||||
console.log(`Mini App: ${config.miniAppUrl} (port ${config.webappPort})`)
|
||||
console.log(`Allowed chat IDs: ${config.allowedChatIds.join(', ')}`)
|
||||
console.log(`[Server] Mini App: ${config.miniAppUrl} (port ${config.webappPort})`)
|
||||
if (!config.telegramEnabled) {
|
||||
console.log('[Server] Telegram: disabled (missing TELEGRAM_BOT_TOKEN)')
|
||||
} else if (config.allowedChatIds.length === 0) {
|
||||
console.log('[Server] Telegram: enabled (allowlist empty; /start shows chat ID)')
|
||||
} else {
|
||||
console.log(`[Server] Telegram: enabled (chat IDs: ${config.allowedChatIds.join(', ')})`)
|
||||
}
|
||||
|
||||
const store = new Store(config.dbPath)
|
||||
const jwtSecret = await getOrCreateJwtSecret()
|
||||
@@ -48,8 +53,15 @@ async function main() {
|
||||
|
||||
syncEngine = new SyncEngine(store, socketServer.io, socketServer.rpcRegistry, sseManager)
|
||||
|
||||
// Initialize Telegram bot
|
||||
happyBot = new HappyBot({ syncEngine })
|
||||
// Initialize Telegram bot (optional)
|
||||
if (config.telegramEnabled && config.telegramBotToken) {
|
||||
happyBot = new HappyBot({
|
||||
syncEngine,
|
||||
botToken: config.telegramBotToken,
|
||||
allowedChatIds: config.allowedChatIds,
|
||||
miniAppUrl: config.miniAppUrl
|
||||
})
|
||||
}
|
||||
|
||||
// Start HTTP server for Telegram Mini App
|
||||
webServer = await startWebServer({
|
||||
@@ -59,10 +71,12 @@ async function main() {
|
||||
socketEngine: socketServer.engine
|
||||
})
|
||||
|
||||
// Start the bot
|
||||
await happyBot.start()
|
||||
// Start the bot if configured
|
||||
if (happyBot) {
|
||||
await happyBot.start()
|
||||
}
|
||||
|
||||
console.log('\nHAPI Bot is ready!')
|
||||
console.log('\nHAPI Server is ready!')
|
||||
|
||||
// Handle shutdown
|
||||
const shutdown = async () => {
|
||||
|
||||
+57
-16
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import { Bot, Context, NextFunction, InlineKeyboard } from 'grammy'
|
||||
import { configuration } from '../configuration'
|
||||
import { SyncEngine, SyncEvent, Session } from '../sync/syncEngine'
|
||||
import { getSessionName, truncate } from './renderer'
|
||||
import {
|
||||
@@ -24,6 +23,9 @@ export interface BotContext extends Context {
|
||||
|
||||
export interface HappyBotConfig {
|
||||
syncEngine: SyncEngine
|
||||
botToken: string
|
||||
allowedChatIds: number[]
|
||||
miniAppUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,6 +35,9 @@ export class HappyBot {
|
||||
private bot: Bot<BotContext>
|
||||
private syncEngine: SyncEngine | null = null
|
||||
private isRunning = false
|
||||
private readonly allowedChatIds: number[]
|
||||
private readonly miniAppUrl: string
|
||||
private readonly allowlistConfigured: boolean
|
||||
|
||||
// Track last known permission requests per session to detect new ones
|
||||
private lastKnownRequests: Map<string, Set<string>> = new Map() // sessionId -> requestIds
|
||||
@@ -48,12 +53,17 @@ export class HappyBot {
|
||||
|
||||
constructor(config: HappyBotConfig) {
|
||||
this.syncEngine = config.syncEngine
|
||||
this.allowedChatIds = config.allowedChatIds
|
||||
this.miniAppUrl = config.miniAppUrl
|
||||
this.allowlistConfigured = this.allowedChatIds.length > 0
|
||||
|
||||
this.bot = new Bot<BotContext>(configuration.telegramBotToken)
|
||||
this.bot = new Bot<BotContext>(config.botToken)
|
||||
this.setupMiddleware()
|
||||
this.setupCommands()
|
||||
this.setupCallbacks()
|
||||
this.setupMessageHandler()
|
||||
if (this.allowlistConfigured) {
|
||||
this.setupCallbacks()
|
||||
this.setupMessageHandler()
|
||||
}
|
||||
|
||||
// Subscribe to sync events immediately if engine is available
|
||||
if (this.syncEngine) {
|
||||
@@ -134,9 +144,21 @@ export class HappyBot {
|
||||
// Security middleware: only allow configured chat IDs
|
||||
this.bot.use(async (ctx: BotContext, next: NextFunction) => {
|
||||
const chatId = ctx.chat?.id
|
||||
if (!chatId || !configuration.allowedChatIds.includes(chatId)) {
|
||||
console.log(`[HAPIBot] Rejected message from unauthorized chat: ${chatId}`)
|
||||
return // Silently ignore unauthorized users
|
||||
if (this.allowlistConfigured) {
|
||||
if (!chatId || !this.allowedChatIds.includes(chatId)) {
|
||||
console.log(`[HAPIBot] Rejected message from unauthorized chat: ${chatId}`)
|
||||
return // Silently ignore unauthorized users
|
||||
}
|
||||
await next()
|
||||
return
|
||||
}
|
||||
|
||||
const messageText = ctx.message?.text ?? ''
|
||||
if (!messageText.startsWith('/start')) {
|
||||
if (chatId) {
|
||||
console.log(`[HAPIBot] Allowlist empty; ignoring chat: ${chatId}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
await next()
|
||||
})
|
||||
@@ -151,6 +173,21 @@ export class HappyBot {
|
||||
* Setup command handlers
|
||||
*/
|
||||
private setupCommands(): void {
|
||||
if (!this.allowlistConfigured) {
|
||||
this.bot.command('start', async (ctx) => {
|
||||
const chatId = ctx.chat?.id
|
||||
const chatIdDisplay = chatId ? String(chatId) : 'unknown'
|
||||
const example = chatId ? `ALLOWED_CHAT_IDS="${chatId}"` : 'ALLOWED_CHAT_IDS="12345678"'
|
||||
|
||||
await ctx.reply(
|
||||
`HAPI bot is not fully configured yet.\n\n` +
|
||||
`Your chat ID is: ${chatIdDisplay}\n` +
|
||||
`Set ${example} and restart the server.`
|
||||
)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// /start - Status + help
|
||||
this.bot.command('start', async (ctx) => {
|
||||
const sessionCount = this.syncEngine?.getActiveSessions().length ?? 0
|
||||
@@ -185,7 +222,7 @@ export class HappyBot {
|
||||
|
||||
// /app - Open Telegram Mini App
|
||||
this.bot.command('app', async (ctx) => {
|
||||
const keyboard = new InlineKeyboard().webApp('📱 Open App', configuration.miniAppUrl)
|
||||
const keyboard = new InlineKeyboard().webApp('📱 Open App', this.miniAppUrl)
|
||||
await ctx.reply('Open HAPI Mini App:', { reply_markup: keyboard })
|
||||
})
|
||||
|
||||
@@ -239,7 +276,7 @@ export class HappyBot {
|
||||
return
|
||||
}
|
||||
|
||||
const keyboard = new InlineKeyboard().webApp('📱 Open App', configuration.miniAppUrl)
|
||||
const keyboard = new InlineKeyboard().webApp('📱 Open App', this.miniAppUrl)
|
||||
await ctx.reply(
|
||||
'Chat and session controls are available in the Mini App.',
|
||||
{ reply_markup: keyboard }
|
||||
@@ -251,6 +288,10 @@ export class HappyBot {
|
||||
* Handle sync engine events for notifications
|
||||
*/
|
||||
private handleSyncEvent(event: SyncEvent): void {
|
||||
if (!this.allowlistConfigured) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === 'session-updated' && event.sessionId) {
|
||||
const session = this.syncEngine?.getSession(event.sessionId)
|
||||
if (session) {
|
||||
@@ -320,11 +361,11 @@ export class HappyBot {
|
||||
|
||||
const name = getSessionName(session)
|
||||
|
||||
const url = buildMiniAppDeepLink(configuration.miniAppUrl, `session_${sessionId}`)
|
||||
const url = buildMiniAppDeepLink(this.miniAppUrl, `session_${sessionId}`)
|
||||
const keyboard = new InlineKeyboard()
|
||||
.webApp('📱 Open Session', url)
|
||||
|
||||
for (const chatId of configuration.allowedChatIds) {
|
||||
for (const chatId of this.allowedChatIds) {
|
||||
await this.bot.api.sendMessage(
|
||||
chatId,
|
||||
`✅ ${name} is ready\n\nClaude is waiting for your next message.`,
|
||||
@@ -340,11 +381,11 @@ export class HappyBot {
|
||||
}
|
||||
const name = getSessionName(session)
|
||||
|
||||
const url = buildMiniAppDeepLink(configuration.miniAppUrl, `session_${sessionId}`)
|
||||
const url = buildMiniAppDeepLink(this.miniAppUrl, `session_${sessionId}`)
|
||||
const keyboard = new InlineKeyboard()
|
||||
.webApp('📱 Details', url)
|
||||
|
||||
for (const chatId of configuration.allowedChatIds) {
|
||||
for (const chatId of this.allowedChatIds) {
|
||||
await this.bot.api.sendMessage(
|
||||
chatId,
|
||||
`🔄 ${name} switched to ${mode}`,
|
||||
@@ -360,13 +401,13 @@ export class HappyBot {
|
||||
}
|
||||
const name = getSessionName(session)
|
||||
|
||||
const url = buildMiniAppDeepLink(configuration.miniAppUrl, `session_${sessionId}`)
|
||||
const url = buildMiniAppDeepLink(this.miniAppUrl, `session_${sessionId}`)
|
||||
const keyboard = new InlineKeyboard()
|
||||
.webApp('📱 Open Session', url)
|
||||
|
||||
const body = truncate(previewText, 600)
|
||||
|
||||
for (const chatId of configuration.allowedChatIds) {
|
||||
for (const chatId of this.allowedChatIds) {
|
||||
await this.bot.api.sendMessage(
|
||||
chatId,
|
||||
`💬 ${name}\n\n${body}`,
|
||||
@@ -470,7 +511,7 @@ export class HappyBot {
|
||||
const keyboard = createNotificationKeyboard(session)
|
||||
|
||||
// Send to all allowed chat IDs
|
||||
for (const chatId of configuration.allowedChatIds) {
|
||||
for (const chatId of this.allowedChatIds) {
|
||||
try {
|
||||
await this.bot.api.sendMessage(chatId, text, {
|
||||
reply_markup: keyboard
|
||||
|
||||
@@ -4,7 +4,7 @@ import { jwtVerify } from 'jose'
|
||||
|
||||
export type WebAppEnv = {
|
||||
Variables: {
|
||||
telegramUserId: number
|
||||
userId: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function createAuthMiddleware(jwtSecret: Uint8Array): MiddlewareHandler<W
|
||||
return c.json({ error: 'Invalid token payload' }, 401)
|
||||
}
|
||||
|
||||
c.set('telegramUserId', parsed.data.uid)
|
||||
c.set('userId', parsed.data.uid)
|
||||
await next()
|
||||
return
|
||||
} catch {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { z } from 'zod'
|
||||
import { configuration } from '../configuration'
|
||||
|
||||
const ownerIdFileSchema = z.object({
|
||||
ownerId: z.number()
|
||||
})
|
||||
|
||||
function generateOwnerId(): number {
|
||||
const bytes = randomBytes(6)
|
||||
let value = 0
|
||||
for (const byte of bytes) {
|
||||
value = (value << 8) + byte
|
||||
}
|
||||
return value > 0 ? value : 1
|
||||
}
|
||||
|
||||
let cachedOwnerId: number | null = null
|
||||
|
||||
export async function getOrCreateOwnerId(): Promise<number> {
|
||||
if (cachedOwnerId !== null) {
|
||||
return cachedOwnerId
|
||||
}
|
||||
|
||||
const ownerIdFile = join(configuration.dataDir, 'owner-id.json')
|
||||
|
||||
if (existsSync(ownerIdFile)) {
|
||||
await chmod(ownerIdFile, 0o600).catch(() => {})
|
||||
const raw = await readFile(ownerIdFile, 'utf8')
|
||||
const parsed = ownerIdFileSchema.parse(JSON.parse(raw))
|
||||
if (!Number.isSafeInteger(parsed.ownerId) || parsed.ownerId <= 0) {
|
||||
throw new Error(`Invalid ownerId in ${ownerIdFile}`)
|
||||
}
|
||||
cachedOwnerId = parsed.ownerId
|
||||
return parsed.ownerId
|
||||
}
|
||||
|
||||
const ownerId = generateOwnerId()
|
||||
const dir = dirname(ownerIdFile)
|
||||
if (!existsSync(dir)) {
|
||||
await mkdir(dir, { recursive: true, mode: 0o700 })
|
||||
}
|
||||
|
||||
const payload = { ownerId }
|
||||
await writeFile(ownerIdFile, JSON.stringify(payload, null, 4), { mode: 0o600 })
|
||||
await chmod(ownerIdFile, 0o600).catch(() => {})
|
||||
|
||||
cachedOwnerId = ownerId
|
||||
return ownerId
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { SignJWT } from 'jose'
|
||||
import { z } from 'zod'
|
||||
import { configuration } from '../../configuration'
|
||||
import { validateTelegramInitData } from '../telegramInitData'
|
||||
import { getOrCreateOwnerId } from '../ownerId'
|
||||
import type { WebAppEnv } from '../middleware/auth'
|
||||
|
||||
const telegramAuthSchema = z.object({
|
||||
@@ -35,21 +36,29 @@ export function createAuthRoutes(jwtSecret: Uint8Array): Hono<WebAppEnv> {
|
||||
if (parsed.data.accessToken !== configuration.cliApiToken) {
|
||||
return c.json({ error: 'Invalid access token' }, 401)
|
||||
}
|
||||
// Use first allowed chat ID as the shared user identity
|
||||
userId = configuration.allowedChatIds[0]
|
||||
userId = await getOrCreateOwnerId()
|
||||
firstName = 'Web User'
|
||||
} else {
|
||||
if (!configuration.telegramEnabled || !configuration.telegramBotToken) {
|
||||
return c.json({ error: 'Telegram authentication is disabled. Configure TELEGRAM_BOT_TOKEN.' }, 503)
|
||||
}
|
||||
|
||||
if (configuration.allowedChatIds.length === 0) {
|
||||
return c.json({ error: 'Telegram allowlist is empty. Configure ALLOWED_CHAT_IDS and restart.' }, 403)
|
||||
}
|
||||
|
||||
// Telegram initData authentication
|
||||
const result = validateTelegramInitData(parsed.data.initData, configuration.telegramBotToken)
|
||||
if (!result.ok) {
|
||||
return c.json({ error: result.error }, 401)
|
||||
}
|
||||
|
||||
userId = result.user.id
|
||||
if (!configuration.isChatIdAllowed(userId)) {
|
||||
const telegramUserId = result.user.id
|
||||
if (!configuration.isChatIdAllowed(telegramUserId)) {
|
||||
return c.json({ error: 'User not allowed' }, 403)
|
||||
}
|
||||
|
||||
userId = await getOrCreateOwnerId()
|
||||
username = result.user.username
|
||||
firstName = result.user.first_name
|
||||
lastName = result.user.last_name
|
||||
@@ -74,4 +83,3 @@ export function createAuthRoutes(jwtSecret: Uint8Array): Hono<WebAppEnv> {
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: telegram-optional-config
|
||||
description: Make Telegram optional; unify owner id auth
|
||||
---
|
||||
|
||||
# Plan
|
||||
|
||||
基于你的决策更新计划:绑定流程为“提示 chat id + 重启”,允许列表仅来自 env;统一 `uid` 为 owner id,确保 Web/Telegram 登录语义一致。
|
||||
|
||||
## Requirements
|
||||
- 未配置 Telegram 相关 env 时,服务端不退出,Web/CLI 正常可用。
|
||||
- Telegram bot 仅在配置 `TELEGRAM_BOT_TOKEN` 时启动;`ALLOWED_CHAT_IDS` 仅来自 env。
|
||||
- 绑定流程为:bot 提示 chat id → 用户配置 env → 重启服务。
|
||||
- `uid` 统一为 owner id(accessToken 与 telegram 登录一致)。
|
||||
|
||||
## Scope
|
||||
- In: 配置解析、启动流程、auth 逻辑、owner id 持久化、文档更新。
|
||||
- Out: 自动绑定、DB/动态 allowlist、复杂 UI 设置页。
|
||||
|
||||
## Files and entry points
|
||||
- `server/src/configuration.ts`
|
||||
- `server/src/index.ts`
|
||||
- `server/src/telegram/bot.ts`
|
||||
- `server/src/web/routes/auth.ts`
|
||||
- `server/src/web/jwtSecret.ts`(或新增轻量 owner id 持久化文件)
|
||||
- `README.md`
|
||||
- `server/README.md`
|
||||
|
||||
## Data model / API changes
|
||||
- 增加“owner id”持久化(建议 `dataDir/owner-id.json`),用于统一 auth 的 `uid`。
|
||||
- 不新增 Telegram 绑定 API(按“提示 chat id + 重启”流程)。
|
||||
|
||||
## Action items
|
||||
[ ] 配置层改为 Telegram 可选:`TELEGRAM_BOT_TOKEN`/`ALLOWED_CHAT_IDS` 允许为空;加入 `telegramEnabled`,`allowedChatIds` 为空数组可接受。
|
||||
[ ] 生成并持久化 `ownerId`(数值或 UUID -> 数值映射),`/api/auth` 的 `uid` 始终为 `ownerId`。
|
||||
[ ] 启动逻辑按 `telegramEnabled` 分支:未启用仅启动 Web/Socket/SSE,并打印 Telegram disabled 日志。
|
||||
[ ] Telegram bot 行为:
|
||||
- 已启用但 `ALLOWED_CHAT_IDS` 未配置:仅响应 `/start`,提示当前 chat id 与配置示例;不开放其它命令/通知。
|
||||
- 已启用且 allowlist 配置:按现有流程运行。
|
||||
[ ] `/api/auth` 调整:
|
||||
- accessToken:验证后直接使用 `ownerId`。
|
||||
- telegram:若 Telegram 未启用,返回清晰错误;启用时校验 initData 与 allowlist,但仍签发 `uid = ownerId`。
|
||||
[ ] 文档更新:说明 Telegram 配置可选;新增“获取 chat id 并重启绑定”的指引。
|
||||
|
||||
## Testing and validation
|
||||
- 仅设置 `CLI_API_TOKEN` 启动:服务正常、Web 登录可用、bot 不启动。
|
||||
- 设置 `TELEGRAM_BOT_TOKEN` 且未配 allowlist:`/start` 能提示 chat id,其它命令受限。
|
||||
- 完整配置 `TELEGRAM_BOT_TOKEN` + `ALLOWED_CHAT_IDS`:通知与 Mini App 正常。
|
||||
- `uid` 在两种登录方式下均为 `ownerId`。
|
||||
|
||||
## Risks and edge cases
|
||||
- `ownerId` 生成/持久化失败会导致登录不稳定。
|
||||
- 只靠 env allowlist,运维更新需重启,需在文档强调。
|
||||
- Telegram 未启用但 Web 侧仍可能尝试 Telegram auth(应给清晰错误)。
|
||||
|
||||
## Open questions
|
||||
- None.
|
||||
Reference in New Issue
Block a user