@blamejs/core 0.4.25 → 0.4.26

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.25** (2026-04-30) — b.objectStore.bucketOps: bucket-level operations (SigV4)
11
12
  - **0.4.24** (2026-04-30) — b.objectStore: multipart upload + server-side encryption
12
13
  - **0.4.23** (2026-04-30) — b.mail.dkim signing + calendar invites
13
14
  - **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
- var parsed;
220
- try { parsed = new URL(url); }
221
- catch (_e) {
222
- throw new OAuthError("auth-oauth/bad-url", label + ": invalid URL '" + url + "'");
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
- if (parsed.protocol === "https:") return url;
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/forms.js CHANGED
@@ -36,6 +36,7 @@
36
36
  * forms.escapeHtml = template.escapeHtml (re-export for convenience)
37
37
  */
38
38
  var nodeCrypto = require("crypto");
39
+ var safeUrl = require("./safe-url");
39
40
  var template = require("./template");
40
41
 
41
42
  // ============================================================
@@ -327,8 +328,17 @@ function validate(spec, body) {
327
328
  }
328
329
  }
329
330
  if (f.type === "url" && typeof coerced === "string") {
330
- try { new URL(coerced); }
331
- catch (_e) { errors[f.name] = (f.label || f.name) + " must be a valid URL"; continue; }
331
+ // Form `url` fields come from the request body — operator/external
332
+ // input. Route through safeUrl so the scheme allowlist is honored
333
+ // (https-only by default; operator opts in to http via field meta).
334
+ try {
335
+ safeUrl.parse(coerced, {
336
+ allowedProtocols: f.allowHttp ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS,
337
+ });
338
+ } catch (_e) {
339
+ errors[f.name] = (f.label || f.name) + " must be a valid URL";
340
+ continue;
341
+ }
332
342
  }
333
343
  if (typeof coerced === "string") {
334
344
  if (f.minlength !== undefined && coerced.length < Number(f.minlength)) {
@@ -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: { ip: req.socket && req.socket.remoteAddress, userAgent: req.headers && req.headers["user-agent"] },
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), userAgent: req.headers && req.headers["user-agent"] },
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: { ip: req.socket && req.socket.remoteAddress, userAgent: req.headers && req.headers["user-agent"] },
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.4.25",
3
+ "version": "0.4.26",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",