blaizejs 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-QQCQRHXJ.js +38 -0
- package/dist/chunk-QQCQRHXJ.js.map +1 -0
- package/dist/chunk-SRD3AB6T.js +38 -0
- package/dist/chunk-SRD3AB6T.js.map +1 -0
- package/dist/chunk-TCPQMZ23.js +148 -0
- package/dist/chunk-TCPQMZ23.js.map +1 -0
- package/dist/index.cjs +1068 -149
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1147 -145
- package/dist/index.d.ts +1147 -145
- package/dist/index.js +793 -152
- package/dist/index.js.map +1 -1
- package/dist/internal-server-error-CVRDTBLL.js +16 -0
- package/dist/internal-server-error-CVRDTBLL.js.map +1 -0
- package/dist/payload-too-large-error-PAYLDBZT.js +29 -0
- package/dist/payload-too-large-error-PAYLDBZT.js.map +1 -0
- package/dist/unsupported-media-type-error-MQZD7YQJ.js +29 -0
- package/dist/unsupported-media-type-error-MQZD7YQJ.js.map +1 -0
- package/dist/validation-error-CM6IKIJU.js +16 -0
- package/dist/validation-error-CM6IKIJU.js.map +1 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* blaizejs v0.
|
|
2
|
+
* blaizejs v0.3.0
|
|
3
3
|
* A blazing-fast, TypeScript-first Node.js framework with HTTP/2 support, file-based routing, powerful middleware system, and end-to-end type safety for building modern APIs.
|
|
4
4
|
*
|
|
5
5
|
* Copyright (c) 2025 BlaizeJS Contributors
|
|
6
6
|
* @license MIT
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
9
|
+
import {
|
|
10
|
+
InternalServerError
|
|
11
|
+
} from "./chunk-QQCQRHXJ.js";
|
|
12
|
+
import {
|
|
13
|
+
ValidationError
|
|
14
|
+
} from "./chunk-SRD3AB6T.js";
|
|
15
|
+
import {
|
|
16
|
+
BlaizeError,
|
|
17
|
+
ErrorSeverity,
|
|
18
|
+
ErrorType,
|
|
19
|
+
__require,
|
|
20
|
+
generateCorrelationId,
|
|
21
|
+
getCurrentCorrelationId,
|
|
22
|
+
isBodyParseError
|
|
23
|
+
} from "./chunk-TCPQMZ23.js";
|
|
15
24
|
|
|
16
25
|
// src/middleware/execute.ts
|
|
17
26
|
function execute(middleware, ctx, next) {
|
|
@@ -391,8 +400,474 @@ function runWithContext(context, callback) {
|
|
|
391
400
|
return contextStorage.run(context, callback);
|
|
392
401
|
}
|
|
393
402
|
|
|
403
|
+
// src/upload/multipart-parser.ts
|
|
404
|
+
import { randomUUID } from "node:crypto";
|
|
405
|
+
import { createWriteStream } from "node:fs";
|
|
406
|
+
import { tmpdir } from "node:os";
|
|
407
|
+
import { join as join2 } from "node:path";
|
|
408
|
+
import { Readable } from "node:stream";
|
|
409
|
+
|
|
410
|
+
// src/upload/utils.ts
|
|
411
|
+
var BOUNDARY_REGEX = /boundary=([^;]+)/i;
|
|
412
|
+
var CONTENT_DISPOSITION_REGEX = /Content-Disposition:\s*form-data;\s*name="([^"]+)"(?:;[\s\r\n]*filename="([^"]*)")?/i;
|
|
413
|
+
var CONTENT_TYPE_REGEX = /Content-Type:\s*([^\r\n]+)/i;
|
|
414
|
+
var MULTIPART_REGEX = /multipart\/form-data/i;
|
|
415
|
+
function extractBoundary(contentType) {
|
|
416
|
+
const match = contentType.match(BOUNDARY_REGEX);
|
|
417
|
+
if (!match || !match[1]) return null;
|
|
418
|
+
let boundary = match[1].trim();
|
|
419
|
+
if (boundary.startsWith('"') && boundary.endsWith('"')) {
|
|
420
|
+
boundary = boundary.slice(1, -1);
|
|
421
|
+
}
|
|
422
|
+
return boundary || null;
|
|
423
|
+
}
|
|
424
|
+
function parseContentDisposition(headers) {
|
|
425
|
+
const match = headers.match(CONTENT_DISPOSITION_REGEX);
|
|
426
|
+
if (!match || !match[1]) return null;
|
|
427
|
+
return {
|
|
428
|
+
name: match[1],
|
|
429
|
+
filename: match[2] !== void 0 ? match[2] : void 0
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function parseContentType(headers) {
|
|
433
|
+
const match = headers.match(CONTENT_TYPE_REGEX);
|
|
434
|
+
return match && match[1]?.trim() ? match[1].trim() : "application/octet-stream";
|
|
435
|
+
}
|
|
436
|
+
function isMultipartContent(contentType) {
|
|
437
|
+
return MULTIPART_REGEX.test(contentType);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/upload/multipart-parser.ts
|
|
441
|
+
var DEFAULT_OPTIONS = {
|
|
442
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
443
|
+
// 10MB
|
|
444
|
+
maxFiles: 10,
|
|
445
|
+
maxFieldSize: 1 * 1024 * 1024,
|
|
446
|
+
// 1MB
|
|
447
|
+
allowedMimeTypes: [],
|
|
448
|
+
allowedExtensions: [],
|
|
449
|
+
strategy: "stream",
|
|
450
|
+
tempDir: tmpdir(),
|
|
451
|
+
computeHash: false
|
|
452
|
+
};
|
|
453
|
+
function createParserState(boundary, options = {}) {
|
|
454
|
+
return {
|
|
455
|
+
boundary: Buffer.from(`--${boundary}`),
|
|
456
|
+
options: { ...DEFAULT_OPTIONS, ...options },
|
|
457
|
+
fields: /* @__PURE__ */ new Map(),
|
|
458
|
+
files: /* @__PURE__ */ new Map(),
|
|
459
|
+
buffer: Buffer.alloc(0),
|
|
460
|
+
stage: "boundary",
|
|
461
|
+
currentHeaders: "",
|
|
462
|
+
currentField: null,
|
|
463
|
+
currentFilename: void 0,
|
|
464
|
+
currentMimetype: "application/octet-stream",
|
|
465
|
+
currentContentLength: 0,
|
|
466
|
+
fileCount: 0,
|
|
467
|
+
fieldCount: 0,
|
|
468
|
+
currentBufferChunks: [],
|
|
469
|
+
currentStream: null,
|
|
470
|
+
currentTempPath: null,
|
|
471
|
+
currentWriteStream: null,
|
|
472
|
+
streamController: null,
|
|
473
|
+
cleanupTasks: [],
|
|
474
|
+
// Track validation state
|
|
475
|
+
hasFoundValidBoundary: false,
|
|
476
|
+
hasProcessedAnyPart: false,
|
|
477
|
+
isFinished: false
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function processChunk(state, chunk) {
|
|
481
|
+
const newBuffer = Buffer.concat([state.buffer, chunk]);
|
|
482
|
+
let currentState = { ...state, buffer: newBuffer };
|
|
483
|
+
while (currentState.buffer.length > 0 && !currentState.isFinished) {
|
|
484
|
+
const nextState = await processCurrentStage(currentState);
|
|
485
|
+
if (nextState === currentState) break;
|
|
486
|
+
currentState = nextState;
|
|
487
|
+
}
|
|
488
|
+
return currentState;
|
|
489
|
+
}
|
|
490
|
+
async function processCurrentStage(state) {
|
|
491
|
+
switch (state.stage) {
|
|
492
|
+
case "boundary":
|
|
493
|
+
return processBoundary(state);
|
|
494
|
+
case "headers":
|
|
495
|
+
return processHeaders(state);
|
|
496
|
+
case "content":
|
|
497
|
+
return processContent(state);
|
|
498
|
+
default: {
|
|
499
|
+
const { InternalServerError: InternalServerError2 } = await import("./internal-server-error-CVRDTBLL.js");
|
|
500
|
+
throw new InternalServerError2(`Invalid parser stage`, {
|
|
501
|
+
operation: state.stage
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function processBoundary(state) {
|
|
507
|
+
const boundaryIndex = state.buffer.indexOf(state.boundary);
|
|
508
|
+
if (boundaryIndex === -1) return state;
|
|
509
|
+
const hasFoundValidBoundary = true;
|
|
510
|
+
let buffer = state.buffer.subarray(boundaryIndex + state.boundary.length);
|
|
511
|
+
if (buffer.length >= 2 && buffer.subarray(0, 2).equals(Buffer.from("--"))) {
|
|
512
|
+
return {
|
|
513
|
+
...state,
|
|
514
|
+
buffer,
|
|
515
|
+
hasFoundValidBoundary,
|
|
516
|
+
isFinished: true,
|
|
517
|
+
stage: "boundary"
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (buffer.length >= 2 && buffer.subarray(0, 2).equals(Buffer.from("\r\n"))) {
|
|
521
|
+
buffer = buffer.subarray(2);
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
...state,
|
|
525
|
+
buffer,
|
|
526
|
+
hasFoundValidBoundary,
|
|
527
|
+
stage: "headers",
|
|
528
|
+
currentHeaders: ""
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
async function processHeaders(state) {
|
|
532
|
+
const headerEnd = state.buffer.indexOf("\r\n\r\n");
|
|
533
|
+
if (headerEnd === -1) return state;
|
|
534
|
+
const headers = state.buffer.subarray(0, headerEnd).toString("utf8");
|
|
535
|
+
const buffer = state.buffer.subarray(headerEnd + 4);
|
|
536
|
+
const disposition = parseContentDisposition(headers);
|
|
537
|
+
if (!disposition) {
|
|
538
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
539
|
+
throw new ValidationError2("Missing or invalid Content-Disposition header");
|
|
540
|
+
}
|
|
541
|
+
const mimetype = parseContentType(headers);
|
|
542
|
+
const isFile = disposition.filename !== void 0;
|
|
543
|
+
if (isFile && state.fileCount >= state.options.maxFiles) {
|
|
544
|
+
const { PayloadTooLargeError } = await import("./payload-too-large-error-PAYLDBZT.js");
|
|
545
|
+
throw new PayloadTooLargeError("Too many files in upload", {
|
|
546
|
+
fileCount: state.fileCount + 1,
|
|
547
|
+
maxFiles: state.options.maxFiles,
|
|
548
|
+
filename: disposition.filename
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (isFile && state.options.allowedMimeTypes.length > 0 && !state.options.allowedMimeTypes.includes(mimetype)) {
|
|
552
|
+
const { UnsupportedMediaTypeError } = await import("./unsupported-media-type-error-MQZD7YQJ.js");
|
|
553
|
+
throw new UnsupportedMediaTypeError("File type not allowed", {
|
|
554
|
+
receivedMimeType: mimetype,
|
|
555
|
+
allowedMimeTypes: state.options.allowedMimeTypes,
|
|
556
|
+
filename: disposition.filename
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
...state,
|
|
561
|
+
buffer,
|
|
562
|
+
stage: "content",
|
|
563
|
+
currentHeaders: headers,
|
|
564
|
+
currentField: disposition.name,
|
|
565
|
+
currentFilename: disposition.filename,
|
|
566
|
+
currentMimetype: mimetype,
|
|
567
|
+
currentContentLength: 0,
|
|
568
|
+
fileCount: isFile ? state.fileCount + 1 : state.fileCount,
|
|
569
|
+
fieldCount: isFile ? state.fieldCount : state.fieldCount + 1,
|
|
570
|
+
currentBufferChunks: []
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async function processContent(state) {
|
|
574
|
+
const nextBoundaryIndex = state.buffer.indexOf(state.boundary);
|
|
575
|
+
let contentChunk;
|
|
576
|
+
let isComplete = false;
|
|
577
|
+
let buffer = state.buffer;
|
|
578
|
+
if (nextBoundaryIndex === -1) {
|
|
579
|
+
const safeLength = Math.max(0, state.buffer.length - state.boundary.length);
|
|
580
|
+
if (safeLength === 0) return state;
|
|
581
|
+
contentChunk = state.buffer.subarray(0, safeLength);
|
|
582
|
+
buffer = state.buffer.subarray(safeLength);
|
|
583
|
+
} else {
|
|
584
|
+
const contentEnd = Math.max(0, nextBoundaryIndex - 2);
|
|
585
|
+
contentChunk = state.buffer.subarray(0, contentEnd);
|
|
586
|
+
buffer = state.buffer.subarray(nextBoundaryIndex);
|
|
587
|
+
isComplete = true;
|
|
588
|
+
}
|
|
589
|
+
let updatedState = { ...state, buffer };
|
|
590
|
+
if (contentChunk.length > 0) {
|
|
591
|
+
updatedState = await processContentChunk(updatedState, contentChunk);
|
|
592
|
+
}
|
|
593
|
+
if (isComplete) {
|
|
594
|
+
updatedState = await finalizeCurrentPart(updatedState);
|
|
595
|
+
updatedState = {
|
|
596
|
+
...updatedState,
|
|
597
|
+
stage: "boundary",
|
|
598
|
+
hasProcessedAnyPart: true
|
|
599
|
+
// Mark that we've processed at least one part
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return updatedState;
|
|
603
|
+
}
|
|
604
|
+
async function processContentChunk(state, chunk) {
|
|
605
|
+
const newContentLength = state.currentContentLength + chunk.length;
|
|
606
|
+
const maxSize = state.currentFilename !== void 0 ? state.options.maxFileSize : state.options.maxFieldSize;
|
|
607
|
+
if (newContentLength > maxSize) {
|
|
608
|
+
const isFile = state.currentFilename !== void 0;
|
|
609
|
+
const { PayloadTooLargeError } = await import("./payload-too-large-error-PAYLDBZT.js");
|
|
610
|
+
const payloadErrorDetals = state.currentField ? {
|
|
611
|
+
contentType: isFile ? "file" : "field",
|
|
612
|
+
currentSize: newContentLength,
|
|
613
|
+
maxSize,
|
|
614
|
+
field: state.currentField,
|
|
615
|
+
filename: state.currentFilename
|
|
616
|
+
} : {
|
|
617
|
+
contentType: isFile ? "file" : "field",
|
|
618
|
+
currentSize: newContentLength,
|
|
619
|
+
maxSize,
|
|
620
|
+
filename: state.currentFilename
|
|
621
|
+
};
|
|
622
|
+
throw new PayloadTooLargeError(
|
|
623
|
+
`${isFile ? "File" : "Field"} size exceeds limit`,
|
|
624
|
+
payloadErrorDetals
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (state.currentFilename !== void 0) {
|
|
628
|
+
return processFileChunk(state, chunk, newContentLength);
|
|
629
|
+
} else {
|
|
630
|
+
return {
|
|
631
|
+
...state,
|
|
632
|
+
currentContentLength: newContentLength,
|
|
633
|
+
currentBufferChunks: [...state.currentBufferChunks, chunk]
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async function processFileChunk(state, chunk, newContentLength) {
|
|
638
|
+
switch (state.options.strategy) {
|
|
639
|
+
case "memory":
|
|
640
|
+
return {
|
|
641
|
+
...state,
|
|
642
|
+
currentContentLength: newContentLength,
|
|
643
|
+
currentBufferChunks: [...state.currentBufferChunks, chunk]
|
|
644
|
+
};
|
|
645
|
+
case "stream":
|
|
646
|
+
if (state.streamController) {
|
|
647
|
+
state.streamController.enqueue(chunk);
|
|
648
|
+
}
|
|
649
|
+
return { ...state, currentContentLength: newContentLength };
|
|
650
|
+
case "temp":
|
|
651
|
+
if (state.currentWriteStream) {
|
|
652
|
+
await writeToStream(state.currentWriteStream, chunk);
|
|
653
|
+
}
|
|
654
|
+
return { ...state, currentContentLength: newContentLength };
|
|
655
|
+
default: {
|
|
656
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
657
|
+
throw new ValidationError2(`Invalid parsing strategy`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
async function initializeFileProcessing(state) {
|
|
662
|
+
if (state.currentFilename === void 0) return state;
|
|
663
|
+
switch (state.options.strategy) {
|
|
664
|
+
case "memory":
|
|
665
|
+
return { ...state, currentBufferChunks: [] };
|
|
666
|
+
case "stream": {
|
|
667
|
+
let streamController = null;
|
|
668
|
+
const stream = new ReadableStream({
|
|
669
|
+
start: (controller) => {
|
|
670
|
+
streamController = controller;
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
return {
|
|
674
|
+
...state,
|
|
675
|
+
currentStream: stream,
|
|
676
|
+
// Type cast for Node.js compatibility
|
|
677
|
+
streamController
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
case "temp": {
|
|
681
|
+
const tempPath = join2(state.options.tempDir, `upload-${randomUUID()}`);
|
|
682
|
+
const writeStream = createWriteStream(tempPath);
|
|
683
|
+
const cleanupTask = async () => {
|
|
684
|
+
try {
|
|
685
|
+
const { unlink } = await import("node:fs/promises");
|
|
686
|
+
await unlink(tempPath);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
console.warn(`Failed to cleanup temp file: ${tempPath}`, error);
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
return {
|
|
692
|
+
...state,
|
|
693
|
+
currentTempPath: tempPath,
|
|
694
|
+
currentWriteStream: writeStream,
|
|
695
|
+
cleanupTasks: [...state.cleanupTasks, cleanupTask]
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
default: {
|
|
699
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
700
|
+
throw new ValidationError2(`Invalid file processing strategy`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
async function finalizeCurrentPart(state) {
|
|
705
|
+
if (!state.currentField) return resetCurrentPart(state);
|
|
706
|
+
if (state.currentFilename !== void 0) {
|
|
707
|
+
return finalizeFile(state);
|
|
708
|
+
} else {
|
|
709
|
+
return finalizeField(state);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
async function finalizeFile(state) {
|
|
713
|
+
if (!state.currentField || state.currentFilename === void 0) {
|
|
714
|
+
return resetCurrentPart(state);
|
|
715
|
+
}
|
|
716
|
+
let stream;
|
|
717
|
+
let buffer;
|
|
718
|
+
let tempPath;
|
|
719
|
+
switch (state.options.strategy) {
|
|
720
|
+
case "memory":
|
|
721
|
+
buffer = Buffer.concat(state.currentBufferChunks);
|
|
722
|
+
stream = Readable.from(buffer);
|
|
723
|
+
break;
|
|
724
|
+
case "stream":
|
|
725
|
+
if (state.streamController) {
|
|
726
|
+
state.streamController.close();
|
|
727
|
+
}
|
|
728
|
+
stream = state.currentStream;
|
|
729
|
+
break;
|
|
730
|
+
case "temp":
|
|
731
|
+
if (state.currentWriteStream) {
|
|
732
|
+
await closeStream(state.currentWriteStream);
|
|
733
|
+
}
|
|
734
|
+
tempPath = state.currentTempPath;
|
|
735
|
+
stream = Readable.from(Buffer.alloc(0));
|
|
736
|
+
break;
|
|
737
|
+
default: {
|
|
738
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
739
|
+
throw new ValidationError2(`Invalid file finalization strategy`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const file = {
|
|
743
|
+
filename: state.currentFilename,
|
|
744
|
+
fieldname: state.currentField,
|
|
745
|
+
mimetype: state.currentMimetype,
|
|
746
|
+
size: state.currentContentLength,
|
|
747
|
+
stream,
|
|
748
|
+
buffer,
|
|
749
|
+
tempPath
|
|
750
|
+
};
|
|
751
|
+
const updatedFiles = addToCollection(state.files, state.currentField, file);
|
|
752
|
+
return {
|
|
753
|
+
...resetCurrentPart(state),
|
|
754
|
+
files: updatedFiles
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function finalizeField(state) {
|
|
758
|
+
if (!state.currentField) return resetCurrentPart(state);
|
|
759
|
+
const value = Buffer.concat(state.currentBufferChunks).toString("utf8");
|
|
760
|
+
const updatedFields = addToCollection(state.fields, state.currentField, value);
|
|
761
|
+
return {
|
|
762
|
+
...resetCurrentPart(state),
|
|
763
|
+
fields: updatedFields
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function resetCurrentPart(state) {
|
|
767
|
+
return {
|
|
768
|
+
...state,
|
|
769
|
+
currentField: null,
|
|
770
|
+
currentFilename: void 0,
|
|
771
|
+
currentContentLength: 0,
|
|
772
|
+
currentBufferChunks: [],
|
|
773
|
+
currentStream: null,
|
|
774
|
+
streamController: null,
|
|
775
|
+
currentTempPath: null,
|
|
776
|
+
currentWriteStream: null
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function addToCollection(collection, key, value) {
|
|
780
|
+
const newCollection = new Map(collection);
|
|
781
|
+
const existing = newCollection.get(key) || [];
|
|
782
|
+
newCollection.set(key, [...existing, value]);
|
|
783
|
+
return newCollection;
|
|
784
|
+
}
|
|
785
|
+
async function finalize(state) {
|
|
786
|
+
if (!state.hasFoundValidBoundary) {
|
|
787
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
788
|
+
throw new ValidationError2("No valid multipart boundary found");
|
|
789
|
+
}
|
|
790
|
+
if (state.hasFoundValidBoundary && !state.hasProcessedAnyPart) {
|
|
791
|
+
const { ValidationError: ValidationError2 } = await import("./validation-error-CM6IKIJU.js");
|
|
792
|
+
throw new ValidationError2("Empty multipart request");
|
|
793
|
+
}
|
|
794
|
+
const fields = {};
|
|
795
|
+
for (const [key, values] of state.fields.entries()) {
|
|
796
|
+
fields[key] = values.length === 1 ? values[0] : values;
|
|
797
|
+
}
|
|
798
|
+
const files = {};
|
|
799
|
+
for (const [key, fileList] of state.files.entries()) {
|
|
800
|
+
files[key] = fileList.length === 1 ? fileList[0] : fileList;
|
|
801
|
+
}
|
|
802
|
+
return { fields, files };
|
|
803
|
+
}
|
|
804
|
+
async function cleanup(state) {
|
|
805
|
+
await Promise.allSettled(state.cleanupTasks.map((task) => task()));
|
|
806
|
+
if (state.streamController) {
|
|
807
|
+
state.streamController.close();
|
|
808
|
+
}
|
|
809
|
+
if (state.currentWriteStream) {
|
|
810
|
+
await closeStream(state.currentWriteStream);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function writeToStream(stream, chunk) {
|
|
814
|
+
return new Promise((resolve3, reject) => {
|
|
815
|
+
stream.write(chunk, (error) => {
|
|
816
|
+
if (error) reject(error);
|
|
817
|
+
else resolve3();
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
async function closeStream(stream) {
|
|
822
|
+
return new Promise((resolve3) => {
|
|
823
|
+
stream.end(() => resolve3());
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
async function parseMultipartRequest(request, options = {}) {
|
|
827
|
+
const contentType = request.headers["content-type"] || "";
|
|
828
|
+
const boundary = extractBoundary(contentType);
|
|
829
|
+
if (!boundary) {
|
|
830
|
+
const { UnsupportedMediaTypeError } = await import("./unsupported-media-type-error-MQZD7YQJ.js");
|
|
831
|
+
throw new UnsupportedMediaTypeError("Missing boundary in multipart content-type", {
|
|
832
|
+
receivedContentType: contentType,
|
|
833
|
+
expectedFormat: "multipart/form-data; boundary=..."
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
let state = createParserState(boundary, options);
|
|
837
|
+
if (state.currentFilename !== void 0) {
|
|
838
|
+
state = await initializeFileProcessing(state);
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
for await (const chunk of request) {
|
|
842
|
+
state = await processChunk(state, chunk);
|
|
843
|
+
}
|
|
844
|
+
return finalize(state);
|
|
845
|
+
} finally {
|
|
846
|
+
await cleanup(state);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
394
850
|
// src/context/create.ts
|
|
395
851
|
var CONTENT_TYPE_HEADER = "Content-Type";
|
|
852
|
+
var DEFAULT_BODY_LIMITS = {
|
|
853
|
+
json: 512 * 1024,
|
|
854
|
+
// 512KB - Most APIs should be much smaller
|
|
855
|
+
form: 1024 * 1024,
|
|
856
|
+
// 1MB - Reasonable for form submissions
|
|
857
|
+
text: 5 * 1024 * 1024,
|
|
858
|
+
// 5MB - Documents, logs, code files
|
|
859
|
+
multipart: {
|
|
860
|
+
maxFileSize: 50 * 1024 * 1024,
|
|
861
|
+
// 50MB per file
|
|
862
|
+
maxTotalSize: 100 * 1024 * 1024,
|
|
863
|
+
// 100MB total request
|
|
864
|
+
maxFiles: 10,
|
|
865
|
+
maxFieldSize: 1024 * 1024
|
|
866
|
+
// 1MB for form fields
|
|
867
|
+
},
|
|
868
|
+
raw: 10 * 1024 * 1024
|
|
869
|
+
// 10MB for unknown content types
|
|
870
|
+
};
|
|
396
871
|
function parseRequestUrl(req) {
|
|
397
872
|
const originalUrl = req.url || "/";
|
|
398
873
|
const host = req.headers.host || "localhost";
|
|
@@ -457,7 +932,7 @@ async function createContext(req, res, options = {}) {
|
|
|
457
932
|
};
|
|
458
933
|
ctx.response = createResponseObject(res, responseState, ctx);
|
|
459
934
|
if (options.parseBody) {
|
|
460
|
-
await parseBodyIfNeeded(req, ctx);
|
|
935
|
+
await parseBodyIfNeeded(req, ctx, options);
|
|
461
936
|
}
|
|
462
937
|
return ctx;
|
|
463
938
|
}
|
|
@@ -633,34 +1108,60 @@ function createStreamResponder(res, responseState) {
|
|
|
633
1108
|
});
|
|
634
1109
|
};
|
|
635
1110
|
}
|
|
636
|
-
async function parseBodyIfNeeded(req, ctx) {
|
|
1111
|
+
async function parseBodyIfNeeded(req, ctx, options = {}) {
|
|
637
1112
|
if (shouldSkipParsing(req.method)) {
|
|
638
1113
|
return;
|
|
639
1114
|
}
|
|
640
1115
|
const contentType = req.headers["content-type"] || "";
|
|
641
1116
|
const contentLength = parseInt(req.headers["content-length"] || "0", 10);
|
|
642
|
-
if (contentLength === 0
|
|
1117
|
+
if (contentLength === 0) {
|
|
643
1118
|
return;
|
|
644
1119
|
}
|
|
1120
|
+
const limits = {
|
|
1121
|
+
json: options.bodyLimits?.json ?? DEFAULT_BODY_LIMITS.json,
|
|
1122
|
+
form: options.bodyLimits?.form ?? DEFAULT_BODY_LIMITS.form,
|
|
1123
|
+
text: options.bodyLimits?.text ?? DEFAULT_BODY_LIMITS.text,
|
|
1124
|
+
raw: options.bodyLimits?.raw ?? DEFAULT_BODY_LIMITS.raw,
|
|
1125
|
+
multipart: {
|
|
1126
|
+
maxFileSize: options.bodyLimits?.multipart?.maxFileSize ?? DEFAULT_BODY_LIMITS.multipart.maxFileSize,
|
|
1127
|
+
maxFiles: options.bodyLimits?.multipart?.maxFiles ?? DEFAULT_BODY_LIMITS.multipart.maxFiles,
|
|
1128
|
+
maxFieldSize: options.bodyLimits?.multipart?.maxFieldSize ?? DEFAULT_BODY_LIMITS.multipart.maxFieldSize,
|
|
1129
|
+
maxTotalSize: options.bodyLimits?.multipart?.maxTotalSize ?? DEFAULT_BODY_LIMITS.multipart.maxTotalSize
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
645
1132
|
try {
|
|
646
|
-
|
|
1133
|
+
if (contentType.includes("application/json")) {
|
|
1134
|
+
if (contentLength > limits.json) {
|
|
1135
|
+
throw new Error(`JSON body too large: ${contentLength} > ${limits.json} bytes`);
|
|
1136
|
+
}
|
|
1137
|
+
await parseJsonBody(req, ctx);
|
|
1138
|
+
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
1139
|
+
if (contentLength > limits.form) {
|
|
1140
|
+
throw new Error(`Form body too large: ${contentLength} > ${limits.form} bytes`);
|
|
1141
|
+
}
|
|
1142
|
+
await parseFormUrlEncodedBody(req, ctx);
|
|
1143
|
+
} else if (contentType.includes("text/")) {
|
|
1144
|
+
if (contentLength > limits.text) {
|
|
1145
|
+
throw new Error(`Text body too large: ${contentLength} > ${limits.text} bytes`);
|
|
1146
|
+
}
|
|
1147
|
+
await parseTextBody(req, ctx);
|
|
1148
|
+
} else if (isMultipartContent(contentType)) {
|
|
1149
|
+
await parseMultipartBody(req, ctx, limits.multipart);
|
|
1150
|
+
} else {
|
|
1151
|
+
if (contentLength > limits.raw) {
|
|
1152
|
+
throw new Error(`Request body too large: ${contentLength} > ${limits.raw} bytes`);
|
|
1153
|
+
}
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
647
1156
|
} catch (error) {
|
|
648
|
-
|
|
1157
|
+
const errorType = contentType.includes("multipart") ? "multipart_parse_error" : "body_read_error";
|
|
1158
|
+
setBodyError(ctx, errorType, "Error reading request body", error);
|
|
649
1159
|
}
|
|
650
1160
|
}
|
|
651
1161
|
function shouldSkipParsing(method) {
|
|
652
1162
|
const skipMethods = ["GET", "HEAD", "OPTIONS"];
|
|
653
1163
|
return skipMethods.includes(method || "GET");
|
|
654
1164
|
}
|
|
655
|
-
async function parseBodyByContentType(req, ctx, contentType) {
|
|
656
|
-
if (contentType.includes("application/json")) {
|
|
657
|
-
await parseJsonBody(req, ctx);
|
|
658
|
-
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
659
|
-
await parseFormUrlEncodedBody(req, ctx);
|
|
660
|
-
} else if (contentType.includes("text/")) {
|
|
661
|
-
await parseTextBody(req, ctx);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
1165
|
async function parseJsonBody(req, ctx) {
|
|
665
1166
|
const body = await readRequestBody(req);
|
|
666
1167
|
if (!body) {
|
|
@@ -712,8 +1213,27 @@ async function parseTextBody(req, ctx) {
|
|
|
712
1213
|
ctx.request.body = body;
|
|
713
1214
|
}
|
|
714
1215
|
}
|
|
1216
|
+
async function parseMultipartBody(req, ctx, multipartLimits) {
|
|
1217
|
+
try {
|
|
1218
|
+
const limits = multipartLimits || DEFAULT_BODY_LIMITS.multipart;
|
|
1219
|
+
const multipartData = await parseMultipartRequest(req, {
|
|
1220
|
+
strategy: "stream",
|
|
1221
|
+
maxFileSize: limits.maxFileSize,
|
|
1222
|
+
maxFiles: limits.maxFiles,
|
|
1223
|
+
maxFieldSize: limits.maxFieldSize
|
|
1224
|
+
// Could add total size validation here
|
|
1225
|
+
});
|
|
1226
|
+
ctx.request.multipart = multipartData;
|
|
1227
|
+
ctx.request.files = multipartData.files;
|
|
1228
|
+
ctx.request.body = multipartData.fields;
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
ctx.request.body = null;
|
|
1231
|
+
setBodyError(ctx, "multipart_parse_error", "Failed to parse multipart data", error);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
715
1234
|
function setBodyError(ctx, type, message, error) {
|
|
716
|
-
|
|
1235
|
+
const bodyError = { type, message, error };
|
|
1236
|
+
ctx.state._bodyError = bodyError;
|
|
717
1237
|
}
|
|
718
1238
|
async function readRequestBody(req) {
|
|
719
1239
|
return new Promise((resolve3, reject) => {
|
|
@@ -730,6 +1250,102 @@ async function readRequestBody(req) {
|
|
|
730
1250
|
});
|
|
731
1251
|
}
|
|
732
1252
|
|
|
1253
|
+
// src/errors/not-found-error.ts
|
|
1254
|
+
var NotFoundError = class extends BlaizeError {
|
|
1255
|
+
/**
|
|
1256
|
+
* Creates a new NotFoundError instance
|
|
1257
|
+
*
|
|
1258
|
+
* @param title - Human-readable error message
|
|
1259
|
+
* @param details - Optional context about the missing resource
|
|
1260
|
+
* @param correlationId - Optional correlation ID (uses current context if not provided)
|
|
1261
|
+
*/
|
|
1262
|
+
constructor(title, details = void 0, correlationId = void 0) {
|
|
1263
|
+
super(
|
|
1264
|
+
"NOT_FOUND" /* NOT_FOUND */,
|
|
1265
|
+
title,
|
|
1266
|
+
404,
|
|
1267
|
+
// HTTP 404 Not Found
|
|
1268
|
+
correlationId ?? getCurrentCorrelationId(),
|
|
1269
|
+
details
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
// src/errors/boundary.ts
|
|
1275
|
+
function isHandledError(error) {
|
|
1276
|
+
return error instanceof BlaizeError;
|
|
1277
|
+
}
|
|
1278
|
+
function formatErrorResponse(error) {
|
|
1279
|
+
if (isHandledError(error)) {
|
|
1280
|
+
return {
|
|
1281
|
+
type: error.type,
|
|
1282
|
+
title: error.title,
|
|
1283
|
+
status: error.status,
|
|
1284
|
+
correlationId: error.correlationId,
|
|
1285
|
+
timestamp: error.timestamp.toISOString(),
|
|
1286
|
+
details: error.details
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
const correlationId = generateCorrelationId();
|
|
1290
|
+
let originalMessage;
|
|
1291
|
+
if (error instanceof Error) {
|
|
1292
|
+
originalMessage = error.message;
|
|
1293
|
+
} else if (error === null || error === void 0) {
|
|
1294
|
+
originalMessage = "Unknown error occurred";
|
|
1295
|
+
} else {
|
|
1296
|
+
originalMessage = String(error);
|
|
1297
|
+
}
|
|
1298
|
+
const wrappedError = new InternalServerError(
|
|
1299
|
+
"Internal Server Error",
|
|
1300
|
+
{ originalMessage },
|
|
1301
|
+
correlationId
|
|
1302
|
+
);
|
|
1303
|
+
return {
|
|
1304
|
+
type: wrappedError.type,
|
|
1305
|
+
title: wrappedError.title,
|
|
1306
|
+
status: wrappedError.status,
|
|
1307
|
+
correlationId: wrappedError.correlationId,
|
|
1308
|
+
timestamp: wrappedError.timestamp.toISOString(),
|
|
1309
|
+
details: wrappedError.details
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
function extractOrGenerateCorrelationId(headerGetter) {
|
|
1313
|
+
return headerGetter("x-correlation-id") ?? generateCorrelationId();
|
|
1314
|
+
}
|
|
1315
|
+
function setErrorResponseHeaders(headerSetter, correlationId) {
|
|
1316
|
+
headerSetter("x-correlation-id", correlationId);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/middleware/error-boundary.ts
|
|
1320
|
+
function createErrorBoundary(options = {}) {
|
|
1321
|
+
const { debug = false } = options;
|
|
1322
|
+
const middlewareFn = async (ctx, next) => {
|
|
1323
|
+
try {
|
|
1324
|
+
await next();
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
if (ctx.response.sent) {
|
|
1327
|
+
if (debug) {
|
|
1328
|
+
console.error("Error occurred after response was sent:", error);
|
|
1329
|
+
}
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (debug) {
|
|
1333
|
+
console.error("Error boundary caught error:", error);
|
|
1334
|
+
}
|
|
1335
|
+
const correlationId = extractOrGenerateCorrelationId(ctx.request.header);
|
|
1336
|
+
const errorResponse = formatErrorResponse(error);
|
|
1337
|
+
errorResponse.correlationId = correlationId;
|
|
1338
|
+
setErrorResponseHeaders(ctx.response.header, correlationId);
|
|
1339
|
+
ctx.response.status(errorResponse.status).json(errorResponse);
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
return {
|
|
1343
|
+
name: "ErrorBoundary",
|
|
1344
|
+
execute: middlewareFn,
|
|
1345
|
+
debug
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
|
|
733
1349
|
// src/server/request-handler.ts
|
|
734
1350
|
function createRequestHandler(serverInstance) {
|
|
735
1351
|
return async (req, res) => {
|
|
@@ -738,32 +1354,20 @@ function createRequestHandler(serverInstance) {
|
|
|
738
1354
|
parseBody: true
|
|
739
1355
|
// Enable automatic body parsing
|
|
740
1356
|
});
|
|
741
|
-
const
|
|
1357
|
+
const errorBoundary = createErrorBoundary();
|
|
1358
|
+
const allMiddleware = [errorBoundary, ...serverInstance.middleware];
|
|
1359
|
+
const handler = compose(allMiddleware);
|
|
742
1360
|
await runWithContext(context, async () => {
|
|
743
|
-
|
|
744
|
-
|
|
1361
|
+
await handler(context, async () => {
|
|
1362
|
+
if (!context.response.sent) {
|
|
1363
|
+
await serverInstance.router.handleRequest(context);
|
|
745
1364
|
if (!context.response.sent) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
error: "Not Found",
|
|
750
|
-
message: `Route not found: ${context.request.method} ${context.request.path}`
|
|
751
|
-
});
|
|
752
|
-
}
|
|
1365
|
+
throw new NotFoundError(
|
|
1366
|
+
`Route not found: ${context.request.method} ${context.request.path}`
|
|
1367
|
+
);
|
|
753
1368
|
}
|
|
754
|
-
});
|
|
755
|
-
} catch (error) {
|
|
756
|
-
console.error("Error processing request:", error);
|
|
757
|
-
if (!context.response.sent) {
|
|
758
|
-
context.response.json(
|
|
759
|
-
{
|
|
760
|
-
error: "Internal Server Error",
|
|
761
|
-
message: process.env.NODE_ENV === "development" ? error || "Unknown error" : "An error occurred processing your request"
|
|
762
|
-
},
|
|
763
|
-
500
|
|
764
|
-
);
|
|
765
1369
|
}
|
|
766
|
-
}
|
|
1370
|
+
});
|
|
767
1371
|
});
|
|
768
1372
|
} catch (error) {
|
|
769
1373
|
console.error("Error creating context:", error);
|
|
@@ -1604,81 +2208,8 @@ function watchRoutes(routesDir, options = {}) {
|
|
|
1604
2208
|
};
|
|
1605
2209
|
}
|
|
1606
2210
|
|
|
1607
|
-
// src/router/
|
|
1608
|
-
|
|
1609
|
-
if (options.log) {
|
|
1610
|
-
console.error("Route error:", error);
|
|
1611
|
-
}
|
|
1612
|
-
const status = getErrorStatus(error);
|
|
1613
|
-
const response = {
|
|
1614
|
-
error: getErrorType(error),
|
|
1615
|
-
message: getErrorMessage(error)
|
|
1616
|
-
};
|
|
1617
|
-
if (options.detailed) {
|
|
1618
|
-
if (error instanceof Error) {
|
|
1619
|
-
response.stack = error.stack;
|
|
1620
|
-
}
|
|
1621
|
-
if (error && typeof error === "object" && "details" in error && error.details) {
|
|
1622
|
-
response.details = error.details;
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
ctx.response.status(status).json(response);
|
|
1626
|
-
}
|
|
1627
|
-
function getErrorStatus(error) {
|
|
1628
|
-
if (error && typeof error === "object") {
|
|
1629
|
-
if ("status" in error && typeof error.status === "number") {
|
|
1630
|
-
return error.status;
|
|
1631
|
-
}
|
|
1632
|
-
if ("statusCode" in error && typeof error.statusCode === "number") {
|
|
1633
|
-
return error.statusCode;
|
|
1634
|
-
}
|
|
1635
|
-
if ("code" in error && typeof error.code === "string") {
|
|
1636
|
-
return getStatusFromCode(error.code);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
return 500;
|
|
1640
|
-
}
|
|
1641
|
-
function getStatusFromCode(code) {
|
|
1642
|
-
switch (code) {
|
|
1643
|
-
case "NOT_FOUND":
|
|
1644
|
-
return 404;
|
|
1645
|
-
case "UNAUTHORIZED":
|
|
1646
|
-
return 401;
|
|
1647
|
-
case "FORBIDDEN":
|
|
1648
|
-
return 403;
|
|
1649
|
-
case "BAD_REQUEST":
|
|
1650
|
-
return 400;
|
|
1651
|
-
case "CONFLICT":
|
|
1652
|
-
return 409;
|
|
1653
|
-
default:
|
|
1654
|
-
return 500;
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
function getErrorType(error) {
|
|
1658
|
-
if (error && typeof error === "object") {
|
|
1659
|
-
if ("type" in error && typeof error.type === "string") {
|
|
1660
|
-
return error.type;
|
|
1661
|
-
}
|
|
1662
|
-
if ("name" in error && typeof error.name === "string") {
|
|
1663
|
-
return error.name;
|
|
1664
|
-
}
|
|
1665
|
-
if (error instanceof Error) {
|
|
1666
|
-
return error.constructor.name;
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
return "Error";
|
|
1670
|
-
}
|
|
1671
|
-
function getErrorMessage(error) {
|
|
1672
|
-
if (error instanceof Error) {
|
|
1673
|
-
return error.message;
|
|
1674
|
-
}
|
|
1675
|
-
if (error && typeof error === "object") {
|
|
1676
|
-
if ("message" in error && typeof error.message === "string") {
|
|
1677
|
-
return error.message;
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
return String(error);
|
|
1681
|
-
}
|
|
2211
|
+
// src/router/validation/schema.ts
|
|
2212
|
+
import { z as z6 } from "zod";
|
|
1682
2213
|
|
|
1683
2214
|
// src/router/validation/body.ts
|
|
1684
2215
|
import { z as z2 } from "zod";
|
|
@@ -1719,35 +2250,45 @@ function validateResponse(response, schema) {
|
|
|
1719
2250
|
// src/router/validation/schema.ts
|
|
1720
2251
|
function createRequestValidator(schema, debug = false) {
|
|
1721
2252
|
const middlewareFn = async (ctx, next) => {
|
|
1722
|
-
const errors = {};
|
|
1723
2253
|
if (schema.params && ctx.request.params) {
|
|
1724
2254
|
try {
|
|
1725
2255
|
ctx.request.params = validateParams(ctx.request.params, schema.params);
|
|
1726
2256
|
} catch (error) {
|
|
1727
|
-
|
|
2257
|
+
const fieldErrors = extractZodFieldErrors(error);
|
|
2258
|
+
const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
|
|
2259
|
+
throw new ValidationError("Request validation failed", {
|
|
2260
|
+
fields: fieldErrors,
|
|
2261
|
+
errorCount,
|
|
2262
|
+
section: "params"
|
|
2263
|
+
});
|
|
1728
2264
|
}
|
|
1729
2265
|
}
|
|
1730
2266
|
if (schema.query && ctx.request.query) {
|
|
1731
2267
|
try {
|
|
1732
2268
|
ctx.request.query = validateQuery(ctx.request.query, schema.query);
|
|
1733
2269
|
} catch (error) {
|
|
1734
|
-
|
|
2270
|
+
const fieldErrors = extractZodFieldErrors(error);
|
|
2271
|
+
const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
|
|
2272
|
+
throw new ValidationError("Request validation failed", {
|
|
2273
|
+
fields: fieldErrors,
|
|
2274
|
+
errorCount,
|
|
2275
|
+
section: "query"
|
|
2276
|
+
});
|
|
1735
2277
|
}
|
|
1736
2278
|
}
|
|
1737
2279
|
if (schema.body) {
|
|
1738
2280
|
try {
|
|
1739
2281
|
ctx.request.body = validateBody(ctx.request.body, schema.body);
|
|
1740
2282
|
} catch (error) {
|
|
1741
|
-
|
|
2283
|
+
const fieldErrors = extractZodFieldErrors(error);
|
|
2284
|
+
const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
|
|
2285
|
+
throw new ValidationError("Request validation failed", {
|
|
2286
|
+
fields: fieldErrors,
|
|
2287
|
+
errorCount,
|
|
2288
|
+
section: "body"
|
|
2289
|
+
});
|
|
1742
2290
|
}
|
|
1743
2291
|
}
|
|
1744
|
-
if (Object.keys(errors).length > 0) {
|
|
1745
|
-
ctx.response.status(400).json({
|
|
1746
|
-
error: "Validation Error",
|
|
1747
|
-
details: errors
|
|
1748
|
-
});
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
2292
|
await next();
|
|
1752
2293
|
};
|
|
1753
2294
|
return {
|
|
@@ -1766,12 +2307,11 @@ function createResponseValidator(responseSchema, debug = false) {
|
|
|
1766
2307
|
return originalJson.call(ctx.response, validatedBody, status);
|
|
1767
2308
|
} catch (error) {
|
|
1768
2309
|
ctx.response.json = originalJson;
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
2310
|
+
throw new InternalServerError("Response validation failed", {
|
|
2311
|
+
responseSchema: responseSchema.description || "Unknown schema",
|
|
2312
|
+
validationError: extractZodFieldErrors(error),
|
|
2313
|
+
originalResponse: body
|
|
1773
2314
|
});
|
|
1774
|
-
return ctx.response;
|
|
1775
2315
|
}
|
|
1776
2316
|
};
|
|
1777
2317
|
await next();
|
|
@@ -1782,11 +2322,25 @@ function createResponseValidator(responseSchema, debug = false) {
|
|
|
1782
2322
|
debug
|
|
1783
2323
|
};
|
|
1784
2324
|
}
|
|
1785
|
-
function
|
|
1786
|
-
if (error
|
|
1787
|
-
|
|
2325
|
+
function extractZodFieldErrors(error) {
|
|
2326
|
+
if (error instanceof z6.ZodError) {
|
|
2327
|
+
const fieldErrorMap = /* @__PURE__ */ new Map();
|
|
2328
|
+
for (const issue of error.issues) {
|
|
2329
|
+
const fieldPath = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
2330
|
+
if (!fieldErrorMap.has(fieldPath)) {
|
|
2331
|
+
fieldErrorMap.set(fieldPath, []);
|
|
2332
|
+
}
|
|
2333
|
+
fieldErrorMap.get(fieldPath).push(issue.message);
|
|
2334
|
+
}
|
|
2335
|
+
return Array.from(fieldErrorMap.entries()).map(([field, messages]) => ({
|
|
2336
|
+
field,
|
|
2337
|
+
messages
|
|
2338
|
+
}));
|
|
1788
2339
|
}
|
|
1789
|
-
|
|
2340
|
+
if (error instanceof Error) {
|
|
2341
|
+
return [{ field: "unknown", messages: [error.message] }];
|
|
2342
|
+
}
|
|
2343
|
+
return [{ field: "unknown", messages: [String(error)] }];
|
|
1790
2344
|
}
|
|
1791
2345
|
|
|
1792
2346
|
// src/router/handlers/executor.ts
|
|
@@ -2221,8 +2775,7 @@ function createRouter(options) {
|
|
|
2221
2775
|
const match = matcher.match(path6, method);
|
|
2222
2776
|
if (!match) {
|
|
2223
2777
|
console.log(`\u274C No match found for: ${method} ${path6}`);
|
|
2224
|
-
|
|
2225
|
-
return;
|
|
2778
|
+
throw new NotFoundError("Not found");
|
|
2226
2779
|
}
|
|
2227
2780
|
console.log(`\u2705 Route matched: ${method} ${path6}`);
|
|
2228
2781
|
console.log(` Params: ${JSON.stringify(match.params)}`);
|
|
@@ -2237,14 +2790,7 @@ function createRouter(options) {
|
|
|
2237
2790
|
return;
|
|
2238
2791
|
}
|
|
2239
2792
|
ctx.request.params = match.params;
|
|
2240
|
-
|
|
2241
|
-
await executeHandler(ctx, match.route, match.params);
|
|
2242
|
-
} catch (error) {
|
|
2243
|
-
handleRouteError(ctx, error, {
|
|
2244
|
-
detailed: process.env.NODE_ENV !== "production",
|
|
2245
|
-
log: true
|
|
2246
|
-
});
|
|
2247
|
-
}
|
|
2793
|
+
await executeHandler(ctx, match.route, match.params);
|
|
2248
2794
|
},
|
|
2249
2795
|
/**
|
|
2250
2796
|
* Get all registered routes (using optimized registry)
|
|
@@ -2305,7 +2851,7 @@ function createRouter(options) {
|
|
|
2305
2851
|
}
|
|
2306
2852
|
|
|
2307
2853
|
// src/server/create.ts
|
|
2308
|
-
var
|
|
2854
|
+
var DEFAULT_OPTIONS2 = {
|
|
2309
2855
|
port: 3e3,
|
|
2310
2856
|
host: "localhost",
|
|
2311
2857
|
routesDir: "./routes",
|
|
@@ -2316,7 +2862,7 @@ var DEFAULT_OPTIONS = {
|
|
|
2316
2862
|
plugins: []
|
|
2317
2863
|
};
|
|
2318
2864
|
function createServerOptions(options = {}) {
|
|
2319
|
-
const baseOptions = { ...
|
|
2865
|
+
const baseOptions = { ...DEFAULT_OPTIONS2 };
|
|
2320
2866
|
setRuntimeConfig({ routesDir: options.routesDir || baseOptions.routesDir });
|
|
2321
2867
|
return {
|
|
2322
2868
|
port: options.port ?? baseOptions.port,
|
|
@@ -2436,6 +2982,90 @@ function create3(options = {}) {
|
|
|
2436
2982
|
return serverInstance;
|
|
2437
2983
|
}
|
|
2438
2984
|
|
|
2985
|
+
// src/errors/unauthorized-error.ts
|
|
2986
|
+
var UnauthorizedError = class extends BlaizeError {
|
|
2987
|
+
/**
|
|
2988
|
+
* Creates a new UnauthorizedError instance
|
|
2989
|
+
*
|
|
2990
|
+
* @param title - Human-readable error message
|
|
2991
|
+
* @param details - Optional authentication context
|
|
2992
|
+
* @param correlationId - Optional correlation ID (uses current context if not provided)
|
|
2993
|
+
*/
|
|
2994
|
+
constructor(title, details = void 0, correlationId = void 0) {
|
|
2995
|
+
super(
|
|
2996
|
+
"UNAUTHORIZED" /* UNAUTHORIZED */,
|
|
2997
|
+
title,
|
|
2998
|
+
401,
|
|
2999
|
+
// HTTP 401 Unauthorized
|
|
3000
|
+
correlationId ?? getCurrentCorrelationId(),
|
|
3001
|
+
details
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
3005
|
+
|
|
3006
|
+
// src/errors/forbidden-error.ts
|
|
3007
|
+
var ForbiddenError = class extends BlaizeError {
|
|
3008
|
+
/**
|
|
3009
|
+
* Creates a new ForbiddenError instance
|
|
3010
|
+
*
|
|
3011
|
+
* @param title - Human-readable error message
|
|
3012
|
+
* @param details - Optional permission context
|
|
3013
|
+
* @param correlationId - Optional correlation ID (uses current context if not provided)
|
|
3014
|
+
*/
|
|
3015
|
+
constructor(title, details = void 0, correlationId = void 0) {
|
|
3016
|
+
super(
|
|
3017
|
+
"FORBIDDEN" /* FORBIDDEN */,
|
|
3018
|
+
title,
|
|
3019
|
+
403,
|
|
3020
|
+
// HTTP 403 Forbidden
|
|
3021
|
+
correlationId ?? getCurrentCorrelationId(),
|
|
3022
|
+
details
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
};
|
|
3026
|
+
|
|
3027
|
+
// src/errors/conflict-error.ts
|
|
3028
|
+
var ConflictError = class extends BlaizeError {
|
|
3029
|
+
/**
|
|
3030
|
+
* Creates a new ConflictError instance
|
|
3031
|
+
*
|
|
3032
|
+
* @param title - Human-readable error message
|
|
3033
|
+
* @param details - Optional conflict context
|
|
3034
|
+
* @param correlationId - Optional correlation ID (uses current context if not provided)
|
|
3035
|
+
*/
|
|
3036
|
+
constructor(title, details = void 0, correlationId = void 0) {
|
|
3037
|
+
super(
|
|
3038
|
+
"CONFLICT" /* CONFLICT */,
|
|
3039
|
+
title,
|
|
3040
|
+
409,
|
|
3041
|
+
// HTTP 409 Conflict
|
|
3042
|
+
correlationId ?? getCurrentCorrelationId(),
|
|
3043
|
+
details
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
3046
|
+
};
|
|
3047
|
+
|
|
3048
|
+
// src/errors/rate-limit-error.ts
|
|
3049
|
+
var RateLimitError = class extends BlaizeError {
|
|
3050
|
+
/**
|
|
3051
|
+
* Creates a new RateLimitError instance
|
|
3052
|
+
*
|
|
3053
|
+
* @param title - Human-readable error message
|
|
3054
|
+
* @param details - Optional rate limit context
|
|
3055
|
+
* @param correlationId - Optional correlation ID (uses current context if not provided)
|
|
3056
|
+
*/
|
|
3057
|
+
constructor(title, details = void 0, correlationId = void 0) {
|
|
3058
|
+
super(
|
|
3059
|
+
"RATE_LIMITED" /* RATE_LIMITED */,
|
|
3060
|
+
title,
|
|
3061
|
+
429,
|
|
3062
|
+
// HTTP 429 Too Many Requests
|
|
3063
|
+
correlationId ?? getCurrentCorrelationId(),
|
|
3064
|
+
details
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
|
|
2439
3069
|
// src/index.ts
|
|
2440
3070
|
var VERSION = "0.1.0";
|
|
2441
3071
|
var ServerAPI = { createServer: create3 };
|
|
@@ -2466,11 +3096,21 @@ var Blaize = {
|
|
|
2466
3096
|
var index_default = Blaize;
|
|
2467
3097
|
export {
|
|
2468
3098
|
Blaize,
|
|
3099
|
+
BlaizeError,
|
|
3100
|
+
ConflictError,
|
|
3101
|
+
ErrorSeverity,
|
|
3102
|
+
ErrorType,
|
|
3103
|
+
ForbiddenError,
|
|
3104
|
+
InternalServerError,
|
|
2469
3105
|
MiddlewareAPI,
|
|
3106
|
+
NotFoundError,
|
|
2470
3107
|
PluginsAPI,
|
|
3108
|
+
RateLimitError,
|
|
2471
3109
|
RouterAPI,
|
|
2472
3110
|
ServerAPI,
|
|
3111
|
+
UnauthorizedError,
|
|
2473
3112
|
VERSION,
|
|
3113
|
+
ValidationError,
|
|
2474
3114
|
compose,
|
|
2475
3115
|
createDeleteRoute,
|
|
2476
3116
|
createGetRoute,
|
|
@@ -2482,6 +3122,7 @@ export {
|
|
|
2482
3122
|
createPostRoute,
|
|
2483
3123
|
createPutRoute,
|
|
2484
3124
|
create3 as createServer,
|
|
2485
|
-
index_default as default
|
|
3125
|
+
index_default as default,
|
|
3126
|
+
isBodyParseError
|
|
2486
3127
|
};
|
|
2487
3128
|
//# sourceMappingURL=index.js.map
|