mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: implement server configuration persistence to ~/.hapi/settings.json
Configuration now loads with priority: environment variable > settings.json > default value. When values are read from environment variables and not present in settings.json, they are automatically saved for future use. This eliminates the need to repeatedly set environment variables. - New serverSettings.ts module handles loading/saving with persistence logic - Async createConfiguration() factory for proper initialization ordering - Configuration sources tracked and displayed in startup logs - Exported Settings interface and read/write functions from cliApiToken.ts - Updated index.ts to display configuration sources in log output
This commit is contained in:
+115
-83
@@ -1,15 +1,37 @@
|
||||
/**
|
||||
* Configuration for hapi-server (Direct Connect)
|
||||
*
|
||||
* Configuration is loaded with priority: environment variable > settings.json > default
|
||||
* When values are read from environment variables and not present in settings.json,
|
||||
* they are automatically saved for future use.
|
||||
*
|
||||
* Optional environment variables:
|
||||
* - CLI_API_TOKEN: Shared secret for hapi CLI authentication (auto-generated if not set)
|
||||
* - TELEGRAM_BOT_TOKEN: Telegram Bot API token from @BotFather
|
||||
* - ALLOWED_CHAT_IDS: Comma-separated list of allowed Telegram chat IDs
|
||||
* - WEBAPP_PORT: Port for Mini App HTTP server (default: 3006)
|
||||
* - WEBAPP_URL: Public URL for Telegram Mini App
|
||||
* - CORS_ORIGINS: Comma-separated CORS origins
|
||||
* - HAPI_HOME: Data directory (default: ~/.hapi)
|
||||
* - DB_PATH: SQLite database path (default: {HAPI_HOME}/hapi.db)
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { loadServerSettings, type ServerSettings, type ServerSettingsResult } from './serverSettings'
|
||||
import { getOrCreateCliApiToken } from './web/cliApiToken'
|
||||
|
||||
export type ConfigSource = 'env' | 'file' | 'default'
|
||||
|
||||
export interface ConfigSources {
|
||||
telegramBotToken: ConfigSource
|
||||
allowedChatIds: ConfigSource
|
||||
webappPort: ConfigSource
|
||||
webappUrl: ConfigSource
|
||||
corsOrigins: ConfigSource
|
||||
cliApiToken: 'env' | 'file' | 'generated'
|
||||
}
|
||||
|
||||
class Configuration {
|
||||
/** Telegram Bot API token */
|
||||
@@ -24,8 +46,11 @@ class Configuration {
|
||||
/** CLI auth token (shared secret) */
|
||||
public cliApiToken: string
|
||||
|
||||
/** Source of CLI API token ('pending' | 'env' | 'file' | 'generated') */
|
||||
public cliApiTokenSource: string
|
||||
/** Source of CLI API token */
|
||||
public cliApiTokenSource: 'env' | 'file' | 'generated' | ''
|
||||
|
||||
/** Whether CLI API token was newly generated (for first-run display) */
|
||||
public cliApiTokenIsNew: boolean
|
||||
|
||||
/** Path to settings.json file */
|
||||
public readonly settingsFile: string
|
||||
@@ -45,87 +70,37 @@ class Configuration {
|
||||
/** Allowed CORS origins for Mini App + Socket.IO (comma-separated env override) */
|
||||
public readonly corsOrigins: string[]
|
||||
|
||||
constructor() {
|
||||
// Optional: Telegram Bot Token
|
||||
const botToken = process.env.TELEGRAM_BOT_TOKEN
|
||||
this.telegramBotToken = botToken ?? null
|
||||
/** Sources of each configuration value */
|
||||
public readonly sources: ConfigSources
|
||||
|
||||
/** Private constructor - use createConfiguration() instead */
|
||||
private constructor(
|
||||
dataDir: string,
|
||||
dbPath: string,
|
||||
serverSettings: ServerSettings,
|
||||
sources: ServerSettingsResult['sources']
|
||||
) {
|
||||
this.dataDir = dataDir
|
||||
this.dbPath = dbPath
|
||||
this.settingsFile = join(dataDir, 'settings.json')
|
||||
|
||||
// Apply server settings
|
||||
this.telegramBotToken = serverSettings.telegramBotToken
|
||||
this.telegramEnabled = Boolean(this.telegramBotToken)
|
||||
this.allowedChatIds = serverSettings.allowedChatIds
|
||||
this.webappPort = serverSettings.webappPort
|
||||
this.miniAppUrl = serverSettings.webappUrl
|
||||
this.corsOrigins = serverSettings.corsOrigins
|
||||
|
||||
// Optional: Allowed Chat IDs
|
||||
const chatIdsStr = process.env.ALLOWED_CHAT_IDS
|
||||
this.allowedChatIds = chatIdsStr
|
||||
? chatIdsStr
|
||||
.split(',')
|
||||
.map(id => parseInt(id.trim(), 10))
|
||||
.filter(id => !isNaN(id))
|
||||
: []
|
||||
|
||||
// CLI API token - will be set later by getOrCreateCliApiToken()
|
||||
// CLI API token - will be set by _setCliApiToken() before create() returns
|
||||
this.cliApiToken = ''
|
||||
this.cliApiTokenSource = 'pending'
|
||||
this.cliApiTokenSource = ''
|
||||
this.cliApiTokenIsNew = false
|
||||
|
||||
// Mini App web server configuration
|
||||
const webappPortRaw = process.env.WEBAPP_PORT
|
||||
const parsedWebappPort = webappPortRaw ? parseInt(webappPortRaw, 10) : 3006
|
||||
if (!Number.isFinite(parsedWebappPort) || parsedWebappPort <= 0) {
|
||||
throw new Error('WEBAPP_PORT must be a valid port number')
|
||||
}
|
||||
this.webappPort = parsedWebappPort
|
||||
|
||||
// For production, Telegram requires HTTPS for Mini Apps.
|
||||
// This URL is what Telegram clients will open when pressing the WebApp button.
|
||||
this.miniAppUrl = process.env.WEBAPP_URL || `http://localhost:${this.webappPort}`
|
||||
|
||||
// CORS origin allowlist (Mini App + Socket.IO browser clients).
|
||||
// - Defaults to the Mini App's origin (derived from WEBAPP_URL).
|
||||
// - If set to "*", allows all origins (not recommended for internet-exposed deployments).
|
||||
const corsOriginsRaw = process.env.CORS_ORIGINS
|
||||
if (corsOriginsRaw) {
|
||||
const entries = corsOriginsRaw
|
||||
.split(',')
|
||||
.map((origin) => origin.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
if (entries.includes('*')) {
|
||||
this.corsOrigins = ['*']
|
||||
} else {
|
||||
const normalized: string[] = []
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
normalized.push(new URL(entry).origin)
|
||||
} catch {
|
||||
// Keep raw value if it's already an origin-like string.
|
||||
normalized.push(entry)
|
||||
}
|
||||
}
|
||||
this.corsOrigins = normalized
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
this.corsOrigins = [new URL(this.miniAppUrl).origin]
|
||||
} catch {
|
||||
this.corsOrigins = []
|
||||
}
|
||||
}
|
||||
|
||||
// Data directory
|
||||
if (process.env.HAPI_HOME) {
|
||||
const expandedPath = process.env.HAPI_HOME.replace(/^~/, homedir())
|
||||
this.dataDir = expandedPath
|
||||
} else {
|
||||
this.dataDir = join(homedir(), '.hapi')
|
||||
}
|
||||
|
||||
// DB path (defaults inside dataDir)
|
||||
if (process.env.DB_PATH) {
|
||||
const expandedPath = process.env.DB_PATH.replace(/^~/, homedir())
|
||||
this.dbPath = expandedPath
|
||||
} else {
|
||||
this.dbPath = join(this.dataDir, 'hapi.db')
|
||||
}
|
||||
|
||||
// Settings file path
|
||||
this.settingsFile = join(this.dataDir, 'settings.json')
|
||||
// Store sources for logging (cliApiToken will be set by _setCliApiToken)
|
||||
this.sources = {
|
||||
...sources,
|
||||
} as ConfigSources
|
||||
|
||||
// Ensure data directory exists
|
||||
if (!existsSync(this.dataDir)) {
|
||||
@@ -133,24 +108,81 @@ class Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
/** Create configuration asynchronously */
|
||||
static async create(): Promise<Configuration> {
|
||||
// 1. Determine data directory (env only - not persisted)
|
||||
const dataDir = process.env.HAPI_HOME
|
||||
? process.env.HAPI_HOME.replace(/^~/, homedir())
|
||||
: join(homedir(), '.hapi')
|
||||
|
||||
// Ensure data directory exists before loading settings
|
||||
if (!existsSync(dataDir)) {
|
||||
mkdirSync(dataDir, { recursive: true })
|
||||
}
|
||||
|
||||
// 2. Determine DB path (env only - not persisted)
|
||||
const dbPath = process.env.DB_PATH
|
||||
? process.env.DB_PATH.replace(/^~/, homedir())
|
||||
: join(dataDir, 'hapi.db')
|
||||
|
||||
// 3. Load server settings (with persistence)
|
||||
const settingsResult = await loadServerSettings(dataDir)
|
||||
|
||||
if (settingsResult.savedToFile) {
|
||||
console.log(`[Server] Configuration saved to ${join(dataDir, 'settings.json')}`)
|
||||
}
|
||||
|
||||
// 4. Create configuration instance
|
||||
const config = new Configuration(
|
||||
dataDir,
|
||||
dbPath,
|
||||
settingsResult.settings,
|
||||
settingsResult.sources
|
||||
)
|
||||
|
||||
// 5. Load CLI API token
|
||||
const tokenResult = await getOrCreateCliApiToken(dataDir)
|
||||
config._setCliApiToken(tokenResult.token, tokenResult.source, tokenResult.isNew)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/** Check if a chat ID is allowed */
|
||||
isChatIdAllowed(chatId: number): boolean {
|
||||
return this.allowedChatIds.includes(chatId)
|
||||
}
|
||||
|
||||
/** Set CLI API token (called after async initialization) */
|
||||
_setCliApiToken(token: string, source: string): void {
|
||||
/** Set CLI API token (called during async initialization) */
|
||||
_setCliApiToken(token: string, source: 'env' | 'file' | 'generated', isNew: boolean): void {
|
||||
this.cliApiToken = token
|
||||
this.cliApiTokenSource = source
|
||||
this.cliApiTokenIsNew = isNew
|
||||
;(this.sources as { cliApiToken: string }).cliApiToken = source
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy initialization to allow configuration to fail gracefully
|
||||
// Singleton instance (set by createConfiguration)
|
||||
let _configuration: Configuration | null = null
|
||||
|
||||
/**
|
||||
* Create and initialize configuration asynchronously.
|
||||
* Must be called once at startup before getConfiguration() can be used.
|
||||
*/
|
||||
export async function createConfiguration(): Promise<Configuration> {
|
||||
if (_configuration) {
|
||||
return _configuration
|
||||
}
|
||||
_configuration = await Configuration.create()
|
||||
return _configuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the initialized configuration.
|
||||
* Throws if createConfiguration() has not been called yet.
|
||||
*/
|
||||
export function getConfiguration(): Configuration {
|
||||
if (!_configuration) {
|
||||
_configuration = new Configuration()
|
||||
throw new Error('Configuration not initialized. Call createConfiguration() first.')
|
||||
}
|
||||
return _configuration
|
||||
}
|
||||
|
||||
+34
-18
@@ -8,18 +8,31 @@
|
||||
* - Optional Telegram bot for notifications and Mini App entrypoint
|
||||
*/
|
||||
|
||||
import { getConfiguration } from './configuration'
|
||||
import { createConfiguration, type ConfigSource } from './configuration'
|
||||
import { Store } from './store'
|
||||
import { SyncEngine, type SyncEvent } from './sync/syncEngine'
|
||||
import { HappyBot } from './telegram/bot'
|
||||
import { startWebServer } from './web/server'
|
||||
import { getOrCreateJwtSecret } from './web/jwtSecret'
|
||||
import { getOrCreateCliApiToken } from './web/cliApiToken'
|
||||
import { createSocketServer } from './socket/server'
|
||||
import { SSEManager } from './sse/sseManager'
|
||||
import type { Server as BunServer } from 'bun'
|
||||
import type { WebSocketData } from '@socket.io/bun-engine'
|
||||
|
||||
/** Format config source for logging */
|
||||
function formatSource(source: ConfigSource | 'generated'): string {
|
||||
switch (source) {
|
||||
case 'env':
|
||||
return 'environment'
|
||||
case 'file':
|
||||
return 'settings.json'
|
||||
case 'default':
|
||||
return 'default'
|
||||
case 'generated':
|
||||
return 'generated'
|
||||
}
|
||||
}
|
||||
|
||||
let syncEngine: SyncEngine | null = null
|
||||
let happyBot: HappyBot | null = null
|
||||
let webServer: BunServer<WebSocketData> | null = null
|
||||
@@ -28,37 +41,40 @@ let sseManager: SSEManager | null = null
|
||||
async function main() {
|
||||
console.log('HAPI Server starting...')
|
||||
|
||||
// Load configuration
|
||||
const config = getConfiguration()
|
||||
// Load configuration (async - loads from env/file with persistence)
|
||||
const config = await createConfiguration()
|
||||
|
||||
// Initialize CLI API token
|
||||
const tokenResult = await getOrCreateCliApiToken(config.dataDir)
|
||||
config._setCliApiToken(tokenResult.token, tokenResult.source)
|
||||
|
||||
// Display token information
|
||||
if (tokenResult.isNew) {
|
||||
// Display CLI API token information
|
||||
if (config.cliApiTokenIsNew) {
|
||||
console.log('')
|
||||
console.log('='.repeat(70))
|
||||
console.log(' NEW CLI_API_TOKEN GENERATED')
|
||||
console.log('='.repeat(70))
|
||||
console.log('')
|
||||
console.log(` Token: ${tokenResult.token}`)
|
||||
console.log(` Token: ${config.cliApiToken}`)
|
||||
console.log('')
|
||||
console.log(` Saved to: ${tokenResult.filePath}`)
|
||||
console.log(` Saved to: ${config.settingsFile}`)
|
||||
console.log('')
|
||||
console.log('='.repeat(70))
|
||||
console.log('')
|
||||
} else {
|
||||
console.log(`[Server] CLI_API_TOKEN: loaded from ${tokenResult.source}`)
|
||||
console.log(`[Server] CLI_API_TOKEN: loaded from ${formatSource(config.sources.cliApiToken)}`)
|
||||
}
|
||||
|
||||
console.log(`[Server] Mini App: ${config.miniAppUrl} (port ${config.webappPort})`)
|
||||
// Display other configuration sources
|
||||
console.log(`[Server] WEBAPP_PORT: ${config.webappPort} (${formatSource(config.sources.webappPort)})`)
|
||||
console.log(`[Server] WEBAPP_URL: ${config.miniAppUrl} (${formatSource(config.sources.webappUrl)})`)
|
||||
|
||||
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)')
|
||||
console.log('[Server] Telegram: disabled (no TELEGRAM_BOT_TOKEN)')
|
||||
} else {
|
||||
console.log(`[Server] Telegram: enabled (chat IDs: ${config.allowedChatIds.join(', ')})`)
|
||||
const tokenSource = formatSource(config.sources.telegramBotToken)
|
||||
if (config.allowedChatIds.length === 0) {
|
||||
console.log(`[Server] Telegram: enabled (${tokenSource}), allowlist empty - /start shows chat ID`)
|
||||
} else {
|
||||
const idsSource = formatSource(config.sources.allowedChatIds)
|
||||
console.log(`[Server] Telegram: enabled (${tokenSource}), chat IDs: ${config.allowedChatIds.join(', ')} (${idsSource})`)
|
||||
}
|
||||
}
|
||||
|
||||
const store = new Store(config.dbPath)
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Server Settings Management
|
||||
*
|
||||
* Handles loading and persistence of server configuration.
|
||||
* Priority: environment variable > settings.json > default value
|
||||
*
|
||||
* When a value is loaded from environment variable and not present in settings.json,
|
||||
* it will be saved to settings.json for future use.
|
||||
*/
|
||||
|
||||
import { join } from 'node:path'
|
||||
import { readSettings, writeSettings, type Settings } from './web/cliApiToken'
|
||||
|
||||
export interface ServerSettings {
|
||||
telegramBotToken: string | null
|
||||
allowedChatIds: number[]
|
||||
webappPort: number
|
||||
webappUrl: string
|
||||
corsOrigins: string[]
|
||||
}
|
||||
|
||||
export interface ServerSettingsResult {
|
||||
settings: ServerSettings
|
||||
sources: {
|
||||
telegramBotToken: 'env' | 'file' | 'default'
|
||||
allowedChatIds: 'env' | 'file' | 'default'
|
||||
webappPort: 'env' | 'file' | 'default'
|
||||
webappUrl: 'env' | 'file' | 'default'
|
||||
corsOrigins: 'env' | 'file' | 'default'
|
||||
}
|
||||
savedToFile: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse comma-separated chat IDs from string
|
||||
*/
|
||||
function parseChatIds(str: string): number[] {
|
||||
return str
|
||||
.split(',')
|
||||
.map(id => parseInt(id.trim(), 10))
|
||||
.filter(id => !isNaN(id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and normalize CORS origins
|
||||
*/
|
||||
function parseCorsOrigins(str: string): string[] {
|
||||
const entries = str
|
||||
.split(',')
|
||||
.map(origin => origin.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
if (entries.includes('*')) {
|
||||
return ['*']
|
||||
}
|
||||
|
||||
const normalized: string[] = []
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
normalized.push(new URL(entry).origin)
|
||||
} catch {
|
||||
// Keep raw value if it's already an origin-like string
|
||||
normalized.push(entry)
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive CORS origins from webapp URL
|
||||
*/
|
||||
function deriveCorsOrigins(webappUrl: string): string[] {
|
||||
try {
|
||||
return [new URL(webappUrl).origin]
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load server settings with priority: env > file > default
|
||||
* Saves new env values to file when not already present
|
||||
*/
|
||||
export async function loadServerSettings(dataDir: string): Promise<ServerSettingsResult> {
|
||||
const settingsFile = join(dataDir, 'settings.json')
|
||||
const settings = await readSettings(settingsFile)
|
||||
|
||||
// If settings file exists but couldn't be parsed, fail fast
|
||||
if (settings === null) {
|
||||
throw new Error(
|
||||
`Cannot read ${settingsFile}. Please fix or remove the file and restart.`
|
||||
)
|
||||
}
|
||||
|
||||
let needsSave = false
|
||||
const sources: ServerSettingsResult['sources'] = {
|
||||
telegramBotToken: 'default',
|
||||
allowedChatIds: 'default',
|
||||
webappPort: 'default',
|
||||
webappUrl: 'default',
|
||||
corsOrigins: 'default',
|
||||
}
|
||||
|
||||
// telegramBotToken: env > file > null
|
||||
let telegramBotToken: string | null = null
|
||||
if (process.env.TELEGRAM_BOT_TOKEN) {
|
||||
telegramBotToken = process.env.TELEGRAM_BOT_TOKEN
|
||||
sources.telegramBotToken = 'env'
|
||||
if (settings.telegramBotToken === undefined) {
|
||||
settings.telegramBotToken = telegramBotToken
|
||||
needsSave = true
|
||||
}
|
||||
} else if (settings.telegramBotToken !== undefined) {
|
||||
telegramBotToken = settings.telegramBotToken
|
||||
sources.telegramBotToken = 'file'
|
||||
}
|
||||
|
||||
// allowedChatIds: env > file > []
|
||||
let allowedChatIds: number[] = []
|
||||
if (process.env.ALLOWED_CHAT_IDS) {
|
||||
allowedChatIds = parseChatIds(process.env.ALLOWED_CHAT_IDS)
|
||||
sources.allowedChatIds = 'env'
|
||||
if (settings.allowedChatIds === undefined) {
|
||||
settings.allowedChatIds = allowedChatIds
|
||||
needsSave = true
|
||||
}
|
||||
} else if (settings.allowedChatIds !== undefined) {
|
||||
allowedChatIds = settings.allowedChatIds
|
||||
sources.allowedChatIds = 'file'
|
||||
}
|
||||
|
||||
// webappPort: env > file > 3006
|
||||
let webappPort = 3006
|
||||
if (process.env.WEBAPP_PORT) {
|
||||
const parsed = parseInt(process.env.WEBAPP_PORT, 10)
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
throw new Error('WEBAPP_PORT must be a valid port number')
|
||||
}
|
||||
webappPort = parsed
|
||||
sources.webappPort = 'env'
|
||||
if (settings.webappPort === undefined) {
|
||||
settings.webappPort = webappPort
|
||||
needsSave = true
|
||||
}
|
||||
} else if (settings.webappPort !== undefined) {
|
||||
webappPort = settings.webappPort
|
||||
sources.webappPort = 'file'
|
||||
}
|
||||
|
||||
// webappUrl: env > file > http://localhost:{port}
|
||||
let webappUrl = `http://localhost:${webappPort}`
|
||||
if (process.env.WEBAPP_URL) {
|
||||
webappUrl = process.env.WEBAPP_URL
|
||||
sources.webappUrl = 'env'
|
||||
if (settings.webappUrl === undefined) {
|
||||
settings.webappUrl = webappUrl
|
||||
needsSave = true
|
||||
}
|
||||
} else if (settings.webappUrl !== undefined) {
|
||||
webappUrl = settings.webappUrl
|
||||
sources.webappUrl = 'file'
|
||||
}
|
||||
|
||||
// corsOrigins: env > file > derived from webappUrl
|
||||
let corsOrigins: string[]
|
||||
if (process.env.CORS_ORIGINS) {
|
||||
corsOrigins = parseCorsOrigins(process.env.CORS_ORIGINS)
|
||||
sources.corsOrigins = 'env'
|
||||
if (settings.corsOrigins === undefined) {
|
||||
settings.corsOrigins = corsOrigins
|
||||
needsSave = true
|
||||
}
|
||||
} else if (settings.corsOrigins !== undefined) {
|
||||
corsOrigins = settings.corsOrigins
|
||||
sources.corsOrigins = 'file'
|
||||
} else {
|
||||
corsOrigins = deriveCorsOrigins(webappUrl)
|
||||
}
|
||||
|
||||
// Save settings if any new values were added
|
||||
if (needsSave) {
|
||||
await writeSettings(settingsFile, settings)
|
||||
}
|
||||
|
||||
return {
|
||||
settings: {
|
||||
telegramBotToken,
|
||||
allowedChatIds,
|
||||
webappPort,
|
||||
webappUrl,
|
||||
corsOrigins,
|
||||
},
|
||||
sources,
|
||||
savedToFile: needsSave,
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,18 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
interface Settings {
|
||||
export interface Settings {
|
||||
onboardingCompleted?: boolean
|
||||
machineId?: string
|
||||
machineIdConfirmedByServer?: boolean
|
||||
daemonAutoStartWhenRunningHappy?: boolean
|
||||
cliApiToken?: string
|
||||
// Server configuration (persisted from environment variables)
|
||||
telegramBotToken?: string
|
||||
allowedChatIds?: number[]
|
||||
webappPort?: number
|
||||
webappUrl?: string
|
||||
corsOrigins?: string[]
|
||||
}
|
||||
|
||||
export interface CliApiTokenResult {
|
||||
@@ -53,7 +59,7 @@ function isWeakToken(token: string): boolean {
|
||||
* Read settings from file, preserving all existing fields.
|
||||
* Returns null if file exists but cannot be parsed (to avoid data loss).
|
||||
*/
|
||||
async function readSettings(settingsFile: string): Promise<Settings | null> {
|
||||
export async function readSettings(settingsFile: string): Promise<Settings | null> {
|
||||
if (!existsSync(settingsFile)) {
|
||||
return {}
|
||||
}
|
||||
@@ -70,7 +76,7 @@ async function readSettings(settingsFile: string): Promise<Settings | null> {
|
||||
/**
|
||||
* Write settings to file atomically (temp file + rename)
|
||||
*/
|
||||
async function writeSettings(settingsFile: string, settings: Settings): Promise<void> {
|
||||
export async function writeSettings(settingsFile: string, settings: Settings): Promise<void> {
|
||||
const dir = dirname(settingsFile)
|
||||
if (!existsSync(dir)) {
|
||||
await mkdir(dir, { recursive: true, mode: 0o700 })
|
||||
|
||||
Reference in New Issue
Block a user