@zeroxsolutions/server 0.3.2 → 0.3.4

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,29 @@
1
+ ## 0.3.4 (2026-09-27)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **server:** declare the refusals content negotiation answers ([b137598](https://github.com/zeroxsolutions/zeroxsolutions/commit/b137598))
6
+
7
+ ### 🩹 Fixes
8
+
9
+ - **server:** stop repeating an unclaimed 500's title as its detail ([64c67d9](https://github.com/zeroxsolutions/zeroxsolutions/commit/64c67d9))
10
+
11
+ ### ❤️ Thank You
12
+
13
+ - Claude Opus 5.5 (1M context)
14
+ - Lương Văn Tú
15
+
16
+ # @zeroxsolutions/server@0.3.3 (2026-09-18)
17
+
18
+ ### 🚀 Features
19
+
20
+ - **server:** answer a guard's refusal from the one error handler ([d4ae306](https://github.com/zeroxsolutions/zeroxsolutions/commit/d4ae306))
21
+
22
+ ### ❤️ Thank You
23
+
24
+ - Claude Opus 5 (1M context)
25
+ - Lương Văn Tú
26
+
1
27
  ## 0.3.2 (2026-09-18)
2
28
 
3
29
  This was a version bump only for @zeroxsolutions/server to align it with other projects, there were no code changes.
@@ -14,4 +14,191 @@ import type { SurfaceVocabulary } from './surface-vocabulary.js';
14
14
  * ```
15
15
  */
16
16
  export declare function jsonApiContentNegotiation(options: SurfaceVocabulary): MiddlewareHandler;
17
+ /**
18
+ * The refusals {@link jsonApiContentNegotiation} answers with, for a route to spread into its declared
19
+ * responses, so a status the gate gains is declared wherever it is mounted.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * responses: { 200: okResponse('The place', placeResource), ...NEGOTIATION_RESPONSES, 500: errorResponse('An unhandled failure') }
24
+ * ```
25
+ */
26
+ export declare const NEGOTIATION_RESPONSES: {
27
+ readonly 406: import("@zeroxsolutions/response").JsonApiResponse<import("zod").ZodObject<{
28
+ errors: import("zod").ZodArray<import("zod").ZodObject<{
29
+ id: import("zod").ZodOptional<import("zod").ZodString>;
30
+ links: import("zod").ZodOptional<import("zod").ZodObject<{
31
+ about: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
32
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
33
+ title: import("zod").ZodOptional<import("zod").ZodString>;
34
+ type: import("zod").ZodOptional<import("zod").ZodString>;
35
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
36
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
37
+ href: import("zod").ZodString;
38
+ describedby: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
39
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
40
+ title: import("zod").ZodOptional<import("zod").ZodString>;
41
+ type: import("zod").ZodOptional<import("zod").ZodString>;
42
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
43
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
44
+ href: import("zod").ZodString;
45
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
46
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
47
+ type: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
48
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
49
+ title: import("zod").ZodOptional<import("zod").ZodString>;
50
+ type: import("zod").ZodOptional<import("zod").ZodString>;
51
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
52
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
53
+ href: import("zod").ZodString;
54
+ describedby: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
55
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
56
+ title: import("zod").ZodOptional<import("zod").ZodString>;
57
+ type: import("zod").ZodOptional<import("zod").ZodString>;
58
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
59
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
60
+ href: import("zod").ZodString;
61
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
62
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
63
+ }, import("zod/v4/core").$strip>>;
64
+ status: import("zod").ZodOptional<import("zod").ZodString>;
65
+ code: import("zod").ZodOptional<import("zod").ZodString>;
66
+ title: import("zod").ZodOptional<import("zod").ZodString>;
67
+ detail: import("zod").ZodOptional<import("zod").ZodString>;
68
+ source: import("zod").ZodOptional<import("zod").ZodObject<{
69
+ pointer: import("zod").ZodOptional<import("zod").ZodString>;
70
+ parameter: import("zod").ZodOptional<import("zod").ZodString>;
71
+ header: import("zod").ZodOptional<import("zod").ZodString>;
72
+ }, import("zod/v4/core").$strip>>;
73
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
74
+ }, import("zod/v4/core").$strip>>;
75
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
76
+ links: import("zod").ZodOptional<import("zod").ZodObject<{
77
+ self: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
78
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
79
+ title: import("zod").ZodOptional<import("zod").ZodString>;
80
+ type: import("zod").ZodOptional<import("zod").ZodString>;
81
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
82
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
83
+ href: import("zod").ZodString;
84
+ describedby: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
85
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
86
+ title: import("zod").ZodOptional<import("zod").ZodString>;
87
+ type: import("zod").ZodOptional<import("zod").ZodString>;
88
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
89
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
90
+ href: import("zod").ZodString;
91
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
92
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
93
+ describedby: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
94
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
95
+ title: import("zod").ZodOptional<import("zod").ZodString>;
96
+ type: import("zod").ZodOptional<import("zod").ZodString>;
97
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
98
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
99
+ href: import("zod").ZodString;
100
+ describedby: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
101
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
102
+ title: import("zod").ZodOptional<import("zod").ZodString>;
103
+ type: import("zod").ZodOptional<import("zod").ZodString>;
104
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
105
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
106
+ href: import("zod").ZodString;
107
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
108
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
109
+ }, import("zod/v4/core").$strip>>;
110
+ jsonapi: import("zod").ZodOptional<import("zod").ZodObject<{
111
+ version: import("zod").ZodOptional<import("zod").ZodString>;
112
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
113
+ }, import("zod/v4/core").$strip>>;
114
+ }, import("zod/v4/core").$strip>>;
115
+ readonly 415: import("@zeroxsolutions/response").JsonApiResponse<import("zod").ZodObject<{
116
+ errors: import("zod").ZodArray<import("zod").ZodObject<{
117
+ id: import("zod").ZodOptional<import("zod").ZodString>;
118
+ links: import("zod").ZodOptional<import("zod").ZodObject<{
119
+ about: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
120
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
121
+ title: import("zod").ZodOptional<import("zod").ZodString>;
122
+ type: import("zod").ZodOptional<import("zod").ZodString>;
123
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
124
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
125
+ href: import("zod").ZodString;
126
+ describedby: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
127
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
128
+ title: import("zod").ZodOptional<import("zod").ZodString>;
129
+ type: import("zod").ZodOptional<import("zod").ZodString>;
130
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
131
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
132
+ href: import("zod").ZodString;
133
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
134
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
135
+ type: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
136
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
137
+ title: import("zod").ZodOptional<import("zod").ZodString>;
138
+ type: import("zod").ZodOptional<import("zod").ZodString>;
139
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
140
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
141
+ href: import("zod").ZodString;
142
+ describedby: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
143
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
144
+ title: import("zod").ZodOptional<import("zod").ZodString>;
145
+ type: import("zod").ZodOptional<import("zod").ZodString>;
146
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
147
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
148
+ href: import("zod").ZodString;
149
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
150
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
151
+ }, import("zod/v4/core").$strip>>;
152
+ status: import("zod").ZodOptional<import("zod").ZodString>;
153
+ code: import("zod").ZodOptional<import("zod").ZodString>;
154
+ title: import("zod").ZodOptional<import("zod").ZodString>;
155
+ detail: import("zod").ZodOptional<import("zod").ZodString>;
156
+ source: import("zod").ZodOptional<import("zod").ZodObject<{
157
+ pointer: import("zod").ZodOptional<import("zod").ZodString>;
158
+ parameter: import("zod").ZodOptional<import("zod").ZodString>;
159
+ header: import("zod").ZodOptional<import("zod").ZodString>;
160
+ }, import("zod/v4/core").$strip>>;
161
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
162
+ }, import("zod/v4/core").$strip>>;
163
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
164
+ links: import("zod").ZodOptional<import("zod").ZodObject<{
165
+ self: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
166
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
167
+ title: import("zod").ZodOptional<import("zod").ZodString>;
168
+ type: import("zod").ZodOptional<import("zod").ZodString>;
169
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
170
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
171
+ href: import("zod").ZodString;
172
+ describedby: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
173
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
174
+ title: import("zod").ZodOptional<import("zod").ZodString>;
175
+ type: import("zod").ZodOptional<import("zod").ZodString>;
176
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
177
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
178
+ href: import("zod").ZodString;
179
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
180
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
181
+ describedby: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
182
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
183
+ title: import("zod").ZodOptional<import("zod").ZodString>;
184
+ type: import("zod").ZodOptional<import("zod").ZodString>;
185
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
186
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
187
+ href: import("zod").ZodString;
188
+ describedby: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodObject<{
189
+ rel: import("zod").ZodOptional<import("zod").ZodString>;
190
+ title: import("zod").ZodOptional<import("zod").ZodString>;
191
+ type: import("zod").ZodOptional<import("zod").ZodString>;
192
+ hreflang: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodString>]>>;
193
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
194
+ href: import("zod").ZodString;
195
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
196
+ }, import("zod/v4/core").$strip>, import("zod").ZodNull]>>;
197
+ }, import("zod/v4/core").$strip>>;
198
+ jsonapi: import("zod").ZodOptional<import("zod").ZodObject<{
199
+ version: import("zod").ZodOptional<import("zod").ZodString>;
200
+ meta: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
201
+ }, import("zod/v4/core").$strip>>;
202
+ }, import("zod/v4/core").$strip>>;
203
+ };
17
204
  //# sourceMappingURL=content-negotiation.d.ts.map
@@ -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;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,iBAAiB,GAAG,iBAAiB,CAQvF"}
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;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAGxB,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { negotiateJsonApi } from '@zeroxsolutions/jsonapi';
2
- import { JSON_API_MEDIA_TYPE } from '@zeroxsolutions/response';
2
+ import { errorResponse, JSON_API_MEDIA_TYPE } from '@zeroxsolutions/response';
3
3
  /**
4
4
  * Hono adapter over {@link negotiateJsonApi}: rejects with 415/406 per JSON:API v1.1 content
5
5
  * negotiation, then stamps the JSON:API media type onto the response `Content-Type`.
@@ -23,3 +23,16 @@ export function jsonApiContentNegotiation(options) {
23
23
  c.header('Content-Type', JSON_API_MEDIA_TYPE);
24
24
  };
25
25
  }
26
+ /**
27
+ * The refusals {@link jsonApiContentNegotiation} answers with, for a route to spread into its declared
28
+ * responses, so a status the gate gains is declared wherever it is mounted.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * responses: { 200: okResponse('The place', placeResource), ...NEGOTIATION_RESPONSES, 500: errorResponse('An unhandled failure') }
33
+ * ```
34
+ */
35
+ export const NEGOTIATION_RESPONSES = {
36
+ 406: errorResponse('The Accept header names no type this surface answers in'),
37
+ 415: errorResponse('The Content-Type header names a type this surface does not read'),
38
+ };
@@ -16,8 +16,8 @@ export interface ErrorHandlerOptions extends SurfaceVocabulary {
16
16
  }
17
17
  /**
18
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.
19
+ * `errors[]` document. It asks, in order: `resolve`, then a guard's own refusal, then a request the validation
20
+ * hook rejected, then a {@link JsonApiError} as thrown, then the HTTP status alone.
21
21
  *
22
22
  * Every answer is written to the log before it goes out, at the level its status class decides: a 5xx at error,
23
23
  * 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;AAGlD,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAkCjE,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;AAmBD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CA+B7E"}
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;AAGlC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAqCjE,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;AA2BD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAiC7E"}
@@ -1,7 +1,9 @@
1
1
  import { getLogger } from '@logtape/logtape';
2
2
  import { HTTPException } from 'hono/http-exception';
3
3
  import { BASE_ERROR_CODES, JsonApiError, JSON_API_MEDIA_TYPE, toErrorDocument, toErrorObject, } from '@zeroxsolutions/response';
4
+ import { PermissionRefused } from './permission-refused.js';
4
5
  import { RequestInvalid } from './request-invalid.js';
6
+ import { Unauthenticated } from './unauthenticated.js';
5
7
  // The `code` and `title` each HTTP status is answered with, from the first entry the base registry gives it.
6
8
  const META_BY_STATUS = new Map();
7
9
  for (const [name, meta] of Object.entries(BASE_ERROR_CODES)) {
@@ -14,7 +16,7 @@ const ANSWERED_LINE = 'http.error.answered {method} {path} {status} {claimedBy}'
14
16
  // Projects a failure hono raised - or one nothing claimed - onto a JSON:API error object by HTTP status.
15
17
  function errorObjectByStatus(err, status, namespace) {
16
18
  const meta = META_BY_STATUS.get(status) ?? { code: 'error', title: 'Error' };
17
- const detail = err instanceof HTTPException ? err.message : 'Internal server error';
19
+ const detail = err instanceof HTTPException ? err.message : undefined;
18
20
  return {
19
21
  status: String(status),
20
22
  code: `${namespace}.${meta.code}`,
@@ -22,13 +24,21 @@ function errorObjectByStatus(err, status, namespace) {
22
24
  ...(detail && detail !== meta.title ? { detail } : {}),
23
25
  };
24
26
  }
25
- // Asks each step of the dispatch in turn: `resolve`, a request the validation hook rejected, a `JsonApiError` as
26
- // thrown, the HTTP status alone.
27
+ // Asks each step of the dispatch in turn: `resolve`, a guard's refusal, a request the validation hook rejected, a
28
+ // `JsonApiError` as thrown, the HTTP status alone.
27
29
  function claimError(err, c, options) {
28
30
  const resolved = options.resolve?.(err, c);
29
31
  if (resolved) {
30
32
  return { claimedBy: 'resolve', resolved };
31
33
  }
34
+ if (err instanceof Unauthenticated) {
35
+ const error = { status: '401', code: `${options.namespace}.unauthenticated`, title: 'Unauthenticated' };
36
+ return { claimedBy: 'auth', resolved: { status: 401, errors: [error] } };
37
+ }
38
+ if (err instanceof PermissionRefused) {
39
+ const error = { status: '403', code: `${options.namespace}.forbidden`, title: 'Forbidden' };
40
+ return { claimedBy: 'auth', resolved: { status: 403, errors: [error] }, logged: { permissions: err.permissions } };
41
+ }
32
42
  if (err instanceof RequestInvalid) {
33
43
  return { claimedBy: 'validation', resolved: { status: err.status, errors: err.errors } };
34
44
  }
@@ -40,8 +50,8 @@ function claimError(err, c, options) {
40
50
  }
41
51
  /**
42
52
  * The one central `onError` a transport installs, rendering every failure as one `application/vnd.api+json`
43
- * `errors[]` document. It asks, in order: `resolve`, then a request the validation hook rejected, then a
44
- * {@link JsonApiError} as thrown, then the HTTP status alone.
53
+ * `errors[]` document. It asks, in order: `resolve`, then a guard's own refusal, then a request the validation
54
+ * hook rejected, then a {@link JsonApiError} as thrown, then the HTTP status alone.
45
55
  *
46
56
  * Every answer is written to the log before it goes out, at the level its status class decides: a 5xx at error,
47
57
  * because this side went wrong, anything else at warn, because the request was refused. Without the line, a
@@ -52,8 +62,10 @@ export function createErrorHandler(options) {
52
62
  return (err, c) => {
53
63
  const requestId = options.requestId?.(c);
54
64
  const stamp = (e) => (requestId && !e.id ? { ...e, id: requestId } : e);
55
- const { claimedBy, resolved } = claimError(err, c, options);
65
+ const { claimedBy, resolved, logged } = claimError(err, c, options);
56
66
  const line = {
67
+ // First, so a step's own facts can never take the place of the ones every line carries.
68
+ ...logged,
57
69
  // Undefined where a transport never mounted `requestId()` - minting one here is a second id for one request.
58
70
  requestId: requestId ?? c.get('requestId'),
59
71
  method: c.req.method,
package/dist/index.d.ts CHANGED
@@ -3,7 +3,9 @@
3
3
  */
4
4
  export { createEnvironment, type EnvironmentOptions } from './environment.js';
5
5
  export type { SurfaceVocabulary } from './surface-vocabulary.js';
6
- export { jsonApiContentNegotiation } from './content-negotiation.js';
6
+ export { jsonApiContentNegotiation, NEGOTIATION_RESPONSES } from './content-negotiation.js';
7
7
  export { createErrorHandler, type ErrorHandlerOptions } from './error-handler.js';
8
+ export { PermissionRefused } from './permission-refused.js';
9
+ export { Unauthenticated } from './unauthenticated.js';
8
10
  export { createValidationHook } from './validation-hook.js';
9
11
  //# sourceMappingURL=index.d.ts.map
@@ -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,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAClF,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,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
  * Every Hono handler the house ships - the bindings gate, and the three the wire spelling decides.
3
3
  */
4
4
  export { createEnvironment } from './environment.js';
5
- export { jsonApiContentNegotiation } from './content-negotiation.js';
5
+ export { jsonApiContentNegotiation, NEGOTIATION_RESPONSES } from './content-negotiation.js';
6
6
  export { createErrorHandler } from './error-handler.js';
7
+ export { PermissionRefused } from './permission-refused.js';
8
+ export { Unauthenticated } from './unauthenticated.js';
7
9
  export { createValidationHook } from './validation-hook.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A caller who is authenticated and may not perform what the route requires, thrown by the authorization
3
+ * guard so the app's one `onError` renders and logs it.
4
+ *
5
+ * `permissions` is what the route asked for, never who asked: the log needs it to say which guard refused,
6
+ * and the answer withholds it, because a 403 naming the resource draws the permission map for the caller
7
+ * who just failed it.
8
+ */
9
+ export declare class PermissionRefused extends Error {
10
+ readonly permissions: Readonly<Record<string, readonly string[]>>;
11
+ constructor(permissions: Readonly<Record<string, readonly string[]>>);
12
+ }
13
+ //# sourceMappingURL=permission-refused.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-refused.d.ts","sourceRoot":"","sources":["../src/permission-refused.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC9B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;gBAAxD,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;CAI9E"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A caller who is authenticated and may not perform what the route requires, thrown by the authorization
3
+ * guard so the app's one `onError` renders and logs it.
4
+ *
5
+ * `permissions` is what the route asked for, never who asked: the log needs it to say which guard refused,
6
+ * and the answer withholds it, because a 403 naming the resource draws the permission map for the caller
7
+ * who just failed it.
8
+ */
9
+ export class PermissionRefused extends Error {
10
+ permissions;
11
+ constructor(permissions) {
12
+ super('The caller may not perform every action this route requires');
13
+ this.permissions = permissions;
14
+ this.name = 'PermissionRefused';
15
+ }
16
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A request that resolved no caller, thrown by whatever guard establishes identity so the app's one
3
+ * `onError` renders and logs it.
4
+ *
5
+ * A guard that answers the request itself writes a second spelling of 401 per surface, and the two drift:
6
+ * one says `unauthenticated`, the next says `unauthorized`, for the same refusal.
7
+ */
8
+ export declare class Unauthenticated extends Error {
9
+ constructor(message?: string);
10
+ }
11
+ //# sourceMappingURL=unauthenticated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unauthenticated.d.ts","sourceRoot":"","sources":["../src/unauthenticated.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,qBAAa,eAAgB,SAAQ,KAAK;gBAC5B,OAAO,SAAgD;CAIpE"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A request that resolved no caller, thrown by whatever guard establishes identity so the app's one
3
+ * `onError` renders and logs it.
4
+ *
5
+ * A guard that answers the request itself writes a second spelling of 401 per surface, and the two drift:
6
+ * one says `unauthenticated`, the next says `unauthorized`, for the same refusal.
7
+ */
8
+ export class Unauthenticated extends Error {
9
+ constructor(message = 'The request carries no authenticated caller') {
10
+ super(message);
11
+ this.name = 'Unauthenticated';
12
+ }
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeroxsolutions/server",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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/response": "0.12.1",
25
- "@zeroxsolutions/jsonapi": "0.11.1"
24
+ "@zeroxsolutions/jsonapi": "0.11.2",
25
+ "@zeroxsolutions/response": "0.12.2"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@hono/zod-openapi": "^1.0.0",