blaizejs 0.2.3 → 0.3.1

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/index.js CHANGED
@@ -1,17 +1,26 @@
1
1
  /**
2
- * blaizejs v0.2.3
2
+ * blaizejs v0.3.1
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
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
11
- }) : x)(function(x) {
12
- if (typeof require !== "undefined") return require.apply(this, arguments);
13
- throw Error('Dynamic require of "' + x + '" is not supported');
14
- });
9
+ import {
10
+ InternalServerError
11
+ } from "./chunk-ZZEQFU5V.js";
12
+ import {
13
+ ValidationError
14
+ } from "./chunk-3A5J5MKL.js";
15
+ import {
16
+ BlaizeError,
17
+ ErrorSeverity,
18
+ ErrorType,
19
+ __require,
20
+ generateCorrelationId,
21
+ getCurrentCorrelationId,
22
+ isBodyParseError
23
+ } from "./chunk-SF7ZGOEK.js";
15
24
 
16
25
  // src/middleware/execute.ts
17
26
  function execute(middleware, ctx, next) {
@@ -207,6 +216,7 @@ var createGetRoute = (config2) => {
207
216
  const path6 = getRoutePath();
208
217
  return {
209
218
  GET: config2,
219
+ // Let TypeScript infer the proper types
210
220
  path: path6
211
221
  };
212
222
  };
@@ -215,6 +225,7 @@ var createPostRoute = (config2) => {
215
225
  const path6 = getRoutePath();
216
226
  return {
217
227
  POST: config2,
228
+ // Let TypeScript infer the proper types
218
229
  path: path6
219
230
  };
220
231
  };
@@ -223,6 +234,7 @@ var createPutRoute = (config2) => {
223
234
  const path6 = getRoutePath();
224
235
  return {
225
236
  PUT: config2,
237
+ // Let TypeScript infer the proper types
226
238
  path: path6
227
239
  };
228
240
  };
@@ -231,6 +243,7 @@ var createDeleteRoute = (config2) => {
231
243
  const path6 = getRoutePath();
232
244
  return {
233
245
  DELETE: config2,
246
+ // Let TypeScript infer the proper types
234
247
  path: path6
235
248
  };
236
249
  };
@@ -239,6 +252,7 @@ var createPatchRoute = (config2) => {
239
252
  const path6 = getRoutePath();
240
253
  return {
241
254
  PATCH: config2,
255
+ // Let TypeScript infer the proper types
242
256
  path: path6
243
257
  };
244
258
  };
@@ -247,6 +261,7 @@ var createHeadRoute = (config2) => {
247
261
  const path6 = getRoutePath();
248
262
  return {
249
263
  HEAD: config2,
264
+ // Let TypeScript infer the proper types
250
265
  path: path6
251
266
  };
252
267
  };
@@ -255,6 +270,7 @@ var createOptionsRoute = (config2) => {
255
270
  const path6 = getRoutePath();
256
271
  return {
257
272
  OPTIONS: config2,
273
+ // Let TypeScript infer the proper types
258
274
  path: path6
259
275
  };
260
276
  };
@@ -391,8 +407,474 @@ function runWithContext(context, callback) {
391
407
  return contextStorage.run(context, callback);
392
408
  }
393
409
 
410
+ // src/upload/multipart-parser.ts
411
+ import { randomUUID } from "node:crypto";
412
+ import { createWriteStream } from "node:fs";
413
+ import { tmpdir } from "node:os";
414
+ import { join as join2 } from "node:path";
415
+ import { Readable } from "node:stream";
416
+
417
+ // src/upload/utils.ts
418
+ var BOUNDARY_REGEX = /boundary=([^;]+)/i;
419
+ var CONTENT_DISPOSITION_REGEX = /Content-Disposition:\s*form-data;\s*name="([^"]+)"(?:;[\s\r\n]*filename="([^"]*)")?/i;
420
+ var CONTENT_TYPE_REGEX = /Content-Type:\s*([^\r\n]+)/i;
421
+ var MULTIPART_REGEX = /multipart\/form-data/i;
422
+ function extractBoundary(contentType) {
423
+ const match = contentType.match(BOUNDARY_REGEX);
424
+ if (!match || !match[1]) return null;
425
+ let boundary = match[1].trim();
426
+ if (boundary.startsWith('"') && boundary.endsWith('"')) {
427
+ boundary = boundary.slice(1, -1);
428
+ }
429
+ return boundary || null;
430
+ }
431
+ function parseContentDisposition(headers) {
432
+ const match = headers.match(CONTENT_DISPOSITION_REGEX);
433
+ if (!match || !match[1]) return null;
434
+ return {
435
+ name: match[1],
436
+ filename: match[2] !== void 0 ? match[2] : void 0
437
+ };
438
+ }
439
+ function parseContentType(headers) {
440
+ const match = headers.match(CONTENT_TYPE_REGEX);
441
+ return match && match[1]?.trim() ? match[1].trim() : "application/octet-stream";
442
+ }
443
+ function isMultipartContent(contentType) {
444
+ return MULTIPART_REGEX.test(contentType);
445
+ }
446
+
447
+ // src/upload/multipart-parser.ts
448
+ var DEFAULT_OPTIONS = {
449
+ maxFileSize: 10 * 1024 * 1024,
450
+ // 10MB
451
+ maxFiles: 10,
452
+ maxFieldSize: 1 * 1024 * 1024,
453
+ // 1MB
454
+ allowedMimeTypes: [],
455
+ allowedExtensions: [],
456
+ strategy: "stream",
457
+ tempDir: tmpdir(),
458
+ computeHash: false
459
+ };
460
+ function createParserState(boundary, options = {}) {
461
+ return {
462
+ boundary: Buffer.from(`--${boundary}`),
463
+ options: { ...DEFAULT_OPTIONS, ...options },
464
+ fields: /* @__PURE__ */ new Map(),
465
+ files: /* @__PURE__ */ new Map(),
466
+ buffer: Buffer.alloc(0),
467
+ stage: "boundary",
468
+ currentHeaders: "",
469
+ currentField: null,
470
+ currentFilename: void 0,
471
+ currentMimetype: "application/octet-stream",
472
+ currentContentLength: 0,
473
+ fileCount: 0,
474
+ fieldCount: 0,
475
+ currentBufferChunks: [],
476
+ currentStream: null,
477
+ currentTempPath: null,
478
+ currentWriteStream: null,
479
+ streamController: null,
480
+ cleanupTasks: [],
481
+ // Track validation state
482
+ hasFoundValidBoundary: false,
483
+ hasProcessedAnyPart: false,
484
+ isFinished: false
485
+ };
486
+ }
487
+ async function processChunk(state, chunk) {
488
+ const newBuffer = Buffer.concat([state.buffer, chunk]);
489
+ let currentState = { ...state, buffer: newBuffer };
490
+ while (currentState.buffer.length > 0 && !currentState.isFinished) {
491
+ const nextState = await processCurrentStage(currentState);
492
+ if (nextState === currentState) break;
493
+ currentState = nextState;
494
+ }
495
+ return currentState;
496
+ }
497
+ async function processCurrentStage(state) {
498
+ switch (state.stage) {
499
+ case "boundary":
500
+ return processBoundary(state);
501
+ case "headers":
502
+ return processHeaders(state);
503
+ case "content":
504
+ return processContent(state);
505
+ default: {
506
+ const { InternalServerError: InternalServerError2 } = await import("./internal-server-error-PKVC3ZEU.js");
507
+ throw new InternalServerError2(`Invalid parser stage`, {
508
+ operation: state.stage
509
+ });
510
+ }
511
+ }
512
+ }
513
+ function processBoundary(state) {
514
+ const boundaryIndex = state.buffer.indexOf(state.boundary);
515
+ if (boundaryIndex === -1) return state;
516
+ const hasFoundValidBoundary = true;
517
+ let buffer = state.buffer.subarray(boundaryIndex + state.boundary.length);
518
+ if (buffer.length >= 2 && buffer.subarray(0, 2).equals(Buffer.from("--"))) {
519
+ return {
520
+ ...state,
521
+ buffer,
522
+ hasFoundValidBoundary,
523
+ isFinished: true,
524
+ stage: "boundary"
525
+ };
526
+ }
527
+ if (buffer.length >= 2 && buffer.subarray(0, 2).equals(Buffer.from("\r\n"))) {
528
+ buffer = buffer.subarray(2);
529
+ }
530
+ return {
531
+ ...state,
532
+ buffer,
533
+ hasFoundValidBoundary,
534
+ stage: "headers",
535
+ currentHeaders: ""
536
+ };
537
+ }
538
+ async function processHeaders(state) {
539
+ const headerEnd = state.buffer.indexOf("\r\n\r\n");
540
+ if (headerEnd === -1) return state;
541
+ const headers = state.buffer.subarray(0, headerEnd).toString("utf8");
542
+ const buffer = state.buffer.subarray(headerEnd + 4);
543
+ const disposition = parseContentDisposition(headers);
544
+ if (!disposition) {
545
+ const { ValidationError: ValidationError2 } = await import("./validation-error-WZFF75S7.js");
546
+ throw new ValidationError2("Missing or invalid Content-Disposition header");
547
+ }
548
+ const mimetype = parseContentType(headers);
549
+ const isFile = disposition.filename !== void 0;
550
+ if (isFile && state.fileCount >= state.options.maxFiles) {
551
+ const { PayloadTooLargeError } = await import("./payload-too-large-error-WZMDORKR.js");
552
+ throw new PayloadTooLargeError("Too many files in upload", {
553
+ fileCount: state.fileCount + 1,
554
+ maxFiles: state.options.maxFiles,
555
+ filename: disposition.filename
556
+ });
557
+ }
558
+ if (isFile && state.options.allowedMimeTypes.length > 0 && !state.options.allowedMimeTypes.includes(mimetype)) {
559
+ const { UnsupportedMediaTypeError } = await import("./unsupported-media-type-error-VUXOJ72O.js");
560
+ throw new UnsupportedMediaTypeError("File type not allowed", {
561
+ receivedMimeType: mimetype,
562
+ allowedMimeTypes: state.options.allowedMimeTypes,
563
+ filename: disposition.filename
564
+ });
565
+ }
566
+ return {
567
+ ...state,
568
+ buffer,
569
+ stage: "content",
570
+ currentHeaders: headers,
571
+ currentField: disposition.name,
572
+ currentFilename: disposition.filename,
573
+ currentMimetype: mimetype,
574
+ currentContentLength: 0,
575
+ fileCount: isFile ? state.fileCount + 1 : state.fileCount,
576
+ fieldCount: isFile ? state.fieldCount : state.fieldCount + 1,
577
+ currentBufferChunks: []
578
+ };
579
+ }
580
+ async function processContent(state) {
581
+ const nextBoundaryIndex = state.buffer.indexOf(state.boundary);
582
+ let contentChunk;
583
+ let isComplete = false;
584
+ let buffer = state.buffer;
585
+ if (nextBoundaryIndex === -1) {
586
+ const safeLength = Math.max(0, state.buffer.length - state.boundary.length);
587
+ if (safeLength === 0) return state;
588
+ contentChunk = state.buffer.subarray(0, safeLength);
589
+ buffer = state.buffer.subarray(safeLength);
590
+ } else {
591
+ const contentEnd = Math.max(0, nextBoundaryIndex - 2);
592
+ contentChunk = state.buffer.subarray(0, contentEnd);
593
+ buffer = state.buffer.subarray(nextBoundaryIndex);
594
+ isComplete = true;
595
+ }
596
+ let updatedState = { ...state, buffer };
597
+ if (contentChunk.length > 0) {
598
+ updatedState = await processContentChunk(updatedState, contentChunk);
599
+ }
600
+ if (isComplete) {
601
+ updatedState = await finalizeCurrentPart(updatedState);
602
+ updatedState = {
603
+ ...updatedState,
604
+ stage: "boundary",
605
+ hasProcessedAnyPart: true
606
+ // Mark that we've processed at least one part
607
+ };
608
+ }
609
+ return updatedState;
610
+ }
611
+ async function processContentChunk(state, chunk) {
612
+ const newContentLength = state.currentContentLength + chunk.length;
613
+ const maxSize = state.currentFilename !== void 0 ? state.options.maxFileSize : state.options.maxFieldSize;
614
+ if (newContentLength > maxSize) {
615
+ const isFile = state.currentFilename !== void 0;
616
+ const { PayloadTooLargeError } = await import("./payload-too-large-error-WZMDORKR.js");
617
+ const payloadErrorDetals = state.currentField ? {
618
+ contentType: isFile ? "file" : "field",
619
+ currentSize: newContentLength,
620
+ maxSize,
621
+ field: state.currentField,
622
+ filename: state.currentFilename
623
+ } : {
624
+ contentType: isFile ? "file" : "field",
625
+ currentSize: newContentLength,
626
+ maxSize,
627
+ filename: state.currentFilename
628
+ };
629
+ throw new PayloadTooLargeError(
630
+ `${isFile ? "File" : "Field"} size exceeds limit`,
631
+ payloadErrorDetals
632
+ );
633
+ }
634
+ if (state.currentFilename !== void 0) {
635
+ return processFileChunk(state, chunk, newContentLength);
636
+ } else {
637
+ return {
638
+ ...state,
639
+ currentContentLength: newContentLength,
640
+ currentBufferChunks: [...state.currentBufferChunks, chunk]
641
+ };
642
+ }
643
+ }
644
+ async function processFileChunk(state, chunk, newContentLength) {
645
+ switch (state.options.strategy) {
646
+ case "memory":
647
+ return {
648
+ ...state,
649
+ currentContentLength: newContentLength,
650
+ currentBufferChunks: [...state.currentBufferChunks, chunk]
651
+ };
652
+ case "stream":
653
+ if (state.streamController) {
654
+ state.streamController.enqueue(chunk);
655
+ }
656
+ return { ...state, currentContentLength: newContentLength };
657
+ case "temp":
658
+ if (state.currentWriteStream) {
659
+ await writeToStream(state.currentWriteStream, chunk);
660
+ }
661
+ return { ...state, currentContentLength: newContentLength };
662
+ default: {
663
+ const { ValidationError: ValidationError2 } = await import("./validation-error-WZFF75S7.js");
664
+ throw new ValidationError2(`Invalid parsing strategy`);
665
+ }
666
+ }
667
+ }
668
+ async function initializeFileProcessing(state) {
669
+ if (state.currentFilename === void 0) return state;
670
+ switch (state.options.strategy) {
671
+ case "memory":
672
+ return { ...state, currentBufferChunks: [] };
673
+ case "stream": {
674
+ let streamController = null;
675
+ const stream = new ReadableStream({
676
+ start: (controller) => {
677
+ streamController = controller;
678
+ }
679
+ });
680
+ return {
681
+ ...state,
682
+ currentStream: stream,
683
+ // Type cast for Node.js compatibility
684
+ streamController
685
+ };
686
+ }
687
+ case "temp": {
688
+ const tempPath = join2(state.options.tempDir, `upload-${randomUUID()}`);
689
+ const writeStream = createWriteStream(tempPath);
690
+ const cleanupTask = async () => {
691
+ try {
692
+ const { unlink } = await import("node:fs/promises");
693
+ await unlink(tempPath);
694
+ } catch (error) {
695
+ console.warn(`Failed to cleanup temp file: ${tempPath}`, error);
696
+ }
697
+ };
698
+ return {
699
+ ...state,
700
+ currentTempPath: tempPath,
701
+ currentWriteStream: writeStream,
702
+ cleanupTasks: [...state.cleanupTasks, cleanupTask]
703
+ };
704
+ }
705
+ default: {
706
+ const { ValidationError: ValidationError2 } = await import("./validation-error-WZFF75S7.js");
707
+ throw new ValidationError2(`Invalid file processing strategy`);
708
+ }
709
+ }
710
+ }
711
+ async function finalizeCurrentPart(state) {
712
+ if (!state.currentField) return resetCurrentPart(state);
713
+ if (state.currentFilename !== void 0) {
714
+ return finalizeFile(state);
715
+ } else {
716
+ return finalizeField(state);
717
+ }
718
+ }
719
+ async function finalizeFile(state) {
720
+ if (!state.currentField || state.currentFilename === void 0) {
721
+ return resetCurrentPart(state);
722
+ }
723
+ let stream;
724
+ let buffer;
725
+ let tempPath;
726
+ switch (state.options.strategy) {
727
+ case "memory":
728
+ buffer = Buffer.concat(state.currentBufferChunks);
729
+ stream = Readable.from(buffer);
730
+ break;
731
+ case "stream":
732
+ if (state.streamController) {
733
+ state.streamController.close();
734
+ }
735
+ stream = state.currentStream;
736
+ break;
737
+ case "temp":
738
+ if (state.currentWriteStream) {
739
+ await closeStream(state.currentWriteStream);
740
+ }
741
+ tempPath = state.currentTempPath;
742
+ stream = Readable.from(Buffer.alloc(0));
743
+ break;
744
+ default: {
745
+ const { ValidationError: ValidationError2 } = await import("./validation-error-WZFF75S7.js");
746
+ throw new ValidationError2(`Invalid file finalization strategy`);
747
+ }
748
+ }
749
+ const file = {
750
+ filename: state.currentFilename,
751
+ fieldname: state.currentField,
752
+ mimetype: state.currentMimetype,
753
+ size: state.currentContentLength,
754
+ stream,
755
+ buffer,
756
+ tempPath
757
+ };
758
+ const updatedFiles = addToCollection(state.files, state.currentField, file);
759
+ return {
760
+ ...resetCurrentPart(state),
761
+ files: updatedFiles
762
+ };
763
+ }
764
+ function finalizeField(state) {
765
+ if (!state.currentField) return resetCurrentPart(state);
766
+ const value = Buffer.concat(state.currentBufferChunks).toString("utf8");
767
+ const updatedFields = addToCollection(state.fields, state.currentField, value);
768
+ return {
769
+ ...resetCurrentPart(state),
770
+ fields: updatedFields
771
+ };
772
+ }
773
+ function resetCurrentPart(state) {
774
+ return {
775
+ ...state,
776
+ currentField: null,
777
+ currentFilename: void 0,
778
+ currentContentLength: 0,
779
+ currentBufferChunks: [],
780
+ currentStream: null,
781
+ streamController: null,
782
+ currentTempPath: null,
783
+ currentWriteStream: null
784
+ };
785
+ }
786
+ function addToCollection(collection, key, value) {
787
+ const newCollection = new Map(collection);
788
+ const existing = newCollection.get(key) || [];
789
+ newCollection.set(key, [...existing, value]);
790
+ return newCollection;
791
+ }
792
+ async function finalize(state) {
793
+ if (!state.hasFoundValidBoundary) {
794
+ const { ValidationError: ValidationError2 } = await import("./validation-error-WZFF75S7.js");
795
+ throw new ValidationError2("No valid multipart boundary found");
796
+ }
797
+ if (state.hasFoundValidBoundary && !state.hasProcessedAnyPart) {
798
+ const { ValidationError: ValidationError2 } = await import("./validation-error-WZFF75S7.js");
799
+ throw new ValidationError2("Empty multipart request");
800
+ }
801
+ const fields = {};
802
+ for (const [key, values] of state.fields.entries()) {
803
+ fields[key] = values.length === 1 ? values[0] : values;
804
+ }
805
+ const files = {};
806
+ for (const [key, fileList] of state.files.entries()) {
807
+ files[key] = fileList.length === 1 ? fileList[0] : fileList;
808
+ }
809
+ return { fields, files };
810
+ }
811
+ async function cleanup(state) {
812
+ await Promise.allSettled(state.cleanupTasks.map((task) => task()));
813
+ if (state.streamController) {
814
+ state.streamController.close();
815
+ }
816
+ if (state.currentWriteStream) {
817
+ await closeStream(state.currentWriteStream);
818
+ }
819
+ }
820
+ async function writeToStream(stream, chunk) {
821
+ return new Promise((resolve3, reject) => {
822
+ stream.write(chunk, (error) => {
823
+ if (error) reject(error);
824
+ else resolve3();
825
+ });
826
+ });
827
+ }
828
+ async function closeStream(stream) {
829
+ return new Promise((resolve3) => {
830
+ stream.end(() => resolve3());
831
+ });
832
+ }
833
+ async function parseMultipartRequest(request, options = {}) {
834
+ const contentType = request.headers["content-type"] || "";
835
+ const boundary = extractBoundary(contentType);
836
+ if (!boundary) {
837
+ const { UnsupportedMediaTypeError } = await import("./unsupported-media-type-error-VUXOJ72O.js");
838
+ throw new UnsupportedMediaTypeError("Missing boundary in multipart content-type", {
839
+ receivedContentType: contentType,
840
+ expectedFormat: "multipart/form-data; boundary=..."
841
+ });
842
+ }
843
+ let state = createParserState(boundary, options);
844
+ if (state.currentFilename !== void 0) {
845
+ state = await initializeFileProcessing(state);
846
+ }
847
+ try {
848
+ for await (const chunk of request) {
849
+ state = await processChunk(state, chunk);
850
+ }
851
+ return finalize(state);
852
+ } finally {
853
+ await cleanup(state);
854
+ }
855
+ }
856
+
394
857
  // src/context/create.ts
395
858
  var CONTENT_TYPE_HEADER = "Content-Type";
859
+ var DEFAULT_BODY_LIMITS = {
860
+ json: 512 * 1024,
861
+ // 512KB - Most APIs should be much smaller
862
+ form: 1024 * 1024,
863
+ // 1MB - Reasonable for form submissions
864
+ text: 5 * 1024 * 1024,
865
+ // 5MB - Documents, logs, code files
866
+ multipart: {
867
+ maxFileSize: 50 * 1024 * 1024,
868
+ // 50MB per file
869
+ maxTotalSize: 100 * 1024 * 1024,
870
+ // 100MB total request
871
+ maxFiles: 10,
872
+ maxFieldSize: 1024 * 1024
873
+ // 1MB for form fields
874
+ },
875
+ raw: 10 * 1024 * 1024
876
+ // 10MB for unknown content types
877
+ };
396
878
  function parseRequestUrl(req) {
397
879
  const originalUrl = req.url || "/";
398
880
  const host = req.headers.host || "localhost";
@@ -457,7 +939,7 @@ async function createContext(req, res, options = {}) {
457
939
  };
458
940
  ctx.response = createResponseObject(res, responseState, ctx);
459
941
  if (options.parseBody) {
460
- await parseBodyIfNeeded(req, ctx);
942
+ await parseBodyIfNeeded(req, ctx, options);
461
943
  }
462
944
  return ctx;
463
945
  }
@@ -633,34 +1115,60 @@ function createStreamResponder(res, responseState) {
633
1115
  });
634
1116
  };
635
1117
  }
636
- async function parseBodyIfNeeded(req, ctx) {
1118
+ async function parseBodyIfNeeded(req, ctx, options = {}) {
637
1119
  if (shouldSkipParsing(req.method)) {
638
1120
  return;
639
1121
  }
640
1122
  const contentType = req.headers["content-type"] || "";
641
1123
  const contentLength = parseInt(req.headers["content-length"] || "0", 10);
642
- if (contentLength === 0 || contentLength > 1048576) {
1124
+ if (contentLength === 0) {
643
1125
  return;
644
1126
  }
1127
+ const limits = {
1128
+ json: options.bodyLimits?.json ?? DEFAULT_BODY_LIMITS.json,
1129
+ form: options.bodyLimits?.form ?? DEFAULT_BODY_LIMITS.form,
1130
+ text: options.bodyLimits?.text ?? DEFAULT_BODY_LIMITS.text,
1131
+ raw: options.bodyLimits?.raw ?? DEFAULT_BODY_LIMITS.raw,
1132
+ multipart: {
1133
+ maxFileSize: options.bodyLimits?.multipart?.maxFileSize ?? DEFAULT_BODY_LIMITS.multipart.maxFileSize,
1134
+ maxFiles: options.bodyLimits?.multipart?.maxFiles ?? DEFAULT_BODY_LIMITS.multipart.maxFiles,
1135
+ maxFieldSize: options.bodyLimits?.multipart?.maxFieldSize ?? DEFAULT_BODY_LIMITS.multipart.maxFieldSize,
1136
+ maxTotalSize: options.bodyLimits?.multipart?.maxTotalSize ?? DEFAULT_BODY_LIMITS.multipart.maxTotalSize
1137
+ }
1138
+ };
645
1139
  try {
646
- await parseBodyByContentType(req, ctx, contentType);
1140
+ if (contentType.includes("application/json")) {
1141
+ if (contentLength > limits.json) {
1142
+ throw new Error(`JSON body too large: ${contentLength} > ${limits.json} bytes`);
1143
+ }
1144
+ await parseJsonBody(req, ctx);
1145
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
1146
+ if (contentLength > limits.form) {
1147
+ throw new Error(`Form body too large: ${contentLength} > ${limits.form} bytes`);
1148
+ }
1149
+ await parseFormUrlEncodedBody(req, ctx);
1150
+ } else if (contentType.includes("text/")) {
1151
+ if (contentLength > limits.text) {
1152
+ throw new Error(`Text body too large: ${contentLength} > ${limits.text} bytes`);
1153
+ }
1154
+ await parseTextBody(req, ctx);
1155
+ } else if (isMultipartContent(contentType)) {
1156
+ await parseMultipartBody(req, ctx, limits.multipart);
1157
+ } else {
1158
+ if (contentLength > limits.raw) {
1159
+ throw new Error(`Request body too large: ${contentLength} > ${limits.raw} bytes`);
1160
+ }
1161
+ return;
1162
+ }
647
1163
  } catch (error) {
648
- setBodyError(ctx, "body_read_error", "Error reading request body", error);
1164
+ const errorType = contentType.includes("multipart") ? "multipart_parse_error" : "body_read_error";
1165
+ setBodyError(ctx, errorType, "Error reading request body", error);
649
1166
  }
650
1167
  }
651
1168
  function shouldSkipParsing(method) {
652
1169
  const skipMethods = ["GET", "HEAD", "OPTIONS"];
653
1170
  return skipMethods.includes(method || "GET");
654
1171
  }
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
1172
  async function parseJsonBody(req, ctx) {
665
1173
  const body = await readRequestBody(req);
666
1174
  if (!body) {
@@ -712,8 +1220,27 @@ async function parseTextBody(req, ctx) {
712
1220
  ctx.request.body = body;
713
1221
  }
714
1222
  }
1223
+ async function parseMultipartBody(req, ctx, multipartLimits) {
1224
+ try {
1225
+ const limits = multipartLimits || DEFAULT_BODY_LIMITS.multipart;
1226
+ const multipartData = await parseMultipartRequest(req, {
1227
+ strategy: "stream",
1228
+ maxFileSize: limits.maxFileSize,
1229
+ maxFiles: limits.maxFiles,
1230
+ maxFieldSize: limits.maxFieldSize
1231
+ // Could add total size validation here
1232
+ });
1233
+ ctx.request.multipart = multipartData;
1234
+ ctx.request.files = multipartData.files;
1235
+ ctx.request.body = multipartData.fields;
1236
+ } catch (error) {
1237
+ ctx.request.body = null;
1238
+ setBodyError(ctx, "multipart_parse_error", "Failed to parse multipart data", error);
1239
+ }
1240
+ }
715
1241
  function setBodyError(ctx, type, message, error) {
716
- ctx.state._bodyError = { type, message, error };
1242
+ const bodyError = { type, message, error };
1243
+ ctx.state._bodyError = bodyError;
717
1244
  }
718
1245
  async function readRequestBody(req) {
719
1246
  return new Promise((resolve3, reject) => {
@@ -730,6 +1257,102 @@ async function readRequestBody(req) {
730
1257
  });
731
1258
  }
732
1259
 
1260
+ // src/errors/not-found-error.ts
1261
+ var NotFoundError = class extends BlaizeError {
1262
+ /**
1263
+ * Creates a new NotFoundError instance
1264
+ *
1265
+ * @param title - Human-readable error message
1266
+ * @param details - Optional context about the missing resource
1267
+ * @param correlationId - Optional correlation ID (uses current context if not provided)
1268
+ */
1269
+ constructor(title, details = void 0, correlationId = void 0) {
1270
+ super(
1271
+ "NOT_FOUND" /* NOT_FOUND */,
1272
+ title,
1273
+ 404,
1274
+ // HTTP 404 Not Found
1275
+ correlationId ?? getCurrentCorrelationId(),
1276
+ details
1277
+ );
1278
+ }
1279
+ };
1280
+
1281
+ // src/errors/boundary.ts
1282
+ function isHandledError(error) {
1283
+ return error instanceof BlaizeError;
1284
+ }
1285
+ function formatErrorResponse(error) {
1286
+ if (isHandledError(error)) {
1287
+ return {
1288
+ type: error.type,
1289
+ title: error.title,
1290
+ status: error.status,
1291
+ correlationId: error.correlationId,
1292
+ timestamp: error.timestamp.toISOString(),
1293
+ details: error.details
1294
+ };
1295
+ }
1296
+ const correlationId = generateCorrelationId();
1297
+ let originalMessage;
1298
+ if (error instanceof Error) {
1299
+ originalMessage = error.message;
1300
+ } else if (error === null || error === void 0) {
1301
+ originalMessage = "Unknown error occurred";
1302
+ } else {
1303
+ originalMessage = String(error);
1304
+ }
1305
+ const wrappedError = new InternalServerError(
1306
+ "Internal Server Error",
1307
+ { originalMessage },
1308
+ correlationId
1309
+ );
1310
+ return {
1311
+ type: wrappedError.type,
1312
+ title: wrappedError.title,
1313
+ status: wrappedError.status,
1314
+ correlationId: wrappedError.correlationId,
1315
+ timestamp: wrappedError.timestamp.toISOString(),
1316
+ details: wrappedError.details
1317
+ };
1318
+ }
1319
+ function extractOrGenerateCorrelationId(headerGetter) {
1320
+ return headerGetter("x-correlation-id") ?? generateCorrelationId();
1321
+ }
1322
+ function setErrorResponseHeaders(headerSetter, correlationId) {
1323
+ headerSetter("x-correlation-id", correlationId);
1324
+ }
1325
+
1326
+ // src/middleware/error-boundary.ts
1327
+ function createErrorBoundary(options = {}) {
1328
+ const { debug = false } = options;
1329
+ const middlewareFn = async (ctx, next) => {
1330
+ try {
1331
+ await next();
1332
+ } catch (error) {
1333
+ if (ctx.response.sent) {
1334
+ if (debug) {
1335
+ console.error("Error occurred after response was sent:", error);
1336
+ }
1337
+ return;
1338
+ }
1339
+ if (debug) {
1340
+ console.error("Error boundary caught error:", error);
1341
+ }
1342
+ const correlationId = extractOrGenerateCorrelationId(ctx.request.header);
1343
+ const errorResponse = formatErrorResponse(error);
1344
+ errorResponse.correlationId = correlationId;
1345
+ setErrorResponseHeaders(ctx.response.header, correlationId);
1346
+ ctx.response.status(errorResponse.status).json(errorResponse);
1347
+ }
1348
+ };
1349
+ return {
1350
+ name: "ErrorBoundary",
1351
+ execute: middlewareFn,
1352
+ debug
1353
+ };
1354
+ }
1355
+
733
1356
  // src/server/request-handler.ts
734
1357
  function createRequestHandler(serverInstance) {
735
1358
  return async (req, res) => {
@@ -738,32 +1361,20 @@ function createRequestHandler(serverInstance) {
738
1361
  parseBody: true
739
1362
  // Enable automatic body parsing
740
1363
  });
741
- const handler = compose(serverInstance.middleware);
1364
+ const errorBoundary = createErrorBoundary();
1365
+ const allMiddleware = [errorBoundary, ...serverInstance.middleware];
1366
+ const handler = compose(allMiddleware);
742
1367
  await runWithContext(context, async () => {
743
- try {
744
- await handler(context, async () => {
1368
+ await handler(context, async () => {
1369
+ if (!context.response.sent) {
1370
+ await serverInstance.router.handleRequest(context);
745
1371
  if (!context.response.sent) {
746
- await serverInstance.router.handleRequest(context);
747
- if (!context.response.sent) {
748
- context.response.status(404).json({
749
- error: "Not Found",
750
- message: `Route not found: ${context.request.method} ${context.request.path}`
751
- });
752
- }
1372
+ throw new NotFoundError(
1373
+ `Route not found: ${context.request.method} ${context.request.path}`
1374
+ );
753
1375
  }
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
1376
  }
766
- }
1377
+ });
767
1378
  });
768
1379
  } catch (error) {
769
1380
  console.error("Error creating context:", error);
@@ -1604,81 +2215,8 @@ function watchRoutes(routesDir, options = {}) {
1604
2215
  };
1605
2216
  }
1606
2217
 
1607
- // src/router/handlers/error.ts
1608
- function handleRouteError(ctx, error, options = {}) {
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
- }
2218
+ // src/router/validation/schema.ts
2219
+ import { z as z6 } from "zod";
1682
2220
 
1683
2221
  // src/router/validation/body.ts
1684
2222
  import { z as z2 } from "zod";
@@ -1719,35 +2257,45 @@ function validateResponse(response, schema) {
1719
2257
  // src/router/validation/schema.ts
1720
2258
  function createRequestValidator(schema, debug = false) {
1721
2259
  const middlewareFn = async (ctx, next) => {
1722
- const errors = {};
1723
2260
  if (schema.params && ctx.request.params) {
1724
2261
  try {
1725
2262
  ctx.request.params = validateParams(ctx.request.params, schema.params);
1726
2263
  } catch (error) {
1727
- errors.params = formatValidationError(error);
2264
+ const fieldErrors = extractZodFieldErrors(error);
2265
+ const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
2266
+ throw new ValidationError("Request validation failed", {
2267
+ fields: fieldErrors,
2268
+ errorCount,
2269
+ section: "params"
2270
+ });
1728
2271
  }
1729
2272
  }
1730
2273
  if (schema.query && ctx.request.query) {
1731
2274
  try {
1732
2275
  ctx.request.query = validateQuery(ctx.request.query, schema.query);
1733
2276
  } catch (error) {
1734
- errors.query = formatValidationError(error);
2277
+ const fieldErrors = extractZodFieldErrors(error);
2278
+ const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
2279
+ throw new ValidationError("Request validation failed", {
2280
+ fields: fieldErrors,
2281
+ errorCount,
2282
+ section: "query"
2283
+ });
1735
2284
  }
1736
2285
  }
1737
2286
  if (schema.body) {
1738
2287
  try {
1739
2288
  ctx.request.body = validateBody(ctx.request.body, schema.body);
1740
2289
  } catch (error) {
1741
- errors.body = formatValidationError(error);
2290
+ const fieldErrors = extractZodFieldErrors(error);
2291
+ const errorCount = fieldErrors.reduce((sum, fe) => sum + fe.messages.length, 0);
2292
+ throw new ValidationError("Request validation failed", {
2293
+ fields: fieldErrors,
2294
+ errorCount,
2295
+ section: "body"
2296
+ });
1742
2297
  }
1743
2298
  }
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
2299
  await next();
1752
2300
  };
1753
2301
  return {
@@ -1766,12 +2314,11 @@ function createResponseValidator(responseSchema, debug = false) {
1766
2314
  return originalJson.call(ctx.response, validatedBody, status);
1767
2315
  } catch (error) {
1768
2316
  ctx.response.json = originalJson;
1769
- console.error("Response validation error:", error);
1770
- ctx.response.status(500).json({
1771
- error: "Internal Server Error",
1772
- message: "Response validation failed"
2317
+ throw new InternalServerError("Response validation failed", {
2318
+ responseSchema: responseSchema.description || "Unknown schema",
2319
+ validationError: extractZodFieldErrors(error),
2320
+ originalResponse: body
1773
2321
  });
1774
- return ctx.response;
1775
2322
  }
1776
2323
  };
1777
2324
  await next();
@@ -1782,11 +2329,25 @@ function createResponseValidator(responseSchema, debug = false) {
1782
2329
  debug
1783
2330
  };
1784
2331
  }
1785
- function formatValidationError(error) {
1786
- if (error && typeof error === "object" && "format" in error && typeof error.format === "function") {
1787
- return error.format();
2332
+ function extractZodFieldErrors(error) {
2333
+ if (error instanceof z6.ZodError) {
2334
+ const fieldErrorMap = /* @__PURE__ */ new Map();
2335
+ for (const issue of error.issues) {
2336
+ const fieldPath = issue.path.length > 0 ? issue.path.join(".") : "root";
2337
+ if (!fieldErrorMap.has(fieldPath)) {
2338
+ fieldErrorMap.set(fieldPath, []);
2339
+ }
2340
+ fieldErrorMap.get(fieldPath).push(issue.message);
2341
+ }
2342
+ return Array.from(fieldErrorMap.entries()).map(([field, messages]) => ({
2343
+ field,
2344
+ messages
2345
+ }));
1788
2346
  }
1789
- return error instanceof Error ? error.message : String(error);
2347
+ if (error instanceof Error) {
2348
+ return [{ field: "unknown", messages: [error.message] }];
2349
+ }
2350
+ return [{ field: "unknown", messages: [String(error)] }];
1790
2351
  }
1791
2352
 
1792
2353
  // src/router/handlers/executor.ts
@@ -2221,8 +2782,7 @@ function createRouter(options) {
2221
2782
  const match = matcher.match(path6, method);
2222
2783
  if (!match) {
2223
2784
  console.log(`\u274C No match found for: ${method} ${path6}`);
2224
- ctx.response.status(404).json({ error: "Not Found" });
2225
- return;
2785
+ throw new NotFoundError("Not found");
2226
2786
  }
2227
2787
  console.log(`\u2705 Route matched: ${method} ${path6}`);
2228
2788
  console.log(` Params: ${JSON.stringify(match.params)}`);
@@ -2237,14 +2797,7 @@ function createRouter(options) {
2237
2797
  return;
2238
2798
  }
2239
2799
  ctx.request.params = match.params;
2240
- try {
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
- }
2800
+ await executeHandler(ctx, match.route, match.params);
2248
2801
  },
2249
2802
  /**
2250
2803
  * Get all registered routes (using optimized registry)
@@ -2305,7 +2858,7 @@ function createRouter(options) {
2305
2858
  }
2306
2859
 
2307
2860
  // src/server/create.ts
2308
- var DEFAULT_OPTIONS = {
2861
+ var DEFAULT_OPTIONS2 = {
2309
2862
  port: 3e3,
2310
2863
  host: "localhost",
2311
2864
  routesDir: "./routes",
@@ -2316,7 +2869,7 @@ var DEFAULT_OPTIONS = {
2316
2869
  plugins: []
2317
2870
  };
2318
2871
  function createServerOptions(options = {}) {
2319
- const baseOptions = { ...DEFAULT_OPTIONS };
2872
+ const baseOptions = { ...DEFAULT_OPTIONS2 };
2320
2873
  setRuntimeConfig({ routesDir: options.routesDir || baseOptions.routesDir });
2321
2874
  return {
2322
2875
  port: options.port ?? baseOptions.port,
@@ -2436,6 +2989,90 @@ function create3(options = {}) {
2436
2989
  return serverInstance;
2437
2990
  }
2438
2991
 
2992
+ // src/errors/unauthorized-error.ts
2993
+ var UnauthorizedError = class extends BlaizeError {
2994
+ /**
2995
+ * Creates a new UnauthorizedError instance
2996
+ *
2997
+ * @param title - Human-readable error message
2998
+ * @param details - Optional authentication context
2999
+ * @param correlationId - Optional correlation ID (uses current context if not provided)
3000
+ */
3001
+ constructor(title, details = void 0, correlationId = void 0) {
3002
+ super(
3003
+ "UNAUTHORIZED" /* UNAUTHORIZED */,
3004
+ title,
3005
+ 401,
3006
+ // HTTP 401 Unauthorized
3007
+ correlationId ?? getCurrentCorrelationId(),
3008
+ details
3009
+ );
3010
+ }
3011
+ };
3012
+
3013
+ // src/errors/forbidden-error.ts
3014
+ var ForbiddenError = class extends BlaizeError {
3015
+ /**
3016
+ * Creates a new ForbiddenError instance
3017
+ *
3018
+ * @param title - Human-readable error message
3019
+ * @param details - Optional permission context
3020
+ * @param correlationId - Optional correlation ID (uses current context if not provided)
3021
+ */
3022
+ constructor(title, details = void 0, correlationId = void 0) {
3023
+ super(
3024
+ "FORBIDDEN" /* FORBIDDEN */,
3025
+ title,
3026
+ 403,
3027
+ // HTTP 403 Forbidden
3028
+ correlationId ?? getCurrentCorrelationId(),
3029
+ details
3030
+ );
3031
+ }
3032
+ };
3033
+
3034
+ // src/errors/conflict-error.ts
3035
+ var ConflictError = class extends BlaizeError {
3036
+ /**
3037
+ * Creates a new ConflictError instance
3038
+ *
3039
+ * @param title - Human-readable error message
3040
+ * @param details - Optional conflict context
3041
+ * @param correlationId - Optional correlation ID (uses current context if not provided)
3042
+ */
3043
+ constructor(title, details = void 0, correlationId = void 0) {
3044
+ super(
3045
+ "CONFLICT" /* CONFLICT */,
3046
+ title,
3047
+ 409,
3048
+ // HTTP 409 Conflict
3049
+ correlationId ?? getCurrentCorrelationId(),
3050
+ details
3051
+ );
3052
+ }
3053
+ };
3054
+
3055
+ // src/errors/rate-limit-error.ts
3056
+ var RateLimitError = class extends BlaizeError {
3057
+ /**
3058
+ * Creates a new RateLimitError instance
3059
+ *
3060
+ * @param title - Human-readable error message
3061
+ * @param details - Optional rate limit context
3062
+ * @param correlationId - Optional correlation ID (uses current context if not provided)
3063
+ */
3064
+ constructor(title, details = void 0, correlationId = void 0) {
3065
+ super(
3066
+ "RATE_LIMITED" /* RATE_LIMITED */,
3067
+ title,
3068
+ 429,
3069
+ // HTTP 429 Too Many Requests
3070
+ correlationId ?? getCurrentCorrelationId(),
3071
+ details
3072
+ );
3073
+ }
3074
+ };
3075
+
2439
3076
  // src/index.ts
2440
3077
  var VERSION = "0.1.0";
2441
3078
  var ServerAPI = { createServer: create3 };
@@ -2466,11 +3103,21 @@ var Blaize = {
2466
3103
  var index_default = Blaize;
2467
3104
  export {
2468
3105
  Blaize,
3106
+ BlaizeError,
3107
+ ConflictError,
3108
+ ErrorSeverity,
3109
+ ErrorType,
3110
+ ForbiddenError,
3111
+ InternalServerError,
2469
3112
  MiddlewareAPI,
3113
+ NotFoundError,
2470
3114
  PluginsAPI,
3115
+ RateLimitError,
2471
3116
  RouterAPI,
2472
3117
  ServerAPI,
3118
+ UnauthorizedError,
2473
3119
  VERSION,
3120
+ ValidationError,
2474
3121
  compose,
2475
3122
  createDeleteRoute,
2476
3123
  createGetRoute,
@@ -2482,6 +3129,7 @@ export {
2482
3129
  createPostRoute,
2483
3130
  createPutRoute,
2484
3131
  create3 as createServer,
2485
- index_default as default
3132
+ index_default as default,
3133
+ isBodyParseError
2486
3134
  };
2487
3135
  //# sourceMappingURL=index.js.map