@centralping/ergo 0.1.0-beta.1 → 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -15
- package/http/accepts.js +1 -2
- package/http/authorization.js +1 -2
- package/http/body.js +1 -2
- package/http/cache-control.js +1 -2
- package/http/compress.js +1 -2
- package/http/cookie.js +1 -2
- package/http/cors.js +1 -2
- package/http/csrf.js +1 -2
- package/http/handler.js +1 -2
- package/http/idempotency.js +110 -0
- package/http/index.js +37 -4
- package/http/json-api-query.js +1 -2
- package/http/logger.js +1 -2
- package/http/main.js +5 -3
- package/http/precondition.js +1 -2
- package/http/prefer.js +1 -2
- package/http/rate-limit.js +1 -2
- package/http/security-headers.js +1 -2
- package/http/send.js +1 -2
- package/http/timeout.js +1 -2
- package/http/url.js +1 -2
- package/http/validate.js +1 -2
- package/lib/accepts.js +1 -2
- package/lib/attach-instance.js +0 -1
- package/lib/authorization.js +1 -2
- package/lib/body/multiparse.js +1 -2
- package/lib/body/multipart/headers.js +1 -2
- package/lib/body/writer.js +1 -2
- package/lib/cookie/cookie.js +1 -2
- package/lib/cookie/index.js +0 -1
- package/lib/cookie/jar.js +16 -12
- package/lib/cookie/parse.js +1 -2
- package/lib/cors.js +1 -2
- package/lib/csrf.js +1 -2
- package/lib/from-connect.js +2 -3
- package/lib/idempotency.js +139 -0
- package/lib/json-api-query/index.js +0 -1
- package/lib/json-api-query/validate.js +1 -2
- package/lib/link.js +57 -5
- package/lib/prefer.js +1 -2
- package/lib/query.js +1 -2
- package/lib/rate-limit.js +1 -2
- package/lib/sanitize-quoted-string.js +0 -1
- package/lib/security-headers.js +1 -2
- package/lib/validate.js +1 -2
- package/lib/vary.js +0 -1
- package/package.json +2 -2
- package/types/http/idempotency.d.ts +20 -0
- package/types/http/main.d.ts +3 -1
- package/types/http/precondition.d.ts +1 -2
- package/types/lib/attach-instance.d.ts +0 -1
- package/types/lib/cookie/jar.d.ts +4 -0
- package/types/lib/from-connect.d.ts +2 -3
- package/types/lib/idempotency.d.ts +64 -0
- package/types/lib/link.d.ts +28 -0
- package/types/lib/prefer.d.ts +1 -2
- package/types/lib/rate-limit.d.ts +1 -2
- package/types/lib/sanitize-quoted-string.d.ts +0 -1
- package/types/lib/vary.d.ts +0 -1
- package/types/utils/compose.d.ts +1 -2
- package/types/utils/iterables/range.d.ts +1 -2
- package/utils/attempt.js +1 -2
- package/utils/buffers/index.js +0 -1
- package/utils/buffers/match.js +1 -2
- package/utils/buffers/split.js +1 -2
- package/utils/compose-with.js +1 -2
- package/utils/compose.js +1 -2
- package/utils/flat-array.js +1 -2
- package/utils/get.js +1 -2
- package/utils/http-errors.js +1 -2
- package/utils/iterables/buffer-split.js +1 -2
- package/utils/iterables/chain.js +1 -2
- package/utils/iterables/exec-all.js +1 -2
- package/utils/iterables/filter.js +1 -2
- package/utils/iterables/for-each.js +1 -2
- package/utils/iterables/from-stream.js +1 -2
- package/utils/iterables/index.js +0 -1
- package/utils/iterables/map.js +1 -2
- package/utils/iterables/range.js +1 -2
- package/utils/iterables/reduce.js +1 -2
- package/utils/iterables/take.js +1 -2
- package/utils/observables/buffer-split.js +0 -1
- package/utils/observables/chain.js +0 -1
- package/utils/observables/index.js +0 -1
- package/utils/observables/map.js +0 -1
- package/utils/observables/take.js +0 -1
- package/utils/pick.js +1 -2
- package/utils/set.js +1 -2
- package/utils/streams/index.js +0 -1
- package/utils/streams/meter.js +1 -2
- package/utils/streams/tee.js +1 -2
- package/utils/type.js +1 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-wordmark-dark.svg">
|
|
4
|
-
<source media="(prefers-color-scheme: light)" srcset="assets/logo-wordmark-light.svg">
|
|
5
|
-
<img alt="ergo" src="assets/logo-wordmark-light.svg" width="240">
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/CentralPing/ergo/main/assets/logo-wordmark-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/CentralPing/ergo/main/assets/logo-wordmark-light.svg">
|
|
5
|
+
<img alt="ergo" src="https://raw.githubusercontent.com/CentralPing/ergo/main/assets/logo-wordmark-light.svg" width="240">
|
|
6
6
|
</picture>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://www.npmjs.com/package/@centralping/ergo)
|
|
12
12
|
[](https://scorecard.dev/viewer/?uri=github.com/CentralPing/ergo)
|
|
13
13
|
[](https://nodejs.org)
|
|
14
|
-
[](LICENSE)
|
|
14
|
+
[](https://github.com/CentralPing/ergo/blob/main/LICENSE)
|
|
15
15
|
|
|
16
16
|
A **Fast Fail** REST API toolkit for Node.js. _ergo_ (error or go) provides composable, stream-native middleware organized around the principle that a server should fail as early as possible -- before doing any expensive work -- through four ordered stages: **Negotiation, Authorization, Validation, and Execution**. Every behavior is backed by an IETF RFC or industry standard, not invented conventions.
|
|
17
17
|
|
|
@@ -57,20 +57,19 @@ Requires **Node.js >= 22**.
|
|
|
57
57
|
|
|
58
58
|
```js
|
|
59
59
|
import {createServer} from 'node:http';
|
|
60
|
-
import {compose, handler, logger, cors, authorization, body
|
|
60
|
+
import {compose, handler, logger, cors, authorization, body} from '@centralping/ergo';
|
|
61
61
|
|
|
62
62
|
const pipeline = compose(
|
|
63
|
-
[logger(),
|
|
64
|
-
[cors(),
|
|
63
|
+
[logger(), 'log'],
|
|
64
|
+
[cors(), 'cors'],
|
|
65
65
|
[authorization({strategies: [{type: 'Bearer', authorizer: (_, token) =>
|
|
66
66
|
token === 'my-token' ? {authorized: true, info: {uid: 1}} : {}
|
|
67
|
-
}]}),
|
|
68
|
-
[body(),
|
|
69
|
-
(req, res,
|
|
70
|
-
send()
|
|
67
|
+
}]}), 'auth'],
|
|
68
|
+
[body(), 'body'],
|
|
69
|
+
(req, res, acc) => ({response: {body: {user: acc.auth, data: acc.body.parsed}}})
|
|
71
70
|
);
|
|
72
71
|
|
|
73
|
-
createServer(handler(pipeline
|
|
72
|
+
createServer(handler(pipeline)).listen(3000);
|
|
74
73
|
```
|
|
75
74
|
|
|
76
75
|
## Middleware Overview
|
|
@@ -96,7 +95,7 @@ createServer(handler(pipeline, send())).listen(3000);
|
|
|
96
95
|
| `cacheControl()` | Cache-Control header management | [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111) |
|
|
97
96
|
| `jsonApiQuery()` | JSON:API query parameter validation | [JSON:API](https://jsonapi.org/) |
|
|
98
97
|
|
|
99
|
-
See the [full API reference](https://centralping.github.io/
|
|
98
|
+
See the [full API reference](https://centralping.github.io/packages/ergo/) for detailed options and examples.
|
|
100
99
|
|
|
101
100
|
## Standards Compliance
|
|
102
101
|
|
|
@@ -120,8 +119,8 @@ See the [full API reference](https://centralping.github.io/api/ergo/) for detail
|
|
|
120
119
|
## Documentation
|
|
121
120
|
|
|
122
121
|
- [Getting Started](https://centralping.github.io/getting-started/)
|
|
123
|
-
- [API Reference](https://centralping.github.io/
|
|
124
|
-
- [
|
|
122
|
+
- [API Reference](https://centralping.github.io/packages/ergo/)
|
|
123
|
+
- [Fast Fail Pipeline](https://centralping.github.io/concepts/fast-fail/)
|
|
125
124
|
- [Benchmarks](https://centralping.github.io/benchmarks/)
|
|
126
125
|
|
|
127
126
|
## Development
|
package/http/accepts.js
CHANGED
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
* Fast Fail pipeline.
|
|
11
11
|
*
|
|
12
12
|
* @module http/accepts
|
|
13
|
-
* @version 0.1.0
|
|
14
13
|
* @since 0.1.0
|
|
15
14
|
* @requires ../lib/accepts.js
|
|
16
15
|
* @requires ../utils/http-errors.js
|
|
17
16
|
*
|
|
18
17
|
* @example
|
|
19
|
-
* import {compose, accepts} from 'ergo';
|
|
18
|
+
* import {compose, accepts} from '@centralping/ergo';
|
|
20
19
|
*
|
|
21
20
|
* const pipeline = compose(
|
|
22
21
|
* [accepts({types: ['application/json']}), 'accepts'],
|
package/http/authorization.js
CHANGED
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
* response header (RFC 7235).
|
|
11
11
|
*
|
|
12
12
|
* @module http/authorization
|
|
13
|
-
* @version 0.1.0
|
|
14
13
|
* @since 0.1.0
|
|
15
14
|
* @requires ../lib/authorization.js
|
|
16
15
|
* @requires ../utils/http-errors.js
|
|
17
16
|
*
|
|
18
17
|
* @example
|
|
19
|
-
* import {compose, authorization} from 'ergo';
|
|
18
|
+
* import {compose, authorization} from '@centralping/ergo';
|
|
20
19
|
*
|
|
21
20
|
* const pipeline = compose(
|
|
22
21
|
* [authorization({
|
package/http/body.js
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
* 3-stream pipeline for reduced per-request overhead.
|
|
18
18
|
*
|
|
19
19
|
* @module http/body
|
|
20
|
-
* @version 0.1.0
|
|
21
20
|
* @since 0.1.0
|
|
22
21
|
* @requires node:stream
|
|
23
22
|
* @requires node:zlib
|
|
@@ -29,7 +28,7 @@
|
|
|
29
28
|
* @requires ../utils/http-errors.js
|
|
30
29
|
*
|
|
31
30
|
* @example
|
|
32
|
-
* import {compose, body} from 'ergo';
|
|
31
|
+
* import {compose, body} from '@centralping/ergo';
|
|
33
32
|
*
|
|
34
33
|
* const pipeline = compose(
|
|
35
34
|
* [body({limit: 2 * 1024 * 1024}), 'body'],
|
package/http/cache-control.js
CHANGED
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
* a raw directive string or structured options that are assembled into a directive.
|
|
7
7
|
*
|
|
8
8
|
* @module http/cache-control
|
|
9
|
-
* @version 0.1.0
|
|
10
9
|
* @since 0.1.0
|
|
11
10
|
*
|
|
12
11
|
* @example
|
|
13
|
-
* import {compose, cacheControl} from 'ergo';
|
|
12
|
+
* import {compose, cacheControl} from '@centralping/ergo';
|
|
14
13
|
*
|
|
15
14
|
* // String shorthand
|
|
16
15
|
* const pipeline = compose(
|
package/http/compress.js
CHANGED
|
@@ -13,13 +13,12 @@
|
|
|
13
13
|
* - Bodies below the configurable `threshold` byte count (default 1 KiB)
|
|
14
14
|
*
|
|
15
15
|
* @module http/compress
|
|
16
|
-
* @version 0.1.0
|
|
17
16
|
* @since 0.1.0
|
|
18
17
|
* @requires node:zlib
|
|
19
18
|
* @requires negotiator
|
|
20
19
|
*
|
|
21
20
|
* @example
|
|
22
|
-
* import {compose, compress} from 'ergo';
|
|
21
|
+
* import {compose, compress} from '@centralping/ergo';
|
|
23
22
|
*
|
|
24
23
|
* const pipeline = compose(
|
|
25
24
|
* compress({threshold: 1024, encodings: ['br', 'gzip', 'deflate']}),
|
package/http/cookie.js
CHANGED
|
@@ -12,12 +12,11 @@
|
|
|
12
12
|
* and other cookie-based workflows.
|
|
13
13
|
*
|
|
14
14
|
* @module http/cookie
|
|
15
|
-
* @version 0.1.0
|
|
16
15
|
* @since 0.1.0
|
|
17
16
|
* @requires ../lib/cookie/index.js
|
|
18
17
|
*
|
|
19
18
|
* @example
|
|
20
|
-
* import {compose, cookie} from 'ergo';
|
|
19
|
+
* import {compose, cookie} from '@centralping/ergo';
|
|
21
20
|
*
|
|
22
21
|
* const pipeline = compose(
|
|
23
22
|
* [cookie(), 'cookies'],
|
package/http/cors.js
CHANGED
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
* Pre-flight `OPTIONS` requests should be handled at the router level using `ergo-router`.
|
|
11
11
|
*
|
|
12
12
|
* @module http/cors
|
|
13
|
-
* @version 0.1.0
|
|
14
13
|
* @since 0.1.0
|
|
15
14
|
* @requires ../lib/cors.js
|
|
16
15
|
* @requires ../utils/http-errors.js
|
|
17
16
|
*
|
|
18
17
|
* @example
|
|
19
|
-
* import {compose, cors} from 'ergo';
|
|
18
|
+
* import {compose, cors} from '@centralping/ergo';
|
|
20
19
|
*
|
|
21
20
|
* const pipeline = compose(
|
|
22
21
|
* [cors({
|
package/http/csrf.js
CHANGED
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
* The CSRF UUID is stored in a separate cookie so the token can be regenerated independently.
|
|
12
12
|
*
|
|
13
13
|
* @module http/csrf
|
|
14
|
-
* @version 0.1.0
|
|
15
14
|
* @since 0.1.0
|
|
16
15
|
* @requires ../lib/csrf.js
|
|
17
16
|
* @requires ../utils/http-errors.js
|
|
18
17
|
*
|
|
19
18
|
* @example
|
|
20
|
-
* import {compose, cookie, csrf} from 'ergo';
|
|
19
|
+
* import {compose, cookie, csrf} from '@centralping/ergo';
|
|
21
20
|
*
|
|
22
21
|
* const csrfMiddleware = csrf({secret: process.env.CSRF_SECRET});
|
|
23
22
|
*
|
package/http/handler.js
CHANGED
|
@@ -13,11 +13,10 @@
|
|
|
13
13
|
* compose middleware directly without the router.
|
|
14
14
|
*
|
|
15
15
|
* @module http/handler
|
|
16
|
-
* @version 0.2.0
|
|
17
16
|
* @since 0.1.0
|
|
18
17
|
*
|
|
19
18
|
* @example
|
|
20
|
-
* import {handler, compose,
|
|
19
|
+
* import {handler, compose, logger, authorization, body} from '@centralping/ergo';
|
|
21
20
|
*
|
|
22
21
|
* const pipeline = compose(
|
|
23
22
|
* [logger(), 'log'],
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Idempotency-Key pipeline middleware.
|
|
3
|
+
*
|
|
4
|
+
* Detects duplicate requests by `Idempotency-Key` header value per
|
|
5
|
+
* draft-ietf-httpapi-idempotency-key-header-07. When a matching key with
|
|
6
|
+
* the same request fingerprint is found, the stored response is replayed.
|
|
7
|
+
* When a key matches but the fingerprint differs, a 409 Conflict is returned.
|
|
8
|
+
*
|
|
9
|
+
* Placed in Stage 3 (Validation) after body parsing so the request body
|
|
10
|
+
* fingerprint can be computed from the parsed body.
|
|
11
|
+
*
|
|
12
|
+
* @module http/idempotency
|
|
13
|
+
* @since 0.1.0-beta.2
|
|
14
|
+
* @requires ../lib/idempotency.js
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* import {compose, body, idempotency} from '@centralping/ergo';
|
|
18
|
+
*
|
|
19
|
+
* const pipeline = compose(
|
|
20
|
+
* [body(), 'body'],
|
|
21
|
+
* [idempotency({required: true}), 'idempotency'],
|
|
22
|
+
* (req, res, acc) => ({response: {statusCode: 201, body: {created: true}}})
|
|
23
|
+
* );
|
|
24
|
+
*
|
|
25
|
+
* @see {@link https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/ Idempotency-Key Header (IETF Draft)}
|
|
26
|
+
*/
|
|
27
|
+
import {IdempotencyStore, parseIdempotencyKey, generateFingerprint} from '../lib/idempotency.js';
|
|
28
|
+
|
|
29
|
+
const DEFAULT_METHODS = new Set(['POST', 'PATCH']);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create an idempotency middleware.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} [options]
|
|
35
|
+
* @param {object} [options.store] - Pluggable store (must implement get/set/complete/delete).
|
|
36
|
+
* Defaults to an in-memory store with 24h TTL.
|
|
37
|
+
* @param {number} [options.ttlMs=86400000] - TTL for stored entries in milliseconds (default: 24h).
|
|
38
|
+
* Only used when creating the default in-memory store.
|
|
39
|
+
* @param {boolean} [options.required=false] - When true, returns 400 if the header is missing
|
|
40
|
+
* on applicable methods
|
|
41
|
+
* @param {Set<string>|string[]} [options.methods] - HTTP methods to apply idempotency to
|
|
42
|
+
* (default: POST, PATCH)
|
|
43
|
+
* @returns {function} - Middleware `(req, res, domainAcc) => {value?, response?}`
|
|
44
|
+
*/
|
|
45
|
+
export default function idempotency({store, ttlMs, required = false, methods} = {}) {
|
|
46
|
+
const _store = store ?? new IdempotencyStore(ttlMs ? {ttlMs} : undefined);
|
|
47
|
+
const _methods = methods instanceof Set ? methods : new Set(methods ?? DEFAULT_METHODS);
|
|
48
|
+
|
|
49
|
+
return (req, _res, domainAcc) => {
|
|
50
|
+
if (!_methods.has(req.method)) return {};
|
|
51
|
+
|
|
52
|
+
const key = parseIdempotencyKey(req.headers?.['idempotency-key']);
|
|
53
|
+
|
|
54
|
+
if (!key) {
|
|
55
|
+
if (required) {
|
|
56
|
+
return {
|
|
57
|
+
response: {
|
|
58
|
+
statusCode: 400,
|
|
59
|
+
detail: 'Idempotency-Key header is required'
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rawBody = domainAcc?.body?.raw ?? domainAcc?.body?.parsed ?? '';
|
|
67
|
+
const fingerprint = generateFingerprint(
|
|
68
|
+
typeof rawBody === 'string' ? rawBody : JSON.stringify(rawBody)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const existing = _store.get(key);
|
|
72
|
+
|
|
73
|
+
if (existing) {
|
|
74
|
+
if (existing.fingerprint !== fingerprint) {
|
|
75
|
+
return {
|
|
76
|
+
response: {
|
|
77
|
+
statusCode: 409,
|
|
78
|
+
detail: 'Idempotency key already used with a different request'
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (existing.status === 'complete' && existing.response) {
|
|
84
|
+
return {
|
|
85
|
+
value: {replayed: true},
|
|
86
|
+
response: existing.response
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Still processing — concurrent duplicate
|
|
91
|
+
return {
|
|
92
|
+
response: {
|
|
93
|
+
statusCode: 409,
|
|
94
|
+
detail: 'A request with this idempotency key is already being processed'
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_store.set(key, fingerprint);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
value: {
|
|
103
|
+
key,
|
|
104
|
+
fingerprint,
|
|
105
|
+
complete: response => _store.complete(key, response),
|
|
106
|
+
discard: () => _store.delete(key)
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
}
|
package/http/index.js
CHANGED
|
@@ -1,13 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Fast Fail REST API toolkit for Node.js.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Composable middleware organized into four ordered pipeline stages — a failure
|
|
5
|
+
* at any stage immediately returns a standards-compliant error response so no
|
|
6
|
+
* downstream work is wasted:
|
|
7
|
+
*
|
|
8
|
+
* | Stage | Middleware | Purpose |
|
|
9
|
+
* | --- | --- | --- |
|
|
10
|
+
* | **Negotiation** | {@link logger}, {@link cors}, {@link accepts}, {@link cookie}, {@link url} | Parse/inspect request headers and URL |
|
|
11
|
+
* | **Authorization** | {@link authorization}, {@link csrf} | Authenticate and verify request integrity |
|
|
12
|
+
* | **Validation** | {@link body}, {@link jsonApiQuery}, {@link validate} | Parse and validate the request body |
|
|
13
|
+
* | **Execution** | {@link timeout}, {@link compress}, {@link handler}, {@link send} | Run the handler and serialize the response |
|
|
14
|
+
*
|
|
15
|
+
* Cross-cutting middleware available at any stage:
|
|
16
|
+
* {@link cacheControl}, {@link prefer}, {@link precondition},
|
|
17
|
+
* {@link rateLimit}, {@link securityHeaders}
|
|
18
|
+
*
|
|
19
|
+
* **Composition utilities:** {@link compose}, {@link createResponseAcc},
|
|
20
|
+
* {@link mergeResponse}, {@link httpErrors}, {@link fromConnect}
|
|
6
21
|
*
|
|
7
22
|
* @module @centralping/ergo
|
|
8
|
-
* @version 0.1.0-beta.1
|
|
9
23
|
* @since 0.1.0
|
|
10
24
|
* @requires ./main.js
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* import {compose, handler, logger, cors, accepts, authorization,
|
|
28
|
+
* body, send} from '@centralping/ergo';
|
|
29
|
+
*
|
|
30
|
+
* const pipeline = compose(
|
|
31
|
+
* [logger(), 'log'],
|
|
32
|
+
* [cors(), 'cors'],
|
|
33
|
+
* [accepts({types: ['application/json']}), 'accepts'],
|
|
34
|
+
* [authorization({strategies: [...]}), 'auth'],
|
|
35
|
+
* [body(), 'body'],
|
|
36
|
+
* (req, res, acc) => ({response: {body: acc.body.parsed}}),
|
|
37
|
+
* );
|
|
38
|
+
*
|
|
39
|
+
* const server = http.createServer(handler(pipeline));
|
|
40
|
+
*
|
|
41
|
+
* @see {@link https://centralping.github.io/concepts/fast-fail/ Fast Fail Pipeline}
|
|
42
|
+
* @see {@link https://centralping.github.io/concepts/security/ Security}
|
|
43
|
+
* @see {@link https://centralping.github.io/concepts/standards/ Standards Compliance}
|
|
11
44
|
*/
|
|
12
45
|
|
|
13
46
|
export * from './main.js';
|
package/http/json-api-query.js
CHANGED
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
* Must be placed after `url()` in the pipeline so that `acc.url` is populated.
|
|
12
12
|
*
|
|
13
13
|
* @module http/json-api-query
|
|
14
|
-
* @version 0.1.0
|
|
15
14
|
* @since 0.1.0
|
|
16
15
|
* @requires ../lib/json-api-query/index.js
|
|
17
16
|
* @requires ../utils/http-errors.js
|
|
18
17
|
*
|
|
19
18
|
* @example
|
|
20
|
-
* import {compose, url, jsonApiQuery} from 'ergo';
|
|
19
|
+
* import {compose, url, jsonApiQuery} from '@centralping/ergo';
|
|
21
20
|
*
|
|
22
21
|
* const pipeline = compose(
|
|
23
22
|
* [url(), 'url'],
|
package/http/logger.js
CHANGED
|
@@ -23,13 +23,12 @@
|
|
|
23
23
|
* accidental secret leakage.
|
|
24
24
|
*
|
|
25
25
|
* @module http/logger
|
|
26
|
-
* @version 0.1.0
|
|
27
26
|
* @since 0.1.0
|
|
28
27
|
* @requires node:os
|
|
29
28
|
* @requires node:crypto
|
|
30
29
|
*
|
|
31
30
|
* @example
|
|
32
|
-
* import {compose, logger} from 'ergo';
|
|
31
|
+
* import {compose, logger} from '@centralping/ergo';
|
|
33
32
|
*
|
|
34
33
|
* const pipeline = compose(
|
|
35
34
|
* [logger(), 'log'],
|
package/http/main.js
CHANGED
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
* accumulated under named keys in `domainAcc`; response properties merge
|
|
15
15
|
* into `responseAcc`. `send()` is called post-pipeline by `handler()`.
|
|
16
16
|
*
|
|
17
|
-
* @module ergo
|
|
18
|
-
* @version 0.1.0
|
|
17
|
+
* @module @centralping/ergo
|
|
19
18
|
* @since 0.1.0
|
|
20
19
|
* @requires ./handler.js
|
|
21
20
|
* @requires ./accepts.js
|
|
@@ -42,7 +41,7 @@
|
|
|
42
41
|
*
|
|
43
42
|
* @example
|
|
44
43
|
* import {compose, handler, logger, cors, authorization, accepts,
|
|
45
|
-
* cookie, url, body} from 'ergo';
|
|
44
|
+
* cookie, url, body} from '@centralping/ergo';
|
|
46
45
|
*
|
|
47
46
|
* const pipeline = compose(
|
|
48
47
|
* // Stage 1: Negotiation
|
|
@@ -81,6 +80,7 @@ import rateLimit from './rate-limit.js';
|
|
|
81
80
|
import securityHeaders from './security-headers.js';
|
|
82
81
|
import send from './send.js';
|
|
83
82
|
import timeout from './timeout.js';
|
|
83
|
+
import idempotency from './idempotency.js';
|
|
84
84
|
import validate from './validate.js';
|
|
85
85
|
import httpErrors from '../utils/http-errors.js';
|
|
86
86
|
import fromConnect from '../lib/from-connect.js';
|
|
@@ -100,6 +100,7 @@ export {
|
|
|
100
100
|
csrf,
|
|
101
101
|
fromConnect,
|
|
102
102
|
httpErrors,
|
|
103
|
+
idempotency,
|
|
103
104
|
jsonApiQuery,
|
|
104
105
|
logger,
|
|
105
106
|
prefer,
|
|
@@ -127,6 +128,7 @@ export default {
|
|
|
127
128
|
csrf,
|
|
128
129
|
fromConnect,
|
|
129
130
|
httpErrors,
|
|
131
|
+
idempotency,
|
|
130
132
|
jsonApiQuery,
|
|
131
133
|
logger,
|
|
132
134
|
prefer,
|
package/http/precondition.js
CHANGED
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
* inspection that short-circuits before authorization, body parsing, or execution.
|
|
11
11
|
*
|
|
12
12
|
* @module http/precondition
|
|
13
|
-
* @version 0.1.0
|
|
14
13
|
* @since 0.1.0
|
|
15
14
|
*
|
|
16
15
|
* @example
|
|
17
|
-
* import {compose, precondition} from 'ergo';
|
|
16
|
+
* import {compose, precondition} from '@centralping/ergo';
|
|
18
17
|
*
|
|
19
18
|
* // Enforce on all requests (method scoping handled by pipeline builder)
|
|
20
19
|
* const pipeline = compose(
|
package/http/prefer.js
CHANGED
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
* Placed in Stage 1 (Negotiation) — cheap header parse with no I/O.
|
|
9
9
|
*
|
|
10
10
|
* @module http/prefer
|
|
11
|
-
* @version 0.1.0
|
|
12
11
|
* @since 0.1.0
|
|
13
12
|
* @requires ../lib/prefer.js
|
|
14
13
|
*
|
|
15
14
|
* @example
|
|
16
|
-
* import {compose, prefer} from 'ergo';
|
|
15
|
+
* import {compose, prefer} from '@centralping/ergo';
|
|
17
16
|
*
|
|
18
17
|
* const pipeline = compose(
|
|
19
18
|
* [prefer(), 'prefer'],
|
package/http/rate-limit.js
CHANGED
|
@@ -10,12 +10,11 @@
|
|
|
10
10
|
* or execution.
|
|
11
11
|
*
|
|
12
12
|
* @module http/rate-limit
|
|
13
|
-
* @version 0.1.0
|
|
14
13
|
* @since 0.1.0
|
|
15
14
|
* @requires ../lib/rate-limit.js
|
|
16
15
|
*
|
|
17
16
|
* @example
|
|
18
|
-
* import {compose, rateLimit} from 'ergo';
|
|
17
|
+
* import {compose, rateLimit} from '@centralping/ergo';
|
|
19
18
|
*
|
|
20
19
|
* const pipeline = compose(
|
|
21
20
|
* [rateLimit({max: 100, windowMs: 60000}), 'rateLimit'],
|
package/http/security-headers.js
CHANGED
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
* Delegates tuple construction to `lib/security-headers.js` (the shared primitive).
|
|
9
9
|
*
|
|
10
10
|
* @module http/security-headers
|
|
11
|
-
* @version 0.1.0
|
|
12
11
|
* @since 0.1.0
|
|
13
12
|
* @requires ../lib/security-headers.js
|
|
14
13
|
*
|
|
15
14
|
* @example
|
|
16
|
-
* import {compose, securityHeaders} from 'ergo';
|
|
15
|
+
* import {compose, securityHeaders} from '@centralping/ergo';
|
|
17
16
|
*
|
|
18
17
|
* // Use defaults
|
|
19
18
|
* const pipeline = compose(
|
package/http/send.js
CHANGED
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
* - Optional response envelope for 2xx Object bodies
|
|
30
30
|
*
|
|
31
31
|
* @module http/send
|
|
32
|
-
* @version 0.2.0
|
|
33
32
|
* @since 0.1.0
|
|
34
33
|
* @requires node:stream
|
|
35
34
|
* @requires node:http
|
|
@@ -38,7 +37,7 @@
|
|
|
38
37
|
* @requires ../lib/vary.js
|
|
39
38
|
*
|
|
40
39
|
* @example
|
|
41
|
-
* import {send, createResponseAcc} from 'ergo';
|
|
40
|
+
* import {send, createResponseAcc} from '@centralping/ergo';
|
|
42
41
|
*
|
|
43
42
|
* const writer = send({etag: true});
|
|
44
43
|
* // After pipeline completes:
|
package/http/timeout.js
CHANGED
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
* can be GC'd.
|
|
13
13
|
*
|
|
14
14
|
* @module http/timeout
|
|
15
|
-
* @version 0.2.0
|
|
16
15
|
* @since 0.1.0
|
|
17
16
|
*
|
|
18
17
|
* @example
|
|
19
|
-
* import {compose, timeout} from 'ergo';
|
|
18
|
+
* import {compose, timeout} from '@centralping/ergo';
|
|
20
19
|
*
|
|
21
20
|
* const pipeline = compose(
|
|
22
21
|
* [timeout({ms: 10000, statusCode: 504}), 'timeout'],
|
package/http/url.js
CHANGED
|
@@ -11,12 +11,11 @@
|
|
|
11
11
|
* - `search` is the raw query string including the `?` prefix, or `undefined`
|
|
12
12
|
*
|
|
13
13
|
* @module http/url
|
|
14
|
-
* @version 0.1.0
|
|
15
14
|
* @since 0.1.0
|
|
16
15
|
* @requires ../lib/query.js
|
|
17
16
|
*
|
|
18
17
|
* @example
|
|
19
|
-
* import {compose, url} from 'ergo';
|
|
18
|
+
* import {compose, url} from '@centralping/ergo';
|
|
20
19
|
*
|
|
21
20
|
* const pipeline = compose(
|
|
22
21
|
* [url(), 'url'],
|
package/http/validate.js
CHANGED
|
@@ -9,12 +9,11 @@
|
|
|
9
9
|
* are populated before validation runs.
|
|
10
10
|
*
|
|
11
11
|
* @module http/validate
|
|
12
|
-
* @version 0.1.0
|
|
13
12
|
* @since 0.1.0
|
|
14
13
|
* @requires ../lib/validate.js
|
|
15
14
|
*
|
|
16
15
|
* @example
|
|
17
|
-
* import {compose, body, url, validate} from 'ergo';
|
|
16
|
+
* import {compose, body, url, validate} from '@centralping/ergo';
|
|
18
17
|
*
|
|
19
18
|
* const pipeline = compose(
|
|
20
19
|
* [body(), 'body'],
|
package/lib/accepts.js
CHANGED
|
@@ -6,13 +6,12 @@
|
|
|
6
6
|
* as the pure-logic backing implementation.
|
|
7
7
|
*
|
|
8
8
|
* @module lib/accepts
|
|
9
|
-
* @version 0.1.0
|
|
10
9
|
* @since 0.1.0
|
|
11
10
|
* @requires negotiator
|
|
12
11
|
* @requires ../utils/flat-array.js
|
|
13
12
|
*
|
|
14
13
|
* @example
|
|
15
|
-
* import accepts from 'ergo/lib/accepts';
|
|
14
|
+
* import accepts from '@centralping/ergo/lib/accepts';
|
|
16
15
|
*
|
|
17
16
|
* const negotiate = accepts({types: ['application/json', 'text/html']});
|
|
18
17
|
* const result = negotiate({'accept': 'text/html,application/json;q=0.9'});
|
package/lib/attach-instance.js
CHANGED
package/lib/authorization.js
CHANGED
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
* 401 for known schemes (with a `WWW-Authenticate` challenge) or 403 for unrecognized schemes.
|
|
12
12
|
*
|
|
13
13
|
* @module lib/authorization
|
|
14
|
-
* @version 0.1.0
|
|
15
14
|
* @since 0.1.0
|
|
16
15
|
*
|
|
17
16
|
* @example
|
|
18
|
-
* import authorize from 'ergo/lib/authorization';
|
|
17
|
+
* import authorize from '@centralping/ergo/lib/authorization';
|
|
19
18
|
*
|
|
20
19
|
* const authorizer = authorize([{
|
|
21
20
|
* type: 'Bearer',
|
package/lib/body/multiparse.js
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* `Content-Disposition`, `Content-Type`, `Content-Transfer-Encoding`.
|
|
13
13
|
*
|
|
14
14
|
* @module lib/body/multiparse
|
|
15
|
-
* @version 0.1.0
|
|
16
15
|
* @since 0.1.0
|
|
17
16
|
* @requires ./multipart/headers.js
|
|
18
17
|
* @requires ../../utils/iterables/buffer-split.js
|
|
@@ -21,7 +20,7 @@
|
|
|
21
20
|
* @see {@link https://www.rfc-editor.org/rfc/rfc7578}
|
|
22
21
|
*
|
|
23
22
|
* @example
|
|
24
|
-
* import multiparse from 'ergo/lib/body/multiparse';
|
|
23
|
+
* import multiparse from '@centralping/ergo/lib/body/multiparse';
|
|
25
24
|
*
|
|
26
25
|
* const parts = multiparse(rawBodyBuffer, 'boundary-string');
|
|
27
26
|
* // parts => [{headers: {...}, name: 'file', filename: 'upload.txt', body: Buffer}]
|
|
@@ -11,14 +11,13 @@
|
|
|
11
11
|
* - `content-transfer-encoding` (deprecated but present in legacy clients)
|
|
12
12
|
*
|
|
13
13
|
* @module lib/body/multipart/headers
|
|
14
|
-
* @version 0.1.0
|
|
15
14
|
* @since 0.1.0
|
|
16
15
|
* @requires ../../../utils/iterables/exec-all.js
|
|
17
16
|
*
|
|
18
17
|
* @see {@link https://www.rfc-editor.org/rfc/rfc7578 RFC 7578 - Returning Values from Forms: multipart/form-data}
|
|
19
18
|
*
|
|
20
19
|
* @example
|
|
21
|
-
* import parseHeaders from 'ergo/lib/body/multipart/headers';
|
|
20
|
+
* import parseHeaders from '@centralping/ergo/lib/body/multipart/headers';
|
|
22
21
|
*
|
|
23
22
|
* parseHeaders(['Content-Disposition: form-data; name="field1"']);
|
|
24
23
|
* // => {'content-disposition': {type: 'form-data', parameters: {name: 'field1'}}}
|
package/lib/body/writer.js
CHANGED
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
* `pipeline(req, meter, decompressor, writer())`
|
|
9
9
|
*
|
|
10
10
|
* @module lib/body/writer
|
|
11
|
-
* @version 0.1.0
|
|
12
11
|
* @since 0.1.0
|
|
13
12
|
* @requires node:stream
|
|
14
13
|
*
|
|
15
14
|
* @example
|
|
16
15
|
* import {pipeline} from 'node:stream';
|
|
17
|
-
* import writer from 'ergo/lib/body/writer';
|
|
16
|
+
* import writer from '@centralping/ergo/lib/body/writer';
|
|
18
17
|
*
|
|
19
18
|
* const w = writer();
|
|
20
19
|
* pipeline(readable, w, err => {
|