@blamejs/core 0.4.18 → 0.4.19

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/CHANGELOG.md CHANGED
@@ -8,6 +8,7 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.4.x
10
10
 
11
+ - **0.4.18** (2026-04-30) — cookieJar forensic-test strengthening (real crypto, replay, nonce)
11
12
  - **0.4.17** (2026-04-30) — b.httpClient.cookieJar (encrypted) + wiki catch-up sweep
12
13
  - **0.4.16** (2026-04-30) — b.httpClient: interceptors + progress events
13
14
  - **0.4.15** (2026-04-30) — b.httpClient: redirect-following + outbound multipart
package/lib/router.js CHANGED
@@ -30,6 +30,114 @@ var { boot } = require("./log");
30
30
 
31
31
  var log = boot("router");
32
32
 
33
+ // ---- Schema-spec helpers (route-level body/query/params validation) ----
34
+
35
+ var ALLOWED_SPEC_KEYS = [
36
+ "body", "query", "params", "response",
37
+ "bodyJsonSchema", "queryJsonSchema", "paramsJsonSchema", "responseJsonSchema",
38
+ "description", "summary", "tags", "validateResponse",
39
+ ];
40
+
41
+ function _validateRouteSpec(spec, method, pattern) {
42
+ var keys = Object.keys(spec);
43
+ for (var i = 0; i < keys.length; i++) {
44
+ if (ALLOWED_SPEC_KEYS.indexOf(keys[i]) === -1) {
45
+ throw new Error("router." + method.toLowerCase() + "(" + pattern +
46
+ "): unknown spec key '" + keys[i] + "'. Allowed: " +
47
+ ALLOWED_SPEC_KEYS.slice().sort().join(", "));
48
+ }
49
+ }
50
+ function _checkSchema(name) {
51
+ var s = spec[name];
52
+ if (s === undefined) return;
53
+ if (!s || typeof s !== "object" || typeof s.safeParse !== "function") {
54
+ throw new Error("router." + method.toLowerCase() + "(" + pattern +
55
+ "): spec." + name + " must be a b.safeSchema-shaped schema (with safeParse)");
56
+ }
57
+ }
58
+ _checkSchema("body");
59
+ _checkSchema("query");
60
+ _checkSchema("params");
61
+ _checkSchema("response");
62
+ if (spec.tags !== undefined) {
63
+ if (!Array.isArray(spec.tags) || !spec.tags.every(function (t) { return typeof t === "string"; })) {
64
+ throw new Error("router." + method.toLowerCase() + "(" + pattern +
65
+ "): spec.tags must be an array of strings");
66
+ }
67
+ }
68
+ }
69
+
70
+ function _writeValidationError(res, where, errors) {
71
+ if (res.writableEnded || res.headersSent) return;
72
+ var body = JSON.stringify({
73
+ error: "validation",
74
+ where: where,
75
+ issues: errors,
76
+ });
77
+ res.writeHead(400, {
78
+ "Content-Type": "application/json; charset=utf-8",
79
+ "Content-Length": Buffer.byteLength(body),
80
+ });
81
+ res.end(body);
82
+ }
83
+
84
+ function _makeSchemaValidator(spec) {
85
+ // 3-arg signature → router treats as middleware, chains via next().
86
+ return function schemaValidator(req, res, next) {
87
+ if (spec.params && req.params !== undefined) {
88
+ var pp = spec.params.safeParse(req.params);
89
+ if (!pp.ok) return _writeValidationError(res, "params", pp.errors);
90
+ req.params = pp.value;
91
+ }
92
+ if (spec.query && req.query !== undefined) {
93
+ var qq = spec.query.safeParse(req.query);
94
+ if (!qq.ok) return _writeValidationError(res, "query", qq.errors);
95
+ req.query = qq.value;
96
+ }
97
+ if (spec.body && req.body !== undefined) {
98
+ var bb = spec.body.safeParse(req.body);
99
+ if (!bb.ok) return _writeValidationError(res, "body", bb.errors);
100
+ req.body = bb.value;
101
+ }
102
+ next();
103
+ };
104
+ }
105
+
106
+ function _makeResponseValidator(spec) {
107
+ // Wraps res.json (and res.end when called with a JSON-shaped buffer)
108
+ // to validate the response body against spec.response. Mode:
109
+ // - BLAMEJS_VALIDATE_RESPONSES=throw (or per-route validateResponse: "throw")
110
+ // → throw a SafeSchemaError-shaped error; route handler's caller sees a 500.
111
+ // - BLAMEJS_VALIDATE_RESPONSES=warn (or per-route validateResponse: "warn")
112
+ // → log a warning; ship the response as-is (prod-safe).
113
+ var perRoute = spec.validateResponse;
114
+ var globalMode = process.env.BLAMEJS_VALIDATE_RESPONSES;
115
+ var mode = (perRoute === "throw" || perRoute === "warn") ? perRoute :
116
+ (globalMode === "throw" || globalMode === "warn") ? globalMode : null;
117
+ if (!mode) return function passthrough(_req, _res, next) { next(); };
118
+
119
+ return function responseValidator(req, res, next) {
120
+ var origJson = typeof res.json === "function" ? res.json.bind(res) : null;
121
+ if (origJson) {
122
+ res.json = function (value) {
123
+ var rr = spec.response.safeParse(value);
124
+ if (!rr.ok) {
125
+ if (mode === "throw") {
126
+ throw new Error("router response-validation failed for " +
127
+ (req.method + " " + req.routePattern) + ": " +
128
+ JSON.stringify(rr.errors));
129
+ }
130
+ // warn mode
131
+ log.warn("response-validation drift on " + req.method + " " + req.routePattern +
132
+ ": " + JSON.stringify(rr.errors).slice(0, 500));
133
+ }
134
+ return origJson(value);
135
+ };
136
+ }
137
+ next();
138
+ };
139
+ }
140
+
33
141
  function compilePattern(pattern) {
34
142
  var keys = [];
35
143
  var regexStr = pattern
@@ -128,24 +236,154 @@ class Router {
128
236
  this.middleware.push(fn);
129
237
  }
130
238
 
131
- get(pattern, ...handlers) {
132
- this.routes.push({ method: "GET", ...compilePattern(pattern), handlers });
239
+ // Internal: split a route registration's args into { spec, handlers }.
240
+ // The first non-pattern arg is the schema spec when it's a plain object
241
+ // (not a function); subsequent args are handler middlewares. Operators
242
+ // who never pass a spec keep the existing two-arg shape working.
243
+ _splitArgs(args) {
244
+ if (args.length > 0 && args[0] && typeof args[0] === "object" &&
245
+ !Array.isArray(args[0]) && typeof args[0] !== "function") {
246
+ return { spec: args[0], handlers: args.slice(1) };
247
+ }
248
+ return { spec: null, handlers: args };
249
+ }
250
+
251
+ _registerRoute(method, pattern, args) {
252
+ var split = this._splitArgs(args);
253
+ if (split.spec) _validateRouteSpec(split.spec, method, pattern);
254
+ var handlers = split.handlers;
255
+ if (split.spec) {
256
+ // Pre-handler validates body / query / params. Runs after the
257
+ // global middleware chain (bodyParser populates req.body before
258
+ // route dispatch) but before any route-specific handler.
259
+ handlers = [_makeSchemaValidator(split.spec)].concat(handlers);
260
+ // Response validation (dev/opt-in via env or per-route opt).
261
+ if (split.spec.response &&
262
+ (process.env.BLAMEJS_VALIDATE_RESPONSES === "throw" ||
263
+ process.env.BLAMEJS_VALIDATE_RESPONSES === "warn" ||
264
+ split.spec.validateResponse)) {
265
+ handlers = [_makeResponseValidator(split.spec)].concat(handlers);
266
+ }
267
+ }
268
+ this.routes.push(Object.assign(
269
+ { method: method, handlers: handlers, spec: split.spec || null },
270
+ compilePattern(pattern)
271
+ ));
272
+ }
273
+
274
+ get(pattern, ...args) {
275
+ this._registerRoute("GET", pattern, args);
276
+ }
277
+
278
+ post(pattern, ...args) {
279
+ this._registerRoute("POST", pattern, args);
280
+ }
281
+
282
+ put(pattern, ...args) {
283
+ this._registerRoute("PUT", pattern, args);
133
284
  }
134
285
 
135
- post(pattern, ...handlers) {
136
- this.routes.push({ method: "POST", ...compilePattern(pattern), handlers });
286
+ patch(pattern, ...args) {
287
+ this._registerRoute("PATCH", pattern, args);
137
288
  }
138
289
 
139
- put(pattern, ...handlers) {
140
- this.routes.push({ method: "PUT", ...compilePattern(pattern), handlers });
290
+ delete(pattern, ...args) {
291
+ this._registerRoute("DELETE", pattern, args);
141
292
  }
142
293
 
143
- patch(pattern, ...handlers) {
144
- this.routes.push({ method: "PATCH", ...compilePattern(pattern), handlers });
294
+ // Operator-facing introspection — returns a copy of the route table
295
+ // with each entry's method, pattern, description, and (when provided)
296
+ // operator-supplied jsonSchema bodies for OpenAPI publication.
297
+ inspectRoutes() {
298
+ return this.routes
299
+ .filter(function (r) { return typeof r.method === "string"; })
300
+ .map(function (r) {
301
+ return {
302
+ method: r.method,
303
+ pattern: r.pattern,
304
+ description: r.spec ? r.spec.description || null : null,
305
+ spec: r.spec ? {
306
+ hasBodySchema: !!r.spec.body,
307
+ hasQuerySchema: !!r.spec.query,
308
+ hasParamsSchema: !!r.spec.params,
309
+ hasResponseSchema: !!r.spec.response,
310
+ bodyJsonSchema: r.spec.bodyJsonSchema || null,
311
+ queryJsonSchema: r.spec.queryJsonSchema || null,
312
+ paramsJsonSchema: r.spec.paramsJsonSchema || null,
313
+ responseJsonSchema: r.spec.responseJsonSchema || null,
314
+ tags: Array.isArray(r.spec.tags) ? r.spec.tags.slice() : [],
315
+ summary: r.spec.summary || null,
316
+ } : null,
317
+ };
318
+ });
145
319
  }
146
320
 
147
- delete(pattern, ...handlers) {
148
- this.routes.push({ method: "DELETE", ...compilePattern(pattern), handlers });
321
+ // openapi(opts?) → minimal Swagger 3.0 document covering every
322
+ // schema-spec'd route. Body / query / params show up as parameter
323
+ // entries when the operator supplies bodyJsonSchema / etc. (a
324
+ // safeSchema → JSON Schema converter is its own primitive — operators
325
+ // who want full schema bodies in the OpenAPI doc supply the JSON
326
+ // Schema alongside the safeSchema today).
327
+ openapi(opts) {
328
+ opts = opts || {};
329
+ var info = opts.info || { title: "blamejs app", version: "0.0.0" };
330
+ var paths = {};
331
+ var routes = this.inspectRoutes();
332
+ for (var i = 0; i < routes.length; i++) {
333
+ var r = routes[i];
334
+ var openapiPath = r.pattern.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
335
+ if (!paths[openapiPath]) paths[openapiPath] = {};
336
+ var op = {
337
+ summary: r.spec ? r.spec.summary || r.description || (r.method + " " + r.pattern) :
338
+ (r.method + " " + r.pattern),
339
+ description: r.description || null,
340
+ };
341
+ if (r.spec) {
342
+ op.tags = r.spec.tags;
343
+ var params = [];
344
+ // Path params from pattern
345
+ var pathParams = (r.pattern.match(/:[a-zA-Z0-9_]+/g) || [])
346
+ .map(function (s) { return s.slice(1); });
347
+ for (var pp = 0; pp < pathParams.length; pp++) {
348
+ params.push({ name: pathParams[pp], in: "path", required: true,
349
+ schema: { type: "string" } });
350
+ }
351
+ if (r.spec.queryJsonSchema && r.spec.queryJsonSchema.properties) {
352
+ var qprops = r.spec.queryJsonSchema.properties;
353
+ var qreq = r.spec.queryJsonSchema.required || [];
354
+ var qkeys = Object.keys(qprops);
355
+ for (var qi = 0; qi < qkeys.length; qi++) {
356
+ params.push({ name: qkeys[qi], in: "query",
357
+ required: qreq.indexOf(qkeys[qi]) !== -1,
358
+ schema: qprops[qkeys[qi]] });
359
+ }
360
+ }
361
+ if (params.length > 0) op.parameters = params;
362
+ if (r.spec.bodyJsonSchema) {
363
+ op.requestBody = {
364
+ required: true,
365
+ content: { "application/json": { schema: r.spec.bodyJsonSchema } },
366
+ };
367
+ } else if (r.spec.hasBodySchema) {
368
+ // Operator validates via safeSchema but didn't supply JSON Schema.
369
+ op["x-blamejs-body-validation"] = "safe-schema (json schema not provided)";
370
+ }
371
+ if (r.spec.responseJsonSchema) {
372
+ op.responses = {
373
+ "200": {
374
+ description: "OK",
375
+ content: { "application/json": { schema: r.spec.responseJsonSchema } },
376
+ },
377
+ };
378
+ }
379
+ }
380
+ paths[openapiPath][r.method.toLowerCase()] = op;
381
+ }
382
+ return {
383
+ openapi: "3.0.3",
384
+ info: info,
385
+ paths: paths,
386
+ };
149
387
  }
150
388
 
151
389
  // ---- WebSocket route registration ----
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",