skyguard-js 1.2.1 → 1.2.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.
- package/README.md +11 -680
- package/dist/app.d.ts +14 -9
- package/dist/app.js +37 -30
- package/dist/http/context.d.ts +115 -0
- package/dist/http/context.js +147 -0
- package/dist/http/httpAdapter.d.ts +4 -4
- package/dist/http/index.d.ts +3 -1
- package/dist/http/index.js +5 -1
- package/dist/http/logger.d.ts +9 -2
- package/dist/http/logger.js +49 -1
- package/dist/http/nodeNativeHttp.d.ts +4 -4
- package/dist/http/nodeNativeHttp.js +11 -4
- package/dist/http/request.d.ts +4 -0
- package/dist/http/request.js +8 -0
- package/dist/http/response.d.ts +21 -2
- package/dist/http/response.js +30 -2
- package/dist/http/webHttp.d.ts +19 -0
- package/dist/http/webHttp.js +95 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/middlewares/cors.d.ts +10 -4
- package/dist/middlewares/cors.js +35 -18
- package/dist/middlewares/csrf.js +33 -33
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/rateLimiter.d.ts +58 -6
- package/dist/middlewares/rateLimiter.js +149 -40
- package/dist/middlewares/session.js +4 -4
- package/dist/parsers/contentParserManager.d.ts +4 -2
- package/dist/parsers/contentParserManager.js +22 -3
- package/dist/routing/routeResolveFunc.d.ts +10 -0
- package/dist/routing/routeResolveFunc.js +21 -0
- package/dist/routing/router.d.ts +16 -10
- package/dist/routing/router.js +32 -25
- package/dist/routing/routerGroup.d.ts +10 -5
- package/dist/routing/routerGroup.js +11 -10
- package/dist/server/bunRuntimeServer.d.ts +7 -0
- package/dist/server/bunRuntimeServer.js +28 -0
- package/dist/server/createRuntimeServer.d.ts +4 -0
- package/dist/server/createRuntimeServer.js +23 -0
- package/dist/server/denoRuntimeServer.d.ts +7 -0
- package/dist/server/denoRuntimeServer.js +27 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.js +19 -0
- package/dist/server/modulePath.d.ts +6 -0
- package/dist/server/modulePath.js +18 -0
- package/dist/server/nodeRuntimeServer.d.ts +7 -0
- package/dist/server/nodeRuntimeServer.js +21 -0
- package/dist/server/runtimeDetector.d.ts +4 -0
- package/dist/server/runtimeDetector.js +15 -0
- package/dist/server/types.d.ts +11 -0
- package/dist/server/types.js +2 -0
- package/dist/sessions/index.d.ts +2 -2
- package/dist/storage/storage.d.ts +3 -3
- package/dist/storage/storage.js +7 -7
- package/dist/storage/types.d.ts +5 -5
- package/dist/storage/uploader.d.ts +9 -9
- package/dist/storage/uploader.js +62 -62
- package/dist/types/index.d.ts +11 -10
- package/dist/validators/index.d.ts +2 -2
- package/dist/validators/rules/index.d.ts +5 -5
- package/dist/validators/validationSchema.js +8 -8
- package/package.json +2 -2
- package/dist/helpers/http.d.ts +0 -95
- package/dist/helpers/http.js +0 -112
package/dist/http/request.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export declare class Request {
|
|
|
29
29
|
private _params;
|
|
30
30
|
/** Session associated with the request */
|
|
31
31
|
private _session;
|
|
32
|
+
/** Network peer address (adapter-provided). */
|
|
33
|
+
private _remoteAddress?;
|
|
32
34
|
/**
|
|
33
35
|
* Per-request shared state container.
|
|
34
36
|
*
|
|
@@ -52,6 +54,8 @@ export declare class Request {
|
|
|
52
54
|
setBody(body: Record<string, any>): void;
|
|
53
55
|
get session(): Session;
|
|
54
56
|
setSession(session: Session): void;
|
|
57
|
+
get remoteAddress(): string | undefined;
|
|
58
|
+
setRemoteAddress(remoteAddress: string): void;
|
|
55
59
|
/**
|
|
56
60
|
* Returns all request cookies as a key-value object.
|
|
57
61
|
*/
|
package/dist/http/request.js
CHANGED
|
@@ -29,6 +29,8 @@ class Request {
|
|
|
29
29
|
_params = Object.create(null);
|
|
30
30
|
/** Session associated with the request */
|
|
31
31
|
_session;
|
|
32
|
+
/** Network peer address (adapter-provided). */
|
|
33
|
+
_remoteAddress;
|
|
32
34
|
/**
|
|
33
35
|
* Per-request shared state container.
|
|
34
36
|
*
|
|
@@ -80,6 +82,12 @@ class Request {
|
|
|
80
82
|
setSession(session) {
|
|
81
83
|
this._session = session;
|
|
82
84
|
}
|
|
85
|
+
get remoteAddress() {
|
|
86
|
+
return this._remoteAddress;
|
|
87
|
+
}
|
|
88
|
+
setRemoteAddress(remoteAddress) {
|
|
89
|
+
this._remoteAddress = remoteAddress;
|
|
90
|
+
}
|
|
83
91
|
/**
|
|
84
92
|
* Returns all request cookies as a key-value object.
|
|
85
93
|
*/
|
package/dist/http/response.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type CookieOptions } from "../sessions/cookies";
|
|
2
2
|
import { IncomingHttpHeaders } from "node:http";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
3
4
|
/**
|
|
4
5
|
* Represents an outgoing response sent to the client.
|
|
5
6
|
*
|
|
@@ -56,8 +57,18 @@ export declare class Response {
|
|
|
56
57
|
* .setContent("<h1>Hello</h1>");
|
|
57
58
|
*/
|
|
58
59
|
setContentType(value: string): this;
|
|
59
|
-
get content(): string | Buffer | null;
|
|
60
|
-
setContent(content: string | Buffer): this;
|
|
60
|
+
get content(): string | Buffer | Readable | null;
|
|
61
|
+
setContent(content: string | Buffer | Readable): this;
|
|
62
|
+
/**
|
|
63
|
+
* Sets a readable stream as the response body.
|
|
64
|
+
*
|
|
65
|
+
* Use this when you need to send large payloads in chunks without
|
|
66
|
+
* buffering the full content in memory first.
|
|
67
|
+
*
|
|
68
|
+
* @param stream - Node.js readable stream
|
|
69
|
+
* @returns The current {@link Response} instance
|
|
70
|
+
*/
|
|
71
|
+
stream(stream: Readable): this;
|
|
61
72
|
/**
|
|
62
73
|
* Prepares the response before being sent to the client.
|
|
63
74
|
*
|
|
@@ -97,6 +108,14 @@ export declare class Response {
|
|
|
97
108
|
* });
|
|
98
109
|
*/
|
|
99
110
|
static text(data: string): Response;
|
|
111
|
+
/**
|
|
112
|
+
* Creates a response whose body is streamed.
|
|
113
|
+
*
|
|
114
|
+
* @param stream - Node.js readable stream
|
|
115
|
+
* @param headers - Optional headers to include in the response
|
|
116
|
+
* @returns A {@link Response} configured for streaming
|
|
117
|
+
*/
|
|
118
|
+
static stream(stream: Readable, headers?: Record<string, string>): Response;
|
|
100
119
|
/**
|
|
101
120
|
* Creates an HTTP redirect response.
|
|
102
121
|
*
|
package/dist/http/response.js
CHANGED
|
@@ -11,6 +11,7 @@ const promises_1 = require("node:fs/promises");
|
|
|
11
11
|
const node_path_1 = require("node:path");
|
|
12
12
|
const mimeTypes_1 = require("../static/mimeTypes");
|
|
13
13
|
const httpExceptions_1 = require("../exceptions/httpExceptions");
|
|
14
|
+
const node_stream_1 = require("node:stream");
|
|
14
15
|
/**
|
|
15
16
|
* Represents an outgoing response sent to the client.
|
|
16
17
|
*
|
|
@@ -115,6 +116,18 @@ class Response {
|
|
|
115
116
|
this._content = content;
|
|
116
117
|
return this;
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Sets a readable stream as the response body.
|
|
121
|
+
*
|
|
122
|
+
* Use this when you need to send large payloads in chunks without
|
|
123
|
+
* buffering the full content in memory first.
|
|
124
|
+
*
|
|
125
|
+
* @param stream - Node.js readable stream
|
|
126
|
+
* @returns The current {@link Response} instance
|
|
127
|
+
*/
|
|
128
|
+
stream(stream) {
|
|
129
|
+
return this.setContent(stream);
|
|
130
|
+
}
|
|
118
131
|
/**
|
|
119
132
|
* Prepares the response before being sent to the client.
|
|
120
133
|
*
|
|
@@ -131,14 +144,16 @@ class Response {
|
|
|
131
144
|
*/
|
|
132
145
|
prepare() {
|
|
133
146
|
if (!this._headers["content-type"] && this._content) {
|
|
134
|
-
if (Buffer.isBuffer(this._content)) {
|
|
147
|
+
if (Buffer.isBuffer(this._content) || this._content instanceof node_stream_1.Readable) {
|
|
135
148
|
this._headers["content-type"] = "application/octet-stream";
|
|
136
149
|
}
|
|
137
150
|
else {
|
|
138
151
|
this._headers["content-type"] = "text/plain";
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
|
-
if (this._content &&
|
|
154
|
+
if (this._content &&
|
|
155
|
+
!(this._content instanceof node_stream_1.Readable) &&
|
|
156
|
+
!this._headers["content-length"]) {
|
|
142
157
|
const length = Buffer.isBuffer(this._content)
|
|
143
158
|
? this._content.length
|
|
144
159
|
: Buffer.byteLength(this._content, "utf-8");
|
|
@@ -175,6 +190,19 @@ class Response {
|
|
|
175
190
|
static text(data) {
|
|
176
191
|
return new this().setContentType("text/plain").setContent(data);
|
|
177
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Creates a response whose body is streamed.
|
|
195
|
+
*
|
|
196
|
+
* @param stream - Node.js readable stream
|
|
197
|
+
* @param headers - Optional headers to include in the response
|
|
198
|
+
* @returns A {@link Response} configured for streaming
|
|
199
|
+
*/
|
|
200
|
+
static stream(stream, headers) {
|
|
201
|
+
const response = new this().stream(stream);
|
|
202
|
+
if (headers)
|
|
203
|
+
response.setHeaders(headers);
|
|
204
|
+
return response;
|
|
205
|
+
}
|
|
178
206
|
/**
|
|
179
207
|
* Creates an HTTP redirect response.
|
|
180
208
|
*
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HttpAdapter } from "./httpAdapter";
|
|
2
|
+
import { Response } from "./response";
|
|
3
|
+
import { Context } from "./context";
|
|
4
|
+
import { type LoggerOptions } from "./logger";
|
|
5
|
+
type WebRequest = globalThis.Request;
|
|
6
|
+
type WebResponse = globalThis.Response;
|
|
7
|
+
export declare class WebHttpAdapter implements HttpAdapter {
|
|
8
|
+
private readonly req;
|
|
9
|
+
private contentParser;
|
|
10
|
+
private logger;
|
|
11
|
+
private startTimeMs;
|
|
12
|
+
private runtimeResponse;
|
|
13
|
+
constructor(req: WebRequest, loggerOptions?: LoggerOptions);
|
|
14
|
+
getContext(): Promise<Context>;
|
|
15
|
+
sendResponse(response: Response): void;
|
|
16
|
+
toWebResponse(): WebResponse;
|
|
17
|
+
private buildHeaders;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebHttpAdapter = void 0;
|
|
4
|
+
const httpMethods_1 = require("./httpMethods");
|
|
5
|
+
const request_1 = require("./request");
|
|
6
|
+
const context_1 = require("./context");
|
|
7
|
+
const contentParserManager_1 = require("../parsers/contentParserManager");
|
|
8
|
+
const httpExceptions_1 = require("../exceptions/httpExceptions");
|
|
9
|
+
const node_stream_1 = require("node:stream");
|
|
10
|
+
const logger_1 = require("./logger");
|
|
11
|
+
class WebHttpAdapter {
|
|
12
|
+
req;
|
|
13
|
+
contentParser;
|
|
14
|
+
logger;
|
|
15
|
+
startTimeMs;
|
|
16
|
+
runtimeResponse = null;
|
|
17
|
+
constructor(req, loggerOptions = {}) {
|
|
18
|
+
this.req = req;
|
|
19
|
+
this.contentParser = new contentParserManager_1.ContentParserManager();
|
|
20
|
+
this.logger = new logger_1.Logger(loggerOptions);
|
|
21
|
+
this.startTimeMs = performance.now();
|
|
22
|
+
}
|
|
23
|
+
async getContext() {
|
|
24
|
+
const url = new URL(this.req.url);
|
|
25
|
+
const request = new request_1.Request(url.pathname);
|
|
26
|
+
request.setMethod((this.req.method || "GET").toUpperCase() ||
|
|
27
|
+
httpMethods_1.HttpMethods.get);
|
|
28
|
+
request.setQuery(Object.fromEntries(url.searchParams.entries()));
|
|
29
|
+
request.setHeaders(this.buildHeaders());
|
|
30
|
+
if (request.method === httpMethods_1.HttpMethods.post ||
|
|
31
|
+
request.method === httpMethods_1.HttpMethods.put ||
|
|
32
|
+
request.method === httpMethods_1.HttpMethods.patch) {
|
|
33
|
+
const parsedData = await this.contentParser.parse(this.req);
|
|
34
|
+
request.setBody(parsedData);
|
|
35
|
+
}
|
|
36
|
+
return new context_1.Context(request);
|
|
37
|
+
}
|
|
38
|
+
sendResponse(response) {
|
|
39
|
+
response.prepare();
|
|
40
|
+
const headers = new Headers();
|
|
41
|
+
for (const [header, value] of Object.entries(response.headers)) {
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
for (const entry of value) {
|
|
44
|
+
headers.append(header, entry);
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value !== "undefined") {
|
|
49
|
+
headers.set(header, String(value));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!response.content) {
|
|
53
|
+
headers.delete("content-type");
|
|
54
|
+
this.runtimeResponse = new globalThis.Response(null, {
|
|
55
|
+
status: response.statusCode,
|
|
56
|
+
headers,
|
|
57
|
+
});
|
|
58
|
+
this.logger.logWeb(this.req, this.runtimeResponse, this.startTimeMs);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (response.content instanceof node_stream_1.Readable) {
|
|
62
|
+
throw new httpExceptions_1.NotImplementedError("Node.js Readable stream responses are not supported in this runtime");
|
|
63
|
+
}
|
|
64
|
+
const body = typeof response.content === "string"
|
|
65
|
+
? response.content
|
|
66
|
+
: new Uint8Array(response.content);
|
|
67
|
+
this.runtimeResponse = new globalThis.Response(body, {
|
|
68
|
+
status: response.statusCode,
|
|
69
|
+
headers,
|
|
70
|
+
});
|
|
71
|
+
this.logger.logWeb(this.req, this.runtimeResponse, this.startTimeMs);
|
|
72
|
+
}
|
|
73
|
+
toWebResponse() {
|
|
74
|
+
return (this.runtimeResponse ||
|
|
75
|
+
new globalThis.Response("No response generated", {
|
|
76
|
+
status: 500,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
buildHeaders() {
|
|
80
|
+
const headers = Object.create(null);
|
|
81
|
+
for (const [key, value] of this.req.headers.entries()) {
|
|
82
|
+
if (typeof headers[key] === "undefined") {
|
|
83
|
+
headers[key] = value;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(headers[key])) {
|
|
87
|
+
headers[key] = [...headers[key], value];
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
headers[key] = [headers[key], value];
|
|
91
|
+
}
|
|
92
|
+
return headers;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.WebHttpAdapter = WebHttpAdapter;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createApp } from "./app";
|
|
2
|
-
export { Request, Response } from "./http";
|
|
2
|
+
export { Request, Response, Context } from "./http";
|
|
3
3
|
export type { Middleware, RouteHandler } from "./types";
|
|
4
4
|
export { RouterGroup } from "./routing";
|
|
5
5
|
export { FileSessionStorage, MemorySessionStorage, DatabaseSessionStorage, type SessionDatabaseAdapter, } from "./sessions";
|
|
@@ -9,5 +9,5 @@ export { StorageType } from "./storage/types";
|
|
|
9
9
|
export { v, schema, validateRequest } from "./validators/validationSchema";
|
|
10
10
|
export { Hasher, JWT } from "./crypto";
|
|
11
11
|
export { sessions, cors, csrf, rateLimit } from "./middlewares";
|
|
12
|
+
export type { RateLimitStore, RateLimitStoreEntry } from "./middlewares";
|
|
12
13
|
export * from "./exceptions/httpExceptions";
|
|
13
|
-
export * from "./helpers/http";
|
package/dist/index.js
CHANGED
|
@@ -14,12 +14,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.rateLimit = exports.csrf = exports.cors = exports.sessions = exports.JWT = exports.Hasher = exports.validateRequest = exports.schema = exports.v = exports.StorageType = exports.createUploader = exports.HttpMethods = exports.DatabaseSessionStorage = exports.MemorySessionStorage = exports.FileSessionStorage = exports.RouterGroup = exports.Response = exports.Request = exports.createApp = void 0;
|
|
17
|
+
exports.rateLimit = exports.csrf = exports.cors = exports.sessions = exports.JWT = exports.Hasher = exports.validateRequest = exports.schema = exports.v = exports.StorageType = exports.createUploader = exports.HttpMethods = exports.DatabaseSessionStorage = exports.MemorySessionStorage = exports.FileSessionStorage = exports.RouterGroup = exports.Context = exports.Response = exports.Request = exports.createApp = void 0;
|
|
18
18
|
var app_1 = require("./app");
|
|
19
19
|
Object.defineProperty(exports, "createApp", { enumerable: true, get: function () { return app_1.createApp; } });
|
|
20
20
|
var http_1 = require("./http");
|
|
21
21
|
Object.defineProperty(exports, "Request", { enumerable: true, get: function () { return http_1.Request; } });
|
|
22
22
|
Object.defineProperty(exports, "Response", { enumerable: true, get: function () { return http_1.Response; } });
|
|
23
|
+
Object.defineProperty(exports, "Context", { enumerable: true, get: function () { return http_1.Context; } });
|
|
23
24
|
var routing_1 = require("./routing");
|
|
24
25
|
Object.defineProperty(exports, "RouterGroup", { enumerable: true, get: function () { return routing_1.RouterGroup; } });
|
|
25
26
|
var sessions_1 = require("./sessions");
|
|
@@ -45,4 +46,3 @@ Object.defineProperty(exports, "cors", { enumerable: true, get: function () { re
|
|
|
45
46
|
Object.defineProperty(exports, "csrf", { enumerable: true, get: function () { return middlewares_1.csrf; } });
|
|
46
47
|
Object.defineProperty(exports, "rateLimit", { enumerable: true, get: function () { return middlewares_1.rateLimit; } });
|
|
47
48
|
__exportStar(require("./exceptions/httpExceptions"), exports);
|
|
48
|
-
__exportStar(require("./helpers/http"), exports);
|
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import { HttpMethods } from "../http";
|
|
1
|
+
import { Context, HttpMethods } from "../http";
|
|
2
2
|
import type { Middleware } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* Callback contract for dynamic CORS allow/deny decisions.
|
|
5
|
+
*
|
|
6
|
+
* Return `true` to allow the current request origin, `false` to deny it.
|
|
7
|
+
*/
|
|
8
|
+
type CorsOriginResolver = (origin: string | undefined, context: Context) => boolean | Promise<boolean>;
|
|
3
9
|
/**
|
|
4
10
|
* CORS middleware configuration options.
|
|
5
11
|
*
|
|
6
12
|
* These options map to standard CORS response headers and preflight behavior.
|
|
7
13
|
*/
|
|
8
|
-
interface CorsOptions {
|
|
14
|
+
export interface CorsOptions {
|
|
9
15
|
/**
|
|
10
16
|
* Allowed origin(s) for cross-origin requests.
|
|
11
17
|
*
|
|
12
18
|
* - `"*"` allows any origin (public).
|
|
13
19
|
* - A specific origin (string) restricts access to that origin.
|
|
14
20
|
* - An array provides a whitelist.
|
|
15
|
-
* - A function
|
|
21
|
+
* - A function implements custom allow/deny logic.
|
|
16
22
|
*
|
|
17
23
|
* Affects: `Access-Control-Allow-Origin`.
|
|
18
24
|
*/
|
|
19
|
-
origin?: string | string[] |
|
|
25
|
+
origin?: string | string[] | CorsOriginResolver;
|
|
20
26
|
/**
|
|
21
27
|
* HTTP methods allowed for cross-origin requests.
|
|
22
28
|
*
|
package/dist/middlewares/cors.js
CHANGED
|
@@ -2,33 +2,50 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.cors = void 0;
|
|
4
4
|
const http_1 = require("../http");
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes a header that may be represented as `string[]`.
|
|
7
|
+
*
|
|
8
|
+
* @param value - Header value.
|
|
9
|
+
* @returns A single header value or `undefined`.
|
|
10
|
+
*/
|
|
11
|
+
const getHeaderValue = (value) => Array.isArray(value) ? value[0] : value;
|
|
5
12
|
/**
|
|
6
13
|
* Resolves the effective `Access-Control-Allow-Origin` value for a request.
|
|
7
14
|
*
|
|
8
|
-
* @param
|
|
9
|
-
* @
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - `null` if the origin is not allowed (no CORS headers will be applied)
|
|
15
|
+
* @param context - Current HTTP context.
|
|
16
|
+
* @param requestOrigin - Incoming `Origin` header value.
|
|
17
|
+
* @param config - CORS config.
|
|
18
|
+
* @returns Allowed origin value or `null` when denied/not configured.
|
|
13
19
|
*/
|
|
14
|
-
function resolveOrigin(requestOrigin, config) {
|
|
20
|
+
async function resolveOrigin(context, requestOrigin, config) {
|
|
15
21
|
if (!requestOrigin)
|
|
16
22
|
return null;
|
|
17
23
|
const cleanOrigin = (origin) => origin.endsWith("/") ? origin.slice(0, -1) : origin;
|
|
18
24
|
if (typeof config.origin === "function") {
|
|
19
|
-
|
|
25
|
+
const result = config.origin(requestOrigin, context);
|
|
26
|
+
const isAllowed = result instanceof Promise ? await result : result;
|
|
27
|
+
return isAllowed ? requestOrigin : null;
|
|
28
|
+
}
|
|
29
|
+
if (!config.origin) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (typeof config.origin !== "string" && !Array.isArray(config.origin)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (typeof config.origin === "string") {
|
|
36
|
+
if (config.origin === "*") {
|
|
37
|
+
return config.credentials ? requestOrigin : "*";
|
|
38
|
+
}
|
|
39
|
+
return cleanOrigin(config.origin) === cleanOrigin(requestOrigin)
|
|
40
|
+
? requestOrigin
|
|
41
|
+
: null;
|
|
20
42
|
}
|
|
21
43
|
if (Array.isArray(config.origin)) {
|
|
22
44
|
return config.origin.map(cleanOrigin).includes(cleanOrigin(requestOrigin))
|
|
23
45
|
? requestOrigin
|
|
24
46
|
: null;
|
|
25
47
|
}
|
|
26
|
-
|
|
27
|
-
return config.credentials ? requestOrigin : "*";
|
|
28
|
-
}
|
|
29
|
-
return cleanOrigin(config.origin) === cleanOrigin(requestOrigin)
|
|
30
|
-
? requestOrigin
|
|
31
|
-
: null;
|
|
48
|
+
return null;
|
|
32
49
|
}
|
|
33
50
|
/**
|
|
34
51
|
* Native CORS middleware, generates a CORS configuration for the server.
|
|
@@ -50,7 +67,7 @@ function resolveOrigin(requestOrigin, config) {
|
|
|
50
67
|
*/
|
|
51
68
|
const cors = (options = {}) => {
|
|
52
69
|
const config = {
|
|
53
|
-
origin: options.origin
|
|
70
|
+
origin: options.origin,
|
|
54
71
|
methods: options.methods ?? [
|
|
55
72
|
http_1.HttpMethods.get,
|
|
56
73
|
http_1.HttpMethods.post,
|
|
@@ -65,8 +82,8 @@ const cors = (options = {}) => {
|
|
|
65
82
|
maxAge: options.maxAge ?? 86400,
|
|
66
83
|
preflightContinue: options.preflightContinue ?? false,
|
|
67
84
|
};
|
|
68
|
-
return async (
|
|
69
|
-
const allowedOrigin = resolveOrigin(
|
|
85
|
+
return async (context, next) => {
|
|
86
|
+
const allowedOrigin = await resolveOrigin(context, getHeaderValue(context.headers.origin), config);
|
|
70
87
|
const corsHeaders = {};
|
|
71
88
|
if (allowedOrigin) {
|
|
72
89
|
corsHeaders["Access-Control-Allow-Origin"] = allowedOrigin;
|
|
@@ -78,7 +95,7 @@ const cors = (options = {}) => {
|
|
|
78
95
|
if (config.exposedHeaders.length)
|
|
79
96
|
corsHeaders["Access-Control-Expose-Headers"] =
|
|
80
97
|
config.exposedHeaders.join(", ");
|
|
81
|
-
if (
|
|
98
|
+
if (context.req.method === http_1.HttpMethods.options) {
|
|
82
99
|
corsHeaders["Access-Control-Allow-Methods"] = config.methods.join(", ");
|
|
83
100
|
corsHeaders["Access-Control-Allow-Headers"] =
|
|
84
101
|
config.allowedHeaders.join(", ");
|
|
@@ -89,7 +106,7 @@ const cors = (options = {}) => {
|
|
|
89
106
|
.setContent(null)
|
|
90
107
|
.setHeaders(corsHeaders);
|
|
91
108
|
}
|
|
92
|
-
const response = await next(
|
|
109
|
+
const response = await next(context);
|
|
93
110
|
for (const [key, value] of Object.entries(corsHeaders)) {
|
|
94
111
|
response.setHeader(key, value);
|
|
95
112
|
}
|
package/dist/middlewares/csrf.js
CHANGED
|
@@ -16,7 +16,7 @@ const defaultTokenGenerator = () => (0, node_crypto_1.randomBytes)(32).toString(
|
|
|
16
16
|
* Normalizes an origin-like string by removing trailing slashes.
|
|
17
17
|
*
|
|
18
18
|
* This helps make origin comparisons stable across minor formatting differences
|
|
19
|
-
* (e.g. `
|
|
19
|
+
* (e.g. `scheme://host/` vs `scheme://host`).
|
|
20
20
|
*
|
|
21
21
|
* @param value - Origin string to normalize.
|
|
22
22
|
* @returns Normalized origin string without trailing slashes.
|
|
@@ -61,8 +61,8 @@ const buildConfig = (options) => ({
|
|
|
61
61
|
* @param headerName - Header key to read (as stored in `request.headers`).
|
|
62
62
|
* @returns The header value if exactly one is present, otherwise `null`.
|
|
63
63
|
*/
|
|
64
|
-
function getSingleHeaderValue(
|
|
65
|
-
const value =
|
|
64
|
+
function getSingleHeaderValue(context, headerName) {
|
|
65
|
+
const value = context.headers[headerName];
|
|
66
66
|
if (!value)
|
|
67
67
|
return null;
|
|
68
68
|
if (Array.isArray(value)) {
|
|
@@ -86,8 +86,8 @@ function getSingleHeaderValue(request, headerName) {
|
|
|
86
86
|
* @param headerName - Header key to inspect.
|
|
87
87
|
* @returns `true` if duplicates/invalid array form exist, otherwise `false`.
|
|
88
88
|
*/
|
|
89
|
-
const hasInvalidDuplicateHeader = (
|
|
90
|
-
|
|
89
|
+
const hasInvalidDuplicateHeader = (context, headerName) => Array.isArray(context.headers[headerName]) &&
|
|
90
|
+
context.headers[headerName].length !== 1;
|
|
91
91
|
/**
|
|
92
92
|
* Extracts a CSRF token from the first matching header in `headerNames`.
|
|
93
93
|
*
|
|
@@ -98,9 +98,9 @@ const hasInvalidDuplicateHeader = (request, headerName) => Array.isArray(request
|
|
|
98
98
|
* @param headerNames - List of candidate header names to check.
|
|
99
99
|
* @returns The token value if found, otherwise `null`.
|
|
100
100
|
*/
|
|
101
|
-
function getHeaderToken(
|
|
101
|
+
function getHeaderToken(context, headerNames) {
|
|
102
102
|
for (const headerName of headerNames) {
|
|
103
|
-
const value = getSingleHeaderValue(
|
|
103
|
+
const value = getSingleHeaderValue(context, headerName);
|
|
104
104
|
if (value)
|
|
105
105
|
return value;
|
|
106
106
|
}
|
|
@@ -135,11 +135,11 @@ function safeCompare(a, b) {
|
|
|
135
135
|
* @param request - Incoming request.
|
|
136
136
|
* @returns `true` if HTTPS is inferred, otherwise `false`.
|
|
137
137
|
*/
|
|
138
|
-
function isHttpsRequest(
|
|
139
|
-
const forwardedProto = getSingleHeaderValue(
|
|
138
|
+
function isHttpsRequest(context) {
|
|
139
|
+
const forwardedProto = getSingleHeaderValue(context, "x-forwarded-proto");
|
|
140
140
|
if (forwardedProto?.split(",")[0]?.trim().toLowerCase() === "https")
|
|
141
141
|
return true;
|
|
142
|
-
const forwarded = getSingleHeaderValue(
|
|
142
|
+
const forwarded = getSingleHeaderValue(context, "forwarded");
|
|
143
143
|
if (forwarded?.toLowerCase().includes("proto=https"))
|
|
144
144
|
return true;
|
|
145
145
|
return false;
|
|
@@ -152,10 +152,10 @@ function isHttpsRequest(request) {
|
|
|
152
152
|
*
|
|
153
153
|
* @param request - Incoming request.
|
|
154
154
|
* @param isHttps - Whether the request is considered HTTPS.
|
|
155
|
-
* @returns An origin string like `
|
|
155
|
+
* @returns An origin string like `scheme://host`, or `null` if `Host` is missing.
|
|
156
156
|
*/
|
|
157
|
-
function inferRequestOrigin(
|
|
158
|
-
const host = getSingleHeaderValue(
|
|
157
|
+
function inferRequestOrigin(context, isHttps) {
|
|
158
|
+
const host = getSingleHeaderValue(context, "host");
|
|
159
159
|
if (!host)
|
|
160
160
|
return null;
|
|
161
161
|
return `${isHttps ? "https" : "http"}://${host}`;
|
|
@@ -164,7 +164,7 @@ function inferRequestOrigin(request, isHttps) {
|
|
|
164
164
|
* Extracts the origin (scheme + host + port) from a Referer header value.
|
|
165
165
|
*
|
|
166
166
|
* @param referer - The raw Referer header.
|
|
167
|
-
* @returns The parsed origin (e.g., `
|
|
167
|
+
* @returns The parsed origin (e.g., `scheme://host`) or `null` if invalid.
|
|
168
168
|
*/
|
|
169
169
|
function extractOriginFromReferer(referer) {
|
|
170
170
|
try {
|
|
@@ -185,12 +185,12 @@ function extractOriginFromReferer(referer) {
|
|
|
185
185
|
* @param headerNames - Token header names to validate for duplication.
|
|
186
186
|
* @returns An error message string, or `null` if headers look safe.
|
|
187
187
|
*/
|
|
188
|
-
function validateDuplicateSecurityHeaders(
|
|
189
|
-
if (headerNames.some(headerName => hasInvalidDuplicateHeader(
|
|
188
|
+
function validateDuplicateSecurityHeaders(context, headerNames) {
|
|
189
|
+
if (headerNames.some(headerName => hasInvalidDuplicateHeader(context, headerName))) {
|
|
190
190
|
return "Invalid CSRF token header format";
|
|
191
191
|
}
|
|
192
|
-
if (hasInvalidDuplicateHeader(
|
|
193
|
-
hasInvalidDuplicateHeader(
|
|
192
|
+
if (hasInvalidDuplicateHeader(context, "origin") ||
|
|
193
|
+
hasInvalidDuplicateHeader(context, "referer")) {
|
|
194
194
|
return "Invalid Origin/Referer headers";
|
|
195
195
|
}
|
|
196
196
|
return null;
|
|
@@ -202,15 +202,15 @@ function validateDuplicateSecurityHeaders(request, headerNames) {
|
|
|
202
202
|
* @param config - Resolved CSRF configuration.
|
|
203
203
|
* @returns An error message string, or `null` if origin is acceptable.
|
|
204
204
|
*/
|
|
205
|
-
function validateOriginHeaders(
|
|
205
|
+
function validateOriginHeaders(context, config) {
|
|
206
206
|
if (!config.validateOrigin)
|
|
207
207
|
return null;
|
|
208
|
-
const httpsRequest = isHttpsRequest(
|
|
209
|
-
const inferredOrigin = inferRequestOrigin(
|
|
208
|
+
const httpsRequest = isHttpsRequest(context);
|
|
209
|
+
const inferredOrigin = inferRequestOrigin(context, httpsRequest);
|
|
210
210
|
const allowedOrigins = config.allowedOrigins ??
|
|
211
211
|
(inferredOrigin ? [normalizeOrigin(inferredOrigin)] : []);
|
|
212
|
-
const originHeader = getSingleHeaderValue(
|
|
213
|
-
const refererHeader = getSingleHeaderValue(
|
|
212
|
+
const originHeader = getSingleHeaderValue(context, "origin");
|
|
213
|
+
const refererHeader = getSingleHeaderValue(context, "referer");
|
|
214
214
|
if (!originHeader &&
|
|
215
215
|
httpsRequest &&
|
|
216
216
|
config.requireRefererOnHttps &&
|
|
@@ -245,9 +245,9 @@ function validateOriginHeaders(request, config) {
|
|
|
245
245
|
* @param expectedToken - The server-issued token (typically from cookie or generator).
|
|
246
246
|
* @returns An error message string, or `null` if the token is valid.
|
|
247
247
|
*/
|
|
248
|
-
function validateCsrfToken(
|
|
249
|
-
const tokenFromHeader = getHeaderToken(
|
|
250
|
-
const tokenFromBody =
|
|
248
|
+
function validateCsrfToken(context, config, expectedToken) {
|
|
249
|
+
const tokenFromHeader = getHeaderToken(context, config.headerNames);
|
|
250
|
+
const tokenFromBody = context.body[config.bodyField];
|
|
251
251
|
const providedToken = tokenFromHeader ?? tokenFromBody;
|
|
252
252
|
if (typeof providedToken !== "string" ||
|
|
253
253
|
!safeCompare(expectedToken, providedToken)) {
|
|
@@ -290,9 +290,9 @@ const buildForbiddenResponse = (message) => {
|
|
|
290
290
|
*/
|
|
291
291
|
const csrf = (options = {}) => {
|
|
292
292
|
const config = buildConfig(options);
|
|
293
|
-
return async (
|
|
294
|
-
const methodRequiresCheck = config.protectedMethods.includes(
|
|
295
|
-
const tokenFromCookie =
|
|
293
|
+
return async (context, next) => {
|
|
294
|
+
const methodRequiresCheck = config.protectedMethods.includes(context.req.method);
|
|
295
|
+
const tokenFromCookie = context.cookies[config.cookieName];
|
|
296
296
|
const issuedToken = tokenFromCookie || config.tokenGenerator();
|
|
297
297
|
const appendCsrfCookie = (response) => {
|
|
298
298
|
if (!tokenFromCookie) {
|
|
@@ -301,14 +301,14 @@ const csrf = (options = {}) => {
|
|
|
301
301
|
return response;
|
|
302
302
|
};
|
|
303
303
|
if (methodRequiresCheck) {
|
|
304
|
-
const validationError = validateDuplicateSecurityHeaders(
|
|
305
|
-
validateOriginHeaders(
|
|
306
|
-
validateCsrfToken(
|
|
304
|
+
const validationError = validateDuplicateSecurityHeaders(context, config.headerNames) ??
|
|
305
|
+
validateOriginHeaders(context, config) ??
|
|
306
|
+
validateCsrfToken(context, config, issuedToken);
|
|
307
307
|
if (validationError) {
|
|
308
308
|
return appendCsrfCookie(buildForbiddenResponse(validationError));
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
|
-
const response = await next(
|
|
311
|
+
const response = await next(context);
|
|
312
312
|
return appendCsrfCookie(response);
|
|
313
313
|
};
|
|
314
314
|
};
|