| |
| import "./instrument"; |
|
|
| import { trpcServer } from "@hono/trpc-server"; |
| import { OpenAPIHono } from "@hono/zod-openapi"; |
| import { |
| buildDependenciesResponse, |
| buildReadinessResponse, |
| checkDependencies, |
| } from "@midday/health/checker"; |
| import { apiDependencies } from "@midday/health/probes"; |
| import { logger } from "@midday/logger"; |
| import { Scalar } from "@scalar/hono-api-reference"; |
| import * as Sentry from "@sentry/bun"; |
| import { cors } from "hono/cors"; |
| import { secureHeaders } from "hono/secure-headers"; |
| import { routers } from "./rest/routers"; |
| import type { Context } from "./rest/types"; |
| import { createTRPCContext } from "./trpc/init"; |
| import { appRouter } from "./trpc/routers/_app"; |
| import { httpLogger } from "./utils/logger"; |
|
|
| const app = new OpenAPIHono<Context>(); |
|
|
| app.use(httpLogger()); |
| app.use( |
| secureHeaders({ |
| crossOriginResourcePolicy: "cross-origin", |
| }), |
| ); |
|
|
| app.use( |
| "*", |
| cors({ |
| origin: process.env.ALLOWED_API_ORIGINS?.split(",") ?? [], |
| allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], |
| allowHeaders: [ |
| "Authorization", |
| "Content-Type", |
| "User-Agent", |
| "accept-language", |
| "x-trpc-source", |
| "x-user-locale", |
| "x-user-timezone", |
| "x-user-country", |
| "x-force-primary", |
| |
| "x-slack-signature", |
| "x-slack-request-timestamp", |
| ], |
| exposeHeaders: [ |
| "Content-Length", |
| "Content-Type", |
| "Cache-Control", |
| "Cross-Origin-Resource-Policy", |
| ], |
| maxAge: 86400, |
| }), |
| ); |
|
|
| app.use( |
| "/trpc/*", |
| trpcServer({ |
| router: appRouter, |
| createContext: createTRPCContext, |
| onError: ({ error, path }) => { |
| logger.error(`[tRPC] ${path}`, { |
| message: error.message, |
| code: error.code, |
| cause: error.cause instanceof Error ? error.cause.message : undefined, |
| stack: error.stack, |
| }); |
|
|
| |
| if (error.code === "INTERNAL_SERVER_ERROR") { |
| Sentry.captureException(error, { |
| tags: { source: "trpc", path: path ?? "unknown" }, |
| }); |
| } |
| }, |
| }), |
| ); |
|
|
| app.get("/favicon.ico", (c) => c.body(null, 204)); |
| app.get("/robots.txt", (c) => c.body(null, 204)); |
|
|
| app.get("/health", (c) => { |
| return c.json({ status: "ok" }, 200); |
| }); |
|
|
| app.get("/info", (c) => { |
| return c.html(` |
| <html> |
| <head> |
| <title>Midday API Info</title> |
| <style> |
| body { font-family: sans-serif; line-height: 1.6; max-width: 800px; margin: 40px auto; padding: 20px; } |
| h1 { color: #333; } |
| .use-case { margin-bottom: 20px; border-left: 4px solid #333; padding-left: 15px; } |
| </style> |
| </head> |
| <body> |
| <h1>Midday API</h1> |
| <p>Welcome to the Midday API. This service provides a suite of business management tools.</p> |
| |
| <h2>Core Use Cases</h2> |
| |
| <div class="use-case"> |
| <h3>1. AI-Powered Financial Insights</h3> |
| <p>Get automated analysis of your revenue, expenses, and profit trends. The API uses Helmholtz Blablador LLMs to provide actionable advice.</p> |
| </div> |
| |
| <div class="use-case"> |
| <h3>2. Intelligent Document Extraction</h3> |
| <p>Upload receipts or invoices and get structured JSON data back. No more manual data entry.</p> |
| </div> |
| |
| <div class="use-case"> |
| <h3>3. Seamless Invoicing</h3> |
| <p>Generate and manage professional invoices via API, integrated with your transaction history.</p> |
| </div> |
| |
| <p>Explore the full <a href="/">API Documentation (Scalar)</a> to get started.</p> |
| </body> |
| </html> |
| `); |
| }); |
|
|
| app.get("/health/ready", async (c) => { |
| const results = await checkDependencies(apiDependencies(), 1); |
| const response = buildReadinessResponse(results); |
| return c.json(response, response.status === "ok" ? 200 : 503); |
| }); |
|
|
| app.get("/health/dependencies", async (c) => { |
| const results = await checkDependencies(apiDependencies()); |
| const response = buildDependenciesResponse(results); |
| return c.json(response, response.status === "ok" ? 200 : 503); |
| }); |
|
|
| app.doc("/openapi", { |
| openapi: "3.1.0", |
| info: { |
| version: "1.0.0", |
| title: "Midday API", |
| description: |
| "Midday is an all-in-one platform for business management. This API provides access to core features including:\n\n" + |
| "- **Financial Insights**: AI-generated reports on business performance and spending patterns.\n" + |
| "- **Document Processing**: Automatic extraction of data from receipts and invoices using advanced LLMs.\n" + |
| "- **Transaction Management**: Real-time tracking and categorization of business transactions.\n" + |
| "- **Invoicing**: Create and manage professional invoices for your clients.\n\n" + |
| "Use this API to integrate Midday's powerful business assistant into your own workflows.", |
| contact: { |
| name: "Midday Support", |
| email: "engineer@midday.ai", |
| url: "https://midday.ai", |
| }, |
| license: { |
| name: "AGPL-3.0 license", |
| url: "https://github.com/midday-ai/midday/blob/main/LICENSE", |
| }, |
| }, |
| servers: [ |
| { |
| url: "https://api.midday.ai", |
| description: "Production API", |
| }, |
| ], |
| security: [ |
| { |
| oauth2: [], |
| }, |
| { token: [] }, |
| ], |
| }); |
|
|
| |
| app.openAPIRegistry.registerComponent("securitySchemes", "token", { |
| type: "http", |
| scheme: "bearer", |
| description: "Default authentication mechanism", |
| "x-speakeasy-example": "MIDDAY_API_KEY", |
| }); |
|
|
| app.get( |
| "/", |
| Scalar({ url: "/openapi", pageTitle: "Midday API", theme: "saturn" }), |
| ); |
|
|
| app.route("/", routers); |
|
|
| |
| app.onError((err, c) => { |
| Sentry.captureException(err, { |
| tags: { source: "hono", path: c.req.path, method: c.req.method }, |
| }); |
| logger.error(`[Hono] ${c.req.method} ${c.req.path}`, { |
| message: err.message, |
| stack: err.stack, |
| }); |
| return c.json({ error: "Internal Server Error" }, 500); |
| }); |
|
|
| |
| |
| |
| process.on("uncaughtException", (err) => { |
| logger.error("Uncaught exception", { error: err.message, stack: err.stack }); |
| Sentry.captureException(err, { |
| tags: { errorType: "uncaught_exception" }, |
| }); |
| }); |
|
|
| process.on("unhandledRejection", (reason, promise) => { |
| logger.error("Unhandled rejection", { |
| reason: reason instanceof Error ? reason.message : String(reason), |
| stack: reason instanceof Error ? reason.stack : undefined, |
| }); |
| Sentry.captureException( |
| reason instanceof Error ? reason : new Error(String(reason)), |
| { |
| tags: { errorType: "unhandled_rejection" }, |
| }, |
| ); |
| }); |
|
|
| export default { |
| port: process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000, |
| fetch: app.fetch, |
| host: "0.0.0.0", |
| idleTimeout: 60, |
| }; |
|
|