@blamejs/core 0.8.82 → 0.8.86
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 +4 -0
- package/README.md +1 -1
- package/index.js +6 -0
- package/lib/a2a-tasks.js +598 -0
- package/lib/a2a.js +10 -0
- package/lib/acme.js +189 -5
- package/lib/audit.js +1 -0
- package/lib/cache-status.js +288 -0
- package/lib/compliance.js +36 -0
- package/lib/framework-error.js +19 -0
- package/lib/mcp-tool-registry.js +473 -0
- package/lib/mcp.js +3 -0
- package/lib/middleware/idempotency-key.js +424 -0
- package/lib/middleware/index.js +10 -0
- package/lib/middleware/no-cache.js +106 -0
- package/lib/problem-details.js +439 -0
- package/lib/server-timing.js +174 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.problemDetails
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title Problem Details
|
|
6
|
+
* @order 300
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* RFC 9457 Problem Details for HTTP APIs — standardized error
|
|
10
|
+
* response envelope with `type` / `title` / `status` / `detail` /
|
|
11
|
+
* `instance` plus operator-supplied extensions. Sets
|
|
12
|
+
* `Content-Type: application/problem+json` per RFC 9457 §3 so
|
|
13
|
+
* clients can branch on the content type rather than scanning ad-
|
|
14
|
+
* hoc JSON shapes. Supersedes RFC 7807 (obsolete).
|
|
15
|
+
*
|
|
16
|
+
* Operators wire this in three places:
|
|
17
|
+
* 1. `b.problemDetails.create({...})` builds a problem object
|
|
18
|
+
* with field validation (type URI, status range, etc.).
|
|
19
|
+
* 2. `b.problemDetails.respond(res, problem)` serializes + sends.
|
|
20
|
+
* 3. `b.problemDetails.fromError(err)` converts a thrown
|
|
21
|
+
* `FrameworkError` into the matching problem document; the
|
|
22
|
+
* `err.code` (e.g. `csv/invalid-record`) becomes the
|
|
23
|
+
* `type` URI suffix (`https://blamejs.com/problems/csv/
|
|
24
|
+
* invalid-record`).
|
|
25
|
+
*
|
|
26
|
+
* `b.problemDetails.validate(doc)` parses an INBOUND problem
|
|
27
|
+
* response (e.g. when blamejs is the client of an upstream API
|
|
28
|
+
* returning RFC 9457) — refuses non-objects, refuses bad type
|
|
29
|
+
* URIs, refuses status outside 100..599.
|
|
30
|
+
*
|
|
31
|
+
* Tier choices per `feedback_validation_tier_policy.md`:
|
|
32
|
+
* - create / fromError / validate — THROW on bad input
|
|
33
|
+
* (config-time / entry-point shape).
|
|
34
|
+
* - respond — THROW on bad input (its first call sets the
|
|
35
|
+
* response shape; silent drop would mask a programming bug).
|
|
36
|
+
*
|
|
37
|
+
* @card
|
|
38
|
+
* RFC 9457 Problem Details for HTTP APIs — standardized error response envelope with `application/problem+json` content type, supersedes RFC 7807.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
var pick = require("./pick");
|
|
42
|
+
var validateOpts = require("./validate-opts");
|
|
43
|
+
var { defineClass } = require("./framework-error");
|
|
44
|
+
|
|
45
|
+
var POISONED_KEYS = pick.POISONED_KEYS;
|
|
46
|
+
|
|
47
|
+
var ProblemDetailsError = defineClass("ProblemDetailsError", { alwaysPermanent: true });
|
|
48
|
+
|
|
49
|
+
// Default problem-type URI base. Operators override via
|
|
50
|
+
// `b.problemDetails.setBase(url)` so deployments running under a
|
|
51
|
+
// custom domain emit problem URIs at their own host.
|
|
52
|
+
var STATE = {
|
|
53
|
+
baseUri: "https://blamejs.com/problems",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// RFC 9457 §3 reserved field names. Extensions land at the top level
|
|
57
|
+
// alongside these — RFC 9457 §3.2 explicitly endorses sibling
|
|
58
|
+
// extensions ("members SHOULD have unique names within their parent
|
|
59
|
+
// object"). Reserved names are NOT allowed as extension keys —
|
|
60
|
+
// re-using them silently masks a programming bug.
|
|
61
|
+
var RESERVED_FIELDS = Object.freeze([
|
|
62
|
+
"type", "title", "status", "detail", "instance",
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @primitive b.problemDetails.setBase
|
|
67
|
+
* @signature b.problemDetails.setBase(baseUri)
|
|
68
|
+
* @since 0.8.84
|
|
69
|
+
* @status stable
|
|
70
|
+
* @related b.problemDetails.create, b.problemDetails.fromError
|
|
71
|
+
*
|
|
72
|
+
* Override the base URI prepended to error-code-derived `type` URIs.
|
|
73
|
+
* Default is `https://blamejs.com/problems`. Operators running under
|
|
74
|
+
* their own published vocabulary (e.g. an internal status page or
|
|
75
|
+
* an organization-owned problem catalog) point this at the canonical
|
|
76
|
+
* location. Throws `problem-details/bad-base` for non-https / non-
|
|
77
|
+
* absolute / non-string inputs.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* b.problemDetails.setBase("https://api.example.com/problems");
|
|
81
|
+
* b.problemDetails.fromError(new Error("csv/invalid-record")).type;
|
|
82
|
+
* // → "https://api.example.com/problems/csv/invalid-record"
|
|
83
|
+
*/
|
|
84
|
+
function setBase(baseUri) {
|
|
85
|
+
validateOpts.requireNonEmptyString(
|
|
86
|
+
baseUri, "setBase.baseUri", ProblemDetailsError, "problem-details/bad-base");
|
|
87
|
+
if (!/^https?:\/\//.test(baseUri)) {
|
|
88
|
+
throw new ProblemDetailsError("problem-details/bad-base",
|
|
89
|
+
"setBase: baseUri must be an absolute http(s) URL", true);
|
|
90
|
+
}
|
|
91
|
+
// Strip trailing slash so the create()-path can prefix with "/".
|
|
92
|
+
// Manual loop instead of /\/+$/ regex — CodeQL flags the latter as
|
|
93
|
+
// a polynomial-time ReDoS candidate on operator-supplied input even
|
|
94
|
+
// though `/+` against an anchored tail is genuinely O(n); manual
|
|
95
|
+
// strip moots the warning + keeps O(n) explicit.
|
|
96
|
+
var bu = baseUri;
|
|
97
|
+
while (bu.length > 0 && bu.charAt(bu.length - 1) === "/") {
|
|
98
|
+
bu = bu.slice(0, -1);
|
|
99
|
+
}
|
|
100
|
+
STATE.baseUri = bu;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @primitive b.problemDetails.getBase
|
|
105
|
+
* @signature b.problemDetails.getBase()
|
|
106
|
+
* @since 0.8.84
|
|
107
|
+
* @status stable
|
|
108
|
+
* @related b.problemDetails.setBase
|
|
109
|
+
*
|
|
110
|
+
* Read the currently-configured base URI. Useful for diagnostic
|
|
111
|
+
* logging and tests.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* b.problemDetails.getBase(); // → "https://blamejs.com/problems"
|
|
115
|
+
*/
|
|
116
|
+
function getBase() {
|
|
117
|
+
return STATE.baseUri;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @primitive b.problemDetails.create
|
|
122
|
+
* @signature b.problemDetails.create(opts)
|
|
123
|
+
* @since 0.8.84
|
|
124
|
+
* @status stable
|
|
125
|
+
* @related b.problemDetails.respond, b.problemDetails.fromError, b.problemDetails.validate
|
|
126
|
+
*
|
|
127
|
+
* Build a frozen RFC 9457 problem-details object. Validates the
|
|
128
|
+
* standard fields per §3:
|
|
129
|
+
* - `type` (optional, defaults to `"about:blank"`) must be a URI
|
|
130
|
+
* reference (string); MAY be relative or absolute.
|
|
131
|
+
* - `title` (recommended) must be a non-empty string when given.
|
|
132
|
+
* - `status` (recommended) must be an integer 100..599.
|
|
133
|
+
* - `detail` (optional) must be a string when given.
|
|
134
|
+
* - `instance` (optional) must be a URI reference string when given.
|
|
135
|
+
* - Extensions: every additional key whose name is NOT in
|
|
136
|
+
* `RESERVED_FIELDS` is preserved at the top level. Reserved-name
|
|
137
|
+
* collisions throw `problem-details/reserved-extension`.
|
|
138
|
+
*
|
|
139
|
+
* Returns a frozen plain object suitable for `JSON.stringify`.
|
|
140
|
+
*
|
|
141
|
+
* @opts
|
|
142
|
+
* type: string, // problem-type URI reference (default "about:blank")
|
|
143
|
+
* title: string, // short summary
|
|
144
|
+
* status: number, // integer 100..599
|
|
145
|
+
* detail: string, // human-readable explanation
|
|
146
|
+
* instance: string, // URI reference for this specific occurrence
|
|
147
|
+
* ...extensions // additional fields preserved as-is
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* var p = b.problemDetails.create({
|
|
151
|
+
* type: "https://example.com/problems/out-of-credit",
|
|
152
|
+
* title: "You do not have enough credit.",
|
|
153
|
+
* status: 403,
|
|
154
|
+
* detail: "Your current balance is 30, but that costs 50.",
|
|
155
|
+
* instance: "/account/12345/msgs/abc",
|
|
156
|
+
* balance: 30,
|
|
157
|
+
* accounts: ["/account/12345", "/account/67890"],
|
|
158
|
+
* });
|
|
159
|
+
* // → {
|
|
160
|
+
* // type: "https://example.com/problems/out-of-credit",
|
|
161
|
+
* // title: "You do not have enough credit.",
|
|
162
|
+
* // status: 403,
|
|
163
|
+
* // detail: "Your current balance is 30, but that costs 50.",
|
|
164
|
+
* // instance: "/account/12345/msgs/abc",
|
|
165
|
+
* // balance: 30,
|
|
166
|
+
* // accounts: ["/account/12345", "/account/67890"]
|
|
167
|
+
* // }
|
|
168
|
+
*/
|
|
169
|
+
function create(opts) {
|
|
170
|
+
if (!opts || typeof opts !== "object" || Array.isArray(opts)) {
|
|
171
|
+
throw new ProblemDetailsError("problem-details/bad-opts",
|
|
172
|
+
"create: opts must be a non-null object", true);
|
|
173
|
+
}
|
|
174
|
+
var out = {};
|
|
175
|
+
|
|
176
|
+
// type (RFC 9457 §3.1.1 — default "about:blank")
|
|
177
|
+
var typeIn = validateOpts.optionalNonEmptyString(
|
|
178
|
+
opts.type, "create.type", ProblemDetailsError, "problem-details/bad-type");
|
|
179
|
+
out.type = (typeIn === undefined || typeIn === null) ? "about:blank" : typeIn;
|
|
180
|
+
|
|
181
|
+
// title (§3.1.2 — short, human-readable summary)
|
|
182
|
+
var titleIn = validateOpts.optionalNonEmptyString(
|
|
183
|
+
opts.title, "create.title", ProblemDetailsError, "problem-details/bad-title");
|
|
184
|
+
if (titleIn !== undefined && titleIn !== null) {
|
|
185
|
+
out.title = titleIn;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// status (§3.1.3 — integer 100..599)
|
|
189
|
+
if (opts.status !== undefined) {
|
|
190
|
+
if (typeof opts.status !== "number" || !Number.isInteger(opts.status) ||
|
|
191
|
+
opts.status < 100 || opts.status > 599) { // allow:raw-byte-literal — HTTP status range bounds
|
|
192
|
+
throw new ProblemDetailsError("problem-details/bad-status",
|
|
193
|
+
"create: status must be an integer 100..599 when provided", true);
|
|
194
|
+
}
|
|
195
|
+
out.status = opts.status;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// detail (§3.1.4)
|
|
199
|
+
if (opts.detail !== undefined) {
|
|
200
|
+
if (typeof opts.detail !== "string") {
|
|
201
|
+
throw new ProblemDetailsError("problem-details/bad-detail",
|
|
202
|
+
"create: detail must be a string when provided", true);
|
|
203
|
+
}
|
|
204
|
+
out.detail = opts.detail;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// instance (§3.1.5 — URI reference)
|
|
208
|
+
var instanceIn = validateOpts.optionalNonEmptyString(
|
|
209
|
+
opts.instance, "create.instance", ProblemDetailsError, "problem-details/bad-instance");
|
|
210
|
+
if (instanceIn !== undefined && instanceIn !== null) {
|
|
211
|
+
out.instance = instanceIn;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Extensions — every additional key. §3.2 endorses sibling
|
|
215
|
+
// extensions as long as their names don't collide with reserved.
|
|
216
|
+
var keys = Object.keys(opts);
|
|
217
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
218
|
+
var k = keys[i];
|
|
219
|
+
if (RESERVED_FIELDS.indexOf(k) !== -1) continue;
|
|
220
|
+
if (POISONED_KEYS.indexOf(k) !== -1) {
|
|
221
|
+
throw new ProblemDetailsError("problem-details/reserved-extension",
|
|
222
|
+
"create: extension key '" + k + "' refused (prototype-pollution shape)", true);
|
|
223
|
+
}
|
|
224
|
+
out[k] = opts[k];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return Object.freeze(out);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @primitive b.problemDetails.fromError
|
|
232
|
+
* @signature b.problemDetails.fromError(err, opts?)
|
|
233
|
+
* @since 0.8.84
|
|
234
|
+
* @status stable
|
|
235
|
+
* @related b.problemDetails.create, b.problemDetails.respond
|
|
236
|
+
*
|
|
237
|
+
* Convert a thrown `FrameworkError` (or any error with a `code` field)
|
|
238
|
+
* into the matching problem-details object. The error's `code`
|
|
239
|
+
* (e.g. `csv/invalid-record`) becomes the type-URI suffix
|
|
240
|
+
* (`<baseUri>/csv/invalid-record`); the error's `message` becomes
|
|
241
|
+
* `detail`; the error's `statusCode` becomes `status` when present,
|
|
242
|
+
* otherwise defaults to 500. Pass `opts.title` to override the title
|
|
243
|
+
* default (which is the error class name humanized: `CsvError` →
|
|
244
|
+
* "CSV Error"). Pass `opts.instance` to attach a request-instance
|
|
245
|
+
* reference (typically the audit-trail ID).
|
|
246
|
+
*
|
|
247
|
+
* @opts
|
|
248
|
+
* title: string, // override the derived title
|
|
249
|
+
* instance: string, // request-instance URI reference
|
|
250
|
+
* status: number, // override err.statusCode / default 500
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* try {
|
|
254
|
+
* b.csv.parse(badInput);
|
|
255
|
+
* } catch (err) {
|
|
256
|
+
* var problem = b.problemDetails.fromError(err, {
|
|
257
|
+
* instance: "/audit/" + req.auditId,
|
|
258
|
+
* });
|
|
259
|
+
* b.problemDetails.respond(res, problem);
|
|
260
|
+
* }
|
|
261
|
+
*/
|
|
262
|
+
function fromError(err, opts2) {
|
|
263
|
+
if (!err || typeof err !== "object") {
|
|
264
|
+
throw new ProblemDetailsError("problem-details/bad-error",
|
|
265
|
+
"fromError: err must be a non-null object", true);
|
|
266
|
+
}
|
|
267
|
+
opts2 = opts2 || {};
|
|
268
|
+
if (typeof opts2 !== "object" || Array.isArray(opts2)) {
|
|
269
|
+
throw new ProblemDetailsError("problem-details/bad-opts",
|
|
270
|
+
"fromError: opts must be an object when provided", true);
|
|
271
|
+
}
|
|
272
|
+
var code = typeof err.code === "string" && err.code.length > 0 ? err.code : "internal-error";
|
|
273
|
+
// Sanitize the code into a URI-safe path segment. RFC 3986
|
|
274
|
+
// unreserved chars + `/` for the namespace separator.
|
|
275
|
+
var typeUri = STATE.baseUri + "/" + code.replace(/[^A-Za-z0-9\-._/]/g, "-");
|
|
276
|
+
|
|
277
|
+
// Derived title: error.name humanized (CsvError → "Csv Error"),
|
|
278
|
+
// or operator override.
|
|
279
|
+
var title;
|
|
280
|
+
if (typeof opts2.title === "string" && opts2.title.length > 0) {
|
|
281
|
+
title = opts2.title;
|
|
282
|
+
} else if (typeof err.name === "string" && err.name.length > 0) {
|
|
283
|
+
title = err.name
|
|
284
|
+
.replace(/Error$/, " Error")
|
|
285
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
286
|
+
.trim();
|
|
287
|
+
} else {
|
|
288
|
+
title = "Error";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Status: opt override > err.statusCode > 500.
|
|
292
|
+
var status;
|
|
293
|
+
if (opts2.status !== undefined) {
|
|
294
|
+
status = opts2.status;
|
|
295
|
+
} else if (typeof err.statusCode === "number" && Number.isInteger(err.statusCode) &&
|
|
296
|
+
err.statusCode >= 100 && err.statusCode <= 599) { // allow:raw-byte-literal — HTTP status range
|
|
297
|
+
status = err.statusCode;
|
|
298
|
+
} else {
|
|
299
|
+
status = 500; // allow:raw-byte-literal — default HTTP status 500 (Internal Server Error)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
var built = {
|
|
303
|
+
type: typeUri,
|
|
304
|
+
title: title,
|
|
305
|
+
status: status,
|
|
306
|
+
detail: typeof err.message === "string" ? err.message : String(err),
|
|
307
|
+
};
|
|
308
|
+
if (typeof opts2.instance === "string" && opts2.instance.length > 0) {
|
|
309
|
+
built.instance = opts2.instance;
|
|
310
|
+
}
|
|
311
|
+
return create(built);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @primitive b.problemDetails.respond
|
|
316
|
+
* @signature b.problemDetails.respond(res, problem)
|
|
317
|
+
* @since 0.8.84
|
|
318
|
+
* @status stable
|
|
319
|
+
* @related b.problemDetails.create, b.problemDetails.fromError
|
|
320
|
+
*
|
|
321
|
+
* Write a problem-details object to the response with the correct
|
|
322
|
+
* RFC 9457 §3 content type (`application/problem+json`). Sets
|
|
323
|
+
* `Cache-Control: no-store` (RFC 9111 §5.2.2.5 — error responses
|
|
324
|
+
* are individualized) and writes the JSON body. Status code is
|
|
325
|
+
* taken from `problem.status` (or 500 when missing). Throws
|
|
326
|
+
* `problem-details/bad-res` for non-response objects; throws
|
|
327
|
+
* `problem-details/bad-problem` for non-object problem inputs.
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* var problem = b.problemDetails.create({
|
|
331
|
+
* type: "https://blamejs.com/problems/csv/invalid-record",
|
|
332
|
+
* title: "CSV record validation failed",
|
|
333
|
+
* status: 400,
|
|
334
|
+
* detail: "Row 3 column 5 has an unterminated quoted field",
|
|
335
|
+
* });
|
|
336
|
+
* b.problemDetails.respond(res, problem);
|
|
337
|
+
* // res.headers: Content-Type: application/problem+json
|
|
338
|
+
* // Cache-Control: no-store
|
|
339
|
+
* // res.body: <JSON-stringified problem>
|
|
340
|
+
* // res.statusCode: 400
|
|
341
|
+
*/
|
|
342
|
+
function respond(res, problem) {
|
|
343
|
+
if (!res || typeof res !== "object" || typeof res.setHeader !== "function" ||
|
|
344
|
+
typeof res.end !== "function") {
|
|
345
|
+
throw new ProblemDetailsError("problem-details/bad-res",
|
|
346
|
+
"respond: res must be an HTTP response object (setHeader + end)", true);
|
|
347
|
+
}
|
|
348
|
+
if (!problem || typeof problem !== "object" || Array.isArray(problem)) {
|
|
349
|
+
throw new ProblemDetailsError("problem-details/bad-problem",
|
|
350
|
+
"respond: problem must be a non-null object", true);
|
|
351
|
+
}
|
|
352
|
+
var status = (typeof problem.status === "number" && Number.isInteger(problem.status) &&
|
|
353
|
+
problem.status >= 100 && problem.status <= 599) ? problem.status : 500; // allow:raw-byte-literal — HTTP status range + default 500
|
|
354
|
+
var body = JSON.stringify(problem);
|
|
355
|
+
res.statusCode = status;
|
|
356
|
+
res.setHeader("Content-Type", "application/problem+json");
|
|
357
|
+
res.setHeader("Cache-Control", "no-store");
|
|
358
|
+
res.end(body);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* @primitive b.problemDetails.validate
|
|
363
|
+
* @signature b.problemDetails.validate(doc)
|
|
364
|
+
* @since 0.8.84
|
|
365
|
+
* @status stable
|
|
366
|
+
* @related b.problemDetails.create
|
|
367
|
+
*
|
|
368
|
+
* Validate an INBOUND problem-details document (e.g. one received
|
|
369
|
+
* from an upstream API). Returns the doc unchanged on success;
|
|
370
|
+
* throws `problem-details/bad-inbound` on shape violations. Useful
|
|
371
|
+
* when blamejs is the client of a RFC 9457-compliant upstream
|
|
372
|
+
* service — converts a "looks JSON-ish" response into a verified
|
|
373
|
+
* problem object before reading fields.
|
|
374
|
+
*
|
|
375
|
+
* - Refuses non-object input.
|
|
376
|
+
* - Refuses `status` outside 100..599 or non-integer.
|
|
377
|
+
* - Refuses `type` / `title` / `detail` / `instance` of non-string
|
|
378
|
+
* shape when present.
|
|
379
|
+
* - Refuses prototype-pollution-shaped extension keys.
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* var rsp = await fetch(url);
|
|
383
|
+
* if (rsp.headers.get("content-type") === "application/problem+json") {
|
|
384
|
+
* var doc = b.problemDetails.validate(await rsp.json());
|
|
385
|
+
* console.log(doc.title, doc.status, doc.detail);
|
|
386
|
+
* }
|
|
387
|
+
*/
|
|
388
|
+
function validate(doc) {
|
|
389
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
390
|
+
throw new ProblemDetailsError("problem-details/bad-inbound",
|
|
391
|
+
"validate: doc must be a non-null object", true);
|
|
392
|
+
}
|
|
393
|
+
validateOpts.optionalNonEmptyString(
|
|
394
|
+
doc.type, "validate.type", ProblemDetailsError, "problem-details/bad-inbound");
|
|
395
|
+
if (doc.title !== undefined && typeof doc.title !== "string") {
|
|
396
|
+
throw new ProblemDetailsError("problem-details/bad-inbound",
|
|
397
|
+
"validate: title must be a string when present", true);
|
|
398
|
+
}
|
|
399
|
+
if (doc.status !== undefined) {
|
|
400
|
+
if (typeof doc.status !== "number" || !Number.isInteger(doc.status) ||
|
|
401
|
+
doc.status < 100 || doc.status > 599) { // allow:raw-byte-literal — HTTP status range
|
|
402
|
+
throw new ProblemDetailsError("problem-details/bad-inbound",
|
|
403
|
+
"validate: status must be an integer 100..599 when present", true);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (doc.detail !== undefined && typeof doc.detail !== "string") {
|
|
407
|
+
throw new ProblemDetailsError("problem-details/bad-inbound",
|
|
408
|
+
"validate: detail must be a string when present", true);
|
|
409
|
+
}
|
|
410
|
+
validateOpts.optionalNonEmptyString(
|
|
411
|
+
doc.instance, "validate.instance", ProblemDetailsError, "problem-details/bad-inbound");
|
|
412
|
+
// Refuse prototype-pollution shape in keys.
|
|
413
|
+
var keys = Object.keys(doc);
|
|
414
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
415
|
+
if (POISONED_KEYS.indexOf(keys[i]) !== -1) {
|
|
416
|
+
throw new ProblemDetailsError("problem-details/bad-inbound",
|
|
417
|
+
"validate: doc key '" + keys[i] + "' refused (prototype-pollution shape)", true);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return doc;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Boundary helper for resetting test state between cases.
|
|
424
|
+
function _resetForTest() {
|
|
425
|
+
STATE.baseUri = "https://blamejs.com/problems";
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
module.exports = {
|
|
429
|
+
setBase: setBase,
|
|
430
|
+
getBase: getBase,
|
|
431
|
+
create: create,
|
|
432
|
+
fromError: fromError,
|
|
433
|
+
respond: respond,
|
|
434
|
+
validate: validate,
|
|
435
|
+
RESERVED_FIELDS: RESERVED_FIELDS,
|
|
436
|
+
ProblemDetailsError: ProblemDetailsError,
|
|
437
|
+
_resetForTest: _resetForTest,
|
|
438
|
+
};
|
|
439
|
+
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.serverTiming
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title Server-Timing
|
|
6
|
+
* @order 315
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* W3C Server-Timing response header builder. Lets the server
|
|
10
|
+
* describe per-request timing metrics (database query duration,
|
|
11
|
+
* downstream HTTP call latency, encryption time) so the browser's
|
|
12
|
+
* Performance API exposes them to client-side telemetry.
|
|
13
|
+
*
|
|
14
|
+
* The header is a comma-separated list of `name; dur=<ms>; desc=<text>`
|
|
15
|
+
* entries. Builder primitives are immutable per-request collectors
|
|
16
|
+
* that operators populate over the lifetime of the request and
|
|
17
|
+
* serialize at response-write time.
|
|
18
|
+
*
|
|
19
|
+
* `b.serverTiming.create()` returns a per-request collector:
|
|
20
|
+
*
|
|
21
|
+
* var timing = b.serverTiming.create();
|
|
22
|
+
* timing.mark("db.query", 12.5, "user fetch");
|
|
23
|
+
* timing.mark("encrypt", 3.1);
|
|
24
|
+
* res.setHeader("Server-Timing", timing.toHeader());
|
|
25
|
+
* // → "db.query; dur=12.5; desc=\"user fetch\", encrypt; dur=3.1"
|
|
26
|
+
*
|
|
27
|
+
* Use `timing.measure(name, fn)` to time an async operation
|
|
28
|
+
* inline:
|
|
29
|
+
*
|
|
30
|
+
* var rows = await timing.measure("db.query", function () {
|
|
31
|
+
* return db.query("SELECT ...");
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* @card
|
|
35
|
+
* W3C Server-Timing response header builder — per-request timing-metric collector that surfaces server-side latency in the browser's Performance API.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
var validateOpts = require("./validate-opts");
|
|
39
|
+
var { defineClass } = require("./framework-error");
|
|
40
|
+
|
|
41
|
+
var ServerTimingError = defineClass("ServerTimingError", { alwaysPermanent: true });
|
|
42
|
+
|
|
43
|
+
// W3C Server-Timing §3 — metric-name is token shape (RFC 7230). Cap
|
|
44
|
+
// at 128 chars for sanity; operator-supplied desc is sf-string.
|
|
45
|
+
var METRIC_NAME_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,128}$/; // allow:raw-byte-literal — RFC 7230 token shape + length cap
|
|
46
|
+
|
|
47
|
+
function _quoteDesc(s) {
|
|
48
|
+
return "\"" + String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @primitive b.serverTiming.create
|
|
53
|
+
* @signature b.serverTiming.create()
|
|
54
|
+
* @since 0.8.86
|
|
55
|
+
* @status stable
|
|
56
|
+
* @related b.serverTiming.entry
|
|
57
|
+
*
|
|
58
|
+
* Return a per-request collector with `mark` / `measure` / `toHeader`
|
|
59
|
+
* methods. The collector is mutable + scoped to a single request;
|
|
60
|
+
* operators discard or stringify at response-write time. Throws
|
|
61
|
+
* `server-timing/bad-name` for non-token metric names and
|
|
62
|
+
* `server-timing/bad-duration` for non-finite negative duration.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* var timing = b.serverTiming.create();
|
|
66
|
+
* timing.mark("cache.lookup", 0.3);
|
|
67
|
+
* var data = await timing.measure("db.query", function () { return db.query("..."); });
|
|
68
|
+
* res.setHeader("Server-Timing", timing.toHeader());
|
|
69
|
+
*/
|
|
70
|
+
function create() {
|
|
71
|
+
var entries = [];
|
|
72
|
+
|
|
73
|
+
function mark(name, durationMs, description) {
|
|
74
|
+
validateOpts.requireNonEmptyString(
|
|
75
|
+
name, "serverTiming.mark.name", ServerTimingError, "server-timing/bad-name");
|
|
76
|
+
if (name.length > 128 || !METRIC_NAME_RE.test(name)) { // allow:raw-byte-literal — metric-name length cap, not bytes
|
|
77
|
+
throw new ServerTimingError("server-timing/bad-name",
|
|
78
|
+
"metric name '" + name + "' must match RFC 7230 token + <= 128 chars");
|
|
79
|
+
}
|
|
80
|
+
if (durationMs !== undefined && durationMs !== null) {
|
|
81
|
+
if (typeof durationMs !== "number" || !isFinite(durationMs) || durationMs < 0) {
|
|
82
|
+
throw new ServerTimingError("server-timing/bad-duration",
|
|
83
|
+
"duration must be a non-negative finite number when provided");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (description !== undefined && description !== null) {
|
|
87
|
+
if (typeof description !== "string") {
|
|
88
|
+
throw new ServerTimingError("server-timing/bad-description",
|
|
89
|
+
"description must be a string when provided");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
entries.push({
|
|
93
|
+
name: name,
|
|
94
|
+
dur: typeof durationMs === "number" ? durationMs : null,
|
|
95
|
+
desc: typeof description === "string" ? description : null,
|
|
96
|
+
});
|
|
97
|
+
return entries[entries.length - 1];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function measure(name, fn) {
|
|
101
|
+
if (typeof fn !== "function") {
|
|
102
|
+
throw new ServerTimingError("server-timing/bad-fn",
|
|
103
|
+
"measure: fn must be a function", true);
|
|
104
|
+
}
|
|
105
|
+
var start = _now();
|
|
106
|
+
try {
|
|
107
|
+
var result = await fn();
|
|
108
|
+
mark(name, _now() - start);
|
|
109
|
+
return result;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
mark(name, _now() - start, "error");
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function toHeader() {
|
|
117
|
+
if (entries.length === 0) return "";
|
|
118
|
+
return entries.map(function (e) {
|
|
119
|
+
var parts = [e.name];
|
|
120
|
+
if (e.dur !== null) parts.push("dur=" + _formatDur(e.dur));
|
|
121
|
+
if (e.desc !== null) parts.push("desc=" + _quoteDesc(e.desc));
|
|
122
|
+
return parts.join("; ");
|
|
123
|
+
}).join(", ");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function snapshot() {
|
|
127
|
+
return entries.map(function (e) { return Object.assign({}, e); });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { mark: mark, measure: measure, toHeader: toHeader, snapshot: snapshot };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @primitive b.serverTiming.entry
|
|
135
|
+
* @signature b.serverTiming.entry(name, durationMs?, description?)
|
|
136
|
+
* @since 0.8.86
|
|
137
|
+
* @status stable
|
|
138
|
+
* @related b.serverTiming.create
|
|
139
|
+
*
|
|
140
|
+
* Format a single Server-Timing entry without building a collector.
|
|
141
|
+
* Useful when the operator wants a one-shot header value without
|
|
142
|
+
* threading a collector through the request scope.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* res.setHeader("Server-Timing", b.serverTiming.entry("db.query", 12.5));
|
|
146
|
+
* // → "db.query; dur=12.5"
|
|
147
|
+
*/
|
|
148
|
+
function entryString(name, durationMs, description) {
|
|
149
|
+
var c = create();
|
|
150
|
+
c.mark(name, durationMs, description);
|
|
151
|
+
return c.toHeader();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _now() {
|
|
155
|
+
// Prefer process.hrtime when available (sub-ms precision); fall back
|
|
156
|
+
// to Date.now in environments without it.
|
|
157
|
+
if (typeof process !== "undefined" && typeof process.hrtime === "function" &&
|
|
158
|
+
typeof process.hrtime.bigint === "function") {
|
|
159
|
+
return Number(process.hrtime.bigint() / 1000n) / 1000; // allow:raw-byte-literal — hrtime ns→ms scale, not bytes
|
|
160
|
+
}
|
|
161
|
+
return Date.now();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function _formatDur(ms) {
|
|
165
|
+
// W3C spec says dur is a number; emit at most 3 decimal places.
|
|
166
|
+
if (Number.isInteger(ms)) return String(ms);
|
|
167
|
+
return ms.toFixed(3).replace(/\.?0+$/, "");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
create: create,
|
|
172
|
+
entry: entryString,
|
|
173
|
+
ServerTimingError: ServerTimingError,
|
|
174
|
+
};
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.6",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:94157e54-2402-4932-84de-96074af286be",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-11T17:16:07.899Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.8.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.86",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.86",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.86",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.8.
|
|
57
|
+
"ref": "@blamejs/core@0.8.86",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|