@nwire/koa 0.11.1 → 0.12.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.
@@ -60,6 +60,20 @@ export interface HttpKoaConfig {
60
60
  * middleware running upstream).
61
61
  */
62
62
  readonly reqId?: boolean;
63
+ /**
64
+ * Echo raw thrown `Error.message` to the client for non-typed errors.
65
+ *
66
+ * Default `false` — a generic `throw new Error("…")` returns an opaque
67
+ * `{ code: "internal_error", summary: "Internal error" }` and the real
68
+ * message is logged server-side only. This is fail-safe: driver errors,
69
+ * connection strings, and PII attached to a stray throw never reach the
70
+ * client, in any environment.
71
+ *
72
+ * Set `true` in local dev to surface the message for debugging. Typed
73
+ * `defineError` values always render their own `{ code, summary }`
74
+ * regardless — those are author-defined and safe to expose.
75
+ */
76
+ readonly exposeErrors?: boolean;
63
77
  /**
64
78
  * Tenant resolver. The dev/test default reads `x-tenant` from the
65
79
  * request header — convenient for local multi-tenancy. Production
package/dist/http-koa.js CHANGED
@@ -62,12 +62,14 @@ export function httpKoa(config = {}) {
62
62
  const koa = new Koa();
63
63
  koaInstance = koa;
64
64
  const isProd = process.env.NODE_ENV === "production";
65
+ const exposeErrors = config.exposeErrors === true;
65
66
  // Top-level error mapper — catches throws from middleware and
66
- // unmatched routes. Renders defineError-shaped values with their
67
- // {code, status, summary} envelope; in production, generic
68
- // (non-NwireError) throws return opaque "internal_error" so a
69
- // stray `throw new Error("DB password X")` cannot leak the
70
- // message to clients. The real error is always logged.
67
+ // unmatched routes. Typed `defineError` values render their own
68
+ // {code, status, summary}. Generic (non-NwireError) throws return an
69
+ // opaque "internal_error" by default so a stray
70
+ // `throw new Error("DB password X")` cannot leak the message to
71
+ // clients in any environment. `exposeErrors: true` (dev) echoes
72
+ // the message. The real error is always logged server-side.
71
73
  koa.use(async (kctx, next) => {
72
74
  try {
73
75
  await next();
@@ -75,21 +77,18 @@ export function httpKoa(config = {}) {
75
77
  catch (err) {
76
78
  const e = err;
77
79
  const isNwireError = e?.$kind === "error";
78
- const status = typeof e.status === "number" ? e.status : 500;
79
- kctx.status = status;
80
- if (!isNwireError && isProd) {
81
- // Server-side: log the full error so SRE can see it. Client:
82
- // opaque body — no error.message leak.
83
- logger.error?.(`[http-koa] unhandled error: ${e?.message ?? e}`, undefined);
80
+ kctx.status = typeof e.status === "number" ? e.status : 500;
81
+ if (isNwireError) {
84
82
  kctx.body = {
85
- error: { code: "internal_error", summary: "Internal error" },
83
+ error: { code: e.code ?? "internal_error", summary: e.summary ?? "Internal error" },
86
84
  };
87
85
  }
88
86
  else {
87
+ logger.error?.(`[http-koa] unhandled error: ${e?.message ?? e}`, undefined);
89
88
  kctx.body = {
90
89
  error: {
91
- code: e.code ?? "internal_error",
92
- summary: e.summary ?? e.message ?? "Internal error",
90
+ code: "internal_error",
91
+ summary: exposeErrors ? (e.message ?? "Internal error") : "Internal error",
93
92
  },
94
93
  };
95
94
  }
@@ -309,25 +308,26 @@ export function httpKoa(config = {}) {
309
308
  }
310
309
  }
311
310
  catch (err) {
312
- // Generic error envelope. NwireError-shaped values (defineError)
313
- // surface their {code, status, summary}. Other throws under
314
- // NODE_ENV=production — return opaque "internal_error" so a
315
- // stray `throw new Error("DB password X")` cannot leak the
316
- // message to clients. The real error is always logged.
311
+ // Generic error envelope. Typed `defineError` values surface
312
+ // their {code, status, summary}. Other throws return an opaque
313
+ // "internal_error" by default so a stray
314
+ // `throw new Error("DB password X")` cannot leak the message to
315
+ // clients in any environment. `exposeErrors: true` (dev)
316
+ // echoes the message. The real error is always logged.
317
317
  const e = err;
318
318
  const isNwireError = e?.$kind === "error";
319
319
  kctx.status = typeof e.status === "number" ? e.status : 500;
320
- if (!isNwireError && isProd) {
321
- logger.error?.(`[http-koa] unhandled error: ${e?.message ?? e}`, undefined);
320
+ if (isNwireError) {
322
321
  kctx.body = {
323
- error: { code: "internal_error", summary: "Internal error" },
322
+ error: { code: e.code ?? "internal_error", summary: e.summary ?? "Internal error" },
324
323
  };
325
324
  }
326
325
  else {
326
+ logger.error?.(`[http-koa] unhandled error: ${e?.message ?? e}`, undefined);
327
327
  kctx.body = {
328
328
  error: {
329
- code: e.code ?? "internal_error",
330
- summary: e.summary ?? e.message ?? "Internal error",
329
+ code: "internal_error",
330
+ summary: exposeErrors ? (e.message ?? "Internal error") : "Internal error",
331
331
  },
332
332
  };
333
333
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nwire/koa",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Nwire — Koa-backed HTTP adopter. Consumes wires from @nwire/wires/http and serves them as Koa routes under endpoint lifecycle.",
5
5
  "keywords": [
6
6
  "adopter",
@@ -33,10 +33,10 @@
33
33
  "koa-bodyparser": "^4.4.1",
34
34
  "koa-helmet": "^8.0.1",
35
35
  "zod": "^4.0.0",
36
- "@nwire/container": "0.11.1",
37
- "@nwire/endpoint": "0.11.1",
38
- "@nwire/logger": "0.11.1",
39
- "@nwire/wires": "0.11.1"
36
+ "@nwire/container": "0.12.0",
37
+ "@nwire/endpoint": "0.12.0",
38
+ "@nwire/logger": "0.12.0",
39
+ "@nwire/wires": "0.12.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@asteasolutions/zod-to-openapi": "^8.0.0",
@@ -47,8 +47,8 @@
47
47
  "@types/node": "^22.19.9",
48
48
  "typescript": "^5.9.3",
49
49
  "vitest": "^4.0.18",
50
- "@nwire/app": "0.11.1",
51
- "@nwire/handler": "0.11.1"
50
+ "@nwire/app": "0.12.0",
51
+ "@nwire/handler": "0.12.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@asteasolutions/zod-to-openapi": "^8.0.0"