security: bound Zod inputs, add SSE per-user cap and tRPC body limit (#51)
CI / Architecture Guardrails (pull_request) Successful in 2m6s
CI / Lint (pull_request) Successful in 7m29s
CI / Typecheck (pull_request) Successful in 8m3s
CI / Unit Tests (pull_request) Successful in 8m11s
CI / Build (pull_request) Successful in 5m24s
CI / E2E Tests (pull_request) Successful in 5m25s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 6m30s
CI / Release Images (pull_request) Has been skipped
CI / Assistant Split Regression (pull_request) Successful in 3m47s
CI / Architecture Guardrails (pull_request) Successful in 2m6s
CI / Lint (pull_request) Successful in 7m29s
CI / Typecheck (pull_request) Successful in 8m3s
CI / Unit Tests (pull_request) Successful in 8m11s
CI / Build (pull_request) Successful in 5m24s
CI / E2E Tests (pull_request) Successful in 5m25s
CI / Fresh-Linux Docker Deploy (pull_request) Successful in 6m30s
CI / Release Images (pull_request) Has been skipped
CI / Assistant Split Regression (pull_request) Successful in 3m47s
Mechanical .max() bounds across 9 router schemas per the convention in #51: IDs at 64, names at 200, search/filter strings at 500, arrays at 100-5000 depending on domain. Webhook secret bounded at min(16)/max(256). Reports route now validates startDate/endDate via zod with year bounds and rejects end<start. SSE timeline route enforces a per-user connection cap of 8 (returns 429 with Retry-After). tRPC route rejects bodies over 2 MiB via Content-Length check before auth/DB work. Covers 12 call-sites listed in #51. ESLint rule and zod conventions doc remain as follow-up.
This commit is contained in:
@@ -12,9 +12,21 @@ type ImportExportMutationContext = ImportExportReadContext & {
|
||||
|
||||
type ImportRow = Record<string, string>;
|
||||
|
||||
const CSV_CELL_MAX = 4000;
|
||||
const CSV_COLUMNS_MAX = 100;
|
||||
const CSV_ROWS_MAX = 10_000;
|
||||
|
||||
export const importCsvInputSchema = z.object({
|
||||
entityType: z.enum(["resources", "projects", "allocations"]),
|
||||
rows: z.array(z.record(z.string(), z.string())),
|
||||
rows: z
|
||||
.array(
|
||||
z
|
||||
.record(z.string().max(200), z.string().max(CSV_CELL_MAX))
|
||||
.refine((row) => Object.keys(row).length <= CSV_COLUMNS_MAX, {
|
||||
message: `CSV row exceeds ${CSV_COLUMNS_MAX} columns`,
|
||||
}),
|
||||
)
|
||||
.max(CSV_ROWS_MAX),
|
||||
dryRun: z.boolean().default(true),
|
||||
});
|
||||
|
||||
@@ -32,7 +44,10 @@ function resolveVisibleBlueprintFields(fieldDefs: unknown): BlueprintFieldDefini
|
||||
}
|
||||
|
||||
function buildCsv(headers: unknown[], rows: unknown[][]) {
|
||||
return [headers.map(escapeCsvValue).join(","), ...rows.map((row) => row.map(escapeCsvValue).join(","))].join("\n");
|
||||
return [
|
||||
headers.map(escapeCsvValue).join(","),
|
||||
...rows.map((row) => row.map(escapeCsvValue).join(",")),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export async function exportResourcesCsv(ctx: ImportExportReadContext) {
|
||||
@@ -168,7 +183,10 @@ export async function importCsv(ctx: ImportExportMutationContext, input: ImportC
|
||||
|
||||
try {
|
||||
if (input.entityType === "resources") {
|
||||
const outcome = await importResourceRow({ ...ctx, db: tx as unknown as typeof ctx.db }, row);
|
||||
const outcome = await importResourceRow(
|
||||
{ ...ctx, db: tx as unknown as typeof ctx.db },
|
||||
row,
|
||||
);
|
||||
if (outcome.updated) {
|
||||
results.updated += 1;
|
||||
} else if (outcome.error) {
|
||||
|
||||
Reference in New Issue
Block a user