@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 +36 -1
- package/dist/hono/index.d.ts +6 -0
- package/dist/hono/index.d.ts.map +1 -1
- package/dist/hono/index.js +6 -0
- package/dist/hono/index.js.map +1 -1
- package/dist/hono/request-logger.d.ts +17 -5
- package/dist/hono/request-logger.d.ts.map +1 -1
- package/dist/hono/request-logger.js +14 -2
- package/dist/hono/request-logger.js.map +1 -1
- package/dist/hono/responses.d.ts +43 -0
- package/dist/hono/responses.d.ts.map +1 -0
- package/dist/hono/responses.js +20 -0
- package/dist/hono/responses.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/logger/serialize.d.ts +15 -3
- package/dist/logger/serialize.d.ts.map +1 -1
- package/dist/logger/serialize.js +24 -4
- package/dist/logger/serialize.js.map +1 -1
- package/dist/responses.d.ts +58 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +42 -9
- package/dist/responses.js.map +1 -1
- package/package.json +1 -1
- package/src/hono/index.ts +6 -0
- package/src/hono/request-logger.test.ts +75 -3
- package/src/hono/request-logger.ts +33 -5
- package/src/hono/responses.test.ts +50 -0
- package/src/hono/responses.ts +41 -0
- package/src/index.ts +14 -2
- package/src/logger/serialize.test.ts +47 -0
- package/src/logger/serialize.ts +23 -4
- package/src/responses.test.ts +38 -0
- package/src/responses.ts +64 -13
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
|
package/dist/hono/index.d.ts
CHANGED
|
@@ -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
|
package/dist/hono/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/dist/hono/index.js
CHANGED
|
@@ -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
|
package/dist/hono/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hono/index.ts"],"names":[],"mappings":"AAAA
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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;
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
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"}
|
package/dist/logger/serialize.js
CHANGED
|
@@ -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
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC;IACrD,sFAAsF;IACtF,yFAAyF;IACzF,4FAA4F;IAC5F,MAAM;IACN,QAAQ;IACR,SAAS;IACT,MAAM;IACN,YAAY;IACZ,UAAU;IACV,wEAAwE;IACxE,YAAY;IACZ,QAAQ;IACR,YAAY;IACZ,QAAQ;IACR,gBAAgB;IAChB,8FAA8F;IAC9F,+FAA+F;IAC/F,iEAAiE;IACjE,YAAY;IACZ,MAAM;IACN,WAAW;IACX,6FAA6F;IAC7F,+EAA+E;IAC/E,MAAM;CACP,CAAC,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,cAAc,GAAG,wBAAwB,CAAC;AAChD,MAAM,WAAW,GAAG,wEAAwE,CAAC;AAE7F,SAAS,SAAS,CAAC,GAAW,EAAE,KAAc;IAC5C,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,SAAS,CAAC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACzE,OAAO,KAAK,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC;IAChE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"serialize.js","sourceRoot":"","sources":["../../src/logger/serialize.ts"],"names":[],"mappings":"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"}
|
package/dist/responses.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
package/dist/responses.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
|
105
|
-
return issues.map((issue) =>
|
|
106
|
-
path
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
|
176
|
+
body: envelope(opts.validation, validationIssues({ issues })),
|
|
144
177
|
headers: {},
|
|
145
178
|
kind: "client",
|
|
146
179
|
};
|