fix(review): harden miniapp auth and finance flows

This commit is contained in:
2026-03-09 00:30:31 +04:00
parent 91a040f2ee
commit c8b17136be
22 changed files with 327 additions and 157 deletions

View File

@@ -1,27 +1,9 @@
import { describe, expect, test } from 'bun:test'
import { createHmac } from 'node:crypto'
import type { FinanceRepository } from '@household/ports'
import { createMiniAppAuthHandler } from './miniapp-auth'
function buildInitData(botToken: string, authDate: number, user: object): string {
const params = new URLSearchParams()
params.set('auth_date', authDate.toString())
params.set('query_id', 'AAHdF6IQAAAAAN0XohDhrOrc')
params.set('user', JSON.stringify(user))
const dataCheckString = [...params.entries()]
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, value]) => `${key}=${value}`)
.join('\n')
const secretKey = createHmac('sha256', 'WebAppData').update(botToken).digest()
const hash = createHmac('sha256', secretKey).update(dataCheckString).digest('hex')
params.set('hash', hash)
return params.toString()
}
import { buildMiniAppInitData } from './telegram-miniapp-test-helpers'
function repository(
member: Awaited<ReturnType<FinanceRepository['getMemberByTelegramUserId']>>
@@ -66,7 +48,7 @@ describe('createMiniAppAuthHandler', () => {
'content-type': 'application/json'
},
body: JSON.stringify({
initData: buildInitData('test-bot-token', authDate, {
initData: buildMiniAppInitData('test-bot-token', authDate, {
id: 123456,
first_name: 'Stan',
username: 'stanislav',
@@ -114,7 +96,7 @@ describe('createMiniAppAuthHandler', () => {
'content-type': 'application/json'
},
body: JSON.stringify({
initData: buildInitData('test-bot-token', authDate, {
initData: buildMiniAppInitData('test-bot-token', authDate, {
id: 123456,
first_name: 'Stan'
})
@@ -129,4 +111,73 @@ describe('createMiniAppAuthHandler', () => {
reason: 'not_member'
})
})
test('returns 400 for malformed JSON bodies', async () => {
const auth = createMiniAppAuthHandler({
allowedOrigins: ['http://localhost:5173'],
botToken: 'test-bot-token',
repository: repository(null)
})
const response = await auth.handler(
new Request('http://localhost/api/miniapp/session', {
method: 'POST',
headers: {
origin: 'http://localhost:5173',
'content-type': 'application/json'
},
body: '{"initData":'
})
)
expect(response.status).toBe(400)
expect(await response.json()).toEqual({
ok: false,
error: 'Invalid JSON body'
})
})
test('does not reflect arbitrary origins in production without an allow-list', async () => {
const previousNodeEnv = process.env.NODE_ENV
process.env.NODE_ENV = 'production'
try {
const authDate = Math.floor(Date.now() / 1000)
const auth = createMiniAppAuthHandler({
allowedOrigins: [],
botToken: 'test-bot-token',
repository: repository({
id: 'member-1',
telegramUserId: '123456',
displayName: 'Stan',
isAdmin: true
})
})
const response = await auth.handler(
new Request('http://localhost/api/miniapp/session', {
method: 'POST',
headers: {
origin: 'https://unknown.example',
'content-type': 'application/json'
},
body: JSON.stringify({
initData: buildMiniAppInitData('test-bot-token', authDate, {
id: 123456,
first_name: 'Stan'
})
})
})
)
expect(response.status).toBe(200)
expect(response.headers.get('access-control-allow-origin')).toBeNull()
} finally {
if (previousNodeEnv === undefined) {
delete process.env.NODE_ENV
} else {
process.env.NODE_ENV = previousNodeEnv
}
}
})
})