@zeroxsolutions/server 0.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,91 @@
1
+ ## 0.3.0 (2026-09-13)
2
+
3
+ ### 💅 Refactors
4
+
5
+ - ⚠️ **server:** render a rejected request through the error handler ([59b6f32](https://github.com/zeroxsolutions/zeroxsolutions/commit/59b6f32))
6
+
7
+ ### ⚠️ Breaking Changes
8
+
9
+ - **server:** render a rejected request through the error handler ([59b6f32](https://github.com/zeroxsolutions/zeroxsolutions/commit/59b6f32))
10
+ `createValidationHook` throws the rejection instead of
11
+ answering it, so an app passing it to `OpenAPIHono` also needs
12
+ `createErrorHandler` as its `onError`; without one hono answers the
13
+ rejection with a text 500. The hook's type is now
14
+ `Hook<unknown, E, string, undefined>`. The handler asks `resolve` before
15
+ it renders the rejection, so a `resolve` answering every error now
16
+ answers validation failures too. Where `requestId` is configured, a
17
+ validation 400 or 422 carries it as each error object's `id`, and every
18
+ error document the handler renders carries `jsonapi: { version: '1.1' }`.
19
+
20
+ ### ❤️ Thank You
21
+
22
+ - Claude Sonnet 5
23
+ - Lương Văn Tú
24
+
25
+ ## 0.2.4 (2026-09-13)
26
+
27
+ This was a version bump only for @zeroxsolutions/server to align it with other projects, there were no code changes.
28
+
29
+ ## 0.2.3 (2026-09-13)
30
+
31
+ This was a version bump only for @zeroxsolutions/server to align it with other projects, there were no code changes.
32
+
33
+ ## 0.2.2 (2026-09-13)
34
+
35
+ ### 🧱 Updated Dependencies
36
+
37
+ - Updated @zeroxsolutions/response to 0.11.2
38
+ - Updated @zeroxsolutions/jsonapi to 0.10.2
39
+
40
+ ## 0.2.1 (2026-09-07)
41
+
42
+ Repackaged only, no source change: the previous tarball still carried the pre-flatten `dist/hono/http/` and `dist/hono/jsonapi/` tree, which `tsc --build` leaves behind when a source file is renamed away. 48 entries down to 21.
43
+
44
+ ## 0.2.0 (2026-09-07)
45
+
46
+ ### 🚀 Features
47
+
48
+ - ⚠️ **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
49
+
50
+ ### ⚠️ Breaking Changes
51
+
52
+ - **server,jsonapi:** one surface, one error-code vocabulary ([32b4aed](https://github.com/zeroxsolutions/zeroxsolutions/commit/32b4aed))
53
+ createErrorHandler, createValidationHook and
54
+ jsonApiContentNegotiation take a required `namespace`; negotiateJsonApi takes it
55
+ as a third argument. Codes these mint are now `<namespace>.<name>`.
56
+
57
+ ### ❤️ Thank You
58
+
59
+ - Lương Văn Tú
60
+
61
+ ## 0.1.0 (2026-09-07)
62
+
63
+ ### 💅 Refactors
64
+
65
+ - ⚠️ **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
66
+
67
+ ### ⚠️ Breaking Changes
68
+
69
+ - **query,response,server:** one wire spelling, flat exports ([5c57b3a](https://github.com/zeroxsolutions/zeroxsolutions/commit/5c57b3a))
70
+ `@zeroxsolutions/response/http`, `@zeroxsolutions/query/http`
71
+ and `@zeroxsolutions/server/hono/http` are gone, and the surviving `/jsonapi`
72
+ and `/hono` subpaths move to the package root. Every import drops its subpath.
73
+
74
+ ### ❤️ Thank You
75
+
76
+ - Lương Văn Tú
77
+
78
+ ## 0.0.3 (2026-09-07)
79
+
80
+ This was a version bump only for @zeroxsolutions/server to align it with other projects, there were no code changes.
81
+
82
+ ## 0.0.2 (2026-09-07)
83
+
84
+ ### 🚀 Features
85
+
86
+ - **server:** add the package holding every house Hono handler ([2b1f05d](https://github.com/zeroxsolutions/zeroxsolutions/commit/2b1f05d))
87
+
88
+ ### ❤️ Thank You
89
+
90
+ - Claude Opus 5 (1M context)
91
+ - Lương Văn Tú
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @zeroxsolutions/server
2
+
3
+ Every Hono handler the house ships: the middlewares, the `onError` and the `defaultHook` a worker wires
4
+ before its routes. A concern package holds the DECISION as a plain function over values;
5
+ this one holds the handler that calls it. That split is what leaves the concern packages framework-free.
6
+
7
+ | Holds | Wires the decisions in |
8
+ | --- | --- |
9
+ | `createEnvironment` | the worker's own `bindingsSchema` - no package owns a worker's bindings |
10
+ | `createErrorHandler`, `createValidationHook`, `jsonApiContentNegotiation` | `@zeroxsolutions/response`, `@zeroxsolutions/jsonapi` |
11
+
12
+ ## What the split bought
13
+
14
+ | Package | Peers before | Peers now |
15
+ | --- | --- | --- |
16
+ | `@zeroxsolutions/jsonapi` | `hono` | none - plain functions over strings and documents, which is what an SPA loads |
17
+ | `@zeroxsolutions/response` | `@hono/zod-openapi`, `@logtape/logtape`, `hono`, `zod` | `zod` - `hono` left with the plain spelling |
18
+
19
+ That is the test for anything else moving here: it moves when the move DROPS a framework peer from the
20
+ package it left. `auth()` and `permission()` do not - the provider's own surface decides them, and
21
+ `hasPermission` is a call rather than a shape. Neither does `dependencies()`, which registers one
22
+ context's own tokens and so has nothing to share.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ pnpm add @zeroxsolutions/server
28
+ ```
29
+
30
+ `hono`, `zod` and `@logtape/logtape` are peers - the worker already declares them.
31
+ `@zeroxsolutions/jsonapi` and `@zeroxsolutions/response` come with it, as dependencies: this package
32
+ CALLS them rather than reading a host's instance.
33
+
34
+ ## Quickstart
35
+
36
+ `createEnvironment` builds the gate that refuses a request whose bindings do not match the schema the
37
+ transport declares its `Bindings` from:
38
+
39
+ ```ts
40
+ // packages/<context>/src/entrypoints/http/middlewares/environment.ts
41
+ import { createEnvironment } from '@zeroxsolutions/server';
42
+ import { LOG_CATEGORY } from '../../../lib/constants/index.js';
43
+ import { bindingsSchema, type AppEnvironment } from '../env.js';
44
+
45
+ /** Refuses a request whose environment does not match `bindingsSchema`, before any other middleware reads a field. */
46
+ export const environment = createEnvironment<AppEnvironment>({ schema: bindingsSchema, category: LOG_CATEGORY });
47
+ ```
48
+
49
+ The mount site is unchanged, because the binder returns a factory rather than the middleware:
50
+
51
+ ```ts
52
+ // entrypoints/http/app.ts - first, so a missing binding is named once rather than at first use
53
+ app.use('*', environment());
54
+ ```
55
+
56
+ A rejected parse logs `app.env.invalid` with the failing PATHS and the request id, then throws an error
57
+ carrying no field name - which input an operator failed to set is not the caller's business, and the
58
+ transport's `onError` renders it as a bare 500.
59
+
60
+ `jsonApiContentNegotiation` binds to nothing of the product's - the media type and the parameter rule
61
+ are the standard's, identical on every surface answering it. It takes the surface's `namespace` for the
62
+ same reason the other two do: a refusal it mints is an error object like any other, and a client reading
63
+ `not_acceptable` beside `api.place.not_found` cannot tell which surface spoke.
64
+
65
+ ```ts
66
+ // entrypoints/http/app.ts - one middleware, before the routes, never a per-handler check
67
+ import { jsonApiContentNegotiation } from '@zeroxsolutions/server';
68
+
69
+ app.use('*', jsonApiContentNegotiation({ namespace: 'api' }));
70
+ ```
71
+
72
+ It refuses with `JsonApiError`, so the refusal renders through the same central `onError` as every other
73
+ failure. `@zeroxsolutions/jsonapi`'s README carries the two decisions the headers alone do not settle -
74
+ why `Content-Type` is read on a write only, and why an empty `Accept` is refused.
75
+
76
+ ## One error seat
77
+
78
+ `createErrorHandler` is the app's `onError`, and every failure renders through it as one
79
+ `application/vnd.api+json` error document carrying `jsonapi: { version: '1.1' }`. It asks, in order:
80
+
81
+ 1. `resolve(err, c)` - where a surface's domain error map answers, through
82
+ `@zeroxsolutions/response`'s `createDomainErrorResolver`;
83
+ 2. a request the validation hook rejected, one error object per issue;
84
+ 3. a `JsonApiError`, rendered as it was thrown;
85
+ 4. otherwise the status alone - a hono `HTTPException` at its own, anything else as a 500, written to
86
+ the log first because that 500 carries nothing of the error.
87
+
88
+ The `requestId` option's value becomes the `id` of each error object that carries none, so a client
89
+ quotes the id the request's log lines carry.
90
+
91
+ `createValidationHook` throws its rejection rather than answering, which is what puts a 400 or 422 on
92
+ that same path. It is still passed to the `OpenAPIHono` constructor, because without a hook
93
+ `@hono/zod-validator` (0.9.0) answers a rejected parse with its own raw 400. And it needs
94
+ `createErrorHandler` on the same app: hono answers an error no `onError` catches with a text 500.
95
+
96
+ ```ts
97
+ // entrypoints/http/app.ts
98
+ const app = new OpenAPIHono<AppEnvironment>({ defaultHook: createValidationHook({ namespace: 'api' }) });
99
+ app.onError(
100
+ createErrorHandler({
101
+ namespace: 'api',
102
+ category: LOG_CATEGORY,
103
+ resolve: createDomainErrorResolver(apiErrors),
104
+ requestId: (c) => c.var.requestId,
105
+ }),
106
+ );
107
+ ```
108
+
109
+ ## Why a binder rather than a ready middleware
110
+
111
+ The schema and the log category are the package's, not the library's: the schema is what the transport
112
+ declares its `Bindings` from, and the category names that worker's isolate. A ready middleware would
113
+ have to read both from somewhere global. Binding them once, at the module that owns them, keeps the
114
+ type flowing (`createEnvironment<AppEnvironment>`) and leaves the mount site the shape a middleware
115
+ module already has - a factory named for its file, called where it is mounted.
116
+
117
+ ## API reference
118
+
119
+ The version-accurate API **is the shipped types** - every export carries TSDoc compiled into
120
+ `dist/**/*.d.ts`. Read it in your editor or at `node_modules/@zeroxsolutions/server/dist/`. This README
121
+ does not restate the API: the types are the source of truth and never drift from the version.
122
+
123
+ ## Building & testing
124
+
125
+ ```sh
126
+ pnpm nx build @zeroxsolutions/server
127
+ pnpm nx test @zeroxsolutions/server
128
+ ```
@@ -0,0 +1,17 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { SurfaceVocabulary } from './surface-vocabulary.js';
3
+ /**
4
+ * Hono adapter over {@link negotiateJsonApi}: rejects with 415/406 per JSON:API v1.1 content
5
+ * negotiation, then stamps the JSON:API media type onto the response `Content-Type`.
6
+ *
7
+ * `Content-Type` is read only where the request carries content - a bodyless `Request` has `body === null`,
8
+ * a write's is a stream - because a header describes a representation and a read route has none, so an
9
+ * unconditional read would cost a client sending a blanket default the whole collection.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * app.use('*', jsonApiContentNegotiation({ namespace: 'api' })); // one middleware, before the routes
14
+ * ```
15
+ */
16
+ export declare function jsonApiContentNegotiation(options: SurfaceVocabulary): MiddlewareHandler;
17
+ //# sourceMappingURL=content-negotiation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-negotiation.d.ts","sourceRoot":"","sources":["../src/content-negotiation.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,iBAAiB,GAAG,iBAAiB,CAQvF"}
@@ -0,0 +1,25 @@
1
+ import { negotiateJsonApi } from '@zeroxsolutions/jsonapi';
2
+ import { JSON_API_MEDIA_TYPE } from '@zeroxsolutions/response';
3
+ /**
4
+ * Hono adapter over {@link negotiateJsonApi}: rejects with 415/406 per JSON:API v1.1 content
5
+ * negotiation, then stamps the JSON:API media type onto the response `Content-Type`.
6
+ *
7
+ * `Content-Type` is read only where the request carries content - a bodyless `Request` has `body === null`,
8
+ * a write's is a stream - because a header describes a representation and a read route has none, so an
9
+ * unconditional read would cost a client sending a blanket default the whole collection.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * app.use('*', jsonApiContentNegotiation({ namespace: 'api' })); // one middleware, before the routes
14
+ * ```
15
+ */
16
+ export function jsonApiContentNegotiation(options) {
17
+ return async (c, next) => {
18
+ const contentType = c.req.raw.body === null ? undefined : c.req.header('Content-Type');
19
+ const error = negotiateJsonApi(contentType, c.req.header('Accept'), options.namespace);
20
+ if (error)
21
+ throw error;
22
+ await next();
23
+ c.header('Content-Type', JSON_API_MEDIA_TYPE);
24
+ };
25
+ }
@@ -0,0 +1,19 @@
1
+ import type { Env, MiddlewareHandler } from 'hono';
2
+ import type { ZodType } from 'zod';
3
+ /** What {@link createEnvironment} binds to: the schema a worker's bindings must match, and the category naming its isolate. */
4
+ export interface EnvironmentOptions {
5
+ /** Parsed against `c.env`. Its inferred type is what the transport declares its `Bindings` from. */
6
+ schema: ZodType;
7
+ /** Handed to `getLogger` once, when the middleware is built rather than on each request. */
8
+ category: readonly string[];
9
+ }
10
+ /**
11
+ * Binds the environment gate to one package's schema and category, returning that package's own
12
+ * `environment` factory - the gate refuses a request whose `c.env` does not match `schema`, before any
13
+ * other middleware reads a field, so a missing binding is named once rather than at first use.
14
+ *
15
+ * It returns a FACTORY rather than the middleware: a middleware module exports a factory named for its
16
+ * file and the mount site calls it, so `app.use('*', environment())` is what a transport still writes.
17
+ */
18
+ export declare function createEnvironment<E extends Env = Env>({ schema, category, }: EnvironmentOptions): () => MiddlewareHandler<E>;
19
+ //# sourceMappingURL=environment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../src/environment.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAKnD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAEnC,+HAA+H;AAC/H,MAAM,WAAW,kBAAkB;IACjC,oGAAoG;IACpG,MAAM,EAAE,OAAO,CAAC;IAChB,4FAA4F;IAC5F,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG,EAAE,EACrD,MAAM,EACN,QAAQ,GACT,EAAE,kBAAkB,GAAG,MAAM,iBAAiB,CAAC,CAAC,CAAC,CAoBjD"}
@@ -0,0 +1,29 @@
1
+ import { getLogger } from '@logtape/logtape';
2
+ import { createMiddleware } from 'hono/factory';
3
+ /**
4
+ * Binds the environment gate to one package's schema and category, returning that package's own
5
+ * `environment` factory - the gate refuses a request whose `c.env` does not match `schema`, before any
6
+ * other middleware reads a field, so a missing binding is named once rather than at first use.
7
+ *
8
+ * It returns a FACTORY rather than the middleware: a middleware module exports a factory named for its
9
+ * file and the mount site calls it, so `app.use('*', environment())` is what a transport still writes.
10
+ */
11
+ export function createEnvironment({ schema, category, }) {
12
+ const logger = getLogger(category);
13
+ return () => createMiddleware(async (c, next) => {
14
+ const parsed = schema.safeParse(c.env);
15
+ if (!parsed.success) {
16
+ // Paths only: the field names are the whole diagnostic, and zod 4.4.3 putting no failing value
17
+ // on an issue is that version's property, not something a log sink could take back.
18
+ logger.error('app.env.invalid {requestId} {fields}', {
19
+ requestId: c.var.requestId,
20
+ fields: parsed.error.issues.map((issue) => issue.path.join('.')),
21
+ });
22
+ // Carries no field name: which input an operator failed to set is not the caller's business,
23
+ // and a transport's `onError` renders this as a bare 500.
24
+ throw new Error('environment is not configured');
25
+ }
26
+ // `parsed.data` goes unused because every consumer reads `c.env`, the raw object.
27
+ await next();
28
+ });
29
+ }
@@ -0,0 +1,26 @@
1
+ import type { Context, ErrorHandler } from 'hono';
2
+ import { type ResolvedError } from '@zeroxsolutions/response';
3
+ import type { SurfaceVocabulary } from './surface-vocabulary.js';
4
+ /** Tunes {@link createErrorHandler} for a specific transport (the edge gateway vs a service). */
5
+ export interface ErrorHandlerOptions extends SurfaceVocabulary {
6
+ /**
7
+ * The ISOLATE's own logging category, which the unhandled-500 line is written under. Required
8
+ * rather than defaulted: a category the isolate did not configure is silent, and silence here
9
+ * looks the same as a request that never failed.
10
+ */
11
+ category: readonly string[];
12
+ /** Resolves a transport's own error type first; `undefined` falls through to the built-in dispatch. */
13
+ resolve?: (err: Error, c: Context) => ResolvedError | undefined;
14
+ /** A request id stamped onto each error object's `id` when it carries none, so a client can correlate. */
15
+ requestId?: (c: Context) => string | undefined | null;
16
+ }
17
+ /**
18
+ * The one central `onError` a transport installs, rendering every failure as one `application/vnd.api+json`
19
+ * `errors[]` document. It asks, in order: `resolve`, then a request the validation hook rejected, then a
20
+ * {@link JsonApiError} as thrown, then the HTTP status alone.
21
+ *
22
+ * A failure none of those claimed is written to the log before it answers, because the generic 500 it renders
23
+ * carries nothing of the error itself.
24
+ */
25
+ export declare function createErrorHandler(options: ErrorHandlerOptions): ErrorHandler;
26
+ //# sourceMappingURL=error-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../src/error-handler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAGlD,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAuBjE,iGAAiG;AACjG,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC5D;;;;OAIG;IACH,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,uGAAuG;IACvG,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,KAAK,aAAa,GAAG,SAAS,CAAC;IAChE,0GAA0G;IAC1G,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;CACvD;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAkC7E"}
@@ -0,0 +1,60 @@
1
+ import { getLogger } from '@logtape/logtape';
2
+ import { HTTPException } from 'hono/http-exception';
3
+ import { BASE_ERROR_CODES, JsonApiError, JSON_API_MEDIA_TYPE, toErrorDocument, toErrorObject, } from '@zeroxsolutions/response';
4
+ import { RequestInvalid } from './request-invalid.js';
5
+ // The `code` and `title` each HTTP status is answered with, from the first entry the base registry gives it.
6
+ const META_BY_STATUS = new Map();
7
+ for (const [name, meta] of Object.entries(BASE_ERROR_CODES)) {
8
+ if (!META_BY_STATUS.has(meta.status)) {
9
+ META_BY_STATUS.set(meta.status, { code: name.toLowerCase(), title: meta.title });
10
+ }
11
+ }
12
+ // Projects a failure hono raised - or one nothing claimed - onto a JSON:API error object by HTTP status.
13
+ function errorObjectByStatus(err, namespace) {
14
+ const status = err instanceof HTTPException ? err.status : 500;
15
+ const meta = META_BY_STATUS.get(status) ?? { code: 'error', title: 'Error' };
16
+ const detail = err instanceof HTTPException ? err.message : 'Internal server error';
17
+ return {
18
+ status: String(status),
19
+ code: `${namespace}.${meta.code}`,
20
+ title: meta.title,
21
+ ...(detail && detail !== meta.title ? { detail } : {}),
22
+ };
23
+ }
24
+ /**
25
+ * The one central `onError` a transport installs, rendering every failure as one `application/vnd.api+json`
26
+ * `errors[]` document. It asks, in order: `resolve`, then a request the validation hook rejected, then a
27
+ * {@link JsonApiError} as thrown, then the HTTP status alone.
28
+ *
29
+ * A failure none of those claimed is written to the log before it answers, because the generic 500 it renders
30
+ * carries nothing of the error itself.
31
+ */
32
+ export function createErrorHandler(options) {
33
+ const logger = getLogger(options.category);
34
+ return (err, c) => {
35
+ const requestId = options.requestId?.(c);
36
+ const stamp = (e) => (requestId && !e.id ? { ...e, id: requestId } : e);
37
+ const claimed = options.resolve?.(err, c) ??
38
+ (err instanceof RequestInvalid ? { status: err.status, errors: err.errors } : undefined) ??
39
+ (err instanceof JsonApiError ? { status: err.status, errors: [toErrorObject(err)] } : undefined);
40
+ if (!claimed && !(err instanceof HTTPException)) {
41
+ logger.error('http.error.unhandled {method} {path} {type}', {
42
+ // Undefined where a transport never mounted `requestId()` - minting one here is a second id for one request.
43
+ requestId: requestId ?? c.get('requestId'),
44
+ method: c.req.method,
45
+ // `path`, not `url`: a query string is where a caller puts a token, and a sink keeps what it is given.
46
+ path: c.req.path,
47
+ type: err.name,
48
+ message: err.message,
49
+ });
50
+ }
51
+ const resolved = claimed ?? {
52
+ status: err instanceof HTTPException ? err.status : 500,
53
+ errors: [errorObjectByStatus(err, options.namespace)],
54
+ };
55
+ const doc = toErrorDocument(resolved.errors.map(stamp));
56
+ return c.body(JSON.stringify(doc), resolved.status, {
57
+ 'Content-Type': JSON_API_MEDIA_TYPE,
58
+ });
59
+ };
60
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Every Hono handler the house ships - the bindings gate, and the three the wire spelling decides.
3
+ */
4
+ export { createEnvironment, type EnvironmentOptions } from './environment.js';
5
+ export type { SurfaceVocabulary } from './surface-vocabulary.js';
6
+ export { jsonApiContentNegotiation } from './content-negotiation.js';
7
+ export { createErrorHandler, type ErrorHandlerOptions } from './error-handler.js';
8
+ export { createValidationHook } from './validation-hook.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC9E,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Every Hono handler the house ships - the bindings gate, and the three the wire spelling decides.
3
+ */
4
+ export { createEnvironment } from './environment.js';
5
+ export { jsonApiContentNegotiation } from './content-negotiation.js';
6
+ export { createErrorHandler } from './error-handler.js';
7
+ export { createValidationHook } from './validation-hook.js';
@@ -0,0 +1,12 @@
1
+ import type { ErrorObject } from '@zeroxsolutions/response';
2
+ /**
3
+ * A request its route's own schema rejected, thrown by the validation hook so the app's one `onError` renders it.
4
+ *
5
+ * The package root does not list it: the hook is its only producer and the error handler its only reader.
6
+ */
7
+ export declare class RequestInvalid extends Error {
8
+ readonly status: 400 | 422;
9
+ readonly errors: ErrorObject[];
10
+ constructor(status: 400 | 422, errors: ErrorObject[]);
11
+ }
12
+ //# sourceMappingURL=request-invalid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-invalid.d.ts","sourceRoot":"","sources":["../src/request-invalid.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,KAAK;IAErC,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG;IAC1B,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE;gBADrB,MAAM,EAAE,GAAG,GAAG,GAAG,EACjB,MAAM,EAAE,WAAW,EAAE;CAKjC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A request its route's own schema rejected, thrown by the validation hook so the app's one `onError` renders it.
3
+ *
4
+ * The package root does not list it: the hook is its only producer and the error handler its only reader.
5
+ */
6
+ export class RequestInvalid extends Error {
7
+ status;
8
+ errors;
9
+ constructor(status, errors) {
10
+ super('The request does not match its route schema');
11
+ this.status = status;
12
+ this.errors = errors;
13
+ this.name = 'RequestInvalid';
14
+ }
15
+ }
@@ -0,0 +1,11 @@
1
+ /** What every code a surface renders is prefixed with, so one surface answers in one vocabulary. */
2
+ export interface SurfaceVocabulary {
3
+ /**
4
+ * The transport's own namespace - `api`, `back_office` - prefixed onto every code these handlers mint.
5
+ *
6
+ * Required rather than defaulted: a handler answering `bad_request` beside a route map answering
7
+ * `api.place.not_found` publishes two vocabularies, and a client cannot tell which surface spoke.
8
+ */
9
+ namespace: string;
10
+ }
11
+ //# sourceMappingURL=surface-vocabulary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surface-vocabulary.d.ts","sourceRoot":"","sources":["../src/surface-vocabulary.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { Hook } from '@hono/zod-openapi';
2
+ import type { Env } from 'hono';
3
+ import type { SurfaceVocabulary } from './surface-vocabulary.js';
4
+ /**
5
+ * The `defaultHook` an `OpenAPIHono` is constructed with, rejecting a request its own schema refuses as one error
6
+ * object per issue - 422 for a `json` or `form` target, 400 otherwise.
7
+ *
8
+ * It throws rather than answering, so the rejection renders through the app's `createErrorHandler` like every other
9
+ * failure, carrying the request id that handler stamps; an app without that handler answers the throw with hono's
10
+ * text 500. @hono/zod-validator 0.9.0 lets a hook's throw through, and answers `c.json(result, 400)` itself unless a
11
+ * hook answers or throws first - which is why the hook is still passed to the constructor.
12
+ */
13
+ export declare function createValidationHook<E extends Env = Env>(options: SurfaceVocabulary): Hook<unknown, E, string, undefined>;
14
+ //# sourceMappingURL=validation-hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation-hook.d.ts","sourceRoot":"","sources":["../src/validation-hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAGhC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAoBjE;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,GAAG,GAAG,GAAG,EACtD,OAAO,EAAE,iBAAiB,GACzB,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,CAoBrC"}
@@ -0,0 +1,47 @@
1
+ import { BASE_ERROR_CODES } from '@zeroxsolutions/response';
2
+ import { RequestInvalid } from './request-invalid.js';
3
+ /**
4
+ * The error object's `source` for one issue, by the slot it was read from.
5
+ *
6
+ * The spec defines exactly three members, and a path segment matches none of them: `pointer` is a JSON
7
+ * Pointer into the request DOCUMENT, `parameter` a URI query parameter, `header` a header name. So a
8
+ * `param` or `cookie` issue names its slot in `meta` rather than borrowing a member that means
9
+ * something else.
10
+ */
11
+ function sourceFor(target, path) {
12
+ if (target === 'json' || target === 'form')
13
+ return { source: { pointer: `/${path.split('.').join('/')}` } };
14
+ if (target === 'query')
15
+ return { source: { parameter: path } };
16
+ if (target === 'header')
17
+ return { source: { header: path } };
18
+ return { meta: { target, path } };
19
+ }
20
+ /**
21
+ * The `defaultHook` an `OpenAPIHono` is constructed with, rejecting a request its own schema refuses as one error
22
+ * object per issue - 422 for a `json` or `form` target, 400 otherwise.
23
+ *
24
+ * It throws rather than answering, so the rejection renders through the app's `createErrorHandler` like every other
25
+ * failure, carrying the request id that handler stamps; an app without that handler answers the throw with hono's
26
+ * text 500. @hono/zod-validator 0.9.0 lets a hook's throw through, and answers `c.json(result, 400)` itself unless a
27
+ * hook answers or throws first - which is why the hook is still passed to the constructor.
28
+ */
29
+ export function createValidationHook(options) {
30
+ return (result) => {
31
+ if (result.success)
32
+ return;
33
+ const target = result.target;
34
+ const body = target === 'json' || target === 'form';
35
+ const { status, title } = body ? BASE_ERROR_CODES.UNPROCESSABLE_ENTITY : BASE_ERROR_CODES.BAD_REQUEST;
36
+ const code = `${options.namespace}.${body ? 'unprocessable_entity' : 'bad_request'}`;
37
+ const errors = result.error.issues.map((issue) => ({
38
+ status: String(status),
39
+ code,
40
+ title,
41
+ detail: issue.message,
42
+ // A whole-object refinement carries an empty path, so the slot it was read from stands in.
43
+ ...sourceFor(target, issue.path.map(String).join('.') || target),
44
+ }));
45
+ throw new RequestInvalid(status, errors);
46
+ };
47
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@zeroxsolutions/server",
3
+ "version": "0.3.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "description": "Every Hono handler the ZeroXSolutions house ships: the environment gate bound to a worker's own bindings schema, the central onError, the OpenAPIHono defaultHook and the JSON:API content-negotiation middleware - kept out of the packages that decide what they do, so those carry no framework peer.",
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "CHANGELOG.md",
12
+ "!**/*.tsbuildinfo"
13
+ ],
14
+ "exports": {
15
+ "./package.json": "./package.json",
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "tslib": "^2.3.0",
24
+ "@zeroxsolutions/jsonapi": "0.11.0",
25
+ "@zeroxsolutions/response": "0.12.0"
26
+ },
27
+ "peerDependencies": {
28
+ "@hono/zod-openapi": "^1.0.0",
29
+ "@logtape/logtape": "^2.0.0",
30
+ "hono": "^4.0.0",
31
+ "zod": "^4.3.6"
32
+ },
33
+ "devDependencies": {
34
+ "@hono/zod-openapi": "^1.4.0",
35
+ "@logtape/logtape": "^2.3.2",
36
+ "hono": "^4.12.27",
37
+ "zod": "^4.4.3"
38
+ },
39
+ "nx": {
40
+ "name": "@zeroxsolutions/server"
41
+ },
42
+ "main": "./dist/index.js",
43
+ "module": "./dist/index.js",
44
+ "types": "./dist/index.d.ts"
45
+ }