feat: add health, miniapp stub, and sync routes

This commit is contained in:
devilreef 2026-04-29 12:14:18 +06:00
parent 4a8b0ac806
commit c16d028983
Signed by: devilreef
SSH key fingerprint: SHA256:UZisRr4iuXx+IhkbZnR655L2RWAT6o2rgbGv5F/6m3Y
3 changed files with 100 additions and 0 deletions

4
src/routes/health.ts Normal file
View file

@ -0,0 +1,4 @@
import { Hono } from 'hono'
export const health = new Hono()
health.get('/health', c => c.json({ ok: true }))

12
src/routes/miniapp.ts Normal file
View file

@ -0,0 +1,12 @@
import { Hono } from 'hono'
const HTML = `<!doctype html>
<html><head><meta charset="utf-8"><title>arcanesync</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script></head>
<body style="font:14px system-ui;padding:24px;color:#888">
<p>managed by arcanegram client. you can close this window.</p>
<script>try { Telegram.WebApp.close() } catch {}</script>
</body></html>`
export const miniapp = new Hono()
miniapp.get('/miniapp', c => c.html(HTML))

84
src/routes/sync.ts Normal file
View file

@ -0,0 +1,84 @@
import type { AuthEnv } from '@/middleware/auth.js'
import { Buffer } from 'node:buffer'
import { eq, sql } from 'drizzle-orm'
import { Hono } from 'hono'
import { userSettings } from '@/db/schema.js'
import { db } from '@/lib/db.js'
import { authMiddleware } from '@/middleware/auth.js'
import { config } from '@/shared/config.js'
export const sync = new Hono<AuthEnv>()
sync.use('*', authMiddleware)
sync.get('/sync', async (c) => {
const userId = c.get('userId')
const [row] = await db.select().from(userSettings).where(eq(userSettings.userId, userId))
if (!row)
return c.json({ version: 0, data: {} })
return c.json({ version: row.version, data: row.data })
})
interface PutBody {
baseVersion: number
patch: Record<string, unknown>
}
sync.put('/sync', async (c) => {
const raw = await c.req.text()
if (Buffer.byteLength(raw, 'utf8') > config.maxPayloadBytes)
return c.json({ error: 'payload-too-large' }, 413)
let body: PutBody
try {
body = JSON.parse(raw)
}
catch {
return c.json({ error: 'bad-json' }, 400)
}
if (typeof body.baseVersion !== 'number' || typeof body.patch !== 'object' || body.patch === null || Array.isArray(body.patch))
return c.json({ error: 'malformed' }, 400)
const userId = c.get('userId')
return db.transaction(async (tx) => {
const [row] = await tx
.select()
.from(userSettings)
.where(eq(userSettings.userId, userId))
.for('update')
if (!row) {
const merged = applyPatch({}, body.patch)
const [inserted] = await tx
.insert(userSettings)
.values({ userId, version: 1, data: merged })
.returning()
return c.json({ version: inserted.version, data: inserted.data })
}
if (body.baseVersion !== row.version)
return c.json({ version: row.version, data: row.data }, 409)
const merged = applyPatch(row.data, body.patch)
const [updated] = await tx
.update(userSettings)
.set({ version: row.version + 1, data: merged, updatedAt: sql`now()` })
.where(eq(userSettings.userId, userId))
.returning()
return c.json({ version: updated.version, data: updated.data })
})
})
function applyPatch(
base: Record<string, unknown>,
patch: Record<string, unknown>,
): Record<string, unknown> {
const out: Record<string, unknown> = { ...base }
for (const [k, v] of Object.entries(patch)) {
if (v === null)
delete out[k]
else
out[k] = v
}
return out
}