@blamejs/core 0.4.25 → 0.4.27
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 +2 -0
- package/lib/auth/oauth.js +17 -8
- package/lib/error-page.js +3 -10
- package/lib/forms.js +17 -6
- package/lib/mail.js +5 -4
- package/lib/middleware/bot-guard.js +12 -8
- package/lib/middleware/csrf-protect.js +2 -1
- package/lib/middleware/rate-limit.js +5 -1
- package/lib/middleware/require-auth.js +2 -1
- package/lib/object-store/sigv4-bucket-ops.js +12 -8
- package/lib/safe-schema.js +42 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.4.x
|
|
10
10
|
|
|
11
|
+
- **0.4.26** (2026-04-30) — primitive-drift sweep: middleware audit context + safeUrl
|
|
12
|
+
- **0.4.25** (2026-04-30) — b.objectStore.bucketOps: bucket-level operations (SigV4)
|
|
11
13
|
- **0.4.24** (2026-04-30) — b.objectStore: multipart upload + server-side encryption
|
|
12
14
|
- **0.4.23** (2026-04-30) — b.mail.dkim signing + calendar invites
|
|
13
15
|
- **0.4.22** (2026-04-30) — b.mail attachments + inline images + plain/HTML alternatives
|
package/lib/auth/oauth.js
CHANGED
|
@@ -216,15 +216,24 @@ function _validateUrl(url, allowHttp, label) {
|
|
|
216
216
|
if (typeof url !== "string" || url.length === 0) {
|
|
217
217
|
throw new OAuthError("auth-oauth/bad-url", label + ": URL is required");
|
|
218
218
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
// Operator-supplied OAuth issuer / endpoint URL — route through
|
|
220
|
+
// safeUrl so the scheme allowlist is consistent with the rest of the
|
|
221
|
+
// framework's outbound gates. Map safe-url's error codes to the
|
|
222
|
+
// domain-specific oauth codes operators already key alerts on.
|
|
223
|
+
try {
|
|
224
|
+
safeUrl.parse(url, {
|
|
225
|
+
allowedProtocols: allowHttp ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS,
|
|
226
|
+
});
|
|
227
|
+
} catch (e) {
|
|
228
|
+
if (e && e.code === "safe-url/protocol-disallowed") {
|
|
229
|
+
throw new OAuthError("auth-oauth/insecure-url",
|
|
230
|
+
label + ": must be https" + (allowHttp ? " or http" : "") +
|
|
231
|
+
" (got '" + url + "')");
|
|
232
|
+
}
|
|
233
|
+
throw new OAuthError("auth-oauth/bad-url",
|
|
234
|
+
label + ": invalid URL '" + url + "'");
|
|
223
235
|
}
|
|
224
|
-
|
|
225
|
-
if (parsed.protocol === "http:" && allowHttp) return url;
|
|
226
|
-
throw new OAuthError("auth-oauth/insecure-url",
|
|
227
|
-
label + ": must be https (got '" + parsed.protocol + "//" + parsed.host + "')");
|
|
236
|
+
return url;
|
|
228
237
|
}
|
|
229
238
|
|
|
230
239
|
// ---- JOSE alg → node:crypto verify parameters ----
|
package/lib/error-page.js
CHANGED
|
@@ -39,8 +39,11 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
var lazyRequire = require("./lazy-require");
|
|
42
|
+
var template = require("./template");
|
|
42
43
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
43
44
|
|
|
45
|
+
var _esc = template.escapeHtml;
|
|
46
|
+
|
|
44
47
|
// Status code → default short reason. Used when the error doesn't carry
|
|
45
48
|
// its own message (e.g. a generic Error thrown from a route).
|
|
46
49
|
var STATUS_REASONS = {
|
|
@@ -114,16 +117,6 @@ function _wantsJson(req, defaultFormat) {
|
|
|
114
117
|
return req && req.method && req.method.toUpperCase() !== "GET";
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
function _esc(s) {
|
|
118
|
-
if (s === undefined || s === null) return "";
|
|
119
|
-
return String(s)
|
|
120
|
-
.replace(/&/g, "&")
|
|
121
|
-
.replace(/</g, "<")
|
|
122
|
-
.replace(/>/g, ">")
|
|
123
|
-
.replace(/"/g, """)
|
|
124
|
-
.replace(/'/g, "'");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
120
|
function _redactHeaders(headers) {
|
|
128
121
|
if (!headers || typeof headers !== "object") return {};
|
|
129
122
|
var out = {};
|
package/lib/forms.js
CHANGED
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
* forms.escapeHtml = template.escapeHtml (re-export for convenience)
|
|
37
37
|
*/
|
|
38
38
|
var nodeCrypto = require("crypto");
|
|
39
|
+
var safeSchema = require("./safe-schema");
|
|
40
|
+
var safeUrl = require("./safe-url");
|
|
39
41
|
var template = require("./template");
|
|
40
42
|
|
|
41
43
|
// ============================================================
|
|
@@ -318,17 +320,26 @@ function validate(spec, body) {
|
|
|
318
320
|
}
|
|
319
321
|
}
|
|
320
322
|
if (f.type === "email" && typeof coerced === "string") {
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
if (
|
|
323
|
+
// Same pragmatic check the rest of the framework uses
|
|
324
|
+
// (safeSchema.EMAIL_RE — shared so we don't carry parallel
|
|
325
|
+
// copies of the same /^[^\s@]+@[^\s@]+\.[^\s@]+$/ regex).
|
|
326
|
+
if (!safeSchema.EMAIL_RE.test(coerced)) {
|
|
325
327
|
errors[f.name] = (f.label || f.name) + " must be a valid email address";
|
|
326
328
|
continue;
|
|
327
329
|
}
|
|
328
330
|
}
|
|
329
331
|
if (f.type === "url" && typeof coerced === "string") {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
+
// Form `url` fields come from the request body — operator/external
|
|
333
|
+
// input. Route through safeUrl so the scheme allowlist is honored
|
|
334
|
+
// (https-only by default; operator opts in to http via field meta).
|
|
335
|
+
try {
|
|
336
|
+
safeUrl.parse(coerced, {
|
|
337
|
+
allowedProtocols: f.allowHttp ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS,
|
|
338
|
+
});
|
|
339
|
+
} catch (_e) {
|
|
340
|
+
errors[f.name] = (f.label || f.name) + " must be a valid URL";
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
332
343
|
}
|
|
333
344
|
if (typeof coerced === "string") {
|
|
334
345
|
if (f.minlength !== undefined && coerced.length < Number(f.minlength)) {
|
package/lib/mail.js
CHANGED
|
@@ -67,6 +67,7 @@ var audit = lazyRequire(function () { return require("./audit"); });
|
|
|
67
67
|
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
68
68
|
var net = lazyRequire(function () { return require("net"); });
|
|
69
69
|
var tls = lazyRequire(function () { return require("tls"); });
|
|
70
|
+
var safeSchema = require("./safe-schema");
|
|
70
71
|
var validateOpts = require("./validate-opts");
|
|
71
72
|
var { FrameworkError } = require("./framework-error");
|
|
72
73
|
|
|
@@ -80,10 +81,10 @@ class MailError extends FrameworkError {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
// Pragmatic email
|
|
84
|
-
// regex is a fool's errand; this catches obvious nonsense
|
|
85
|
-
// real-world addresses through.
|
|
86
|
-
var EMAIL_RE =
|
|
84
|
+
// Pragmatic email regex — shared with forms.validate / safe-schema.
|
|
85
|
+
// RFC 5322 in a regex is a fool's errand; this catches obvious nonsense
|
|
86
|
+
// and lets real-world addresses through.
|
|
87
|
+
var EMAIL_RE = safeSchema.EMAIL_RE;
|
|
87
88
|
|
|
88
89
|
function _normalizeRecipientList(value, label) {
|
|
89
90
|
if (value === undefined || value === null) return [];
|
|
@@ -45,9 +45,19 @@ var DEFAULT_BLOCKED_AGENTS = [
|
|
|
45
45
|
];
|
|
46
46
|
|
|
47
47
|
var lazyRequire = require("../lazy-require");
|
|
48
|
+
var requestHelpers = require("../request-helpers");
|
|
48
49
|
var validateOpts = require("../validate-opts");
|
|
49
50
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
50
51
|
|
|
52
|
+
// Bot-guard's "trust the proxy header" semantics for actor.ip — the
|
|
53
|
+
// audit event records the apparent source even when behind a CDN.
|
|
54
|
+
// extractActorContext defaults to socket.remoteAddress; we override.
|
|
55
|
+
function _xffIp(req) {
|
|
56
|
+
var xff = req.headers && req.headers["x-forwarded-for"];
|
|
57
|
+
if (xff) return String(xff).split(",")[0].trim();
|
|
58
|
+
return (req.socket && req.socket.remoteAddress) || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
function create(opts) {
|
|
52
62
|
opts = opts || {};
|
|
53
63
|
validateOpts(opts, [
|
|
@@ -105,10 +115,7 @@ function create(opts) {
|
|
|
105
115
|
req.suspectedBot = hit;
|
|
106
116
|
try {
|
|
107
117
|
audit().emit({
|
|
108
|
-
actor: {
|
|
109
|
-
ip: (req.headers && req.headers["x-forwarded-for"]) || (req.socket && req.socket.remoteAddress),
|
|
110
|
-
userAgent: req.headers && req.headers["user-agent"],
|
|
111
|
-
},
|
|
118
|
+
actor: requestHelpers.extractActorContext(req, { ip: _xffIp(req) }),
|
|
112
119
|
action: "system.botguard.tag",
|
|
113
120
|
outcome: "denied",
|
|
114
121
|
reason: hit,
|
|
@@ -121,10 +128,7 @@ function create(opts) {
|
|
|
121
128
|
|
|
122
129
|
try {
|
|
123
130
|
audit().emit({
|
|
124
|
-
actor: {
|
|
125
|
-
ip: (req.headers && req.headers["x-forwarded-for"]) || (req.socket && req.socket.remoteAddress),
|
|
126
|
-
userAgent: req.headers && req.headers["user-agent"],
|
|
127
|
-
},
|
|
131
|
+
actor: requestHelpers.extractActorContext(req, { ip: _xffIp(req) }),
|
|
128
132
|
action: "system.botguard.block",
|
|
129
133
|
outcome: "denied",
|
|
130
134
|
reason: hit,
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
*/
|
|
67
67
|
var lazyRequire = require("../lazy-require");
|
|
68
68
|
var forms = require("../forms");
|
|
69
|
+
var requestHelpers = require("../request-helpers");
|
|
69
70
|
var validateOpts = require("../validate-opts");
|
|
70
71
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
71
72
|
|
|
@@ -203,7 +204,7 @@ function create(opts) {
|
|
|
203
204
|
audit().safeEmit({
|
|
204
205
|
action: "auth.csrf.denied",
|
|
205
206
|
outcome: "denied",
|
|
206
|
-
actor:
|
|
207
|
+
actor: requestHelpers.extractActorContext(req),
|
|
207
208
|
reason: reason,
|
|
208
209
|
metadata: { method: req.method, path: (req.url || "").split("?")[0] },
|
|
209
210
|
});
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
*/
|
|
46
46
|
var C = require("../constants");
|
|
47
47
|
var lazyRequire = require("../lazy-require");
|
|
48
|
+
var requestHelpers = require("../request-helpers");
|
|
48
49
|
var validateOpts = require("../validate-opts");
|
|
49
50
|
var clusterStorage = require("../cluster-storage");
|
|
50
51
|
|
|
@@ -252,8 +253,11 @@ function create(opts) {
|
|
|
252
253
|
if (verdict.retryAfter > 0) res.setHeader("Retry-After", String(verdict.retryAfter));
|
|
253
254
|
}
|
|
254
255
|
try {
|
|
256
|
+
// Override `ip` with the x-forwarded-for-aware client IP so the
|
|
257
|
+
// audit event carries the proxied origin even when extractActorContext
|
|
258
|
+
// would have read the socket address.
|
|
255
259
|
audit().emit({
|
|
256
|
-
actor: { ip: _clientIp(req)
|
|
260
|
+
actor: requestHelpers.extractActorContext(req, { ip: _clientIp(req) }),
|
|
257
261
|
action: "system.ratelimit.block",
|
|
258
262
|
outcome: "denied",
|
|
259
263
|
reason: "rate limit exceeded",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* }
|
|
30
30
|
*/
|
|
31
31
|
var lazyRequire = require("../lazy-require");
|
|
32
|
+
var requestHelpers = require("../request-helpers");
|
|
32
33
|
var validateOpts = require("../validate-opts");
|
|
33
34
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
34
35
|
|
|
@@ -61,7 +62,7 @@ function create(opts) {
|
|
|
61
62
|
audit().emit({
|
|
62
63
|
action: "auth.required.denied",
|
|
63
64
|
outcome: "denied",
|
|
64
|
-
actor:
|
|
65
|
+
actor: requestHelpers.extractActorContext(req),
|
|
65
66
|
reason: "no authenticated user on request",
|
|
66
67
|
metadata: { method: req.method, path: req.pathname || (req.url || "").split("?")[0] },
|
|
67
68
|
});
|
|
@@ -54,6 +54,7 @@ var nodeCrypto = require("crypto");
|
|
|
54
54
|
var sigv4 = require("./sigv4");
|
|
55
55
|
var safeXml = require("../parsers/safe-xml");
|
|
56
56
|
var safeUrl = require("../safe-url");
|
|
57
|
+
var template = require("../template");
|
|
57
58
|
var httpClient = require("../http-client");
|
|
58
59
|
var { ObjectStoreError } = require("../framework-error");
|
|
59
60
|
|
|
@@ -83,15 +84,18 @@ function _validateBucketName(name) {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.replace(/'/g, "'");
|
|
93
|
-
}
|
|
87
|
+
// XML body strings flow through template.escapeHtml — `'` (which
|
|
88
|
+
// it emits for the apostrophe) is a numeric character reference and
|
|
89
|
+
// is valid in both XML and HTML, where `'` is XML-only. AWS S3
|
|
90
|
+
// accepts both; using the shared HTML escape keeps the framework
|
|
91
|
+
// down to one canonical escape primitive.
|
|
92
|
+
var _xmlEscape = template.escapeHtml;
|
|
94
93
|
|
|
94
|
+
// AWS PutBucketLifecycle / PutBucketCors require a Content-MD5 header
|
|
95
|
+
// for body integrity (legacy AWS API requirement; SigV4 already covers
|
|
96
|
+
// integrity via x-amz-content-sha256 but the API still validates this).
|
|
97
|
+
// MD5 here is NOT a credential or security primitive — it's an AWS API
|
|
98
|
+
// shape. b.credentialHash is the wrong tool.
|
|
95
99
|
function _md5Base64(buf) {
|
|
96
100
|
return nodeCrypto.createHash("md5").update(buf).digest("base64");
|
|
97
101
|
}
|
package/lib/safe-schema.js
CHANGED
|
@@ -109,6 +109,7 @@
|
|
|
109
109
|
* so the two stay distinct rather than one wrapping the other.
|
|
110
110
|
*/
|
|
111
111
|
|
|
112
|
+
var safeJson = require("./safe-json");
|
|
112
113
|
var { defineClass } = require("./framework-error");
|
|
113
114
|
|
|
114
115
|
var SafeSchemaError = defineClass("SafeSchemaError", { alwaysPermanent: true });
|
|
@@ -143,10 +144,17 @@ var ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
|
143
144
|
// base64 (standard alphabet, with optional padding). Base64url variants
|
|
144
145
|
// rejected — operators chain .regex(...) for that.
|
|
145
146
|
var BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
146
|
-
// IPv6 —
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
147
|
+
// IPv6 structural pattern — full 8-hextet, every `::`-compressed shape,
|
|
148
|
+
// `::` and `::1` literals, IPv4-mapped (`::ffff:1.2.3.4`), and 6-prefix
|
|
149
|
+
// + IPv4 tail. Adapted from validator.js (Apache-2.0); zone IDs
|
|
150
|
+
// (`fe80::1%eth0`) are deliberately omitted — the framework rejects
|
|
151
|
+
// them as non-portable, matching `safe-json.formats.ipv6`. Bounded
|
|
152
|
+
// quantifiers, no nested-quantifier alternation, ReDoS-safe.
|
|
153
|
+
//
|
|
154
|
+
// `.ipv6()` schema method delegates to `safeJson.formats.ipv6` for
|
|
155
|
+
// stricter algorithmic validation; this regex is exported as a
|
|
156
|
+
// structural pattern for operators who want it directly.
|
|
157
|
+
var IPV6_RE = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
|
150
158
|
|
|
151
159
|
// ---- helpers ----
|
|
152
160
|
|
|
@@ -424,22 +432,33 @@ function _stringMethods(schema, spec) {
|
|
|
424
432
|
_fail(p, "string/datetime", "must be an ISO-8601 datetime with timezone");
|
|
425
433
|
});
|
|
426
434
|
};
|
|
435
|
+
// IP-address validators delegate to safe-json's algorithmic format
|
|
436
|
+
// checks rather than re-running regex matches. The algorithmic path
|
|
437
|
+
// handles edge cases that pure regex misses (compressed `::` shapes,
|
|
438
|
+
// IPv4-mapped `::ffff:1.2.3.4`, multi-`::` rejection, group-count
|
|
439
|
+
// bounds) and keeps the framework's IP-validation behavior in one
|
|
440
|
+
// tested place. IPV4_RE / IPV6_RE remain exported for operators who
|
|
441
|
+
// want the structural pattern, but `.ipv4()` / `.ipv6()` / `.ip()`
|
|
442
|
+
// are the canonical validation surface.
|
|
427
443
|
schema.ipv4 = function () {
|
|
428
444
|
return chain(function (v, p) {
|
|
429
|
-
return
|
|
430
|
-
|
|
445
|
+
return (typeof v === "string" && safeJson.formats.ipv4(v))
|
|
446
|
+
? { ok: true }
|
|
447
|
+
: _fail(p, "string/ipv4", "must be a valid IPv4 address");
|
|
431
448
|
});
|
|
432
449
|
};
|
|
433
450
|
schema.ipv6 = function () {
|
|
434
451
|
return chain(function (v, p) {
|
|
435
|
-
return
|
|
436
|
-
|
|
452
|
+
return (typeof v === "string" && safeJson.formats.ipv6(v))
|
|
453
|
+
? { ok: true }
|
|
454
|
+
: _fail(p, "string/ipv6", "must be a valid IPv6 address");
|
|
437
455
|
});
|
|
438
456
|
};
|
|
439
457
|
schema.ip = function () {
|
|
440
458
|
return chain(function (v, p) {
|
|
441
|
-
return (
|
|
442
|
-
|
|
459
|
+
return (typeof v === "string" && safeJson.formats.ip(v))
|
|
460
|
+
? { ok: true }
|
|
461
|
+
: _fail(p, "string/ip", "must be a valid IP address (v4 or v6)");
|
|
443
462
|
});
|
|
444
463
|
};
|
|
445
464
|
schema.cuid = function () {
|
|
@@ -1173,4 +1192,17 @@ module.exports = {
|
|
|
1173
1192
|
|
|
1174
1193
|
// Errors
|
|
1175
1194
|
SafeSchemaError: SafeSchemaError,
|
|
1195
|
+
|
|
1196
|
+
// Validation regexes — exported so other modules don't re-declare
|
|
1197
|
+
// their own copies. Pragmatic patterns; operators wanting RFC-strict
|
|
1198
|
+
// behavior chain `.refine()` on top of the schema instead.
|
|
1199
|
+
EMAIL_RE: EMAIL_RE,
|
|
1200
|
+
URL_RE: URL_RE,
|
|
1201
|
+
UUID_RE: UUID_RE,
|
|
1202
|
+
DATE_RE: DATE_RE,
|
|
1203
|
+
DATETIME_RE: DATETIME_RE,
|
|
1204
|
+
IPV4_RE: IPV4_RE,
|
|
1205
|
+
IPV6_RE: IPV6_RE,
|
|
1206
|
+
CUID_RE: CUID_RE,
|
|
1207
|
+
ULID_RE: ULID_RE,
|
|
1176
1208
|
};
|