@nifrajs/middleware 0.1.0-alpha.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.md +79 -0
- package/dist/_utils.d.ts +27 -0
- package/dist/_utils.d.ts.map +1 -0
- package/dist/_utils.js +141 -0
- package/dist/_utils.js.map +1 -0
- package/dist/basic-auth.d.ts +27 -0
- package/dist/basic-auth.d.ts.map +1 -0
- package/dist/basic-auth.js +64 -0
- package/dist/basic-auth.js.map +1 -0
- package/dist/body-limit.d.ts +19 -0
- package/dist/body-limit.d.ts.map +1 -0
- package/dist/body-limit.js +42 -0
- package/dist/body-limit.js.map +1 -0
- package/dist/cache-control.d.ts +23 -0
- package/dist/cache-control.d.ts.map +1 -0
- package/dist/cache-control.js +35 -0
- package/dist/cache-control.js.map +1 -0
- package/dist/cache.d.ts +60 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +224 -0
- package/dist/cache.js.map +1 -0
- package/dist/combine.d.ts +7 -0
- package/dist/combine.d.ts.map +1 -0
- package/dist/combine.js +21 -0
- package/dist/combine.js.map +1 -0
- package/dist/compression.d.ts +29 -0
- package/dist/compression.d.ts.map +1 -0
- package/dist/compression.js +105 -0
- package/dist/compression.js.map +1 -0
- package/dist/context-storage.d.ts +14 -0
- package/dist/context-storage.d.ts.map +1 -0
- package/dist/context-storage.js +30 -0
- package/dist/context-storage.js.map +1 -0
- package/dist/cors.d.ts +25 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +75 -0
- package/dist/cors.js.map +1 -0
- package/dist/csrf.d.ts +23 -0
- package/dist/csrf.d.ts.map +1 -0
- package/dist/csrf.js +68 -0
- package/dist/csrf.js.map +1 -0
- package/dist/etag.d.ts +14 -0
- package/dist/etag.d.ts.map +1 -0
- package/dist/etag.js +95 -0
- package/dist/etag.js.map +1 -0
- package/dist/healthcheck.d.ts +25 -0
- package/dist/healthcheck.d.ts.map +1 -0
- package/dist/healthcheck.js +41 -0
- package/dist/healthcheck.js.map +1 -0
- package/dist/idempotency.d.ts +83 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +217 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/ip-restriction.d.ts +21 -0
- package/dist/ip-restriction.d.ts.map +1 -0
- package/dist/ip-restriction.js +161 -0
- package/dist/ip-restriction.js.map +1 -0
- package/dist/jwt.d.ts +82 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +327 -0
- package/dist/jwt.js.map +1 -0
- package/dist/language.d.ts +20 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +94 -0
- package/dist/language.js.map +1 -0
- package/dist/logger.d.ts +19 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +28 -0
- package/dist/logger.js.map +1 -0
- package/dist/method-override.d.ts +28 -0
- package/dist/method-override.d.ts.map +1 -0
- package/dist/method-override.js +67 -0
- package/dist/method-override.js.map +1 -0
- package/dist/openapi.d.ts +83 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +100 -0
- package/dist/openapi.js.map +1 -0
- package/dist/powered-by.d.ts +15 -0
- package/dist/powered-by.d.ts.map +1 -0
- package/dist/powered-by.js +23 -0
- package/dist/powered-by.js.map +1 -0
- package/dist/pretty-json.d.ts +18 -0
- package/dist/pretty-json.d.ts.map +1 -0
- package/dist/pretty-json.js +88 -0
- package/dist/pretty-json.js.map +1 -0
- package/dist/rate-limit.d.ts +92 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +172 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/request-id.d.ts +18 -0
- package/dist/request-id.d.ts.map +1 -0
- package/dist/request-id.js +21 -0
- package/dist/request-id.js.map +1 -0
- package/dist/security-headers.d.ts +23 -0
- package/dist/security-headers.d.ts.map +1 -0
- package/dist/security-headers.js +36 -0
- package/dist/security-headers.js.map +1 -0
- package/dist/timing.d.ts +25 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +101 -0
- package/dist/timing.js.map +1 -0
- package/dist/token-auth.d.ts +65 -0
- package/dist/token-auth.d.ts.map +1 -0
- package/dist/token-auth.js +90 -0
- package/dist/token-auth.js.map +1 -0
- package/dist/trailing-slash.d.ts +23 -0
- package/dist/trailing-slash.d.ts.map +1 -0
- package/dist/trailing-slash.js +70 -0
- package/dist/trailing-slash.js.map +1 -0
- package/package.json +68 -0
- package/src/_utils.ts +163 -0
- package/src/basic-auth.ts +109 -0
- package/src/body-limit.ts +53 -0
- package/src/cache-control.ts +45 -0
- package/src/cache.ts +277 -0
- package/src/combine.ts +24 -0
- package/src/compression.ts +111 -0
- package/src/context-storage.ts +36 -0
- package/src/cors.ts +90 -0
- package/src/csrf.ts +102 -0
- package/src/etag.ts +99 -0
- package/src/healthcheck.ts +59 -0
- package/src/idempotency.ts +313 -0
- package/src/index.ts +110 -0
- package/src/ip-restriction.ts +187 -0
- package/src/jwt.ts +441 -0
- package/src/language.ts +109 -0
- package/src/logger.ts +44 -0
- package/src/method-override.ts +87 -0
- package/src/openapi.ts +174 -0
- package/src/powered-by.ts +31 -0
- package/src/pretty-json.ts +97 -0
- package/src/rate-limit.ts +248 -0
- package/src/request-id.ts +30 -0
- package/src/security-headers.ts +51 -0
- package/src/timing.ts +133 -0
- package/src/token-auth.ts +172 -0
- package/src/trailing-slash.ts +94 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { definePlugin } from "@nifrajs/core";
|
|
2
|
+
const JSON_TYPE = /^(?:application\/json|[^/]+\/[^;]+\+json)(?:\s*;|$)/i;
|
|
3
|
+
function isJson(contentType) {
|
|
4
|
+
return JSON_TYPE.test(contentType);
|
|
5
|
+
}
|
|
6
|
+
function parseLength(value) {
|
|
7
|
+
if (value === null)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (!/^(?:0|[1-9]\d*)$/.test(value))
|
|
10
|
+
return undefined;
|
|
11
|
+
return Number(value);
|
|
12
|
+
}
|
|
13
|
+
async function readTextCapped(res, maxBytes) {
|
|
14
|
+
const body = res.clone().body;
|
|
15
|
+
if (body === null)
|
|
16
|
+
return null;
|
|
17
|
+
const reader = body.getReader();
|
|
18
|
+
const decoder = new TextDecoder();
|
|
19
|
+
let total = 0;
|
|
20
|
+
let text = "";
|
|
21
|
+
try {
|
|
22
|
+
while (true) {
|
|
23
|
+
const { done, value } = await reader.read();
|
|
24
|
+
if (done)
|
|
25
|
+
return text + decoder.decode();
|
|
26
|
+
total += value.byteLength;
|
|
27
|
+
if (total > maxBytes) {
|
|
28
|
+
await reader.cancel();
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
text += decoder.decode(value, { stream: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pretty-print JSON responses for debugging and developer-facing APIs. It only touches JSON content,
|
|
40
|
+
* skips encoded responses, caps inspection size, and leaves invalid JSON untouched.
|
|
41
|
+
*/
|
|
42
|
+
export function prettyJson(options = {}) {
|
|
43
|
+
const spaces = options.spaces ?? 2;
|
|
44
|
+
if (!Number.isInteger(spaces) || spaces < 0 || spaces > 10) {
|
|
45
|
+
throw new Error("prettyJson: spaces must be an integer from 0 to 10");
|
|
46
|
+
}
|
|
47
|
+
const maxBytes = options.maxBytes ?? 1_000_000;
|
|
48
|
+
if (!Number.isInteger(maxBytes) || maxBytes < 0) {
|
|
49
|
+
throw new Error("prettyJson: maxBytes must be a non-negative integer");
|
|
50
|
+
}
|
|
51
|
+
const newline = options.newline !== false;
|
|
52
|
+
const query = options.query ?? false;
|
|
53
|
+
if (query !== false && query.trim() === "")
|
|
54
|
+
throw new Error("prettyJson: query is empty");
|
|
55
|
+
const enabled = options.enabled ?? true;
|
|
56
|
+
const isEnabled = typeof enabled === "function" ? enabled : enabled ? () => true : () => false;
|
|
57
|
+
return definePlugin("prettyJson", (app) => app.onResponse(async (res, req) => {
|
|
58
|
+
if (!isEnabled(req))
|
|
59
|
+
return res;
|
|
60
|
+
if (query !== false && !new URL(req.url).searchParams.has(query))
|
|
61
|
+
return res;
|
|
62
|
+
if (res.body === null || res.headers.has("content-encoding"))
|
|
63
|
+
return res;
|
|
64
|
+
if (!isJson(res.headers.get("content-type") ?? ""))
|
|
65
|
+
return res;
|
|
66
|
+
const declared = parseLength(res.headers.get("content-length"));
|
|
67
|
+
if (declared !== undefined && declared > maxBytes)
|
|
68
|
+
return res;
|
|
69
|
+
const text = await readTextCapped(res, maxBytes);
|
|
70
|
+
if (text === null)
|
|
71
|
+
return res;
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(text);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return res;
|
|
78
|
+
}
|
|
79
|
+
const headers = new Headers(res.headers);
|
|
80
|
+
headers.delete("content-length");
|
|
81
|
+
return new Response(`${JSON.stringify(parsed, null, spaces)}${newline ? "\n" : ""}`, {
|
|
82
|
+
status: res.status,
|
|
83
|
+
statusText: res.statusText,
|
|
84
|
+
headers,
|
|
85
|
+
});
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=pretty-json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pretty-json.js","sourceRoot":"","sources":["../src/pretty-json.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Middleware } from "@nifrajs/core";
|
|
2
|
+
export interface RateLimitResult {
|
|
3
|
+
/** Hits recorded in the current window, including this one. */
|
|
4
|
+
readonly count: number;
|
|
5
|
+
/** Epoch-ms when the current window resets. */
|
|
6
|
+
readonly resetAt: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Counter backend. Production deploys MUST use a shared store (Redis, etc.) so the
|
|
10
|
+
* limit holds across instances — that's a user dependency, not ours, hence the
|
|
11
|
+
* interface. {@link MemoryStore} is for dev / single-instance only.
|
|
12
|
+
*/
|
|
13
|
+
export interface RateLimitStore {
|
|
14
|
+
hit(key: string, windowMs: number): Promise<RateLimitResult>;
|
|
15
|
+
}
|
|
16
|
+
export interface MemoryStoreOptions {
|
|
17
|
+
/** Allow the in-memory store in production. Off by default — a per-instance limiter is unsafe across instances. */
|
|
18
|
+
readonly allowInProduction?: boolean;
|
|
19
|
+
/** Hard cap on tracked client keys; expired keys are evicted first, then oldest active keys. Default `100_000`.
|
|
20
|
+
* Bounds memory against an unbounded key space (bot scans, per-IP buckets). */
|
|
21
|
+
readonly maxKeys?: number;
|
|
22
|
+
/** Minimum interval (ms) between amortized sweeps of expired windows. Default `30_000`. */
|
|
23
|
+
readonly sweepIntervalMs?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* In-process fixed-window store. Refuses to run in production unless explicitly allowed.
|
|
27
|
+
*
|
|
28
|
+
* Bounded against unbounded growth: expired windows are swept lazily (amortized, at most once per
|
|
29
|
+
* `sweepIntervalMs`) and the key set is hard-capped at `maxKeys` (expired keys are evicted first,
|
|
30
|
+
* then oldest active keys).
|
|
31
|
+
* Without this, expired entries for keys never seen again — and the unbounded key space of a bot scan
|
|
32
|
+
* — would accumulate in the map forever and OOM a single-instance deploy.
|
|
33
|
+
*/
|
|
34
|
+
export declare class MemoryStore implements RateLimitStore {
|
|
35
|
+
private readonly windows;
|
|
36
|
+
private readonly maxKeys;
|
|
37
|
+
private readonly sweepIntervalMs;
|
|
38
|
+
private lastSweep;
|
|
39
|
+
private evictionScans;
|
|
40
|
+
/** Cumulative entries inspected by the eviction scan since construction — an eviction-pressure
|
|
41
|
+
* gauge. The bounded scan keeps this ~O(1) per over-cap insert (≤ {@link MAX_EVICTION_SCAN}); a
|
|
42
|
+
* regressed full O(n) sweep would make it ~maxKeys per insert (what the regression test asserts). */
|
|
43
|
+
get evictionScanCount(): number;
|
|
44
|
+
constructor(options?: MemoryStoreOptions);
|
|
45
|
+
hit(key: string, windowMs: number): Promise<RateLimitResult>;
|
|
46
|
+
private sweepExpired;
|
|
47
|
+
private enforceMaxKeys;
|
|
48
|
+
}
|
|
49
|
+
export interface RateLimitOptions {
|
|
50
|
+
/** Where counters live. `MemoryStore` for dev; a shared store in production. */
|
|
51
|
+
readonly store: RateLimitStore;
|
|
52
|
+
/** Max requests allowed per window. */
|
|
53
|
+
readonly max: number;
|
|
54
|
+
/** Window length, in milliseconds. */
|
|
55
|
+
readonly windowMs: number;
|
|
56
|
+
/**
|
|
57
|
+
* How many trusted reverse proxies sit in front of the app and append to `X-Forwarded-For`.
|
|
58
|
+
* Default `0`.
|
|
59
|
+
*
|
|
60
|
+
* The default key reads the client IP from `X-Forwarded-For` as the address your **edge** proxy
|
|
61
|
+
* observed — the entry `trustedProxies` from the right (1 proxy → the rightmost hop; 2 → the
|
|
62
|
+
* second-from-right; …). Your proxies append on the right, so a client can only inject fake hops on
|
|
63
|
+
* the *left*, which this skips → not spoofable when `trustedProxies` matches your topology.
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ With the default `0`, `X-Forwarded-For` is treated as fully client-controlled and **ignored**.
|
|
66
|
+
* Reading the
|
|
67
|
+
* *first* XFF hop — the old behavior — let any client mint a fresh bucket per request and defeat the
|
|
68
|
+
* limiter. Set `trustedProxies` (only safe behind a proxy you control that appends XFF), configure a
|
|
69
|
+
* trusted single-IP {@link header}, or supply a custom {@link key} (e.g. an authenticated user id).
|
|
70
|
+
*/
|
|
71
|
+
readonly trustedProxies?: number;
|
|
72
|
+
/** Exact trusted single-IP header, e.g. an infra-set `x-real-ip`. Not read unless configured. */
|
|
73
|
+
readonly header?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Allow one shared bucket when no per-request key can be derived. Off by default because it lets one
|
|
76
|
+
* client consume the quota for everyone. Enable only for intentional global throttles.
|
|
77
|
+
*/
|
|
78
|
+
readonly allowGlobalKey?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Bucket key for a request. Overrides the default XFF-based key entirely — set this for accurate
|
|
81
|
+
* per-client limiting (e.g. an authenticated user id, or a header your proxy sets). A `Middleware`
|
|
82
|
+
* can't see the socket IP (that needs the server instance).
|
|
83
|
+
*/
|
|
84
|
+
readonly key?: (req: Request) => string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Rate limiting as a {@link Middleware}. Runs in `onRequest` (before routing, so it
|
|
88
|
+
* also covers 404s); over the limit → `429` + `Retry-After`. Every response carries
|
|
89
|
+
* `RateLimit-Limit/Remaining/Reset` (added in `onResponse`, keyed off the request).
|
|
90
|
+
*/
|
|
91
|
+
export declare function rateLimit(options: RateLimitOptions): Middleware;
|
|
92
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;CAC7D;AAED,MAAM,WAAW,kBAAkB;IACjC,mHAAmH;IACnH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IACpC;mFAC+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,2FAA2F;IAC3F,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAClC;AAOD;;;;;;;;GAQG;AACH,qBAAa,WAAY,YAAW,cAAc;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwD;IAChF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,aAAa,CAAI;IAEzB;;yGAEqG;IACrG,IAAI,iBAAiB,IAAI,MAAM,CAE9B;gBAEW,OAAO,GAAE,kBAAuB;IAiB5C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAmB5D,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,cAAc;CAuBvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;IAC9B,uCAAuC;IACvC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,iGAAiG;IACjG,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAA;IACjC;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAA;CACxC;AA4BD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,UAAU,CAyD/D"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** Max entries scanned (oldest-first) per eviction looking for an expired victim before falling back
|
|
2
|
+
* to evicting the oldest-inserted. Bounds eviction to O(1) per insertion instead of a full O(n) sweep
|
|
3
|
+
* under a distinct-key flood — the abuse the key cap defends against. */
|
|
4
|
+
const MAX_EVICTION_SCAN = 64;
|
|
5
|
+
/**
|
|
6
|
+
* In-process fixed-window store. Refuses to run in production unless explicitly allowed.
|
|
7
|
+
*
|
|
8
|
+
* Bounded against unbounded growth: expired windows are swept lazily (amortized, at most once per
|
|
9
|
+
* `sweepIntervalMs`) and the key set is hard-capped at `maxKeys` (expired keys are evicted first,
|
|
10
|
+
* then oldest active keys).
|
|
11
|
+
* Without this, expired entries for keys never seen again — and the unbounded key space of a bot scan
|
|
12
|
+
* — would accumulate in the map forever and OOM a single-instance deploy.
|
|
13
|
+
*/
|
|
14
|
+
export class MemoryStore {
|
|
15
|
+
windows = new Map();
|
|
16
|
+
maxKeys;
|
|
17
|
+
sweepIntervalMs;
|
|
18
|
+
lastSweep = 0;
|
|
19
|
+
evictionScans = 0;
|
|
20
|
+
/** Cumulative entries inspected by the eviction scan since construction — an eviction-pressure
|
|
21
|
+
* gauge. The bounded scan keeps this ~O(1) per over-cap insert (≤ {@link MAX_EVICTION_SCAN}); a
|
|
22
|
+
* regressed full O(n) sweep would make it ~maxKeys per insert (what the regression test asserts). */
|
|
23
|
+
get evictionScanCount() {
|
|
24
|
+
return this.evictionScans;
|
|
25
|
+
}
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
if (options.allowInProduction !== true && process.env.NODE_ENV === "production") {
|
|
28
|
+
throw new Error("MemoryStore is per-instance and unsafe in production (each instance limits separately). " +
|
|
29
|
+
"Use a shared store (e.g. Redis), or pass { allowInProduction: true } for a single-instance deploy.");
|
|
30
|
+
}
|
|
31
|
+
this.maxKeys = options.maxKeys ?? 100_000;
|
|
32
|
+
if (!Number.isInteger(this.maxKeys) || this.maxKeys < 1) {
|
|
33
|
+
throw new Error("MemoryStore: maxKeys must be a positive integer");
|
|
34
|
+
}
|
|
35
|
+
this.sweepIntervalMs = options.sweepIntervalMs ?? 30_000;
|
|
36
|
+
if (!Number.isFinite(this.sweepIntervalMs) || this.sweepIntervalMs < 0) {
|
|
37
|
+
throw new Error("MemoryStore: sweepIntervalMs must be a non-negative number");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
hit(key, windowMs) {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
// Amortized GC: at most once per sweepIntervalMs, drop every window whose reset has passed. This
|
|
43
|
+
// bounds memory to ~the active key set instead of leaking an entry per distinct key ever seen.
|
|
44
|
+
if (now - this.lastSweep >= this.sweepIntervalMs) {
|
|
45
|
+
this.sweepExpired(now);
|
|
46
|
+
this.lastSweep = now;
|
|
47
|
+
}
|
|
48
|
+
const current = this.windows.get(key);
|
|
49
|
+
if (current === undefined || now >= current.resetAt) {
|
|
50
|
+
const fresh = { count: 1, resetAt: now + windowMs };
|
|
51
|
+
this.windows.set(key, fresh);
|
|
52
|
+
this.enforceMaxKeys(now);
|
|
53
|
+
return Promise.resolve({ count: fresh.count, resetAt: fresh.resetAt });
|
|
54
|
+
}
|
|
55
|
+
current.count += 1;
|
|
56
|
+
return Promise.resolve({ count: current.count, resetAt: current.resetAt });
|
|
57
|
+
}
|
|
58
|
+
sweepExpired(now) {
|
|
59
|
+
for (const [k, v] of this.windows)
|
|
60
|
+
if (now >= v.resetAt)
|
|
61
|
+
this.windows.delete(k);
|
|
62
|
+
}
|
|
63
|
+
enforceMaxKeys(now) {
|
|
64
|
+
if (this.windows.size <= this.maxKeys)
|
|
65
|
+
return;
|
|
66
|
+
// Prefer evicting an expired window over an active user's, but bound the work. A full sweep on
|
|
67
|
+
// every insertion is O(n)/request under a distinct-key flood — the exact abuse the cap defends
|
|
68
|
+
// against. Scan only a small fixed budget (oldest-first, where expired entries cluster) for a
|
|
69
|
+
// victim; if none is found, evict the oldest-inserted (O(1)). Expired entries beyond the budget
|
|
70
|
+
// are still reclaimed by the amortized sweep in `hit`.
|
|
71
|
+
while (this.windows.size > this.maxKeys) {
|
|
72
|
+
let victim;
|
|
73
|
+
let scanned = 0;
|
|
74
|
+
for (const [k, v] of this.windows) {
|
|
75
|
+
this.evictionScans++;
|
|
76
|
+
if (now >= v.resetAt) {
|
|
77
|
+
victim = k;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (++scanned >= MAX_EVICTION_SCAN)
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (victim === undefined)
|
|
84
|
+
victim = this.windows.keys().next().value;
|
|
85
|
+
if (victim === undefined)
|
|
86
|
+
break;
|
|
87
|
+
this.windows.delete(victim);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function defaultKey(req, trustedProxies, header, allowGlobalKey) {
|
|
92
|
+
if (header !== undefined) {
|
|
93
|
+
const ip = req.headers.get(header);
|
|
94
|
+
if (ip !== null && ip.trim() !== "")
|
|
95
|
+
return ip.trim();
|
|
96
|
+
}
|
|
97
|
+
if (trustedProxies > 0) {
|
|
98
|
+
const xff = req.headers.get("x-forwarded-for");
|
|
99
|
+
if (xff !== null) {
|
|
100
|
+
const parts = xff.split(",");
|
|
101
|
+
// The leftmost of the trusted suffix (your proxies append on the right) = the address your edge
|
|
102
|
+
// proxy observed = the real client. A client can only prepend fakes further left, which this
|
|
103
|
+
// index skips. A chain shorter than `trustedProxies` (misconfig) → undefined → fall through.
|
|
104
|
+
const ip = parts[parts.length - trustedProxies]?.trim();
|
|
105
|
+
if (ip !== undefined && ip !== "")
|
|
106
|
+
return ip;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// No trusted proxy (or XFF absent/too-short): XFF isn't trustworthy, so don't derive a per-client key
|
|
110
|
+
// from it. A shared bucket is only safe when the app deliberately asked for a global throttle.
|
|
111
|
+
return allowGlobalKey ? "global" : null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Rate limiting as a {@link Middleware}. Runs in `onRequest` (before routing, so it
|
|
115
|
+
* also covers 404s); over the limit → `429` + `Retry-After`. Every response carries
|
|
116
|
+
* `RateLimit-Limit/Remaining/Reset` (added in `onResponse`, keyed off the request).
|
|
117
|
+
*/
|
|
118
|
+
export function rateLimit(options) {
|
|
119
|
+
const { store, max, windowMs } = options;
|
|
120
|
+
if (!Number.isInteger(max) || max < 1)
|
|
121
|
+
throw new Error("rateLimit: max must be a positive integer");
|
|
122
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
123
|
+
throw new Error("rateLimit: windowMs must be a positive number");
|
|
124
|
+
}
|
|
125
|
+
const trustedProxies = options.trustedProxies ?? 0;
|
|
126
|
+
if (!Number.isInteger(trustedProxies) || trustedProxies < 0) {
|
|
127
|
+
throw new Error("rateLimit: trustedProxies must be a non-negative integer");
|
|
128
|
+
}
|
|
129
|
+
const header = options.header?.trim().toLowerCase();
|
|
130
|
+
if (header !== undefined && header.trim() === "")
|
|
131
|
+
throw new Error("rateLimit: header is empty");
|
|
132
|
+
const allowGlobalKey = options.allowGlobalKey === true;
|
|
133
|
+
if (options.key === undefined &&
|
|
134
|
+
header === undefined &&
|
|
135
|
+
trustedProxies === 0 &&
|
|
136
|
+
!allowGlobalKey) {
|
|
137
|
+
throw new Error("rateLimit: configure key, header, or trustedProxies; pass allowGlobalKey: true only for an intentional shared bucket");
|
|
138
|
+
}
|
|
139
|
+
const keyOf = options.key ?? ((req) => defaultKey(req, trustedProxies, header, allowGlobalKey));
|
|
140
|
+
const quota = new WeakMap();
|
|
141
|
+
return {
|
|
142
|
+
name: "rate-limit",
|
|
143
|
+
async onRequest(req) {
|
|
144
|
+
const key = keyOf(req);
|
|
145
|
+
if (typeof key !== "string" || key.trim() === "") {
|
|
146
|
+
return Response.json({ ok: false, error: "rate_limit_key_unavailable" }, { status: 500 });
|
|
147
|
+
}
|
|
148
|
+
const { count, resetAt } = await store.hit(key, windowMs);
|
|
149
|
+
const resetSeconds = Math.max(0, Math.ceil((resetAt - Date.now()) / 1000));
|
|
150
|
+
quota.set(req, { remaining: Math.max(0, max - count), resetSeconds });
|
|
151
|
+
if (count > max) {
|
|
152
|
+
return new Response(JSON.stringify({ ok: false, error: "rate_limited" }), {
|
|
153
|
+
status: 429,
|
|
154
|
+
headers: { "content-type": "application/json", "retry-after": String(resetSeconds) },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
},
|
|
159
|
+
onResponse(res, req) {
|
|
160
|
+
const info = quota.get(req);
|
|
161
|
+
if (info === undefined)
|
|
162
|
+
return res;
|
|
163
|
+
quota.delete(req);
|
|
164
|
+
const headers = new Headers(res.headers);
|
|
165
|
+
headers.set("RateLimit-Limit", String(max));
|
|
166
|
+
headers.set("RateLimit-Remaining", String(info.remaining));
|
|
167
|
+
headers.set("RateLimit-Reset", String(info.resetSeconds));
|
|
168
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RequestIdOptions {
|
|
2
|
+
/** Header read for an inbound id (trace propagation) + echoed on the response. Default `"x-request-id"`. */
|
|
3
|
+
readonly header?: string;
|
|
4
|
+
/** Generate an id when the inbound header is absent. Default `crypto.randomUUID()`. */
|
|
5
|
+
readonly generate?: () => string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A {@link definePlugin} plugin that gives every request a stable id: it reuses an inbound
|
|
9
|
+
* `x-request-id` (or generates one), exposes it on the handler context as **`c.requestId`** (typed,
|
|
10
|
+
* threaded by `derive`), and echoes it on the response header. Idempotent — applying it twice is a
|
|
11
|
+
* no-op.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* app.use(requestId()) // c.requestId available downstream; X-Request-Id on responses
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function requestId(options?: RequestIdOptions): import("@nifrajs/core").NifraPlugin<import("@nifrajs/core").AnyServer, import("@nifrajs/core").Server<any, any>>;
|
|
18
|
+
//# sourceMappingURL=request-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-id.d.ts","sourceRoot":"","sources":["../src/request-id.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,4GAA4G;IAC5G,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,uFAAuF;IACvF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,MAAM,CAAA;CACjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,OAAO,GAAE,gBAAqB,oHAUvD"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { definePlugin } from "@nifrajs/core";
|
|
2
|
+
/**
|
|
3
|
+
* A {@link definePlugin} plugin that gives every request a stable id: it reuses an inbound
|
|
4
|
+
* `x-request-id` (or generates one), exposes it on the handler context as **`c.requestId`** (typed,
|
|
5
|
+
* threaded by `derive`), and echoes it on the response header. Idempotent — applying it twice is a
|
|
6
|
+
* no-op.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* app.use(requestId()) // c.requestId available downstream; X-Request-Id on responses
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export function requestId(options = {}) {
|
|
13
|
+
const header = options.header ?? "x-request-id";
|
|
14
|
+
const generate = options.generate ?? (() => crypto.randomUUID());
|
|
15
|
+
return definePlugin("requestId", (app) => app.derive((c) => {
|
|
16
|
+
const id = c.req.headers.get(header) ?? generate();
|
|
17
|
+
c.set.headers[header] = id; // echo on the response (route handlers)
|
|
18
|
+
return { requestId: id };
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=request-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-id.js","sourceRoot":"","sources":["../src/request-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAS5C;;;;;;;;;GASG;AACH,MAAM,UAAU,SAAS,CAAC,UAA4B,EAAE;IACtD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAA;IAC/C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAA;IAChE,OAAO,YAAY,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,EAAE,CACvC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACf,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAA;QAClD,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAA,CAAC,wCAAwC;QACnE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,CAAA;IAC1B,CAAC,CAAC,CACH,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Middleware } from "@nifrajs/core";
|
|
2
|
+
export interface SecurityHeadersOptions {
|
|
3
|
+
/** `Strict-Transport-Security`. Off by default — opt in once you're sure you're HTTPS-only. */
|
|
4
|
+
readonly hsts?: {
|
|
5
|
+
readonly maxAge: number;
|
|
6
|
+
readonly includeSubDomains?: boolean;
|
|
7
|
+
readonly preload?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/** `Content-Security-Policy` value. Off by default (app-specific). */
|
|
10
|
+
readonly contentSecurityPolicy?: string;
|
|
11
|
+
/** `X-Frame-Options`. Default `"DENY"`. */
|
|
12
|
+
readonly frameOptions?: "DENY" | "SAMEORIGIN";
|
|
13
|
+
/** `Referrer-Policy`. Default `"no-referrer"`. */
|
|
14
|
+
readonly referrerPolicy?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A safe-by-default set of response security headers (`onResponse`, so they cover
|
|
18
|
+
* errors and 404s too): `X-Content-Type-Options: nosniff`, `X-Frame-Options`, and
|
|
19
|
+
* `Referrer-Policy` always; `Strict-Transport-Security` and `Content-Security-Policy`
|
|
20
|
+
* only when configured (both are environment-/app-specific).
|
|
21
|
+
*/
|
|
22
|
+
export declare function securityHeaders(options?: SecurityHeadersOptions): Middleware;
|
|
23
|
+
//# sourceMappingURL=security-headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAG/C,MAAM,WAAW,sBAAsB;IACrC,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,CAAC,EAAE;QACd,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;QACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAC3B,CAAA;IACD,sEAAsE;IACtE,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAA;IACvC,2CAA2C;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;IAC7C,kDAAkD;IAClD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,UAAU,CA0BhF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { withHeaders } from "./_utils.js";
|
|
2
|
+
/**
|
|
3
|
+
* A safe-by-default set of response security headers (`onResponse`, so they cover
|
|
4
|
+
* errors and 404s too): `X-Content-Type-Options: nosniff`, `X-Frame-Options`, and
|
|
5
|
+
* `Referrer-Policy` always; `Strict-Transport-Security` and `Content-Security-Policy`
|
|
6
|
+
* only when configured (both are environment-/app-specific).
|
|
7
|
+
*/
|
|
8
|
+
export function securityHeaders(options = {}) {
|
|
9
|
+
const frameOptions = options.frameOptions ?? "DENY";
|
|
10
|
+
const referrerPolicy = options.referrerPolicy ?? "no-referrer";
|
|
11
|
+
const csp = options.contentSecurityPolicy;
|
|
12
|
+
let hstsValue;
|
|
13
|
+
if (options.hsts !== undefined) {
|
|
14
|
+
const parts = [`max-age=${options.hsts.maxAge}`];
|
|
15
|
+
if (options.hsts.includeSubDomains)
|
|
16
|
+
parts.push("includeSubDomains");
|
|
17
|
+
if (options.hsts.preload)
|
|
18
|
+
parts.push("preload");
|
|
19
|
+
hstsValue = parts.join("; ");
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
name: "security-headers",
|
|
23
|
+
// Mutate in place on the common (mutable-headers) response; clone only for an immutable one
|
|
24
|
+
// (see withHeaders — the old always-clone was ~3% of a realistic request).
|
|
25
|
+
onResponse: (res) => withHeaders(res, (headers) => {
|
|
26
|
+
headers.set("X-Content-Type-Options", "nosniff");
|
|
27
|
+
headers.set("X-Frame-Options", frameOptions);
|
|
28
|
+
headers.set("Referrer-Policy", referrerPolicy);
|
|
29
|
+
if (hstsValue !== undefined)
|
|
30
|
+
headers.set("Strict-Transport-Security", hstsValue);
|
|
31
|
+
if (csp !== undefined)
|
|
32
|
+
headers.set("Content-Security-Policy", csp);
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=security-headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAiBzC;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkC,EAAE;IAClE,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,MAAM,CAAA;IACnD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,aAAa,CAAA;IAC9D,MAAM,GAAG,GAAG,OAAO,CAAC,qBAAqB,CAAA;IAEzC,IAAI,SAA6B,CAAA;IACjC,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,CAAC,WAAW,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;QAChD,IAAI,OAAO,CAAC,IAAI,CAAC,iBAAiB;YAAE,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QACnE,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAC/C,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC9B,CAAC;IAED,OAAO;QACL,IAAI,EAAE,kBAAkB;QACxB,4FAA4F;QAC5F,2EAA2E;QAC3E,UAAU,EAAE,CAAC,GAAG,EAAE,EAAE,CAClB,WAAW,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YAC3B,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAA;YAChD,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAA;YAC5C,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAA;YAC9C,IAAI,SAAS,KAAK,SAAS;gBAAE,OAAO,CAAC,GAAG,CAAC,2BAA2B,EAAE,SAAS,CAAC,CAAA;YAChF,IAAI,GAAG,KAAK,SAAS;gBAAE,OAAO,CAAC,GAAG,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAA;QACpE,CAAC,CAAC;KACL,CAAA;AACH,CAAC"}
|
package/dist/timing.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface TimingMetric {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly duration: number;
|
|
4
|
+
readonly description?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TimingControls {
|
|
7
|
+
mark(name: string): void;
|
|
8
|
+
measure(name: string, start: string, end?: string): void;
|
|
9
|
+
metric(name: string, duration: number, description?: string): void;
|
|
10
|
+
}
|
|
11
|
+
export interface TimingOptions {
|
|
12
|
+
/** Name for the automatic total request metric. Default `"total"`; set `false` to disable. */
|
|
13
|
+
readonly total?: string | false;
|
|
14
|
+
/** Decimal places for durations. Default `1`. */
|
|
15
|
+
readonly precision?: number;
|
|
16
|
+
/** Only emit timing for matching requests. Default `true`. */
|
|
17
|
+
readonly enabled?: boolean | ((request: Request) => boolean);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Adds a `Server-Timing` response header and typed `c.timing` controls for custom metrics.
|
|
21
|
+
* Put request-rewriting middleware (for example `methodOverride`) before `timing()` so timing is
|
|
22
|
+
* attached to the final routed request.
|
|
23
|
+
*/
|
|
24
|
+
export declare function timing(options?: TimingOptions): import("@nifrajs/core").NifraPlugin<import("@nifrajs/core").AnyServer, import("@nifrajs/core").Server<any, any>>;
|
|
25
|
+
//# sourceMappingURL=timing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACnE;AAED,MAAM,WAAW,aAAa;IAC5B,8FAA8F;IAC9F,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC/B,iDAAiD;IACjD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,CAAA;CAC7D;AAqCD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,oHAoEjD"}
|
package/dist/timing.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { definePlugin } from "@nifrajs/core";
|
|
2
|
+
import { withHeaders } from "./_utils.js";
|
|
3
|
+
const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
4
|
+
function assertMetricName(name) {
|
|
5
|
+
if (!TOKEN.test(name))
|
|
6
|
+
throw new Error(`timing: invalid metric name "${name}"`);
|
|
7
|
+
}
|
|
8
|
+
function assertDuration(duration) {
|
|
9
|
+
if (!Number.isFinite(duration) || duration < 0) {
|
|
10
|
+
throw new Error("timing: duration must be a finite non-negative number");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function quoteDescription(value) {
|
|
14
|
+
let out = "";
|
|
15
|
+
for (let i = 0; i < value.length; i++) {
|
|
16
|
+
const code = value.charCodeAt(i);
|
|
17
|
+
if (code < 0x20 || code === 0x7f)
|
|
18
|
+
continue;
|
|
19
|
+
const ch = value[i] ?? "";
|
|
20
|
+
out += ch === "\\" || ch === '"' ? `\\${ch}` : ch;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function formatMetric(metric, precision) {
|
|
25
|
+
let out = `${metric.name};dur=${metric.duration.toFixed(precision)}`;
|
|
26
|
+
if (metric.description !== undefined)
|
|
27
|
+
out += `;desc="${quoteDescription(metric.description)}"`;
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Adds a `Server-Timing` response header and typed `c.timing` controls for custom metrics.
|
|
32
|
+
* Put request-rewriting middleware (for example `methodOverride`) before `timing()` so timing is
|
|
33
|
+
* attached to the final routed request.
|
|
34
|
+
*/
|
|
35
|
+
export function timing(options = {}) {
|
|
36
|
+
const totalName = options.total === undefined ? "total" : options.total;
|
|
37
|
+
if (totalName !== false)
|
|
38
|
+
assertMetricName(totalName);
|
|
39
|
+
const precision = options.precision ?? 1;
|
|
40
|
+
if (!Number.isInteger(precision) || precision < 0 || precision > 6) {
|
|
41
|
+
throw new Error("timing: precision must be an integer from 0 to 6");
|
|
42
|
+
}
|
|
43
|
+
const enabled = options.enabled ?? true;
|
|
44
|
+
const isEnabled = typeof enabled === "function" ? enabled : enabled ? () => true : () => false;
|
|
45
|
+
const states = new WeakMap();
|
|
46
|
+
const controls = (req) => ({
|
|
47
|
+
mark(name) {
|
|
48
|
+
assertMetricName(name);
|
|
49
|
+
const state = states.get(req);
|
|
50
|
+
if (state !== undefined)
|
|
51
|
+
state.marks.set(name, performance.now());
|
|
52
|
+
},
|
|
53
|
+
measure(name, start, end) {
|
|
54
|
+
assertMetricName(name);
|
|
55
|
+
const state = states.get(req);
|
|
56
|
+
if (state === undefined)
|
|
57
|
+
return;
|
|
58
|
+
const startAt = state.marks.get(start);
|
|
59
|
+
const endAt = end === undefined ? performance.now() : state.marks.get(end);
|
|
60
|
+
if (startAt === undefined || endAt === undefined) {
|
|
61
|
+
throw new Error("timing: unknown mark");
|
|
62
|
+
}
|
|
63
|
+
const duration = endAt - startAt;
|
|
64
|
+
assertDuration(duration);
|
|
65
|
+
state.metrics.push({ name, duration });
|
|
66
|
+
},
|
|
67
|
+
metric(name, duration, description) {
|
|
68
|
+
assertMetricName(name);
|
|
69
|
+
assertDuration(duration);
|
|
70
|
+
const state = states.get(req);
|
|
71
|
+
if (state !== undefined) {
|
|
72
|
+
state.metrics.push(description === undefined ? { name, duration } : { name, duration, description });
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
return definePlugin("timing", (app) => app
|
|
77
|
+
.onRequest((req) => {
|
|
78
|
+
if (!isEnabled(req))
|
|
79
|
+
return undefined;
|
|
80
|
+
states.set(req, { start: performance.now(), marks: new Map(), metrics: [] });
|
|
81
|
+
return undefined;
|
|
82
|
+
})
|
|
83
|
+
.derive((c) => ({ timing: controls(c.req) }))
|
|
84
|
+
.onResponse((res, req) => {
|
|
85
|
+
const state = states.get(req);
|
|
86
|
+
if (state === undefined)
|
|
87
|
+
return res;
|
|
88
|
+
states.delete(req);
|
|
89
|
+
const metrics = totalName === false
|
|
90
|
+
? state.metrics
|
|
91
|
+
: [{ name: totalName, duration: performance.now() - state.start }, ...state.metrics];
|
|
92
|
+
if (metrics.length === 0)
|
|
93
|
+
return res;
|
|
94
|
+
const value = metrics.map((metric) => formatMetric(metric, precision)).join(", ");
|
|
95
|
+
return withHeaders(res, (headers) => {
|
|
96
|
+
const existing = headers.get("server-timing");
|
|
97
|
+
headers.set("server-timing", existing === null ? value : `${existing}, ${value}`);
|
|
98
|
+
});
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=timing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.js","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"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"}
|