@carno.js/core 0.2.3 → 0.2.4

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/Carno.d.ts CHANGED
@@ -16,6 +16,7 @@ export declare class Carno {
16
16
  config: ApplicationConfig;
17
17
  router: Memoirist<TokenRouteWithProvider>;
18
18
  private injector;
19
+ private corsCache?;
19
20
  private fetch;
20
21
  private server;
21
22
  constructor(config?: ApplicationConfig);
@@ -57,7 +58,6 @@ export declare class Carno {
57
58
  private reportHookFailure;
58
59
  private isCorsEnabled;
59
60
  private isOriginAllowed;
60
- private buildCorsHeaders;
61
61
  private handlePreflightRequest;
62
62
  private applyCorsHeaders;
63
63
  close(closeActiveConnections?: boolean): void;
package/dist/Carno.js CHANGED
@@ -12,7 +12,7 @@ const createInjector_1 = require("./container/createInjector");
12
12
  const domain_1 = require("./domain");
13
13
  const Context_1 = require("./domain/Context");
14
14
  const LocalsContainer_1 = require("./domain/LocalsContainer");
15
- const cors_config_1 = require("./domain/cors-config");
15
+ const cors_headers_cache_1 = require("./domain/cors-headers-cache");
16
16
  const on_event_1 = require("./events/on-event");
17
17
  const HttpException_1 = require("./exceptions/HttpException");
18
18
  const RouteExecutor_1 = require("./route/RouteExecutor");
@@ -53,6 +53,9 @@ class Carno {
53
53
  console.error("Unhandled error:", error);
54
54
  return new Response("Internal Server Error", { status: 500 });
55
55
  };
56
+ if (config.cors) {
57
+ this.corsCache = new cors_headers_cache_1.CorsHeadersCache(config.cors);
58
+ }
56
59
  void this.bootstrapApplication();
57
60
  }
58
61
  /**
@@ -249,50 +252,19 @@ class Carno {
249
252
  }
250
253
  return false;
251
254
  }
252
- buildCorsHeaders(origin) {
253
- const cors = this.config.cors;
254
- const headers = {};
255
- const allowedOrigin = typeof cors.origins === "string" && cors.origins === "*"
256
- ? "*"
257
- : origin;
258
- headers["Access-Control-Allow-Origin"] = allowedOrigin;
259
- if (cors.credentials) {
260
- headers["Access-Control-Allow-Credentials"] = "true";
261
- }
262
- const methods = cors.methods || cors_config_1.DEFAULT_CORS_METHODS;
263
- headers["Access-Control-Allow-Methods"] = methods.join(", ");
264
- const allowedHeaders = cors.allowedHeaders || cors_config_1.DEFAULT_CORS_ALLOWED_HEADERS;
265
- headers["Access-Control-Allow-Headers"] = allowedHeaders.join(", ");
266
- if (cors.exposedHeaders && cors.exposedHeaders.length > 0) {
267
- headers["Access-Control-Expose-Headers"] = cors.exposedHeaders.join(", ");
268
- }
269
- if (cors.maxAge !== undefined) {
270
- headers["Access-Control-Max-Age"] = cors.maxAge.toString();
271
- }
272
- return headers;
273
- }
274
255
  handlePreflightRequest(request) {
275
256
  const origin = request.headers.get("origin");
276
257
  if (!this.isOriginAllowed(origin)) {
277
258
  return new Response(null, { status: 403 });
278
259
  }
279
- const corsHeaders = this.buildCorsHeaders(origin);
260
+ const corsHeaders = this.corsCache.get(origin);
280
261
  return new Response(null, {
281
262
  status: 204,
282
263
  headers: corsHeaders,
283
264
  });
284
265
  }
285
266
  applyCorsHeaders(response, origin) {
286
- const corsHeaders = this.buildCorsHeaders(origin);
287
- const newHeaders = new Headers(response.headers);
288
- for (const [key, value] of Object.entries(corsHeaders)) {
289
- newHeaders.set(key, value);
290
- }
291
- return new Response(response.body, {
292
- status: response.status,
293
- statusText: response.statusText,
294
- headers: newHeaders,
295
- });
267
+ return this.corsCache.applyToResponse(response, origin);
296
268
  }
297
269
  close(closeActiveConnections = false) {
298
270
  this.server?.stop(closeActiveConnections);
@@ -23,4 +23,9 @@ export declare class Context {
23
23
  getResponseStatus(): number;
24
24
  private buildQueryObject;
25
25
  private resolveBody;
26
+ private parseJsonFromBuffer;
27
+ private parseJsonText;
28
+ private isEmptyBuffer;
29
+ private parseUrlEncodedFromBuffer;
30
+ private decodeBuffer;
26
31
  }
@@ -11,7 +11,9 @@ var __metadata = (this && this.__metadata) || function (k, v) {
11
11
  var Context_1;
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.Context = void 0;
14
+ const http_code_enum_1 = require("../commons/http-code.enum");
14
15
  const Injectable_decorator_1 = require("../commons/decorators/Injectable.decorator");
16
+ const HttpException_1 = require("../exceptions/HttpException");
15
17
  const provider_scope_1 = require("./provider-scope");
16
18
  let Context = Context_1 = class Context {
17
19
  constructor() {
@@ -88,16 +90,54 @@ let Context = Context_1 = class Context {
88
90
  }
89
91
  async resolveBody(request) {
90
92
  const contentType = request.headers.get('content-type') || '';
91
- this.rawBody = await request.clone().arrayBuffer();
93
+ // Clone request once - preserve original request untouched
94
+ const clonedRequest = request.clone();
95
+ // FormData multipart requires consuming as formData
96
+ if (contentType.includes('multipart/form-data')) {
97
+ // Need separate clone for rawBody since formData() consumes the body
98
+ this.rawBody = await request.clone().arrayBuffer();
99
+ this.setBody(await clonedRequest.formData());
100
+ return;
101
+ }
102
+ // For all other content types, consume body once as ArrayBuffer from clone
103
+ this.rawBody = await clonedRequest.arrayBuffer();
92
104
  if (contentType.includes('application/json')) {
93
- this.body = await request.clone().json();
105
+ this.body = this.parseJsonFromBuffer(this.rawBody);
94
106
  return;
95
107
  }
96
- if (contentType.includes('application/x-www-form-urlencoded') || contentType.includes('multipart/form-data')) {
97
- this.setBody(await request.clone().formData());
108
+ if (contentType.includes('application/x-www-form-urlencoded')) {
109
+ this.body = this.parseUrlEncodedFromBuffer(this.rawBody);
98
110
  return;
99
111
  }
100
- this.body = { body: await request.clone().text() };
112
+ // Plain text or unknown content type
113
+ this.body = { body: this.decodeBuffer(this.rawBody) };
114
+ }
115
+ parseJsonFromBuffer(buffer) {
116
+ if (this.isEmptyBuffer(buffer)) {
117
+ return {};
118
+ }
119
+ return this.parseJsonText(this.decodeBuffer(buffer));
120
+ }
121
+ parseJsonText(text) {
122
+ try {
123
+ return JSON.parse(text);
124
+ }
125
+ catch {
126
+ throw new HttpException_1.HttpException("Invalid JSON body", http_code_enum_1.HttpCode.BAD_REQUEST);
127
+ }
128
+ }
129
+ isEmptyBuffer(buffer) {
130
+ return buffer.byteLength === 0;
131
+ }
132
+ parseUrlEncodedFromBuffer(buffer) {
133
+ if (buffer.byteLength === 0) {
134
+ return {};
135
+ }
136
+ const text = this.decodeBuffer(buffer);
137
+ return Object.fromEntries(new URLSearchParams(text));
138
+ }
139
+ decodeBuffer(buffer) {
140
+ return new TextDecoder().decode(buffer);
101
141
  }
102
142
  };
103
143
  exports.Context = Context;
@@ -0,0 +1,15 @@
1
+ import { CorsConfig } from './cors-config';
2
+ export declare class CorsHeadersCache {
3
+ private readonly config;
4
+ private readonly cache;
5
+ private readonly methodsString;
6
+ private readonly allowedHeadersString;
7
+ private readonly exposedHeadersString;
8
+ private readonly maxAgeString;
9
+ private readonly hasCredentials;
10
+ private readonly isWildcard;
11
+ constructor(config: CorsConfig);
12
+ get(origin: string): Record<string, string>;
13
+ private buildHeaders;
14
+ applyToResponse(response: Response, origin: string): Response;
15
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CorsHeadersCache = void 0;
4
+ const cors_config_1 = require("./cors-config");
5
+ class CorsHeadersCache {
6
+ constructor(config) {
7
+ this.config = config;
8
+ this.cache = new Map();
9
+ const methods = config.methods || cors_config_1.DEFAULT_CORS_METHODS;
10
+ this.methodsString = methods.join(', ');
11
+ const allowedHeaders = config.allowedHeaders || cors_config_1.DEFAULT_CORS_ALLOWED_HEADERS;
12
+ this.allowedHeadersString = allowedHeaders.join(', ');
13
+ this.exposedHeadersString = config.exposedHeaders?.length
14
+ ? config.exposedHeaders.join(', ')
15
+ : null;
16
+ this.maxAgeString = config.maxAge !== undefined
17
+ ? config.maxAge.toString()
18
+ : null;
19
+ this.hasCredentials = !!config.credentials;
20
+ this.isWildcard = config.origins === '*';
21
+ }
22
+ get(origin) {
23
+ const cacheKey = this.isWildcard ? '*' : origin;
24
+ let cached = this.cache.get(cacheKey);
25
+ if (cached) {
26
+ return cached;
27
+ }
28
+ cached = this.buildHeaders(origin);
29
+ this.cache.set(cacheKey, cached);
30
+ return cached;
31
+ }
32
+ buildHeaders(origin) {
33
+ const headers = {
34
+ 'Access-Control-Allow-Origin': this.isWildcard ? '*' : origin,
35
+ 'Access-Control-Allow-Methods': this.methodsString,
36
+ 'Access-Control-Allow-Headers': this.allowedHeadersString,
37
+ };
38
+ if (this.hasCredentials) {
39
+ headers['Access-Control-Allow-Credentials'] = 'true';
40
+ }
41
+ if (this.exposedHeadersString) {
42
+ headers['Access-Control-Expose-Headers'] = this.exposedHeadersString;
43
+ }
44
+ if (this.maxAgeString) {
45
+ headers['Access-Control-Max-Age'] = this.maxAgeString;
46
+ }
47
+ return headers;
48
+ }
49
+ applyToResponse(response, origin) {
50
+ const corsHeaders = this.get(origin);
51
+ for (const key in corsHeaders) {
52
+ response.headers.set(key, corsHeaders[key]);
53
+ }
54
+ return response;
55
+ }
56
+ }
57
+ exports.CorsHeadersCache = CorsHeadersCache;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carno.js/core",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Carno.js is a framework for building web applications object oriented with TypeScript and Bun.sh",
5
5
  "keywords": [
6
6
  "bun",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "git+ssh://git@github.com:mlusca/carno.js.git"
31
+ "url": "git+ssh://git@github.com:carnojs/carno.js.git"
32
32
  },
33
33
  "license": "MIT",
34
34
  "dependencies": {
@@ -51,5 +51,5 @@
51
51
  "publishConfig": {
52
52
  "access": "public"
53
53
  },
54
- "gitHead": "179037d66f41ab7014a008b141afce3c9232190e"
54
+ "gitHead": "91d257751a1386de821cd4dd3252fce3c070cfca"
55
55
  }