@blamejs/core 0.6.11 → 0.6.12
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/backup/index.js +21 -5
- package/lib/log-stream-webhook.js +22 -4
- package/lib/mail.js +16 -1
- package/lib/middleware/require-auth.js +9 -4
- package/lib/queue.js +10 -6
- package/lib/restore.js +104 -0
- package/lib/safe-url.js +16 -0
- package/lib/session.js +5 -2
- 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.6.x
|
|
10
10
|
|
|
11
|
+
- **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
12
|
- **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
13
|
- **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
14
|
- **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/backup/index.js
CHANGED
|
@@ -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.
|
|
276
|
-
// a flush failure logs but doesn't fail
|
|
277
|
-
//
|
|
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:
|
|
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
|
|
|
@@ -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
|
|
126
|
-
//
|
|
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)
|
|
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
|
|
9
|
-
*
|
|
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/queue.js
CHANGED
|
@@ -188,16 +188,20 @@ function consume(queueName, handler, opts) {
|
|
|
188
188
|
// accounting keeps it cheap (just a sliding deque of timestamps).
|
|
189
189
|
var rateLimit = null;
|
|
190
190
|
if (opts.rateLimit) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
var rlMax = opts.rateLimit.max;
|
|
192
|
+
var rlPer = opts.rateLimit.perSeconds;
|
|
193
|
+
// Strict positive-finite on both: NaN, Infinity, 0, and negatives
|
|
194
|
+
// all produce undefined throttling math (NaN deque comparisons,
|
|
195
|
+
// perma-locked queues, perma-open windows). Reject at config time.
|
|
196
|
+
if (typeof rlMax !== "number" || !isFinite(rlMax) || rlMax <= 0 || Math.floor(rlMax) !== rlMax ||
|
|
197
|
+
typeof rlPer !== "number" || !isFinite(rlPer) || rlPer <= 0) {
|
|
194
198
|
throw _err("BAD_RATE_LIMIT",
|
|
195
|
-
"consume({ rateLimit }): expected { max:
|
|
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:
|
|
200
|
-
windowMs: C.TIME.seconds(
|
|
203
|
+
max: rlMax,
|
|
204
|
+
windowMs: C.TIME.seconds(rlPer),
|
|
201
205
|
timestamps: [],
|
|
202
206
|
};
|
|
203
207
|
}
|
package/lib/restore.js
CHANGED
|
@@ -89,6 +89,7 @@ function create(opts) {
|
|
|
89
89
|
opts = opts || {};
|
|
90
90
|
validateOpts(opts, [
|
|
91
91
|
"dataDir", "storage", "passphrase", "rollbackRoot", "audit",
|
|
92
|
+
"maxPulledBytes", "maxPulledFiles",
|
|
92
93
|
], "restore");
|
|
93
94
|
if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
|
|
94
95
|
throw new RestoreError("restore/no-datadir",
|
|
@@ -106,6 +107,47 @@ function create(opts) {
|
|
|
106
107
|
var rollbackRoot = opts.rollbackRoot || (dataDir + ".rollbacks");
|
|
107
108
|
var auditOn = opts.audit !== false;
|
|
108
109
|
|
|
110
|
+
// Preflight footprint caps. Defended against storage that returns a
|
|
111
|
+
// tampered or oversized bundle: we cap both the storage-reported size
|
|
112
|
+
// (cheap, before pull) AND the actually-pulled bytes/file-count
|
|
113
|
+
// (defense-in-depth in case the backend lied). Default 4 GiB / 100K
|
|
114
|
+
// files keeps the small-bundle path uncapped while bounding the
|
|
115
|
+
// pathological case.
|
|
116
|
+
var maxPulledBytes = typeof opts.maxPulledBytes === "number" && isFinite(opts.maxPulledBytes) && opts.maxPulledBytes > 0
|
|
117
|
+
? opts.maxPulledBytes : 4 * 1024 * 1024 * 1024;
|
|
118
|
+
var maxPulledFiles = typeof opts.maxPulledFiles === "number" && isFinite(opts.maxPulledFiles) && opts.maxPulledFiles > 0
|
|
119
|
+
? opts.maxPulledFiles : 100000;
|
|
120
|
+
|
|
121
|
+
function _walkPullDirFootprint(dir) {
|
|
122
|
+
var totalBytes = 0, fileCount = 0;
|
|
123
|
+
var stack = [dir];
|
|
124
|
+
while (stack.length > 0) {
|
|
125
|
+
var current = stack.pop();
|
|
126
|
+
var entries;
|
|
127
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); }
|
|
128
|
+
catch (_e) { continue; }
|
|
129
|
+
for (var i = 0; i < entries.length; i++) {
|
|
130
|
+
var entry = entries[i];
|
|
131
|
+
var full = path.join(current, entry.name);
|
|
132
|
+
if (entry.isDirectory()) {
|
|
133
|
+
stack.push(full);
|
|
134
|
+
} else if (entry.isFile()) {
|
|
135
|
+
fileCount++;
|
|
136
|
+
if (fileCount > maxPulledFiles) {
|
|
137
|
+
return { tooManyFiles: true, fileCount: fileCount };
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
totalBytes += fs.statSync(full).size;
|
|
141
|
+
if (totalBytes > maxPulledBytes) {
|
|
142
|
+
return { tooManyBytes: true, totalBytes: totalBytes };
|
|
143
|
+
}
|
|
144
|
+
} catch (_e) { /* file vanished mid-walk */ }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { totalBytes: totalBytes, fileCount: fileCount };
|
|
149
|
+
}
|
|
150
|
+
|
|
109
151
|
function _emitAudit(action, info, outcome) {
|
|
110
152
|
if (!auditOn) return;
|
|
111
153
|
audit().safeEmit({
|
|
@@ -118,6 +160,32 @@ function create(opts) {
|
|
|
118
160
|
|
|
119
161
|
async function list() { return await storage.listBundles(); }
|
|
120
162
|
|
|
163
|
+
// Find a bundle in storage.listBundles() output and check its
|
|
164
|
+
// reported size against maxPulledBytes BEFORE pulling. listBundles()
|
|
165
|
+
// returns the storage-reported size; cheap to scan and rejects an
|
|
166
|
+
// oversized object before any bytes hit local disk. Returns the
|
|
167
|
+
// bundle metadata when within bounds, throws when oversized, returns
|
|
168
|
+
// null when listBundles doesn't surface the bundle (e.g. listing is
|
|
169
|
+
// truncated by the backend).
|
|
170
|
+
async function _preflightBundleSize(bundleId) {
|
|
171
|
+
var listed;
|
|
172
|
+
try { listed = await storage.listBundles(); }
|
|
173
|
+
catch (_e) { return null; }
|
|
174
|
+
if (!Array.isArray(listed)) return null;
|
|
175
|
+
for (var i = 0; i < listed.length; i++) {
|
|
176
|
+
var entry = listed[i];
|
|
177
|
+
if (entry && entry.bundleId === bundleId) {
|
|
178
|
+
if (typeof entry.size === "number" && entry.size > maxPulledBytes) {
|
|
179
|
+
throw new RestoreError("restore/bundle-too-large",
|
|
180
|
+
"bundle '" + bundleId + "' reports size " + entry.size +
|
|
181
|
+
" bytes, exceeds maxPulledBytes " + maxPulledBytes);
|
|
182
|
+
}
|
|
183
|
+
return entry;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
121
189
|
async function inspect(bundleId) {
|
|
122
190
|
if (typeof bundleId !== "string" || bundleId.length === 0) {
|
|
123
191
|
throw new RestoreError("restore/bad-bundle-id", "inspect: bundleId is required");
|
|
@@ -127,10 +195,22 @@ function create(opts) {
|
|
|
127
195
|
throw new RestoreError("restore/bundle-not-found",
|
|
128
196
|
"inspect: bundle '" + bundleId + "' not in storage");
|
|
129
197
|
}
|
|
198
|
+
await _preflightBundleSize(bundleId);
|
|
130
199
|
var pullDir = path.join(os.tmpdir(),
|
|
131
200
|
"blamejs-restore-inspect-" + crypto.generateToken(4));
|
|
132
201
|
try {
|
|
133
202
|
await storage.readBundle(bundleId, pullDir);
|
|
203
|
+
var pulled = _walkPullDirFootprint(pullDir);
|
|
204
|
+
if (pulled.tooManyBytes) {
|
|
205
|
+
throw new RestoreError("restore/pulled-too-large",
|
|
206
|
+
"bundle '" + bundleId + "' pulled " + pulled.totalBytes +
|
|
207
|
+
" bytes (caught mid-pull), exceeds maxPulledBytes " + maxPulledBytes);
|
|
208
|
+
}
|
|
209
|
+
if (pulled.tooManyFiles) {
|
|
210
|
+
throw new RestoreError("restore/pulled-too-many-files",
|
|
211
|
+
"bundle '" + bundleId + "' pulled " + pulled.fileCount +
|
|
212
|
+
" files, exceeds maxPulledFiles " + maxPulledFiles);
|
|
213
|
+
}
|
|
134
214
|
return restoreBundle.inspect({ bundleDir: pullDir });
|
|
135
215
|
} finally {
|
|
136
216
|
try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
|
|
@@ -160,6 +240,17 @@ function create(opts) {
|
|
|
160
240
|
}
|
|
161
241
|
|
|
162
242
|
// 1. Pull bundle out of storage
|
|
243
|
+
// Preflight: reject an oversized bundle BEFORE pulling bytes to
|
|
244
|
+
// disk. Cheap when the backend lists size; no-op when it doesn't.
|
|
245
|
+
try {
|
|
246
|
+
await _preflightBundleSize(bundleId);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
_cleanupTmp();
|
|
249
|
+
_emitAudit("restore.failure",
|
|
250
|
+
{ bundleId: bundleId, reason: (e && e.message) || String(e) },
|
|
251
|
+
"failure");
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
163
254
|
try {
|
|
164
255
|
await storage.readBundle(bundleId, pullDir);
|
|
165
256
|
} catch (e) {
|
|
@@ -170,6 +261,19 @@ function create(opts) {
|
|
|
170
261
|
throw new RestoreError("restore/storage-read-failed",
|
|
171
262
|
"pulling bundle from storage failed: " + ((e && e.message) || String(e)));
|
|
172
263
|
}
|
|
264
|
+
// Defense-in-depth: walk the pulled bundle and re-check footprint.
|
|
265
|
+
// Catches a backend that under-reported size in listBundles or that
|
|
266
|
+
// doesn't surface size at all.
|
|
267
|
+
var pulled = _walkPullDirFootprint(pullDir);
|
|
268
|
+
if (pulled.tooManyBytes || pulled.tooManyFiles) {
|
|
269
|
+
_cleanupTmp();
|
|
270
|
+
var capCode = pulled.tooManyBytes ? "restore/pulled-too-large" : "restore/pulled-too-many-files";
|
|
271
|
+
var capMsg = pulled.tooManyBytes
|
|
272
|
+
? "bundle '" + bundleId + "' pulled " + pulled.totalBytes + " bytes, exceeds maxPulledBytes " + maxPulledBytes
|
|
273
|
+
: "bundle '" + bundleId + "' pulled " + pulled.fileCount + " files, exceeds maxPulledFiles " + maxPulledFiles;
|
|
274
|
+
_emitAudit("restore.failure", { bundleId: bundleId, reason: capMsg }, "failure");
|
|
275
|
+
throw new RestoreError(capCode, capMsg);
|
|
276
|
+
}
|
|
173
277
|
|
|
174
278
|
// 2. Decrypt + verify into stagingDir
|
|
175
279
|
var extracted;
|
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
|
-
|
|
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 = ? " +
|