@blamejs/core 0.6.11 → 0.6.13

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,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.6.x
10
10
 
11
+ - **0.6.13** (2026-05-02) — wiki primitive-section docs catch up to the v0.6.12 surface (`safeUrl.parse` allowUserinfo, `session.touch` extendBy ceiling, `queue.consume` rateLimit validation, `mail.transports.console` redactBcc, `logStream` webhook-sink onDrop, `backup.create` requireFlush, `restore.create` maxPulledBytes / maxPulledFiles); restore default cap stated as `C.BYTES.gib(4)` instead of a raw byte literal; numeric-check predicates (`isPositiveInt`, `isFiniteNonNegative`, `isPositiveFinite`) consolidated into `lib/numeric-checks.js` — were duplicated across api-key, cache, notify, queue, restore, retry, slug, testing, webhook; api-snapshot.json refreshed
12
+ - **0.6.12** (2026-05-02) — `b.safeUrl.parse` now rejects URLs with `user:pass@` userinfo by default (opt in per-call via `allowUserinfo: true`); `b.session.touch({ extendBy })` enforces the same `MAX_TTL_MS` ceiling as `create` / `rotate`; `b.queue.consume({ rateLimit })` rejects negative / zero / `NaN` / `Infinity` / fractional `max`; `b.middleware.requireAuth` no longer treats request `Content-Type: application/json` as a JSON-preference signal (only `Accept` and `X-Requested-With` count); `b.backup.create({ requireFlush: true })` opt-in fails the backup if pre-flush fails instead of producing a stale snapshot; `b.restore.create({ maxPulledBytes, maxPulledFiles })` preflight bounds bundle footprint before and after pull (defaults 4 GiB / 100K files); `b.mail.transports.console({ redactBcc: true })` opt-in prints recipient count instead of addresses; `b.logStream.transports.webhook({ onDrop })` callback fires on overflow + retry-exhausted batch drops. Wiki: admin login wired through `b.auth.lockout` (exponential-backoff after bad-cred attempts) and cookie `Secure` flag now routes through `b.requestHelpers.requestProtocol` with `WIKI_TRUST_PROXY` opt-in instead of trusting raw `x-forwarded-proto`. Wiki README documents the trust model for editable page bodies and the sanitization pattern operators should adopt before expanding the editor surface.
11
13
  - **0.6.11** (2026-05-01) — wiki example-execution validator: fixture init no longer reaches across module realms (unblocks the npm-publish workflow's wiki-e2e gate, which `npm install --install-links` copies the framework into the wiki's node_modules — two distinct singletons before this fix)
12
14
  - **0.6.10** (2026-05-01) — README / SECURITY / CONTRIBUTING / wiki: removed stale version stamps and an inaccurate vendored-dep list; SECURITY now points at `lib/vendor/MANIFEST.json` for the authoritative vendor list; supported-versions table no longer pins to a specific minor; wiki archive example names the digest variable correctly (was `sha256`, output is SHA3-512 hex)
13
15
  - **0.6.9** (2026-05-01) — b.archive.zip().digest() returns a SHA3-512 hex string (was SHA-256); operators reconciling against an external SHA-256 must hash the bytes themselves
package/lib/api-key.js CHANGED
@@ -63,6 +63,7 @@ var cryptoField = require("./crypto-field");
63
63
  var requestHelpers = require("./request-helpers");
64
64
  var validateOpts = require("./validate-opts");
65
65
  var C = require("./constants");
66
+ var numericChecks = require("./numeric-checks");
66
67
  var { ApiKeyError } = require("./framework-error");
67
68
 
68
69
  var observability = lazyRequire(function () { return require("./observability"); });
@@ -129,9 +130,7 @@ var DEFAULTS = Object.freeze({
129
130
 
130
131
  // ---- Config-time validation helpers (throw on bad input) ----
131
132
 
132
- function _isPositiveInt(n) {
133
- return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
134
- }
133
+ var _isPositiveInt = numericChecks.isPositiveInt;
135
134
 
136
135
  function _validateIdentifier(name, value) {
137
136
  if (typeof value !== "string" || value.length === 0) {
@@ -238,6 +238,13 @@ function create(opts) {
238
238
  var flushBeforeBackup = typeof opts.flushBeforeBackup === "function"
239
239
  ? opts.flushBeforeBackup
240
240
  : (opts.flushBeforeBackup === false ? null : null);
241
+ // requireFlush — when true, a flush failure FAILS the backup instead
242
+ // of producing a (potentially stale) snapshot. Operators on
243
+ // encrypted-at-rest with hard freshness requirements (compliance,
244
+ // audit, point-in-time recovery) opt in. Default false preserves the
245
+ // long-standing best-effort posture for operators who care about
246
+ // backup completing more than freshness.
247
+ var requireFlush = opts.requireFlush === true;
241
248
  // Default: try b.db.flushToDisk if available. Wired this way so the
242
249
  // backup primitive doesn't take a hard dependency on b.db (operators
243
250
  // running backup against an external db handle still work).
@@ -272,15 +279,24 @@ function create(opts) {
272
279
  var stagingDir = path.join(os.tmpdir(),
273
280
  "blamejs-backup-staging-" + bundleId.replace(/[:.]/g, "-"));
274
281
 
275
- // Flush the live DB to disk so the snapshot is current. Best-effort —
276
- // a flush failure logs but doesn't fail the whole backup; the bundle
277
- // will just snapshot whatever's on disk.
282
+ // Flush the live DB to disk so the snapshot is current. Default
283
+ // posture is best-effort — a flush failure logs but doesn't fail
284
+ // the whole backup. With requireFlush:true the failure aborts the
285
+ // backup so a stale snapshot never lands in storage.
278
286
  if (flushBeforeBackup) {
279
287
  try { await flushBeforeBackup(); }
280
288
  catch (e) {
289
+ var flushReason = (e && e.message) || String(e);
281
290
  _emitAudit("backup.flush.failure",
282
- { bundleId: bundleId, reason: (e && e.message) || String(e) },
283
- "warning");
291
+ { bundleId: bundleId, reason: flushReason },
292
+ requireFlush ? "failure" : "warning");
293
+ if (requireFlush) {
294
+ _emitAudit("backup.failure",
295
+ { bundleId: bundleId, reason: "flush-required-but-failed: " + flushReason },
296
+ "failure");
297
+ throw new BackupError("backup/flush-required-failed",
298
+ "backup flush required but failed: " + flushReason);
299
+ }
284
300
  }
285
301
  }
286
302
 
package/lib/cache.js CHANGED
@@ -93,6 +93,7 @@
93
93
  var clusterStorage = require("./cluster-storage");
94
94
  var C = require("./constants");
95
95
  var lazyRequire = require("./lazy-require");
96
+ var numericChecks = require("./numeric-checks");
96
97
  var requestHelpers = require("./request-helpers");
97
98
  var safeAsync = require("./safe-async");
98
99
  var validateOpts = require("./validate-opts");
@@ -116,13 +117,8 @@ var DEFAULTS = Object.freeze({
116
117
 
117
118
  // ---- Config-time validation helpers (throw on bad input) ----
118
119
 
119
- function _isFiniteNonNegative(n) {
120
- return typeof n === "number" && isFinite(n) && n >= 0;
121
- }
122
-
123
- function _isPositiveInt(n) {
124
- return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
125
- }
120
+ var _isFiniteNonNegative = numericChecks.isFiniteNonNegative;
121
+ var _isPositiveInt = numericChecks.isPositiveInt;
126
122
 
127
123
  // ttlMs accepts: any non-negative finite number OR Infinity. NaN, negative,
128
124
  // or non-number is rejected.
@@ -97,6 +97,19 @@ function create(config) {
97
97
  errorClass: LogStreamError,
98
98
  });
99
99
  var headers = Object.assign({ "Content-Type": cfg.contentType }, _authHeaders(cfg));
100
+ // onDrop callback: invoked when a batch is dropped, either by buffer
101
+ // overflow ("overflow") or by retry exhaustion ("retry-exhausted").
102
+ // Operator wiring this directly (without the framework's dispatcher
103
+ // wrapping) needs visibility into permanent-drop events; the
104
+ // dispatcher path emits its own audit, but a sink used in isolation
105
+ // would otherwise lose drops silently. The callback is invoked
106
+ // best-effort — a throw inside it is swallowed.
107
+ var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
108
+ function _emitDrop(reason, batch, err) {
109
+ if (!onDrop) return;
110
+ try { onDrop({ reason: reason, batch: batch, error: err || null }); }
111
+ catch (_e) { /* drop callback is best-effort by design */ }
112
+ }
100
113
  var buffer = [];
101
114
  var dropCount = 0;
102
115
  var flushTimer = null;
@@ -121,9 +134,13 @@ function create(config) {
121
134
  await retryHelper.withRetry(function () {
122
135
  return _post(cfg.url, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
123
136
  }, cfg.retry);
124
- } catch {
125
- // Batch permanently rejected — surface via the dropped counter.
126
- // Caller's audit hook recorded the drop already at the dispatcher.
137
+ } catch (e) {
138
+ // Batch permanently rejected — surface via dropCount AND the
139
+ // operator-supplied onDrop callback. The dispatcher path
140
+ // wraps its own audit hook around emit(); operators using
141
+ // this sink directly rely on dropCount + onDrop.
142
+ dropCount += batch.length;
143
+ _emitDrop("retry-exhausted", batch, e);
127
144
  break;
128
145
  }
129
146
  }
@@ -136,8 +153,9 @@ function create(config) {
136
153
  function emit(record) {
137
154
  if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
138
155
  if (buffer.length >= cfg.bufferLimit) {
139
- buffer.shift(); // drop oldest
156
+ var dropped = buffer.shift(); // drop oldest
140
157
  dropCount += 1;
158
+ _emitDrop("overflow", [dropped], null);
141
159
  }
142
160
  buffer.push(record);
143
161
  if (buffer.length >= cfg.batchSize) {
package/lib/mail.js CHANGED
@@ -246,6 +246,14 @@ function _toArray(v) {
246
246
  function consoleTransport(opts) {
247
247
  opts = opts || {};
248
248
  var stream = opts.stream || process.stderr;
249
+ // redactBcc: print only the recipient COUNT instead of the addresses.
250
+ // Default false preserves the dev-visibility purpose of this
251
+ // transport. Operators piping dev logs into shared / centralized
252
+ // sinks (Slack, log aggregator, ticket system) opt in to avoid
253
+ // leaking the BCC list — the property exists precisely so a recipient
254
+ // doesn't see who else got the message, and that promise breaks the
255
+ // moment the addresses land in a non-private log.
256
+ var redactBcc = opts.redactBcc === true;
249
257
  return {
250
258
  name: "console",
251
259
  send: async function (message) {
@@ -255,7 +263,14 @@ function consoleTransport(opts) {
255
263
  "[mail.console] Subject: " + (message.subject || ""),
256
264
  ];
257
265
  if (message.cc) lines.push("[mail.console] Cc: " + (Array.isArray(message.cc) ? message.cc.join(", ") : message.cc));
258
- if (message.bcc) lines.push("[mail.console] Bcc: " + (Array.isArray(message.bcc) ? message.bcc.join(", ") : message.bcc));
266
+ if (message.bcc) {
267
+ if (redactBcc) {
268
+ var bccCount = Array.isArray(message.bcc) ? message.bcc.length : 1;
269
+ lines.push("[mail.console] Bcc: <" + bccCount + " recipient" + (bccCount === 1 ? "" : "s") + " — redacted>");
270
+ } else {
271
+ lines.push("[mail.console] Bcc: " + (Array.isArray(message.bcc) ? message.bcc.join(", ") : message.bcc));
272
+ }
273
+ }
259
274
  var body = message.text || (message.html ? "(html body, " + message.html.length + " bytes)" : "");
260
275
  lines.push("");
261
276
  lines.push(body);
@@ -5,14 +5,21 @@
5
5
  * either lets the request through or rejects it.
6
6
  *
7
7
  * Rejection shape:
8
- * - JSON-preferring caller (Accept: application/json, or X-Requested-With:
9
- * XMLHttpRequest, or content-type starts application/json):
8
+ * - JSON-preferring caller (Accept includes application/json, or
9
+ * X-Requested-With: XMLHttpRequest):
10
10
  * 401 application/json with { error: "Authentication required." }
11
11
  * - Browser-preferring caller, when opts.redirectTo is set:
12
12
  * 302 with Location header
13
13
  * - Otherwise:
14
14
  * 401 text/plain
15
15
  *
16
+ * Note: the Content-Type of the REQUEST is intentionally NOT a signal.
17
+ * A server-to-server POST with `Content-Type: application/json` and no
18
+ * `Accept` header should get the same response shape as any other
19
+ * unauthenticated request — Content-Type describes what the client
20
+ * SENT, not what they want back. Operators with a non-default
21
+ * preference contract supply opts.prefersJson.
22
+ *
16
23
  * Always emits `auth.required.denied` audit event on rejection (when
17
24
  * opts.audit !== false). The event records request method + path +
18
25
  * client IP — keys-only, no body content.
@@ -37,8 +44,6 @@ function _defaultPrefersJson(req) {
37
44
  var h = req.headers || {};
38
45
  if (typeof h.accept === "string" && h.accept.indexOf("application/json") !== -1) return true;
39
46
  if (h["x-requested-with"] === "XMLHttpRequest") return true;
40
- if (typeof h["content-type"] === "string" &&
41
- h["content-type"].indexOf("application/json") === 0) return true;
42
47
  return false;
43
48
  }
44
49
 
package/lib/notify.js CHANGED
@@ -49,6 +49,7 @@
49
49
 
50
50
  var lazyRequire = require("./lazy-require");
51
51
  var bootLog = require("./log");
52
+ var numericChecks = require("./numeric-checks");
52
53
  var requestHelpers = require("./request-helpers");
53
54
  var safeAsync = require("./safe-async");
54
55
  var safeUrl = require("./safe-url");
@@ -76,9 +77,7 @@ var DEFAULTS = Object.freeze({
76
77
 
77
78
  // ---- Call-site validation (throw on bad input) ----
78
79
 
79
- function _isFiniteNonNegative(n) {
80
- return typeof n === "number" && isFinite(n) && n >= 0;
81
- }
80
+ var _isFiniteNonNegative = numericChecks.isFiniteNonNegative;
82
81
 
83
82
  function _validateTransport(name, t) {
84
83
  if (!t || typeof t !== "object") {
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * numeric-checks — predicate helpers for opts / arg validation.
4
+ *
5
+ * Existed previously as private `_isPositiveInt` / `_isFiniteNonNegative`
6
+ * / `_isNonNegFinite` copies inside api-key, cache, notify, retry, slug,
7
+ * testing, webhook, and (new in v0.6.12) inline checks in queue and
8
+ * restore. Same shape, different file — that's the repeat-means-primitive
9
+ * rule. Everything routes through here now so adding (e.g.) NaN-or-
10
+ * Infinity-string handling is a one-file change.
11
+ *
12
+ * Predicates only — callers throw with their own framework-error class.
13
+ *
14
+ * isPositiveInt(n) n is a finite integer >= 1
15
+ * isFiniteNonNegative(n) n is a finite number >= 0
16
+ * isPositiveFinite(n) n is a finite number > 0
17
+ *
18
+ * All return false for non-numbers, NaN, Infinity, -Infinity, null,
19
+ * undefined, strings, etc. — operators get one consistent gate against
20
+ * the silent-NaN-cap class of bug regardless of which primitive they're
21
+ * configuring.
22
+ */
23
+
24
+ function isPositiveInt(n) {
25
+ return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
26
+ }
27
+
28
+ function isFiniteNonNegative(n) {
29
+ return typeof n === "number" && isFinite(n) && n >= 0;
30
+ }
31
+
32
+ function isPositiveFinite(n) {
33
+ return typeof n === "number" && isFinite(n) && n > 0;
34
+ }
35
+
36
+ module.exports = {
37
+ isPositiveInt: isPositiveInt,
38
+ isFiniteNonNegative: isFiniteNonNegative,
39
+ isPositiveFinite: isPositiveFinite,
40
+ };
package/lib/queue.js CHANGED
@@ -34,6 +34,7 @@ var C = require("./constants");
34
34
  var clusterStorage = require("./cluster-storage");
35
35
  var crypto = require("./crypto");
36
36
  var lazyRequire = require("./lazy-require");
37
+ var numericChecks = require("./numeric-checks");
37
38
  var observability = require("./observability");
38
39
  var protocolDispatcher = require("./protocol-dispatcher");
39
40
  var localProto = require("./queue-local");
@@ -188,16 +189,19 @@ function consume(queueName, handler, opts) {
188
189
  // accounting keeps it cheap (just a sliding deque of timestamps).
189
190
  var rateLimit = null;
190
191
  if (opts.rateLimit) {
191
- if (!opts.rateLimit.max || !opts.rateLimit.perSeconds ||
192
- typeof opts.rateLimit.max !== "number" ||
193
- typeof opts.rateLimit.perSeconds !== "number") {
192
+ var rlMax = opts.rateLimit.max;
193
+ var rlPer = opts.rateLimit.perSeconds;
194
+ // NaN, Infinity, 0, negatives, fractional max all produce undefined
195
+ // throttling math (NaN deque comparisons, perma-locked queues,
196
+ // perma-open windows). Reject at config time.
197
+ if (!numericChecks.isPositiveInt(rlMax) || !numericChecks.isPositiveFinite(rlPer)) {
194
198
  throw _err("BAD_RATE_LIMIT",
195
- "consume({ rateLimit }): expected { max: number, perSeconds: number }, got " +
199
+ "consume({ rateLimit }): expected { max: positive integer, perSeconds: positive finite number }, got " +
196
200
  JSON.stringify(opts.rateLimit), true);
197
201
  }
198
202
  rateLimit = {
199
- max: opts.rateLimit.max,
200
- windowMs: C.TIME.seconds(opts.rateLimit.perSeconds),
203
+ max: rlMax,
204
+ windowMs: C.TIME.seconds(rlPer),
201
205
  timestamps: [],
202
206
  };
203
207
  }
package/lib/restore.js CHANGED
@@ -54,7 +54,9 @@
54
54
  var fs = require("fs");
55
55
  var os = require("os");
56
56
  var path = require("path");
57
+ var C = require("./constants");
57
58
  var crypto = require("./crypto");
59
+ var numericChecks = require("./numeric-checks");
58
60
  var restoreBundle = require("./restore-bundle");
59
61
  var restoreRollback = require("./restore-rollback");
60
62
  var lazyRequire = require("./lazy-require");
@@ -89,6 +91,7 @@ function create(opts) {
89
91
  opts = opts || {};
90
92
  validateOpts(opts, [
91
93
  "dataDir", "storage", "passphrase", "rollbackRoot", "audit",
94
+ "maxPulledBytes", "maxPulledFiles",
92
95
  ], "restore");
93
96
  if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
94
97
  throw new RestoreError("restore/no-datadir",
@@ -106,6 +109,47 @@ function create(opts) {
106
109
  var rollbackRoot = opts.rollbackRoot || (dataDir + ".rollbacks");
107
110
  var auditOn = opts.audit !== false;
108
111
 
112
+ // Preflight footprint caps. Defended against storage that returns a
113
+ // tampered or oversized bundle: we cap both the storage-reported size
114
+ // (cheap, before pull) AND the actually-pulled bytes/file-count
115
+ // (defense-in-depth in case the backend lied). Default 4 GiB / 100K
116
+ // files keeps the small-bundle path uncapped while bounding the
117
+ // pathological case.
118
+ var maxPulledBytes = numericChecks.isPositiveFinite(opts.maxPulledBytes)
119
+ ? opts.maxPulledBytes : C.BYTES.gib(4);
120
+ var maxPulledFiles = numericChecks.isPositiveInt(opts.maxPulledFiles)
121
+ ? opts.maxPulledFiles : 100000;
122
+
123
+ function _walkPullDirFootprint(dir) {
124
+ var totalBytes = 0, fileCount = 0;
125
+ var stack = [dir];
126
+ while (stack.length > 0) {
127
+ var current = stack.pop();
128
+ var entries;
129
+ try { entries = fs.readdirSync(current, { withFileTypes: true }); }
130
+ catch (_e) { continue; }
131
+ for (var i = 0; i < entries.length; i++) {
132
+ var entry = entries[i];
133
+ var full = path.join(current, entry.name);
134
+ if (entry.isDirectory()) {
135
+ stack.push(full);
136
+ } else if (entry.isFile()) {
137
+ fileCount++;
138
+ if (fileCount > maxPulledFiles) {
139
+ return { tooManyFiles: true, fileCount: fileCount };
140
+ }
141
+ try {
142
+ totalBytes += fs.statSync(full).size;
143
+ if (totalBytes > maxPulledBytes) {
144
+ return { tooManyBytes: true, totalBytes: totalBytes };
145
+ }
146
+ } catch (_e) { /* file vanished mid-walk */ }
147
+ }
148
+ }
149
+ }
150
+ return { totalBytes: totalBytes, fileCount: fileCount };
151
+ }
152
+
109
153
  function _emitAudit(action, info, outcome) {
110
154
  if (!auditOn) return;
111
155
  audit().safeEmit({
@@ -118,6 +162,32 @@ function create(opts) {
118
162
 
119
163
  async function list() { return await storage.listBundles(); }
120
164
 
165
+ // Find a bundle in storage.listBundles() output and check its
166
+ // reported size against maxPulledBytes BEFORE pulling. listBundles()
167
+ // returns the storage-reported size; cheap to scan and rejects an
168
+ // oversized object before any bytes hit local disk. Returns the
169
+ // bundle metadata when within bounds, throws when oversized, returns
170
+ // null when listBundles doesn't surface the bundle (e.g. listing is
171
+ // truncated by the backend).
172
+ async function _preflightBundleSize(bundleId) {
173
+ var listed;
174
+ try { listed = await storage.listBundles(); }
175
+ catch (_e) { return null; }
176
+ if (!Array.isArray(listed)) return null;
177
+ for (var i = 0; i < listed.length; i++) {
178
+ var entry = listed[i];
179
+ if (entry && entry.bundleId === bundleId) {
180
+ if (typeof entry.size === "number" && entry.size > maxPulledBytes) {
181
+ throw new RestoreError("restore/bundle-too-large",
182
+ "bundle '" + bundleId + "' reports size " + entry.size +
183
+ " bytes, exceeds maxPulledBytes " + maxPulledBytes);
184
+ }
185
+ return entry;
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+
121
191
  async function inspect(bundleId) {
122
192
  if (typeof bundleId !== "string" || bundleId.length === 0) {
123
193
  throw new RestoreError("restore/bad-bundle-id", "inspect: bundleId is required");
@@ -127,10 +197,22 @@ function create(opts) {
127
197
  throw new RestoreError("restore/bundle-not-found",
128
198
  "inspect: bundle '" + bundleId + "' not in storage");
129
199
  }
200
+ await _preflightBundleSize(bundleId);
130
201
  var pullDir = path.join(os.tmpdir(),
131
202
  "blamejs-restore-inspect-" + crypto.generateToken(4));
132
203
  try {
133
204
  await storage.readBundle(bundleId, pullDir);
205
+ var pulled = _walkPullDirFootprint(pullDir);
206
+ if (pulled.tooManyBytes) {
207
+ throw new RestoreError("restore/pulled-too-large",
208
+ "bundle '" + bundleId + "' pulled " + pulled.totalBytes +
209
+ " bytes (caught mid-pull), exceeds maxPulledBytes " + maxPulledBytes);
210
+ }
211
+ if (pulled.tooManyFiles) {
212
+ throw new RestoreError("restore/pulled-too-many-files",
213
+ "bundle '" + bundleId + "' pulled " + pulled.fileCount +
214
+ " files, exceeds maxPulledFiles " + maxPulledFiles);
215
+ }
134
216
  return restoreBundle.inspect({ bundleDir: pullDir });
135
217
  } finally {
136
218
  try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
@@ -160,6 +242,17 @@ function create(opts) {
160
242
  }
161
243
 
162
244
  // 1. Pull bundle out of storage
245
+ // Preflight: reject an oversized bundle BEFORE pulling bytes to
246
+ // disk. Cheap when the backend lists size; no-op when it doesn't.
247
+ try {
248
+ await _preflightBundleSize(bundleId);
249
+ } catch (e) {
250
+ _cleanupTmp();
251
+ _emitAudit("restore.failure",
252
+ { bundleId: bundleId, reason: (e && e.message) || String(e) },
253
+ "failure");
254
+ throw e;
255
+ }
163
256
  try {
164
257
  await storage.readBundle(bundleId, pullDir);
165
258
  } catch (e) {
@@ -170,6 +263,19 @@ function create(opts) {
170
263
  throw new RestoreError("restore/storage-read-failed",
171
264
  "pulling bundle from storage failed: " + ((e && e.message) || String(e)));
172
265
  }
266
+ // Defense-in-depth: walk the pulled bundle and re-check footprint.
267
+ // Catches a backend that under-reported size in listBundles or that
268
+ // doesn't surface size at all.
269
+ var pulled = _walkPullDirFootprint(pullDir);
270
+ if (pulled.tooManyBytes || pulled.tooManyFiles) {
271
+ _cleanupTmp();
272
+ var capCode = pulled.tooManyBytes ? "restore/pulled-too-large" : "restore/pulled-too-many-files";
273
+ var capMsg = pulled.tooManyBytes
274
+ ? "bundle '" + bundleId + "' pulled " + pulled.totalBytes + " bytes, exceeds maxPulledBytes " + maxPulledBytes
275
+ : "bundle '" + bundleId + "' pulled " + pulled.fileCount + " files, exceeds maxPulledFiles " + maxPulledFiles;
276
+ _emitAudit("restore.failure", { bundleId: bundleId, reason: capMsg }, "failure");
277
+ throw new RestoreError(capCode, capMsg);
278
+ }
173
279
 
174
280
  // 2. Decrypt + verify into stagingDir
175
281
  var extracted;
package/lib/retry.js CHANGED
@@ -34,6 +34,7 @@
34
34
 
35
35
  var C = require("./constants");
36
36
  var lazyRequire = require("./lazy-require");
37
+ var numericChecks = require("./numeric-checks");
37
38
  // safe-async re-exports withRetry + CircuitBreaker from this module, so a
38
39
  // direct top-level require would create a cycle. Lazy-require defers the
39
40
  // resolution until the first sleep() call, by which point both modules
@@ -82,12 +83,8 @@ var DEFAULT_BREAKER = Object.freeze({
82
83
 
83
84
  // ---- Call-site validation helpers (throw on bad input) ----
84
85
 
85
- function _isPositiveInt(n) {
86
- return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
87
- }
88
- function _isNonNegFinite(n) {
89
- return typeof n === "number" && isFinite(n) && n >= 0;
90
- }
86
+ var _isPositiveInt = numericChecks.isPositiveInt;
87
+ var _isNonNegFinite = numericChecks.isFiniteNonNegative;
91
88
  function _isAbortSignal(s) {
92
89
  // Duck-typed: AbortSignal exposes .aborted (bool) and .addEventListener (fn)
93
90
  return s != null && typeof s === "object" &&
package/lib/safe-url.js CHANGED
@@ -23,6 +23,14 @@
23
23
  * log-stream, http-client) surface their
24
24
  * own decorated error class. Default:
25
25
  * SafeUrlError.
26
+ * allowUserinfo — accept URLs that carry user:pass@ credentials
27
+ * in the authority. Default: false. Userinfo in
28
+ * outbound URLs leaks into request logs, error
29
+ * messages, metric labels, and trace spans;
30
+ * credential placement belongs in headers /
31
+ * cookies / a credential store, not the URL.
32
+ * Operators with a legacy endpoint that
33
+ * REQUIRES userinfo opt in explicitly per call.
26
34
  *
27
35
  * Constants — pre-baked allowlists for the common caller cases:
28
36
  *
@@ -95,6 +103,14 @@ function parse(url, opts) {
95
103
  "]. Pass opts.allowedProtocols to override (e.g. safeUrl.ALLOW_HTTP_ALL for cleartext endpoints).");
96
104
  }
97
105
 
106
+ if (opts.allowUserinfo !== true && (parsed.username !== "" || parsed.password !== "")) {
107
+ throw _makeError(errClass, "safe-url/userinfo-disallowed",
108
+ "URL contains user:pass@ credentials in the authority. These leak into " +
109
+ "request logs / error messages / metric labels / trace spans. Move the " +
110
+ "credential to an Authorization header (or a credential store the client " +
111
+ "reads at call time), or pass opts.allowUserinfo: true to opt this URL in.");
112
+ }
113
+
98
114
  return parsed;
99
115
  }
100
116
 
package/lib/session.js CHANGED
@@ -212,8 +212,11 @@ async function touch(token, opts) {
212
212
  // assembly) and matches the call shape clusterStorage expects.
213
213
  // extendBy resets expiresAt relative to NOW, not relative to the
214
214
  // current expiresAt — a soaked session with continuous traffic
215
- // shouldn't accumulate unbounded expiry.
216
- if (typeof opts.extendBy === "number" && opts.extendBy > 0) {
215
+ // shouldn't accumulate unbounded expiry. The same MAX_TTL_MS
216
+ // ceiling create() and rotate() apply gates extendBy too repeated
217
+ // touch() calls cannot push expiresAt past the framework's bound.
218
+ if (opts.extendBy !== undefined && opts.extendBy !== null) {
219
+ _validateTtl(opts.extendBy, "session.touch");
217
220
  var newExpires = nowMs + opts.extendBy;
218
221
  var result = await clusterStorage.execute(
219
222
  "UPDATE _blamejs_sessions SET lastActivity = ?, expiresAt = ? " +
package/lib/slug.js CHANGED
@@ -34,6 +34,7 @@
34
34
  * - HTML-tag stripping (sanitize textually before slugging).
35
35
  */
36
36
 
37
+ var numericChecks = require("./numeric-checks");
37
38
  var { SlugError } = require("./framework-error");
38
39
  var _err = SlugError.factory;
39
40
 
@@ -63,9 +64,7 @@ var _RESERVED = Object.freeze([
63
64
 
64
65
  // ---- Call-site validation helpers (throw on bad input) ----
65
66
 
66
- function _isPositiveInt(n) {
67
- return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
68
- }
67
+ var _isPositiveInt = numericChecks.isPositiveInt;
69
68
 
70
69
  function _validateOpts(name, opts) {
71
70
  if (typeof opts.separator !== "string" || opts.separator.length !== 1) {
package/lib/testing.js CHANGED
@@ -65,6 +65,7 @@ var os = require("node:os");
65
65
  var nodePath = require("node:path");
66
66
  var EventEmitter = require("node:events").EventEmitter;
67
67
  var lazyRequire = require("./lazy-require");
68
+ var numericChecks = require("./numeric-checks");
68
69
  var safeAsync = require("./safe-async");
69
70
  var { TestingError } = require("./framework-error");
70
71
 
@@ -84,13 +85,8 @@ var DEFAULTS = Object.freeze({
84
85
 
85
86
  // ---- Call-site validation helpers (throw on bad input) ----
86
87
 
87
- function _isPositiveInt(n) {
88
- return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
89
- }
90
-
91
- function _isFiniteNonNegative(n) {
92
- return typeof n === "number" && isFinite(n) && n >= 0;
93
- }
88
+ var _isPositiveInt = numericChecks.isPositiveInt;
89
+ var _isFiniteNonNegative = numericChecks.isFiniteNonNegative;
94
90
 
95
91
  // ---- HTTP mocks (standalone — no framework primitive overlap) ----
96
92
  // These mimic Node's `http` module's request/response shapes. They're
package/lib/webhook.js CHANGED
@@ -72,6 +72,7 @@ var safeUrl = require("./safe-url");
72
72
  var retry = require("./retry");
73
73
  var C = require("./constants");
74
74
  var lazyRequire = require("./lazy-require");
75
+ var numericChecks = require("./numeric-checks");
75
76
  var requestHelpers = require("./request-helpers");
76
77
  var { WebhookError } = require("./framework-error");
77
78
 
@@ -113,12 +114,8 @@ var DEFAULTS = Object.freeze({
113
114
 
114
115
  // ---- Call-site validation helpers (throw on bad input) ----
115
116
 
116
- function _isPositiveInt(n) {
117
- return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
118
- }
119
- function _isNonNegFinite(n) {
120
- return typeof n === "number" && isFinite(n) && n >= 0;
121
- }
117
+ var _isPositiveInt = numericChecks.isPositiveInt;
118
+ var _isNonNegFinite = numericChecks.isFiniteNonNegative;
122
119
  function _hasOwn(obj, k) { return Object.prototype.hasOwnProperty.call(obj, k); }
123
120
  function _objectKeys(obj) { return Object.keys(obj); }
124
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",