@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
@@ -0,0 +1,232 @@
1
+ /**
2
+ * @fileoverview Path-based compose utility for Ergo middleware pipelines (v2).
3
+ *
4
+ * Extends `utils/compose` with a two-accumulator model: a **domain accumulator** for
5
+ * inter-middleware data and a **response accumulator** for HTTP response construction.
6
+ *
7
+ * Each operation can be expressed as:
8
+ * - A plain function: `fn` (receives `(...args, domainAcc)`)
9
+ * - A tuple: `[fn, setPath]` where:
10
+ * - `fn` — the middleware function
11
+ * - `setPath` — the domain accumulator key to store the return value under
12
+ *
13
+ * Middleware returns are interpreted as `{value?, response?}`:
14
+ * - `value` is merged into the domain accumulator (at `setPath` for tuples, or
15
+ * `Object.assign` for plain functions)
16
+ * - `response` is merged into the response accumulator (headers append, scalars set)
17
+ * - When `responseAcc.statusCode` is set, serial iteration breaks immediately
18
+ * - `undefined` / `null` returns skip all merges
19
+ *
20
+ * Plain return values (without `value` or `response` keys) are treated as `{value: ret}`
21
+ * for compatibility with simple middleware that only produce domain data.
22
+ *
23
+ * @module utils/compose-with
24
+ * @version 0.2.0
25
+ * @since 0.1.0
26
+ * @requires ./compose.js
27
+ * @requires ./set.js
28
+ *
29
+ * @example
30
+ * import compose, {createResponseAcc} from 'ergo/utils/compose-with';
31
+ *
32
+ * const responseAcc = createResponseAcc();
33
+ * const pipeline = compose(
34
+ * [logger(), 'log'],
35
+ * [authorization({...}), 'auth'],
36
+ * [body(), 'body'],
37
+ * (req, res, acc) => ({response: {body: process(acc.body), statusCode: 200}})
38
+ * );
39
+ *
40
+ * await pipeline(req, res, responseAcc, domainAcc);
41
+ * // responseAcc.statusCode, responseAcc.headers, etc.
42
+ */
43
+ import {accumulator} from './compose.js';
44
+ import set from './set.js';
45
+
46
+ /**
47
+ * Creates a null-prototype response accumulator.
48
+ *
49
+ * The returned object has a non-enumerable `isResponseAcc` flag used by the pipeline
50
+ * to distinguish it from the domain accumulator in the argument list.
51
+ *
52
+ * @returns {object} - Null-prototype object with `isResponseAcc: true`
53
+ */
54
+ export function createResponseAcc() {
55
+ const acc = Object.create(null);
56
+ Object.defineProperty(acc, 'isResponseAcc', {value: true});
57
+ return acc;
58
+ }
59
+
60
+ /**
61
+ * Merges a response patch into the response accumulator.
62
+ *
63
+ * - `headers` arrays are **appended** (additive across middleware)
64
+ * - All other properties are **assigned** (last writer wins)
65
+ *
66
+ * @param {object} responseAcc - Mutable response accumulator
67
+ * @param {object} patch - Response contribution from middleware
68
+ */
69
+ export function mergeResponse(responseAcc, patch) {
70
+ if (patch.headers) {
71
+ responseAcc.headers ??= [];
72
+ responseAcc.headers.push(...patch.headers);
73
+ }
74
+
75
+ for (const key of Object.keys(patch)) {
76
+ if (key !== 'headers') responseAcc[key] = patch[key];
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Extracts `{value, response}` from a middleware return value.
82
+ *
83
+ * If the return is an object with a `value` or `response` key, those are used directly.
84
+ * Otherwise the entire return is treated as `{value: ret}` for backward compatibility
85
+ * with middleware that return plain domain data.
86
+ *
87
+ * @param {*} resolved - Resolved return value from middleware
88
+ * @returns {{value: *, response: *}} - Extracted value and response
89
+ */
90
+ function extractReturn(resolved) {
91
+ if (
92
+ resolved !== null &&
93
+ typeof resolved === 'object' &&
94
+ !Array.isArray(resolved) &&
95
+ ('value' in resolved || 'response' in resolved)
96
+ ) {
97
+ return resolved;
98
+ }
99
+
100
+ return {value: resolved};
101
+ }
102
+
103
+ /**
104
+ * Converts an operation spec into a normalized descriptor.
105
+ *
106
+ * @param {function|Array} op - A plain function or `[fn, setPath]` tuple
107
+ * @returns {{fn: function, setPath: string|undefined}} - Normalized descriptor
108
+ */
109
+ function normalizeOp(op) {
110
+ if (!Array.isArray(op)) return {fn: op, setPath: undefined};
111
+ return {fn: op[0], setPath: op[1]};
112
+ }
113
+
114
+ /**
115
+ * Runs middleware descriptors sequentially with two-accumulator support.
116
+ *
117
+ * Uses a sync fast-path: when a middleware returns a non-thenable value, merging
118
+ * happens immediately without a microtask hop.
119
+ *
120
+ * @param {Array<{fn: function, setPath: string|undefined}>} descriptors - Normalized ops
121
+ * @param {*[]} args - Original request arguments (req, res, etc.)
122
+ * @param {object} domainAcc - Domain accumulator
123
+ * @param {object} responseAcc - Response accumulator
124
+ * @returns {object} - Final domain accumulator
125
+ */
126
+ async function serial(descriptors, args, domainAcc, responseAcc) {
127
+ for (const {fn, setPath} of descriptors) {
128
+ const raw = fn(...args, domainAcc, responseAcc);
129
+ const resolved = typeof raw?.then === 'function' ? await raw : raw;
130
+
131
+ if (resolved == null) continue;
132
+
133
+ const {value, response} = extractReturn(resolved);
134
+
135
+ if (value !== undefined) {
136
+ if (setPath !== undefined) {
137
+ set(domainAcc, setPath, value);
138
+ } else if (typeof value === 'object') {
139
+ Object.assign(domainAcc, value);
140
+ }
141
+ }
142
+
143
+ if (response !== undefined) {
144
+ mergeResponse(responseAcc, response);
145
+ }
146
+
147
+ if (responseAcc.statusCode !== undefined) break;
148
+ }
149
+
150
+ return domainAcc;
151
+ }
152
+
153
+ /**
154
+ * Runs middleware descriptors concurrently with two-accumulator support.
155
+ *
156
+ * Each branch receives an isolated domain accumulator copy. After all branches
157
+ * complete, values and responses are merged in declaration order.
158
+ *
159
+ * @param {Array<{fn: function, setPath: string|undefined}>} descriptors - Normalized ops
160
+ * @param {*[]} args - Original request arguments
161
+ * @param {object} domainAcc - Domain accumulator
162
+ * @param {object} responseAcc - Response accumulator
163
+ * @returns {object} - Final domain accumulator
164
+ */
165
+ async function concurrent(descriptors, args, domainAcc, responseAcc) {
166
+ const copies = descriptors.map(() => Object.assign(accumulator(), domainAcc));
167
+ const rets = descriptors.map(({fn}, i) => fn(...args, copies[i], responseAcc));
168
+ const hasAsync = rets.some(r => typeof r?.then === 'function');
169
+ const results = hasAsync ? await Promise.all(rets) : rets;
170
+
171
+ for (let i = 0; i < descriptors.length; i++) {
172
+ const resolved = results[i];
173
+ if (resolved == null) continue;
174
+
175
+ const {setPath} = descriptors[i];
176
+ const {value, response} = extractReturn(resolved);
177
+
178
+ if (value !== undefined) {
179
+ if (setPath !== undefined) {
180
+ set(domainAcc, setPath, value);
181
+ } else if (typeof value === 'object') {
182
+ Object.assign(domainAcc, value);
183
+ }
184
+ }
185
+
186
+ if (response !== undefined) {
187
+ mergeResponse(responseAcc, response);
188
+ }
189
+ }
190
+
191
+ return domainAcc;
192
+ }
193
+
194
+ /**
195
+ * Composes middleware with path-based result storage and two-accumulator support.
196
+ *
197
+ * The composed function pops the domain accumulator (last arg with `isAccumulator`)
198
+ * and response accumulator (last arg with `isResponseAcc`) from its arguments.
199
+ * If either is absent, a fresh one is created.
200
+ *
201
+ * @param {...(function|Array)} ops - Operation specs; each is a function or `[fn, setPath]`
202
+ * @returns {function} - Async composed pipeline
203
+ */
204
+ const composeWith = (...ops) => {
205
+ const descriptors = ops.map(normalizeOp);
206
+
207
+ return async (...args) => {
208
+ const domainAcc = args.at(-1)?.isAccumulator ? args.pop() : accumulator();
209
+ const responseAcc = args.at(-1)?.isResponseAcc ? args.pop() : createResponseAcc();
210
+
211
+ return await serial(descriptors, args, domainAcc, responseAcc);
212
+ };
213
+ };
214
+
215
+ /**
216
+ * Concurrent variant of composeWith.
217
+ *
218
+ * @param {...(function|Array)} ops - Operation specs
219
+ * @returns {function} - Async composed pipeline
220
+ */
221
+ composeWith.all = (...ops) => {
222
+ const descriptors = ops.map(normalizeOp);
223
+
224
+ return async (...args) => {
225
+ const domainAcc = args.at(-1)?.isAccumulator ? args.pop() : accumulator();
226
+ const responseAcc = args.at(-1)?.isResponseAcc ? args.pop() : createResponseAcc();
227
+
228
+ return await concurrent(descriptors, args, domainAcc, responseAcc);
229
+ };
230
+ };
231
+
232
+ export default composeWith;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @fileoverview Async pipeline composition utility.
3
+ *
4
+ * Creates an async pipeline from a list of middleware functions. Each function receives
5
+ * the original arguments plus the accumulated state object from prior middleware.
6
+ *
7
+ * - `compose(...fns)` — runs functions sequentially (`serial`)
8
+ * - `compose.all(...fns)` — runs functions concurrently and merges all results
9
+ * - `compose.withOptions(options, ...fns)` — sequential with options (e.g. `breakWhen`)
10
+ * - `compose.all.withOptions(options, ...fns)` — concurrent with options
11
+ *
12
+ * State is accumulated into a null-prototype accumulator object with an
13
+ * `isAccumulator: true` flag and a `size` getter. If the last argument passed to the
14
+ * composed pipeline is already an accumulator object, it is reused for accumulation.
15
+ *
16
+ * Serial composition uses a sync fast-path: when a middleware returns a non-thenable
17
+ * value, the result is merged immediately without scheduling a microtask. This
18
+ * eliminates unnecessary `await` overhead for synchronous middleware (the majority
19
+ * of ergo's built-in middleware).
20
+ *
21
+ * @module utils/compose
22
+ * @version 0.2.0
23
+ * @since 0.1.0
24
+ *
25
+ * @example
26
+ * import compose from 'ergo/utils/compose';
27
+ *
28
+ * const pipeline = compose(
29
+ * async (req, res) => ({user: await getUser(req)}),
30
+ * async (req, res, {user}) => ({role: await getRole(user)})
31
+ * );
32
+ *
33
+ * const result = await pipeline(req, res);
34
+ * // result.user, result.role
35
+ *
36
+ * @example
37
+ * // Early termination with breakWhen
38
+ * const pipeline = compose.withOptions(
39
+ * {breakWhen: (acc) => acc.done},
40
+ * () => ({done: true, a: 1}),
41
+ * () => ({b: 2}) // never reached
42
+ * );
43
+ */
44
+ /**
45
+ * Composes middleware functions into an async pipeline with result accumulation.
46
+ *
47
+ * @param {...function} fns - Middleware functions to compose
48
+ * @returns {function} - Async composed pipeline
49
+ */
50
+ const compose = (...fns) => setup(serial, fns);
51
+ compose.all = (...fns) => setup(concurrent, fns);
52
+
53
+ /**
54
+ * Creates a sequential pipeline with configuration options.
55
+ *
56
+ * @param {object} options - Pipeline options
57
+ * @param {function} [options.breakWhen] - Predicate `(acc) => boolean`; when truthy,
58
+ * serial iteration stops after the current step's result is merged
59
+ * @param {...function} fns - Middleware functions to compose
60
+ * @returns {function} - Async composed pipeline
61
+ */
62
+ compose.withOptions = (options, ...fns) => setup(serial, fns, options);
63
+
64
+ /**
65
+ * Creates a concurrent pipeline with configuration options.
66
+ *
67
+ * @param {object} options - Pipeline options
68
+ * @param {...function} fns - Middleware functions to compose
69
+ * @returns {function} - Async composed pipeline
70
+ */
71
+ compose.all.withOptions = (options, ...fns) => setup(concurrent, fns, options);
72
+
73
+ export default compose;
74
+ export {accumulator};
75
+
76
+ /**
77
+ * Runs middleware functions sequentially, accumulating results.
78
+ *
79
+ * Uses a sync fast-path: if a function returns a non-thenable value the result
80
+ * is merged immediately, avoiding a microtask hop for synchronous middleware.
81
+ * The accumulator is passed directly (no defensive copy) because serial steps
82
+ * cannot observe concurrent mutations.
83
+ *
84
+ * When `breakWhen` is provided, the predicate is evaluated after each step's
85
+ * result is merged. If it returns a truthy value, iteration stops immediately.
86
+ *
87
+ * @param {function[]} fns - Ordered middleware functions
88
+ * @param {*[]} args - Original request arguments (req, res, etc.)
89
+ * @param {object} acc - Accumulator
90
+ * @param {object} [options] - Pipeline options
91
+ * @param {function} [options.breakWhen] - Predicate `(acc) => boolean`
92
+ * @returns {object} - Final accumulated state
93
+ */
94
+ async function serial(fns, args, acc, {breakWhen} = {}) {
95
+ for (const f of fns) {
96
+ const ret = f(...args, acc);
97
+ Object.assign(acc, typeof ret?.then === 'function' ? await ret : ret);
98
+
99
+ if (breakWhen?.(acc)) break;
100
+ }
101
+
102
+ return acc;
103
+ }
104
+
105
+ /**
106
+ * Runs middleware functions concurrently, merging all results.
107
+ *
108
+ * Each branch receives an isolated accumulator copy so concurrent functions
109
+ * cannot observe each other's mutations. When every branch returns a
110
+ * non-thenable value, `Promise.all` is skipped entirely.
111
+ *
112
+ * @param {function[]} fns - Middleware functions to run concurrently
113
+ * @param {*[]} args - Original request arguments
114
+ * @param {object} acc - Accumulator
115
+ * @param {object} [_options] - Pipeline options (unused for concurrent; accepted for API symmetry)
116
+ * @returns {object} - Merged accumulated state
117
+ */
118
+ async function concurrent(fns, args, acc, _options) {
119
+ const copies = fns.map(() => accumulator(acc));
120
+ const rets = fns.map((f, i) => f(...args, copies[i]));
121
+ const hasAsync = rets.some(r => typeof r?.then === 'function');
122
+
123
+ if (hasAsync) {
124
+ return Object.assign(acc, ...(await Promise.all(rets)));
125
+ }
126
+
127
+ return Object.assign(acc, ...rets);
128
+ }
129
+
130
+ /**
131
+ * Creates a composed async function with an initialized accumulator.
132
+ *
133
+ * @param {function} processor - `serial` or `concurrent`
134
+ * @param {function[]} fns - Middleware functions
135
+ * @param {object} [options] - Pipeline options forwarded to the processor
136
+ * @returns {function} - Async composed function `(...args) => Accumulator`
137
+ */
138
+ function setup(processor, fns, options = {}) {
139
+ return async (...args) => {
140
+ const acc = args.length && args.at(-1)?.isAccumulator ? args.pop() : accumulator();
141
+
142
+ return await processor(fns, args, acc, options);
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Creates a null-prototype accumulator object.
148
+ *
149
+ * @param {object} [defaults={}] - Initial properties to copy into the accumulator
150
+ * @returns {object} - Null-prototype accumulator with `isAccumulator: true` and `size` getter
151
+ */
152
+ function accumulator(defaults = {}) {
153
+ const acc = Object.create(null);
154
+
155
+ Object.defineProperties(acc, {
156
+ isAccumulator: {value: true},
157
+ size: {
158
+ get() {
159
+ return Object.keys(this).length;
160
+ }
161
+ }
162
+ });
163
+
164
+ return Object.assign(acc, defaults);
165
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @fileoverview Variadic flat-array utility.
3
+ *
4
+ * Flattens all arguments into a single array using `Array.prototype.flat()`.
5
+ * Equivalent to `[...args].flat()` — accepts any mix of scalars and nested arrays.
6
+ *
7
+ * @module utils/flat-array
8
+ * @version 0.1.0
9
+ * @since 0.1.0
10
+ *
11
+ * @example
12
+ * import flatArray from 'ergo/utils/flat-array';
13
+ *
14
+ * flatArray('a', ['b', 'c'], 'd') // => ['a', 'b', 'c', 'd']
15
+ * flatArray(1, [2, 3], 4) // => [1, 2, 3, 4]
16
+ */
17
+
18
+ /**
19
+ * @param {...*} items - Values or nested arrays to flatten
20
+ * @returns {Array} - Single-level flattened array
21
+ */
22
+ export default (...items) => {
23
+ return items.flat();
24
+ };
package/utils/get.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @fileoverview Deep property getter with dot-notation path support.
3
+ *
4
+ * Traverses a nested object using a dot-delimited path string. Optionally:
5
+ * - Optionally invokes function values at each step (`invoke: false`, default)
6
+ * - Returns `undefined` gracefully when a step is missing (`safe: true`)
7
+ *
8
+ * @module utils/get
9
+ * @version 0.1.0
10
+ * @since 0.1.0
11
+ *
12
+ * @example
13
+ * import get from 'ergo/utils/get';
14
+ *
15
+ * get({a: {b: {c: 42}}}, 'a.b.c') // => 42
16
+ * get({a: null}, 'a.b.c', {safe: true}) // => undefined (no throw)
17
+ */
18
+
19
+ /**
20
+ * @param {object} [obj={}] - Source object
21
+ * @param {string} [path=''] - Dot-delimited property path
22
+ * @param {object} [options] - Traversal options
23
+ * @param {boolean} [options.safe=false] - Return undefined instead of throwing on missing paths
24
+ * @param {boolean} [options.invoke=false] - Call function values at each path step
25
+ * @returns {*} - Value at the path, or undefined in safe mode
26
+ */
27
+ export default (obj = {}, path = '', {safe = false, invoke = false} = {}) => {
28
+ let val = obj;
29
+
30
+ for (const subPath of path.split('.')) {
31
+ if (val == null && safe) {
32
+ return undefined;
33
+ }
34
+
35
+ val = typeof val[subPath] === 'function' && invoke ? val[subPath]() : val[subPath];
36
+ }
37
+
38
+ return val;
39
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @fileoverview HTTP error factory producing RFC 9457 Problem Details errors.
3
+ *
4
+ * Creates `Error`-prototype objects with RFC 9457 (Problem Details for HTTP APIs)
5
+ * properties: `type`, `title`, `status`, `detail`, plus internal properties `headers`
6
+ * and `originalError`. Uses `Object.create(Error.prototype)` instead of `new Error()`
7
+ * to avoid V8's automatic stack trace capture (~10 µs overhead per call). This is
8
+ * critical in hot paths like rate-limit rejections where errors are expected control
9
+ * flow, not exceptional conditions. The objects pass `instanceof Error` checks.
10
+ *
11
+ * The `toJSON()` method serializes to RFC 9457 format automatically when
12
+ * `JSON.stringify()` is called (e.g. by `send()`).
13
+ *
14
+ * All Ergo middleware throws these errors directly. The `handler()` middleware
15
+ * catches them and forwards to `send()` for serialization.
16
+ *
17
+ * @module utils/http-errors
18
+ * @version 0.1.0
19
+ * @since 0.1.0
20
+ * @requires node:http
21
+ *
22
+ * @see {@link https://www.rfc-editor.org/rfc/rfc9457 RFC 9457 - Problem Details for HTTP APIs}
23
+ *
24
+ * @example
25
+ * import httpErrors from 'ergo/utils/http-errors';
26
+ *
27
+ * throw httpErrors(404);
28
+ * // Error { name: 'NotFound', status: 404, type: 'https://...', title: 'Not Found', detail: 'Not Found' }
29
+ * // JSON.stringify → {"type":"https://...","title":"Not Found","status":404,"detail":"Not Found"}
30
+ *
31
+ * throw httpErrors(422, {detail: 'Invalid email', details: errors});
32
+ * // Error { name: 'UnprocessableEntity', status: 422, detail: 'Invalid email', details: [...] }
33
+ */
34
+ import {STATUS_CODES} from 'node:http';
35
+
36
+ const baseUrl = 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/';
37
+
38
+ /**
39
+ * @param {number} [statusCode=500] - HTTP status code
40
+ * @param {object} [options] - RFC 9457 Problem Details fields
41
+ * @param {string} [options.type] - RFC 9457 `type` URI (defaults to MDN docs link)
42
+ * @param {string} [options.detail] - RFC 9457 `detail` (human-readable explanation)
43
+ * @param {string} [options.message] - Alias for `detail` (backward compat)
44
+ * @param {Array<[string, string]>} [options.headers] - Response headers to attach
45
+ * @param {string} [options.instance] - RFC 9457 `instance` URI identifying the specific occurrence
46
+ * @param {number|string} [options.retryAfter] - Retry-After value (seconds or HTTP-date).
47
+ * Auto-appended to `headers` as `['Retry-After', String(value)]` and included in toJSON().
48
+ * @param {Error} [options.originalError] - Underlying error
49
+ * @returns {Error} - Error with RFC 9457 properties and `toJSON()` method
50
+ */
51
+ export default function httpErrors(
52
+ statusCode = 500,
53
+ {
54
+ type = STATUS_CODES[statusCode] ? `${baseUrl}${statusCode}` : undefined,
55
+ message,
56
+ detail: rawDetail = message,
57
+ headers,
58
+ instance,
59
+ retryAfter,
60
+ originalError,
61
+ ...extra
62
+ } = {}
63
+ ) {
64
+ const title = STATUS_CODES[statusCode] ?? STATUS_CODES[500];
65
+ const detail = rawDetail ?? title;
66
+ const name = title.replace(/\s/g, '');
67
+
68
+ const effectiveHeaders =
69
+ retryAfter != null ? [...(headers ?? []), ['Retry-After', String(retryAfter)]] : headers;
70
+
71
+ if (retryAfter != null) {
72
+ extra.retryAfter = retryAfter;
73
+ }
74
+
75
+ const err = Object.create(Error.prototype);
76
+ err.message = detail;
77
+ err.name = name;
78
+ err.statusCode = statusCode;
79
+ err.status = statusCode;
80
+ err.type = type;
81
+ err.title = title;
82
+ err.detail = detail;
83
+ err.instance = instance;
84
+ err.headers = effectiveHeaders;
85
+ err.originalError = originalError;
86
+ if (extra) Object.assign(err, extra);
87
+ err.message = detail;
88
+ err.name = name;
89
+ err.statusCode = statusCode;
90
+ err.status = statusCode;
91
+ err.type = type;
92
+ err.title = title;
93
+ err.detail = detail;
94
+
95
+ const extraKeys = Object.keys(extra);
96
+
97
+ Object.defineProperty(err, 'toJSON', {
98
+ value() {
99
+ const json = {};
100
+ for (const key of extraKeys) {
101
+ json[key] = this[key];
102
+ }
103
+ json.type = this.type;
104
+ json.title = this.title;
105
+ json.status = this.status;
106
+ json.detail = this.detail;
107
+ if (this.instance !== undefined) json.instance = this.instance;
108
+ return json;
109
+ }
110
+ });
111
+
112
+ return err;
113
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @fileoverview Streaming Buffer split generator for async iterable pipelines.
3
+ *
4
+ * Creates a generator function that consumes an iterable of Buffer chunks and yields
5
+ * `[index, buffer]` pairs split by the given separator. Handles partial matches across
6
+ * chunk boundaries by carrying over unmatched bytes to the next iteration.
7
+ *
8
+ * Used by `lib/body/multiparse.js` for multipart boundary splitting.
9
+ *
10
+ * @module utils/iterables/buffer-split
11
+ * @version 0.1.0
12
+ * @since 0.1.0
13
+ * @requires ../buffers/split.js
14
+ *
15
+ * @example
16
+ * import {chain, bufferSplit, reduce} from 'ergo/utils/iterables';
17
+ *
18
+ * const chunks = [Buffer.from('a--b--c')];
19
+ * const result = chain(chunks, bufferSplit('--'), reduce((acc, [, b]) => [...acc, b.toString()], []));
20
+ * // => ['a', 'b', 'c']
21
+ */
22
+ import split from '../buffers/split.js';
23
+ /**
24
+ * Creates a Buffer-splitting generator.
25
+ *
26
+ * @param {import('node:buffer').Buffer|string} [separator=Buffer.from('')] - Byte pattern to split on
27
+ * @param {number} [limit=Infinity] - Maximum number of parts to yield
28
+ * @returns {function} - Generator function `(iterable) => Generator<[number, import('node:buffer').Buffer]>`
29
+ */
30
+ export default (separator = Buffer.from(''), limit = Infinity) => {
31
+ const sepBuffer = Buffer.isBuffer(separator) ? separator : Buffer.from(separator);
32
+
33
+ /**
34
+ * @param {Iterable} iterable - Source iterable of Buffers to split
35
+ */
36
+ return function* (iterable) {
37
+ let partialCarryover = Buffer.from('');
38
+ let partialCarryoverIndexInc = false;
39
+ let index = 0;
40
+
41
+ try {
42
+ let buffers;
43
+ let lookup;
44
+ let partial;
45
+
46
+ for (let chunk of iterable) {
47
+ if (index >= limit) {
48
+ return;
49
+ }
50
+
51
+ // Append the previous iteration's carryover from a partial match
52
+ // for the separator.
53
+ const pCoLen = partialCarryover.length;
54
+
55
+ if (pCoLen > 0) {
56
+ chunk = Buffer.concat([partialCarryover, chunk], pCoLen + chunk.length);
57
+ }
58
+
59
+ const incStart = partialCarryoverIndexInc && partial ? -1 : 0;
60
+
61
+ ({buffers, lookup, partial} = split(chunk, sepBuffer, {limit, lookup, partial}));
62
+ /*
63
+ buffers:
64
+ A: [n] no matches
65
+ B: [0, 0] one match, entire chunk
66
+ C: [0, x[, ...]] one match at beginning and at least one more match after
67
+ D: [x[, ...], 0] one match at end and at least one more match before
68
+ E: [x[, ...], y] at least one match in the middle
69
+
70
+ index increment:
71
+ A: none
72
+ B: on second element
73
+ C: on second element and beyond
74
+ D: on second element and beyond
75
+ E: on second element and beyond
76
+ */
77
+
78
+ partialCarryoverIndexInc = buffers.length > 1;
79
+
80
+ if (partial > 0) {
81
+ if (partial === buffers.at(-1).length) {
82
+ partialCarryover = buffers.pop();
83
+ } else {
84
+ const last = buffers.at(-1);
85
+ partialCarryover = last.slice(-partial);
86
+ buffers[buffers.length - 1] = last.slice(0, -partial);
87
+ }
88
+ } else {
89
+ partialCarryover = Buffer.from('');
90
+ }
91
+
92
+ for (let i = 0; i < buffers.length; i++) {
93
+ if (i > incStart) {
94
+ index++;
95
+ }
96
+
97
+ if (index >= limit) {
98
+ return;
99
+ }
100
+
101
+ const buffer = buffers[i];
102
+
103
+ yield [index, buffer];
104
+ }
105
+ }
106
+ } finally {
107
+ if (partialCarryoverIndexInc) {
108
+ index++;
109
+ }
110
+
111
+ // Push the carryover if a buffer ends with a partial match
112
+ if (partialCarryover.length && index < limit) {
113
+ yield [index, partialCarryover];
114
+ }
115
+ }
116
+ };
117
+ };