@zeroxsolutions/server 0.5.4 → 0.6.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 CHANGED
@@ -1,3 +1,21 @@
1
+ ## 0.6.0 (2026-10-05)
2
+
3
+ ### 🚀 Features
4
+
5
+ - ⚠️ **server:** answer an update a resource does not support 403 ([7607c77](https://github.com/zeroxsolutions/zeroxsolutions/commit/7607c77))
6
+
7
+ ### ⚠️ Breaking Changes
8
+
9
+ - **server:** answer an update a resource does not support 403 ([7607c77](https://github.com/zeroxsolutions/zeroxsolutions/commit/7607c77))
10
+ jsonApiNotFound throws instead of rendering, so a surface
11
+ installing it installs createErrorHandler as its onError.
12
+ Refs: zeroxsolutions/travel#283
13
+
14
+ ### ❤️ Thank You
15
+
16
+ - Claude Opus 5.5 (1M context)
17
+ - Lương Văn Tú
18
+
1
19
  ## 0.5.3 (2026-10-03)
2
20
 
3
21
  This was a version bump only for @zeroxsolutions/server to align it with other projects, there were no code changes.
package/README.md CHANGED
@@ -116,19 +116,26 @@ app.onError(
116
116
  category: LOG_CATEGORY,
117
117
  resolve: createDomainErrorResolver(apiErrors),
118
118
  requestId: (c) => c.var.requestId,
119
- authenticationChallenge: 'Bearer',
119
+ authenticationChallenge: 'Session realm="api"',
120
120
  }),
121
121
  );
122
122
  ```
123
123
 
124
- `jsonApiNotFound({ namespace })` is `app.notFound(...)` - the answer for a route nobody declared. hono
125
- runs `notFound` outside `onError`, so this renders the same 404 error document without writing an
126
- `http.error.answered` line or stamping a request id: there is no failure here for the log line to join
127
- to, since nothing was thrown.
124
+ `jsonApiNotFound({ namespace, routes })` is `app.notFound(...)` - the answer for a request no route
125
+ matched. It throws rather than rendering, so `createErrorHandler` answers it, writes its
126
+ `http.error.answered` line and stamps its request id; an app installing it installs
127
+ `createErrorHandler` too, or hono answers the throw with a text 500.
128
+
129
+ A PATCH to a path one of `routes` serves for another method throws `RequestForbidden` at `/data`,
130
+ answered `403` as `<namespace>.request_forbidden`, because JSON:API 1.1 answers an update a resource
131
+ does not support 403. Anything else, and every request when `routes` is left out, throws a 404
132
+ `HTTPException`, answered as `<namespace>.not_found`. `routes` is the app's own `app.routes`, read on
133
+ the first unmatched request; middleware (method `ALL`) serves no path, and a trailing slash the app
134
+ does not serve stays a 404.
128
135
 
129
136
  ```ts
130
- // entrypoints/http/app.ts
131
- app.notFound(jsonApiNotFound({ namespace: 'api' }));
137
+ // entrypoints/http/app.ts, after app.onError(createErrorHandler(...))
138
+ app.notFound(jsonApiNotFound({ namespace: 'api', routes: app.routes }));
132
139
  ```
133
140
 
134
141
  ## Why a binder rather than a ready middleware
@@ -1,7 +1,6 @@
1
1
  import type { Context, ErrorHandler } from 'hono';
2
- import { type ErrorObject, type ResolvedError } from '@zeroxsolutions/response';
2
+ import { type ResolvedError } from '@zeroxsolutions/response';
3
3
  import type { SurfaceVocabulary } from './surface-vocabulary.js';
4
- export declare function errorObjectByStatus(err: Error, status: number, namespace: string): ErrorObject;
5
4
  /** Tunes {@link createErrorHandler} for a specific transport (the edge gateway vs a service). */
6
5
  export interface ErrorHandlerOptions extends SurfaceVocabulary {
7
6
  /**
@@ -1 +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;AAIlD,OAAO,EAQL,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AA4BjE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CAS9F;AAED,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;IACtD;;;OAGG;IACH,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAsED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAkC7E"}
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;AAIlD,OAAO,EASL,KAAK,aAAa,EACnB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAuCjE,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;IACtD;;;OAGG;IACH,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAsED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAkC7E"}
@@ -15,7 +15,7 @@ for (const [name, meta] of Object.entries(BASE_ERROR_CODES)) {
15
15
  // The message template both levels write, so one search finds every error this handler answered.
16
16
  const ANSWERED_LINE = 'http.error.answered {method} {path} {status} {claimedBy}';
17
17
  // Projects a failure hono raised - or one nothing claimed - onto a JSON:API error object by HTTP status.
18
- export function errorObjectByStatus(err, status, namespace) {
18
+ function errorObjectByStatus(err, status, namespace) {
19
19
  const meta = META_BY_STATUS.get(status) ?? { code: 'error', title: 'Error' };
20
20
  const detail = err instanceof HTTPException ? err.message : undefined;
21
21
  return {
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { createEnvironment, type EnvironmentOptions } from './environment.js';
5
5
  export type { SurfaceVocabulary } from './surface-vocabulary.js';
6
6
  export { jsonApiContentNegotiation, NEGOTIATION_RESPONSES } from './content-negotiation.js';
7
7
  export { createErrorHandler, type ErrorHandlerOptions } from './error-handler.js';
8
- export { jsonApiNotFound } from './not-found.js';
8
+ export { jsonApiNotFound, type NotFoundOptions } from './not-found.js';
9
9
  export { PermissionRefused } from './permission-refused.js';
10
10
  export { Unauthenticated } from './unauthenticated.js';
11
11
  export { createValidationHook } from './validation-hook.js';
@@ -1 +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,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
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,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -1,5 +1,21 @@
1
1
  import type { NotFoundHandler } from 'hono';
2
2
  import type { SurfaceVocabulary } from './surface-vocabulary.js';
3
- /** The `notFound` handler a JSON:API surface installs, so a route nobody declared answers an error document. */
4
- export declare function jsonApiNotFound(options: SurfaceVocabulary): NotFoundHandler;
3
+ /** What {@link jsonApiNotFound} is told about the surface it answers for. */
4
+ export interface NotFoundOptions extends SurfaceVocabulary {
5
+ /** The routes the app serves, as `app.routes` lists them; a PATCH to a path one serves for another method answers 403. */
6
+ readonly routes?: readonly {
7
+ readonly method: string;
8
+ readonly path: string;
9
+ }[];
10
+ }
11
+ /**
12
+ * The `notFound` handler a JSON:API surface installs; it throws, so the surface's `onError` renders and logs the
13
+ * answer. A surface installing it installs `createErrorHandler` as its `onError`, or hono answers the throw
14
+ * with its own text 500.
15
+ *
16
+ * A PATCH to a path some route in {@link NotFoundOptions.routes} serves for another method throws
17
+ * `RequestForbidden` at `/data`, which JSON:API 1.1 answers 403; anything else throws a 404 `HTTPException`,
18
+ * answered `<namespace>.not_found`. Without `routes`, every request is the 404.
19
+ */
20
+ export declare function jsonApiNotFound(options: NotFoundOptions): NotFoundHandler;
5
21
  //# sourceMappingURL=not-found.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"not-found.d.ts","sourceRoot":"","sources":["../src/not-found.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAE5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,gHAAgH;AAChH,wBAAgB,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,eAAe,CAK3E"}
1
+ {"version":3,"file":"not-found.d.ts","sourceRoot":"","sources":["../src/not-found.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAI5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,6EAA6E;AAC7E,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD,0HAA0H;IAC1H,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACjF;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,eAAe,CAczE"}
package/dist/not-found.js CHANGED
@@ -1,8 +1,37 @@
1
- import { JSON_API_MEDIA_TYPE, toErrorDocument } from '@zeroxsolutions/response';
2
- import { errorObjectByStatus } from './error-handler.js';
3
- /** The `notFound` handler a JSON:API surface installs, so a route nobody declared answers an error document. */
1
+ import { RequestForbidden } from '@zeroxsolutions/response';
2
+ import { HTTPException } from 'hono/http-exception';
3
+ import { METHOD_NAME_ALL } from 'hono/router';
4
+ import { TrieRouter } from 'hono/router/trie-router';
5
+ /**
6
+ * The `notFound` handler a JSON:API surface installs; it throws, so the surface's `onError` renders and logs the
7
+ * answer. A surface installing it installs `createErrorHandler` as its `onError`, or hono answers the throw
8
+ * with its own text 500.
9
+ *
10
+ * A PATCH to a path some route in {@link NotFoundOptions.routes} serves for another method throws
11
+ * `RequestForbidden` at `/data`, which JSON:API 1.1 answers 403; anything else throws a 404 `HTTPException`,
12
+ * answered `<namespace>.not_found`. Without `routes`, every request is the 404.
13
+ */
4
14
  export function jsonApiNotFound(options) {
5
- return (c) => c.body(JSON.stringify(toErrorDocument(errorObjectByStatus(new Error('Not Found'), 404, options.namespace))), 404, {
6
- 'Content-Type': JSON_API_MEDIA_TYPE,
7
- });
15
+ // Built on the first unmatched request, because `routes` is the app's own array and fills as routes mount.
16
+ // TrieRouter matches strictly, as the app's default router does; PatternRouter and LinearRouter also match
17
+ // `/things/1/` against `/things/:id`, which the app answers 404.
18
+ let served;
19
+ return (c) => {
20
+ if (c.req.method === 'PATCH' && options.routes !== undefined) {
21
+ served ??= routerServing(options.routes);
22
+ if (served.match(c.req.method, c.req.path)[0].length > 0) {
23
+ throw new RequestForbidden('/data', 'This resource does not support an update.');
24
+ }
25
+ }
26
+ throw new HTTPException(404);
27
+ };
28
+ }
29
+ // Middleware mounts with method `ALL`, so leaving those out keeps a path only middleware covers a 404.
30
+ function routerServing(routes) {
31
+ const router = new TrieRouter();
32
+ for (const route of routes) {
33
+ if (route.method !== METHOD_NAME_ALL)
34
+ router.add(METHOD_NAME_ALL, route.path, true);
35
+ }
36
+ return router;
8
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/server",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -21,9 +21,9 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "tslib": "^2.3.0",
24
- "@zeroxsolutions/query": "0.7.3",
25
24
  "@zeroxsolutions/jsonapi": "0.13.1",
26
- "@zeroxsolutions/response": "0.14.3"
25
+ "@zeroxsolutions/response": "0.14.3",
26
+ "@zeroxsolutions/query": "0.7.3"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "@hono/zod-openapi": "^1.0.0",