skyguard-js 1.1.2 → 1.1.3

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.
Files changed (41) hide show
  1. package/README.md +35 -3
  2. package/dist/app.js +3 -2
  3. package/dist/crypto/jwt.d.ts +26 -12
  4. package/dist/crypto/jwt.js +56 -30
  5. package/dist/http/request.d.ts +12 -0
  6. package/dist/http/request.js +19 -0
  7. package/dist/http/response.d.ts +9 -0
  8. package/dist/http/response.js +27 -0
  9. package/dist/middlewares/cors.d.ts +1 -18
  10. package/dist/middlewares/session.d.ts +38 -16
  11. package/dist/middlewares/session.js +63 -61
  12. package/dist/parsers/jsonParser.js +1 -2
  13. package/dist/parsers/multipartParser.d.ts +1 -1
  14. package/dist/parsers/multipartParser.js +3 -4
  15. package/dist/parsers/parserInterface.d.ts +2 -13
  16. package/dist/parsers/parserInterface.js +0 -14
  17. package/dist/parsers/textParser.js +2 -3
  18. package/dist/parsers/urlEncodedParser.js +1 -2
  19. package/dist/parsers/xmlParser.js +2 -3
  20. package/dist/routing/layer.js +5 -1
  21. package/dist/sessions/cookies.d.ts +120 -0
  22. package/dist/sessions/cookies.js +92 -0
  23. package/dist/sessions/fileSessionStorage.d.ts +133 -81
  24. package/dist/sessions/fileSessionStorage.js +199 -135
  25. package/dist/sessions/index.d.ts +1 -1
  26. package/dist/sessions/index.js +4 -1
  27. package/dist/sessions/memorySessionStorage.d.ts +108 -59
  28. package/dist/sessions/memorySessionStorage.js +156 -73
  29. package/dist/sessions/session.d.ts +11 -71
  30. package/dist/sessions/session.js +23 -72
  31. package/dist/sessions/sessionStorage.d.ts +24 -65
  32. package/dist/sessions/sessionStorage.js +3 -0
  33. package/dist/static/fileStaticHandler.js +1 -1
  34. package/dist/storage/storage.d.ts +17 -31
  35. package/dist/storage/storage.js +102 -35
  36. package/dist/storage/types.d.ts +23 -6
  37. package/dist/storage/uploader.js +24 -8
  38. package/dist/views/engineTemplate.js +4 -3
  39. package/package.json +1 -1
  40. package/dist/sessions/cookieOptions.d.ts +0 -72
  41. package/dist/sessions/cookieOptions.js +0 -2
package/README.md CHANGED
@@ -242,7 +242,20 @@ To handle sessions, you must use the framework’s built-in middleware. Dependin
242
242
  import { sessions } from "skyguard-js/middlewares";
243
243
  import { FileSessionStorage } from "skyguard-js";
244
244
 
245
- app.middlewares([sessions(FileSessionStorage)]);
245
+ app.middlewares([
246
+ sessions(FileSessionStorage, {
247
+ name: "connect.sid",
248
+ rolling: true,
249
+ saveUninitialized: false,
250
+ cookie: {
251
+ maxAge: 60 * 60 * 24,
252
+ httpOnly: true,
253
+ sameSite: "Lax",
254
+ secure: false,
255
+ path: "/",
256
+ },
257
+ }),
258
+ ]);
246
259
 
247
260
  app.post("/login", (request: Request) => {
248
261
  const { username, password } = request.data;
@@ -300,10 +313,25 @@ app.post("/login", async (request: Request) => {
300
313
  throw new UnauthorizedError("Invalid credentials");
301
314
  }
302
315
 
303
- const token = createJWT({ sub: user.id, role: user.role }, "1h");
316
+ const token = createJWT(
317
+ { sub: user.id, role: user.role },
318
+ "secret-key",
319
+ 3600,
320
+ );
304
321
 
305
322
  return Response.json({ token });
306
323
  });
324
+
325
+ app.get(
326
+ "/verify-jwt",
327
+ (request: Request) => {
328
+ // The request stores a user state; the middleware creates and assigns this state to the request.state object.
329
+ const user = request.state.user;
330
+
331
+ return json({ user });
332
+ },
333
+ [authJWT("secret-key")],
334
+ );
307
335
  ```
308
336
 
309
337
  ---
@@ -318,7 +346,9 @@ import { createUploader, StorageType } from "skyguard-js";
318
346
  const uploader = createUploader({
319
347
  storageType: StorageType.DISK,
320
348
  storageOptions: {
321
- destination: "./uploads",
349
+ disk: {
350
+ destination: "./uploads",
351
+ },
322
352
  },
323
353
  });
324
354
 
@@ -334,6 +364,8 @@ app.post(
334
364
  );
335
365
  ```
336
366
 
367
+ Depending on the `Storage Type` you have selected, the storage options will contain two properties: `disk` and `memory`
368
+
337
369
  ---
338
370
 
339
371
  ## 📄 Views & Template Engine
package/dist/app.js CHANGED
@@ -50,8 +50,8 @@ class App {
50
50
  */
51
51
  static bootstrap() {
52
52
  const app = container_1.Container.singleton(App);
53
- app.router = new routing_1.Router();
54
- app.logger = new http_1.Logger();
53
+ app.router = container_1.Container.singleton(routing_1.Router);
54
+ app.logger = container_1.Container.singleton(http_1.Logger);
55
55
  app.viewEngine = container_1.Container.singleton(engineTemplate_1.ViewEngine);
56
56
  return app;
57
57
  }
@@ -228,6 +228,7 @@ class App {
228
228
  statusCode: 500,
229
229
  message: "Internal Server Error",
230
230
  }).setStatusCode(500));
231
+ // eslint-disable-next-line no-console
231
232
  console.error(error);
232
233
  }
233
234
  }
@@ -8,24 +8,38 @@ interface JWTHeader {
8
8
  typ: string;
9
9
  }
10
10
  /**
11
- * Crea un JSON Web Token
12
- * @param payload - Datos a incluir en el token
13
- * @param secret - Clave secreta para firmar
14
- * @param expiresIn - Tiempo de expiración en segundos (opcional)
15
- * @returns JWT string
11
+ * Creates a JSON Web Token (JWT) signed using HMAC-SHA256 (HS256).
12
+ *
13
+ * Note:
14
+ * This implementation produces **stateless signed tokens** and does not
15
+ * encrypt the payload. Anyone can decode the payload, but only holders of
16
+ * the secret can generate a valid signature.
17
+ *
18
+ * @param payload - Custom claims to include in the token.
19
+ * @param secret - Secret key used to sign the token.
20
+ * @param expiresIn - Optional expiration time in seconds.
21
+ * @returns Signed JWT string.
16
22
  */
17
23
  export declare const createJWT: (payload: JWTPayload, secret: string, expiresIn?: number) => string;
18
24
  /**
19
- * Verifica y decodifica un JSON Web Token
20
- * @param token - JWT a verificar
21
- * @param secret - Clave secreta usada para firmar
22
- * @returns Payload decodificado o null si es inválido
25
+ * Verifies the integrity and validity of a JSON Web Token.
26
+ *
27
+ * If any validation step fails, the function returns `null`.
28
+ *
29
+ * @param token - JWT string to verify.
30
+ * @param secret - Secret used to sign the token.
31
+ * @returns Decoded payload if the token is valid, otherwise `null`.
23
32
  */
24
33
  export declare const verifyJWT: (token: string, secret: string) => JWTPayload | null;
25
34
  /**
26
- * Decodifica un JWT sin verificar la firma (útil para debugging)
27
- * @param token - JWT a decodificar
28
- * @returns Objeto con header y payload decodificados
35
+ * Decodes a JWT without verifying its signature.
36
+ *
37
+ * ⚠️ Security warning:
38
+ * This function MUST NOT be used for authentication or authorization.
39
+ * It is intended only for debugging, logging, or inspecting token contents.
40
+ *
41
+ * @param token - JWT string to decode.
42
+ * @returns Object containing decoded header and payload, or `null` if malformed.
29
43
  */
30
44
  export declare const decodeJWT: (token: string) => {
31
45
  header: JWTHeader;
@@ -3,7 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.decodeJWT = exports.verifyJWT = exports.createJWT = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  /**
6
- * Codifica en Base64URL (sin padding)
6
+ * Encodes a string using Base64URL encoding (RFC 7515).
7
+ *
8
+ * This format is required by JWT to ensure the token is URL-safe.
9
+ *
10
+ * @param str - UTF-8 string to encode.
11
+ * @returns Base64URL encoded string without padding.
7
12
  */
8
13
  function base64UrlEncode(str) {
9
14
  return Buffer.from(str)
@@ -13,7 +18,13 @@ function base64UrlEncode(str) {
13
18
  .replace(/=/g, "");
14
19
  }
15
20
  /**
16
- * Decodifica desde Base64URL
21
+ * Decodes a Base64URL string back to a UTF-8 string.
22
+ *
23
+ * The function restores the standard Base64 alphabet and padding
24
+ * before performing the decoding.
25
+ *
26
+ * @param str - Base64URL encoded string.
27
+ * @returns Decoded UTF-8 string.
17
28
  */
18
29
  function base64UrlDecode(str) {
19
30
  let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
@@ -23,11 +34,17 @@ function base64UrlDecode(str) {
23
34
  return Buffer.from(base64, "base64").toString("utf-8");
24
35
  }
25
36
  /**
26
- * Crea un JSON Web Token
27
- * @param payload - Datos a incluir en el token
28
- * @param secret - Clave secreta para firmar
29
- * @param expiresIn - Tiempo de expiración en segundos (opcional)
30
- * @returns JWT string
37
+ * Creates a JSON Web Token (JWT) signed using HMAC-SHA256 (HS256).
38
+ *
39
+ * Note:
40
+ * This implementation produces **stateless signed tokens** and does not
41
+ * encrypt the payload. Anyone can decode the payload, but only holders of
42
+ * the secret can generate a valid signature.
43
+ *
44
+ * @param payload - Custom claims to include in the token.
45
+ * @param secret - Secret key used to sign the token.
46
+ * @param expiresIn - Optional expiration time in seconds.
47
+ * @returns Signed JWT string.
31
48
  */
32
49
  const createJWT = (payload, secret, expiresIn) => {
33
50
  const header = {
@@ -50,33 +67,37 @@ const createJWT = (payload, secret, expiresIn) => {
50
67
  .replace(/\+/g, "-")
51
68
  .replace(/\//g, "_")
52
69
  .replace(/=/g, "");
53
- // Retornar JWT completo
54
70
  return `${data}.${signature}`;
55
71
  };
56
72
  exports.createJWT = createJWT;
57
73
  /**
58
- * Verifica y decodifica un JSON Web Token
59
- * @param token - JWT a verificar
60
- * @param secret - Clave secreta usada para firmar
61
- * @returns Payload decodificado o null si es inválido
74
+ * Verifies the integrity and validity of a JSON Web Token.
75
+ *
76
+ * If any validation step fails, the function returns `null`.
77
+ *
78
+ * @param token - JWT string to verify.
79
+ * @param secret - Secret used to sign the token.
80
+ * @returns Decoded payload if the token is valid, otherwise `null`.
62
81
  */
63
82
  const verifyJWT = (token, secret) => {
64
- const parts = token.split(".");
65
- if (parts.length !== 3)
66
- return null;
67
- const [encodedHeader, encodedPayload, signature] = parts;
68
- const data = `${encodedHeader}.${encodedPayload}`;
69
- const expectedSignature = (0, node_crypto_1.createHmac)("sha256", secret)
70
- .update(data)
71
- .digest("base64")
72
- .replace(/\+/g, "-")
73
- .replace(/\//g, "_")
74
- .replace(/=/g, "");
75
- const signatureBuffer = Buffer.from(signature);
76
- const computedBuffer = Buffer.from(expectedSignature);
77
- if (!(0, node_crypto_1.timingSafeEqual)(signatureBuffer, computedBuffer))
78
- return null;
79
83
  try {
84
+ const parts = token.split(".");
85
+ if (parts.length !== 3)
86
+ return null;
87
+ const [encodedHeader, encodedPayload, signature] = parts;
88
+ const data = `${encodedHeader}.${encodedPayload}`;
89
+ const expectedSignature = (0, node_crypto_1.createHmac)("sha256", secret)
90
+ .update(data)
91
+ .digest("base64")
92
+ .replace(/\+/g, "-")
93
+ .replace(/\//g, "_")
94
+ .replace(/=/g, "");
95
+ const signatureBuffer = Buffer.from(signature);
96
+ const computedBuffer = Buffer.from(expectedSignature);
97
+ if (signatureBuffer.length !== computedBuffer.length)
98
+ return null;
99
+ if (!(0, node_crypto_1.timingSafeEqual)(signatureBuffer, computedBuffer))
100
+ return null;
80
101
  const payload = JSON.parse(base64UrlDecode(encodedPayload));
81
102
  if (!payload.exp)
82
103
  return null;
@@ -91,9 +112,14 @@ const verifyJWT = (token, secret) => {
91
112
  };
92
113
  exports.verifyJWT = verifyJWT;
93
114
  /**
94
- * Decodifica un JWT sin verificar la firma (útil para debugging)
95
- * @param token - JWT a decodificar
96
- * @returns Objeto con header y payload decodificados
115
+ * Decodes a JWT without verifying its signature.
116
+ *
117
+ * ⚠️ Security warning:
118
+ * This function MUST NOT be used for authentication or authorization.
119
+ * It is intended only for debugging, logging, or inspecting token contents.
120
+ *
121
+ * @param token - JWT string to decode.
122
+ * @returns Object containing decoded header and payload, or `null` if malformed.
97
123
  */
98
124
  const decodeJWT = (token) => {
99
125
  try {
@@ -55,6 +55,18 @@ export declare class Request {
55
55
  setData(data: Record<string, any>): this;
56
56
  get session(): Session;
57
57
  setSession(session: Session): void;
58
+ /**
59
+ * Returns all request cookies as a key-value object.
60
+ */
61
+ get cookies(): Record<string, string>;
62
+ /**
63
+ * Returns a cookie value by name.
64
+ */
65
+ getCookie(name: string): string | undefined;
66
+ /**
67
+ * Checks whether a cookie exists.
68
+ */
69
+ hasCookie(name: string): boolean;
58
70
  /**
59
71
  * Validates the request payload against a validation schema.
60
72
  *
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Request = void 0;
4
4
  const validators_1 = require("../validators");
5
+ const cookies_1 = require("../sessions/cookies");
5
6
  /**
6
7
  * Represents an incoming client request within the framework.
7
8
  *
@@ -87,6 +88,24 @@ class Request {
87
88
  setSession(session) {
88
89
  this._session = session;
89
90
  }
91
+ /**
92
+ * Returns all request cookies as a key-value object.
93
+ */
94
+ get cookies() {
95
+ return (0, cookies_1.parseCookies)(this._headers?.cookie);
96
+ }
97
+ /**
98
+ * Returns a cookie value by name.
99
+ */
100
+ getCookie(name) {
101
+ return this.cookies[name];
102
+ }
103
+ /**
104
+ * Checks whether a cookie exists.
105
+ */
106
+ hasCookie(name) {
107
+ return name in this.cookies;
108
+ }
90
109
  /**
91
110
  * Validates the request payload against a validation schema.
92
111
  *
@@ -1,4 +1,5 @@
1
1
  import type { Headers } from "../types";
2
+ import { type CookieOptions } from "../sessions/cookies";
2
3
  /**
3
4
  * Represents an outgoing response sent to the client.
4
5
  *
@@ -35,6 +36,14 @@ export declare class Response {
35
36
  private merge;
36
37
  setHeader(header: string, value: string): this;
37
38
  removeHeader(header: string): void;
39
+ /**
40
+ * Sets a cookie in the `Set-Cookie` response header.
41
+ */
42
+ setCookie(name: string, value: string, options?: CookieOptions): this;
43
+ /**
44
+ * Clears a cookie by setting an empty value and immediate expiration.
45
+ */
46
+ removeCookie(name: string, options?: CookieOptions): this;
38
47
  /**
39
48
  * Semantic shortcut to define the `Content-Type` header.
40
49
  *
@@ -6,6 +6,7 @@ const invalidHttpStatusException_1 = require("../exceptions/invalidHttpStatusExc
6
6
  const fileDownload_1 = require("../static/fileDownload");
7
7
  const engineTemplate_1 = require("../views/engineTemplate");
8
8
  const container_1 = require("../container/container");
9
+ const cookies_1 = require("../sessions/cookies");
9
10
  /**
10
11
  * Represents an outgoing response sent to the client.
11
12
  *
@@ -62,6 +63,32 @@ class Response {
62
63
  removeHeader(header) {
63
64
  delete this._headers[header];
64
65
  }
66
+ /**
67
+ * Sets a cookie in the `Set-Cookie` response header.
68
+ */
69
+ setCookie(name, value, options = {}) {
70
+ const cookie = (0, cookies_1.serializeCookie)(name, value, options);
71
+ const current = this._headers["Set-Cookie"];
72
+ if (!current) {
73
+ this._headers["Set-Cookie"] = cookie;
74
+ return this;
75
+ }
76
+ if (Array.isArray(current)) {
77
+ this._headers["Set-Cookie"] = [...current, cookie];
78
+ return this;
79
+ }
80
+ this._headers["Set-Cookie"] = [current, cookie];
81
+ return this;
82
+ }
83
+ /**
84
+ * Clears a cookie by setting an empty value and immediate expiration.
85
+ */
86
+ removeCookie(name, options = {}) {
87
+ return this.setCookie(name, "", {
88
+ ...options,
89
+ maxAge: 0,
90
+ });
91
+ }
65
92
  /**
66
93
  * Semantic shortcut to define the `Content-Type` header.
67
94
  *
@@ -1,22 +1,5 @@
1
1
  import { HttpMethods } from "../http";
2
2
  import type { Middleware } from "../types";
3
- /**
4
- * CORS origin matcher type.
5
- *
6
- * Controls how the middleware determines the value of
7
- * `Access-Control-Allow-Origin` for a given request.
8
- *
9
- * Supported forms:
10
- * - `string`: a single allowed origin (e.g. `"https://example.com"`) or `"*"` for public access
11
- * - `string[]`: a whitelist of allowed origins
12
- * - `function`: a custom resolver that receives the request Origin and decides what to allow
13
- *
14
- * Notes:
15
- * - If `credentials: true` is enabled, `"*"` cannot be used as the final
16
- * `Access-Control-Allow-Origin` value. In that case this middleware will
17
- * reflect the request origin when allowed.
18
- */
19
- type CorsOrigin = string | string[] | ((origin: string | undefined) => string);
20
3
  /**
21
4
  * CORS middleware configuration options.
22
5
  *
@@ -33,7 +16,7 @@ interface CorsOptions {
33
16
  *
34
17
  * Affects: `Access-Control-Allow-Origin`.
35
18
  */
36
- origin?: CorsOrigin;
19
+ origin?: string | string[] | ((origin: string | undefined) => string);
37
20
  /**
38
21
  * HTTP methods allowed for cross-origin requests.
39
22
  *
@@ -1,26 +1,48 @@
1
- import { type SessionStorage, type CookieOptions } from "../sessions";
1
+ import { type SessionStorage } from "../sessions";
2
+ import { type CookieOptions } from "../sessions/cookies";
2
3
  import type { Middleware } from "../types";
3
4
  /**
4
5
  * Constructor type for `SessionStorage` implementations.
5
- *
6
- * Allows injecting different session storage strategies
7
- * (memory, file, redis, etc.).
8
6
  */
9
7
  type SessionStorageConstructor<T extends SessionStorage = SessionStorage> = new (...args: any[]) => T;
10
8
  /**
11
- * Session lifecycle middleware implementation.
9
+ * Options inspired by express-session and adapted to Skyguard.
10
+ */
11
+ export interface SessionOptions {
12
+ /**
13
+ * Name of the cookie that carries the session id.
14
+ * @default "connect.sid"
15
+ */
16
+ name?: string;
17
+ /**
18
+ * Enables issuing a fresh session cookie on every response.
19
+ * @default false
20
+ */
21
+ rolling?: boolean;
22
+ /**
23
+ * Save a cookie even if the session was never initialized with data.
24
+ * @default false
25
+ */
26
+ saveUninitialized?: boolean;
27
+ /**
28
+ * Session cookie attributes.
29
+ */
30
+ cookie?: CookieOptions;
31
+ }
32
+ /**
33
+ * Session lifecycle middleware.
34
+ *
35
+ * API inspired by `express-session`, adapted to the Skyguard architecture.
12
36
  *
13
- * @param StorageClass - `SessionStorage` implementation
14
- * @param options - Session cookie configuration
15
- * @returns Session middleware
37
+ * Cookie emission rules:
38
+ * - A cookie is set when a session id exists AND at least one of the following is true:
39
+ * - `rolling` is enabled (refresh cookie every request),
40
+ * - `saveUninitialized` is enabled (always create/set cookie when no prior session),
41
+ * - the session was created during this request.
16
42
  *
17
- * @example
18
- * app.middlewares([
19
- * sessionMiddleware(MemorySessionStorage, {
20
- * cookieName: "sid",
21
- * maxAge: 86400,
22
- * }),
23
- * ]);
43
+ * @param StorageClass - Session storage constructor used per request.
44
+ * @param options - Raw session middleware options.
45
+ * @returns A Skyguard `Middleware` that manages session load/save and cookie IO.
24
46
  */
25
- export declare const sessions: (StorageClass: SessionStorageConstructor, options?: CookieOptions) => Middleware;
47
+ export declare const sessions: (StorageClass: SessionStorageConstructor, options?: SessionOptions) => Middleware;
26
48
  export {};
@@ -2,85 +2,87 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sessions = void 0;
4
4
  const sessions_1 = require("../sessions");
5
+ const cookies_1 = require("../sessions/cookies");
6
+ const httpExceptions_1 = require("../exceptions/httpExceptions");
5
7
  /**
6
- * Parses the `Cookie` header into a key/value object.
8
+ * Attempts to load a session from a cookie value into the provided storage.
7
9
  *
8
- * @param cookieHeader - Raw `Cookie` header value
9
- * @returns Parsed cookies
10
+ * If `cookieValue` is missing, this is a no-op.
10
11
  *
11
- * @example
12
- * parseCookies("foo=bar; session_id=abc123");
13
- * // { foo: "bar", session_id: "abc123" }
14
- */
15
- function parseCookies(cookieHeader) {
16
- if (!cookieHeader)
17
- return {};
18
- return Object.fromEntries(cookieHeader.split(";").map(cookie => {
19
- const [key, ...value] = cookie.trim().split("=");
20
- return [key, decodeURIComponent(value.join("="))];
21
- }));
22
- }
23
- /**
24
- * Builds the `Set-Cookie` header value for the session.
12
+ * If the storage rejects with `UnauthorizedError` (e.g., invalid or expired session),
13
+ * the error is swallowed and the request proceeds as unauthenticated/uninitialized.
25
14
  *
26
- * @param sessionId - Session identifier
27
- * @param config - Fully resolved cookie configuration
28
- * @returns Value ready to be used in `Set-Cookie`
15
+ * Any other error type is treated as unexpected and is re-thrown.
29
16
  *
30
- * @example
31
- * buildSessionCookie("abc123", config);
32
- * // "session_id=abc123; Max-Age=86400; Path=/; SameSite=Lax; HttpOnly"
17
+ * @param storage - Session storage instance used to load the session.
18
+ * @param cookieValue - Session id extracted from the request cookie.
19
+ * @throws Unknown errors coming from the storage layer (non-UnauthorizedError).
33
20
  */
34
- function buildSessionCookie(sessionId, config) {
35
- const parts = [
36
- `${config.cookieName}=${encodeURIComponent(sessionId)}`,
37
- `Max-Age=${config.maxAge}`,
38
- `Path=${config.path}`,
39
- `SameSite=${config.sameSite}`,
40
- ];
41
- if (config.httpOnly)
42
- parts.push("HttpOnly");
43
- if (config.secure)
44
- parts.push("Secure");
45
- return parts.join("; ");
21
+ async function loadSessionFromCookie(storage, cookieValue) {
22
+ if (!cookieValue)
23
+ return;
24
+ try {
25
+ await storage.load(cookieValue);
26
+ }
27
+ catch (error) {
28
+ if (error instanceof httpExceptions_1.UnauthorizedError)
29
+ return;
30
+ throw error;
31
+ }
46
32
  }
47
33
  /**
48
- * Session lifecycle middleware implementation.
34
+ * Session lifecycle middleware.
35
+ *
36
+ * API inspired by `express-session`, adapted to the Skyguard architecture.
49
37
  *
50
- * @param StorageClass - `SessionStorage` implementation
51
- * @param options - Session cookie configuration
52
- * @returns Session middleware
38
+ * Cookie emission rules:
39
+ * - A cookie is set when a session id exists AND at least one of the following is true:
40
+ * - `rolling` is enabled (refresh cookie every request),
41
+ * - `saveUninitialized` is enabled (always create/set cookie when no prior session),
42
+ * - the session was created during this request.
53
43
  *
54
- * @example
55
- * app.middlewares([
56
- * sessionMiddleware(MemorySessionStorage, {
57
- * cookieName: "sid",
58
- * maxAge: 86400,
59
- * }),
60
- * ]);
44
+ * @param StorageClass - Session storage constructor used per request.
45
+ * @param options - Raw session middleware options.
46
+ * @returns A Skyguard `Middleware` that manages session load/save and cookie IO.
61
47
  */
62
48
  const sessions = (StorageClass, options = {}) => {
63
- const timeMaxAge = 86400;
64
49
  const config = {
65
- cookieName: options.cookieName ?? "session_id",
66
- maxAge: options.maxAge ?? timeMaxAge,
67
- httpOnly: options.httpOnly ?? true,
68
- secure: options.secure ?? false,
69
- sameSite: options.sameSite ?? "Lax",
70
- path: options.path ?? "/",
50
+ name: options.name ?? "connect.sid",
51
+ rolling: options.rolling ?? false,
52
+ saveUninitialized: options.saveUninitialized ?? false,
53
+ cookie: {
54
+ maxAge: options.cookie?.maxAge ?? 86400,
55
+ httpOnly: options.cookie?.httpOnly ?? true,
56
+ secure: options.cookie?.secure ?? false,
57
+ sameSite: options.cookie?.sameSite ?? "Lax",
58
+ path: options.cookie?.path ?? "/",
59
+ },
71
60
  };
72
61
  return async (request, next) => {
73
- const cookies = parseCookies(request.headers.cookie || "");
74
- const sessionId = cookies[config.cookieName];
75
- const storage = new StorageClass(options.maxAge || timeMaxAge);
76
- if (sessionId)
77
- await storage.load(sessionId);
62
+ const cookies = (0, cookies_1.parseCookies)(request.headers.cookie);
63
+ const sessionIdFromCookie = cookies[config.name];
64
+ const storage = new StorageClass(config.cookie.maxAge);
65
+ await loadSessionFromCookie(storage, sessionIdFromCookie);
78
66
  const session = new sessions_1.Session(storage);
79
67
  request.setSession(session);
68
+ const sessionIdBefore = storage.id();
69
+ if (!sessionIdBefore && config.saveUninitialized)
70
+ await storage.start();
80
71
  const response = await next(request);
81
- if (storage.id()) {
82
- const cookieValue = buildSessionCookie(storage.id(), config);
83
- response.setHeader("Set-Cookie", cookieValue);
72
+ const sessionIdAfter = storage.id();
73
+ if (sessionIdAfter && config.rolling)
74
+ await storage.touch();
75
+ const sessionWasCreated = !sessionIdBefore && Boolean(sessionIdAfter);
76
+ const shouldSetCookie = Boolean(sessionIdAfter) &&
77
+ (config.rolling || config.saveUninitialized || sessionWasCreated);
78
+ if (shouldSetCookie && sessionIdAfter) {
79
+ response.setHeader("Set-Cookie", (0, cookies_1.serializeCookie)(config.name, sessionIdAfter, {
80
+ maxAge: config.cookie.maxAge,
81
+ path: config.cookie.path,
82
+ httpOnly: config.cookie.httpOnly,
83
+ secure: config.cookie.secure,
84
+ sameSite: config.cookie.sameSite,
85
+ }));
84
86
  }
85
87
  return response;
86
88
  };
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.JsonParser = void 0;
4
4
  const httpExceptions_1 = require("../exceptions/httpExceptions");
5
- const parserInterface_1 = require("./parserInterface");
6
5
  /**
7
6
  * JSON content parser.
8
7
  *
@@ -10,7 +9,7 @@ const parserInterface_1 = require("./parserInterface");
10
9
  */
11
10
  class JsonParser {
12
11
  canParse(contentType) {
13
- return contentType.includes(parserInterface_1.contentTypes["application-json"]);
12
+ return contentType.includes("application/json");
14
13
  }
15
14
  parse(body) {
16
15
  try {
@@ -1,5 +1,5 @@
1
1
  import type { ContentParser } from "./contentParser";
2
- import { type MultipartData } from "./parserInterface";
2
+ import type { MultipartData } from "./parserInterface";
3
3
  /**
4
4
  * `multipart/form-data` content parser.
5
5
  *