feat(db): enforce runtime RLS boundaries

This commit is contained in:
2026-03-22 22:49:47 +04:00
parent 7665af0268
commit 97b5edcc0a
24 changed files with 2054 additions and 545 deletions

View File

@@ -4,7 +4,12 @@ LOG_LEVEL=info
PORT=3000
# Database
# Owner connection for migrations, seed, and schema checks only
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:54322/postgres
# Runtime connection used by mini app and authenticated user-triggered API flows
APP_DATABASE_URL=postgres://housebot_app:housebot_app@127.0.0.1:54322/postgres
# Runtime connection used by bot ingestion, reminders, schedulers, and other worker flows
WORKER_DATABASE_URL=postgres://housebot_worker:housebot_worker@127.0.0.1:54322/postgres
DB_SCHEMA=public
# Telegram

View File

@@ -4,6 +4,8 @@ export interface BotRuntimeConfig {
telegramBotToken: string
telegramWebhookSecret: string
telegramWebhookPath: string
appDatabaseUrl?: string
workerDatabaseUrl?: string
databaseUrl?: string
purchaseTopicIngestionEnabled: boolean
financeCommandsEnabled: boolean
@@ -101,20 +103,22 @@ function parsePositiveInteger(raw: string | undefined, fallback: number, key: st
export function getBotRuntimeConfig(env: NodeJS.ProcessEnv = process.env): BotRuntimeConfig {
const databaseUrl = parseOptionalValue(env.DATABASE_URL)
const appDatabaseUrl = parseOptionalValue(env.APP_DATABASE_URL)
const workerDatabaseUrl = parseOptionalValue(env.WORKER_DATABASE_URL)
const schedulerSharedSecret = parseOptionalValue(env.SCHEDULER_SHARED_SECRET)
const schedulerOidcAllowedEmails = parseOptionalCsv(env.SCHEDULER_OIDC_ALLOWED_EMAILS)
const miniAppAllowedOrigins = parseOptionalCsv(env.MINI_APP_ALLOWED_ORIGINS)
const miniAppUrl = parseOptionalValue(env.MINI_APP_URL)
const purchaseTopicIngestionEnabled = databaseUrl !== undefined
const financeCommandsEnabled = databaseUrl !== undefined
const anonymousFeedbackEnabled = databaseUrl !== undefined
const assistantEnabled = databaseUrl !== undefined
const miniAppAuthEnabled = databaseUrl !== undefined
const purchaseTopicIngestionEnabled = workerDatabaseUrl !== undefined
const financeCommandsEnabled = workerDatabaseUrl !== undefined
const anonymousFeedbackEnabled = workerDatabaseUrl !== undefined
const assistantEnabled = workerDatabaseUrl !== undefined
const miniAppAuthEnabled = appDatabaseUrl !== undefined
const hasSchedulerOidcConfig = schedulerOidcAllowedEmails.length > 0
const reminderJobsEnabled =
databaseUrl !== undefined && (schedulerSharedSecret !== undefined || hasSchedulerOidcConfig)
workerDatabaseUrl !== undefined &&
(schedulerSharedSecret !== undefined || hasSchedulerOidcConfig)
const runtime: BotRuntimeConfig = {
port: parsePort(env.PORT),
@@ -173,6 +177,12 @@ export function getBotRuntimeConfig(env: NodeJS.ProcessEnv = process.env): BotRu
if (databaseUrl !== undefined) {
runtime.databaseUrl = databaseUrl
}
if (appDatabaseUrl !== undefined) {
runtime.appDatabaseUrl = appDatabaseUrl
}
if (workerDatabaseUrl !== undefined) {
runtime.workerDatabaseUrl = workerDatabaseUrl
}
if (schedulerSharedSecret !== undefined) {
runtime.schedulerSharedSecret = schedulerSharedSecret
}

View File

@@ -47,7 +47,11 @@ import { createReminderJobsHandler } from './reminder-jobs'
import { registerReminderTopicUtilities } from './reminder-topic-utilities'
import { createSchedulerRequestAuthorizer } from './scheduler-auth'
import { createBotWebhookServer } from './server'
import { createMiniAppAuthHandler, createMiniAppJoinHandler } from './miniapp-auth'
import {
createMiniAppAuthHandler,
createMiniAppJoinHandler,
type MiniAppAuthorizedSession
} from './miniapp-auth'
import { createMiniAppDashboardHandler } from './miniapp-dashboard'
import {
createMiniAppApproveMemberHandler,
@@ -90,13 +94,13 @@ configureLogger({
const logger = getLogger('runtime')
const shutdownTasks: Array<() => Promise<void>> = []
const householdConfigurationRepositoryClient = runtime.databaseUrl
? createDbHouseholdConfigurationRepository(runtime.databaseUrl)
const workerHouseholdConfigurationRepositoryClient = runtime.workerDatabaseUrl
? createDbHouseholdConfigurationRepository(runtime.workerDatabaseUrl)
: null
const bot = createTelegramBot(
runtime.telegramBotToken,
getLogger('telegram'),
householdConfigurationRepositoryClient?.repository
workerHouseholdConfigurationRepositoryClient?.repository
)
bot.botInfo = await bot.api.getMe()
const webhookHandler = webhookCallback(bot, 'std/http', {
@@ -111,29 +115,23 @@ const paymentConfirmationServices = new Map<
const exchangeRateProvider = createNbgExchangeRateProvider({
logger: getLogger('fx')
})
const householdOnboardingService = householdConfigurationRepositoryClient
const householdOnboardingService = workerHouseholdConfigurationRepositoryClient
? createHouseholdOnboardingService({
repository: householdConfigurationRepositoryClient.repository
repository: workerHouseholdConfigurationRepositoryClient.repository
})
: null
const miniAppAdminService = householdConfigurationRepositoryClient
? createMiniAppAdminService(householdConfigurationRepositoryClient.repository)
: null
const localePreferenceService = householdConfigurationRepositoryClient
? createLocalePreferenceService(householdConfigurationRepositoryClient.repository)
: null
const telegramPendingActionRepositoryClient = runtime.databaseUrl
? createDbTelegramPendingActionRepository(runtime.databaseUrl!)
const telegramPendingActionRepositoryClient = runtime.workerDatabaseUrl
? createDbTelegramPendingActionRepository(runtime.workerDatabaseUrl!)
: null
const processedBotMessageRepositoryClient =
runtime.databaseUrl && runtime.assistantEnabled
? createDbProcessedBotMessageRepository(runtime.databaseUrl!)
runtime.workerDatabaseUrl && runtime.assistantEnabled
? createDbProcessedBotMessageRepository(runtime.workerDatabaseUrl!)
: null
const purchaseRepositoryClient = runtime.databaseUrl
? createPurchaseMessageRepository(runtime.databaseUrl!)
const purchaseRepositoryClient = runtime.workerDatabaseUrl
? createPurchaseMessageRepository(runtime.workerDatabaseUrl!)
: null
const topicMessageHistoryRepositoryClient = runtime.databaseUrl
? createDbTopicMessageHistoryRepository(runtime.databaseUrl!)
const topicMessageHistoryRepositoryClient = runtime.workerDatabaseUrl
? createDbTopicMessageHistoryRepository(runtime.workerDatabaseUrl!)
: null
const purchaseInterpreter = createOpenAiPurchaseInterpreter(
runtime.openaiApiKey,
@@ -169,6 +167,167 @@ const anonymousFeedbackServices = new Map<
string,
ReturnType<typeof createAnonymousFeedbackService>
>()
const appHouseholdConfigurationRepositoryClients = new Map<
string,
ReturnType<typeof createDbHouseholdConfigurationRepository>
>()
const appOnboardingServices = new Map<string, ReturnType<typeof createHouseholdOnboardingService>>()
const appFinanceRepositoryClients = new Map<string, ReturnType<typeof createDbFinanceRepository>>()
const appFinanceServices = new Map<string, ReturnType<typeof createFinanceCommandService>>()
const appMiniAppAdminServices = new Map<string, ReturnType<typeof createMiniAppAdminService>>()
const appLocalePreferenceServices = new Map<
string,
ReturnType<typeof createLocalePreferenceService>
>()
function miniAppSessionKey(session: MiniAppAuthorizedSession): string {
return [
session.telegramUserId,
session.member.householdId,
session.member.id,
session.member.isAdmin ? 'admin' : 'member'
].join(':')
}
function appHouseholdConfigurationRepositoryKey(input: {
telegramUserId: string
householdId?: string
memberId?: string
isAdmin?: boolean
}): string {
return [
input.telegramUserId,
input.householdId ?? 'none',
input.memberId ?? 'none',
input.isAdmin === true ? 'admin' : 'member'
].join(':')
}
function appHouseholdConfigurationRepositoryForContext(input: {
telegramUserId: string
householdId?: string
memberId?: string
isAdmin?: boolean
}) {
const key = appHouseholdConfigurationRepositoryKey(input)
const existing = appHouseholdConfigurationRepositoryClients.get(key)
if (existing) {
return existing
}
const repositoryClient = createDbHouseholdConfigurationRepository(runtime.appDatabaseUrl!, {
sessionContext: {
telegramUserId: input.telegramUserId,
...(input.householdId
? {
householdId: input.householdId
}
: {}),
...(input.memberId
? {
memberId: input.memberId
}
: {}),
...(input.isAdmin !== undefined
? {
isAdmin: input.isAdmin
}
: {})
}
})
appHouseholdConfigurationRepositoryClients.set(key, repositoryClient)
shutdownTasks.push(repositoryClient.close)
return repositoryClient
}
function appOnboardingServiceForTelegramUserId(telegramUserId: string) {
const existing = appOnboardingServices.get(telegramUserId)
if (existing) {
return existing
}
const service = createHouseholdOnboardingService({
repository: appHouseholdConfigurationRepositoryForContext({
telegramUserId
}).repository
})
appOnboardingServices.set(telegramUserId, service)
return service
}
function appHouseholdConfigurationRepositoryForSession(session: MiniAppAuthorizedSession) {
return appHouseholdConfigurationRepositoryForContext({
telegramUserId: session.telegramUserId,
householdId: session.member.householdId,
memberId: session.member.id,
isAdmin: session.member.isAdmin
})
}
function appFinanceServiceForSession(session: MiniAppAuthorizedSession) {
const key = miniAppSessionKey(session)
const existing = appFinanceServices.get(key)
if (existing) {
return existing
}
const repositoryClient = createDbFinanceRepository(
runtime.appDatabaseUrl!,
session.member.householdId,
{
sessionContext: {
telegramUserId: session.telegramUserId,
householdId: session.member.householdId,
memberId: session.member.id,
...(session.member.isAdmin !== undefined
? {
isAdmin: session.member.isAdmin
}
: {})
}
}
)
appFinanceRepositoryClients.set(key, repositoryClient)
shutdownTasks.push(repositoryClient.close)
const service = createFinanceCommandService({
householdId: session.member.householdId,
repository: repositoryClient.repository,
householdConfigurationRepository:
appHouseholdConfigurationRepositoryForSession(session).repository,
exchangeRateProvider
})
appFinanceServices.set(key, service)
return service
}
function appMiniAppAdminServiceForSession(session: MiniAppAuthorizedSession) {
const key = miniAppSessionKey(session)
const existing = appMiniAppAdminServices.get(key)
if (existing) {
return existing
}
const service = createMiniAppAdminService(
appHouseholdConfigurationRepositoryForSession(session).repository
)
appMiniAppAdminServices.set(key, service)
return service
}
function appLocalePreferenceServiceForSession(session: MiniAppAuthorizedSession) {
const key = miniAppSessionKey(session)
const existing = appLocalePreferenceServices.get(key)
if (existing) {
return existing
}
const service = createLocalePreferenceService(
appHouseholdConfigurationRepositoryForSession(session).repository
)
appLocalePreferenceServices.set(key, service)
return service
}
function financeServiceForHousehold(householdId: string) {
const existing = financeServices.get(householdId)
@@ -180,7 +339,7 @@ function financeServiceForHousehold(householdId: string) {
const service = createFinanceCommandService({
householdId,
repository: repositoryClient.repository,
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient!.repository,
exchangeRateProvider
})
financeServices.set(householdId, service)
@@ -193,7 +352,7 @@ function financeRepositoryForHousehold(householdId: string) {
return existing
}
const repositoryClient = createDbFinanceRepository(runtime.databaseUrl!, householdId)
const repositoryClient = createDbFinanceRepository(runtime.workerDatabaseUrl!, householdId)
financeRepositoryClients.set(householdId, repositoryClient)
shutdownTasks.push(repositoryClient.close)
return repositoryClient
@@ -209,7 +368,7 @@ function paymentConfirmationServiceForHousehold(householdId: string) {
householdId,
financeService: financeServiceForHousehold(householdId),
repository: financeRepositoryForHousehold(householdId).repository,
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient!.repository,
exchangeRateProvider
})
paymentConfirmationServices.set(householdId, service)
@@ -222,7 +381,10 @@ function anonymousFeedbackServiceForHousehold(householdId: string) {
return existing
}
const repositoryClient = createDbAnonymousFeedbackRepository(runtime.databaseUrl!, householdId)
const repositoryClient = createDbAnonymousFeedbackRepository(
runtime.workerDatabaseUrl!,
householdId
)
anonymousFeedbackRepositoryClients.set(householdId, repositoryClient)
shutdownTasks.push(repositoryClient.close)
@@ -231,8 +393,8 @@ function anonymousFeedbackServiceForHousehold(householdId: string) {
return service
}
if (householdConfigurationRepositoryClient) {
shutdownTasks.push(householdConfigurationRepositoryClient.close)
if (workerHouseholdConfigurationRepositoryClient) {
shutdownTasks.push(workerHouseholdConfigurationRepositoryClient.close)
}
if (telegramPendingActionRepositoryClient) {
@@ -251,10 +413,10 @@ if (topicMessageHistoryRepositoryClient) {
shutdownTasks.push(topicMessageHistoryRepositoryClient.close)
}
if (purchaseRepositoryClient && householdConfigurationRepositoryClient) {
if (purchaseRepositoryClient && workerHouseholdConfigurationRepositoryClient) {
registerConfiguredPurchaseTopicIngestion(
bot,
householdConfigurationRepositoryClient.repository,
workerHouseholdConfigurationRepositoryClient.repository,
purchaseRepositoryClient.repository,
{
...(topicProcessor
@@ -280,7 +442,7 @@ if (purchaseRepositoryClient && householdConfigurationRepositoryClient) {
registerConfiguredPaymentTopicIngestion(
bot,
householdConfigurationRepositoryClient.repository,
workerHouseholdConfigurationRepositoryClient.repository,
telegramPendingActionRepositoryClient!.repository,
financeServiceForHousehold,
paymentConfirmationServiceForHousehold,
@@ -306,13 +468,13 @@ if (purchaseRepositoryClient && householdConfigurationRepositoryClient) {
event: 'runtime.feature_disabled',
feature: 'purchase-topic-ingestion'
},
'Purchase topic ingestion is disabled. Set DATABASE_URL to enable Telegram topic lookups.'
'Purchase topic ingestion is disabled. Set WORKER_DATABASE_URL to enable Telegram topic lookups.'
)
}
if (runtime.financeCommandsEnabled) {
const financeCommands = createFinanceCommandsService({
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient!.repository,
financeServiceForHousehold,
...(runtime.miniAppUrl
? {
@@ -329,21 +491,21 @@ if (runtime.financeCommandsEnabled) {
event: 'runtime.feature_disabled',
feature: 'finance-commands'
},
'Finance commands are disabled. Set DATABASE_URL to enable household lookups.'
'Finance commands are disabled. Set WORKER_DATABASE_URL to enable household lookups.'
)
}
if (householdConfigurationRepositoryClient) {
if (workerHouseholdConfigurationRepositoryClient) {
registerHouseholdSetupCommands({
bot,
householdSetupService: createHouseholdSetupService(
householdConfigurationRepositoryClient.repository
workerHouseholdConfigurationRepositoryClient.repository
),
householdAdminService: createHouseholdAdminService(
householdConfigurationRepositoryClient.repository
workerHouseholdConfigurationRepositoryClient.repository
),
householdOnboardingService: householdOnboardingService!,
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient.repository,
...(telegramPendingActionRepositoryClient
? {
promptRepository: telegramPendingActionRepositoryClient.repository
@@ -362,20 +524,22 @@ if (householdConfigurationRepositoryClient) {
event: 'runtime.feature_disabled',
feature: 'household-setup'
},
'Household setup commands are disabled. Set DATABASE_URL to enable.'
'Household setup commands are disabled. Set WORKER_DATABASE_URL to enable.'
)
}
const reminderJobs = runtime.reminderJobsEnabled
? (() => {
const reminderRepositoryClient = createDbReminderDispatchRepository(runtime.databaseUrl!)
const reminderRepositoryClient = createDbReminderDispatchRepository(
runtime.workerDatabaseUrl!
)
const reminderService = createReminderJobService(reminderRepositoryClient.repository)
shutdownTasks.push(reminderRepositoryClient.close)
return createReminderJobsHandler({
listReminderTargets: () =>
householdConfigurationRepositoryClient!.repository.listReminderTargets(),
workerHouseholdConfigurationRepositoryClient!.repository.listReminderTargets(),
ensureBillingCycle: async ({ householdId, at }) => {
await financeServiceForHousehold(householdId).ensureExpectedCycle(at)
},
@@ -426,19 +590,19 @@ if (!runtime.reminderJobsEnabled) {
event: 'runtime.feature_disabled',
feature: 'reminder-jobs'
},
'Reminder jobs are disabled. Set DATABASE_URL and either SCHEDULER_SHARED_SECRET or SCHEDULER_OIDC_ALLOWED_EMAILS to enable.'
'Reminder jobs are disabled. Set WORKER_DATABASE_URL and either SCHEDULER_SHARED_SECRET or SCHEDULER_OIDC_ALLOWED_EMAILS to enable.'
)
}
if (
runtime.anonymousFeedbackEnabled &&
householdConfigurationRepositoryClient &&
workerHouseholdConfigurationRepositoryClient &&
telegramPendingActionRepositoryClient
) {
registerAnonymousFeedback({
bot,
anonymousFeedbackServiceForHousehold,
householdConfigurationRepository: householdConfigurationRepositoryClient!.repository,
householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient!.repository,
promptRepository: telegramPendingActionRepositoryClient!.repository,
logger: getLogger('anonymous-feedback')
})
@@ -448,19 +612,19 @@ if (
event: 'runtime.feature_disabled',
feature: 'anonymous-feedback'
},
'Anonymous feedback is disabled. Set DATABASE_URL to enable household and topic lookups.'
'Anonymous feedback is disabled. Set WORKER_DATABASE_URL to enable household and topic lookups.'
)
}
if (
runtime.assistantEnabled &&
householdConfigurationRepositoryClient &&
workerHouseholdConfigurationRepositoryClient &&
telegramPendingActionRepositoryClient
) {
if (processedBotMessageRepositoryClient) {
registerDmAssistant({
bot,
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient.repository,
messageProcessingRepository: processedBotMessageRepositoryClient.repository,
promptRepository: telegramPendingActionRepositoryClient.repository,
financeServiceForHousehold,
@@ -492,7 +656,7 @@ if (
} else {
registerDmAssistant({
bot,
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient.repository,
promptRepository: telegramPendingActionRepositoryClient.repository,
financeServiceForHousehold,
memoryStore: assistantMemoryStore,
@@ -523,10 +687,10 @@ if (
}
}
if (householdConfigurationRepositoryClient && telegramPendingActionRepositoryClient) {
if (workerHouseholdConfigurationRepositoryClient && telegramPendingActionRepositoryClient) {
registerReminderTopicUtilities({
bot,
householdConfigurationRepository: householdConfigurationRepositoryClient.repository,
householdConfigurationRepository: workerHouseholdConfigurationRepositoryClient.repository,
promptRepository: telegramPendingActionRepositoryClient.repository,
financeServiceForHousehold,
logger: getLogger('reminder-utilities')
@@ -537,272 +701,272 @@ const server = createBotWebhookServer({
webhookPath: runtime.telegramWebhookPath,
webhookSecret: runtime.telegramWebhookSecret,
webhookHandler,
miniAppAuth: householdOnboardingService
miniAppAuth: runtime.miniAppAuthEnabled
? createMiniAppAuthHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
logger: getLogger('miniapp-auth')
})
: undefined,
miniAppJoin: householdOnboardingService
miniAppJoin: runtime.miniAppAuthEnabled
? createMiniAppJoinHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
logger: getLogger('miniapp-auth')
})
: undefined,
miniAppDashboard: householdOnboardingService
miniAppDashboard: runtime.miniAppAuthEnabled
? createMiniAppDashboardHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
financeServiceForHousehold,
onboardingService: householdOnboardingService!,
financeServiceForSession: appFinanceServiceForSession,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
logger: getLogger('miniapp-dashboard')
})
: undefined,
miniAppPendingMembers: householdOnboardingService
miniAppPendingMembers: runtime.miniAppAuthEnabled
? createMiniAppPendingMembersHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppApproveMember: householdOnboardingService
miniAppApproveMember: runtime.miniAppAuthEnabled
? createMiniAppApproveMemberHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppRejectMember: householdOnboardingService
miniAppRejectMember: runtime.miniAppAuthEnabled
? createMiniAppRejectMemberHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppSettings: householdOnboardingService
miniAppSettings: runtime.miniAppAuthEnabled
? createMiniAppSettingsHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
assistantUsageTracker,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppUpdateSettings: householdOnboardingService
miniAppUpdateSettings: runtime.miniAppAuthEnabled
? createMiniAppUpdateSettingsHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppUpsertUtilityCategory: householdOnboardingService
miniAppUpsertUtilityCategory: runtime.miniAppAuthEnabled
? createMiniAppUpsertUtilityCategoryHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppPromoteMember: householdOnboardingService
miniAppPromoteMember: runtime.miniAppAuthEnabled
? createMiniAppPromoteMemberHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppUpdateOwnDisplayName: householdOnboardingService
miniAppUpdateOwnDisplayName: runtime.miniAppAuthEnabled
? createMiniAppUpdateOwnDisplayNameHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppUpdateMemberDisplayName: householdOnboardingService
miniAppUpdateMemberDisplayName: runtime.miniAppAuthEnabled
? createMiniAppUpdateMemberDisplayNameHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppUpdateMemberRentWeight: householdOnboardingService
miniAppUpdateMemberRentWeight: runtime.miniAppAuthEnabled
? createMiniAppUpdateMemberRentWeightHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppUpdateMemberStatus: householdOnboardingService
miniAppUpdateMemberStatus: runtime.miniAppAuthEnabled
? createMiniAppUpdateMemberStatusHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppUpdateMemberAbsencePolicy: householdOnboardingService
miniAppUpdateMemberAbsencePolicy: runtime.miniAppAuthEnabled
? createMiniAppUpdateMemberAbsencePolicyHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
miniAppAdminServiceForSession: appMiniAppAdminServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppBillingCycle: householdOnboardingService
miniAppBillingCycle: runtime.miniAppAuthEnabled
? createMiniAppBillingCycleHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppOpenCycle: householdOnboardingService
miniAppOpenCycle: runtime.miniAppAuthEnabled
? createMiniAppOpenCycleHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppCloseCycle: householdOnboardingService
miniAppCloseCycle: runtime.miniAppAuthEnabled
? createMiniAppCloseCycleHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppRentUpdate: householdOnboardingService
miniAppRentUpdate: runtime.miniAppAuthEnabled
? createMiniAppRentUpdateHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppAddUtilityBill: householdOnboardingService
miniAppAddUtilityBill: runtime.miniAppAuthEnabled
? createMiniAppAddUtilityBillHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppSubmitUtilityBill: householdOnboardingService
miniAppSubmitUtilityBill: runtime.miniAppAuthEnabled
? createMiniAppSubmitUtilityBillHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppUpdateUtilityBill: householdOnboardingService
miniAppUpdateUtilityBill: runtime.miniAppAuthEnabled
? createMiniAppUpdateUtilityBillHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppDeleteUtilityBill: householdOnboardingService
miniAppDeleteUtilityBill: runtime.miniAppAuthEnabled
? createMiniAppDeleteUtilityBillHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppAddPurchase: householdOnboardingService
miniAppAddPurchase: runtime.miniAppAuthEnabled
? createMiniAppAddPurchaseHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppUpdatePurchase: householdOnboardingService
miniAppUpdatePurchase: runtime.miniAppAuthEnabled
? createMiniAppUpdatePurchaseHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppDeletePurchase: householdOnboardingService
miniAppDeletePurchase: runtime.miniAppAuthEnabled
? createMiniAppDeletePurchaseHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppAddPayment: householdOnboardingService
miniAppAddPayment: runtime.miniAppAuthEnabled
? createMiniAppAddPaymentHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppUpdatePayment: householdOnboardingService
miniAppUpdatePayment: runtime.miniAppAuthEnabled
? createMiniAppUpdatePaymentHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppDeletePayment: householdOnboardingService
miniAppDeletePayment: runtime.miniAppAuthEnabled
? createMiniAppDeletePaymentHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
financeServiceForHousehold,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
financeServiceForSession: appFinanceServiceForSession,
logger: getLogger('miniapp-billing')
})
: undefined,
miniAppLocalePreference: householdOnboardingService
miniAppLocalePreference: runtime.miniAppAuthEnabled
? createMiniAppLocalePreferenceHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
localePreferenceService: localePreferenceService!,
onboardingServiceForTelegramUserId: appOnboardingServiceForTelegramUserId,
localePreferenceServiceForSession: appLocalePreferenceServiceForSession,
logger: getLogger('miniapp-admin')
})
: undefined,

View File

@@ -12,12 +12,59 @@ import type { AssistantUsageTracker } from './dm-assistant'
import {
allowedMiniAppOrigin,
type MiniAppAuthorizedSession,
createMiniAppSessionService,
miniAppErrorResponse,
miniAppJsonResponse,
readMiniAppRequestPayload
} from './miniapp-auth'
interface MiniAppAdminHandlerBaseOptions {
allowedOrigins: readonly string[]
botToken: string
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
miniAppAdminServiceForSession?: (session: MiniAppAuthorizedSession) => MiniAppAdminService
miniAppAdminService?: MiniAppAdminService
logger?: Logger
}
function createConfiguredMiniAppSessionService(options: {
botToken: string
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
}) {
return createMiniAppSessionService({
botToken: options.botToken,
...(options.onboardingServiceForTelegramUserId
? {
onboardingServiceForTelegramUserId: options.onboardingServiceForTelegramUserId
}
: {}),
...(options.onboardingService
? {
onboardingService: options.onboardingService
}
: {})
})
}
function resolveMiniAppAdminService(
options: Pick<
MiniAppAdminHandlerBaseOptions,
'miniAppAdminServiceForSession' | 'miniAppAdminService'
>,
session: MiniAppAuthorizedSession
): MiniAppAdminService {
const service = options.miniAppAdminServiceForSession?.(session) ?? options.miniAppAdminService
if (!service) {
throw new Error('Mini app admin service is not configured')
}
return service
}
async function readApprovalPayload(request: Request): Promise<{
initData: string
pendingTelegramUserId: string
@@ -401,6 +448,7 @@ async function authenticateAdminSession(
| Response
| {
member: NonNullable<MiniAppSessionResult['member']>
telegramUserId: string
}
> {
const payload = await readMiniAppRequestPayload(request)
@@ -413,7 +461,7 @@ async function authenticateAdminSession(
return miniAppJsonResponse({ ok: false, error: 'Invalid Telegram init data' }, 401, origin)
}
if (!session.authorized || !session.member) {
if (!session.authorized || !session.member || !session.telegramUser) {
return miniAppJsonResponse(
{ ok: false, error: 'Admin access required for active household members' },
403,
@@ -430,23 +478,15 @@ async function authenticateAdminSession(
}
return {
member: session.member
member: session.member,
telegramUserId: session.telegramUser.id
}
}
export function createMiniAppPendingMembersHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
export function createMiniAppPendingMembersHandler(options: MiniAppAdminHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -478,6 +518,7 @@ export function createMiniAppPendingMembersHandler(options: {
if (
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active' ||
!session.member.isAdmin
) {
@@ -488,7 +529,10 @@ export function createMiniAppPendingMembersHandler(options: {
)
}
const result = await options.miniAppAdminService.listPendingMembers({
const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).listPendingMembers({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin
})
@@ -513,20 +557,14 @@ export function createMiniAppPendingMembersHandler(options: {
}
}
export function createMiniAppSettingsHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
export function createMiniAppSettingsHandler(
options: MiniAppAdminHandlerBaseOptions & {
assistantUsageTracker?: AssistantUsageTracker
logger?: Logger
}): {
}
): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -545,9 +583,12 @@ export function createMiniAppSettingsHandler(options: {
if (auth instanceof Response) {
return auth
}
const { member } = auth
const { member, telegramUserId } = auth
const result = await options.miniAppAdminService.getSettings({
const result = await resolveMiniAppAdminService(options, {
member,
telegramUserId
}).getSettings({
householdId: member.householdId,
actorIsAdmin: member.isAdmin
})
@@ -580,19 +621,10 @@ export function createMiniAppSettingsHandler(options: {
}
}
export function createMiniAppUpdateSettingsHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
export function createMiniAppUpdateSettingsHandler(options: MiniAppAdminHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -623,6 +655,7 @@ export function createMiniAppUpdateSettingsHandler(options: {
if (
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active' ||
!session.member.isAdmin
) {
@@ -633,7 +666,10 @@ export function createMiniAppUpdateSettingsHandler(options: {
)
}
const result = await options.miniAppAdminService.updateSettings({
const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateSettings({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin,
...(payload.householdName !== undefined
@@ -715,19 +751,12 @@ export function createMiniAppUpdateSettingsHandler(options: {
}
}
export function createMiniAppUpsertUtilityCategoryHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
export function createMiniAppUpsertUtilityCategoryHandler(
options: MiniAppAdminHandlerBaseOptions
): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -758,6 +787,7 @@ export function createMiniAppUpsertUtilityCategoryHandler(options: {
if (
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active' ||
!session.member.isAdmin
) {
@@ -768,7 +798,10 @@ export function createMiniAppUpsertUtilityCategoryHandler(options: {
)
}
const result = await options.miniAppAdminService.upsertUtilityCategory({
const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).upsertUtilityCategory({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin,
...(payload.slug
@@ -811,19 +844,10 @@ export function createMiniAppUpsertUtilityCategoryHandler(options: {
}
}
export function createMiniAppPromoteMemberHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
export function createMiniAppPromoteMemberHandler(options: MiniAppAdminHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -854,6 +878,7 @@ export function createMiniAppPromoteMemberHandler(options: {
if (
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active' ||
!session.member.isAdmin
) {
@@ -864,7 +889,10 @@ export function createMiniAppPromoteMemberHandler(options: {
)
}
const result = await options.miniAppAdminService.promoteMemberToAdmin({
const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).promoteMemberToAdmin({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin,
memberId: payload.memberId
@@ -898,19 +926,10 @@ export function createMiniAppPromoteMemberHandler(options: {
}
}
export function createMiniAppUpdateOwnDisplayNameHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
export function createMiniAppUpdateOwnDisplayNameHandler(options: MiniAppAdminHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -930,7 +949,7 @@ export function createMiniAppUpdateOwnDisplayNameHandler(options: {
initData: payload.initData
})
if (!session || !session.authorized || !session.member) {
if (!session || !session.authorized || !session.member || !session.telegramUser) {
return miniAppJsonResponse(
{ ok: false, error: 'Active household membership required' },
session ? 403 : 401,
@@ -938,7 +957,10 @@ export function createMiniAppUpdateOwnDisplayNameHandler(options: {
)
}
const result = await options.miniAppAdminService.updateOwnDisplayName({
const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateOwnDisplayName({
householdId: session.member.householdId,
actorMemberId: session.member.id,
displayName: payload.displayName
@@ -974,19 +996,12 @@ export function createMiniAppUpdateOwnDisplayNameHandler(options: {
}
}
export function createMiniAppUpdateMemberRentWeightHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
export function createMiniAppUpdateMemberRentWeightHandler(
options: MiniAppAdminHandlerBaseOptions
): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -1017,6 +1032,7 @@ export function createMiniAppUpdateMemberRentWeightHandler(options: {
if (
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active' ||
!session.member.isAdmin
) {
@@ -1027,7 +1043,10 @@ export function createMiniAppUpdateMemberRentWeightHandler(options: {
)
}
const result = await options.miniAppAdminService.updateMemberRentShareWeight({
const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateMemberRentShareWeight({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin,
memberId: payload.memberId,
@@ -1070,19 +1089,12 @@ export function createMiniAppUpdateMemberRentWeightHandler(options: {
}
}
export function createMiniAppUpdateMemberDisplayNameHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
export function createMiniAppUpdateMemberDisplayNameHandler(
options: MiniAppAdminHandlerBaseOptions
): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -1106,6 +1118,7 @@ export function createMiniAppUpdateMemberDisplayNameHandler(options: {
!session ||
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active' ||
!session.member.isAdmin
) {
@@ -1120,7 +1133,10 @@ export function createMiniAppUpdateMemberDisplayNameHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing memberId' }, 400, origin)
}
const result = await options.miniAppAdminService.updateMemberDisplayName({
const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateMemberDisplayName({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin,
memberId: payload.memberId,
@@ -1163,19 +1179,10 @@ export function createMiniAppUpdateMemberDisplayNameHandler(options: {
}
}
export function createMiniAppUpdateMemberStatusHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
export function createMiniAppUpdateMemberStatusHandler(options: MiniAppAdminHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -1199,6 +1206,7 @@ export function createMiniAppUpdateMemberStatusHandler(options: {
!session ||
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active' ||
!session.member.isAdmin
) {
@@ -1209,7 +1217,10 @@ export function createMiniAppUpdateMemberStatusHandler(options: {
)
}
const result = await options.miniAppAdminService.updateMemberStatus({
const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateMemberStatus({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin,
memberId: payload.memberId,
@@ -1244,19 +1255,12 @@ export function createMiniAppUpdateMemberStatusHandler(options: {
}
}
export function createMiniAppUpdateMemberAbsencePolicyHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
export function createMiniAppUpdateMemberAbsencePolicyHandler(
options: MiniAppAdminHandlerBaseOptions
): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -1280,6 +1284,7 @@ export function createMiniAppUpdateMemberAbsencePolicyHandler(options: {
!session ||
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active' ||
!session.member.isAdmin
) {
@@ -1290,7 +1295,10 @@ export function createMiniAppUpdateMemberAbsencePolicyHandler(options: {
)
}
const result = await options.miniAppAdminService.updateMemberAbsencePolicy({
const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).updateMemberAbsencePolicy({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin,
memberId: payload.memberId,
@@ -1325,19 +1333,10 @@ export function createMiniAppUpdateMemberAbsencePolicyHandler(options: {
}
}
export function createMiniAppApproveMemberHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
export function createMiniAppApproveMemberHandler(options: MiniAppAdminHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -1368,6 +1367,7 @@ export function createMiniAppApproveMemberHandler(options: {
if (
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active' ||
!session.member.isAdmin
) {
@@ -1378,7 +1378,10 @@ export function createMiniAppApproveMemberHandler(options: {
)
}
const result = await options.miniAppAdminService.approvePendingMember({
const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).approvePendingMember({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin,
pendingTelegramUserId: payload.pendingTelegramUserId
@@ -1410,19 +1413,10 @@ export function createMiniAppApproveMemberHandler(options: {
}
}
export function createMiniAppRejectMemberHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
export function createMiniAppRejectMemberHandler(options: MiniAppAdminHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppSessionService(options)
return {
handler: async (request) => {
@@ -1453,6 +1447,7 @@ export function createMiniAppRejectMemberHandler(options: {
if (
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active' ||
!session.member.isAdmin
) {
@@ -1463,7 +1458,10 @@ export function createMiniAppRejectMemberHandler(options: {
)
}
const result = await options.miniAppAdminService.rejectPendingMember({
const result = await resolveMiniAppAdminService(options, {
member: session.member,
telegramUserId: session.telegramUser.id
}).rejectPendingMember({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin,
pendingTelegramUserId: payload.pendingTelegramUserId

View File

@@ -115,12 +115,28 @@ export interface MiniAppSessionResult {
}
}
export interface MiniAppAuthorizedSession {
member: NonNullable<MiniAppSessionResult['member']>
telegramUserId: string
}
export function createMiniAppSessionService(options: {
botToken: string
onboardingService: HouseholdOnboardingService
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
}): {
authenticate: (payload: MiniAppRequestPayload) => Promise<MiniAppSessionResult | null>
} {
const resolveOnboardingService =
options.onboardingServiceForTelegramUserId ??
(() => {
if (!options.onboardingService) {
throw new Error('Mini app onboarding service is not configured')
}
return options.onboardingService
})
return {
authenticate: async (payload) => {
if (!payload.initData) {
@@ -132,7 +148,7 @@ export function createMiniAppSessionService(options: {
return null
}
const access = await options.onboardingService.getMiniAppAccess({
const access = await resolveOnboardingService(telegramUser.id).getMiniAppAccess({
identity: {
telegramUserId: telegramUser.id,
displayName:
@@ -190,14 +206,24 @@ export function createMiniAppSessionService(options: {
export function createMiniAppAuthHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
...(options.onboardingServiceForTelegramUserId
? {
onboardingServiceForTelegramUserId: options.onboardingServiceForTelegramUserId
}
: {}),
...(options.onboardingService
? {
onboardingService: options.onboardingService
}
: {})
})
return {
@@ -264,7 +290,8 @@ export function createMiniAppAuthHandler(options: {
export function createMiniAppJoinHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response>
@@ -304,7 +331,13 @@ export function createMiniAppJoinHandler(options: {
)
}
const result = await options.onboardingService.joinHousehold({
const onboardingService =
options.onboardingServiceForTelegramUserId?.(telegramUser.id) ?? options.onboardingService
if (!onboardingService) {
throw new Error('Mini app onboarding service is not configured')
}
const result = await onboardingService.joinHousehold({
identity: {
telegramUserId: telegramUser.id,
displayName:

View File

@@ -2,7 +2,7 @@ import type { FinanceCommandService, HouseholdOnboardingService } from '@househo
import { BillingPeriod } from '@household/domain'
import type { Logger } from '@household/observability'
import type { HouseholdConfigurationRepository } from '@household/ports'
import type { MiniAppSessionResult } from './miniapp-auth'
import type { MiniAppAuthorizedSession, MiniAppSessionResult } from './miniapp-auth'
import {
allowedMiniAppOrigin,
@@ -12,6 +12,54 @@ import {
readMiniAppRequestPayload
} from './miniapp-auth'
interface MiniAppBillingHandlerBaseOptions {
allowedOrigins: readonly string[]
botToken: string
financeServiceForSession?: (session: MiniAppAuthorizedSession) => FinanceCommandService
financeServiceForHousehold?: (householdId: string) => FinanceCommandService
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
logger?: Logger
}
function createConfiguredMiniAppBillingSessionService(options: {
botToken: string
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
}) {
return createMiniAppSessionService({
botToken: options.botToken,
...(options.onboardingServiceForTelegramUserId
? {
onboardingServiceForTelegramUserId: options.onboardingServiceForTelegramUserId
}
: {}),
...(options.onboardingService
? {
onboardingService: options.onboardingService
}
: {})
})
}
function resolveFinanceService(
options: Pick<
MiniAppBillingHandlerBaseOptions,
'financeServiceForSession' | 'financeServiceForHousehold'
>,
session: MiniAppAuthorizedSession
): FinanceCommandService {
const service =
options.financeServiceForSession?.(session) ??
options.financeServiceForHousehold?.(session.member.householdId)
if (!service) {
throw new Error('Mini app finance service is not configured')
}
return service
}
function serializeCycleState(
state: Awaited<ReturnType<FinanceCommandService['getAdminCycleState']>>
) {
@@ -42,6 +90,7 @@ async function authenticateAdminSession(
| Response
| {
member: NonNullable<MiniAppSessionResult['member']>
telegramUserId: string
}
> {
const payload = await readMiniAppRequestPayload(request)
@@ -54,7 +103,7 @@ async function authenticateAdminSession(
return miniAppJsonResponse({ ok: false, error: 'Invalid Telegram init data' }, 401, origin)
}
if (!session.authorized || !session.member) {
if (!session.authorized || !session.member || !session.telegramUser) {
return miniAppJsonResponse(
{ ok: false, error: 'Access limited to active household members' },
403,
@@ -67,7 +116,8 @@ async function authenticateAdminSession(
}
return {
member: session.member
member: session.member,
telegramUserId: session.telegramUser.id
}
}
@@ -79,6 +129,7 @@ async function authenticateMemberSession(
| Response
| {
member: NonNullable<MiniAppSessionResult['member']>
telegramUserId: string
}
> {
const payload = await readMiniAppRequestPayload(request)
@@ -91,7 +142,12 @@ async function authenticateMemberSession(
return miniAppJsonResponse({ ok: false, error: 'Invalid Telegram init data' }, 401, origin)
}
if (!session.authorized || !session.member || session.member.status !== 'active') {
if (
!session.authorized ||
!session.member ||
!session.telegramUser ||
session.member.status !== 'active'
) {
return miniAppJsonResponse(
{ ok: false, error: 'Access limited to active household members' },
403,
@@ -100,7 +156,8 @@ async function authenticateMemberSession(
}
return {
member: session.member
member: session.member,
telegramUserId: session.telegramUser.id
}
}
@@ -530,19 +587,10 @@ async function readPaymentMutationPayload(request: Request): Promise<{
}
}
export function createMiniAppBillingCycleHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppBillingCycleHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -567,9 +615,9 @@ export function createMiniAppBillingCycleHandler(options: {
}
const payload = await readCycleQueryPayload(request)
const cycleState = await options
.financeServiceForHousehold(auth.member.householdId)
.getAdminCycleState(payload.period)
const cycleState = await resolveFinanceService(options, auth).getAdminCycleState(
payload.period
)
return miniAppJsonResponse(
{
@@ -587,19 +635,10 @@ export function createMiniAppBillingCycleHandler(options: {
}
}
export function createMiniAppOpenCycleHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppOpenCycleHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -624,7 +663,7 @@ export function createMiniAppOpenCycleHandler(options: {
}
const payload = await readOpenCyclePayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
await service.openCycle(payload.period, payload.currency)
const cycleState = await service.getAdminCycleState(payload.period)
@@ -644,19 +683,10 @@ export function createMiniAppOpenCycleHandler(options: {
}
}
export function createMiniAppCloseCycleHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppCloseCycleHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -681,7 +711,7 @@ export function createMiniAppCloseCycleHandler(options: {
}
const payload = await readCycleQueryPayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
await service.closeCycle(payload.period)
const cycleState = await service.getAdminCycleState()
@@ -701,19 +731,10 @@ export function createMiniAppCloseCycleHandler(options: {
}
}
export function createMiniAppRentUpdateHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppRentUpdateHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -738,7 +759,7 @@ export function createMiniAppRentUpdateHandler(options: {
}
const payload = await readRentUpdatePayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const result = await service.setRent(payload.amountMajor, payload.currency, payload.period)
if (!result) {
return miniAppJsonResponse(
@@ -766,19 +787,10 @@ export function createMiniAppRentUpdateHandler(options: {
}
}
export function createMiniAppAddUtilityBillHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppAddUtilityBillHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -803,7 +815,7 @@ export function createMiniAppAddUtilityBillHandler(options: {
}
const payload = await readUtilityBillPayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const result = await service.addUtilityBill(
payload.billName,
payload.amountMajor,
@@ -837,19 +849,10 @@ export function createMiniAppAddUtilityBillHandler(options: {
}
}
export function createMiniAppSubmitUtilityBillHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppSubmitUtilityBillHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -874,7 +877,7 @@ export function createMiniAppSubmitUtilityBillHandler(options: {
}
const payload = await readUtilityBillPayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const result = await service.addUtilityBill(
payload.billName,
payload.amountMajor,
@@ -905,35 +908,33 @@ export function createMiniAppSubmitUtilityBillHandler(options: {
}
}
export function createMiniAppSubmitPaymentHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
householdConfigurationRepository: HouseholdConfigurationRepository
logger?: Logger
}): {
export function createMiniAppSubmitPaymentHandler(
options: MiniAppBillingHandlerBaseOptions & {
householdConfigurationRepositoryForSession: (
session: MiniAppAuthorizedSession
) => HouseholdConfigurationRepository
}
): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
async function notifyPaymentRecorded(input: {
async function notifyPaymentRecorded(
session: MiniAppAuthorizedSession,
input: {
householdId: string
memberName: string
kind: 'rent' | 'utilities'
amountMajor: string
currency: string
period: string
}) {
}
) {
const householdConfigurationRepository =
options.householdConfigurationRepositoryForSession(session)
const [chat, topic] = await Promise.all([
options.householdConfigurationRepository.getHouseholdChatByHouseholdId(input.householdId),
options.householdConfigurationRepository.getHouseholdTopicBinding(
input.householdId,
'reminders'
)
householdConfigurationRepository.getHouseholdChatByHouseholdId(input.householdId),
householdConfigurationRepository.getHouseholdTopicBinding(input.householdId, 'reminders')
])
if (!chat || !topic) {
@@ -996,7 +997,7 @@ export function createMiniAppSubmitPaymentHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing payment fields' }, 400, origin)
}
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const payment = await service.addPayment(
auth.member.id,
payload.kind,
@@ -1008,7 +1009,7 @@ export function createMiniAppSubmitPaymentHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Failed to record payment' }, 500, origin)
}
await notifyPaymentRecorded({
await notifyPaymentRecorded(auth, {
householdId: auth.member.householdId,
memberName: auth.member.displayName,
kind: payload.kind,
@@ -1032,19 +1033,10 @@ export function createMiniAppSubmitPaymentHandler(options: {
}
}
export function createMiniAppUpdateUtilityBillHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppUpdateUtilityBillHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -1069,7 +1061,7 @@ export function createMiniAppUpdateUtilityBillHandler(options: {
}
const payload = await readUtilityBillUpdatePayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const result = await service.updateUtilityBill(
payload.billId,
payload.billName,
@@ -1099,19 +1091,10 @@ export function createMiniAppUpdateUtilityBillHandler(options: {
}
}
export function createMiniAppDeleteUtilityBillHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppDeleteUtilityBillHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -1136,7 +1119,7 @@ export function createMiniAppDeleteUtilityBillHandler(options: {
}
const payload = await readUtilityBillDeletePayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const deleted = await service.deleteUtilityBill(payload.billId)
if (!deleted) {
@@ -1161,19 +1144,10 @@ export function createMiniAppDeleteUtilityBillHandler(options: {
}
}
export function createMiniAppAddPurchaseHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppAddPurchaseHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -1200,7 +1174,7 @@ export function createMiniAppAddPurchaseHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing purchase fields' }, 400, origin)
}
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const payerMemberId = payload.payerMemberId ?? auth.member.id
await service.addPurchase(
payload.description,
@@ -1218,19 +1192,10 @@ export function createMiniAppAddPurchaseHandler(options: {
}
}
export function createMiniAppUpdatePurchaseHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppUpdatePurchaseHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -1257,7 +1222,7 @@ export function createMiniAppUpdatePurchaseHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing purchase fields' }, 400, origin)
}
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const payerMemberId = payload.payerMemberId
const updated = await service.updatePurchase(
payload.purchaseId,
@@ -1280,19 +1245,10 @@ export function createMiniAppUpdatePurchaseHandler(options: {
}
}
export function createMiniAppDeletePurchaseHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppDeletePurchaseHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -1315,7 +1271,7 @@ export function createMiniAppDeletePurchaseHandler(options: {
}
const payload = await readPurchaseMutationPayload(request)
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const deleted = await service.deletePurchase(payload.purchaseId)
if (!deleted) {
@@ -1330,19 +1286,10 @@ export function createMiniAppDeletePurchaseHandler(options: {
}
}
export function createMiniAppAddPaymentHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppAddPaymentHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -1369,7 +1316,7 @@ export function createMiniAppAddPaymentHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing payment fields' }, 400, origin)
}
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const payment = await service.addPayment(
payload.memberId,
payload.kind,
@@ -1389,19 +1336,10 @@ export function createMiniAppAddPaymentHandler(options: {
}
}
export function createMiniAppUpdatePaymentHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppUpdatePaymentHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -1428,7 +1366,7 @@ export function createMiniAppUpdatePaymentHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing payment fields' }, 400, origin)
}
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const payment = await service.updatePayment(
payload.paymentId,
payload.memberId,
@@ -1449,19 +1387,10 @@ export function createMiniAppUpdatePaymentHandler(options: {
}
}
export function createMiniAppDeletePaymentHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
logger?: Logger
}): {
export function createMiniAppDeletePaymentHandler(options: MiniAppBillingHandlerBaseOptions): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
onboardingService: options.onboardingService
})
const sessionService = createConfiguredMiniAppBillingSessionService(options)
return {
handler: async (request) => {
@@ -1488,7 +1417,7 @@ export function createMiniAppDeletePaymentHandler(options: {
return miniAppJsonResponse({ ok: false, error: 'Missing payment id' }, 400, origin)
}
const service = options.financeServiceForHousehold(auth.member.householdId)
const service = resolveFinanceService(options, auth)
const deleted = await service.deletePayment(payload.paymentId)
if (!deleted) {

View File

@@ -3,6 +3,7 @@ import type { Logger } from '@household/observability'
import {
allowedMiniAppOrigin,
type MiniAppAuthorizedSession,
createMiniAppSessionService,
miniAppErrorResponse,
miniAppJsonResponse,
@@ -12,15 +13,26 @@ import {
export function createMiniAppDashboardHandler(options: {
allowedOrigins: readonly string[]
botToken: string
financeServiceForHousehold: (householdId: string) => FinanceCommandService
onboardingService: HouseholdOnboardingService
financeServiceForSession?: (session: MiniAppAuthorizedSession) => FinanceCommandService
financeServiceForHousehold?: (householdId: string) => FinanceCommandService
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
...(options.onboardingServiceForTelegramUserId
? {
onboardingServiceForTelegramUserId: options.onboardingServiceForTelegramUserId
}
: {}),
...(options.onboardingService
? {
onboardingService: options.onboardingService
}
: {})
})
return {
@@ -62,7 +74,7 @@ export function createMiniAppDashboardHandler(options: {
)
}
if (!session.member) {
if (!session.member || !session.telegramUser) {
return miniAppJsonResponse(
{ ok: false, error: 'Authenticated session is missing member context' },
500,
@@ -70,9 +82,17 @@ export function createMiniAppDashboardHandler(options: {
)
}
const dashboard = await options
.financeServiceForHousehold(session.member.householdId)
.generateDashboard()
const financeService =
options.financeServiceForSession?.({
member: session.member,
telegramUserId: session.telegramUser.id
}) ?? options.financeServiceForHousehold?.(session.member.householdId)
if (!financeService) {
throw new Error('Mini app finance service is not configured')
}
const dashboard = await financeService.generateDashboard()
if (!dashboard) {
return miniAppJsonResponse(
{ ok: false, error: 'No billing cycle available' },

View File

@@ -4,6 +4,7 @@ import type { Logger } from '@household/observability'
import {
allowedMiniAppOrigin,
type MiniAppAuthorizedSession,
createMiniAppSessionService,
miniAppErrorResponse,
miniAppJsonResponse
@@ -53,15 +54,26 @@ async function readLocalePreferenceRequest(request: Request): Promise<LocalePref
export function createMiniAppLocalePreferenceHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
localePreferenceService: LocalePreferenceService
onboardingServiceForTelegramUserId?: (telegramUserId: string) => HouseholdOnboardingService
onboardingService?: HouseholdOnboardingService
localePreferenceServiceForSession?: (session: MiniAppAuthorizedSession) => LocalePreferenceService
localePreferenceService?: LocalePreferenceService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response>
} {
const sessionService = createMiniAppSessionService({
botToken: options.botToken,
...(options.onboardingServiceForTelegramUserId
? {
onboardingServiceForTelegramUserId: options.onboardingServiceForTelegramUserId
}
: {}),
...(options.onboardingService
? {
onboardingService: options.onboardingService
}
: {})
})
return {
@@ -100,9 +112,18 @@ export function createMiniAppLocalePreferenceHandler(options: {
let memberPreferredLocale = session.member.preferredLocale
let householdDefaultLocale = session.member.householdDefaultLocale
const localePreferenceService =
options.localePreferenceServiceForSession?.({
member: session.member,
telegramUserId: session.telegramUser.id
}) ?? options.localePreferenceService
if (!localePreferenceService) {
throw new Error('Mini app locale preference service is not configured')
}
if (payload.scope === 'member') {
const result = await options.localePreferenceService.updateMemberLocale({
const result = await localePreferenceService.updateMemberLocale({
householdId: session.member.householdId,
telegramUserId: session.telegramUser.id,
locale: payload.locale
@@ -115,7 +136,7 @@ export function createMiniAppLocalePreferenceHandler(options: {
memberPreferredLocale = result.member.preferredLocale
householdDefaultLocale = result.member.householdDefaultLocale
} else {
const result = await options.localePreferenceService.updateHouseholdLocale({
const result = await localePreferenceService.updateHouseholdLocale({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin,
locale: payload.locale

View File

@@ -64,12 +64,13 @@ codex review --base origin/main
- Copy `.env.example` to `.env` before running app/database commands.
- `bun run db:seed` refreshes the committed fixture household and is destructive for previously seeded fixture rows.
- Local bot feature flags come from env presence:
- finance commands require `DATABASE_URL` plus household setup in Telegram via `/setup`
- purchase ingestion requires `DATABASE_URL` plus a bound purchase topic via `/bind_purchase_topic`
- anonymous feedback requires `DATABASE_URL` plus a bound feedback topic via `/bind_feedback_topic`
- reminders require `DATABASE_URL` plus `SCHEDULER_SHARED_SECRET` or `SCHEDULER_OIDC_ALLOWED_EMAILS`
- mini app auth and mini app API routes require `APP_DATABASE_URL`
- finance commands require `WORKER_DATABASE_URL` plus household setup in Telegram via `/setup`
- purchase ingestion requires `WORKER_DATABASE_URL` plus a bound purchase topic via `/bind_purchase_topic`
- anonymous feedback requires `WORKER_DATABASE_URL` plus a bound feedback topic via `/bind_feedback_topic`
- reminders require `WORKER_DATABASE_URL` plus `SCHEDULER_SHARED_SECRET` or `SCHEDULER_OIDC_ALLOWED_EMAILS`
and optionally use a dedicated reminders topic via `/bind_reminders_topic`
- mini app CORS can be constrained with `MINI_APP_ALLOWED_ORIGINS`
- mini app CORS must be set explicitly with `MINI_APP_ALLOWED_ORIGINS`
- Migration workflow is documented in `docs/runbooks/migrations.md`.
- Destructive dev reset guidance is documented in `docs/runbooks/dev-reset.md`.
- First deploy flow is documented in `docs/runbooks/first-deploy.md`.

View File

@@ -12,7 +12,10 @@ Execute the first real deployment with a repeatable sequence that covers infrast
- GCP project
- GitHub repo settings
- Telegram bot token
- Supabase project and database URL
- Supabase project and three database URLs:
- owner `DATABASE_URL` for migrations only
- `APP_DATABASE_URL` for authenticated request paths
- `WORKER_DATABASE_URL` for bot and scheduler workers
## Required Configuration Inventory
@@ -28,7 +31,8 @@ Required in your environment `*.tfvars`:
Recommended:
- `database_url_secret_id = "database-url"`
- `app_database_url_secret_id = "app-database-url"`
- `worker_database_url_secret_id = "worker-database-url"`
- `telegram_bot_token_secret_id = "telegram-bot-token"`
- `openai_api_key_secret_id = "openai-api-key"`
- `bot_mini_app_allowed_origins`
@@ -46,10 +50,9 @@ Create the secret resources via Terraform, then add secret versions for:
- `telegram-bot-token`
- `telegram-webhook-secret`
- `scheduler-shared-secret`
- `database-url`
- `app-database-url`
- `worker-database-url`
- optional `openai-api-key`
- optional `supabase-url`
- optional `supabase-publishable-key`
### GitHub Actions secrets
@@ -129,15 +132,19 @@ Use the real project ID from Terraform variables:
echo -n "<telegram-bot-token>" | gcloud secrets versions add telegram-bot-token --data-file=- --project <project_id>
echo -n "<telegram-webhook-secret>" | gcloud secrets versions add telegram-webhook-secret --data-file=- --project <project_id>
echo -n "<scheduler-shared-secret>" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project <project_id>
echo -n "<database-url>" | gcloud secrets versions add database-url --data-file=- --project <project_id>
echo -n "<app-database-url>" | gcloud secrets versions add app-database-url --data-file=- --project <project_id>
echo -n "<worker-database-url>" | gcloud secrets versions add worker-database-url --data-file=- --project <project_id>
```
Add optional secret versions only if those integrations are enabled.
For a functional household dev deployment, set `database_url_secret_id = "database-url"` in
`dev.tfvars` before the apply that creates the Cloud Run services. Otherwise the bot deploys
without `DATABASE_URL`, and finance commands, reminders, mini app auth/dashboard, and anonymous
feedback remain disabled.
For a functional household deployment, set both `app_database_url_secret_id` and
`worker_database_url_secret_id` in `dev.tfvars` before the apply that creates the Cloud Run
services. Otherwise the bot deploys without `APP_DATABASE_URL` and `WORKER_DATABASE_URL`, and mini
app auth, finance commands, reminders, purchase ingestion, and anonymous feedback remain disabled.
Keep `DATABASE_URL` out of normal runtime secrets. It is only required in GitHub Actions for the
migration step that runs before deploy.
Keep `telegram_bot_token_secret_id = "telegram-bot-token"` aligned with the actual bot token
secret name. CD uses that secret to sync the Telegram command menu after deploy.
@@ -218,6 +225,9 @@ The smoke script verifies:
- scheduler endpoint rejects unauthenticated requests
- Telegram webhook matches the expected URL when bot token is provided
Production deploys should also set `MINI_APP_ALLOWED_ORIGINS` explicitly. The browser path remains
bot API only; there is no supported direct browser access to Supabase.
## Phase 8: Scheduler Enablement
First release:

View File

@@ -47,9 +47,16 @@ bun run build
## CD behavior
- CD deploy runs migrations before deploy and now requires the `DATABASE_URL` GitHub secret.
- CD deploy runs migrations before deploy and requires the owner-only `DATABASE_URL` GitHub secret.
- If `DATABASE_URL` is missing, CD fails fast instead of deploying schema-dependent code without migrations.
## Runtime connection split
- `DATABASE_URL` is for migrations, schema checks, and other owner-only maintenance tasks.
- `APP_DATABASE_URL` is for authenticated request paths such as mini app routes.
- `WORKER_DATABASE_URL` is for Telegram ingestion, reminders, scheduler jobs, and other internal worker flows.
- Runtime services should not use `DATABASE_URL`.
## Safety rules
- Prefer additive migrations first (new columns/tables) over destructive changes.

View File

@@ -29,8 +29,8 @@ Define a reproducible GCP infrastructure baseline for deployment of the bot API
- Bot runtime reads secret-backed env vars:
- `TELEGRAM_WEBHOOK_SECRET`
- `SCHEDULER_SHARED_SECRET`
- `SUPABASE_URL` (optional)
- `SUPABASE_PUBLISHABLE_KEY` (optional)
- `APP_DATABASE_URL` (optional)
- `WORKER_DATABASE_URL` (optional)
## Domain Rules

View File

@@ -58,8 +58,9 @@ echo -n "<value>" | gcloud secrets versions add telegram-webhook-secret --data-f
echo -n "<value>" | gcloud secrets versions add scheduler-shared-secret --data-file=- --project <project_id>
```
If you configure optional secret IDs such as `database_url_secret_id` or
`openai_api_key_secret_id`, add versions for those secrets as well.
If you configure optional secret IDs such as `app_database_url_secret_id`,
`worker_database_url_secret_id`, or `openai_api_key_secret_id`, add versions for those secrets as
well.
If GitHub OIDC deploy access is enabled, keep `telegram_bot_token_secret_id` aligned with the
real bot token secret name so CD can read it and sync Telegram commands automatically.
@@ -84,6 +85,9 @@ Recommended approach:
`bot_assistant_rate_limit_rolling_window_ms`
- optional `bot_mini_app_allowed_origins`
- optional `alert_notification_emails`
- runtime DB URLs should stay split:
`APP_DATABASE_URL` for authenticated request flows and `WORKER_DATABASE_URL` for background
workers
## Alerting baseline
@@ -115,3 +119,4 @@ CI runs:
- Scheduler jobs default to `paused = true` and `dry_run = true` to prevent accidental sends before live reminder delivery is ready.
- Bot API is public to accept Telegram webhooks; scheduler endpoint should still verify app-level auth.
- `bot_mini_app_allowed_origins` cannot be auto-derived in Terraform because the bot and mini app Cloud Run services reference each other; set it explicitly once the mini app URL is known.
- `DATABASE_URL` is migration-only and should not be injected into the bot runtime service.

View File

@@ -30,7 +30,8 @@ locals {
runtime_secret_ids = toset(compact([
var.telegram_webhook_secret_id,
var.scheduler_shared_secret_id,
var.database_url_secret_id,
var.app_database_url_secret_id,
var.worker_database_url_secret_id,
var.telegram_bot_token_secret_id,
var.openai_api_key_secret_id
]))

View File

@@ -179,8 +179,11 @@ module "bot_api_service" {
TELEGRAM_WEBHOOK_SECRET = var.telegram_webhook_secret_id
SCHEDULER_SHARED_SECRET = var.scheduler_shared_secret_id
},
var.database_url_secret_id == null ? {} : {
DATABASE_URL = var.database_url_secret_id
var.app_database_url_secret_id == null ? {} : {
APP_DATABASE_URL = var.app_database_url_secret_id
},
var.worker_database_url_secret_id == null ? {} : {
WORKER_DATABASE_URL = var.worker_database_url_secret_id
},
var.telegram_bot_token_secret_id == null ? {} : {
TELEGRAM_BOT_TOKEN = var.telegram_bot_token_secret_id

View File

@@ -8,7 +8,8 @@ artifact_repository_id = "household-bot"
bot_api_image = "europe-west1-docker.pkg.dev/my-gcp-project/household-bot/bot:latest"
mini_app_image = "europe-west1-docker.pkg.dev/my-gcp-project/household-bot/miniapp:latest"
database_url_secret_id = "database-url"
app_database_url_secret_id = "app-database-url"
worker_database_url_secret_id = "worker-database-url"
telegram_bot_token_secret_id = "telegram-bot-token"
openai_api_key_secret_id = "openai-api-key"
bot_purchase_parser_model = "gpt-4o-mini"

View File

@@ -57,7 +57,21 @@ variable "scheduler_shared_secret_id" {
}
variable "database_url_secret_id" {
description = "Optional Secret Manager ID for DATABASE_URL"
description = "Optional Secret Manager ID for owner-only DATABASE_URL used outside runtime deploys"
type = string
default = null
nullable = true
}
variable "app_database_url_secret_id" {
description = "Optional Secret Manager ID for APP_DATABASE_URL"
type = string
default = null
nullable = true
}
variable "worker_database_url_secret_id" {
description = "Optional Secret Manager ID for WORKER_DATABASE_URL"
type = string
default = null
nullable = true

View File

@@ -1,6 +1,6 @@
import { and, desc, eq, gte, inArray, isNotNull, isNull, lt, lte, or, sql } from 'drizzle-orm'
import { createDbClient, schema } from '@household/db'
import { createDbClient, type DbSessionContext, schema } from '@household/db'
import type { FinanceRepository } from '@household/ports'
import {
instantFromDatabaseValue,
@@ -22,14 +22,22 @@ function toCurrencyCode(raw: string): CurrencyCode {
export function createDbFinanceRepository(
databaseUrl: string,
householdId: string
householdId: string,
options: {
sessionContext?: DbSessionContext
} = {}
): {
repository: FinanceRepository
close: () => Promise<void>
} {
const { db, queryClient } = createDbClient(databaseUrl, {
max: 5,
prepare: false
prepare: false,
...(options.sessionContext
? {
sessionContext: options.sessionContext
}
: {})
})
async function loadPurchaseParticipants(purchaseIds: readonly string[]): Promise<

View File

@@ -1,6 +1,6 @@
import { and, asc, eq, sql } from 'drizzle-orm'
import { createDbClient, schema } from '@household/db'
import { createDbClient, type DbSessionContext, schema } from '@household/db'
import {
instantToDate,
normalizeSupportedLocale,
@@ -334,13 +334,23 @@ function utilityCategorySlug(name: string): string {
.slice(0, 48)
}
export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
export function createDbHouseholdConfigurationRepository(
databaseUrl: string,
options: {
sessionContext?: DbSessionContext
} = {}
): {
repository: HouseholdConfigurationRepository
close: () => Promise<void>
} {
const { db, queryClient } = createDbClient(databaseUrl, {
max: 5,
prepare: false
prepare: false,
...(options.sessionContext
? {
sessionContext: options.sessionContext
}
: {})
})
const defaultUtilityCategories = [

View File

@@ -23,6 +23,7 @@
"0019_faithful_madame_masque.sql": "38711341799b04a7c47fcc64fd19faf5b26e6f183d6a4c01d492b9929cd63641",
"0020_natural_mauler.sql": "a80a4a0196a3b4931040850089346d1bc99b34a5afca77d6d62478ee4b8902c1",
"0020_silver_payments.sql": "9686235c75453f1eaa016f2f4ab7fce8fe964c76a4e3515987a2b9f90bd7b1ad",
"0021_sharp_payer.sql": "973596e154382984ba7769979ea58298b6d93c5139540854be01e8b283ddb4f1"
"0021_sharp_payer.sql": "973596e154382984ba7769979ea58298b6d93c5139540854be01e8b283ddb4f1",
"0022_harden_rls.sql": "d2e24b3e5b7ec7ef9da7e90c0ddf0e408764f3578af3872f76b9b3198ffbd70e"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -155,6 +155,13 @@
"when": 1774200000000,
"tag": "0021_sharp_payer",
"breakpoints": true
},
{
"idx": 22,
"version": "7",
"when": 1774204831000,
"tag": "0022_harden_rls",
"breakpoints": true
}
]
}

View File

@@ -1,9 +1,34 @@
import postgres from 'postgres'
import { drizzle } from 'drizzle-orm/postgres-js'
export interface DbSessionContext {
telegramUserId?: string
householdId?: string
memberId?: string
isAdmin?: boolean
isWorker?: boolean
}
export interface DbClientOptions {
max?: number
prepare?: boolean
sessionContext?: DbSessionContext
}
function quoteRuntimeOptionValue(value: string): string {
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`
}
function appendRuntimeOption(
options: string[],
key: string,
value: string | boolean | undefined
): void {
if (value === undefined) {
return
}
options.push(`-c ${key}=${quoteRuntimeOptionValue(String(value))}`)
}
export function createDbClient(databaseUrl: string, options: DbClientOptions = {}) {
@@ -17,7 +42,17 @@ export function createDbClient(databaseUrl: string, options: DbClientOptions = {
url.searchParams.delete('options')
// Set search_path via options parameter (required for PgBouncer compatibility)
url.searchParams.set('options', `-c search_path=${dbSchema}`)
const runtimeOptions = [`-c search_path=${dbSchema}`]
appendRuntimeOption(
runtimeOptions,
'app.telegram_user_id',
options.sessionContext?.telegramUserId
)
appendRuntimeOption(runtimeOptions, 'app.household_id', options.sessionContext?.householdId)
appendRuntimeOption(runtimeOptions, 'app.member_id', options.sessionContext?.memberId)
appendRuntimeOption(runtimeOptions, 'app.is_admin', options.sessionContext?.isAdmin)
appendRuntimeOption(runtimeOptions, 'app.is_worker', options.sessionContext?.isWorker)
url.searchParams.set('options', runtimeOptions.join(' '))
const cleanUrl = url.toString()

View File

@@ -1,2 +1,3 @@
export { createDbClient } from './client'
export type { DbSessionContext } from './client'
export * as schema from './schema'