Some checks are pending
CI / Type-check bridge (push) Blocked by required conditions
CI / Tests unit bridge (push) Blocked by required conditions
CI / Lint bridge (Biome) (push) Waiting to run
CI / Tests integration bridge (push) Blocked by required conditions
CI / Security scan (push) Waiting to run
CI / Docker build + healthcheck (push) Blocked by required conditions
104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
/**
|
|
* Bridge service entrypoint — Hono HTTP server.
|
|
*
|
|
* Boot sequence : loadConfig -> initContainer -> wire middleware globaux +
|
|
* routes /api/v1/tables/* avec auth + serve.
|
|
*
|
|
* R1 — Plus de routes metier. Le bridge expose un proxy generique
|
|
* `/api/v1/tables/*` style Notion. Le metier vit cote consumer.
|
|
*/
|
|
|
|
import { serve } from '@hono/node-server';
|
|
import { Hono } from 'hono';
|
|
import { logger as honoLogger } from 'hono/logger';
|
|
import { loadConfig } from './lib/config.js';
|
|
import { getContainer, initContainer } from './lib/container.js';
|
|
import { logger } from './lib/logger.js';
|
|
import { type AuthVariables, authMiddleware } from './middleware/auth.js';
|
|
import { errorHandler } from './middleware/error-handler.js';
|
|
import { defaultRateLimitKey, rateLimit } from './middleware/rate-limit.js';
|
|
import { tablesRoutes } from './routes/tables.js';
|
|
import { webhooksRoutes } from './routes/webhooks.js';
|
|
|
|
export async function buildApp(): Promise<Hono<{ Variables: AuthVariables }>> {
|
|
const app = new Hono<{ Variables: AuthVariables }>();
|
|
app.use('*', honoLogger());
|
|
app.onError(errorHandler);
|
|
|
|
app.get('/api/health', (c) => c.json({ status: 'ok', service: 'bridge', version: '0.1.0' }));
|
|
|
|
app.get('/api/ready', async (c) => {
|
|
const { baserow, redis } = getContainer();
|
|
const [baserowOk, redisOk] = await Promise.all([baserow.healthCheck(), redis.healthCheck()]);
|
|
const status = baserowOk && redisOk ? 'ok' : 'degraded';
|
|
const code = baserowOk && redisOk ? 200 : 503;
|
|
return c.json(
|
|
{ status, dependencies: { baserow: baserowOk, redis: redisOk } },
|
|
code as 200 | 503,
|
|
);
|
|
});
|
|
|
|
// Webhooks (HMAC auth) montes AVANT v1 (Bearer auth) — pas de double auth.
|
|
app.route('/api/webhooks', webhooksRoutes);
|
|
|
|
// Auth middleware applique sur tout /api/v1/*
|
|
const ctn = getContainer();
|
|
const v1 = new Hono<{ Variables: AuthVariables }>();
|
|
v1.use(
|
|
'*',
|
|
authMiddleware({
|
|
tokens: ctn.tokens,
|
|
oidc: ctn.oidc,
|
|
docmostJwt: ctn.docmostJwt,
|
|
groupsScopesMap: ctn.groupsScopesMap,
|
|
logger: ctn.logger,
|
|
}),
|
|
);
|
|
// Rate limit global sur toute identite authentifiee. Place APRES authMiddleware
|
|
// pour que `c.var.user` soit deja peuple — sinon la cle tombe sur IP.
|
|
v1.use(
|
|
'*',
|
|
rateLimit(ctn.redis, {
|
|
maxRequests: ctn.config.rateLimitGlobalMax,
|
|
windowSeconds: ctn.config.rateLimitGlobalWindow,
|
|
}),
|
|
);
|
|
// Rate limit mutation : compteur dedie (suffixe `:mut`) qui ne s'applique
|
|
// qu'aux verbes ecrivants. Plus strict que le global car bursts d'ecriture =
|
|
// plus dangereux (charge Baserow + integrite donnees).
|
|
const mutationLimiter = rateLimit(ctn.redis, {
|
|
maxRequests: ctn.config.rateLimitMutationMax,
|
|
windowSeconds: ctn.config.rateLimitMutationWindow,
|
|
keyFrom: (c) => `${defaultRateLimitKey(c)}:mut`,
|
|
});
|
|
v1.use('*', async (c, next) => {
|
|
const method = c.req.method.toUpperCase();
|
|
if (method === 'POST' || method === 'PATCH' || method === 'PUT' || method === 'DELETE') {
|
|
return mutationLimiter(c, next);
|
|
}
|
|
await next();
|
|
});
|
|
v1.route('/tables', tablesRoutes);
|
|
app.route('/api/v1', v1);
|
|
|
|
app.notFound((c) => c.json({ error: { code: 'NOT_FOUND', message: 'Route not found' } }, 404));
|
|
|
|
return app;
|
|
}
|
|
|
|
async function main() {
|
|
const config = loadConfig();
|
|
await initContainer({ config });
|
|
const app = await buildApp();
|
|
|
|
serve({ fetch: app.fetch, port: config.port }, (info) => {
|
|
logger.info({ port: info.port, env: config.nodeEnv }, 'Bridge service started');
|
|
});
|
|
}
|
|
|
|
if (process.env.NODE_ENV !== 'test' && import.meta.url === `file://${process.argv[1]}`) {
|
|
main().catch((err) => {
|
|
logger.fatal({ err }, 'Bridge service failed to start');
|
|
process.exit(1);
|
|
});
|
|
}
|