more package declarations

This commit is contained in:
2026-05-17 21:52:38 -04:00
parent a8a5930ced
commit f118d3a4f3
44 changed files with 14019 additions and 1918 deletions

View File

@@ -1,4 +1,11 @@
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || process.env.NEXTAUTH_SECRET;
if (!JWT_SECRET && process.env.NODE_ENV === 'production') {
console.error('JWT_SECRET or NEXTAUTH_SECRET must be set in production');
}
export interface AuthRequest extends FastifyRequest {
user?: {
@@ -27,17 +34,35 @@ export async function authMiddleware(fastify: FastifyInstance) {
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.slice(7);
try {
// In production, decode and verify JWT
// For now, we'll attach a placeholder user
if (!JWT_SECRET) {
throw new Error('JWT_SECRET not configured');
}
const decoded = jwt.verify(token, JWT_SECRET) as {
id: string;
email: string;
role: string;
organizationId?: string;
iat?: number;
exp?: number;
};
authReq.user = {
id: 'user-placeholder',
email: 'user@example.com',
role: 'user',
id: decoded.id,
email: decoded.email,
role: decoded.role,
organizationId: decoded.organizationId,
};
authReq.authType = 'jwt';
return;
} catch (err) {
// JWT invalid, continue to API key check
if (err instanceof jwt.JsonWebTokenError) {
throw { statusCode: 401, message: 'Invalid token' };
}
if (err instanceof jwt.TokenExpiredError) {
throw { statusCode: 401, message: 'Token expired', expiredAt: err.expiredAt };
}
throw { statusCode: 401, message: 'Authentication failed' };
}
}

View File

@@ -7,39 +7,97 @@ function getUserId(request: FastifyRequest): string | undefined {
return (request.user as AuthUser | undefined)?.id;
}
const timeWindowSchema = {
type: "object",
properties: {
timeWindow: { type: "integer", minimum: 1, maximum: 10080 },
},
};
const paginatedQuerySchema = {
type: "object",
properties: {
timeWindow: { type: "integer", minimum: 1, maximum: 10080 },
limit: { type: "integer", minimum: 1, maximum: 200 },
offset: { type: "integer", minimum: 0, maximum: 10000 },
},
};
export function correlationRoutes(fastify: FastifyInstance) {
fastify.get("/dashboard", async (request, reply) => {
const userId = getUserId(request);
if (!userId || userId === "anonymous") {
return reply.code(401).send({ error: "User not authenticated" });
fastify.get(
"/dashboard",
{
schema: {
...timeWindowSchema,
response: {
"400": {
type: "object",
properties: {
error: { type: "string" },
},
},
"401": {
type: "object",
properties: {
error: { type: "string" },
},
},
},
},
},
async (request, reply) => {
const userId = getUserId(request);
if (!userId || userId === "anonymous") {
return reply.code(401).send({ error: "User not authenticated" });
}
const query = request.query as Record<string, string | number>;
const timeWindow =
typeof query.timeWindow === "number" ? query.timeWindow : 60;
const data = await correlationService.getDashboardData(userId, timeWindow);
return reply.send(data);
}
);
const timeWindow =
parseInt(
(request.query as Record<string, string>).timeWindow as string
) || 60;
const data = await correlationService.getDashboardData(userId, timeWindow);
return reply.send(data);
});
fastify.get(
"/groups",
{
schema: {
...paginatedQuerySchema,
response: {
"400": {
type: "object",
properties: {
error: { type: "string" },
},
},
"401": {
type: "object",
properties: {
error: { type: "string" },
},
},
},
},
},
async (request, reply) => {
const userId = getUserId(request);
if (!userId || userId === "anonymous") {
return reply.code(401).send({ error: "User not authenticated" });
}
fastify.get("/groups", async (request, reply) => {
const userId = getUserId(request);
if (!userId || userId === "anonymous") {
return reply.code(401).send({ error: "User not authenticated" });
const query = request.query as Record<string, string | number>;
const result = await correlationService.getCorrelationGroups({
userId,
status: (query.status as any) || undefined,
timeWindowMinutes:
typeof query.timeWindow === "number" ? query.timeWindow : 60,
limit: typeof query.limit === "number" ? query.limit : 50,
offset: typeof query.offset === "number" ? query.offset : 0,
});
return reply.send(result);
}
const query = request.query as Record<string, string>;
const result = await correlationService.getCorrelationGroups({
userId,
status: (query.status as any) || undefined,
timeWindowMinutes: query.timeWindow
? parseInt(query.timeWindow)
: 60,
limit: query.limit ? parseInt(query.limit) : 50,
offset: query.offset ? parseInt(query.offset) : 0,
});
return reply.send(result);
});
);
fastify.get(
"/groups/:groupId",
@@ -114,26 +172,47 @@ export function correlationRoutes(fastify: FastifyInstance) {
}
);
fastify.get("/alerts", async (request, reply) => {
const userId = getUserId(request);
if (!userId || userId === "anonymous") {
return reply.code(401).send({ error: "User not authenticated" });
}
fastify.get(
"/alerts",
{
schema: {
...paginatedQuerySchema,
response: {
"400": {
type: "object",
properties: {
error: { type: "string" },
},
},
"401": {
type: "object",
properties: {
error: { type: "string" },
},
},
},
},
},
async (request, reply) => {
const userId = getUserId(request);
if (!userId || userId === "anonymous") {
return reply.code(401).send({ error: "User not authenticated" });
}
const query = request.query as Record<string, string>;
const result = await correlationService.getCorrelatedAlerts({
userId,
source: (query.source as any) || undefined,
category: (query.category as any) || undefined,
severity: (query.severity as any) || undefined,
timeWindowMinutes: query.timeWindow
? parseInt(query.timeWindow)
: 60,
limit: query.limit ? parseInt(query.limit) : 50,
offset: query.offset ? parseInt(query.offset) : 0,
});
return reply.send(result);
});
const query = request.query as Record<string, string | number>;
const result = await correlationService.getCorrelatedAlerts({
userId,
source: (query.source as any) || undefined,
category: (query.category as any) || undefined,
severity: (query.severity as any) || undefined,
timeWindowMinutes:
typeof query.timeWindow === "number" ? query.timeWindow : 60,
limit: typeof query.limit === "number" ? query.limit : 50,
offset: typeof query.offset === "number" ? query.offset : 0,
});
return reply.send(result);
}
);
fastify.post(
"/ingest/darkwatch",

View File

@@ -169,4 +169,9 @@ export async function reportRoutes(fastify: FastifyInstance) {
const createdIds = await reportService.scheduleAnnualReports();
return reply.code(200).send({ scheduled: createdIds.length, reportIds: createdIds });
});
fastify.post('/schedule/weekly-digest', async (request: FastifyRequest, reply: FastifyReply) => {
const createdIds = await reportService.scheduleWeeklyDigest();
return reply.code(200).send({ scheduled: createdIds.length, reportIds: createdIds });
});
}

View File

@@ -4,6 +4,7 @@ import Fastify from "fastify";
import cors from "@fastify/cors";
import helmet from "@fastify/helmet";
import sensible from "@fastify/sensible";
import rawBody from "fastify-raw-body";
import { extractOrGenerateRequestId } from "@shieldai/types";
import { authMiddleware } from "./middleware/auth.middleware";
import { errorHandlingMiddleware } from "./middleware/error-handling.middleware";
@@ -16,8 +17,13 @@ import { extensionRoutes } from "./routes/extension.routes";
import { waitlistRoutes } from "./routes/waitlist.routes";
import { blogRoutes } from "./routes/blog.routes";
import { blogAdminRoutes } from "./routes/blog-admin.routes";
import { routes } from "./routes";
import { captureSentryError } from "@shieldai/monitoring";
import { getCorsOrigins } from "./config/api.config";
import fastifySwagger from "@fastify/swagger";
import fastifySwaggerUi from "@fastify/swagger-ui";
import * as fs from "fs";
import * as path from "path";
const app = Fastify({
logger: {
@@ -30,6 +36,7 @@ async function bootstrap() {
await app.register(cors, { origin: corsOrigins });
await app.register(helmet);
await app.register(sensible);
await app.register(rawBody, { runFirst: true });
// Register auth middleware to populate request.user
await app.register(authMiddleware);
@@ -52,16 +59,54 @@ async function bootstrap() {
request.headers["x-request-id"] = requestId;
});
await app.register(darkwatchRoutes);
await app.register(voiceprintRoutes);
await app.register(correlationRoutes);
await app.register(extensionRoutes, { prefix: '/extension' });
await app.register(waitlistRoutes);
await app.register(blogRoutes, { prefix: '/blog' });
await app.register(blogAdminRoutes);
await app.register(routes);
app.get("/health", async () => ({ status: "ok", timestamp: new Date().toISOString() }));
// Swagger/OpenAPI documentation
const openapiSpec = JSON.parse(
fs.readFileSync(path.join(__dirname, "openapi", "spec.json"), "utf-8"),
) as Record<string, unknown>;
const swaggerDefinition: Record<string, unknown> = {
openapi: "3.0.3",
info: {
title: "ShieldAI API",
description:
"ShieldAI API documentation — reverse-engineer endpoints and run contract tests",
version: "1.0.0",
},
servers: openapiSpec.servers,
paths: openapiSpec.paths,
components: openapiSpec.components,
security: openapiSpec.security,
tags: openapiSpec.tags,
};
await app.register(fastifySwagger, {
openapi: swaggerDefinition,
});
await app.register(fastifySwaggerUi, {
routePrefix: "/docs",
uiConfig: {
docExpansion: "list",
},
staticCSP: true,
theme: {
js: [
{
filename: "custom.js",
content: `
window.addEventListener('DOMContentLoaded', () => {
document.querySelector('link[rel="icon"]')?.remove();
});
`,
},
],
},
});
try {
await app.listen({ port: parseInt(process.env.PORT || "3000", 10), host: "0.0.0.0" });
app.log.info(`Server listening on port ${process.env.PORT || 3000}`);