@gusnips/server 0.1.0 → 0.1.1

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
@@ -181,7 +181,9 @@ contract, and a "too big" without it costs somebody a bisect to rediscover a num
181
181
  already state.
182
182
 
183
183
  Validation is recognized by shape, not by an import, so your validator does not become this
184
- package's dependency. Measured against zod 3.25, 4.4 and 4.5.
184
+ package's dependency. Measured against zod 3.25, 4.4 and 4.5. A tool or queue consumer can use
185
+ `validationIssues(error)` from the root package to apply the same allow-list without building an
186
+ HTTP answer.
185
187
 
186
188
  **A 5xx keeps its message unless its code is masked.** By default only `INTERNAL_ERROR` is,
187
189
  because that is the code you raise when something unexpected broke, so its message may carry
@@ -264,6 +266,14 @@ hypothetical. The boundary wraps one in an `Error` and keeps the original as `ca
264
266
  document number in a log and in whatever reads that log afterwards. `/health` is skipped with
265
267
  everything under it, because every deploy polls it in a loop; a throw is logged anyway.
266
268
 
269
+ Add safe product metadata with `requestLogger<AppEnv>({ logger, fields: (c) => ({ … }) })`.
270
+ `fields` runs after the response exists, so it can read values a handler set and `c.res`; return only
271
+ bounded, sanitized values, never a raw path, query, header set, body or authentication object. Your
272
+ fields are written first, so they cannot replace the canonical request id, method, route, status,
273
+ duration or error code. A hook that throws is caught: the line is written with
274
+ `requestFieldsFailed: true` instead of your fields. A value whose own `toJSON` throws is not — that
275
+ one loses the whole line, request id included — so return plain data, not live objects.
276
+
267
277
  The request id goes back on `X-Request-ID`, on every answer including `onError`'s and
268
278
  `notFound`'s. A caller's own id is echoed only if it is 64 characters of `A-Z a-z 0-9 . _ -`,
269
279
  so the id in a line is always either the caller's or ours. Cross-origin, list that header in your
@@ -1,4 +1,4 @@
1
- import type { MiddlewareHandler } from "hono";
1
+ import type { Context, MiddlewareHandler } from "hono";
2
2
  import type { Logger } from "../logger/index.ts";
3
3
  /** The two variables this adapter writes. Put them in your app's `Variables`. */
4
4
  export interface RequestVariables<Code extends string = string> {
@@ -6,8 +6,21 @@ export interface RequestVariables<Code extends string = string> {
6
6
  /** The code of the refusal this request got, for the request line. `null` until one happens. */
7
7
  errorCode: Code | null;
8
8
  }
9
- export interface RequestLoggerOptions {
9
+ type RequestLoggerEnv = {
10
+ Variables: RequestVariables;
11
+ };
12
+ export interface RequestLoggerOptions<E extends RequestLoggerEnv = RequestLoggerEnv> {
10
13
  logger: Logger;
14
+ /**
15
+ * Sanitized product fields to add to the request line. Runs after the response exists, so it can
16
+ * read downstream variables and `c.res`. Keep caller-controlled values bounded; never return a
17
+ * raw path, query, header set, body or authentication object.
18
+ *
19
+ * Return plain data, not live objects: a throw from this hook is caught, but a value whose own
20
+ * `toJSON` throws is caught by the logger instead, which costs the whole line rather than the
21
+ * field.
22
+ */
23
+ fields?: (c: Context<E>) => Record<string, unknown>;
11
24
  /**
12
25
  * Paths answered but never logged, each with everything under it: `/health` covers
13
26
  * `/health/db` and not `/healthz`. Defaults to `["/health"]`. A throw is logged anyway.
@@ -25,7 +38,6 @@ export interface RequestLoggerOptions {
25
38
  * answer including `onError`'s and `notFound`'s. Cross-origin, list that header in your CORS
26
39
  * `exposeHeaders` or the browser hides it from the page.
27
40
  */
28
- export declare function requestLogger({ logger, skipPaths, }: RequestLoggerOptions): MiddlewareHandler<{
29
- Variables: RequestVariables;
30
- }>;
41
+ export declare function requestLogger<E extends RequestLoggerEnv = RequestLoggerEnv>({ logger, fields, skipPaths, }: RequestLoggerOptions<E>): MiddlewareHandler<E>;
42
+ export {};
31
43
  //# sourceMappingURL=request-logger.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-logger.d.ts","sourceRoot":"","sources":["../../src/hono/request-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAE9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,iFAAiF;AACjF,MAAM,WAAW,gBAAgB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,gGAAgG;IAChG,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AASD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,SAAuB,GACxB,EAAE,oBAAoB,GAAG,iBAAiB,CAAC;IAAE,SAAS,EAAE,gBAAgB,CAAA;CAAE,CAAC,CAmC3E"}
1
+ {"version":3,"file":"request-logger.d.ts","sourceRoot":"","sources":["../../src/hono/request-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,iFAAiF;AACjF,MAAM,WAAW,gBAAgB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,gGAAgG;IAChG,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;CACxB;AAED,KAAK,gBAAgB,GAAG;IAAE,SAAS,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAExD,MAAM,WAAW,oBAAoB,CAAC,CAAC,SAAS,gBAAgB,GAAG,gBAAgB;IACjF,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpD;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC/B;AAuBD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,gBAAgB,GAAG,gBAAgB,EAAE,EAC3E,MAAM,EACN,MAAM,EACN,SAAuB,GACxB,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAoChD"}
@@ -5,6 +5,18 @@ import { routePath } from "hono/route";
5
5
  * trimmed, so the id in the log is always either the caller's or ours.
6
6
  */
7
7
  const REQUEST_ID = /^[A-Za-z0-9._-]{1,64}$/;
8
+ function collectFields(fields, c) {
9
+ if (fields === undefined)
10
+ return {};
11
+ try {
12
+ // Materialize here too: a throwing getter is just as capable of losing the request line as a
13
+ // throwing callback. Logging metadata must never change the response it describes.
14
+ return { ...fields(c) };
15
+ }
16
+ catch {
17
+ return { requestFieldsFailed: true };
18
+ }
19
+ }
8
20
  /**
9
21
  * One line per request, and the request id.
10
22
  *
@@ -16,7 +28,7 @@ const REQUEST_ID = /^[A-Za-z0-9._-]{1,64}$/;
16
28
  * answer including `onError`'s and `notFound`'s. Cross-origin, list that header in your CORS
17
29
  * `exposeHeaders` or the browser hides it from the page.
18
30
  */
19
- export function requestLogger({ logger, skipPaths = ["/health"], }) {
31
+ export function requestLogger({ logger, fields, skipPaths = ["/health"], }) {
20
32
  return async (c, next) => {
21
33
  const supplied = c.req.header("X-Request-ID");
22
34
  const requestId = supplied && REQUEST_ID.test(supplied) ? supplied : crypto.randomUUID();
@@ -51,7 +63,7 @@ export function requestLogger({ logger, skipPaths = ["/health"], }) {
51
63
  c.header("X-Request-ID", requestId);
52
64
  const skipped = skipPaths.some((skip) => path === skip || path.startsWith(`${skip}/`));
53
65
  if (method !== "OPTIONS" && !skipped)
54
- logger.info("request", line(c.res.status));
66
+ logger.info("request", { ...collectFields(fields, c), ...line(c.res.status) });
55
67
  };
56
68
  }
57
69
  //# sourceMappingURL=request-logger.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-logger.js","sourceRoot":"","sources":["../../src/hono/request-logger.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAmBvC;;;;GAIG;AACH,MAAM,UAAU,GAAG,wBAAwB,CAAC;AAE5C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,EAC5B,MAAM,EACN,SAAS,GAAG,CAAC,SAAS,CAAC,GACF;IACrB,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,QAAQ,IAAI,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;QACzF,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;QAC9B,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QACzB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,+FAA+F;QAC/F,0FAA0F;QAC1F,4FAA4F;QAC5F,uDAAuD;QACvD,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,CAAC;YAChC,SAAS;YACT,MAAM;YACN,KAAK,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACvB,MAAM;YACN,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;YACtB,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,SAAS;SAC3C,CAAC,CAAC;QACH,IAAI,CAAC;YACH,MAAM,IAAI,EAAE,CAAC;QACf,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,4FAA4F;YAC5F,0FAA0F;YAC1F,oFAAoF;YACpF,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YACtD,MAAM,GAAG,CAAC;QACZ,CAAC;QACD,8FAA8F;QAC9F,sCAAsC;QACtC,CAAC,CAAC,MAAM,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;QACpC,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC;QACvF,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,OAAO;YAAE,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACnF,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"request-logger.js","sourceRoot":"","sources":["../../src/hono/request-logger.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AA+BvC;;;;GAIG;AACH,MAAM,UAAU,GAAG,wBAAwB,CAAC;AAE5C,SAAS,aAAa,CACpB,MAAyC,EACzC,CAAa;IAEb,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACH,6FAA6F;QAC7F,mFAAmF;QACnF,OAAO,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAAC;IACvC,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAgD,EAC3E,MAAM,EACN,MAAM,EACN,SAAS,GAAG,CAAC,SAAS,CAAC,GACC;IACxB,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACvB,MAAM,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,QAAQ,IAAI,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;QACzF,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;QAC9B,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QACzB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,+FAA+F;QAC/F,0FAA0F;QAC1F,4FAA4F;QAC5F,uDAAuD;QACvD,MAAM,IAAI,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,CAAC;YAChC,SAAS;YACT,MAAM;YACN,KAAK,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACvB,MAAM;YACN,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;YACtB,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,SAAS;SAC3C,CAAC,CAAC;QACH,IAAI,CAAC;YACH,MAAM,IAAI,EAAE,CAAC;QACf,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,4FAA4F;YAC5F,0FAA0F;YAC1F,oFAAoF;YACpF,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YACtD,MAAM,GAAG,CAAC;QACZ,CAAC;QACD,8FAA8F;QAC9F,sCAAsC;QACtC,CAAC,CAAC,MAAM,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;QACpC,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC;QACvF,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,OAAO;YAClC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACnF,CAAC,CAAC;AACJ,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { AppError, createAppError, toMessage } from "./errors.ts";
2
2
  export type { AppErrorOptions } from "./errors.ts";
3
- export { created, createErrorResponse, noContent, ok, paginated } from "./responses.ts";
4
- export type { CannedError, ErrorAnswer, ErrorResponseOptions } from "./responses.ts";
3
+ export { created, createErrorResponse, noContent, ok, paginated, validationIssues, } from "./responses.ts";
4
+ export type { CannedError, ErrorAnswer, ErrorResponseOptions, ValidationIssue, } from "./responses.ts";
5
5
  export { createLogger, errorReplacer, keptErrorFields } from "./logger/index.ts";
6
6
  export type { Logger, LoggerOptions, LogLevel, LogThreshold } from "./logger/index.ts";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACxF,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACrF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACjF,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EACL,OAAO,EACP,mBAAmB,EACnB,SAAS,EACT,EAAE,EACF,SAAS,EACT,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,WAAW,EACX,WAAW,EACX,oBAAoB,EACpB,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACjF,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export { AppError, createAppError, toMessage } from "./errors.js";
2
- export { created, createErrorResponse, noContent, ok, paginated } from "./responses.js";
2
+ export { created, createErrorResponse, noContent, ok, paginated, validationIssues, } from "./responses.js";
3
3
  export { createLogger, errorReplacer, keptErrorFields } from "./logger/index.js";
4
4
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAElE,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAExF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAElE,OAAO,EACL,OAAO,EACP,mBAAmB,EACnB,SAAS,EACT,EAAE,EACF,SAAS,EACT,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAOxB,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
@@ -17,8 +17,10 @@
17
17
  * a PostgREST client rejects with plain objects. So in a Hono app the cause slot is precisely where
18
18
  * a vendor's rejection object ends up, and a leak there reads as if the list had run.
19
19
  *
20
- * `cause` means "the error this one came from", so whatever sits in it is in the error slot and
21
- * gets the same treatment. `name` and `message` come along because a rejection object usually
20
+ * A plain object passed directly as `meta.error` is the other door. Hono wraps it, but a worker,
21
+ * a fire-and-forget catch or a database client outside Hono does not. `error` is the raw-error slot
22
+ * the logger documents, so it gets the same treatment as `cause`; an ordinary metadata object under
23
+ * any other key stays untouched. `name` and `message` come along because a rejection object usually
22
24
  * carries them and a line with neither says nothing at all.
23
25
  *
24
26
  * Deliberate state it does NOT keep: context an app attaches on purpose. That belongs in the
@@ -33,6 +35,9 @@ export declare function narrowErrorLike(value: object): Record<string, unknown>;
33
35
  * `JSON.stringify(err)` is `{}` — which is how a logger ends up printing nothing about the
34
36
  * failure it was called to report. They are added explicitly, and the allow-listed extras ride
35
37
  * along beside them.
38
+ * - A plain object in the root `error` slot is narrowed through the same allow-list. Hono's
39
+ * boundary turns one into an Error cause, but workers and swallowed catches log it directly.
40
+ * Other metadata objects stay untouched.
36
41
  * - A nested `cause` is followed, and so is an `AggregateError`'s `errors`. Both are
37
42
  * non-enumerable, so both are invisible to the loop above; without this line "all attempts
38
43
  * failed" is the whole log entry. Each one goes back through this replacer, so the allow-list
@@ -1 +1 @@
1
- {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/logger/serialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA2EH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEtE;AA+BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,aAAa,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CA4BvF;AAED,qGAAqG;AACrG,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAAqB,CAAC"}
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/logger/serialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA2EH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEtE;AA+BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,aAAa,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAiCvF;AAED,qGAAqG;AACrG,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAAqB,CAAC"}
@@ -87,8 +87,10 @@ function keptValue(key, value) {
87
87
  * a PostgREST client rejects with plain objects. So in a Hono app the cause slot is precisely where
88
88
  * a vendor's rejection object ends up, and a leak there reads as if the list had run.
89
89
  *
90
- * `cause` means "the error this one came from", so whatever sits in it is in the error slot and
91
- * gets the same treatment. `name` and `message` come along because a rejection object usually
90
+ * A plain object passed directly as `meta.error` is the other door. Hono wraps it, but a worker,
91
+ * a fire-and-forget catch or a database client outside Hono does not. `error` is the raw-error slot
92
+ * the logger documents, so it gets the same treatment as `cause`; an ordinary metadata object under
93
+ * any other key stays untouched. `name` and `message` come along because a rejection object usually
92
94
  * carries them and a line with neither says nothing at all.
93
95
  *
94
96
  * Deliberate state it does NOT keep: context an app attaches on purpose. That belongs in the
@@ -133,6 +135,9 @@ function narrowCause(cause, seen) {
133
135
  * `JSON.stringify(err)` is `{}` — which is how a logger ends up printing nothing about the
134
136
  * failure it was called to report. They are added explicitly, and the allow-listed extras ride
135
137
  * along beside them.
138
+ * - A plain object in the root `error` slot is narrowed through the same allow-list. Hono's
139
+ * boundary turns one into an Error cause, but workers and swallowed catches log it directly.
140
+ * Other metadata objects stay untouched.
136
141
  * - A nested `cause` is followed, and so is an `AggregateError`'s `errors`. Both are
137
142
  * non-enumerable, so both are invisible to the loop above; without this line "all attempts
138
143
  * failed" is the whole log entry. Each one goes back through this replacer, so the allow-list
@@ -164,12 +169,18 @@ function narrowCause(cause, seen) {
164
169
  */
165
170
  export function errorReplacer() {
166
171
  const seen = new WeakSet();
172
+ let root;
167
173
  return function (key, value) {
168
174
  const held = typeof this === "object" && this !== null
169
175
  ? this[key]
170
176
  : undefined;
177
+ if (key === "" && typeof held === "object" && held !== null)
178
+ root = held;
171
179
  if (held instanceof Error)
172
180
  value = held;
181
+ else if (this === root && key === "error" && typeof held === "object" && held !== null) {
182
+ return seen.has(held) ? "[Circular]" : narrow(held, seen);
183
+ }
173
184
  if (typeof value === "bigint")
174
185
  return value.toString();
175
186
  if (value instanceof Error) {
@@ -1 +1 @@
1
- {"version":3,"file":"serialize.js","sourceRoot":"","sources":["../../src/logger/serialize.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"serialize.js","sourceRoot":"","sources":["../../src/logger/serialize.ts"],"names":[],"mappings":"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"}
@@ -88,6 +88,49 @@ export interface ErrorResponseOptions<Code extends string, Key extends string> {
88
88
  */
89
89
  maskDetails?: boolean;
90
90
  }
91
+ /**
92
+ * What an issue MIGHT carry — every field optional, because the gate below proves only that
93
+ * `issues` is an array and nothing at all about an element. Typing the element as certain is
94
+ * what turned this projection into a throw: `path.map` on an issue that arrived without one.
95
+ */
96
+ interface RawIssue {
97
+ readonly path?: unknown;
98
+ readonly code?: unknown;
99
+ readonly maximum?: unknown;
100
+ readonly minimum?: unknown;
101
+ }
102
+ /** One rejected field: enough to fix the call, and nothing about the schema. */
103
+ export interface ValidationIssue {
104
+ /** The field that failed, as the caller spelled it. */
105
+ path: (string | number)[];
106
+ /** The rule that rejected it, such as `too_big`. */
107
+ code: string;
108
+ /** The numeric bound, when the rule has one. */
109
+ maximum?: number;
110
+ minimum?: number;
111
+ }
112
+ /**
113
+ * The field path, the rule it failed, and — for a range — the BOUND it failed against.
114
+ *
115
+ * Never the rejected value, and never the schema's internals. All six donors carry a version of
116
+ * that comment; what none of them carries is the proof, so here it is: handing the validator's
117
+ * issues straight to the client ships back the caller's own key names (`keys`), the enum's
118
+ * allowed values (`values`), the validator's English sentence and the expected type
119
+ * (`origin`) — four disclosures from one convenience, and two repos in the fleet do it today.
120
+ * An audit note written against an older validator looks for `received`, which the current one
121
+ * no longer emits; the projection is an allow-list precisely so a rename cannot reopen this.
122
+ *
123
+ * The bound is the exception, and it belongs to the caller: it is the published contract, and
124
+ * a `too_big` without it costs somebody a bisect to rediscover a number our own docs state.
125
+ *
126
+ * Total on purpose: this runs inside the function that turns a thrown thing into an answer, and
127
+ * it is advertised to the queue and tool doors, where an issue list has crossed a serialization
128
+ * hop. An allow-list that throws on a malformed issue sends its caller back to shipping the
129
+ * validator's issues raw, which is the disclosure it exists to prevent.
130
+ */
131
+ export declare function validationIssues(error: {
132
+ readonly issues: readonly RawIssue[];
133
+ }): ValidationIssue[];
91
134
  /**
92
135
  * Binds the mask policy and the two canned bodies, and returns the function that answers.
93
136
  *
@@ -104,4 +147,5 @@ export interface ErrorResponseOptions<Code extends string, Key extends string> {
104
147
  * ```
105
148
  */
106
149
  export declare function createErrorResponse<Code extends string = string, Key extends string = string>(opts: ErrorResponseOptions<Code, Key>): (err: unknown) => ErrorAnswer<Code>;
150
+ export {};
107
151
  //# sourceMappingURL=responses.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../src/responses.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG1E,4FAA4F;AAC5F,wBAAgB,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC;;;EAG1D;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;;;EAGjC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC;;;EAM5E;AAED,wBAAgB,SAAS;;;EAExB;AAED,MAAM,WAAW,WAAW,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrB,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC;;;;;OAKG;IACH,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,YAAY,CAAC;CAC1C;AAED,0FAA0F;AAC1F,MAAM,WAAW,WAAW,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM;IAClE,IAAI,EAAE,IAAI,CAAC;IACX,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM;IAC3E,sDAAsD;IACtD,QAAQ,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACjC,oCAAoC;IACpC,UAAU,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACnC;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;IAC9B,6FAA6F;IAC7F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA+GD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,MAAM,GAAG,MAAM,EAC3F,IAAI,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,IAIP,KAAK,OAAO,KAAG,WAAW,CAAC,IAAI,CAAC,CA8C/D"}
1
+ {"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../src/responses.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG1E,4FAA4F;AAC5F,wBAAgB,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC;;;EAG1D;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;;;EAGjC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC;;;EAM5E;AAED,wBAAgB,SAAS;;;EAExB;AAED,MAAM,WAAW,WAAW,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrB,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC;;;;;OAKG;IACH,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,YAAY,CAAC;CAC1C;AAED,0FAA0F;AAC1F,MAAM,WAAW,WAAW,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM;IAClE,IAAI,EAAE,IAAI,CAAC;IACX,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM;IAC3E,sDAAsD;IACtD,QAAQ,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACjC,oCAAoC;IACpC,UAAU,EAAE,WAAW,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACnC;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,SAAS,IAAI,EAAE,CAAC;IAC9B,6FAA6F;IAC7F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAuDD;;;;GAIG;AACH,UAAU,QAAQ;IAChB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,uDAAuD;IACvD,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC1B,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA6BD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAC;CACtC,GAAG,eAAe,EAAE,CAUpB;AAaD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,MAAM,GAAG,MAAM,EAC3F,IAAI,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,IAIP,KAAK,OAAO,KAAG,WAAW,CAAC,IAAI,CAAC,CA8C/D"}
package/dist/responses.js CHANGED
@@ -87,6 +87,18 @@ function zodIssues(err) {
87
87
  return null;
88
88
  return issues;
89
89
  }
90
+ /**
91
+ * One path segment, as something that survives `JSON.stringify`.
92
+ *
93
+ * A symbol keyed a field the caller cannot name back at us, so its description is the only
94
+ * useful thing in it — and an unnamed symbol has none, which is an empty segment rather than
95
+ * the `null` that `JSON.stringify` would otherwise write.
96
+ */
97
+ function pathSegment(segment) {
98
+ if (typeof segment === "symbol")
99
+ return segment.description ?? "";
100
+ return typeof segment === "number" ? segment : String(segment);
101
+ }
90
102
  /**
91
103
  * The field path, the rule it failed, and — for a range — the BOUND it failed against.
92
104
  *
@@ -100,14 +112,22 @@ function zodIssues(err) {
100
112
  *
101
113
  * The bound is the exception, and it belongs to the caller: it is the published contract, and
102
114
  * a `too_big` without it costs somebody a bisect to rediscover a number our own docs state.
115
+ *
116
+ * Total on purpose: this runs inside the function that turns a thrown thing into an answer, and
117
+ * it is advertised to the queue and tool doors, where an issue list has crossed a serialization
118
+ * hop. An allow-list that throws on a malformed issue sends its caller back to shipping the
119
+ * validator's issues raw, which is the disclosure it exists to prevent.
103
120
  */
104
- function safeIssues(issues) {
105
- return issues.map((issue) => ({
106
- path: issue.path,
107
- code: issue.code,
108
- ...(typeof issue.maximum === "number" && { maximum: issue.maximum }),
109
- ...(typeof issue.minimum === "number" && { minimum: issue.minimum }),
110
- }));
121
+ export function validationIssues(error) {
122
+ return error.issues.map((issue) => {
123
+ const { path, code, maximum, minimum } = issue ?? {};
124
+ return {
125
+ path: Array.isArray(path) ? path.map(pathSegment) : [],
126
+ code: typeof code === "string" ? code : "",
127
+ ...(typeof maximum === "number" && { maximum }),
128
+ ...(typeof minimum === "number" && { minimum }),
129
+ };
130
+ });
111
131
  }
112
132
  /**
113
133
  * `AppError` is generic over the product's own code union, and no runtime check can verify
@@ -140,7 +160,7 @@ export function createErrorResponse(opts) {
140
160
  if (issues !== null) {
141
161
  return {
142
162
  status: 400,
143
- body: envelope(opts.validation, safeIssues(issues)),
163
+ body: envelope(opts.validation, validationIssues({ issues })),
144
164
  headers: {},
145
165
  kind: "client",
146
166
  };
@@ -1 +1 @@
1
- {"version":3,"file":"responses.js","sourceRoot":"","sources":["../src/responses.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"responses.js","sourceRoot":"","sources":["../src/responses.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gusnips/server",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "The layer under a TypeScript backend: one error shape, one response envelope, one retry rule, one logger.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -7,20 +7,92 @@ import {
7
7
  type RequestVariables,
8
8
  } from "./request-logger.ts";
9
9
 
10
- function setup(options: Omit<RequestLoggerOptions, "logger"> = {}) {
10
+ function setup<E extends { Variables: RequestVariables } = { Variables: RequestVariables }>(
11
+ options: Omit<RequestLoggerOptions<E>, "logger"> = {},
12
+ ) {
11
13
  const lines: Array<Record<string, unknown>> = [];
12
14
  const logger = createLogger({
13
15
  level: "debug",
14
16
  write: (line) => lines.push(JSON.parse(line) as Record<string, unknown>),
15
17
  });
16
- const app = new Hono<{ Variables: RequestVariables }>();
17
- app.use(requestLogger({ logger, ...options }));
18
+ const app = new Hono<E>();
19
+ app.use(requestLogger<E>({ logger, ...options }));
18
20
  return { app, lines };
19
21
  }
20
22
 
21
23
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
22
24
 
25
+ type AppEnv = {
26
+ Variables: RequestVariables<"DENIED"> & { actorId: string };
27
+ };
28
+
23
29
  describe("the request line", () => {
30
+ it("adds typed adopter fields after the response exists", async () => {
31
+ const { app, lines } = setup<AppEnv>({
32
+ fields: (c) => ({ actorId: c.get("actorId"), responseStatus: c.res.status }),
33
+ });
34
+ app.post("/items", (c) => {
35
+ c.set("actorId", "user_1");
36
+ return c.text("created", 201);
37
+ });
38
+
39
+ await app.request("/items", { method: "POST" });
40
+
41
+ expect(lines[0]).toMatchObject({ actorId: "user_1", responseStatus: 201 });
42
+ });
43
+
44
+ it("keeps every canonical request field when adopter fields use the same names", async () => {
45
+ const { app, lines } = setup<AppEnv>({
46
+ fields: () => ({
47
+ actorId: "user_1",
48
+ requestId: "wrong",
49
+ method: "DELETE",
50
+ route: "/wrong",
51
+ status: 599,
52
+ ms: -1,
53
+ errorCode: "WRONG",
54
+ }),
55
+ });
56
+ app.post("/items/:id", (c) => {
57
+ c.set("errorCode", "DENIED");
58
+ return c.text("no", 403);
59
+ });
60
+
61
+ await app.request("/items/secret", {
62
+ method: "POST",
63
+ headers: { "X-Request-ID": "trace_1" },
64
+ });
65
+
66
+ expect(lines[0]).toMatchObject({
67
+ actorId: "user_1",
68
+ requestId: "trace_1",
69
+ method: "POST",
70
+ route: "/items/:id",
71
+ status: 403,
72
+ errorCode: "DENIED",
73
+ });
74
+ expect(lines[0]!.ms).not.toBe(-1);
75
+ });
76
+
77
+ it("does not let a broken adopter field hook lose the response or its request line", async () => {
78
+ const { app, lines } = setup<AppEnv>({
79
+ fields: () => {
80
+ throw new Error("field hook broke");
81
+ },
82
+ });
83
+ app.get("/items", (c) => c.text("ok"));
84
+
85
+ const response = await app.request("/items");
86
+
87
+ expect(response.status).toBe(200);
88
+ expect(lines[0]).toMatchObject({
89
+ requestFieldsFailed: true,
90
+ method: "GET",
91
+ route: "/items",
92
+ status: 200,
93
+ });
94
+ });
95
+
24
96
  it("names the route template, never the path", async () => {
25
97
  // A path carries whatever the caller put in it. In one backend that was a customer's national
26
98
  // id number, and the request line carried it into the log and on into an analytics event.
@@ -1,4 +1,4 @@
1
- import type { MiddlewareHandler } from "hono";
1
+ import type { Context, MiddlewareHandler } from "hono";
2
2
  import { routePath } from "hono/route";
3
3
  import type { Logger } from "../logger/index.ts";
4
4
 
@@ -9,8 +9,20 @@ export interface RequestVariables<Code extends string = string> {
9
9
  errorCode: Code | null;
10
10
  }
11
11
 
12
- export interface RequestLoggerOptions {
12
+ type RequestLoggerEnv = { Variables: RequestVariables };
13
+
14
+ export interface RequestLoggerOptions<E extends RequestLoggerEnv = RequestLoggerEnv> {
13
15
  logger: Logger;
16
+ /**
17
+ * Sanitized product fields to add to the request line. Runs after the response exists, so it can
18
+ * read downstream variables and `c.res`. Keep caller-controlled values bounded; never return a
19
+ * raw path, query, header set, body or authentication object.
20
+ *
21
+ * Return plain data, not live objects: a throw from this hook is caught, but a value whose own
22
+ * `toJSON` throws is caught by the logger instead, which costs the whole line rather than the
23
+ * field.
24
+ */
25
+ fields?: (c: Context<E>) => Record<string, unknown>;
14
26
  /**
15
27
  * Paths answered but never logged, each with everything under it: `/health` covers
16
28
  * `/health/db` and not `/healthz`. Defaults to `["/health"]`. A throw is logged anyway.
@@ -25,6 +37,20 @@ export interface RequestLoggerOptions {
25
37
  */
26
38
  const REQUEST_ID = /^[A-Za-z0-9._-]{1,64}$/;
27
39
 
40
+ function collectFields<E extends RequestLoggerEnv>(
41
+ fields: RequestLoggerOptions<E>["fields"],
42
+ c: Context<E>,
43
+ ): Record<string, unknown> {
44
+ if (fields === undefined) return {};
45
+ try {
46
+ // Materialize here too: a throwing getter is just as capable of losing the request line as a
47
+ // throwing callback. Logging metadata must never change the response it describes.
48
+ return { ...fields(c) };
49
+ } catch {
50
+ return { requestFieldsFailed: true };
51
+ }
52
+ }
53
+
28
54
  /**
29
55
  * One line per request, and the request id.
30
56
  *
@@ -36,10 +62,11 @@ const REQUEST_ID = /^[A-Za-z0-9._-]{1,64}$/;
36
62
  * answer including `onError`'s and `notFound`'s. Cross-origin, list that header in your CORS
37
63
  * `exposeHeaders` or the browser hides it from the page.
38
64
  */
39
- export function requestLogger({
65
+ export function requestLogger<E extends RequestLoggerEnv = RequestLoggerEnv>({
40
66
  logger,
67
+ fields,
41
68
  skipPaths = ["/health"],
42
- }: RequestLoggerOptions): MiddlewareHandler<{ Variables: RequestVariables }> {
69
+ }: RequestLoggerOptions<E>): MiddlewareHandler<E> {
43
70
  return async (c, next) => {
44
71
  const supplied = c.req.header("X-Request-ID");
45
72
  const requestId = supplied && REQUEST_ID.test(supplied) ? supplied : crypto.randomUUID();
@@ -72,6 +99,7 @@ export function requestLogger({
72
99
  // returns a Response it built itself.
73
100
  c.header("X-Request-ID", requestId);
74
101
  const skipped = skipPaths.some((skip) => path === skip || path.startsWith(`${skip}/`));
75
- if (method !== "OPTIONS" && !skipped) logger.info("request", line(c.res.status));
102
+ if (method !== "OPTIONS" && !skipped)
103
+ logger.info("request", { ...collectFields(fields, c), ...line(c.res.status) });
76
104
  };
77
105
  }
package/src/index.ts CHANGED
@@ -1,6 +1,18 @@
1
1
  export { AppError, createAppError, toMessage } from "./errors.ts";
2
2
  export type { AppErrorOptions } from "./errors.ts";
3
- export { created, createErrorResponse, noContent, ok, paginated } from "./responses.ts";
4
- export type { CannedError, ErrorAnswer, ErrorResponseOptions } from "./responses.ts";
3
+ export {
4
+ created,
5
+ createErrorResponse,
6
+ noContent,
7
+ ok,
8
+ paginated,
9
+ validationIssues,
10
+ } from "./responses.ts";
11
+ export type {
12
+ CannedError,
13
+ ErrorAnswer,
14
+ ErrorResponseOptions,
15
+ ValidationIssue,
16
+ } from "./responses.ts";
5
17
  export { createLogger, errorReplacer, keptErrorFields } from "./logger/index.ts";
6
18
  export type { Logger, LoggerOptions, LogLevel, LogThreshold } from "./logger/index.ts";
@@ -221,6 +221,28 @@ describe("what an Error contributes to a log line", () => {
221
221
  expect(JSON.stringify(line)).not.toContain("4242");
222
222
  });
223
223
 
224
+ it("allow-lists a thrown plain object passed directly as the error", () => {
225
+ // Hono wraps a non-Error throw as a cause, but workers, fire-and-forget catches and a database
226
+ // client's `{ code, message, details }` rejection reach the logger directly. `error` is the
227
+ // raw-error slot the logger documents; an ordinary metadata object under another key stays
228
+ // ordinary metadata.
229
+ const rejection = {
230
+ message: "insert failed",
231
+ code: "23514",
232
+ detail: "Failing row contains (someone@example.com, 4242424242424242).",
233
+ payload: '{"customer_email":"someone@example.com"}',
234
+ };
235
+
236
+ const line = serialized(rejection);
237
+
238
+ expect(line).toEqual({
239
+ message: "insert failed",
240
+ code: "23514",
241
+ detail: "[row omitted: Postgres DETAIL for this error is the whole failing row]",
242
+ });
243
+ expect(entryFor({ context: rejection }).context).toEqual(rejection);
244
+ });
245
+
224
246
  it("survives a chain of plain-object causes that holds itself", () => {
225
247
  // The Error branch has had this guard since it was written; the narrowing builds a new object
226
248
  // and so needs its own, or a self-referencing rejection recurses until the stack ends — inside
@@ -91,8 +91,10 @@ function keptValue(key: string, value: unknown): unknown {
91
91
  * a PostgREST client rejects with plain objects. So in a Hono app the cause slot is precisely where
92
92
  * a vendor's rejection object ends up, and a leak there reads as if the list had run.
93
93
  *
94
- * `cause` means "the error this one came from", so whatever sits in it is in the error slot and
95
- * gets the same treatment. `name` and `message` come along because a rejection object usually
94
+ * A plain object passed directly as `meta.error` is the other door. Hono wraps it, but a worker,
95
+ * a fire-and-forget catch or a database client outside Hono does not. `error` is the raw-error slot
96
+ * the logger documents, so it gets the same treatment as `cause`; an ordinary metadata object under
97
+ * any other key stays untouched. `name` and `message` come along because a rejection object usually
96
98
  * carries them and a line with neither says nothing at all.
97
99
  *
98
100
  * Deliberate state it does NOT keep: context an app attaches on purpose. That belongs in the
@@ -139,6 +141,9 @@ function narrowCause(cause: unknown, seen: WeakSet<object>): unknown {
139
141
  * `JSON.stringify(err)` is `{}` — which is how a logger ends up printing nothing about the
140
142
  * failure it was called to report. They are added explicitly, and the allow-listed extras ride
141
143
  * along beside them.
144
+ * - A plain object in the root `error` slot is narrowed through the same allow-list. Hono's
145
+ * boundary turns one into an Error cause, but workers and swallowed catches log it directly.
146
+ * Other metadata objects stay untouched.
142
147
  * - A nested `cause` is followed, and so is an `AggregateError`'s `errors`. Both are
143
148
  * non-enumerable, so both are invisible to the loop above; without this line "all attempts
144
149
  * failed" is the whole log entry. Each one goes back through this replacer, so the allow-list
@@ -170,12 +175,17 @@ function narrowCause(cause: unknown, seen: WeakSet<object>): unknown {
170
175
  */
171
176
  export function errorReplacer(): (this: unknown, key: string, value: unknown) => unknown {
172
177
  const seen = new WeakSet<object>();
178
+ let root: object | undefined;
173
179
  return function (key, value) {
174
180
  const held =
175
181
  typeof this === "object" && this !== null
176
182
  ? (this as Record<string, unknown>)[key]
177
183
  : undefined;
184
+ if (key === "" && typeof held === "object" && held !== null) root = held;
178
185
  if (held instanceof Error) value = held;
186
+ else if (this === root && key === "error" && typeof held === "object" && held !== null) {
187
+ return seen.has(held) ? "[Circular]" : narrow(held, seen);
188
+ }
179
189
  if (typeof value === "bigint") return value.toString();
180
190
  if (value instanceof Error) {
181
191
  if (seen.has(value)) return "[Circular]";
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { z } from "zod";
3
+ import { type ValidationIssue, validationIssues } from "./index.ts";
3
4
  import { AppError, createAppError } from "./errors.ts";
4
5
  import { created, createErrorResponse, noContent, ok, paginated } from "./responses.ts";
5
6
 
@@ -110,6 +111,43 @@ describe("a validation failure is a 400 that reflects nothing back", () => {
110
111
  .object({ days: z.enum(["7", "30"]), timeoutMs: z.number().max(30_000) })
111
112
  .strict();
112
113
 
114
+ it("exports the safe issue projection for every door that validates input", () => {
115
+ const validationError = {
116
+ issues: [
117
+ {
118
+ path: ["items", 0, Symbol("field"), Symbol()],
119
+ code: "too_big",
120
+ maximum: 10,
121
+ minimum: 1,
122
+ received: "secret",
123
+ values: ["private", "schema"],
124
+ message: "Expected a private schema value",
125
+ },
126
+ { path: ["amount"], code: "too_big", maximum: 10n, minimum: "1" },
127
+ ],
128
+ } as const;
129
+ const projected: ValidationIssue[] = validationIssues(validationError);
130
+
131
+ expect(projected).toEqual([
132
+ { path: ["items", 0, "field", ""], code: "too_big", maximum: 10, minimum: 1 },
133
+ { path: ["amount"], code: "too_big" },
134
+ ]);
135
+ expect(() => JSON.stringify(projected)).not.toThrow();
136
+ expect(JSON.stringify(projected)).not.toContain("secret");
137
+ expect(JSON.stringify(projected)).not.toContain("private");
138
+ });
139
+
140
+ it("answers rather than throws when an issue arrives without a usable path", () => {
141
+ // The gate admits anything named ZodError with an array of issues, so an element that lost a
142
+ // field crossing a queue or a tool boundary reaches the projection. Throwing here throws
143
+ // inside the function whose whole job is to turn a thrown thing into an answer.
144
+ for (const issue of [{ code: "custom" }, null, { path: "amount", code: "too_big" }]) {
145
+ const answer = errorResponse({ name: "ZodError", issues: [issue] });
146
+ expect(answer.status).toBe(400);
147
+ expect(answer.body.error.details).toEqual([{ path: [], code: issue?.code ?? "" }]);
148
+ }
149
+ });
150
+
113
151
  it("answers 400 with the path and the rule", () => {
114
152
  const parsed = schema.safeParse({ days: "90", timeoutMs: 1 });
115
153
  const answer = errorResponse(parsed.error);
package/src/responses.ts CHANGED
@@ -152,11 +152,27 @@ function envelope<Code extends string, Key extends string>(
152
152
  };
153
153
  }
154
154
 
155
+ /**
156
+ * What an issue MIGHT carry — every field optional, because the gate below proves only that
157
+ * `issues` is an array and nothing at all about an element. Typing the element as certain is
158
+ * what turned this projection into a throw: `path.map` on an issue that arrived without one.
159
+ */
155
160
  interface RawIssue {
156
- path?: unknown;
157
- code?: unknown;
158
- maximum?: unknown;
159
- minimum?: unknown;
161
+ readonly path?: unknown;
162
+ readonly code?: unknown;
163
+ readonly maximum?: unknown;
164
+ readonly minimum?: unknown;
165
+ }
166
+
167
+ /** One rejected field: enough to fix the call, and nothing about the schema. */
168
+ export interface ValidationIssue {
169
+ /** The field that failed, as the caller spelled it. */
170
+ path: (string | number)[];
171
+ /** The rule that rejected it, such as `too_big`. */
172
+ code: string;
173
+ /** The numeric bound, when the rule has one. */
174
+ maximum?: number;
175
+ minimum?: number;
160
176
  }
161
177
 
162
178
  /**
@@ -174,6 +190,18 @@ function zodIssues(err: unknown): RawIssue[] | null {
174
190
  return issues as RawIssue[];
175
191
  }
176
192
 
193
+ /**
194
+ * One path segment, as something that survives `JSON.stringify`.
195
+ *
196
+ * A symbol keyed a field the caller cannot name back at us, so its description is the only
197
+ * useful thing in it — and an unnamed symbol has none, which is an empty segment rather than
198
+ * the `null` that `JSON.stringify` would otherwise write.
199
+ */
200
+ function pathSegment(segment: unknown): string | number {
201
+ if (typeof segment === "symbol") return segment.description ?? "";
202
+ return typeof segment === "number" ? segment : String(segment);
203
+ }
204
+
177
205
  /**
178
206
  * The field path, the rule it failed, and — for a range — the BOUND it failed against.
179
207
  *
@@ -187,14 +215,24 @@ function zodIssues(err: unknown): RawIssue[] | null {
187
215
  *
188
216
  * The bound is the exception, and it belongs to the caller: it is the published contract, and
189
217
  * a `too_big` without it costs somebody a bisect to rediscover a number our own docs state.
218
+ *
219
+ * Total on purpose: this runs inside the function that turns a thrown thing into an answer, and
220
+ * it is advertised to the queue and tool doors, where an issue list has crossed a serialization
221
+ * hop. An allow-list that throws on a malformed issue sends its caller back to shipping the
222
+ * validator's issues raw, which is the disclosure it exists to prevent.
190
223
  */
191
- function safeIssues(issues: RawIssue[]): unknown[] {
192
- return issues.map((issue) => ({
193
- path: issue.path,
194
- code: issue.code,
195
- ...(typeof issue.maximum === "number" && { maximum: issue.maximum }),
196
- ...(typeof issue.minimum === "number" && { minimum: issue.minimum }),
197
- }));
224
+ export function validationIssues(error: {
225
+ readonly issues: readonly RawIssue[];
226
+ }): ValidationIssue[] {
227
+ return error.issues.map((issue): ValidationIssue => {
228
+ const { path, code, maximum, minimum } = issue ?? {};
229
+ return {
230
+ path: Array.isArray(path) ? path.map(pathSegment) : [],
231
+ code: typeof code === "string" ? code : "",
232
+ ...(typeof maximum === "number" && { maximum }),
233
+ ...(typeof minimum === "number" && { minimum }),
234
+ };
235
+ });
198
236
  }
199
237
 
200
238
  /**
@@ -233,7 +271,7 @@ export function createErrorResponse<Code extends string = string, Key extends st
233
271
  if (issues !== null) {
234
272
  return {
235
273
  status: 400,
236
- body: envelope(opts.validation, safeIssues(issues)),
274
+ body: envelope(opts.validation, validationIssues({ issues })),
237
275
  headers: {},
238
276
  kind: "client",
239
277
  };