security: bound Zod inputs, add SSE per-user cap and tRPC body limit (#51, PR #59)
CI / Architecture Guardrails (push) Successful in 3m38s
CI / Assistant Split Regression (push) Successful in 4m40s
CI / Lint (push) Successful in 5m17s
CI / Typecheck (push) Successful in 5m46s
CI / Build (push) Successful in 7m1s
CI / Unit Tests (push) Failing after 9m41s
CI / Release Images (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / E2E Tests (push) Has started running

Closes #51 (ESLint rule + conventions doc remain as follow-up).

Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #59.
This commit is contained in:
2026-04-18 13:53:28 +02:00
committed by Hartmut
parent f0251a654a
commit 17471af7f8
12 changed files with 254 additions and 148 deletions
@@ -2,13 +2,18 @@ import { PermissionKey, SkillEntrySchema } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { adminProcedure, managerProcedure, protectedProcedure, requirePermission } from "../trpc.js";
import {
adminProcedure,
managerProcedure,
protectedProcedure,
requirePermission,
} from "../trpc.js";
const employeeInfoSchema = z
.object({
roleId: z.string().optional(),
yearsOfExperience: z.number().optional(),
portfolioUrl: z.string().url().optional().or(z.literal("")),
roleId: z.string().max(64).optional(),
yearsOfExperience: z.number().min(0).max(100).optional(),
portfolioUrl: z.string().url().max(2048).optional().or(z.literal("")),
})
.optional();
@@ -16,7 +21,7 @@ export const resourceSkillImportProcedures = {
importSkillMatrix: protectedProcedure
.input(
z.object({
skills: z.array(SkillEntrySchema),
skills: z.array(SkillEntrySchema).max(2000),
employeeInfo: employeeInfoSchema,
}),
)
@@ -40,7 +45,9 @@ export const resourceSkillImportProcedures = {
...(input.employeeInfo?.portfolioUrl !== undefined
? { portfolioUrl: input.employeeInfo.portfolioUrl || null }
: {}),
...(input.employeeInfo?.roleId !== undefined ? { roleId: input.employeeInfo.roleId } : {}),
...(input.employeeInfo?.roleId !== undefined
? { roleId: input.employeeInfo.roleId }
: {}),
},
});
@@ -50,8 +57,8 @@ export const resourceSkillImportProcedures = {
importSkillMatrixForResource: managerProcedure
.input(
z.object({
resourceId: z.string(),
skills: z.array(SkillEntrySchema),
resourceId: z.string().max(64),
skills: z.array(SkillEntrySchema).max(2000),
employeeInfo: employeeInfoSchema,
}),
)
@@ -70,7 +77,9 @@ export const resourceSkillImportProcedures = {
...(input.employeeInfo?.portfolioUrl !== undefined
? { portfolioUrl: input.employeeInfo.portfolioUrl || null }
: {}),
...(input.employeeInfo?.roleId !== undefined ? { roleId: input.employeeInfo.roleId } : {}),
...(input.employeeInfo?.roleId !== undefined
? { roleId: input.employeeInfo.roleId }
: {}),
},
});
@@ -80,13 +89,15 @@ export const resourceSkillImportProcedures = {
batchImportSkillMatrices: adminProcedure
.input(
z.object({
entries: z.array(
z.object({
eid: z.string(),
skills: z.array(SkillEntrySchema),
employeeInfo: employeeInfoSchema,
}),
),
entries: z
.array(
z.object({
eid: z.string().max(64),
skills: z.array(SkillEntrySchema).max(2000),
employeeInfo: employeeInfoSchema,
}),
)
.max(5000),
}),
)
.mutation(async ({ ctx, input }) => {
@@ -110,7 +121,9 @@ export const resourceSkillImportProcedures = {
...(entry.employeeInfo?.portfolioUrl !== undefined
? { portfolioUrl: entry.employeeInfo.portfolioUrl || null }
: {}),
...(entry.employeeInfo?.roleId !== undefined ? { roleId: entry.employeeInfo.roleId } : {}),
...(entry.employeeInfo?.roleId !== undefined
? { roleId: entry.employeeInfo.roleId }
: {}),
},
}),
);