@zeroxsolutions/server 0.5.0 → 0.5.2

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,25 @@
1
+ ## 0.5.2 (2026-09-30)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **server:** answer a refused request document 409 or 403 ([3792166](https://github.com/zeroxsolutions/zeroxsolutions/commit/3792166))
6
+
7
+ ### ❤️ Thank You
8
+
9
+ - Claude Opus 5.5 (1M context)
10
+ - Lương Văn Tú
11
+
12
+ ## 0.5.1 (2026-09-29)
13
+
14
+ ### 🚀 Features
15
+
16
+ - **server:** name accept in vary on every negotiated response ([9978fd6](https://github.com/zeroxsolutions/zeroxsolutions/commit/9978fd6))
17
+
18
+ ### ❤️ Thank You
19
+
20
+ - Claude Opus 5.5 (1M context)
21
+ - Lương Văn Tú
22
+
1
23
  ## 0.5.0 (2026-09-29)
2
24
 
3
25
  ### 🩹 Fixes
package/README.md CHANGED
@@ -80,17 +80,24 @@ why `Content-Type` is read on a write only, and why an empty `Accept` is refused
80
80
 
81
81
  1. a query the permit-list refused (`QueryNotPermitted`), answered `400` as
82
82
  `<namespace>.query_not_permitted`, naming the parameter in `source.parameter`, before `resolve` sees it;
83
- 2. `resolve(err, c)` - where a surface's domain error map answers, through
83
+ 2. a request document the route does not accept (`RequestConflict` / `RequestForbidden` from
84
+ `@zeroxsolutions/response`, which its `requestDocumentSchema` and `readRequestDocument` throw),
85
+ answered `409` as `<namespace>.request_conflict` or `403` as `<namespace>.request_forbidden`, naming the
86
+ member in `source.pointer`, before `resolve` sees it;
87
+ 3. `resolve(err, c)` - where a surface's domain error map answers, through
84
88
  `@zeroxsolutions/response`'s `createDomainErrorResolver`;
85
- 3. a request the validation hook rejected, one error object per issue;
86
- 4. a `JsonApiError`, rendered as it was thrown;
87
- 5. otherwise the status alone - a hono `HTTPException` at its own, anything else as a 500.
89
+ 4. a request a guard refused - `Unauthenticated`, answered `401` as `<namespace>.unauthenticated`, or
90
+ `PermissionRefused`, answered `403` as `<namespace>.forbidden`;
91
+ 5. a request the validation hook rejected, one error object per issue;
92
+ 6. a `JsonApiError`, rendered as it was thrown;
93
+ 7. otherwise the status alone - a hono `HTTPException` at its own, anything else as a 500.
88
94
 
89
95
  Whichever step answers, the handler writes one `http.error.answered` line under `category` before the
90
96
  document goes out, at the level the answered status class decides: a 5xx at error, anything else at
91
97
  warn. The line carries the request id, the method, the path, the answered status, the step that claimed
92
- the failure (`resolve`, `validation`, `jsonapi`, `status`) and the error's type and message. Without it
93
- a mapped 500 or a guard's 403 reaches the client with nothing in the log for an operator to join it to.
98
+ the failure (`query`, `request`, `resolve`, `auth`, `validation`, `jsonapi`, `status`) and the error's type
99
+ and message. Without it a mapped 500 or a guard's 403 reaches the client with nothing in the log for an
100
+ operator to join it to.
94
101
 
95
102
  The `requestId` option's value becomes the `id` of each error object that carries none, so a client
96
103
  quotes the id the request's log lines carry.
@@ -1 +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;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,iBAAiB,GAAG;IAAE,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACvE,iBAAiB,CAWnB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAOxB,CAAC"}
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;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,iBAAiB,GAAG;IAAE,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACvE,iBAAiB,CAcnB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAOxB,CAAC"}
@@ -20,6 +20,9 @@ import { errorResponse, JSON_API_MEDIA_TYPE } from '@zeroxsolutions/response';
20
20
  export function jsonApiContentNegotiation(options) {
21
21
  const supported = new Set(options.supportedExtensions ?? []);
22
22
  return async (c, next) => {
23
+ // JSON:API v1.1: a server supporting the ext or profile parameters SHOULD name Accept in Vary. It is set
24
+ // before the decision so a refusal, which the error handler answers, carries it too.
25
+ c.header('Vary', 'Accept', { append: true });
23
26
  const error = negotiateJsonApi(c.req.header('Content-Type'), c.req.header('Accept'), options.namespace, supported);
24
27
  if (error)
25
28
  throw error;
@@ -22,9 +22,10 @@ export interface ErrorHandlerOptions extends SurfaceVocabulary {
22
22
  }
23
23
  /**
24
24
  * The one central `onError` a transport installs, rendering every failure as one `application/vnd.api+json`
25
- * `errors[]` document. It asks, in order: a query the permit-list refused, then `resolve`, then a guard's own
26
- * refusal, then a request the validation hook rejected, then a {@link JsonApiError} as thrown, then the HTTP
27
- * status alone.
25
+ * `errors[]` document. It asks, in order: a query the permit-list refused, then a request document the route does not
26
+ * accept (`RequestConflict` as 409, `RequestForbidden` as 403, each naming its member in `source.pointer`), then
27
+ * `resolve`, then a guard's own refusal, then a request the validation hook rejected, then a {@link JsonApiError} as
28
+ * thrown, then the HTTP status alone.
28
29
  *
29
30
  * Every answer is written to the log before it goes out, at the level its status class decides: a 5xx at error,
30
31
  * because this side went wrong, anything else at warn, because the request was refused. Without the line, a
@@ -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,EAML,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;AAyDD;;;;;;;;;;;;GAYG;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,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,7 +1,7 @@
1
1
  import { getLogger } from '@logtape/logtape';
2
2
  import { HTTPException } from 'hono/http-exception';
3
3
  import { QueryNotPermitted } from '@zeroxsolutions/query';
4
- import { BASE_ERROR_CODES, JsonApiError, JSON_API_MEDIA_TYPE, toErrorDocument, toErrorObject, } from '@zeroxsolutions/response';
4
+ import { BASE_ERROR_CODES, JsonApiError, JSON_API_MEDIA_TYPE, RequestConflict, RequestForbidden, toErrorDocument, toErrorObject, } from '@zeroxsolutions/response';
5
5
  import { PermissionRefused } from './permission-refused.js';
6
6
  import { RequestInvalid } from './request-invalid.js';
7
7
  import { Unauthenticated } from './unauthenticated.js';
@@ -25,8 +25,9 @@ export function errorObjectByStatus(err, status, namespace) {
25
25
  ...(detail && detail !== meta.title ? { detail } : {}),
26
26
  };
27
27
  }
28
- // Asks each step of the dispatch in turn: a query the permit-list refused, `resolve`, a guard's refusal, a request
29
- // the validation hook rejected, a `JsonApiError` as thrown, the HTTP status alone.
28
+ // Asks each step of the dispatch in turn: a query the permit-list refused, a request document the route does not
29
+ // accept, `resolve`, a guard's refusal, a request the validation hook rejected, a `JsonApiError` as thrown, the HTTP
30
+ // status alone.
30
31
  function claimError(err, c, options) {
31
32
  if (err instanceof QueryNotPermitted) {
32
33
  const error = {
@@ -38,6 +39,18 @@ function claimError(err, c, options) {
38
39
  };
39
40
  return { claimedBy: 'query', resolved: { status: 400, errors: [error] } };
40
41
  }
42
+ if (err instanceof RequestConflict || err instanceof RequestForbidden) {
43
+ const conflict = err instanceof RequestConflict;
44
+ const { status, title } = conflict ? BASE_ERROR_CODES.CONFLICT : BASE_ERROR_CODES.FORBIDDEN;
45
+ const error = {
46
+ status: String(status),
47
+ code: `${options.namespace}.${conflict ? 'request_conflict' : 'request_forbidden'}`,
48
+ title,
49
+ detail: err.message,
50
+ source: { pointer: err.pointer },
51
+ };
52
+ return { claimedBy: 'request', resolved: { status, errors: [error] } };
53
+ }
41
54
  const resolved = options.resolve?.(err, c);
42
55
  if (resolved) {
43
56
  return { claimedBy: 'resolve', resolved };
@@ -81,9 +94,10 @@ function withChallenge(claimed, authenticationChallenge) {
81
94
  }
82
95
  /**
83
96
  * The one central `onError` a transport installs, rendering every failure as one `application/vnd.api+json`
84
- * `errors[]` document. It asks, in order: a query the permit-list refused, then `resolve`, then a guard's own
85
- * refusal, then a request the validation hook rejected, then a {@link JsonApiError} as thrown, then the HTTP
86
- * status alone.
97
+ * `errors[]` document. It asks, in order: a query the permit-list refused, then a request document the route does not
98
+ * accept (`RequestConflict` as 409, `RequestForbidden` as 403, each naming its member in `source.pointer`), then
99
+ * `resolve`, then a guard's own refusal, then a request the validation hook rejected, then a {@link JsonApiError} as
100
+ * thrown, then the HTTP status alone.
87
101
  *
88
102
  * Every answer is written to the log before it goes out, at the level its status class decides: a 5xx at error,
89
103
  * because this side went wrong, anything else at warn, because the request was refused. Without the line, a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/server",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "tslib": "^2.3.0",
24
- "@zeroxsolutions/jsonapi": "0.13.0",
25
- "@zeroxsolutions/response": "0.14.2",
24
+ "@zeroxsolutions/jsonapi": "0.13.1",
25
+ "@zeroxsolutions/response": "0.14.3",
26
26
  "@zeroxsolutions/query": "0.7.1"
27
27
  },
28
28
  "peerDependencies": {
@@ -35,7 +35,7 @@
35
35
  "@hono/zod-openapi": "^1.4.0",
36
36
  "@logtape/logtape": "^2.3.2",
37
37
  "hono": "^4.12.27",
38
- "zod": "^4.4.3"
38
+ "zod": "^4.3.6"
39
39
  },
40
40
  "nx": {
41
41
  "name": "@zeroxsolutions/server"