@zudojs/middleware 1.0.2 → 1.1.0
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 +34 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/middlewareResponse/guardResponse.factory.d.ts +33 -0
- package/dist/middlewareResponse/guardResponse.factory.js +72 -0
- package/dist/middlewareResponse/guardResponse.type.d.ts +45 -0
- package/dist/middlewareResponse/guardResponse.type.js +16 -0
- package/dist/middlewareResponse/index.d.ts +10 -0
- package/dist/middlewareResponse/index.js +10 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -183,6 +183,40 @@ const timed = withTiming("db-lookup", lookup, {
|
|
|
183
183
|
});
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
## Guard responses
|
|
187
|
+
|
|
188
|
+
A middleware that refuses a request — authentication, authorization, tenant
|
|
189
|
+
resolution — answers with `createGuardResponse()` instead of calling `next()`:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { createGuardResponse, isGuardResponse } from "@zudojs/middleware";
|
|
193
|
+
|
|
194
|
+
const requireApiKey = async (context, next) => {
|
|
195
|
+
if (!context.request.getHeader("x-api-key")) {
|
|
196
|
+
return createGuardResponse({
|
|
197
|
+
status: 401,
|
|
198
|
+
body: { error: "Unauthorized" },
|
|
199
|
+
headers: { "www-authenticate": "ApiKey" },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return next();
|
|
203
|
+
};
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`@zudojs/http` sends a guard response with its own status, headers and body
|
|
207
|
+
(a structured body as JSON, with `content-type: application/json` unless you
|
|
208
|
+
set one). It is the contract lower-tier packages use: `@zudojs/permissions`'
|
|
209
|
+
`authorize()` and `@zudojs/tenancy`'s middleware return one for every 401,
|
|
210
|
+
403 and 404, without depending on `@zudojs/http`.
|
|
211
|
+
|
|
212
|
+
Only the brand counts. The object carries the registered symbol
|
|
213
|
+
`GUARD_RESPONSE` (`Symbol.for("zudojs.middleware.guardResponse")`), which JSON
|
|
214
|
+
cannot carry, so data that happens to have a `status` key is never mistaken
|
|
215
|
+
for a response, and `isGuardResponse()` is `false` for any plain
|
|
216
|
+
`{ status, body, headers }` object. The result is frozen; `status` must be an
|
|
217
|
+
integer in 100–599 (`RangeError` otherwise) and header values strings
|
|
218
|
+
(`TypeError`).
|
|
219
|
+
|
|
186
220
|
## Errors
|
|
187
221
|
|
|
188
222
|
All errors extend `MiddlewareError`, which is the class owned by
|
package/dist/index.d.ts
CHANGED
|
@@ -14,4 +14,5 @@ export * from "./middlewareCore/index.js";
|
|
|
14
14
|
export * from "./middlewarePipeline/index.js";
|
|
15
15
|
export * from "./middlewareUtils/index.js";
|
|
16
16
|
export * from "./middlewareErrors/index.js";
|
|
17
|
+
export * from "./middlewareResponse/index.js";
|
|
17
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -14,4 +14,5 @@ export * from "./middlewareCore/index.js";
|
|
|
14
14
|
export * from "./middlewarePipeline/index.js";
|
|
15
15
|
export * from "./middlewareUtils/index.js";
|
|
16
16
|
export * from "./middlewareErrors/index.js";
|
|
17
|
+
export * from "./middlewareResponse/index.js";
|
|
17
18
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creation and detection of guard responses.
|
|
3
|
+
*
|
|
4
|
+
* @module middlewareResponse/guardResponse.factory
|
|
5
|
+
*/
|
|
6
|
+
import { type GuardResponse, type GuardResponseInit } from "./guardResponse.type.js";
|
|
7
|
+
/**
|
|
8
|
+
* Create a response a middleware returns to answer the request itself.
|
|
9
|
+
*
|
|
10
|
+
* Use it for a refusal — 400, 401, 403, 404, 423, 429 — so the transport
|
|
11
|
+
* sends that status. A plain `{ status, body, headers }` object is not
|
|
12
|
+
* recognised as a response: it is ordinary data.
|
|
13
|
+
*
|
|
14
|
+
* @param init - Status, optional body and headers.
|
|
15
|
+
* @returns A frozen, branded response.
|
|
16
|
+
* @throws {RangeError} If `status` is not an integer in 100–599.
|
|
17
|
+
* @throws {TypeError} If a header value is not a string.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* return createGuardResponse({ status: 403, body: { error: "Forbidden" } });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function createGuardResponse(init: GuardResponseInit): GuardResponse;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a value is a {@link GuardResponse}.
|
|
27
|
+
*
|
|
28
|
+
* Only the brand counts. An object with `status`, `body` and `headers` keys
|
|
29
|
+
* but no brand is not a guard response, so JSON data is never mistaken for
|
|
30
|
+
* one.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isGuardResponse(value: unknown): value is GuardResponse;
|
|
33
|
+
//# sourceMappingURL=guardResponse.factory.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creation and detection of guard responses.
|
|
3
|
+
*
|
|
4
|
+
* @module middlewareResponse/guardResponse.factory
|
|
5
|
+
*/
|
|
6
|
+
import { GUARD_RESPONSE, } from "./guardResponse.type.js";
|
|
7
|
+
const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
8
|
+
function isStatus(value) {
|
|
9
|
+
return (typeof value === "number" &&
|
|
10
|
+
Number.isInteger(value) &&
|
|
11
|
+
value >= 100 &&
|
|
12
|
+
value <= 599);
|
|
13
|
+
}
|
|
14
|
+
function isRawBody(body) {
|
|
15
|
+
return typeof body === "string" || body instanceof Uint8Array;
|
|
16
|
+
}
|
|
17
|
+
function normalizeHeaders(headers, body) {
|
|
18
|
+
const result = {};
|
|
19
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
20
|
+
if (typeof value !== "string") {
|
|
21
|
+
throw new TypeError(`Guard response header "${name}" must be a string.`);
|
|
22
|
+
}
|
|
23
|
+
result[name.toLowerCase()] = value;
|
|
24
|
+
}
|
|
25
|
+
if (body !== undefined && !isRawBody(body) && !result["content-type"]) {
|
|
26
|
+
result["content-type"] = JSON_CONTENT_TYPE;
|
|
27
|
+
}
|
|
28
|
+
return Object.freeze(result);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a response a middleware returns to answer the request itself.
|
|
32
|
+
*
|
|
33
|
+
* Use it for a refusal — 400, 401, 403, 404, 423, 429 — so the transport
|
|
34
|
+
* sends that status. A plain `{ status, body, headers }` object is not
|
|
35
|
+
* recognised as a response: it is ordinary data.
|
|
36
|
+
*
|
|
37
|
+
* @param init - Status, optional body and headers.
|
|
38
|
+
* @returns A frozen, branded response.
|
|
39
|
+
* @throws {RangeError} If `status` is not an integer in 100–599.
|
|
40
|
+
* @throws {TypeError} If a header value is not a string.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* return createGuardResponse({ status: 403, body: { error: "Forbidden" } });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function createGuardResponse(init) {
|
|
48
|
+
if (!isStatus(init.status)) {
|
|
49
|
+
throw new RangeError(`Guard response status must be an integer in 100-599, got ${String(init.status)}.`);
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
[GUARD_RESPONSE]: true,
|
|
53
|
+
status: init.status,
|
|
54
|
+
body: init.body,
|
|
55
|
+
headers: normalizeHeaders(init.headers, init.body),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether a value is a {@link GuardResponse}.
|
|
60
|
+
*
|
|
61
|
+
* Only the brand counts. An object with `status`, `body` and `headers` keys
|
|
62
|
+
* but no brand is not a guard response, so JSON data is never mistaken for
|
|
63
|
+
* one.
|
|
64
|
+
*/
|
|
65
|
+
export function isGuardResponse(value) {
|
|
66
|
+
return (typeof value === "object" &&
|
|
67
|
+
value !== null &&
|
|
68
|
+
value[GUARD_RESPONSE] ===
|
|
69
|
+
true &&
|
|
70
|
+
isStatus(value.status));
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=guardResponse.factory.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guard response contract: how a middleware that refuses a request tells
|
|
3
|
+
* the transport which status, headers and body to answer with.
|
|
4
|
+
*
|
|
5
|
+
* @module middlewareResponse/guardResponse.type
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Brand carried by every {@link GuardResponse}.
|
|
9
|
+
*
|
|
10
|
+
* A registered symbol (`Symbol.for`), so two copies of this package in one
|
|
11
|
+
* process still agree on it. JSON cannot carry a symbol, so request data can
|
|
12
|
+
* never forge the brand: a plain object that merely has a `status` key is not
|
|
13
|
+
* a guard response.
|
|
14
|
+
*/
|
|
15
|
+
export declare const GUARD_RESPONSE: unique symbol;
|
|
16
|
+
/**
|
|
17
|
+
* A response a middleware returns instead of calling `next()`.
|
|
18
|
+
*
|
|
19
|
+
* Created with {@link createGuardResponse}. `@zudojs/http` turns one into a
|
|
20
|
+
* real response with this status, these headers and this body; any other
|
|
21
|
+
* returned object keeps its previous meaning.
|
|
22
|
+
*/
|
|
23
|
+
export interface GuardResponse {
|
|
24
|
+
/** Marks the object as a guard response. */
|
|
25
|
+
readonly [GUARD_RESPONSE]: true;
|
|
26
|
+
/** HTTP status code, an integer in 100–599. */
|
|
27
|
+
readonly status: number;
|
|
28
|
+
/** Response body. Serialized as JSON unless it is a string or bytes. */
|
|
29
|
+
readonly body: unknown;
|
|
30
|
+
/** Response headers, keyed by lower-case name. */
|
|
31
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
32
|
+
}
|
|
33
|
+
/** Input to {@link createGuardResponse}. */
|
|
34
|
+
export interface GuardResponseInit {
|
|
35
|
+
/** HTTP status code, an integer in 100–599. */
|
|
36
|
+
readonly status: number;
|
|
37
|
+
/** Response body. Omit for an empty body. */
|
|
38
|
+
readonly body?: unknown;
|
|
39
|
+
/**
|
|
40
|
+
* Response headers. A JSON body gets `content-type: application/json`
|
|
41
|
+
* unless one is given here.
|
|
42
|
+
*/
|
|
43
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=guardResponse.type.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guard response contract: how a middleware that refuses a request tells
|
|
3
|
+
* the transport which status, headers and body to answer with.
|
|
4
|
+
*
|
|
5
|
+
* @module middlewareResponse/guardResponse.type
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Brand carried by every {@link GuardResponse}.
|
|
9
|
+
*
|
|
10
|
+
* A registered symbol (`Symbol.for`), so two copies of this package in one
|
|
11
|
+
* process still agree on it. JSON cannot carry a symbol, so request data can
|
|
12
|
+
* never forge the brand: a plain object that merely has a `status` key is not
|
|
13
|
+
* a guard response.
|
|
14
|
+
*/
|
|
15
|
+
export const GUARD_RESPONSE = Symbol.for("zudojs.middleware.guardResponse");
|
|
16
|
+
//# sourceMappingURL=guardResponse.type.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard responses: the contract a middleware uses to answer a request itself
|
|
3
|
+
* (a 401, 403, 404 …) instead of calling `next()`, and that `@zudojs/http`
|
|
4
|
+
* honours.
|
|
5
|
+
*
|
|
6
|
+
* @module middlewareResponse
|
|
7
|
+
*/
|
|
8
|
+
export { GUARD_RESPONSE, type GuardResponse, type GuardResponseInit, } from "./guardResponse.type.js";
|
|
9
|
+
export { createGuardResponse, isGuardResponse, } from "./guardResponse.factory.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard responses: the contract a middleware uses to answer a request itself
|
|
3
|
+
* (a 401, 403, 404 …) instead of calling `next()`, and that `@zudojs/http`
|
|
4
|
+
* honours.
|
|
5
|
+
*
|
|
6
|
+
* @module middlewareResponse
|
|
7
|
+
*/
|
|
8
|
+
export { GUARD_RESPONSE, } from "./guardResponse.type.js";
|
|
9
|
+
export { createGuardResponse, isGuardResponse, } from "./guardResponse.factory.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/middleware",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Composable middleware pipeline with composition, timing, error handling, and context propagation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"!dist/.tsbuildinfo"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@zudojs/errors": "1.
|
|
28
|
+
"@zudojs/errors": "1.3.0"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=24.0.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"typescript": "7.0.2",
|
|
35
|
-
"vitest": "^
|
|
35
|
+
"vitest": "^5.0.1"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"pipeline",
|
|
44
44
|
"composition"
|
|
45
45
|
],
|
|
46
|
-
"homepage": "https://
|
|
46
|
+
"homepage": "https://zudojs.oyinlola.site/docs/packages-middleware",
|
|
47
47
|
"bugs": {
|
|
48
48
|
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
49
49
|
},
|