@centralping/ergo 0.1.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.
Files changed (155) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +139 -0
  4. package/http/accepts.js +69 -0
  5. package/http/authorization.js +65 -0
  6. package/http/body.js +311 -0
  7. package/http/cache-control.js +123 -0
  8. package/http/compress.js +157 -0
  9. package/http/cookie.js +39 -0
  10. package/http/cors.js +79 -0
  11. package/http/csrf.js +76 -0
  12. package/http/handler.js +74 -0
  13. package/http/index.js +13 -0
  14. package/http/json-api-query.js +53 -0
  15. package/http/logger.js +167 -0
  16. package/http/main.js +140 -0
  17. package/http/precondition.js +53 -0
  18. package/http/prefer.js +36 -0
  19. package/http/rate-limit.js +66 -0
  20. package/http/security-headers.js +62 -0
  21. package/http/send.js +399 -0
  22. package/http/timeout.js +47 -0
  23. package/http/url.js +47 -0
  24. package/http/validate.js +84 -0
  25. package/lib/accepts.js +49 -0
  26. package/lib/attach-instance.js +23 -0
  27. package/lib/authorization.js +187 -0
  28. package/lib/body/multiparse.js +173 -0
  29. package/lib/body/multipart/headers.js +69 -0
  30. package/lib/body/writer.js +73 -0
  31. package/lib/cookie/cookie.js +192 -0
  32. package/lib/cookie/index.js +14 -0
  33. package/lib/cookie/jar.js +106 -0
  34. package/lib/cookie/parse.js +101 -0
  35. package/lib/cors.js +191 -0
  36. package/lib/csrf.js +96 -0
  37. package/lib/from-connect.js +69 -0
  38. package/lib/json-api-query/index.js +25 -0
  39. package/lib/json-api-query/schema.json +105 -0
  40. package/lib/json-api-query/validate.js +56 -0
  41. package/lib/link.js +96 -0
  42. package/lib/prefer.js +52 -0
  43. package/lib/query.js +113 -0
  44. package/lib/rate-limit.js +115 -0
  45. package/lib/sanitize-quoted-string.js +28 -0
  46. package/lib/security-headers.js +125 -0
  47. package/lib/validate.js +80 -0
  48. package/lib/vary.js +40 -0
  49. package/package.json +158 -0
  50. package/types/http/accepts.d.ts +8 -0
  51. package/types/http/authorization.d.ts +8 -0
  52. package/types/http/body.d.ts +20 -0
  53. package/types/http/cache-control.d.ts +16 -0
  54. package/types/http/compress.d.ts +5 -0
  55. package/types/http/cookie.d.ts +2 -0
  56. package/types/http/cors.d.ts +9 -0
  57. package/types/http/csrf.d.ts +9 -0
  58. package/types/http/handler.d.ts +2 -0
  59. package/types/http/index.d.ts +1 -0
  60. package/types/http/json-api-query.d.ts +2 -0
  61. package/types/http/logger.d.ts +9 -0
  62. package/types/http/main.d.ts +142 -0
  63. package/types/http/precondition.d.ts +44 -0
  64. package/types/http/prefer.d.ts +2 -0
  65. package/types/http/rate-limit.d.ts +17 -0
  66. package/types/http/security-headers.d.ts +10 -0
  67. package/types/http/send.d.ts +8 -0
  68. package/types/http/timeout.d.ts +5 -0
  69. package/types/http/url.d.ts +2 -0
  70. package/types/http/validate.d.ts +6 -0
  71. package/types/lib/accepts.d.ts +7 -0
  72. package/types/lib/attach-instance.d.ts +19 -0
  73. package/types/lib/authorization.d.ts +6 -0
  74. package/types/lib/body/multiparse.d.ts +9 -0
  75. package/types/lib/body/multipart/headers.d.ts +2 -0
  76. package/types/lib/body/writer.d.ts +2 -0
  77. package/types/lib/cookie/cookie.d.ts +32 -0
  78. package/types/lib/cookie/index.d.ts +2 -0
  79. package/types/lib/cookie/jar.d.ts +8 -0
  80. package/types/lib/cookie/parse.d.ts +19 -0
  81. package/types/lib/cors.d.ts +9 -0
  82. package/types/lib/csrf.d.ts +32 -0
  83. package/types/lib/from-connect.d.ts +47 -0
  84. package/types/lib/json-api-query/index.d.ts +123 -0
  85. package/types/lib/json-api-query/validate.d.ts +5 -0
  86. package/types/lib/link.d.ts +37 -0
  87. package/types/lib/prefer.d.ts +36 -0
  88. package/types/lib/query.d.ts +6 -0
  89. package/types/lib/rate-limit.d.ts +76 -0
  90. package/types/lib/sanitize-quoted-string.d.ts +19 -0
  91. package/types/lib/security-headers.d.ts +24 -0
  92. package/types/lib/validate.d.ts +16 -0
  93. package/types/lib/vary.d.ts +17 -0
  94. package/types/utils/attempt.d.ts +2 -0
  95. package/types/utils/buffers/index.d.ts +2 -0
  96. package/types/utils/buffers/match.d.ts +10 -0
  97. package/types/utils/buffers/split.d.ts +10 -0
  98. package/types/utils/compose-with.d.ts +40 -0
  99. package/types/utils/compose.d.ts +83 -0
  100. package/types/utils/flat-array.d.ts +2 -0
  101. package/types/utils/get.d.ts +5 -0
  102. package/types/utils/http-errors.d.ts +22 -0
  103. package/types/utils/iterables/buffer-split.d.ts +2 -0
  104. package/types/utils/iterables/chain.d.ts +2 -0
  105. package/types/utils/iterables/exec-all.d.ts +2 -0
  106. package/types/utils/iterables/filter.d.ts +2 -0
  107. package/types/utils/iterables/for-each.d.ts +2 -0
  108. package/types/utils/iterables/from-stream.d.ts +2 -0
  109. package/types/utils/iterables/index.d.ts +10 -0
  110. package/types/utils/iterables/map.d.ts +2 -0
  111. package/types/utils/iterables/range.d.ts +24 -0
  112. package/types/utils/iterables/reduce.d.ts +2 -0
  113. package/types/utils/iterables/take.d.ts +2 -0
  114. package/types/utils/observables/buffer-split.d.ts +2 -0
  115. package/types/utils/observables/chain.d.ts +2 -0
  116. package/types/utils/observables/index.d.ts +4 -0
  117. package/types/utils/observables/map.d.ts +2 -0
  118. package/types/utils/observables/take.d.ts +2 -0
  119. package/types/utils/pick.d.ts +2 -0
  120. package/types/utils/set.d.ts +2 -0
  121. package/types/utils/streams/index.d.ts +2 -0
  122. package/types/utils/streams/meter.d.ts +5 -0
  123. package/types/utils/streams/tee.d.ts +2 -0
  124. package/types/utils/type.d.ts +2 -0
  125. package/utils/attempt.js +37 -0
  126. package/utils/buffers/index.js +13 -0
  127. package/utils/buffers/match.js +96 -0
  128. package/utils/buffers/split.js +55 -0
  129. package/utils/compose-with.js +232 -0
  130. package/utils/compose.js +165 -0
  131. package/utils/flat-array.js +24 -0
  132. package/utils/get.js +39 -0
  133. package/utils/http-errors.js +113 -0
  134. package/utils/iterables/buffer-split.js +117 -0
  135. package/utils/iterables/chain.js +32 -0
  136. package/utils/iterables/exec-all.js +42 -0
  137. package/utils/iterables/filter.js +35 -0
  138. package/utils/iterables/for-each.js +33 -0
  139. package/utils/iterables/from-stream.js +29 -0
  140. package/utils/iterables/index.js +21 -0
  141. package/utils/iterables/map.js +47 -0
  142. package/utils/iterables/range.js +34 -0
  143. package/utils/iterables/reduce.js +43 -0
  144. package/utils/iterables/take.js +36 -0
  145. package/utils/observables/buffer-split.js +109 -0
  146. package/utils/observables/chain.js +33 -0
  147. package/utils/observables/index.js +19 -0
  148. package/utils/observables/map.js +34 -0
  149. package/utils/observables/take.js +40 -0
  150. package/utils/pick.js +41 -0
  151. package/utils/set.js +38 -0
  152. package/utils/streams/index.js +11 -0
  153. package/utils/streams/meter.js +98 -0
  154. package/utils/streams/tee.js +84 -0
  155. package/utils/type.js +47 -0
package/http/logger.js ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @fileoverview HTTP middleware factory for structured request/response logging.
3
+ *
4
+ * Logs request completion events as structured objects to the configured log function.
5
+ * Each log entry includes operationally essential fields:
6
+ * - Request: method, URL, headers, client IP, HTTP version, request ID
7
+ * - Response: status code, duration (ms), headers, and status message
8
+ * - Host: static machine identity (hostname, arch, platform, pid)
9
+ *
10
+ * The request ID is resolved using a three-step chain:
11
+ * 1. `res.getHeader(headerRequestIdName)` — set by ergo-router's transport layer (primary path)
12
+ * 2. `req.headers[headerRequestIdName]` — set by an upstream proxy (standalone fallback)
13
+ * 3. `uuid()` — generates a new UUID when no upstream ID is available
14
+ *
15
+ * The response header is only set when not already present, so a transport-layer ID
16
+ * is never overwritten.
17
+ *
18
+ * System metrics (CPU, memory, load average) are intentionally excluded — OTel treats
19
+ * logs and metrics as distinct observability signals. Collect system metrics via a
20
+ * dedicated metrics pipeline at periodic intervals, not per-request.
21
+ *
22
+ * Environment variables (`process.env`) are intentionally excluded from logs to prevent
23
+ * accidental secret leakage.
24
+ *
25
+ * @module http/logger
26
+ * @version 0.1.0
27
+ * @since 0.1.0
28
+ * @requires node:os
29
+ * @requires node:crypto
30
+ *
31
+ * @example
32
+ * import {compose, logger} from 'ergo';
33
+ *
34
+ * const pipeline = compose(
35
+ * [logger(), 'log'],
36
+ * // On finish logs: {"requestId":"...","method":"GET","url":"/users","statusCode":200,"duration":12,...}
37
+ * );
38
+ */
39
+ import {hostname} from 'node:os';
40
+ import {randomUUID} from 'node:crypto';
41
+
42
+ const DEFAULT_REDACTED = new Set(['authorization', 'proxy-authorization', 'cookie', 'set-cookie']);
43
+
44
+ /**
45
+ * @param {object} headers - Header object to redact
46
+ * @param {Set<string>} redactSet - Header names to replace with '[REDACTED]'
47
+ * @returns {object} - Copy with sensitive values replaced
48
+ */
49
+ function redact(headers, redactSet) {
50
+ if (!redactSet?.size) return headers;
51
+ const safe = {};
52
+ for (const [k, v] of Object.entries(headers)) {
53
+ safe[k] = redactSet.has(k) ? '[REDACTED]' : v;
54
+ }
55
+ return safe;
56
+ }
57
+
58
+ const host = Object.freeze({
59
+ hostname: hostname(),
60
+ arch: process.arch,
61
+ platform: process.platform,
62
+ pid: process.pid
63
+ });
64
+
65
+ /**
66
+ * Creates a structured request/response logging middleware.
67
+ *
68
+ * @param {object} [options] - Logger configuration
69
+ * @param {function} [options.log] - Log function for completed requests (default: console.log)
70
+ * @param {function} [options.error] - Log function for errors (default: console.error)
71
+ * @param {function} [options.uuid] - Fallback UUID generator used only when no upstream request ID
72
+ * is found on the response or request headers (default: crypto.randomUUID)
73
+ * @param {string} [options.headerRequestIdName] - Request ID header name (default: 'x-request-id')
74
+ * @param {string} [options.headerRequestIpName] - Client IP header name (default: 'x-real-ip')
75
+ * @param {Set<string>} [options.redactHeaders] - Header names to replace with '[REDACTED]' in logs
76
+ * (default: authorization, proxy-authorization, cookie, set-cookie)
77
+ * @returns {object} - Log entry with request metadata and host info (statusCode/duration added on finish)
78
+ */
79
+ export default ({
80
+ /* eslint-disable-next-line no-console */
81
+ log = console.log,
82
+ /* eslint-disable-next-line no-console */
83
+ error: logError = console.error,
84
+ uuid = randomUUID,
85
+ headerRequestIdName = 'x-request-id',
86
+ headerRequestIpName = 'x-real-ip',
87
+ redactHeaders = DEFAULT_REDACTED
88
+ } = {}) =>
89
+ (req, res) => {
90
+ const time = performance.now();
91
+ const timestamp = Date.now();
92
+ const requestId =
93
+ res.getHeader(headerRequestIdName) || req.headers[headerRequestIdName] || uuid();
94
+ const ip = req.headers[headerRequestIpName];
95
+
96
+ if (!res.getHeader(headerRequestIdName)) {
97
+ res.setHeader(headerRequestIdName, requestId);
98
+ }
99
+
100
+ const info = {
101
+ requestId,
102
+ timestamp,
103
+ ip,
104
+ method: req.method,
105
+ url: req.url,
106
+ httpVersion: req.httpVersion,
107
+ host,
108
+ request: {
109
+ headers: redact(req.headers, redactHeaders),
110
+ encrypted: req.socket?.encrypted,
111
+ remoteAddress: req.socket?.remoteAddress,
112
+ remotePort: req.socket?.remotePort
113
+ }
114
+ };
115
+
116
+ res.on('finish', finish);
117
+ res.on('close', abort);
118
+ res.on('error', error);
119
+
120
+ return {...info};
121
+
122
+ /** Removes all response event listeners. */
123
+ function cleanup() {
124
+ res.removeListener('finish', finish);
125
+ res.removeListener('close', abort);
126
+ res.removeListener('error', error);
127
+ }
128
+
129
+ /**
130
+ * @param {boolean} aborted - Whether the connection was aborted before completion
131
+ */
132
+ function finish(aborted) {
133
+ cleanup();
134
+
135
+ info[aborted ? 'inprogressTime' : 'duration'] = performance.now() - time;
136
+ info.statusCode = res.statusCode;
137
+
138
+ info.response = {
139
+ headers: redact(res.getHeaders(), redactHeaders),
140
+ statusMessage: res.statusMessage,
141
+ writableFinished: res.writableFinished
142
+ };
143
+
144
+ log(info);
145
+ }
146
+
147
+ /** Handles connection close before response completes. */
148
+ function abort() {
149
+ finish(true);
150
+ }
151
+
152
+ /**
153
+ * @param {*} err - The error emitted by the response stream
154
+ */
155
+ function error(err) {
156
+ logError({
157
+ requestId,
158
+ timestamp,
159
+ name: err?.name,
160
+ message: err?.message,
161
+ status: err?.status,
162
+ statusCode: err?.statusCode,
163
+ originalError: err?.originalError,
164
+ stack: err?.stack
165
+ });
166
+ }
167
+ };
package/http/main.js ADDED
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @fileoverview Ergo Fast Fail REST API toolkit - main module export.
3
+ *
4
+ * Provides composable middleware for the four-stage Fast Fail request processing pipeline:
5
+ * 1. Negotiation - Parse/inspect request headers and URL (logger, cors, accepts, cookie, url)
6
+ * 2. Authorization - Authenticate user and verify request integrity (authorization, csrf)
7
+ * 3. Validation - Parse and validate request body (body, jsonApiQuery, validate)
8
+ * 4. Execution - Run the route handler and send the response (handler, send)
9
+ *
10
+ * Middleware is composed via the two-accumulator pattern using `compose`:
11
+ * compose([fn, setPath], ...)
12
+ *
13
+ * Each middleware factory returns `{value?, response?}`. Domain values are
14
+ * accumulated under named keys in `domainAcc`; response properties merge
15
+ * into `responseAcc`. `send()` is called post-pipeline by `handler()`.
16
+ *
17
+ * @module ergo
18
+ * @version 0.1.0
19
+ * @since 0.1.0
20
+ * @requires ./handler.js
21
+ * @requires ./accepts.js
22
+ * @requires ./authorization.js
23
+ * @requires ./body.js
24
+ * @requires ./cache-control.js
25
+ * @requires ./compress.js
26
+ * @requires ./cookie.js
27
+ * @requires ./cors.js
28
+ * @requires ./csrf.js
29
+ * @requires ./json-api-query.js
30
+ * @requires ./logger.js
31
+ * @requires ./prefer.js
32
+ * @requires ./precondition.js
33
+ * @requires ./rate-limit.js
34
+ * @requires ./url.js
35
+ * @requires ./security-headers.js
36
+ * @requires ./send.js
37
+ * @requires ./timeout.js
38
+ * @requires ./validate.js
39
+ * @requires ../utils/compose-with.js
40
+ * @requires ../utils/http-errors.js
41
+ * @requires ../lib/from-connect.js
42
+ *
43
+ * @example
44
+ * import {compose, handler, logger, cors, authorization, accepts,
45
+ * cookie, url, body} from 'ergo';
46
+ *
47
+ * const pipeline = compose(
48
+ * // Stage 1: Negotiation
49
+ * [logger(), 'log'],
50
+ * [cors(), 'cors'],
51
+ * [accepts({types: ['application/json']}), 'accepts'],
52
+ * [cookie(), 'cookies'],
53
+ * [url(), 'url'],
54
+ * // Stage 2: Authorization
55
+ * [authorization({strategies: [...]}), 'auth'],
56
+ * // Stage 3: Validation
57
+ * [body(), 'body'],
58
+ * // Stage 4: Execution
59
+ * (req, res, acc) => ({response: {body: acc.body.parsed}}),
60
+ * );
61
+ *
62
+ * export default handler(pipeline);
63
+ */
64
+
65
+ import compose, {createResponseAcc, mergeResponse} from '../utils/compose-with.js';
66
+ import handler from './handler.js';
67
+ import accepts from './accepts.js';
68
+ import authorization from './authorization.js';
69
+ import body from './body.js';
70
+ import cacheControl from './cache-control.js';
71
+ import compress from './compress.js';
72
+ import cookie from './cookie.js';
73
+ import cors from './cors.js';
74
+ import csrf from './csrf.js';
75
+ import jsonApiQuery from './json-api-query.js';
76
+ import logger from './logger.js';
77
+ import url from './url.js';
78
+ import prefer from './prefer.js';
79
+ import precondition from './precondition.js';
80
+ import rateLimit from './rate-limit.js';
81
+ import securityHeaders from './security-headers.js';
82
+ import send from './send.js';
83
+ import timeout from './timeout.js';
84
+ import validate from './validate.js';
85
+ import httpErrors from '../utils/http-errors.js';
86
+ import fromConnect from '../lib/from-connect.js';
87
+
88
+ export {
89
+ compose,
90
+ createResponseAcc,
91
+ mergeResponse,
92
+ handler,
93
+ accepts,
94
+ authorization,
95
+ body,
96
+ cacheControl,
97
+ compress,
98
+ cookie,
99
+ cors,
100
+ csrf,
101
+ fromConnect,
102
+ httpErrors,
103
+ jsonApiQuery,
104
+ logger,
105
+ prefer,
106
+ precondition,
107
+ rateLimit,
108
+ securityHeaders,
109
+ url,
110
+ send,
111
+ timeout,
112
+ validate
113
+ };
114
+
115
+ // Default export mirrors the named exports above — keep both in sync.
116
+ /** @type {object} */
117
+ export default {
118
+ compose,
119
+ handler,
120
+ accepts,
121
+ authorization,
122
+ body,
123
+ cacheControl,
124
+ compress,
125
+ cookie,
126
+ cors,
127
+ csrf,
128
+ fromConnect,
129
+ httpErrors,
130
+ jsonApiQuery,
131
+ logger,
132
+ prefer,
133
+ precondition,
134
+ rateLimit,
135
+ securityHeaders,
136
+ url,
137
+ send,
138
+ timeout,
139
+ validate
140
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @fileoverview Precondition Required middleware (RFC 6585 §3).
3
+ *
4
+ * Enforces that unsafe requests include a conditional header (`If-Match` or
5
+ * `If-Unmodified-Since`) before the pipeline proceeds. This prevents "lost update"
6
+ * problems where a client overwrites changes made by another client without first
7
+ * fetching the current resource state.
8
+ *
9
+ * Placed in Stage 1 (Negotiation) for Fast Fail — the check is a cheap header
10
+ * inspection that short-circuits before authorization, body parsing, or execution.
11
+ *
12
+ * @module http/precondition
13
+ * @version 0.1.0
14
+ * @since 0.1.0
15
+ *
16
+ * @example
17
+ * import {compose, precondition} from 'ergo';
18
+ *
19
+ * // Enforce on all requests (method scoping handled by pipeline builder)
20
+ * const pipeline = compose(
21
+ * [precondition(), 'precondition'],
22
+ * (req, res, acc) => ({response: {statusCode: 200, body: {updated: true}}})
23
+ * );
24
+ *
25
+ * // Enforce only on specific methods (standalone usage)
26
+ * const pipeline = compose(
27
+ * [precondition({methods: ['PUT', 'PATCH']}), 'precondition'],
28
+ * (req, res, acc) => ({response: {statusCode: 200, body: {updated: true}}})
29
+ * );
30
+ *
31
+ * @see {@link https://www.rfc-editor.org/rfc/rfc6585#section-3 RFC 6585 Section 3 - 428 Precondition Required}
32
+ */
33
+
34
+ /**
35
+ * Create a precondition enforcement middleware.
36
+ *
37
+ * @param {object} [options]
38
+ * @param {string[]|Set<string>} [options.methods] - HTTP methods to enforce on.
39
+ * When omitted, enforces unconditionally (the pipeline builder handles method scoping).
40
+ * When provided, only activates for the specified methods.
41
+ * @returns {function} - Middleware `(req) => void` that returns `{response: {statusCode: 428}}` if no conditional header is present
42
+ */
43
+ export default function precondition({methods} = {}) {
44
+ const methodSet = methods ? (methods instanceof Set ? methods : new Set(methods)) : undefined;
45
+
46
+ return req => {
47
+ if (methodSet && !methodSet.has(req.method)) return;
48
+
49
+ if (!req.headers['if-match'] && !req.headers['if-unmodified-since']) {
50
+ return {response: {statusCode: 428}};
51
+ }
52
+ };
53
+ }
package/http/prefer.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @fileoverview HTTP middleware factory for Prefer header parsing (RFC 7240).
3
+ *
4
+ * Parses the `Prefer` request header and returns a preferences object for the
5
+ * accumulator. Combined with `send()`'s `preferKey` option, enables automatic
6
+ * `return=minimal` / `return=representation` response handling.
7
+ *
8
+ * Placed in Stage 1 (Negotiation) — cheap header parse with no I/O.
9
+ *
10
+ * @module http/prefer
11
+ * @version 0.1.0
12
+ * @since 0.1.0
13
+ * @requires ../lib/prefer.js
14
+ *
15
+ * @example
16
+ * import {compose, prefer} from 'ergo';
17
+ *
18
+ * const pipeline = compose(
19
+ * [prefer(), 'prefer'],
20
+ * (req, res, acc) => ({response: {body: {id: 1, name: 'item'}}}),
21
+ * );
22
+ * // Client sends: Prefer: return=minimal
23
+ * // Response: 204 No Content + Preference-Applied: return=minimal
24
+ *
25
+ * @see {@link https://www.rfc-editor.org/rfc/rfc7240 RFC 7240 - Prefer Header for HTTP}
26
+ */
27
+ import parsePrefer from '../lib/prefer.js';
28
+
29
+ /**
30
+ * Creates a Prefer header parsing middleware.
31
+ *
32
+ * @returns {function} - Middleware `(req) => object` returning parsed preferences
33
+ */
34
+ export default () => {
35
+ return req => parsePrefer(req.headers?.prefer);
36
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @fileoverview Rate limiting pipeline middleware (RFC 6585 §4).
3
+ *
4
+ * Tracks request counts per client key within a configurable time window.
5
+ * Returns rate-limit header tuples for the response accumulator on allowed requests;
6
+ * returns `{response: {statusCode: 429, retryAfter}}` when the limit is exceeded.
7
+ *
8
+ * Placed in Stage 1 (Negotiation) for Fast Fail — the check is a cheap
9
+ * counter lookup that short-circuits before authorization, body parsing,
10
+ * or execution.
11
+ *
12
+ * @module http/rate-limit
13
+ * @version 0.1.0
14
+ * @since 0.1.0
15
+ * @requires ../lib/rate-limit.js
16
+ *
17
+ * @example
18
+ * import {compose, rateLimit} from 'ergo';
19
+ *
20
+ * const pipeline = compose(
21
+ * [rateLimit({max: 100, windowMs: 60000}), 'rateLimit'],
22
+ * (req, res, acc) => ({response: {statusCode: 200, body: {ok: true}}})
23
+ * );
24
+ *
25
+ * @see {@link https://www.rfc-editor.org/rfc/rfc6585#section-4 RFC 6585 Section 4 - 429 Too Many Requests}
26
+ */
27
+ import {MemoryStore, checkRateLimit, defaultKeyGenerator} from '../lib/rate-limit.js';
28
+
29
+ /**
30
+ * Create a rate limiting middleware.
31
+ *
32
+ * @param {object} [options]
33
+ * @param {number} [options.max=100] - Maximum requests per window
34
+ * @param {number} [options.windowMs=60000] - Window size in milliseconds (default: 1 minute)
35
+ * @param {object} [options.store] - Pluggable store (must implement `hit(key, windowMs)`)
36
+ * @param {function} [options.keyGenerator] - `(req) => string` client identifier (default: remote IP)
37
+ * @returns {function} - Middleware `(req) => {response}` that returns rate-limit header tuples on allowed
38
+ * requests and `{response: {statusCode: 429, retryAfter}}` when the limit is exceeded
39
+ */
40
+ export default function rateLimit({max = 100, windowMs = 60000, store, keyGenerator} = {}) {
41
+ const _store = store ?? new MemoryStore();
42
+ const _keyGen = keyGenerator ?? defaultKeyGenerator;
43
+
44
+ return req => {
45
+ const result = checkRateLimit(_store, _keyGen(req), max, windowMs);
46
+
47
+ if (result.limited) {
48
+ return {
49
+ response: {
50
+ statusCode: 429,
51
+ retryAfter: result.retryAfter
52
+ }
53
+ };
54
+ }
55
+
56
+ return {
57
+ response: {
58
+ headers: [
59
+ ['X-RateLimit-Limit', String(max)],
60
+ ['X-RateLimit-Remaining', String(result.remaining)],
61
+ ['X-RateLimit-Reset', String(result.reset)]
62
+ ]
63
+ }
64
+ };
65
+ };
66
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @fileoverview HTTP middleware factory for security response headers.
3
+ *
4
+ * Returns pre-computed header tuples for common security headers recommended for
5
+ * REST APIs. Each header is individually configurable or disableable (pass `false`).
6
+ * Header tuples are built at factory time for zero per-request overhead.
7
+ *
8
+ * Delegates tuple construction to `lib/security-headers.js` (the shared primitive).
9
+ *
10
+ * @module http/security-headers
11
+ * @version 0.1.0
12
+ * @since 0.1.0
13
+ * @requires ../lib/security-headers.js
14
+ *
15
+ * @example
16
+ * import {compose, securityHeaders} from 'ergo';
17
+ *
18
+ * // Use defaults
19
+ * const pipeline = compose(
20
+ * [securityHeaders(), 'security'],
21
+ * // ...
22
+ * );
23
+ *
24
+ * // Customize or disable individual headers
25
+ * const pipeline = compose(
26
+ * [securityHeaders({
27
+ * xFrameOptions: 'SAMEORIGIN',
28
+ * permissionsPolicy: 'camera=(), microphone=()',
29
+ * xXssProtection: false // disable
30
+ * }), 'security'],
31
+ * );
32
+ *
33
+ * @see {@link https://www.rfc-editor.org/rfc/rfc6797 RFC 6797 - HTTP Strict Transport Security}
34
+ * @see {@link https://www.w3.org/TR/CSP3/ W3C Content Security Policy Level 3}
35
+ */
36
+ import buildSecurityHeaderTuples from '../lib/security-headers.js';
37
+
38
+ /**
39
+ * Creates a security headers middleware that returns pre-computed header tuples.
40
+ *
41
+ * Pass `false` for any header to omit it entirely. Pass a string to override
42
+ * the default value.
43
+ *
44
+ * @param {object} [options] - Security header configuration
45
+ * @param {string|false} [options.contentSecurityPolicy="default-src 'none'"] - Content-Security-Policy header
46
+ * @param {string|false} [options.strictTransportSecurity=false] - Strict-Transport-Security header.
47
+ * Defaults to `false` because this middleware has no request context to verify the connection
48
+ * is HTTPS, and HSTS MUST only be sent over secure transport (RFC 6797 §7.2). Enable explicitly
49
+ * when the app is known to be behind HTTPS, or use ergo-router's transport layer which performs
50
+ * the HTTPS check automatically.
51
+ * @param {string|false} [options.xContentTypeOptions='nosniff'] - X-Content-Type-Options header
52
+ * @param {string|false} [options.xFrameOptions='DENY'] - X-Frame-Options header
53
+ * @param {string|false} [options.referrerPolicy='no-referrer'] - Referrer-Policy header
54
+ * @param {string|false} [options.xXssProtection='0'] - X-XSS-Protection header (0 disables the browser filter)
55
+ * @param {string} [options.permissionsPolicy] - Permissions-Policy header (omitted by default)
56
+ * @returns {function} - Ergo middleware `() => Array<[string, string]>`
57
+ */
58
+ export default (options = {}) => {
59
+ const headerTuples = buildSecurityHeaderTuples(options);
60
+ const response = {response: {headers: headerTuples}};
61
+ return () => response;
62
+ };