@fluojs/http 1.0.0-beta.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/LICENSE +21 -0
- package/README.ko.md +142 -0
- package/README.md +144 -0
- package/dist/adapter.d.ts +58 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +42 -0
- package/dist/adapters/binding.d.ts +11 -0
- package/dist/adapters/binding.d.ts.map +1 -0
- package/dist/adapters/binding.js +185 -0
- package/dist/adapters/dto-validation-adapter.d.ts +10 -0
- package/dist/adapters/dto-validation-adapter.d.ts.map +1 -0
- package/dist/adapters/dto-validation-adapter.js +46 -0
- package/dist/client-identity.d.ts +21 -0
- package/dist/client-identity.d.ts.map +1 -0
- package/dist/client-identity.js +108 -0
- package/dist/context/request-context.d.ts +53 -0
- package/dist/context/request-context.d.ts.map +1 -0
- package/dist/context/request-context.js +89 -0
- package/dist/context/sse.d.ts +21 -0
- package/dist/context/sse.d.ts.map +1 -0
- package/dist/context/sse.js +106 -0
- package/dist/decorators.d.ts +188 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +378 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts +9 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -0
- package/dist/dispatch/dispatch-content-negotiation.js +164 -0
- package/dist/dispatch/dispatch-error-policy.d.ts +3 -0
- package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-error-policy.js +24 -0
- package/dist/dispatch/dispatch-handler-policy.d.ts +3 -0
- package/dist/dispatch/dispatch-handler-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-handler-policy.js +21 -0
- package/dist/dispatch/dispatch-response-policy.d.ts +7 -0
- package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-response-policy.js +45 -0
- package/dist/dispatch/dispatch-routing-policy.d.ts +4 -0
- package/dist/dispatch/dispatch-routing-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-routing-policy.js +14 -0
- package/dist/dispatch/dispatcher.d.ts +36 -0
- package/dist/dispatch/dispatcher.d.ts.map +1 -0
- package/dist/dispatch/dispatcher.js +196 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/exceptions.d.ts +174 -0
- package/dist/exceptions.d.ts.map +1 -0
- package/dist/exceptions.js +222 -0
- package/dist/guards.d.ts +3 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +19 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/input-error-detail.d.ts +10 -0
- package/dist/input-error-detail.d.ts.map +1 -0
- package/dist/input-error-detail.js +8 -0
- package/dist/interceptors.d.ts +3 -0
- package/dist/interceptors.d.ts.map +1 -0
- package/dist/interceptors.js +22 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +2 -0
- package/dist/mapping.d.ts +7 -0
- package/dist/mapping.d.ts.map +1 -0
- package/dist/mapping.js +244 -0
- package/dist/middleware/correlation.d.ts +3 -0
- package/dist/middleware/correlation.d.ts.map +1 -0
- package/dist/middleware/correlation.js +19 -0
- package/dist/middleware/cors.d.ts +11 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +57 -0
- package/dist/middleware/middleware.d.ts +8 -0
- package/dist/middleware/middleware.d.ts.map +1 -0
- package/dist/middleware/middleware.js +64 -0
- package/dist/middleware/rate-limit.d.ts +39 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +106 -0
- package/dist/middleware/security-headers.d.ts +12 -0
- package/dist/middleware/security-headers.d.ts.map +1 -0
- package/dist/middleware/security-headers.js +47 -0
- package/dist/route-path.d.ts +15 -0
- package/dist/route-path.d.ts.map +1 -0
- package/dist/route-path.js +69 -0
- package/dist/types.d.ts +274 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +114 -0
- package/package.json +58 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
function resolveOrigin(options, requestOrigin) {
|
|
2
|
+
if (typeof options.allowOrigin === 'function') {
|
|
3
|
+
return options.allowOrigin(requestOrigin);
|
|
4
|
+
}
|
|
5
|
+
if (Array.isArray(options.allowOrigin)) {
|
|
6
|
+
if (!requestOrigin) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
return options.allowOrigin.includes(requestOrigin) ? requestOrigin : undefined;
|
|
10
|
+
}
|
|
11
|
+
return options.allowOrigin ?? '*';
|
|
12
|
+
}
|
|
13
|
+
function setHeaderIfValue(response, name, value) {
|
|
14
|
+
if (value) {
|
|
15
|
+
response.setHeader(name, value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function createCorsMiddleware(options = {}) {
|
|
19
|
+
if (options.allowCredentials === true) {
|
|
20
|
+
if (options.allowOrigin === '*' || options.allowOrigin === undefined) {
|
|
21
|
+
throw new Error('CORS misconfiguration: allowCredentials cannot be true when allowOrigin is "*" or unset (defaults to "*"). Specify explicit origins instead.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const allowMethods = options.allowMethods ?? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
|
|
25
|
+
return {
|
|
26
|
+
async handle(context, next) {
|
|
27
|
+
const requestOriginHeader = context.request.headers.origin;
|
|
28
|
+
const requestOrigin = Array.isArray(requestOriginHeader) ? requestOriginHeader[0] : requestOriginHeader;
|
|
29
|
+
const origin = resolveOrigin(options, requestOrigin);
|
|
30
|
+
if (origin) {
|
|
31
|
+
if (options.allowCredentials === true && origin === '*') {
|
|
32
|
+
throw new Error('CORS misconfiguration: allowCredentials cannot be true when allowOrigin function returns "*". Specify explicit origins instead.');
|
|
33
|
+
}
|
|
34
|
+
context.response.setHeader('Access-Control-Allow-Origin', origin);
|
|
35
|
+
}
|
|
36
|
+
setHeaderIfValue(context.response, 'Access-Control-Allow-Methods', allowMethods.join(', '));
|
|
37
|
+
setHeaderIfValue(context.response, 'Access-Control-Allow-Headers', options.allowHeaders?.join(', '));
|
|
38
|
+
setHeaderIfValue(context.response, 'Access-Control-Expose-Headers', options.exposeHeaders?.join(', '));
|
|
39
|
+
setHeaderIfValue(context.response, 'Access-Control-Allow-Credentials', options.allowCredentials ? 'true' : undefined);
|
|
40
|
+
setHeaderIfValue(context.response, 'Access-Control-Max-Age', options.maxAge !== undefined ? String(options.maxAge) : undefined);
|
|
41
|
+
if (origin && origin !== '*') {
|
|
42
|
+
const existingVary = context.response.headers['vary'] ?? context.response.headers['Vary'];
|
|
43
|
+
const varyValues = existingVary ? Array.isArray(existingVary) ? existingVary : String(existingVary).split(',').map(v => v.trim()) : [];
|
|
44
|
+
if (!varyValues.some(v => v.toLowerCase() === 'origin')) {
|
|
45
|
+
varyValues.push('Origin');
|
|
46
|
+
}
|
|
47
|
+
context.response.setHeader('Vary', varyValues.join(', '));
|
|
48
|
+
}
|
|
49
|
+
if (context.request.method === 'OPTIONS') {
|
|
50
|
+
context.response.setStatus(204);
|
|
51
|
+
await context.response.send(undefined);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await next();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Constructor } from '@fluojs/core';
|
|
2
|
+
import type { Middleware, MiddlewareContext, MiddlewareLike, MiddlewareRouteConfig, Next } from '../types.js';
|
|
3
|
+
export declare function isMiddlewareRouteConfig(value: MiddlewareLike): value is MiddlewareRouteConfig;
|
|
4
|
+
export declare function normalizeRoutePattern(path: string): string;
|
|
5
|
+
export declare function matchRoutePattern(pattern: string, path: string): boolean;
|
|
6
|
+
export declare function forRoutes<T extends Constructor<Middleware>>(middlewareClass: T, ...routes: string[]): MiddlewareRouteConfig;
|
|
7
|
+
export declare function runMiddlewareChain(definitions: MiddlewareLike[], context: MiddlewareContext, terminal: Next): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAS,MAAM,cAAc,CAAC;AAGvD,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,qBAAqB,EAAE,IAAI,EAAkB,MAAM,aAAa,CAAC;AAM9H,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,GAAG,KAAK,IAAI,qBAAqB,CAE7F;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAUxE;AAED,wBAAgB,SAAS,CAAC,CAAC,SAAS,WAAW,CAAC,UAAU,CAAC,EACzD,eAAe,EAAE,CAAC,EAClB,GAAG,MAAM,EAAE,MAAM,EAAE,GAClB,qBAAqB,CAEvB;AA2CD,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,cAAc,EAAE,EAC7B,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,EAAE,IAAI,GACb,OAAO,CAAC,IAAI,CAAC,CAUf"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { normalizeRoutePath } from '../route-path.js';
|
|
2
|
+
function isMiddleware(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null && 'handle' in value;
|
|
4
|
+
}
|
|
5
|
+
export function isMiddlewareRouteConfig(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null && 'middleware' in value && 'routes' in value;
|
|
7
|
+
}
|
|
8
|
+
export function normalizeRoutePattern(path) {
|
|
9
|
+
if (path.endsWith('/*')) {
|
|
10
|
+
return `${normalizeRoutePattern(path.slice(0, -2))}/*`;
|
|
11
|
+
}
|
|
12
|
+
return normalizeRoutePath(path);
|
|
13
|
+
}
|
|
14
|
+
export function matchRoutePattern(pattern, path) {
|
|
15
|
+
const normalizedPath = normalizeRoutePattern(path);
|
|
16
|
+
const normalizedPattern = normalizeRoutePattern(pattern);
|
|
17
|
+
if (normalizedPattern.endsWith('/*')) {
|
|
18
|
+
const prefix = normalizedPattern.slice(0, -2);
|
|
19
|
+
return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`);
|
|
20
|
+
}
|
|
21
|
+
return normalizedPath === normalizedPattern;
|
|
22
|
+
}
|
|
23
|
+
export function forRoutes(middlewareClass, ...routes) {
|
|
24
|
+
return {
|
|
25
|
+
middleware: middlewareClass,
|
|
26
|
+
routes
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function resolveMiddleware(definition, requestContext) {
|
|
30
|
+
if (isMiddleware(definition)) {
|
|
31
|
+
return definition;
|
|
32
|
+
}
|
|
33
|
+
return requestContext.container.resolve(definition);
|
|
34
|
+
}
|
|
35
|
+
async function resolveActiveMiddlewareDefinitions(definitions, context) {
|
|
36
|
+
const requestPath = context.request.path;
|
|
37
|
+
const middlewareChain = [];
|
|
38
|
+
for (const definition of definitions) {
|
|
39
|
+
if (isMiddlewareRouteConfig(definition)) {
|
|
40
|
+
const matches = definition.routes.length === 0 || definition.routes.some(route => matchRoutePattern(route, requestPath));
|
|
41
|
+
if (!matches) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const middleware = await context.requestContext.container.resolve(definition.middleware);
|
|
45
|
+
middlewareChain.push(middleware);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
middlewareChain.push(await resolveMiddleware(definition, context.requestContext));
|
|
49
|
+
}
|
|
50
|
+
return middlewareChain;
|
|
51
|
+
}
|
|
52
|
+
function deferNext(next) {
|
|
53
|
+
return async () => {
|
|
54
|
+
await Promise.resolve();
|
|
55
|
+
await next();
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export async function runMiddlewareChain(definitions, context, terminal) {
|
|
59
|
+
const middlewareChain = await resolveActiveMiddlewareDefinitions(definitions, context);
|
|
60
|
+
const composed = middlewareChain.reduceRight((next, middleware) => deferNext(async () => {
|
|
61
|
+
await middleware.handle(context, next);
|
|
62
|
+
}), deferNext(terminal));
|
|
63
|
+
await composed();
|
|
64
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MiddlewareContext, Middleware } from '../types.js';
|
|
2
|
+
/** Snapshot of one key's current rate-limit window state. */
|
|
3
|
+
export interface RateLimitStoreEntry {
|
|
4
|
+
count: number;
|
|
5
|
+
resetAt: number;
|
|
6
|
+
}
|
|
7
|
+
/** Store contract used by `createRateLimitMiddleware(...)` request windows. */
|
|
8
|
+
export interface RateLimitStore {
|
|
9
|
+
get(key: string): RateLimitStoreEntry | undefined | Promise<RateLimitStoreEntry | undefined>;
|
|
10
|
+
set(key: string, entry: RateLimitStoreEntry): void | Promise<void>;
|
|
11
|
+
increment(key: string): number | Promise<number>;
|
|
12
|
+
evict(now: number): void | Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/** Public configuration contract for `createRateLimitMiddleware(...)`. */
|
|
15
|
+
export interface RateLimitOptions {
|
|
16
|
+
limit: number;
|
|
17
|
+
windowMs: number;
|
|
18
|
+
keyResolver?: (ctx: MiddlewareContext) => string;
|
|
19
|
+
store?: RateLimitStore;
|
|
20
|
+
/**
|
|
21
|
+
* Trust `Forwarded`, `X-Forwarded-For`, and `X-Real-IP` before the raw socket address.
|
|
22
|
+
* Enable this only when the adapter sits behind a trusted proxy that overwrites spoofable headers.
|
|
23
|
+
*/
|
|
24
|
+
trustProxyHeaders?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create the built-in in-memory store for request rate-limit windows.
|
|
28
|
+
*
|
|
29
|
+
* @returns Store instance that tracks request counts in process memory.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createMemoryRateLimitStore(): RateLimitStore;
|
|
32
|
+
/**
|
|
33
|
+
* Create middleware that rejects requests once a per-key window exceeds its limit.
|
|
34
|
+
*
|
|
35
|
+
* @param options Limit, window, trust, and storage settings for one middleware instance.
|
|
36
|
+
* @returns Middleware that enforces the configured request budget.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createRateLimitMiddleware(options: RateLimitOptions): Middleware;
|
|
39
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIjE,6DAA6D;AAC7D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CAAC;IAC7F,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,MAAM,CAAC;IACjD,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAQD;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,cAAc,CAwC3D;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gBAAgB,GAAG,UAAU,CAkD/E"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { TooManyRequestsException, createErrorResponse } from '../exceptions.js';
|
|
2
|
+
import { resolveClientIdentity } from '../client-identity.js';
|
|
3
|
+
|
|
4
|
+
/** Snapshot of one key's current rate-limit window state. */
|
|
5
|
+
|
|
6
|
+
/** Store contract used by `createRateLimitMiddleware(...)` request windows. */
|
|
7
|
+
|
|
8
|
+
/** Public configuration contract for `createRateLimitMiddleware(...)`. */
|
|
9
|
+
|
|
10
|
+
function defaultKeyResolver(ctx, options) {
|
|
11
|
+
return resolveClientIdentity(ctx.request, {
|
|
12
|
+
trustProxyHeaders: options.trustProxyHeaders ?? false
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create the built-in in-memory store for request rate-limit windows.
|
|
18
|
+
*
|
|
19
|
+
* @returns Store instance that tracks request counts in process memory.
|
|
20
|
+
*/
|
|
21
|
+
export function createMemoryRateLimitStore() {
|
|
22
|
+
const map = new Map();
|
|
23
|
+
let nextSweepAt = 0;
|
|
24
|
+
return {
|
|
25
|
+
get(key) {
|
|
26
|
+
return map.get(key);
|
|
27
|
+
},
|
|
28
|
+
set(key, entry) {
|
|
29
|
+
map.set(key, entry);
|
|
30
|
+
},
|
|
31
|
+
increment(key) {
|
|
32
|
+
const entry = map.get(key);
|
|
33
|
+
if (!entry) {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
entry.count++;
|
|
37
|
+
return entry.count;
|
|
38
|
+
},
|
|
39
|
+
evict(now) {
|
|
40
|
+
if (now < nextSweepAt) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let next = Number.POSITIVE_INFINITY;
|
|
44
|
+
for (const [key, entry] of map) {
|
|
45
|
+
if (now >= entry.resetAt) {
|
|
46
|
+
map.delete(key);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
next = Math.min(next, entry.resetAt);
|
|
50
|
+
}
|
|
51
|
+
nextSweepAt = Number.isFinite(next) ? next : 0;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create middleware that rejects requests once a per-key window exceeds its limit.
|
|
58
|
+
*
|
|
59
|
+
* @param options Limit, window, trust, and storage settings for one middleware instance.
|
|
60
|
+
* @returns Middleware that enforces the configured request budget.
|
|
61
|
+
*/
|
|
62
|
+
export function createRateLimitMiddleware(options) {
|
|
63
|
+
const store = options.store ?? createMemoryRateLimitStore();
|
|
64
|
+
return {
|
|
65
|
+
async handle(context, next) {
|
|
66
|
+
const key = options.keyResolver ? options.keyResolver(context) : defaultKeyResolver(context, options);
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
await store.evict(now);
|
|
69
|
+
const entry = await store.get(key);
|
|
70
|
+
if (!entry || now >= entry.resetAt) {
|
|
71
|
+
const resetAt = now + options.windowMs;
|
|
72
|
+
await store.set(key, {
|
|
73
|
+
count: 1,
|
|
74
|
+
resetAt
|
|
75
|
+
});
|
|
76
|
+
if (1 > options.limit) {
|
|
77
|
+
const retryAfter = Math.ceil(options.windowMs / 1000);
|
|
78
|
+
const error = new TooManyRequestsException('Too Many Requests', {
|
|
79
|
+
meta: {
|
|
80
|
+
retryAfter
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
context.response.setHeader('Retry-After', String(retryAfter));
|
|
84
|
+
context.response.setStatus(429);
|
|
85
|
+
await context.response.send(createErrorResponse(error, context.requestContext.requestId));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
return next();
|
|
89
|
+
}
|
|
90
|
+
const count = await store.increment(key);
|
|
91
|
+
if (count > options.limit) {
|
|
92
|
+
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
|
|
93
|
+
const error = new TooManyRequestsException('Too Many Requests', {
|
|
94
|
+
meta: {
|
|
95
|
+
retryAfter
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
context.response.setHeader('Retry-After', String(retryAfter));
|
|
99
|
+
context.response.setStatus(429);
|
|
100
|
+
await context.response.send(createErrorResponse(error, context.requestContext.requestId));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
return next();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Middleware } from '../types.js';
|
|
2
|
+
export interface SecurityHeadersOptions {
|
|
3
|
+
contentSecurityPolicy?: string | false;
|
|
4
|
+
crossOriginOpenerPolicy?: string | false;
|
|
5
|
+
referrerPolicy?: string | false;
|
|
6
|
+
strictTransportSecurity?: string | false;
|
|
7
|
+
xContentTypeOptions?: false;
|
|
8
|
+
xFrameOptions?: string | false;
|
|
9
|
+
xXssProtection?: string | false;
|
|
10
|
+
}
|
|
11
|
+
export declare function createSecurityHeadersMiddleware(options?: SecurityHeadersOptions): Middleware;
|
|
12
|
+
//# sourceMappingURL=security-headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../../src/middleware/security-headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,WAAW,sBAAsB;IACrC,qBAAqB,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACvC,uBAAuB,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACzC,cAAc,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAChC,uBAAuB,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACzC,mBAAmB,CAAC,EAAE,KAAK,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACjC;AAYD,wBAAgB,+BAA+B,CAAC,OAAO,GAAE,sBAA2B,GAAG,UAAU,CA6ChG"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
contentSecurityPolicy: "default-src 'self'",
|
|
3
|
+
crossOriginOpenerPolicy: 'same-origin',
|
|
4
|
+
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
5
|
+
strictTransportSecurity: 'max-age=15552000; includeSubDomains',
|
|
6
|
+
xContentTypeOptions: 'nosniff',
|
|
7
|
+
xFrameOptions: 'SAMEORIGIN',
|
|
8
|
+
xXssProtection: '0'
|
|
9
|
+
};
|
|
10
|
+
export function createSecurityHeadersMiddleware(options = {}) {
|
|
11
|
+
const csp = 'contentSecurityPolicy' in options ? options.contentSecurityPolicy : DEFAULTS.contentSecurityPolicy;
|
|
12
|
+
const coop = 'crossOriginOpenerPolicy' in options ? options.crossOriginOpenerPolicy : DEFAULTS.crossOriginOpenerPolicy;
|
|
13
|
+
const referrer = 'referrerPolicy' in options ? options.referrerPolicy : DEFAULTS.referrerPolicy;
|
|
14
|
+
const hsts = 'strictTransportSecurity' in options ? options.strictTransportSecurity : DEFAULTS.strictTransportSecurity;
|
|
15
|
+
const xcto = 'xContentTypeOptions' in options ? options.xContentTypeOptions : DEFAULTS.xContentTypeOptions;
|
|
16
|
+
const xfo = 'xFrameOptions' in options ? options.xFrameOptions : DEFAULTS.xFrameOptions;
|
|
17
|
+
const xxp = 'xXssProtection' in options ? options.xXssProtection : DEFAULTS.xXssProtection;
|
|
18
|
+
const applyHeaders = response => {
|
|
19
|
+
if (csp) {
|
|
20
|
+
response.setHeader('Content-Security-Policy', csp);
|
|
21
|
+
}
|
|
22
|
+
if (coop) {
|
|
23
|
+
response.setHeader('Cross-Origin-Opener-Policy', coop);
|
|
24
|
+
}
|
|
25
|
+
if (referrer) {
|
|
26
|
+
response.setHeader('Referrer-Policy', referrer);
|
|
27
|
+
}
|
|
28
|
+
if (hsts) {
|
|
29
|
+
response.setHeader('Strict-Transport-Security', hsts);
|
|
30
|
+
}
|
|
31
|
+
if (xcto) {
|
|
32
|
+
response.setHeader('X-Content-Type-Options', xcto);
|
|
33
|
+
}
|
|
34
|
+
if (xfo) {
|
|
35
|
+
response.setHeader('X-Frame-Options', xfo);
|
|
36
|
+
}
|
|
37
|
+
if (xxp) {
|
|
38
|
+
response.setHeader('X-XSS-Protection', xxp);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
async handle(context, next) {
|
|
43
|
+
applyHeaders(context.response);
|
|
44
|
+
await next();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface RoutePathLiteralSegment {
|
|
2
|
+
kind: 'literal';
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
export interface RoutePathParamSegment {
|
|
6
|
+
kind: 'param';
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
export type RoutePathSegment = RoutePathLiteralSegment | RoutePathParamSegment;
|
|
10
|
+
export declare function normalizeRoutePath(path: string): string;
|
|
11
|
+
export declare function parseRoutePath(path: string, label?: string): RoutePathSegment[];
|
|
12
|
+
export declare function validateRoutePath(path: string, label?: string): void;
|
|
13
|
+
export declare function extractRoutePathParams(path: string): string[];
|
|
14
|
+
export declare function matchRoutePath(registeredSegments: readonly RoutePathSegment[], incomingSegments: readonly string[]): Readonly<Record<string, string>> | undefined;
|
|
15
|
+
//# sourceMappingURL=route-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-path.d.ts","sourceRoot":"","sources":["../src/route-path.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,gBAAgB,GAAG,uBAAuB,GAAG,qBAAqB,CAAC;AAsD/E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKvD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAK/E;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAEpE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAE7D;AAED,wBAAgB,cAAc,CAC5B,kBAAkB,EAAE,SAAS,gBAAgB,EAAE,EAC/C,gBAAgB,EAAE,SAAS,MAAM,EAAE,GAClC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAqB9C"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { InvalidRoutePathError } from './errors.js';
|
|
2
|
+
const routeParamNamePattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3
|
+
const unsupportedLiteralTokenPattern = /[*?+()[\]{}\\]/;
|
|
4
|
+
const supportedRouteSyntaxDescription = 'Only literal segments and full-segment ":param" placeholders are supported.';
|
|
5
|
+
function normalizeLabel(label) {
|
|
6
|
+
const value = label?.trim();
|
|
7
|
+
return value && value.length > 0 ? value : 'Route path';
|
|
8
|
+
}
|
|
9
|
+
function describeSegment(segment) {
|
|
10
|
+
return `"${segment}"`;
|
|
11
|
+
}
|
|
12
|
+
function throwInvalidRoutePath(label, path, segment, reason) {
|
|
13
|
+
throw new InvalidRoutePathError(`${normalizeLabel(label)} "${path}" is invalid at segment ${describeSegment(segment)}: ${reason}. ${supportedRouteSyntaxDescription}`);
|
|
14
|
+
}
|
|
15
|
+
function parseRoutePathSegment(segment, path, label) {
|
|
16
|
+
if (segment.startsWith(':')) {
|
|
17
|
+
const paramName = segment.slice(1);
|
|
18
|
+
if (!routeParamNamePattern.test(paramName)) {
|
|
19
|
+
throwInvalidRoutePath(label, path, segment, 'Parameter names must match /[a-zA-Z_][a-zA-Z0-9_]*/');
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
kind: 'param',
|
|
23
|
+
name: paramName
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (segment.includes(':')) {
|
|
27
|
+
throwInvalidRoutePath(label, path, segment, 'Parameters must occupy the entire segment');
|
|
28
|
+
}
|
|
29
|
+
if (unsupportedLiteralTokenPattern.test(segment)) {
|
|
30
|
+
throwInvalidRoutePath(label, path, segment, 'Wildcards, regex-like tokens, and inline modifiers are not supported');
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
kind: 'literal',
|
|
34
|
+
value: segment
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function normalizeRoutePath(path) {
|
|
38
|
+
const segments = path.split('/').filter(Boolean);
|
|
39
|
+
const normalized = `/${segments.join('/')}`;
|
|
40
|
+
return normalized === '' ? '/' : normalized;
|
|
41
|
+
}
|
|
42
|
+
export function parseRoutePath(path, label) {
|
|
43
|
+
const normalizedPath = normalizeRoutePath(path);
|
|
44
|
+
const segments = normalizedPath.split('/').filter(Boolean);
|
|
45
|
+
return segments.map(segment => parseRoutePathSegment(segment, path, label));
|
|
46
|
+
}
|
|
47
|
+
export function validateRoutePath(path, label) {
|
|
48
|
+
void parseRoutePath(path, label);
|
|
49
|
+
}
|
|
50
|
+
export function extractRoutePathParams(path) {
|
|
51
|
+
return parseRoutePath(path).flatMap(segment => segment.kind === 'param' ? [segment.name] : []);
|
|
52
|
+
}
|
|
53
|
+
export function matchRoutePath(registeredSegments, incomingSegments) {
|
|
54
|
+
if (registeredSegments.length !== incomingSegments.length) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const params = {};
|
|
58
|
+
for (const [index, segment] of registeredSegments.entries()) {
|
|
59
|
+
const incoming = incomingSegments[index];
|
|
60
|
+
if (segment.kind === 'param') {
|
|
61
|
+
params[segment.name] = incoming;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (segment.value !== incoming) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return params;
|
|
69
|
+
}
|