@gusnips/server 0.1.0 → 0.2.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/README.md CHANGED
@@ -42,6 +42,18 @@ paginated(rows, { total: 128, limit: 20, offset: 100 });
42
42
  `hasMore` is computed from the rows you actually returned, not from `limit`, so a page cut short
43
43
  by a filter still answers honestly.
44
44
 
45
+ **What comes back is an answer, not a body.** A framework wants the `body`:
46
+
47
+ ```ts
48
+ return c.json(ok(data), 200); // WRONG: {"status":200,"body":{"data":…}}
49
+ return c.json(ok(data).body, 200); // the envelope
50
+ ```
51
+
52
+ Nothing catches the first line. `c.json` takes any JSON value, so the types hold; the status is
53
+ still 200, so a health probe and a deploy gate both pass; and a test that calls `ok` never sees
54
+ the body its caller sends. That shipped, and a client found it fifty minutes later. On Hono,
55
+ import the four adapters from `@gusnips/server/hono` and the question does not arise.
56
+
45
57
  ## Refusing a request
46
58
 
47
59
  Declare your own codes and what status each one answers with. The `satisfies` is the line that
@@ -181,7 +193,9 @@ contract, and a "too big" without it costs somebody a bisect to rediscover a num
181
193
  already state.
182
194
 
183
195
  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.
196
+ package's dependency. Measured against zod 3.25, 4.4 and 4.5. A tool or queue consumer can use
197
+ `validationIssues(error)` from the root package to apply the same allow-list without building an
198
+ HTTP answer.
185
199
 
186
200
  **A 5xx keeps its message unless its code is masked.** By default only `INTERNAL_ERROR` is,
187
201
  because that is the code you raise when something unexpected broke, so its message may carry
@@ -254,6 +268,19 @@ app.onError(errorHandler({ errorResponse, logger }));
254
268
  app.notFound(notFoundHandler(errorResponse(errors.notFound("Route"))));
255
269
  ```
256
270
 
271
+ ```ts
272
+ import { created, noContent, ok, paginated } from "@gusnips/server/hono";
273
+
274
+ app.get("/users/:id", (c) => ok(c, user)); // { data: user }, 200
275
+ app.post("/users", (c) => created(c, user)); // 201
276
+ app.get("/users", (c) => paginated(c, rows, { total, limit, offset }));
277
+ app.delete("/users/:id", (c) => noContent(c)); // 204, no body, no content-type
278
+ ```
279
+
280
+ Three adopters wrote those four functions by hand before they were here, and one of the three
281
+ got the unwrap wrong in production. `ok` takes an explicit status for the cases that are not
282
+ 200 — `ok(c, job, 202)` where the route accepted rather than answered.
283
+
257
284
  **`errorBoundary` is not optional.** Hono hands `onError` only what is `instanceof Error`.
258
285
  Anything else is rethrown past every layer and escapes as an unhandled rejection: no answer, a
259
286
  dropped connection, and a browser that reports it as a CORS failure — which sends whoever reads
@@ -264,6 +291,14 @@ hypothetical. The boundary wraps one in an `Error` and keeps the original as `ca
264
291
  document number in a log and in whatever reads that log afterwards. `/health` is skipped with
265
292
  everything under it, because every deploy polls it in a loop; a throw is logged anyway.
266
293
 
294
+ Add safe product metadata with `requestLogger<AppEnv>({ logger, fields: (c) => ({ … }) })`.
295
+ `fields` runs after the response exists, so it can read values a handler set and `c.res`; return only
296
+ bounded, sanitized values, never a raw path, query, header set, body or authentication object. Your
297
+ fields are written first, so they cannot replace the canonical request id, method, route, status,
298
+ duration or error code. A hook that throws is caught: the line is written with
299
+ `requestFieldsFailed: true` instead of your fields. A value whose own `toJSON` throws is not — that
300
+ one loses the whole line, request id included — so return plain data, not live objects.
301
+
267
302
  The request id goes back on `X-Request-ID`, on every answer including `onError`'s and
268
303
  `notFound`'s. A caller's own id is echoed only if it is 64 characters of `A-Z a-z 0-9 . _ -`,
269
304
  so the id in a line is always either the caller's or ours. Cross-origin, list that header in your
@@ -6,6 +6,11 @@
6
6
  * app.onError(errorHandler({ errorResponse, logger }));
7
7
  * app.notFound(notFoundHandler(errorResponse(errors.notFound("Route"))));
8
8
  *
9
+ * `ok`, `created`, `paginated` and `noContent` are the success half: they take the `Context` and
10
+ * put the envelope on the wire, so no route has to unwrap the `{ status, body }` answer that the
11
+ * framework-free builders return. Three adopters wrote them by hand and one got that unwrap wrong
12
+ * in production — see `responses.ts` beside this file.
13
+ *
9
14
  * and, in a test of the real app, `assertEveryRouteGuarded(app, { isPublic })`, with the rule the
10
15
  * app itself uses for what anyone may call.
11
16
  */
@@ -13,6 +18,7 @@ export { errorBoundary, errorHandler, notFoundHandler } from "./errors.ts";
13
18
  export type { ErrorHandlerOptions } from "./errors.ts";
14
19
  export { assertEveryRouteGuarded, guard, underAny } from "./guards.ts";
15
20
  export type { GuardCheckOptions } from "./guards.ts";
21
+ export { created, noContent, ok, paginated } from "./responses.ts";
16
22
  export { requestLogger } from "./request-logger.ts";
17
23
  export type { RequestLoggerOptions, RequestVariables } from "./request-logger.ts";
18
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,uBAAuB,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,uBAAuB,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -6,10 +6,16 @@
6
6
  * app.onError(errorHandler({ errorResponse, logger }));
7
7
  * app.notFound(notFoundHandler(errorResponse(errors.notFound("Route"))));
8
8
  *
9
+ * `ok`, `created`, `paginated` and `noContent` are the success half: they take the `Context` and
10
+ * put the envelope on the wire, so no route has to unwrap the `{ status, body }` answer that the
11
+ * framework-free builders return. Three adopters wrote them by hand and one got that unwrap wrong
12
+ * in production — see `responses.ts` beside this file.
13
+ *
9
14
  * and, in a test of the real app, `assertEveryRouteGuarded(app, { isPublic })`, with the rule the
10
15
  * app itself uses for what anyone may call.
11
16
  */
12
17
  export { errorBoundary, errorHandler, notFoundHandler } from "./errors.js";
13
18
  export { assertEveryRouteGuarded, guard, underAny } from "./guards.js";
19
+ export { created, noContent, ok, paginated } from "./responses.js";
14
20
  export { requestLogger } from "./request-logger.js";
15
21
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE3E,OAAO,EAAE,uBAAuB,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE3E,OAAO,EAAE,uBAAuB,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
@@ -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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The success half of the Hono edge: four adapters that put the envelope on the wire.
3
+ *
4
+ * They exist because the builders in `../responses.ts` return an ANSWER — `{ status, body }` —
5
+ * and every Hono adopter therefore has to unwrap one before `c.json` sees it. Three of three
6
+ * wrote these same four functions by hand, and one of the three wrote `c.json(ok(data))` instead
7
+ * of `c.json(ok(data).body)`: every 200 from a live API answered
8
+ * `{"status":200,"body":{"data":…}}` for fifty minutes. Nothing caught it. `c.json` takes any
9
+ * JSON value, so the types were satisfied; the status was still 200, so every probe and every
10
+ * deploy gate was satisfied; and a test that calls the module never sees the body its caller
11
+ * sends. A client found it, because a client is the only reader that parses the envelope.
12
+ *
13
+ * A doc comment would have been read by whoever was already careful. These make the wrong line
14
+ * unreachable, which is the only fix available to a package that owns both sides of the seam.
15
+ */
16
+ import type { PaginationMeta } from "@gusnips/http";
17
+ import type { Context } from "hono";
18
+ import type { ContentfulStatusCode } from "hono/utils/http-status";
19
+ /** `return ok(c, user)` — or `ok(c, job, 202)` where the route accepted rather than answered. */
20
+ export declare function ok<T>(c: Context, data: T, status?: ContentfulStatusCode): Response & import("hono").TypedResponse<PaginationMeta | T | undefined extends bigint | readonly bigint[] ? never : { [K in keyof {
21
+ data: T;
22
+ meta?: PaginationMeta | undefined;
23
+ } as (import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] extends infer T_1 ? T_1 extends import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] ? T_1 extends import("hono/utils/types").InvalidJSONValue ? true : false : never : never) extends true ? never : K]: boolean extends (import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] extends infer T_2 ? T_2 extends import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] ? T_2 extends import("hono/utils/types").InvalidJSONValue ? true : false : never : never) ? import("hono/utils/types").JSONParsed<import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K], bigint | readonly bigint[]> | undefined : import("hono/utils/types").JSONParsed<import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K], bigint | readonly bigint[]>; }, 429 | 500 | 200 | 201 | 400 | 401 | 100 | 102 | 103 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 431 | 451 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1, "json">;
24
+ export declare function created<T>(c: Context, data: T): Response & import("hono").TypedResponse<PaginationMeta | T | undefined extends bigint | readonly bigint[] ? never : { [K in keyof {
25
+ data: T;
26
+ meta?: PaginationMeta | undefined;
27
+ } as (import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] extends infer T_1 ? T_1 extends import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] ? T_1 extends import("hono/utils/types").InvalidJSONValue ? true : false : never : never) extends true ? never : K]: boolean extends (import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] extends infer T_2 ? T_2 extends import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K] ? T_2 extends import("hono/utils/types").InvalidJSONValue ? true : false : never : never) ? import("hono/utils/types").JSONParsed<import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K], bigint | readonly bigint[]> | undefined : import("hono/utils/types").JSONParsed<import("@gusnips/http").ApiSuccess<T, PaginationMeta>[K], bigint | readonly bigint[]>; }, 429 | 500 | 200 | 201 | 400 | 401 | 100 | 102 | 103 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 431 | 451 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | -1, "json">;
28
+ /** `hasMore` is computed from the rows actually returned — see `paginated` in `../responses.ts`. */
29
+ export declare function paginated<T>(c: Context, rows: T[], meta: Omit<PaginationMeta, "hasMore">): Response & import("hono").TypedResponse<{
30
+ data: import("hono/utils/types").JSONParsed<T extends import("hono/utils/types").InvalidJSONValue ? null : T, bigint | readonly bigint[]>[];
31
+ meta?: {
32
+ total: number;
33
+ limit: number;
34
+ offset: number;
35
+ hasMore: boolean;
36
+ } | undefined;
37
+ }, 200, "json">;
38
+ /**
39
+ * `c.body(null, 204)`, not `c.json`: a 204 carries no body, and `c.json(null, 204)` writes the
40
+ * four bytes `null` and a `content-type` header under a status that promises neither.
41
+ */
42
+ export declare function noContent(c: Context): Response & import("hono").TypedResponse<null, 204, "body">;
43
+ //# sourceMappingURL=responses.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/hono/responses.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,iGAAiG;AACjG,wBAAgB,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,GAAE,oBAA0B;;;ooCAE5E;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;;;ooCAE7C;AAED,oGAAoG;AACpG,wBAAgB,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,CAAC;;;;;;;;gBAExF;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,8DAEnC"}
@@ -0,0 +1,20 @@
1
+ import { ok as okBody, paginated as paginatedBody } from "../responses.js";
2
+ /** `return ok(c, user)` — or `ok(c, job, 202)` where the route accepted rather than answered. */
3
+ export function ok(c, data, status = 200) {
4
+ return c.json(okBody(data).body, status);
5
+ }
6
+ export function created(c, data) {
7
+ return ok(c, data, 201);
8
+ }
9
+ /** `hasMore` is computed from the rows actually returned — see `paginated` in `../responses.ts`. */
10
+ export function paginated(c, rows, meta) {
11
+ return c.json(paginatedBody(rows, meta).body, 200);
12
+ }
13
+ /**
14
+ * `c.body(null, 204)`, not `c.json`: a 204 carries no body, and `c.json(null, 204)` writes the
15
+ * four bytes `null` and a `content-type` header under a status that promises neither.
16
+ */
17
+ export function noContent(c) {
18
+ return c.body(null, 204);
19
+ }
20
+ //# sourceMappingURL=responses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"responses.js","sourceRoot":"","sources":["../../src/hono/responses.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE3E,iGAAiG;AACjG,MAAM,UAAU,EAAE,CAAI,CAAU,EAAE,IAAO,EAAE,SAA+B,GAAG;IAC3E,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,OAAO,CAAI,CAAU,EAAE,IAAO;IAC5C,OAAO,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,SAAS,CAAI,CAAU,EAAE,IAAS,EAAE,IAAqC;IACvF,OAAO,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AACrD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,CAAU;IAClC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AAC3B,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,9 +17,18 @@
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
22
- * carries them and a line with neither says nothing at all.
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`, `message` and `stack` come along because a rejection
24
+ * object usually carries them and a line with none of them says nothing at all.
25
+ *
26
+ * `stack` is here because an adopter's queue found it missing. A job that dies is stored by its
27
+ * queue through a serializer, so the error reaching the dead-letter handler is a plain object with
28
+ * its stack in a string — and that stack is the whole of the "why" in a line whose job is to say
29
+ * which job died and why. The Error branch below has always written `stack` unfiltered; leaving it
30
+ * out here was an asymmetry, not a decision. It is a conventional field name, not one an SDK hangs
31
+ * its own inputs off, which is what the allow-list exists to stop.
23
32
  *
24
33
  * Deliberate state it does NOT keep: context an app attaches on purpose. That belongs in the
25
34
  * logger's `meta`, which is untouched — `cause` is not the place for it, and one incident of a
@@ -33,6 +42,9 @@ export declare function narrowErrorLike(value: object): Record<string, unknown>;
33
42
  * `JSON.stringify(err)` is `{}` — which is how a logger ends up printing nothing about the
34
43
  * failure it was called to report. They are added explicitly, and the allow-listed extras ride
35
44
  * along beside them.
45
+ * - A plain object in the root `error` slot is narrowed through the same allow-list. Hono's
46
+ * boundary turns one into an Error cause, but workers and swallowed catches log it directly.
47
+ * Other metadata objects stay untouched.
36
48
  * - A nested `cause` is followed, and so is an `AggregateError`'s `errors`. Both are
37
49
  * non-enumerable, so both are invisible to the loop above; without this line "all attempts
38
50
  * 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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEtE;AAiCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,9 +87,18 @@ 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
92
- * carries them and a line with neither says nothing at all.
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`, `message` and `stack` come along because a rejection
94
+ * object usually carries them and a line with none of them says nothing at all.
95
+ *
96
+ * `stack` is here because an adopter's queue found it missing. A job that dies is stored by its
97
+ * queue through a serializer, so the error reaching the dead-letter handler is a plain object with
98
+ * its stack in a string — and that stack is the whole of the "why" in a line whose job is to say
99
+ * which job died and why. The Error branch below has always written `stack` unfiltered; leaving it
100
+ * out here was an asymmetry, not a decision. It is a conventional field name, not one an SDK hangs
101
+ * its own inputs off, which is what the allow-list exists to stop.
93
102
  *
94
103
  * Deliberate state it does NOT keep: context an app attaches on purpose. That belongs in the
95
104
  * logger's `meta`, which is untouched — `cause` is not the place for it, and one incident of a
@@ -101,11 +110,13 @@ export function narrowErrorLike(value) {
101
110
  function narrow(value, seen) {
102
111
  seen.add(value);
103
112
  const out = {};
104
- const { name, message, cause } = value;
113
+ const { name, message, stack, cause } = value;
105
114
  if (typeof name === "string")
106
115
  out.name = name;
107
116
  if (typeof message === "string")
108
117
  out.message = message;
118
+ if (typeof stack === "string")
119
+ out.stack = stack;
109
120
  for (const [k, v] of Object.entries(value)) {
110
121
  if (KEPT_ERROR_FIELDS.has(k))
111
122
  out[k] = keptValue(k, v);
@@ -133,6 +144,9 @@ function narrowCause(cause, seen) {
133
144
  * `JSON.stringify(err)` is `{}` — which is how a logger ends up printing nothing about the
134
145
  * failure it was called to report. They are added explicitly, and the allow-listed extras ride
135
146
  * along beside them.
147
+ * - A plain object in the root `error` slot is narrowed through the same allow-list. Hono's
148
+ * boundary turns one into an Error cause, but workers and swallowed catches log it directly.
149
+ * Other metadata objects stay untouched.
136
150
  * - A nested `cause` is followed, and so is an `AggregateError`'s `errors`. Both are
137
151
  * non-enumerable, so both are invisible to the loop above; without this line "all attempts
138
152
  * failed" is the whole log entry. Each one goes back through this replacer, so the allow-list
@@ -164,12 +178,18 @@ function narrowCause(cause, seen) {
164
178
  */
165
179
  export function errorReplacer() {
166
180
  const seen = new WeakSet();
181
+ let root;
167
182
  return function (key, value) {
168
183
  const held = typeof this === "object" && this !== null
169
184
  ? this[key]
170
185
  : undefined;
186
+ if (key === "" && typeof held === "object" && held !== null)
187
+ root = held;
171
188
  if (held instanceof Error)
172
189
  value = held;
190
+ else if (this === root && key === "error" && typeof held === "object" && held !== null) {
191
+ return seen.has(held) ? "[Circular]" : narrow(held, seen);
192
+ }
173
193
  if (typeof value === "bigint")
174
194
  return value.toString();
175
195
  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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"}
@@ -11,7 +11,20 @@
11
11
  * callers. The framework adapter is eight lines and lives in `/hono`.
12
12
  */
13
13
  import type { ApiError, ApiSuccess, PaginationMeta } from "@gusnips/http";
14
- /** Every 2xx body is `{ data }`, or `{ data, meta }` where a route has counts to report. */
14
+ /**
15
+ * The `body` is `{ data }`, or `{ data, meta }` where a route has counts to report.
16
+ *
17
+ * What comes back is an ANSWER — `{ status, body }` — not a body, because this layer is
18
+ * framework-free and has to hand its caller a status too. An adapter takes `.body`:
19
+ *
20
+ * return c.json(ok(data), status); // WRONG: {"status":200,"body":{"data":…}}
21
+ * return c.json(ok(data).body, status); // the envelope
22
+ *
23
+ * Nothing catches the first line — `c.json` takes any JSON value, the status is still whatever
24
+ * you passed, and a test that calls this module never sees the body its caller sends. It shipped,
25
+ * and a client found it. On Hono, import `ok` from `@gusnips/server/hono` instead: the four
26
+ * adapters there take the `Context`, and the question does not arise.
27
+ */
15
28
  export declare function ok<T, M = PaginationMeta>(data: T, meta?: M): {
16
29
  status: 200;
17
30
  body: ApiSuccess<T, M>;
@@ -88,6 +101,49 @@ export interface ErrorResponseOptions<Code extends string, Key extends string> {
88
101
  */
89
102
  maskDetails?: boolean;
90
103
  }
104
+ /**
105
+ * What an issue MIGHT carry — every field optional, because the gate below proves only that
106
+ * `issues` is an array and nothing at all about an element. Typing the element as certain is
107
+ * what turned this projection into a throw: `path.map` on an issue that arrived without one.
108
+ */
109
+ interface RawIssue {
110
+ readonly path?: unknown;
111
+ readonly code?: unknown;
112
+ readonly maximum?: unknown;
113
+ readonly minimum?: unknown;
114
+ }
115
+ /** One rejected field: enough to fix the call, and nothing about the schema. */
116
+ export interface ValidationIssue {
117
+ /** The field that failed, as the caller spelled it. */
118
+ path: (string | number)[];
119
+ /** The rule that rejected it, such as `too_big`. */
120
+ code: string;
121
+ /** The numeric bound, when the rule has one. */
122
+ maximum?: number;
123
+ minimum?: number;
124
+ }
125
+ /**
126
+ * The field path, the rule it failed, and — for a range — the BOUND it failed against.
127
+ *
128
+ * Never the rejected value, and never the schema's internals. All six donors carry a version of
129
+ * that comment; what none of them carries is the proof, so here it is: handing the validator's
130
+ * issues straight to the client ships back the caller's own key names (`keys`), the enum's
131
+ * allowed values (`values`), the validator's English sentence and the expected type
132
+ * (`origin`) — four disclosures from one convenience, and two repos in the fleet do it today.
133
+ * An audit note written against an older validator looks for `received`, which the current one
134
+ * no longer emits; the projection is an allow-list precisely so a rename cannot reopen this.
135
+ *
136
+ * The bound is the exception, and it belongs to the caller: it is the published contract, and
137
+ * a `too_big` without it costs somebody a bisect to rediscover a number our own docs state.
138
+ *
139
+ * Total on purpose: this runs inside the function that turns a thrown thing into an answer, and
140
+ * it is advertised to the queue and tool doors, where an issue list has crossed a serialization
141
+ * hop. An allow-list that throws on a malformed issue sends its caller back to shipping the
142
+ * validator's issues raw, which is the disclosure it exists to prevent.
143
+ */
144
+ export declare function validationIssues(error: {
145
+ readonly issues: readonly RawIssue[];
146
+ }): ValidationIssue[];
91
147
  /**
92
148
  * Binds the mask policy and the two canned bodies, and returns the function that answers.
93
149
  *
@@ -104,4 +160,5 @@ export interface ErrorResponseOptions<Code extends string, Key extends string> {
104
160
  * ```
105
161
  */
106
162
  export declare function createErrorResponse<Code extends string = string, Key extends string = string>(opts: ErrorResponseOptions<Code, Key>): (err: unknown) => ErrorAnswer<Code>;
163
+ export {};
107
164
  //# 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;;;;;;;;;;;;;GAaG;AACH,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
@@ -1,5 +1,18 @@
1
1
  import { AppError } from "./errors.js";
2
- /** Every 2xx body is `{ data }`, or `{ data, meta }` where a route has counts to report. */
2
+ /**
3
+ * The `body` is `{ data }`, or `{ data, meta }` where a route has counts to report.
4
+ *
5
+ * What comes back is an ANSWER — `{ status, body }` — not a body, because this layer is
6
+ * framework-free and has to hand its caller a status too. An adapter takes `.body`:
7
+ *
8
+ * return c.json(ok(data), status); // WRONG: {"status":200,"body":{"data":…}}
9
+ * return c.json(ok(data).body, status); // the envelope
10
+ *
11
+ * Nothing catches the first line — `c.json` takes any JSON value, the status is still whatever
12
+ * you passed, and a test that calls this module never sees the body its caller sends. It shipped,
13
+ * and a client found it. On Hono, import `ok` from `@gusnips/server/hono` instead: the four
14
+ * adapters there take the `Context`, and the question does not arise.
15
+ */
3
16
  export function ok(data, meta) {
4
17
  const body = meta === undefined ? { data } : { data, meta };
5
18
  return { status: 200, body };
@@ -87,6 +100,18 @@ function zodIssues(err) {
87
100
  return null;
88
101
  return issues;
89
102
  }
103
+ /**
104
+ * One path segment, as something that survives `JSON.stringify`.
105
+ *
106
+ * A symbol keyed a field the caller cannot name back at us, so its description is the only
107
+ * useful thing in it — and an unnamed symbol has none, which is an empty segment rather than
108
+ * the `null` that `JSON.stringify` would otherwise write.
109
+ */
110
+ function pathSegment(segment) {
111
+ if (typeof segment === "symbol")
112
+ return segment.description ?? "";
113
+ return typeof segment === "number" ? segment : String(segment);
114
+ }
90
115
  /**
91
116
  * The field path, the rule it failed, and — for a range — the BOUND it failed against.
92
117
  *
@@ -100,14 +125,22 @@ function zodIssues(err) {
100
125
  *
101
126
  * The bound is the exception, and it belongs to the caller: it is the published contract, and
102
127
  * a `too_big` without it costs somebody a bisect to rediscover a number our own docs state.
128
+ *
129
+ * Total on purpose: this runs inside the function that turns a thrown thing into an answer, and
130
+ * it is advertised to the queue and tool doors, where an issue list has crossed a serialization
131
+ * hop. An allow-list that throws on a malformed issue sends its caller back to shipping the
132
+ * validator's issues raw, which is the disclosure it exists to prevent.
103
133
  */
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
- }));
134
+ export function validationIssues(error) {
135
+ return error.issues.map((issue) => {
136
+ const { path, code, maximum, minimum } = issue ?? {};
137
+ return {
138
+ path: Array.isArray(path) ? path.map(pathSegment) : [],
139
+ code: typeof code === "string" ? code : "",
140
+ ...(typeof maximum === "number" && { maximum }),
141
+ ...(typeof minimum === "number" && { minimum }),
142
+ };
143
+ });
111
144
  }
112
145
  /**
113
146
  * `AppError` is generic over the product's own code union, and no runtime check can verify
@@ -140,7 +173,7 @@ export function createErrorResponse(opts) {
140
173
  if (issues !== null) {
141
174
  return {
142
175
  status: 400,
143
- body: envelope(opts.validation, safeIssues(issues)),
176
+ body: envelope(opts.validation, validationIssues({ issues })),
144
177
  headers: {},
145
178
  kind: "client",
146
179
  };