import Elysia, { t } from 'elysia' import { db, table as $t } from '~/db' import { and, eq } from 'drizzle-orm' import { apiAuth } from '~/config' import merchantNonces from '~/db/schema/merchant_nonces' import { verify } from '~/helpers/signature' import { UnauthenticatedError } from '~/helpers/errors' export const apiKeyAuth = new Elysia().macro({ verifyKey: { headers: t.Object({ 'x-api-key': t.String(), 'x-nonce': t.String(), 'x-signature': t.String(), 'x-timestamp': t.Number(), }), body: t.Unknown(), async beforeHandle({ headers, body }) { const apiKey = headers['x-api-key'] const nonce = headers['x-nonce'] const signature = headers['x-signature'] const timestamp = headers['x-timestamp'] if (!apiKey || !nonce || !signature || !timestamp) { throw new UnauthenticatedError('Missing auth headers') } const tsNum = Number(timestamp) if (!Number.isFinite(tsNum)) { throw new UnauthenticatedError('Invalid timestamp') } const nowSec = Math.floor(Date.now() / 1000) if (Math.abs(nowSec - tsNum) > apiAuth.skewSeconds) { throw new UnauthenticatedError('Timestamp out of range') } const merchant = await db.query.merchants.findFirst({ where: (t, { and, eq }) => and( eq(t.apiKey, apiKey), eq(t.isActive, true), ), }) if (!merchant) { throw new UnauthenticatedError('Invalid API key') } if (!nonce) { throw new UnauthenticatedError('Nonce is required') } const existingNonce = await db.$count($t.merchantNonces, and( eq($t.merchantNonces.merchantId, merchant.merchantId), eq($t.merchantNonces.nonce, nonce), )) if (existingNonce > 0) { throw new UnauthenticatedError('API replay detected') } db.insert(merchantNonces).values({ merchantId: merchant.merchantId, nonce, }) if (!verify(body, signature, merchant.secretKey)) { throw new UnauthenticatedError('Invalid signature') } }, }, })