@gusnips/server 0.1.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -0
  3. package/dist/errors.d.ts +190 -0
  4. package/dist/errors.d.ts.map +1 -0
  5. package/dist/errors.js +154 -0
  6. package/dist/errors.js.map +1 -0
  7. package/dist/hono/errors.d.ts +46 -0
  8. package/dist/hono/errors.d.ts.map +1 -0
  9. package/dist/hono/errors.js +54 -0
  10. package/dist/hono/errors.js.map +1 -0
  11. package/dist/hono/guards.d.ts +45 -0
  12. package/dist/hono/guards.d.ts.map +1 -0
  13. package/dist/hono/guards.js +88 -0
  14. package/dist/hono/guards.js.map +1 -0
  15. package/dist/hono/index.d.ts +18 -0
  16. package/dist/hono/index.d.ts.map +1 -0
  17. package/dist/hono/index.js +15 -0
  18. package/dist/hono/index.js.map +1 -0
  19. package/dist/hono/request-logger.d.ts +31 -0
  20. package/dist/hono/request-logger.d.ts.map +1 -0
  21. package/dist/hono/request-logger.js +57 -0
  22. package/dist/hono/request-logger.js.map +1 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +4 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/logger/index.d.ts +44 -0
  28. package/dist/logger/index.d.ts.map +1 -0
  29. package/dist/logger/index.js +74 -0
  30. package/dist/logger/index.js.map +1 -0
  31. package/dist/logger/serialize.d.ts +68 -0
  32. package/dist/logger/serialize.d.ts.map +1 -0
  33. package/dist/logger/serialize.js +203 -0
  34. package/dist/logger/serialize.js.map +1 -0
  35. package/dist/responses.d.ts +107 -0
  36. package/dist/responses.d.ts.map +1 -0
  37. package/dist/responses.js +183 -0
  38. package/dist/responses.js.map +1 -0
  39. package/package.json +79 -0
  40. package/src/errors.test.ts +93 -0
  41. package/src/errors.ts +264 -0
  42. package/src/errors.types.test.ts +96 -0
  43. package/src/hono/errors.test.ts +215 -0
  44. package/src/hono/errors.ts +86 -0
  45. package/src/hono/guards.test.ts +234 -0
  46. package/src/hono/guards.ts +107 -0
  47. package/src/hono/index.ts +17 -0
  48. package/src/hono/request-logger.test.ts +200 -0
  49. package/src/hono/request-logger.ts +77 -0
  50. package/src/index.ts +6 -0
  51. package/src/logger/index.test.ts +137 -0
  52. package/src/logger/index.ts +112 -0
  53. package/src/logger/serialize.test.ts +300 -0
  54. package/src/logger/serialize.ts +202 -0
  55. package/src/readme.test.ts +132 -0
  56. package/src/responses.test.ts +291 -0
  57. package/src/responses.ts +277 -0
package/dist/errors.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * The one error a route throws, and the one function that turns any thrown value into words.
3
+ *
4
+ * Extracted from six backends whose copies of this file are byte-identical in the parts that
5
+ * matter: the envelope builder in four of them, `toMessage()` in six. Where they differ, the
6
+ * version carrying the production reason won — every comment below names a failure somebody
7
+ * shipped.
8
+ *
9
+ * Nothing here knows about the wire. That is deliberate and it is the fix to a live bug; see
10
+ * the note on {@link AppError}.
11
+ */
12
+ import { narrowErrorLike } from "./logger/serialize.js";
13
+ /**
14
+ * The one error type routes throw; {@link errorResponse} formats the envelope.
15
+ *
16
+ * `Code` is your product's error-code union and `Key` its message-key union. Neither is
17
+ * shipped here: across six donors the factory tables hold 46 distinct code names and exactly
18
+ * nine appear in all six. The codes are an API's vocabulary. What this package ships is the
19
+ * shape, the wire format and the mask.
20
+ *
21
+ * Prefer {@link createAppError} over `new AppError(…)`: it reads the status off your own
22
+ * code→status map, so no call site names a status and a code added without one is a build
23
+ * error.
24
+ *
25
+ * Constructing one directly is the one path where a 429 with no wait is still representable:
26
+ * the rule lives on the factory, because only the factory knows the map. That is the reason to
27
+ * prefer it, not a style note.
28
+ *
29
+ * **There is no `toJSON()`, on purpose**, and the reason is not the one first written here.
30
+ * `JSON.stringify` calls a value's own `toJSON()` BEFORE the replacer, so an error class that
31
+ * defines one hands a logger whatever that method returns instead of the error. Seven backends
32
+ * define one, and every one of them logs `{"error":{"error":{code,message}}}` — doubly nested,
33
+ * no `stack`, no `cause` — from a line that still looks like a log line.
34
+ *
35
+ * This file used to say a logger cannot fix that from its side. It can, and ours does: the
36
+ * replacer is called with the HOLDER as `this`, whose own property is still the untouched error
37
+ * (see `errorReplacer`). What remains true is the design: the wire body is built by
38
+ * `errorResponse`, where the mask lives anyway, so one function owns the shape a client sees —
39
+ * and this stays an ordinary Error to anything that serializes it.
40
+ */
41
+ export class AppError extends Error {
42
+ statusCode;
43
+ code;
44
+ details;
45
+ messageKey;
46
+ params;
47
+ retryAfterSecs;
48
+ expose;
49
+ constructor(statusCode, code, message, opts = {}) {
50
+ super(message, { cause: opts.cause });
51
+ this.name = "AppError";
52
+ this.statusCode = statusCode;
53
+ this.code = code;
54
+ this.details = opts.details;
55
+ this.messageKey = opts.messageKey;
56
+ this.params = opts.params;
57
+ this.retryAfterSecs = opts.retryAfterSecs;
58
+ this.expose = opts.expose ?? false;
59
+ }
60
+ }
61
+ /**
62
+ * Binds your code→status map, and returns the factory your `errors.*` table calls.
63
+ *
64
+ * ```ts
65
+ * const ERROR_STATUS = {
66
+ * NOT_FOUND: 404,
67
+ * RATE_LIMIT_EXCEEDED: 429,
68
+ * } as const satisfies Record<ErrorCode, number>;
69
+ *
70
+ * const appError = createAppError<typeof ERROR_STATUS, MessageKey>(ERROR_STATUS);
71
+ *
72
+ * export const errors = {
73
+ * notFound: (what = "Resource") => appError("NOT_FOUND", `${what} not found`),
74
+ * rateLimit: (retryAfterSecs: number) =>
75
+ * appError("RATE_LIMIT_EXCEEDED", "Too many requests", { retryAfterSecs }),
76
+ * };
77
+ * ```
78
+ *
79
+ * The `satisfies` on your map is what makes a code with no status a build error — one line,
80
+ * in your repo, and the only version of this that cannot drift. Three of the five newest
81
+ * donors pass the status at every call site instead, which compiles no matter what.
82
+ */
83
+ export function createAppError(statusOf) {
84
+ return (code, message, ...opts) =>
85
+ // A code with no status is a build error at your `satisfies`. One reaching here anyway —
86
+ // a map assembled at runtime, a code narrowed off the wire — is our bug, not the caller's.
87
+ new AppError(statusOf[code] ?? 500, code, message, opts[0]);
88
+ }
89
+ /**
90
+ * `JSON.stringify` that never throws and never answers `"[object Object]"`.
91
+ *
92
+ * Private on purpose: it exists for {@link toMessage}'s last branch. A logger wants a richer
93
+ * one (an allow-list over an error's own fields), which is a different function.
94
+ */
95
+ function safeStringify(value) {
96
+ const seen = new WeakSet();
97
+ try {
98
+ return (JSON.stringify(value, (_key, val) => {
99
+ if (val instanceof Error)
100
+ return { name: val.name, message: val.message };
101
+ if (typeof val === "bigint")
102
+ return val.toString();
103
+ if (typeof val === "object" && val !== null) {
104
+ if (seen.has(val))
105
+ return "[Circular]";
106
+ seen.add(val);
107
+ }
108
+ return val;
109
+ }) ?? "null");
110
+ }
111
+ catch {
112
+ return "[unserializable]";
113
+ }
114
+ }
115
+ /**
116
+ * Turn any thrown value into a string.
117
+ *
118
+ * The single home for the `err instanceof Error ? err.message : String(err)` idiom, which is
119
+ * wrong twice over and shipped that way in six repos:
120
+ *
121
+ * 1. A data layer rejects with a PLAIN OBJECT — `{code, message, hint}` is what PostgREST and
122
+ * several drivers throw — so the useful text is in `message` and `String()` never reads it.
123
+ * Six donors fixed this half.
124
+ * 2. An object with no string `message` still flattens to `"[object Object]"`, which is the
125
+ * real failure masked by a useless string. One donor fixed that half and named it exactly:
126
+ * *"masking the real failure."* Its version is the one here.
127
+ */
128
+ export function toMessage(err) {
129
+ if (err instanceof Error)
130
+ return err.message;
131
+ if (typeof err === "string")
132
+ return err;
133
+ if (typeof err === "object" && err !== null) {
134
+ const { message } = err;
135
+ if (typeof message === "string")
136
+ return message;
137
+ // With no message to read, the object itself has to say what failed — and it cannot be
138
+ // printed whole. This string becomes an Error's `message` in `errorBoundary`, and a message
139
+ // is printed, so an SDK that rejects with the request it sent would put that request in the
140
+ // log through here. Same allow-list as the log serializer, one step earlier.
141
+ const kept = narrowErrorLike(err);
142
+ if (Object.keys(kept).length > 0)
143
+ return safeStringify(kept);
144
+ // Nothing printable left. `{}` would be the "[object Object]" failure again in a new
145
+ // spelling: a useless string standing where the real failure was. The key names come from
146
+ // whoever threw, never from a caller, so they are the part that still identifies it.
147
+ const keys = Object.keys(err);
148
+ return keys.length > 0
149
+ ? `An object was thrown with no message. Its keys: ${keys.join(", ")}.`
150
+ : "An object was thrown with no message and no fields.";
151
+ }
152
+ return String(err);
153
+ }
154
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { Context, ErrorHandler, MiddlewareHandler, NotFoundHandler } from "hono";
2
+ import type { Logger } from "../logger/index.ts";
3
+ import type { ErrorAnswer } from "../responses.ts";
4
+ import type { RequestVariables } from "./request-logger.ts";
5
+ /**
6
+ * The app's own env, taken from the `onError` / `notFound` call it is passed to, so its bindings
7
+ * and variables fit as they are. The code type comes from its `errorCode`, which is what stops
8
+ * `errorResponse` from answering a code that slot cannot hold.
9
+ */
10
+ type ErrorEnv = {
11
+ Variables: RequestVariables;
12
+ };
13
+ type CodeOf<E extends ErrorEnv> = NonNullable<E["Variables"]["errorCode"]>;
14
+ /**
15
+ * Turns a thrown non-`Error` into an `Error`, so it reaches `onError`.
16
+ *
17
+ * Hono hands `onError` only what is `instanceof Error`. Anything else is rethrown past every
18
+ * layer and escapes as an unhandled rejection: no answer, a dropped connection, and a browser that
19
+ * reports it as a CORS failure — which sends whoever reads it to the wrong layer. A PostgREST
20
+ * client rejects with plain objects, so this is not hypothetical. The original rides as `cause`.
21
+ *
22
+ * Mount it right after `requestLogger`.
23
+ */
24
+ export declare const errorBoundary: MiddlewareHandler;
25
+ export interface ErrorHandlerOptions<E extends ErrorEnv> {
26
+ /** Your bound `createErrorResponse(...)`. */
27
+ errorResponse: (err: unknown) => ErrorAnswer<CodeOf<E>>;
28
+ logger: Logger;
29
+ /**
30
+ * Called for a throw nobody raised on purpose, which is where an alert belongs. It runs inside
31
+ * `onError`, so it must not throw, and anything slow should be sent without being awaited.
32
+ */
33
+ onUnexpected?: (err: unknown, c: Context<E>) => void;
34
+ }
35
+ /**
36
+ * `app.onError(errorHandler({ errorResponse, logger }))`.
37
+ *
38
+ * A refusal under 500 writes no line of its own: its code rides on the request line, because one
39
+ * line per wrong password buries the failures that need a human. A 5xx and an escaped throw are
40
+ * logged with the raw error, stack and cause included.
41
+ */
42
+ export declare function errorHandler<E extends ErrorEnv>({ errorResponse, logger, onUnexpected, }: ErrorHandlerOptions<E>): ErrorHandler<E>;
43
+ /** `app.notFound(notFoundHandler(errorResponse(errors.notFound("Route"))))`. */
44
+ export declare const notFoundHandler: <E extends ErrorEnv>(answer: ErrorAnswer<CodeOf<E>>) => NotFoundHandler<E>;
45
+ export {};
46
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/hono/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAGtF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;GAIG;AACH,KAAK,QAAQ,GAAG;IAAE,SAAS,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAChD,KAAK,MAAM,CAAC,CAAC,SAAS,QAAQ,IAAI,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;AAE3E;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,EAAE,iBAO3B,CAAC;AAEF,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,QAAQ;IACrD,6CAA6C;IAC7C,aAAa,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACxD,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;CACtD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,QAAQ,EAAE,EAC/C,aAAa,EACb,MAAM,EACN,YAAY,GACb,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAY1C;AAED,gFAAgF;AAChF,eAAO,MAAM,eAAe,GACzB,CAAC,SAAS,QAAQ,EAAE,QAAQ,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAG,eAAe,CAAC,CAAC,CAEnD,CAAC"}
@@ -0,0 +1,54 @@
1
+ import { toMessage } from "../errors.js";
2
+ /**
3
+ * Turns a thrown non-`Error` into an `Error`, so it reaches `onError`.
4
+ *
5
+ * Hono hands `onError` only what is `instanceof Error`. Anything else is rethrown past every
6
+ * layer and escapes as an unhandled rejection: no answer, a dropped connection, and a browser that
7
+ * reports it as a CORS failure — which sends whoever reads it to the wrong layer. A PostgREST
8
+ * client rejects with plain objects, so this is not hypothetical. The original rides as `cause`.
9
+ *
10
+ * Mount it right after `requestLogger`.
11
+ */
12
+ export const errorBoundary = async (_c, next) => {
13
+ try {
14
+ await next();
15
+ }
16
+ catch (err) {
17
+ if (err instanceof Error)
18
+ throw err;
19
+ throw new Error(toMessage(err), { cause: err });
20
+ }
21
+ };
22
+ /**
23
+ * `app.onError(errorHandler({ errorResponse, logger }))`.
24
+ *
25
+ * A refusal under 500 writes no line of its own: its code rides on the request line, because one
26
+ * line per wrong password buries the failures that need a human. A 5xx and an escaped throw are
27
+ * logged with the raw error, stack and cause included.
28
+ */
29
+ export function errorHandler({ errorResponse, logger, onUnexpected, }) {
30
+ return (err, c) => {
31
+ const answer = errorResponse(err);
32
+ if (answer.kind !== "client")
33
+ logger.error("request failed", {
34
+ requestId: c.get("requestId"),
35
+ kind: answer.kind,
36
+ error: err,
37
+ });
38
+ if (answer.kind === "unexpected")
39
+ onUnexpected?.(err, c);
40
+ return respond(c, answer);
41
+ };
42
+ }
43
+ /** `app.notFound(notFoundHandler(errorResponse(errors.notFound("Route"))))`. */
44
+ export const notFoundHandler = (answer) => (c) => respond(c, answer);
45
+ function respond(c, answer) {
46
+ // An error answered as a 2xx reads as success to every client. Thrown instead, it comes back
47
+ // through onError as the unexpected throw it is: the generic 500, logged, and alerted on.
48
+ if (!isErrorStatus(answer.status))
49
+ throw new RangeError(`An error answered ${answer.status}.`);
50
+ c.set("errorCode", answer.body.error.code);
51
+ return c.json(answer.body, answer.status, answer.headers);
52
+ }
53
+ const isErrorStatus = (status) => status >= 400 && status <= 599;
54
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/hono/errors.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAazC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAsB,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE;IACjE,IAAI,CAAC;QACH,MAAM,IAAI,EAAE,CAAC;IACf,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,KAAK;YAAE,MAAM,GAAG,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAClD,CAAC;AACH,CAAC,CAAC;AAaF;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAqB,EAC/C,aAAa,EACb,MAAM,EACN,YAAY,GACW;IACvB,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;QAChB,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ;YAC1B,MAAM,CAAC,KAAK,CAAC,gBAAgB,EAAE;gBAC7B,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC;gBAC7B,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,KAAK,EAAE,GAAG;aACX,CAAC,CAAC;QACL,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY;YAAE,YAAY,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACzD,OAAO,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC5B,CAAC,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,MAAM,eAAe,GAC1B,CAAqB,MAA8B,EAAsB,EAAE,CAC3E,CAAC,CAAC,EAAE,EAAE,CACJ,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAEvB,SAAS,OAAO,CAAqB,CAAa,EAAE,MAA8B;IAChF,6FAA6F;IAC7F,0FAA0F;IAC1F,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,qBAAqB,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IAC/F,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3C,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,aAAa,GAAG,CAAC,MAAc,EAAkC,EAAE,CACvE,MAAM,IAAI,GAAG,IAAI,MAAM,IAAI,GAAG,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { Hono, MiddlewareHandler } from "hono";
2
+ /**
3
+ * Marks a middleware as one that decides who may pass, for `assertEveryRouteGuarded`. Returns the
4
+ * same function. Mark it where it is defined, so every place it is mounted counts.
5
+ *
6
+ * export const requireAdmin = guard(async (c, next) => { … });
7
+ */
8
+ export declare function guard<H extends MiddlewareHandler>(middleware: H): H;
9
+ export interface GuardCheckOptions {
10
+ /**
11
+ * The app's own rule for what anyone may call, asked with the path a request would carry:
12
+ * `/people/:id` is asked as `/people/probe`, `/reports/:id{[0-9]+}` as `/reports/1`. Pass the
13
+ * predicate or the list the app itself uses, never a second one kept for the test: an exemption
14
+ * list nothing else reads is the next thing to drift. `underAny(list)` reads a list as prefixes.
15
+ */
16
+ isPublic: (path: string) => boolean;
17
+ }
18
+ /**
19
+ * "Under one of these", reading each entry as a prefix whichever pattern form it holds:
20
+ * `/feedback` and `/feedback/*` both cover `/feedback` and `/feedback/mine`, and neither covers
21
+ * `/feedbacks`. Hono reads `use("/feedback")` as that one path, which is how the route under it
22
+ * once shipped public while the list said it was guarded.
23
+ */
24
+ export declare function underAny(prefixes: readonly string[]): (path: string) => boolean;
25
+ /**
26
+ * Throws, naming every endpoint no guard runs in front of, and every guard that runs in front of
27
+ * nothing. Build the real app, and call it in a test once every route is registered: the first
28
+ * match freezes Hono's router.
29
+ *
30
+ * It asks Hono's own router what would run for each of the app's real routes, and in what order,
31
+ * rather than comparing patterns. A pattern list cannot see order, and order is the whole bug: a
32
+ * `use()` registered after its route matches that route and never runs for it, because the
33
+ * handler answers first. Only the matcher tells a guard that never fires from one that does.
34
+ *
35
+ * A guard in front of nothing is the trace a router leaves when it mounted no routes, which is
36
+ * what a stand-in dependency's router does in a test: its routes drop out of the check, and the
37
+ * check would pass by having nothing to ask. A guard inside that router drops out with it, so a
38
+ * stand-in that yields a router should throw when Hono reads its `routes`.
39
+ *
40
+ * An endpoint is what Hono's own route inspector calls one, a handler taking fewer than two
41
+ * arguments. So a `(c, next)` handler that answers is not checked, and a path whose parameter
42
+ * pattern rejects the probe is reported as unprobeable rather than passed.
43
+ */
44
+ export declare function assertEveryRouteGuarded(app: Pick<Hono, "routes" | "router">, { isPublic }: GuardCheckOptions): void;
45
+ //# sourceMappingURL=guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/hono/guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAOpD;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,iBAAiB,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,CAGnE;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACrC;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAG/E;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC,EACpC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,GAC9B,IAAI,CA2CN"}
@@ -0,0 +1,88 @@
1
+ import { METHODS } from "hono/router";
2
+ import { findTargetHandler, isMiddleware } from "hono/utils/handler";
3
+ const GUARD = Symbol.for("@gusnips/server/hono:guard");
4
+ /**
5
+ * Marks a middleware as one that decides who may pass, for `assertEveryRouteGuarded`. Returns the
6
+ * same function. Mark it where it is defined, so every place it is mounted counts.
7
+ *
8
+ * export const requireAdmin = guard(async (c, next) => { … });
9
+ */
10
+ export function guard(middleware) {
11
+ Object.defineProperty(middleware, GUARD, { value: true });
12
+ return middleware;
13
+ }
14
+ /**
15
+ * "Under one of these", reading each entry as a prefix whichever pattern form it holds:
16
+ * `/feedback` and `/feedback/*` both cover `/feedback` and `/feedback/mine`, and neither covers
17
+ * `/feedbacks`. Hono reads `use("/feedback")` as that one path, which is how the route under it
18
+ * once shipped public while the list said it was guarded.
19
+ */
20
+ export function underAny(prefixes) {
21
+ const bases = prefixes.map((prefix) => prefix.replace(/\/\*$/, ""));
22
+ return (path) => bases.some((base) => path === base || path.startsWith(`${base}/`));
23
+ }
24
+ const isGuard = (handler) => GUARD in findTargetHandler(handler);
25
+ /**
26
+ * Throws, naming every endpoint no guard runs in front of, and every guard that runs in front of
27
+ * nothing. Build the real app, and call it in a test once every route is registered: the first
28
+ * match freezes Hono's router.
29
+ *
30
+ * It asks Hono's own router what would run for each of the app's real routes, and in what order,
31
+ * rather than comparing patterns. A pattern list cannot see order, and order is the whole bug: a
32
+ * `use()` registered after its route matches that route and never runs for it, because the
33
+ * handler answers first. Only the matcher tells a guard that never fires from one that does.
34
+ *
35
+ * A guard in front of nothing is the trace a router leaves when it mounted no routes, which is
36
+ * what a stand-in dependency's router does in a test: its routes drop out of the check, and the
37
+ * check would pass by having nothing to ask. A guard inside that router drops out with it, so a
38
+ * stand-in that yields a router should throw when Hono reads its `routes`.
39
+ *
40
+ * An endpoint is what Hono's own route inspector calls one, a handler taking fewer than two
41
+ * arguments. So a `(c, next)` handler that answers is not checked, and a path whose parameter
42
+ * pattern rejects the probe is reported as unprobeable rather than passed.
43
+ */
44
+ export function assertEveryRouteGuarded(app, { isPublic }) {
45
+ const endpoints = app.routes.filter((route) => !isMiddleware(findTargetHandler(route.handler)));
46
+ if (endpoints.length === 0)
47
+ throw new Error("The app has no endpoints to check, so the check would pass by asking nothing.");
48
+ const idle = new Set(app.routes.filter((route) => isGuard(route.handler)));
49
+ const unguarded = new Set();
50
+ for (const route of endpoints) {
51
+ const { method, path } = route;
52
+ const probe = path.replace(/:\w+\{[^}]*\}\??/g, "1").replace(/:\w+\??|\*/g, "probe");
53
+ const open = isPublic(probe);
54
+ for (const each of method === "ALL" ? METHODS.map((m) => m.toUpperCase()) : [method]) {
55
+ const [matched] = app.router.match(each, probe);
56
+ const at = matched.findIndex(([[, candidate]]) => candidate === route);
57
+ const ahead = matched.slice(0, Math.max(at, 0)).filter(([[handler]]) => isGuard(handler));
58
+ for (const [[, guardRoute]] of ahead)
59
+ idle.delete(guardRoute);
60
+ if (open)
61
+ continue;
62
+ if (at === -1)
63
+ unguarded.add(`${method} ${path} (could not be probed at ${probe})`);
64
+ else if (ahead.length === 0)
65
+ unguarded.add(`${method} ${path}`);
66
+ }
67
+ }
68
+ if (unguarded.size === 0 && idle.size === 0)
69
+ return;
70
+ const list = (lines) => [...new Set(lines)]
71
+ .sort()
72
+ .map((line) => ` ${line}\n`)
73
+ .join("");
74
+ const idleLines = [...idle].map((route) => `${route.method} ${route.path}`);
75
+ throw new Error((unguarded.size > 0
76
+ ? `${String(unguarded.size)} endpoint(s) answer with no guard running in front of them:\n` +
77
+ list(unguarded)
78
+ : "") +
79
+ (idle.size > 0
80
+ ? `${String(new Set(idleLines).size)} guard(s) run in front of no endpoint:\n` +
81
+ list(idleLines)
82
+ : "") +
83
+ "A guard counts when it is wrapped in guard() and registered BEFORE its routes: a use() " +
84
+ "added after them never runs for them. A guard in front of nothing sits after its routes, " +
85
+ "or over a router that mounted none. Move it up, build that router for real, or have " +
86
+ "isPublic say so if anyone may call the path.");
87
+ }
88
+ //# sourceMappingURL=guards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.js","sourceRoot":"","sources":["../../src/hono/guards.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `@gusnips/server/hono`: the package mounted on a Hono app.
3
+ *
4
+ * app.use(requestLogger({ logger }));
5
+ * app.use(errorBoundary);
6
+ * app.onError(errorHandler({ errorResponse, logger }));
7
+ * app.notFound(notFoundHandler(errorResponse(errors.notFound("Route"))));
8
+ *
9
+ * and, in a test of the real app, `assertEveryRouteGuarded(app, { isPublic })`, with the rule the
10
+ * app itself uses for what anyone may call.
11
+ */
12
+ export { errorBoundary, errorHandler, notFoundHandler } from "./errors.ts";
13
+ export type { ErrorHandlerOptions } from "./errors.ts";
14
+ export { assertEveryRouteGuarded, guard, underAny } from "./guards.ts";
15
+ export type { GuardCheckOptions } from "./guards.ts";
16
+ export { requestLogger } from "./request-logger.ts";
17
+ export type { RequestLoggerOptions, RequestVariables } from "./request-logger.ts";
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `@gusnips/server/hono`: the package mounted on a Hono app.
3
+ *
4
+ * app.use(requestLogger({ logger }));
5
+ * app.use(errorBoundary);
6
+ * app.onError(errorHandler({ errorResponse, logger }));
7
+ * app.notFound(notFoundHandler(errorResponse(errors.notFound("Route"))));
8
+ *
9
+ * and, in a test of the real app, `assertEveryRouteGuarded(app, { isPublic })`, with the rule the
10
+ * app itself uses for what anyone may call.
11
+ */
12
+ export { errorBoundary, errorHandler, notFoundHandler } from "./errors.js";
13
+ export { assertEveryRouteGuarded, guard, underAny } from "./guards.js";
14
+ export { requestLogger } from "./request-logger.js";
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,31 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ import type { Logger } from "../logger/index.ts";
3
+ /** The two variables this adapter writes. Put them in your app's `Variables`. */
4
+ export interface RequestVariables<Code extends string = string> {
5
+ requestId: string;
6
+ /** The code of the refusal this request got, for the request line. `null` until one happens. */
7
+ errorCode: Code | null;
8
+ }
9
+ export interface RequestLoggerOptions {
10
+ logger: Logger;
11
+ /**
12
+ * Paths answered but never logged, each with everything under it: `/health` covers
13
+ * `/health/db` and not `/healthz`. Defaults to `["/health"]`. A throw is logged anyway.
14
+ */
15
+ skipPaths?: readonly string[];
16
+ }
17
+ /**
18
+ * One line per request, and the request id.
19
+ *
20
+ * Mount it first, so it times and sees everything under it:
21
+ *
22
+ * app.use(requestLogger({ logger }));
23
+ *
24
+ * The id goes back on `X-Request-ID`, which is where the browser client reads it from, on every
25
+ * answer including `onError`'s and `notFound`'s. Cross-origin, list that header in your CORS
26
+ * `exposeHeaders` or the browser hides it from the page.
27
+ */
28
+ export declare function requestLogger({ logger, skipPaths, }: RequestLoggerOptions): MiddlewareHandler<{
29
+ Variables: RequestVariables;
30
+ }>;
31
+ //# sourceMappingURL=request-logger.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,57 @@
1
+ import { routePath } from "hono/route";
2
+ /**
3
+ * A caller's id is echoed back and written into every line, so it is kept only in a shape that
4
+ * cannot carry anything else: 64 characters of `A-Z a-z 0-9 . _ -`. Anything else is replaced, not
5
+ * trimmed, so the id in the log is always either the caller's or ours.
6
+ */
7
+ const REQUEST_ID = /^[A-Za-z0-9._-]{1,64}$/;
8
+ /**
9
+ * One line per request, and the request id.
10
+ *
11
+ * Mount it first, so it times and sees everything under it:
12
+ *
13
+ * app.use(requestLogger({ logger }));
14
+ *
15
+ * The id goes back on `X-Request-ID`, which is where the browser client reads it from, on every
16
+ * answer including `onError`'s and `notFound`'s. Cross-origin, list that header in your CORS
17
+ * `exposeHeaders` or the browser hides it from the page.
18
+ */
19
+ export function requestLogger({ logger, skipPaths = ["/health"], }) {
20
+ return async (c, next) => {
21
+ const supplied = c.req.header("X-Request-ID");
22
+ const requestId = supplied && REQUEST_ID.test(supplied) ? supplied : crypto.randomUUID();
23
+ c.set("requestId", requestId);
24
+ c.set("errorCode", null);
25
+ const { method, path } = c.req;
26
+ const start = Date.now();
27
+ // The route TEMPLATE, never the path: a path carries whatever the caller put in it, and in one
28
+ // backend that was a national id number, which the request line carried into an analytics
29
+ // event. The last matched route rather than the deepest one that ran, so a 401 from an auth
30
+ // middleware is filed under the endpoint it protected.
31
+ const line = (status) => ({
32
+ requestId,
33
+ method,
34
+ route: routePath(c, -1),
35
+ status,
36
+ ms: Date.now() - start,
37
+ errorCode: c.get("errorCode") ?? undefined,
38
+ });
39
+ try {
40
+ await next();
41
+ }
42
+ catch (err) {
43
+ // Only what Hono would not hand to onError gets here: a non-`Error` with no `errorBoundary`
44
+ // above it, or a throw from onError itself. Nothing answered it, so the runtime will, and
45
+ // the request that most needs a line is the one that would otherwise never get one.
46
+ logger.error("request", { ...line(500), error: err });
47
+ throw err;
48
+ }
49
+ // After `next()`, not before: a header set earlier lives on a draft Hono drops when a handler
50
+ // returns a Response it built itself.
51
+ c.header("X-Request-ID", requestId);
52
+ const skipped = skipPaths.some((skip) => path === skip || path.startsWith(`${skip}/`));
53
+ if (method !== "OPTIONS" && !skipped)
54
+ logger.info("request", line(c.res.status));
55
+ };
56
+ }
57
+ //# sourceMappingURL=request-logger.js.map
@@ -0,0 +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"}
@@ -0,0 +1,7 @@
1
+ export { AppError, createAppError, toMessage } from "./errors.ts";
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";
5
+ export { createLogger, errorReplacer, keptErrorFields } from "./logger/index.ts";
6
+ export type { Logger, LoggerOptions, LogLevel, LogThreshold } from "./logger/index.ts";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { AppError, createAppError, toMessage } from "./errors.js";
2
+ export { created, createErrorResponse, noContent, ok, paginated } from "./responses.js";
3
+ export { createLogger, errorReplacer, keptErrorFields } from "./logger/index.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,44 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ /** A threshold, which is a level or "silent". "silent" is not a level: nothing logs AT it. */
3
+ export type LogThreshold = LogLevel | "silent";
4
+ export interface Logger {
5
+ debug(message: string, meta?: Record<string, unknown>): void;
6
+ info(message: string, meta?: Record<string, unknown>): void;
7
+ warn(message: string, meta?: Record<string, unknown>): void;
8
+ error(message: string, meta?: Record<string, unknown>): void;
9
+ }
10
+ export interface LoggerOptions {
11
+ /**
12
+ * The lowest level that gets written. Defaults to "info".
13
+ *
14
+ * It is an argument rather than a `process.env.LOG_LEVEL` read, and that is the boundary this
15
+ * package is built on: a Cloudflare Worker has no `process` at all, so a module-scope read makes
16
+ * the package Node-only by accident. The adopter writes the read:
17
+ *
18
+ * export const logger = createLogger({ level: process.env.LOG_LEVEL });
19
+ *
20
+ * and a Worker writes `createLogger({ level: env.LOG_LEVEL })` from its handler argument.
21
+ *
22
+ * Typed `string` on purpose, so passing `process.env.LOG_LEVEL` needs no cast. An unrecognized
23
+ * value throws — see `resolveThreshold`.
24
+ */
25
+ level?: string | undefined;
26
+ /**
27
+ * Where a finished line goes. Defaults to the console, `error`/`warn` to stderr.
28
+ *
29
+ * This is a seam, not a transport — the package ships none. It exists because one backend in
30
+ * the fleet needs the line twice (stdout and a batched exporter it flushes on SIGTERM), and
31
+ * because without it every test of anything that logs has to monkey-patch a global.
32
+ */
33
+ write?: (line: string, level: LogLevel) => void;
34
+ }
35
+ /**
36
+ * Build a logger. Call it once, at boot, and pass the result to everything that logs.
37
+ *
38
+ * const logger = createLogger({ level: process.env.LOG_LEVEL });
39
+ * logger.info("server started", { port: 3000 });
40
+ * logger.error("charge failed", { orderId, error: err }); // the RAW error, never String(err)
41
+ */
42
+ export declare function createLogger(options?: LoggerOptions): Logger;
43
+ export { errorReplacer, keptErrorFields } from "./serialize.ts";
44
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/logger/index.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,8FAA8F;AAC9F,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE/C,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9D;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;;;;;;;;OAaG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;CACjD;AAyBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,aAAkB,GAAG,MAAM,CA6BhE;AAED,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}