@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 +1 -0
- package/lib/router.js +248 -10
- package/package.json +1 -1
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
this.
|
|
286
|
+
patch(pattern, ...args) {
|
|
287
|
+
this._registerRoute("PATCH", pattern, args);
|
|
137
288
|
}
|
|
138
289
|
|
|
139
|
-
|
|
140
|
-
this.
|
|
290
|
+
delete(pattern, ...args) {
|
|
291
|
+
this._registerRoute("DELETE", pattern, args);
|
|
141
292
|
}
|
|
142
293
|
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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 ----
|