@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 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.2",
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.1.0"
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": "^4.1.11"
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://github.com/oyinlola-tech/zudo#readme",
46
+ "homepage": "https://zudojs.oyinlola.site/docs/packages-middleware",
47
47
  "bugs": {
48
48
  "url": "https://github.com/oyinlola-tech/zudo/issues"
49
49
  },