@nwire/koa 0.11.1 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http-koa.d.ts +14 -0
- package/dist/http-koa.js +58 -26
- package/package.json +7 -7
package/dist/http-koa.d.ts
CHANGED
|
@@ -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
|
@@ -35,6 +35,24 @@ import bodyParser from "koa-bodyparser";
|
|
|
35
35
|
import cors from "@koa/cors";
|
|
36
36
|
import helmet from "koa-helmet";
|
|
37
37
|
import KoaRouter from "@koa/router";
|
|
38
|
+
/** Zod throws a `ZodError` (name + `issues[]`) on a failed `.parse()`. */
|
|
39
|
+
function isZodError(e) {
|
|
40
|
+
return (typeof e === "object" &&
|
|
41
|
+
e !== null &&
|
|
42
|
+
e.name === "ZodError" &&
|
|
43
|
+
Array.isArray(e.issues));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A human-readable summary of the first validation issue. Safe to expose:
|
|
47
|
+
* it describes the caller's own input, not internal state.
|
|
48
|
+
*/
|
|
49
|
+
function zodSummary(e) {
|
|
50
|
+
const first = e.issues[0];
|
|
51
|
+
if (!first)
|
|
52
|
+
return "validation failed";
|
|
53
|
+
const path = (first.path ?? []).join(".") || "(body)";
|
|
54
|
+
return `${path}: ${first.message}`;
|
|
55
|
+
}
|
|
38
56
|
function isHttpBinding(b) {
|
|
39
57
|
return (typeof b === "object" &&
|
|
40
58
|
b !== null &&
|
|
@@ -62,12 +80,14 @@ export function httpKoa(config = {}) {
|
|
|
62
80
|
const koa = new Koa();
|
|
63
81
|
koaInstance = koa;
|
|
64
82
|
const isProd = process.env.NODE_ENV === "production";
|
|
83
|
+
const exposeErrors = config.exposeErrors === true;
|
|
65
84
|
// Top-level error mapper — catches throws from middleware and
|
|
66
|
-
// unmatched routes.
|
|
67
|
-
// {code, status, summary}
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
85
|
+
// unmatched routes. Typed `defineError` values render their own
|
|
86
|
+
// {code, status, summary}. Generic (non-NwireError) throws return an
|
|
87
|
+
// opaque "internal_error" by default so a stray
|
|
88
|
+
// `throw new Error("DB password X")` cannot leak the message to
|
|
89
|
+
// clients — in any environment. `exposeErrors: true` (dev) echoes
|
|
90
|
+
// the message. The real error is always logged server-side.
|
|
71
91
|
koa.use(async (kctx, next) => {
|
|
72
92
|
try {
|
|
73
93
|
await next();
|
|
@@ -75,21 +95,27 @@ export function httpKoa(config = {}) {
|
|
|
75
95
|
catch (err) {
|
|
76
96
|
const e = err;
|
|
77
97
|
const isNwireError = e?.$kind === "error";
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
98
|
+
if (isZodError(err)) {
|
|
99
|
+
// A schema `.parse()` that escaped the per-route catch (e.g. an
|
|
100
|
+
// action's own input schema). Validation failures are the
|
|
101
|
+
// caller's fault → 400 with a readable summary, never an opaque
|
|
102
|
+
// 500. The message is about their input, so it's safe to expose.
|
|
103
|
+
kctx.status = 400;
|
|
104
|
+
kctx.body = { error: { code: "validation_failed", summary: zodSummary(err) } };
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
kctx.status = typeof e.status === "number" ? e.status : 500;
|
|
108
|
+
if (isNwireError) {
|
|
84
109
|
kctx.body = {
|
|
85
|
-
error: { code: "internal_error", summary: "Internal error" },
|
|
110
|
+
error: { code: e.code ?? "internal_error", summary: e.summary ?? "Internal error" },
|
|
86
111
|
};
|
|
87
112
|
}
|
|
88
113
|
else {
|
|
114
|
+
logger.error?.(`[http-koa] unhandled error: ${e?.message ?? e}`, undefined);
|
|
89
115
|
kctx.body = {
|
|
90
116
|
error: {
|
|
91
|
-
code:
|
|
92
|
-
summary:
|
|
117
|
+
code: "internal_error",
|
|
118
|
+
summary: exposeErrors ? (e.message ?? "Internal error") : "Internal error",
|
|
93
119
|
},
|
|
94
120
|
};
|
|
95
121
|
}
|
|
@@ -250,7 +276,7 @@ export function httpKoa(config = {}) {
|
|
|
250
276
|
kctx.body = {
|
|
251
277
|
error: {
|
|
252
278
|
code: "validation_failed",
|
|
253
|
-
summary: err.message,
|
|
279
|
+
summary: isZodError(err) ? zodSummary(err) : err.message,
|
|
254
280
|
},
|
|
255
281
|
};
|
|
256
282
|
return;
|
|
@@ -275,7 +301,12 @@ export function httpKoa(config = {}) {
|
|
|
275
301
|
: (kc.request.headers["x-tenant"] ?? undefined));
|
|
276
302
|
const envelopePartial = {
|
|
277
303
|
tenant: resolveTenant(kctx),
|
|
278
|
-
|
|
304
|
+
// Prefer an explicit `state.userId`, but fall back to the
|
|
305
|
+
// user object's id so handlers reading `envelope.userId` work
|
|
306
|
+
// whenever auth middleware populated `state.user`.
|
|
307
|
+
userId: kctx.state.userId ??
|
|
308
|
+
kctx.state.user?.id ??
|
|
309
|
+
undefined,
|
|
279
310
|
// Pass the full user object through when adopter auth middleware
|
|
280
311
|
// populated it. Without this, transport-agnostic RBAC has to
|
|
281
312
|
// reach into `ctx.koa.state.user` and couples the handler to
|
|
@@ -309,25 +340,26 @@ export function httpKoa(config = {}) {
|
|
|
309
340
|
}
|
|
310
341
|
}
|
|
311
342
|
catch (err) {
|
|
312
|
-
// Generic error envelope.
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
343
|
+
// Generic error envelope. Typed `defineError` values surface
|
|
344
|
+
// their {code, status, summary}. Other throws return an opaque
|
|
345
|
+
// "internal_error" by default so a stray
|
|
346
|
+
// `throw new Error("DB password X")` cannot leak the message to
|
|
347
|
+
// clients — in any environment. `exposeErrors: true` (dev)
|
|
348
|
+
// echoes the message. The real error is always logged.
|
|
317
349
|
const e = err;
|
|
318
350
|
const isNwireError = e?.$kind === "error";
|
|
319
351
|
kctx.status = typeof e.status === "number" ? e.status : 500;
|
|
320
|
-
if (
|
|
321
|
-
logger.error?.(`[http-koa] unhandled error: ${e?.message ?? e}`, undefined);
|
|
352
|
+
if (isNwireError) {
|
|
322
353
|
kctx.body = {
|
|
323
|
-
error: { code: "internal_error", summary: "Internal error" },
|
|
354
|
+
error: { code: e.code ?? "internal_error", summary: e.summary ?? "Internal error" },
|
|
324
355
|
};
|
|
325
356
|
}
|
|
326
357
|
else {
|
|
358
|
+
logger.error?.(`[http-koa] unhandled error: ${e?.message ?? e}`, undefined);
|
|
327
359
|
kctx.body = {
|
|
328
360
|
error: {
|
|
329
|
-
code:
|
|
330
|
-
summary:
|
|
361
|
+
code: "internal_error",
|
|
362
|
+
summary: exposeErrors ? (e.message ?? "Internal error") : "Internal error",
|
|
331
363
|
},
|
|
332
364
|
};
|
|
333
365
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nwire/koa",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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/
|
|
37
|
-
"@nwire/
|
|
38
|
-
"@nwire/logger": "0.
|
|
39
|
-
"@nwire/
|
|
36
|
+
"@nwire/endpoint": "0.12.1",
|
|
37
|
+
"@nwire/wires": "0.12.1",
|
|
38
|
+
"@nwire/logger": "0.12.1",
|
|
39
|
+
"@nwire/container": "0.12.1"
|
|
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.
|
|
51
|
-
"@nwire/handler": "0.
|
|
50
|
+
"@nwire/app": "0.12.1",
|
|
51
|
+
"@nwire/handler": "0.12.1"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@asteasolutions/zod-to-openapi": "^8.0.0"
|