move openapi out of src dir

Adapted from #3579 by @gorkem-bwl. The openapi/registry, helpers, and
per-resource routes/* files are largely their work; this commit relocates
the directory out of src/ so the build-time generator code is not emitted
into dist/, and shifts spec generation from runtime to a build step.

Co-authored-by: gorkem-bwl <gorkem.cetin@bluewavelabs.ca>
This commit is contained in:
Alex Hollidayandgorkem-bwl committed 2026-05-01 11:31:48 -07:00
1 parent 28baeaf218
commit 3f9019f34c
22 files changed
+11259 -3944

No files matched your search

+10031 -3940
View File
File diff suppressed because it is too large. Load diff
+54
View File
@@ -0,0 +1,54 @@
import "./registry.js";
import { z } from "zod";
export const successEnvelope = <T extends z.ZodTypeAny>(data: T) =>
z.object({
success: z.literal(true),
msg: z.string(),
data,
});
export const successEnvelopeNoData = z.object({
success: z.literal(true),
msg: z.string(),
});
export const errorEnvelope = z.object({
success: z.literal(false),
msg: z.string(),
data: z.unknown().optional(),
});
export const bearer = [{ bearerAuth: [] }];
export const json = <T extends z.ZodTypeAny>(schema: T, example?: unknown) => ({
"application/json": example === undefined ? { schema } : { schema, example },
});
export const standardErrors = {
"401": { description: "Unauthorized", content: json(errorEnvelope) },
"403": { description: "Forbidden", content: json(errorEnvelope) },
"500": { description: "Internal server error", content: json(errorEnvelope) },
};
export const okJson = <T extends z.ZodTypeAny>(data: T, description = "OK", example?: unknown) => ({
description,
content: json(successEnvelope(data), example === undefined ? undefined : { success: true, msg: "OK", data: example }),
});
export const okJsonNoData = (description = "OK") => ({
description,
content: json(successEnvelopeNoData, { success: true, msg: "OK" }),
});
export const okUnknown = okJson(z.unknown());
export const multipart = (fields: Record<string, z.ZodTypeAny>, fileField?: string) => {
const shape: Record<string, z.ZodTypeAny> = { ...fields };
if (fileField) {
shape[fileField] = z.string().openapi({ type: "string", format: "binary" });
}
return {
"multipart/form-data": { schema: z.object(shape) },
};
};
+56
View File
@@ -0,0 +1,56 @@
import "./registry.js";
import "./routes/auth.js";
import "./routes/check.js";
import "./routes/diagnostic.js";
import "./routes/geoCheck.js";
import "./routes/incident.js";
import "./routes/invite.js";
import "./routes/log.js";
import "./routes/maintenanceWindow.js";
import "./routes/monitor.js";
import "./routes/notification.js";
import "./routes/queue.js";
import "./routes/settings.js";
import "./routes/statusPage.js";
import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
import type { JsonObject } from "swagger-ui-express";
import { registry } from "./registry.js";
let cached: JsonObject | null = null;
export function getOpenApiSpec(): JsonObject {
if (cached) return cached;
const generator = new OpenApiGeneratorV3(registry.definitions);
const document = generator.generateDocument({
openapi: "3.0.0",
info: {
title: "Checkmate API",
version: "1.0.0",
description: "Generated from Zod validation schemas. Source of truth: server/src/validation/*.ts",
},
servers: [{ url: "/api/v1", description: "Current server" }],
tags: [
{ name: "auth", description: "Sign up, log in, password reset, and the authenticated user's profile." },
{ name: "monitors", description: "Configure and inspect monitors (HTTP, port, ping, hardware, docker, game, gRPC) for the caller's team." },
{ name: "checks", description: "Individual monitoring check results, including history, summaries, and bulk deletes." },
{ name: "geo-checks", description: "Geographic check results for monitors with multi-region probing enabled." },
{ name: "incidents", description: "Downtime incidents derived from monitor state changes, including filtering and CSV export." },
{
name: "notifications",
description: "Notification channels (email, webhook, Slack, Discord, PagerDuty, Matrix, Teams, Telegram, Pushover, Twilio) and test alerts.",
},
{ name: "maintenance-window", description: "Scheduled maintenance windows during which monitors do not generate incidents or alerts." },
{ name: "status-page", description: "Public status pages, their configuration, and the monitors they expose." },
{ name: "settings", description: "Global application settings (admin/superadmin)." },
{ name: "invite", description: "Team invitations and accepting them." },
{ name: "queue", description: "Background job queue introspection and admin actions (admin/superadmin)." },
{ name: "diagnostic", description: "System diagnostics for the running server (admin/superadmin)." },
{ name: "logs", description: "Application logs for the running server (admin/superadmin)." },
],
});
cached = document as unknown as JsonObject;
return cached;
}
+12
View File
@@ -0,0 +1,12 @@
import { OpenAPIRegistry, extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";
import { z } from "zod";
extendZodWithOpenApi(z);
export const registry = new OpenAPIRegistry();
registry.registerComponent("securitySchemes", "bearerAuth", {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
});
+203
View File
@@ -0,0 +1,203 @@
import { z } from "zod";
import { registry } from "../registry.js";
import { bearer, json, multipart, okJson, okJsonNoData, standardErrors } from "../helpers.js";
import {
loginValidation,
registrationBodyValidation,
recoveryValidation,
recoveryTokenBodyValidation,
newPasswordValidation,
} from "@/validation/authValidation.js";
import {
getUserByIdParamValidation,
editUserByIdParamValidation,
editUserByIdBodyValidation,
editUserPasswordByIdBodyValidation,
createUserBodyValidation,
editUserBodyValidation,
} from "@/validation/userValidation.js";
const tags = ["auth"];
const userObject = z
.object({
_id: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }),
firstName: z.string().openapi({ example: "Ada" }),
lastName: z.string().openapi({ example: "Lovelace" }),
email: z.string().openapi({ example: "ada@example.com" }),
role: z.array(z.string()).openapi({ example: ["admin"] }),
teamId: z.string().optional().openapi({ example: "65f1c2a4d8b9e01234567890" }),
profileImage: z.string().nullable().optional(),
createdAt: z.string().openapi({ example: "2026-04-01T10:00:00.000Z" }),
updatedAt: z.string().openapi({ example: "2026-04-15T14:30:00.000Z" }),
})
.passthrough()
.openapi("User");
const authPayload = z
.object({
user: userObject,
token: z.string().optional().openapi({ example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }),
})
.openapi("AuthPayload");
registry.registerPath({
method: "post",
path: "/auth/register",
tags,
summary: "Register a new user (first user becomes superadmin)",
request: { body: { content: multipart(registrationBodyValidation.shape, "profileImage") } },
responses: { "200": okJson(authPayload), ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/auth/login",
tags,
summary: "Log in",
request: {
body: {
content: json(loginValidation, { email: "ada@example.com", password: "S3cure!Passw0rd" }),
},
},
responses: {
"200": okJson(authPayload, "OK", {
user: {
_id: "65f1c2a4d8b9e0123456789a",
firstName: "Ada",
lastName: "Lovelace",
email: "ada@example.com",
role: ["admin"],
teamId: "65f1c2a4d8b9e01234567890",
createdAt: "2026-04-01T10:00:00.000Z",
updatedAt: "2026-04-15T14:30:00.000Z",
},
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
}),
"401": { description: "Invalid credentials", content: json(z.object({ success: z.literal(false), msg: z.string() })) },
"500": standardErrors["500"],
},
});
registry.registerPath({
method: "post",
path: "/auth/recovery/request",
tags,
summary: "Request a password recovery email",
request: { body: { content: json(recoveryValidation, { email: "ada@example.com" }) } },
responses: { "200": okJsonNoData(), "500": standardErrors["500"] },
});
registry.registerPath({
method: "post",
path: "/auth/recovery/validate",
tags,
summary: "Validate a password recovery token",
request: { body: { content: json(recoveryTokenBodyValidation) } },
responses: { "200": okJsonNoData(), "500": standardErrors["500"] },
});
registry.registerPath({
method: "post",
path: "/auth/recovery/reset",
tags,
summary: "Reset password using a recovery token",
request: { body: { content: json(newPasswordValidation) } },
responses: { "200": okJsonNoData(), "500": standardErrors["500"] },
});
registry.registerPath({
method: "get",
path: "/auth/users/superadmin",
tags,
summary: "Check whether a superadmin user exists",
responses: { "200": okJson(z.object({ superAdminExists: z.boolean() })), "500": standardErrors["500"] },
});
registry.registerPath({
method: "get",
path: "/auth/users",
tags,
summary: "List all users (admin/superadmin)",
security: bearer,
responses: { "200": okJson(z.array(userObject)), ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/auth/users",
tags,
summary: "Create a new user (superadmin)",
security: bearer,
request: { body: { content: multipart(createUserBodyValidation.shape, "profileImage") } },
responses: { "201": okJson(userObject, "User created"), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/auth/users/{userId}",
tags,
summary: "Get a user by id",
security: bearer,
request: { params: getUserByIdParamValidation },
responses: {
"200": okJson(userObject),
"404": { description: "User not found", content: json(z.object({ success: z.literal(false), msg: z.string() })) },
...standardErrors,
},
});
registry.registerPath({
method: "patch",
path: "/auth/users/{userId}",
tags,
summary: "Edit a user (superadmin)",
security: bearer,
request: {
params: editUserByIdParamValidation,
body: { content: json(editUserByIdBodyValidation) },
},
responses: { "200": okJsonNoData(), ...standardErrors },
});
registry.registerPath({
method: "patch",
path: "/auth/users/{userId}/password",
tags,
summary: "Change a user's password (superadmin)",
security: bearer,
request: {
params: editUserByIdParamValidation,
body: { content: json(editUserPasswordByIdBodyValidation) },
},
responses: { "200": okJsonNoData(), ...standardErrors },
});
registry.registerPath({
method: "delete",
path: "/auth/users/{userId}",
tags,
summary: "Delete a user (admin/superadmin)",
security: bearer,
request: { params: getUserByIdParamValidation },
responses: { "200": okJsonNoData(), ...standardErrors },
});
registry.registerPath({
method: "patch",
path: "/auth/user",
tags,
summary: "Edit the currently authenticated user",
security: bearer,
request: { body: { content: multipart(editUserBodyValidation.shape, "profileImage") } },
responses: { "200": okJson(userObject), ...standardErrors },
});
registry.registerPath({
method: "delete",
path: "/auth/user",
tags,
summary: "Delete the currently authenticated user",
security: bearer,
responses: { "200": okJsonNoData(), ...standardErrors },
});
+61
View File
@@ -0,0 +1,61 @@
import { z } from "zod";
import { registry } from "../registry.js";
import { bearer, okJson, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
import {
getChecksParamValidation,
getChecksQueryValidation,
getTeamChecksQueryValidation,
deleteChecksParamValidation,
getChecksSummaryByTeamIdQueryValidation,
} from "@/validation/checkValidation.js";
const tags = ["checks"];
registry.registerPath({
method: "get",
path: "/checks/team/summary",
tags,
summary: "Aggregate check summary for the caller's team",
security: bearer,
request: { query: getChecksSummaryByTeamIdQueryValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/checks/team",
tags,
summary: "List checks across the team",
security: bearer,
request: { query: getTeamChecksQueryValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "delete",
path: "/checks/team",
tags,
summary: "Delete all checks for the team (admin/superadmin)",
security: bearer,
responses: { "200": okJson(z.object({ deletedCount: z.number() })), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/checks/{monitorId}",
tags,
summary: "List checks for a monitor",
security: bearer,
request: { params: getChecksParamValidation, query: getChecksQueryValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "delete",
path: "/checks/{monitorId}",
tags,
summary: "Delete checks for a monitor",
security: bearer,
request: { params: deleteChecksParamValidation },
responses: { "200": okJsonNoData(), ...standardErrors },
});
+11
View File
@@ -0,0 +1,11 @@
import { registry } from "../registry.js";
import { bearer, okUnknown, standardErrors } from "../helpers.js";
registry.registerPath({
method: "get",
path: "/diagnostic/system",
tags: ["diagnostic"],
summary: "Get system diagnostics (admin/superadmin)",
security: bearer,
responses: { "200": okUnknown, ...standardErrors },
});
+13
View File
@@ -0,0 +1,13 @@
import { registry } from "../registry.js";
import { bearer, okUnknown, standardErrors } from "../helpers.js";
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
registry.registerPath({
method: "get",
path: "/geo-checks/{monitorId}",
tags: ["geo-checks"],
summary: "Get geo check results for a monitor",
security: bearer,
request: { params: getChecksParamValidation, query: getChecksQueryValidation },
responses: { "200": okUnknown, ...standardErrors },
});
+104
View File
@@ -0,0 +1,104 @@
import { z } from "zod";
import { registry } from "../registry.js";
import { bearer, okJson, okJsonNoData, standardErrors } from "../helpers.js";
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
const tags = ["incidents"];
const incidentIdParam = z.object({ incidentId: z.string() });
const incidentObject = z
.object({
id: z.string().openapi({ example: "65f1c2a4d8b9e0123456789c" }),
monitorId: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }),
teamId: z.string().openapi({ example: "65f1c2a4d8b9e01234567890" }),
startTime: z.string().openapi({ example: "2026-04-15T03:21:00.000Z" }),
endTime: z.string().nullable().openapi({ example: "2026-04-15T03:34:00.000Z" }),
status: z.boolean().openapi({ example: true, description: "true = resolved, false = ongoing" }),
message: z.string().nullable().optional().openapi({ example: "HTTP 503 from origin" }),
statusCode: z.number().nullable().optional().openapi({ example: 503 }),
resolutionType: z.enum(["automatic", "manual"]).nullable().openapi({ example: "automatic" }),
resolvedBy: z.string().nullable().optional(),
resolvedByEmail: z.string().nullable().optional(),
comment: z.string().nullable().optional(),
createdAt: z.string().openapi({ example: "2026-04-15T03:21:00.000Z" }),
updatedAt: z.string().openapi({ example: "2026-04-15T03:34:00.000Z" }),
})
.passthrough()
.openapi("Incident");
const incidentSummaryItem = z
.object({
id: z.string().openapi({ example: "65f1c2a4d8b9e0123456789c" }),
monitorId: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }),
monitorName: z.string().nullable().openapi({ example: "Marketing site" }),
status: z.boolean().openapi({ example: true }),
startTime: z.string().openapi({ example: "2026-04-15T03:21:00.000Z" }),
endTime: z.string().nullable().openapi({ example: "2026-04-15T03:34:00.000Z" }),
resolutionType: z.enum(["automatic", "manual"]).nullable().openapi({ example: "automatic" }),
message: z.string().nullable().openapi({ example: "HTTP 503 from origin" }),
statusCode: z.number().nullable().openapi({ example: 503 }),
createdAt: z.string().openapi({ example: "2026-04-15T03:21:00.000Z" }),
})
.openapi("IncidentSummaryItem");
const incidentSummary = z
.object({
total: z.number().openapi({ example: 12 }),
totalActive: z.number().openapi({ example: 1 }),
totalManualResolutions: z.number().openapi({ example: 2 }),
totalAutomaticResolutions: z.number().openapi({ example: 9 }),
avgResolutionTimeHours: z.number().openapi({ example: 0.5 }),
topMonitor: z
.object({
monitorId: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }),
monitorName: z.string().nullable().openapi({ example: "Marketing site" }),
incidentCount: z.number().openapi({ example: 4 }),
})
.nullable(),
latestIncidents: z.array(incidentSummaryItem),
})
.openapi("IncidentSummary");
registry.registerPath({
method: "get",
path: "/incidents/team",
tags,
summary: "List incidents for the caller's team",
security: bearer,
request: { query: getIncidentsByTeamQueryValidation },
responses: { "200": okJson(z.array(incidentObject)), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/incidents/team/summary",
tags,
summary: "Incident summary for the caller's team",
security: bearer,
request: { query: getIncidentSummaryQueryValidation },
responses: { "200": okJson(incidentSummary), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/incidents/{incidentId}",
tags,
summary: "Get an incident by id",
security: bearer,
request: { params: incidentIdParam },
responses: {
"200": okJson(z.object({ incident: incidentObject, monitor: z.unknown(), user: z.unknown().nullable() })),
...standardErrors,
},
});
registry.registerPath({
method: "put",
path: "/incidents/{incidentId}/resolve",
tags,
summary: "Manually resolve an incident (admin/superadmin)",
security: bearer,
request: { params: incidentIdParam },
responses: { "200": okJsonNoData(), ...standardErrors },
});
+35
View File
@@ -0,0 +1,35 @@
import { registry } from "../registry.js";
import { bearer, json, okJson, okJsonNoData, standardErrors } from "../helpers.js";
import { z } from "zod";
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js";
const tags = ["invite"];
registry.registerPath({
method: "post",
path: "/invite/send",
tags,
summary: "Send an invite email (admin/superadmin)",
security: bearer,
request: { body: { content: json(inviteBodyValidation) } },
responses: { "200": okJsonNoData(), ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/invite/verify",
tags,
summary: "Verify an invite token",
request: { body: { content: json(inviteVerificationBodyValidation) } },
responses: { "200": okJson(z.object({ email: z.string(), role: z.array(z.string()).optional() }).passthrough()), "500": standardErrors["500"] },
});
registry.registerPath({
method: "post",
path: "/invite",
tags,
summary: "Create an invite token (admin/superadmin)",
security: bearer,
request: { body: { content: json(inviteBodyValidation) } },
responses: { "200": okJson(z.object({ token: z.string() }).passthrough()), ...standardErrors },
});
+11
View File
@@ -0,0 +1,11 @@
import { registry } from "../registry.js";
import { bearer, okUnknown, standardErrors } from "../helpers.js";
registry.registerPath({
method: "get",
path: "/logs",
tags: ["logs"],
summary: "Get application logs (admin/superadmin)",
security: bearer,
responses: { "200": okUnknown, ...standardErrors },
});
@@ -0,0 +1,73 @@
import { registry } from "../registry.js";
import { bearer, json, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
import {
createMaintenanceWindowBodyValidation,
getMaintenanceWindowByIdParamValidation,
getMaintenanceWindowsByTeamIdQueryValidation,
getMaintenanceWindowsByMonitorIdParamValidation,
deleteMaintenanceWindowByIdParamValidation,
editMaintenanceWindowByIdParamValidation,
editMaintenanceByIdWindowBodyValidation,
} from "@/validation/maintenanceWindowValidation.js";
const tags = ["maintenance-window"];
registry.registerPath({
method: "post",
path: "/maintenance-window",
tags,
summary: "Create one or more maintenance windows",
security: bearer,
request: { body: { content: json(createMaintenanceWindowBodyValidation) } },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/maintenance-window/team",
tags,
summary: "List maintenance windows for the caller's team",
security: bearer,
request: { query: getMaintenanceWindowsByTeamIdQueryValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/maintenance-window/monitor/{monitorId}",
tags,
summary: "List maintenance windows for a monitor",
security: bearer,
request: { params: getMaintenanceWindowsByMonitorIdParamValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/maintenance-window/{id}",
tags,
summary: "Get a maintenance window by id",
security: bearer,
request: { params: getMaintenanceWindowByIdParamValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "patch",
path: "/maintenance-window/{id}",
tags,
summary: "Edit a maintenance window",
security: bearer,
request: { params: editMaintenanceWindowByIdParamValidation, body: { content: json(editMaintenanceByIdWindowBodyValidation) } },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "delete",
path: "/maintenance-window/{id}",
tags,
summary: "Delete a maintenance window",
security: bearer,
request: { params: deleteMaintenanceWindowByIdParamValidation },
responses: { "200": okJsonNoData(), ...standardErrors },
});
+247
View File
@@ -0,0 +1,247 @@
import { z } from "zod";
import { registry } from "../registry.js";
import { bearer, json, okJson, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
import { GeoContinents } from "@/types/geoCheck.js";
import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js";
import {
getMonitorByIdParamValidation,
getMonitorByIdQueryValidation,
getMonitorsByTeamIdQueryValidation,
getMonitorsWithChecksQueryValidation,
getCertificateParamValidation,
createMonitorBodyValidation,
editMonitorBodyValidation,
pauseMonitorParamValidation,
getUptimeDetailsByIdParamValidation,
getUptimeDetailsByIdQueryValidation,
importMonitorsBodyValidation,
getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation,
} from "@/validation/monitorValidation.js";
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
const tags = ["monitors"];
const monitorObject = z
.object({
_id: z.string().openapi({ example: "65f1c2a4d8b9e0123456789a" }),
name: z.string().openapi({ example: "Marketing site" }),
description: z.string().optional().openapi({ example: "Production marketing site monitored from the EU region" }),
type: z.enum(MonitorTypes).openapi({ example: "http" }),
url: z.string().openapi({ example: "https://www.example.com" }),
port: z.number().optional().openapi({ example: 443 }),
isActive: z.boolean().openapi({ example: true }),
interval: z.number().openapi({ example: 60000 }),
status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).openapi({ example: "up" }),
statusWindowSize: z.number().openapi({ example: 5 }),
statusWindowThreshold: z.number().openapi({ example: 3 }),
ignoreTlsErrors: z.boolean().openapi({ example: false }),
useAdvancedMatching: z.boolean().openapi({ example: false }),
jsonPath: z.string().optional(),
expectedValue: z.string().optional(),
matchMethod: z.enum(MonitorMatchMethods).optional(),
notifications: z.array(z.string()).openapi({ example: ["65f1c2a4d8b9e0123456789b"] }),
secret: z.string().optional(),
cpuAlertThreshold: z.number().openapi({ example: 90 }),
memoryAlertThreshold: z.number().openapi({ example: 90 }),
diskAlertThreshold: z.number().openapi({ example: 90 }),
tempAlertThreshold: z.number().openapi({ example: 80 }),
selectedDisks: z.array(z.string()).openapi({ example: [] }),
gameId: z.string().optional(),
grpcServiceName: z.string().optional(),
group: z.string().nullable().optional(),
geoCheckEnabled: z.boolean().openapi({ example: false }),
geoCheckLocations: z.array(z.enum(GeoContinents)).openapi({ example: [] }),
geoCheckInterval: z.number().openapi({ example: 300000 }),
teamId: z.string().openapi({ example: "65f1c2a4d8b9e01234567890" }),
userId: z.string().openapi({ example: "65f1c2a4d8b9e01234567891" }),
createdAt: z.string().openapi({ example: "2026-04-01T10:00:00.000Z" }),
updatedAt: z.string().openapi({ example: "2026-04-15T14:30:00.000Z" }),
})
.passthrough()
.openapi("Monitor");
registry.registerPath({
method: "get",
path: "/monitors/team",
tags,
summary: "List monitors for the caller's team",
security: bearer,
request: { query: getMonitorsByTeamIdQueryValidation },
responses: { "200": okJson(z.array(monitorObject)), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/team/with-checks",
tags,
summary: "List team monitors with their most recent checks (paginated)",
security: bearer,
request: { query: getMonitorsWithChecksQueryValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/team/groups",
tags,
summary: "List monitor groups for the caller's team",
security: bearer,
responses: { "200": okJson(z.array(z.string())), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/uptime/details/{monitorId}",
tags,
summary: "Get uptime details for a monitor",
security: bearer,
request: { params: getUptimeDetailsByIdParamValidation, query: getUptimeDetailsByIdQueryValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/hardware/details/{monitorId}",
tags,
summary: "Get hardware metrics detail for a monitor",
security: bearer,
request: { params: getHardwareDetailsByIdParamValidation, query: getHardwareDetailsByIdQueryValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/pagespeed/details/{monitorId}",
tags,
summary: "Get PageSpeed detail for a monitor",
security: bearer,
request: { params: getHardwareDetailsByIdParamValidation, query: getHardwareDetailsByIdQueryValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/{monitorId}/geo-checks",
tags,
summary: "Get geo check results for a monitor",
security: bearer,
request: { params: getMonitorByIdParamValidation, query: getMonitorByIdQueryValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/monitors/pause/{monitorId}",
tags,
summary: "Toggle pause state for a monitor",
security: bearer,
request: { params: pauseMonitorParamValidation },
responses: { "200": okJson(monitorObject), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/certificate/{monitorId}",
tags,
summary: "Get SSL certificate info for a monitor",
security: bearer,
request: { params: getCertificateParamValidation },
responses: { "200": okJson(z.object({ certificateDate: z.string() })), ...standardErrors },
});
registry.registerPath({
method: "patch",
path: "/monitors/notifications",
tags,
summary: "Bulk update notifications across monitors",
security: bearer,
request: { body: { content: json(updateNotificationsValidation) } },
responses: { "200": okJson(z.object({ modifiedCount: z.number() })), ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/monitors",
tags,
summary: "Create a new monitor",
security: bearer,
request: { body: { content: json(createMonitorBodyValidation) } },
responses: { "200": okJson(monitorObject, "Monitor created"), ...standardErrors },
});
registry.registerPath({
method: "delete",
path: "/monitors",
tags,
summary: "Delete every monitor (superadmin only)",
security: bearer,
responses: { "200": okJsonNoData(), ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/monitors/demo",
tags,
summary: "Insert preconfigured demo monitors",
security: bearer,
responses: { "200": okJson(z.number()), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/export/json",
tags,
summary: "Export all team monitors as JSON",
security: bearer,
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/monitors/import/json",
tags,
summary: "Import monitors from JSON",
security: bearer,
request: { body: { content: json(importMonitorsBodyValidation) } },
responses: { "200": okJson(z.object({ imported: z.number() }).passthrough()), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/games",
tags,
summary: "List supported game-server types",
security: bearer,
responses: { "200": okJson(z.array(z.string())), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/monitors/{monitorId}",
tags,
summary: "Get a monitor by id",
security: bearer,
request: { params: getMonitorByIdParamValidation },
responses: { "200": okJson(monitorObject), ...standardErrors },
});
registry.registerPath({
method: "patch",
path: "/monitors/{monitorId}",
tags,
summary: "Edit a monitor",
security: bearer,
request: { params: getMonitorByIdParamValidation, body: { content: json(editMonitorBodyValidation) } },
responses: { "200": okJson(monitorObject), ...standardErrors },
});
registry.registerPath({
method: "delete",
path: "/monitors/{monitorId}",
tags,
summary: "Delete a monitor",
security: bearer,
request: { params: getMonitorByIdParamValidation },
responses: { "200": okJson(monitorObject), ...standardErrors },
});
+155
View File
@@ -0,0 +1,155 @@
import { z } from "zod";
import { registry } from "../registry.js";
import { bearer, json, okJson, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
import {
createNotificationBodyValidation,
deleteNotificationParamValidation,
getNotificationByIdParamValidation,
editNotificationParamValidation,
testAllNotificationsBodyValidation,
} from "@/validation/notificationValidation.js";
const tags = ["notifications"];
// OpenAPI metadata for each notification variant. Keyed by the discriminator
// value in createNotificationBodyValidation; build fails loudly if a variant
// in the validator has no entry here, which doubles as drift detection.
const notificationVariantMeta: Record<string, { component: string; example: Record<string, unknown> }> = {
email: {
component: "EmailNotification",
example: { notificationName: "Ops on-call email", type: "email", address: "alerts@example.com" },
},
webhook: {
component: "WebhookNotification",
example: { notificationName: "Custom webhook", type: "webhook", address: "https://example.com/hooks/checkmate" },
},
slack: {
component: "SlackNotification",
example: { notificationName: "#alerts", type: "slack", address: "https://hooks.slack.com/services/T000/B000/XXXX" },
},
discord: {
component: "DiscordNotification",
example: { notificationName: "#status", type: "discord", address: "https://discord.com/api/webhooks/123/abc" },
},
pager_duty: {
component: "PagerDutyNotification",
example: { notificationName: "PagerDuty primary", type: "pager_duty", address: "R01XXXXXXXXXXXXXXXXXXXXXXX" },
},
matrix: {
component: "MatrixNotification",
example: {
notificationName: "Matrix room",
type: "matrix",
homeserverUrl: "https://matrix.example.com",
roomId: "!abc123:example.com",
accessToken: "syt_xxx",
},
},
teams: {
component: "TeamsNotification",
example: { notificationName: "Teams ops channel", type: "teams", address: "https://outlook.office.com/webhook/..." },
},
telegram: {
component: "TelegramNotification",
example: { notificationName: "Telegram bot", type: "telegram", address: "-1001234567890", accessToken: "123456:ABC-DEF" },
},
pushover: {
component: "PushoverNotification",
example: { notificationName: "Pushover personal", type: "pushover", address: "u1234567890abcdef", accessToken: "a1234567890abcdef" },
},
twilio: {
component: "TwilioNotification",
example: {
notificationName: "Twilio SMS",
type: "twilio",
accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
accessToken: "your-auth-token",
phone: "+15551234567",
twilioPhoneNumber: "+15557654321",
},
},
};
const decoratedVariants = createNotificationBodyValidation.options.map((variant) => {
const typeField = variant.shape.type as z.ZodLiteral<string>;
const type = typeField.value;
const meta = notificationVariantMeta[type];
if (!meta) {
throw new Error(`Missing OpenAPI metadata for notification variant "${type}". Add an entry in notificationVariantMeta.`);
}
return variant.openapi(meta.component, { example: meta.example });
});
const notificationBody = z
.discriminatedUnion("type", decoratedVariants as typeof createNotificationBodyValidation.options)
.openapi("NotificationChannelBody");
registry.registerPath({
method: "post",
path: "/notifications",
tags,
summary: "Create a notification channel",
description: "Create a notification channel of any supported type. The `type` field discriminates the body shape.",
security: bearer,
request: { body: { content: json(notificationBody) } },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/notifications/test/all",
tags,
summary: "Send a test alert through every notification channel for the team",
security: bearer,
request: { body: { content: json(testAllNotificationsBodyValidation) } },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/notifications/test",
tags,
summary: "Send a test alert through a single notification channel",
security: bearer,
request: { body: { content: json(notificationBody) } },
responses: { "200": okJson(z.object({ success: z.boolean() }).passthrough()), ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/notifications/team",
tags,
summary: "List notification channels for the caller's team",
security: bearer,
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/notifications/{id}",
tags,
summary: "Get a notification channel by id",
security: bearer,
request: { params: getNotificationByIdParamValidation },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "delete",
path: "/notifications/{id}",
tags,
summary: "Delete a notification channel",
security: bearer,
request: { params: deleteNotificationParamValidation },
responses: { "200": okJsonNoData(), ...standardErrors },
});
registry.registerPath({
method: "patch",
path: "/notifications/{id}",
tags,
summary: "Edit a notification channel",
security: bearer,
request: { params: editNotificationParamValidation, body: { content: json(notificationBody) } },
responses: { "200": okUnknown, ...standardErrors },
});
+40
View File
@@ -0,0 +1,40 @@
import { registry } from "../registry.js";
import { bearer, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
const tags = ["queue"];
registry.registerPath({
method: "get",
path: "/queue/jobs",
tags,
summary: "List queued monitor jobs (admin/superadmin)",
security: bearer,
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/queue/metrics",
tags,
summary: "Get queue runtime metrics (admin/superadmin)",
security: bearer,
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/queue/all-metrics",
tags,
summary: "Get queue metrics across all monitors (admin/superadmin)",
security: bearer,
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/queue/flush",
tags,
summary: "Flush the monitor job queue (admin/superadmin)",
security: bearer,
responses: { "200": okJsonNoData(), ...standardErrors },
});
+35
View File
@@ -0,0 +1,35 @@
import { registry } from "../registry.js";
import { bearer, json, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
const tags = ["settings"];
registry.registerPath({
method: "get",
path: "/settings",
tags,
summary: "Get application settings",
security: bearer,
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "patch",
path: "/settings",
tags,
summary: "Update application settings (admin/superadmin)",
security: bearer,
request: { body: { content: json(updateAppSettingsBodyValidation) } },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/settings/test-email",
tags,
summary: "Send a test email using current SMTP settings (admin/superadmin)",
security: bearer,
request: { body: { content: json(sendTestEmailBodyValidation) } },
responses: { "200": okJsonNoData(), ...standardErrors },
});
+52
View File
@@ -0,0 +1,52 @@
import { registry } from "../registry.js";
import { bearer, multipart, okJsonNoData, okUnknown, standardErrors } from "../helpers.js";
import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation } from "@/validation/statusPageValidation.js";
const tags = ["status-page"];
registry.registerPath({
method: "get",
path: "/status-page/team",
tags,
summary: "List status pages for the caller's team",
security: bearer,
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "post",
path: "/status-page",
tags,
summary: "Create a status page (logo upload optional)",
security: bearer,
request: { body: { content: multipart(createStatusPageBodyValidation.shape, "logo") } },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "put",
path: "/status-page/{id}",
tags,
summary: "Update a status page (logo upload optional)",
security: bearer,
request: { body: { content: multipart(createStatusPageBodyValidation.shape, "logo") } },
responses: { "200": okUnknown, ...standardErrors },
});
registry.registerPath({
method: "get",
path: "/status-page/{url}",
tags,
summary: "Get a public status page by its URL slug",
request: { params: getStatusPageParamValidation, query: getStatusPageQueryValidation },
responses: { "200": okUnknown, "404": { description: "Status page not found" }, "500": standardErrors["500"] },
});
registry.registerPath({
method: "delete",
path: "/status-page/{id}",
tags,
summary: "Delete a status page",
security: bearer,
responses: { "200": okJsonNoData(), ...standardErrors },
});
+41
View File
@@ -46,6 +46,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@asteasolutions/zod-to-openapi": "8.5.0",
"@eslint/js": "^9.17.0",
"@types/compression": "1.8.1",
"@types/cookie-parser": "1.4.10",
@@ -126,6 +127,19 @@
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
"license": "MIT"
},
"node_modules/@asteasolutions/zod-to-openapi": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.5.0.tgz",
"integrity": "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"openapi3-ts": "^4.1.2"
},
"peerDependencies": {
"zod": "^4.0.0"
}
},
"node_modules/@aws-crypto/sha256-browser": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
@@ -851,6 +865,7 @@
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
@@ -1470,6 +1485,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -1514,6 +1530,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -4227,6 +4244,7 @@
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
@@ -4387,6 +4405,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -4624,6 +4643,7 @@
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.56.1",
"@typescript-eslint/types": "8.56.1",
@@ -5237,6 +5257,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5815,6 +5836,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -6708,6 +6730,7 @@
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"cssnano-preset-default": "^7.0.9",
"lilconfig": "^3.1.3"
@@ -7472,6 +7495,7 @@
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7833,6 +7857,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -9501,6 +9526,7 @@
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "30.2.0",
"@jest/types": "30.2.0",
@@ -11944,6 +11970,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi3-ts": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz",
"integrity": "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"yaml": "^2.8.0"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -12335,6 +12371,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -14357,6 +14394,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14525,6 +14563,7 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -14688,6 +14727,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -15522,6 +15562,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+4 -1
View File
@@ -11,7 +11,9 @@
"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --no-coverage test/integration",
"dev": "nodemon --exec tsx src/index.js",
"start": "node --watch ./dist/index.js",
"build": "tsc && tsc-alias && cp -r src/templates dist/templates",
"clean": "rm -rf dist",
"build": "npm run clean && npm run build:openapi && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && cp -r src/templates dist/templates",
"build:openapi": "tsc --noEmit && tsx scripts/buildOpenApi.ts",
"lint": "eslint .",
"lint-fix": "eslint --fix .",
"format": "prettier --write .",
@@ -64,6 +66,7 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@asteasolutions/zod-to-openapi": "8.5.0",
"@eslint/js": "^9.17.0",
"@types/compression": "1.8.1",
"@types/cookie-parser": "1.4.10",
+10
View File
@@ -0,0 +1,10 @@
import { writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { getOpenApiSpec } from "../openapi/index.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const outPath = path.resolve(__dirname, "..", "openapi.json");
writeFileSync(outPath, JSON.stringify(getOpenApiSpec(), null, 2) + "\n");
console.log(`OpenAPI spec written to ${outPath}`);
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"noEmit": false
},
"include": ["src"]
}
+3 -3
View File
@@ -1,6 +1,5 @@
{
"compilerOptions": {
"rootDir": "./src",
"allowJs": true,
"checkJs": false,
"paths": {
@@ -15,8 +14,9 @@
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"isolatedModules": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"noEmit": true
},
"include": ["src"],
"include": ["src", "openapi", "scripts"],
"exclude": ["tests", "dist", "node_modules"]
}