@blamejs/core 0.6.69 → 0.7.0
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 +6 -0
- package/README.md +11 -1
- package/index.js +2 -5
- package/lib/api-key.js +24 -55
- package/lib/api-snapshot.js +4 -1
- package/lib/app-shutdown.js +20 -16
- package/lib/app.js +6 -5
- package/lib/archive.js +58 -50
- package/lib/atomic-file.js +11 -5
- package/lib/audit-chain.js +12 -4
- package/lib/audit-sign.js +38 -13
- package/lib/audit-tools.js +4 -4
- package/lib/audit.js +8 -4
- package/lib/auth/jwt.js +3 -2
- package/lib/auth/lockout.js +3 -13
- package/lib/auth/oauth.js +27 -17
- package/lib/auth/passkey.js +1 -5
- package/lib/auth/password.js +23 -13
- package/lib/backup/bundle.js +2 -4
- package/lib/backup/crypto.js +8 -6
- package/lib/backup/index.js +18 -14
- package/lib/backup/manifest.js +6 -2
- package/lib/break-glass.js +12 -7
- package/lib/bundler.js +32 -21
- package/lib/cache-redis.js +8 -3
- package/lib/cache.js +39 -59
- package/lib/canonical-json.js +17 -2
- package/lib/chain-writer.js +2 -2
- package/lib/cli-helpers.js +9 -4
- package/lib/cli.js +27 -5
- package/lib/cluster-provider-db.js +2 -1
- package/lib/cluster-storage.js +71 -11
- package/lib/cluster.js +30 -25
- package/lib/config-drift.js +3 -4
- package/lib/constants.js +8 -3
- package/lib/cookies.js +16 -4
- package/lib/credential-hash.js +4 -7
- package/lib/crypto.js +9 -9
- package/lib/csv.js +11 -8
- package/lib/db-declare-row-policy.js +4 -9
- package/lib/db-declare-view.js +3 -6
- package/lib/db-query.js +9 -3
- package/lib/db-schema.js +75 -10
- package/lib/db.js +116 -70
- package/lib/deprecate.js +3 -8
- package/lib/dev.js +30 -19
- package/lib/dual-control.js +2 -4
- package/lib/error-page.js +32 -39
- package/lib/external-db-migrate.js +27 -20
- package/lib/external-db.js +11 -3
- package/lib/file-type.js +16 -8
- package/lib/forms.js +48 -12
- package/lib/framework-error.js +1 -2
- package/lib/{internal-sha1-hibp.js → framework-sha1-hibp.js} +1 -1
- package/lib/handlers.js +1 -1
- package/lib/http-client-cookie-jar.js +25 -18
- package/lib/http-client.js +46 -23
- package/lib/i18n.js +6 -16
- package/lib/jobs.js +5 -1
- package/lib/log-stream-cloudwatch.js +31 -30
- package/lib/log-stream-local.js +12 -3
- package/lib/log-stream-otlp-grpc.js +29 -23
- package/lib/log-stream-otlp.js +13 -19
- package/lib/log-stream-syslog.js +27 -22
- package/lib/log-stream-webhook.js +8 -17
- package/lib/log-stream.js +13 -2
- package/lib/log.js +83 -5
- package/lib/mail-bounce.js +22 -15
- package/lib/mail-dkim.js +3 -6
- package/lib/mail.js +62 -34
- package/lib/metrics.js +61 -25
- package/lib/middleware/api-encrypt.js +15 -13
- package/lib/middleware/body-parser.js +46 -39
- package/lib/middleware/bot-guard.js +25 -4
- package/lib/middleware/compression.js +26 -11
- package/lib/middleware/cors.js +6 -5
- package/lib/middleware/csp-nonce.js +17 -4
- package/lib/middleware/csrf-protect.js +1 -1
- package/lib/middleware/db-role-for.js +7 -30
- package/lib/middleware/health.js +42 -8
- package/lib/middleware/index.js +55 -36
- package/lib/middleware/rate-limit.js +11 -8
- package/lib/middleware/request-id.js +10 -2
- package/lib/middleware/request-log.js +3 -2
- package/lib/middleware/require-auth.js +5 -2
- package/lib/middleware/sse.js +5 -4
- package/lib/migration-files.js +37 -0
- package/lib/migrations.js +42 -17
- package/lib/mtls-ca.js +38 -17
- package/lib/mtls-engine-default.js +15 -7
- package/lib/network-dns.js +49 -20
- package/lib/network-heartbeat.js +7 -7
- package/lib/network-nts.js +70 -48
- package/lib/network-proxy.js +34 -15
- package/lib/network-tls.js +7 -5
- package/lib/network.js +3 -3
- package/lib/notify.js +14 -36
- package/lib/ntp-check.js +7 -6
- package/lib/numeric-bounds.js +35 -3
- package/lib/object-store/azure-blob-bucket-ops.js +73 -15
- package/lib/object-store/azure-blob.js +31 -24
- package/lib/object-store/gcs-bucket-ops.js +41 -17
- package/lib/object-store/gcs.js +41 -34
- package/lib/object-store/http-put.js +2 -11
- package/lib/object-store/http-request.js +38 -0
- package/lib/object-store/index.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +33 -37
- package/lib/object-store/sigv4.js +50 -30
- package/lib/observability.js +14 -3
- package/lib/otel-export.js +16 -14
- package/lib/pagination.js +23 -8
- package/lib/parsers/index.js +11 -5
- package/lib/parsers/safe-env.js +43 -14
- package/lib/parsers/safe-ini.js +29 -10
- package/lib/parsers/safe-toml.js +55 -25
- package/lib/parsers/safe-xml.js +22 -11
- package/lib/parsers/safe-yaml.js +55 -17
- package/lib/permissions.js +24 -33
- package/lib/pqc-agent.js +4 -2
- package/lib/pqc-gate.js +28 -15
- package/lib/protobuf-encoder.js +9 -3
- package/lib/protocol-dispatcher.js +29 -12
- package/lib/pubsub-redis.js +25 -8
- package/lib/pubsub.js +84 -12
- package/lib/queue-local.js +13 -4
- package/lib/queue-redis.js +2 -2
- package/lib/queue-sqs.js +21 -16
- package/lib/queue.js +8 -4
- package/lib/redis-client.js +13 -10
- package/lib/request-helpers.js +33 -2
- package/lib/restore-bundle.js +2 -4
- package/lib/restore-rollback.js +4 -1
- package/lib/restore.js +12 -10
- package/lib/retry.js +58 -9
- package/lib/router.js +96 -27
- package/lib/safe-async.js +97 -4
- package/lib/safe-buffer.js +47 -0
- package/lib/safe-json.js +18 -9
- package/lib/safe-schema.js +54 -18
- package/lib/safe-url.js +6 -2
- package/lib/scheduler.js +25 -14
- package/lib/security-assert.js +31 -26
- package/lib/seeders.js +71 -60
- package/lib/session.js +13 -7
- package/lib/slug.js +35 -8
- package/lib/ssrf-guard.js +58 -43
- package/lib/static.js +11 -7
- package/lib/storage.js +2 -1
- package/lib/template.js +17 -7
- package/lib/testing.js +28 -8
- package/lib/time.js +26 -5
- package/lib/totp.js +18 -9
- package/lib/tracing.js +15 -5
- package/lib/uuid.js +47 -20
- package/lib/validate-opts.js +213 -0
- package/lib/vault/index.js +36 -27
- package/lib/vault/rotate.js +41 -22
- package/lib/vault/wrap.js +26 -19
- package/lib/webhook.js +37 -68
- package/lib/websocket.js +60 -46
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
- package/lib/auth/totp.js +0 -17
- package/lib/object-store/retry.js +0 -15
package/lib/error-page.js
CHANGED
|
@@ -39,61 +39,67 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
var lazyRequire = require("./lazy-require");
|
|
42
|
+
var logModule = require("./log");
|
|
42
43
|
var requestHelpers = require("./request-helpers");
|
|
43
44
|
var safeEnv = require("./parsers/safe-env");
|
|
44
45
|
var template = require("./template");
|
|
45
46
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
46
47
|
|
|
48
|
+
var bootLog = logModule.boot("errors-page");
|
|
49
|
+
|
|
47
50
|
var _esc = template.escapeHtml;
|
|
51
|
+
var H = requestHelpers.HTTP_STATUS;
|
|
48
52
|
|
|
49
53
|
// Status code → default short reason. Used when the error doesn't carry
|
|
50
|
-
// its own message (e.g. a generic Error thrown from a route).
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
// its own message (e.g. a generic Error thrown from a route). Keyed by
|
|
55
|
+
// the framework's centralized HTTP_STATUS hex IDs (RFC 9110 reason
|
|
56
|
+
// phrases). JS coerces numeric keys to strings, so STATUS_REASONS[404]
|
|
57
|
+
// == STATUS_REASONS[H.NOT_FOUND].
|
|
58
|
+
var STATUS_REASONS = {};
|
|
59
|
+
STATUS_REASONS[H.BAD_REQUEST] = "Bad Request";
|
|
60
|
+
STATUS_REASONS[H.UNAUTHORIZED] = "Unauthorized";
|
|
61
|
+
STATUS_REASONS[H.FORBIDDEN] = "Forbidden";
|
|
62
|
+
STATUS_REASONS[H.NOT_FOUND] = "Not Found";
|
|
63
|
+
STATUS_REASONS[H.METHOD_NOT_ALLOWED] = "Method Not Allowed";
|
|
64
|
+
STATUS_REASONS[H.CONFLICT] = "Conflict";
|
|
65
|
+
STATUS_REASONS[H.PAYLOAD_TOO_LARGE] = "Payload Too Large";
|
|
66
|
+
STATUS_REASONS[H.UNSUPPORTED_MEDIA_TYPE] = "Unsupported Media Type";
|
|
67
|
+
STATUS_REASONS[H.UNPROCESSABLE_CONTENT] = "Unprocessable Content";
|
|
68
|
+
STATUS_REASONS[H.TOO_MANY_REQUESTS] = "Too Many Requests";
|
|
69
|
+
STATUS_REASONS[H.INTERNAL_SERVER_ERROR] = "Internal Server Error";
|
|
70
|
+
STATUS_REASONS[H.BAD_GATEWAY] = "Bad Gateway";
|
|
71
|
+
STATUS_REASONS[H.SERVICE_UNAVAILABLE] = "Service Unavailable";
|
|
72
|
+
STATUS_REASONS[H.GATEWAY_TIMEOUT] = "Gateway Timeout";
|
|
67
73
|
|
|
68
74
|
// Map common framework error classes → HTTP status. Operators who
|
|
69
75
|
// throw their own AppError-like classes set isAppError + statusCode
|
|
70
76
|
// and bypass this map entirely.
|
|
71
77
|
function _classify(err) {
|
|
72
78
|
if (!err || typeof err !== "object") {
|
|
73
|
-
return { status:
|
|
79
|
+
return { status: H.INTERNAL_SERVER_ERROR, code: "INTERNAL_ERROR", message: "Internal Server Error", classified: false };
|
|
74
80
|
}
|
|
75
81
|
// Apps that follow the standard convention: { isAppError, statusCode, code, message }
|
|
76
82
|
if (err.isAppError) {
|
|
77
83
|
return {
|
|
78
|
-
status: err.statusCode ||
|
|
84
|
+
status: err.statusCode || H.INTERNAL_SERVER_ERROR,
|
|
79
85
|
code: err.code || "APP_ERROR",
|
|
80
|
-
message: err.message || STATUS_REASONS[err.statusCode ||
|
|
86
|
+
message: err.message || STATUS_REASONS[err.statusCode || H.INTERNAL_SERVER_ERROR] || "Error",
|
|
81
87
|
classified: true,
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
90
|
// Framework error subclasses we know map to specific HTTP statuses.
|
|
85
91
|
if (err.isAuthError) {
|
|
86
|
-
return { status:
|
|
92
|
+
return { status: H.UNAUTHORIZED, code: err.code || "AUTH_FAILED", message: err.message || "Unauthorized", classified: true };
|
|
87
93
|
}
|
|
88
94
|
if (err.code === "VALIDATION_ERROR" || err.name === "ValidationError") {
|
|
89
|
-
return { status:
|
|
95
|
+
return { status: H.BAD_REQUEST, code: err.code || "VALIDATION_ERROR", message: err.message || "Bad Request", classified: true };
|
|
90
96
|
}
|
|
91
97
|
if (err.isSafeJsonError) {
|
|
92
|
-
return { status:
|
|
98
|
+
return { status: H.BAD_REQUEST, code: err.code || "json/invalid", message: err.message || "Bad Request", path: err.path || null, classified: true };
|
|
93
99
|
}
|
|
94
100
|
// Permanent framework errors that map cleanly to 4xx
|
|
95
101
|
if ((err.isStorageError || err.isQueueError || err.isExternalDbError) && err.permanent) {
|
|
96
|
-
return { status:
|
|
102
|
+
return { status: H.BAD_REQUEST, code: err.code || "ERROR", message: err.message || "Bad Request", classified: true };
|
|
97
103
|
}
|
|
98
104
|
if (typeof err.statusCode === "number") {
|
|
99
105
|
return {
|
|
@@ -103,7 +109,7 @@ function _classify(err) {
|
|
|
103
109
|
classified: true,
|
|
104
110
|
};
|
|
105
111
|
}
|
|
106
|
-
return { status:
|
|
112
|
+
return { status: H.INTERNAL_SERVER_ERROR, code: err.code || "INTERNAL_ERROR", message: err.message || "Internal Server Error", classified: false };
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
function _wantsJson(req, defaultFormat) {
|
|
@@ -303,20 +309,7 @@ function create(opts) {
|
|
|
303
309
|
var showEnvVars = mode === "dev" && opts.showEnvVars === true;
|
|
304
310
|
var onErrorHook = typeof opts.onError === "function" ? opts.onError : null;
|
|
305
311
|
|
|
306
|
-
|
|
307
|
-
if (log && typeof log[level] === "function") {
|
|
308
|
-
try { log[level](message, fields); }
|
|
309
|
-
catch (_e) { /* logger best-effort */ }
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
// Fallback: framework console with status-aware severity
|
|
313
|
-
var line = "[blamejs:errors-page] " + message + " " + JSON.stringify(fields || {});
|
|
314
|
-
if (level === "error" || level === "fatal" || level === "warn") {
|
|
315
|
-
console.error(line);
|
|
316
|
-
} else {
|
|
317
|
-
console.log(line);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
312
|
+
var _log = logModule.makeViaOrFallback(log, bootLog);
|
|
320
313
|
|
|
321
314
|
function handler(err, req, res) {
|
|
322
315
|
var info = _classify(err);
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
var path = require("path");
|
|
52
52
|
var atomicFile = require("./atomic-file");
|
|
53
53
|
var lazyRequire = require("./lazy-require");
|
|
54
|
+
var migrationFiles = require("./migration-files");
|
|
55
|
+
var numericBounds = require("./numeric-bounds");
|
|
54
56
|
var validateOpts = require("./validate-opts");
|
|
55
57
|
var { defineClass } = require("./framework-error");
|
|
56
58
|
|
|
@@ -67,9 +69,18 @@ var LOCK_TABLE = "_blamejs_externaldb_migrations_lock";
|
|
|
67
69
|
var Q_TRACKING = '"' + TRACKING_TABLE + '"';
|
|
68
70
|
var Q_LOCK = '"' + LOCK_TABLE + '"';
|
|
69
71
|
|
|
70
|
-
// Filename grammar
|
|
71
|
-
//
|
|
72
|
-
|
|
72
|
+
// Filename grammar lives in lib/migration-files (shared with the local
|
|
73
|
+
// migrations.js + seeders.js runners). Length capped before the regex
|
|
74
|
+
// test so a hostile directory listing can't drive the engine against
|
|
75
|
+
// an unbounded filename string.
|
|
76
|
+
var FILE_NAME_MAX = 255;
|
|
77
|
+
|
|
78
|
+
function _isMigrationFile(name) {
|
|
79
|
+
return typeof name === "string" &&
|
|
80
|
+
name.length > 0 &&
|
|
81
|
+
name.length <= FILE_NAME_MAX &&
|
|
82
|
+
migrationFiles.MIGRATION_FILE_RE.test(name);
|
|
83
|
+
}
|
|
73
84
|
|
|
74
85
|
function _err(code, message) {
|
|
75
86
|
return new ExternalDbMigrateError(code, message);
|
|
@@ -113,8 +124,15 @@ async function _acquireLock(xdb, opts) {
|
|
|
113
124
|
await _ensureLockTable(xdb);
|
|
114
125
|
var holder = _lockHolderId();
|
|
115
126
|
var nowMs = Date.now();
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
// See migrations.acquireLock for the same fix — Infinity was
|
|
128
|
+
// silently identical to 0 (no staleness check) but obscured the typo.
|
|
129
|
+
var staleAfterMs = 0;
|
|
130
|
+
if (opts) {
|
|
131
|
+
numericBounds.requireNonNegativeFiniteIntIfPresent(opts.staleAfterMs,
|
|
132
|
+
"externalDb.migrate.acquireLock: staleAfterMs",
|
|
133
|
+
ExternalDbMigrateError, "externalDb-migrate/bad-opt");
|
|
134
|
+
if (opts.staleAfterMs !== undefined) staleAfterMs = opts.staleAfterMs;
|
|
135
|
+
}
|
|
118
136
|
try {
|
|
119
137
|
await xdb.query(
|
|
120
138
|
"INSERT INTO " + Q_LOCK + " (scope, lockedAt, lockedBy) VALUES ('lock', $1, $2)",
|
|
@@ -175,7 +193,7 @@ async function _releaseLock(xdb, holder) {
|
|
|
175
193
|
|
|
176
194
|
function _list(dir) {
|
|
177
195
|
return atomicFile.listDir(dir, {
|
|
178
|
-
filter:
|
|
196
|
+
filter: _isMigrationFile,
|
|
179
197
|
}).map(function (e) { return e.name; }).sort();
|
|
180
198
|
}
|
|
181
199
|
|
|
@@ -236,20 +254,9 @@ function _resolveBackendName(opts) {
|
|
|
236
254
|
function create(opts) {
|
|
237
255
|
opts = opts || {};
|
|
238
256
|
validateOpts(opts, ["dir", "backend", "audit", "staleAfterMs"], "b.externalDb.migrate");
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
if (opts.staleAfterMs !== undefined &&
|
|
244
|
-
(typeof opts.staleAfterMs !== "number" || !isFinite(opts.staleAfterMs) || opts.staleAfterMs < 0)) {
|
|
245
|
-
throw _err("externaldb-migrate/bad-stale",
|
|
246
|
-
"staleAfterMs must be a non-negative finite number");
|
|
247
|
-
}
|
|
248
|
-
if (opts.audit !== undefined && opts.audit !== null &&
|
|
249
|
-
(typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function")) {
|
|
250
|
-
throw _err("externaldb-migrate/bad-audit",
|
|
251
|
-
"audit must be a b.audit-shaped object (safeEmit fn)");
|
|
252
|
-
}
|
|
257
|
+
validateOpts.requireNonEmptyString(opts.dir, "externalDb.migrate.create: opts.dir (path to migrations directory)", ExternalDbMigrateError, "externaldb-migrate/no-dir");
|
|
258
|
+
validateOpts.optionalFiniteNonNegative(opts.staleAfterMs, "externalDb.migrate: staleAfterMs", ExternalDbMigrateError, "externaldb-migrate/bad-stale");
|
|
259
|
+
validateOpts.auditShape(opts.audit, "externalDb.migrate", ExternalDbMigrateError, "externaldb-migrate/bad-audit");
|
|
253
260
|
var dir = opts.dir;
|
|
254
261
|
var audit = opts.audit || null;
|
|
255
262
|
|
package/lib/external-db.js
CHANGED
|
@@ -53,11 +53,15 @@
|
|
|
53
53
|
var retryHelper = require("./retry");
|
|
54
54
|
var C = require("./constants");
|
|
55
55
|
var dbRoleContext = require("./db-role-context");
|
|
56
|
+
var externalDbMigrate = require("./external-db-migrate");
|
|
56
57
|
var lazyRequire = require("./lazy-require");
|
|
58
|
+
var { boot } = require("./log");
|
|
57
59
|
var safeAsync = require("./safe-async");
|
|
58
60
|
var safeSql = require("./safe-sql");
|
|
59
61
|
var { ExternalDbError } = require("./framework-error");
|
|
60
62
|
|
|
63
|
+
var log = boot("external-db");
|
|
64
|
+
|
|
61
65
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
62
66
|
var db = lazyRequire(function () { return require("./db"); });
|
|
63
67
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
@@ -771,10 +775,14 @@ var write = {
|
|
|
771
775
|
|
|
772
776
|
function _resetForTest() {
|
|
773
777
|
Object.keys(backends).forEach(function (n) {
|
|
774
|
-
try { backends[n].pool.drain(); }
|
|
778
|
+
try { backends[n].pool.drain(); }
|
|
779
|
+
catch (e) { log.debug("test-reset pool drain failed", { backend: n, error: e.message }); }
|
|
775
780
|
var bk = backends[n];
|
|
776
781
|
if (bk && bk.replicas) {
|
|
777
|
-
bk.replicas.forEach(function (r) {
|
|
782
|
+
bk.replicas.forEach(function (r) {
|
|
783
|
+
try { r.pool.drain(); }
|
|
784
|
+
catch (e2) { log.debug("test-reset replica drain failed", { backend: n, error: e2.message }); }
|
|
785
|
+
});
|
|
778
786
|
}
|
|
779
787
|
});
|
|
780
788
|
backends = {};
|
|
@@ -1047,7 +1055,7 @@ module.exports = {
|
|
|
1047
1055
|
// Migration runner targeting an externalDb backend. Mirrors b.migrations
|
|
1048
1056
|
// (which targets local SQLite) but runs against externalDb. Tracking +
|
|
1049
1057
|
// lock tables live on the externalDb side. See lib/external-db-migrate.js.
|
|
1050
|
-
migrate:
|
|
1058
|
+
migrate: externalDbMigrate,
|
|
1051
1059
|
Pool: Pool,
|
|
1052
1060
|
_resetForTest: _resetForTest,
|
|
1053
1061
|
};
|
package/lib/file-type.js
CHANGED
|
@@ -42,11 +42,17 @@
|
|
|
42
42
|
* extensions are operator-controlled metadata, magic bytes are
|
|
43
43
|
* the wire truth.
|
|
44
44
|
*/
|
|
45
|
+
var C = require("./constants");
|
|
45
46
|
var { defineClass } = require("./framework-error");
|
|
46
47
|
|
|
47
48
|
var FileTypeError = defineClass("FileTypeError", { alwaysPermanent: true });
|
|
48
49
|
var _err = FileTypeError.factory;
|
|
49
50
|
|
|
51
|
+
// Cap on bytes inspected for OOXML / WEBP shape-check probes. Set well
|
|
52
|
+
// above any legitimate Office central-directory prefix and any RIFF
|
|
53
|
+
// header — operators don't tune this, the magic-byte registry is fixed.
|
|
54
|
+
var SNIFF_HEAD_BYTES = C.BYTES.kib(4);
|
|
55
|
+
|
|
50
56
|
// Signature registry. Each entry: { name, mime, extension, category,
|
|
51
57
|
// offset, magic: Buffer | [Buffer, ...], extra?: function(buffer)→bool }.
|
|
52
58
|
// Order matters — earlier entries win on ambiguous matches (e.g. ZIP
|
|
@@ -62,21 +68,21 @@ var SIGNATURES = [
|
|
|
62
68
|
extension: "docx", category: "document",
|
|
63
69
|
offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
64
70
|
extra: function (buf) {
|
|
65
|
-
var head = buf.subarray(0, Math.min(buf.length,
|
|
71
|
+
var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
|
|
66
72
|
return head.indexOf("word/") !== -1 || head.indexOf("[Content_Types].xml") !== -1 && head.indexOf("word") !== -1;
|
|
67
73
|
} },
|
|
68
74
|
{ name: "xlsx", mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
69
75
|
extension: "xlsx", category: "document",
|
|
70
76
|
offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
71
77
|
extra: function (buf) {
|
|
72
|
-
var head = buf.subarray(0, Math.min(buf.length,
|
|
78
|
+
var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
|
|
73
79
|
return head.indexOf("xl/") !== -1;
|
|
74
80
|
} },
|
|
75
81
|
{ name: "pptx", mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
76
82
|
extension: "pptx", category: "document",
|
|
77
83
|
offset: 0, magic: Buffer.from([0x50, 0x4B, 0x03, 0x04]),
|
|
78
84
|
extra: function (buf) {
|
|
79
|
-
var head = buf.subarray(0, Math.min(buf.length,
|
|
85
|
+
var head = buf.subarray(0, Math.min(buf.length, SNIFF_HEAD_BYTES)).toString("binary");
|
|
80
86
|
return head.indexOf("ppt/") !== -1;
|
|
81
87
|
} },
|
|
82
88
|
// ---- Plain ZIP (after OOXML so OOXML wins) ----
|
|
@@ -97,7 +103,9 @@ var SIGNATURES = [
|
|
|
97
103
|
{ name: "webp", mime: "image/webp", extension: "webp", category: "image",
|
|
98
104
|
offset: 0, magic: Buffer.from("RIFF", "ascii"),
|
|
99
105
|
extra: function (buf) {
|
|
100
|
-
|
|
106
|
+
// RIFF header: "RIFF" (0..3), file size (4..7), format tag at offset 8..11.
|
|
107
|
+
return buf.length >= C.BYTES.bytes(12) &&
|
|
108
|
+
buf.subarray(C.BYTES.bytes(0x08), C.BYTES.bytes(12)).toString("ascii") === "WEBP";
|
|
101
109
|
} },
|
|
102
110
|
{ name: "bmp", mime: "image/bmp", extension: "bmp", category: "image",
|
|
103
111
|
offset: 0, magic: Buffer.from([0x42, 0x4D]) },
|
|
@@ -168,10 +176,10 @@ var SIGNATURES = [
|
|
|
168
176
|
offset: 0, magic: Buffer.from([0x7F, 0x45, 0x4C, 0x46]) },
|
|
169
177
|
{ name: "macho", mime: "application/x-mach-binary", extension: "macho", category: "executable",
|
|
170
178
|
offset: 0, magic: [
|
|
171
|
-
Buffer.from([0xFE, 0xED, 0xFA, 0xCE]), //
|
|
172
|
-
Buffer.from([0xFE, 0xED, 0xFA, 0xCF]), //
|
|
173
|
-
Buffer.from([0xCE, 0xFA, 0xED, 0xFE]), //
|
|
174
|
-
Buffer.from([0xCF, 0xFA, 0xED, 0xFE]), //
|
|
179
|
+
Buffer.from([0xFE, 0xED, 0xFA, 0xCE]), // big-endian, narrow word
|
|
180
|
+
Buffer.from([0xFE, 0xED, 0xFA, 0xCF]), // big-endian, wide word
|
|
181
|
+
Buffer.from([0xCE, 0xFA, 0xED, 0xFE]), // little-endian, narrow word
|
|
182
|
+
Buffer.from([0xCF, 0xFA, 0xED, 0xFE]), // little-endian, wide word
|
|
175
183
|
Buffer.from([0xCA, 0xFE, 0xBA, 0xBE]), // universal binary (also Java .class)
|
|
176
184
|
] },
|
|
177
185
|
];
|
package/lib/forms.js
CHANGED
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
* forms.escapeAttribute(value) → string (double-quoted-attr context)
|
|
36
36
|
* forms.escapeHtml = template.escapeHtml (re-export for convenience)
|
|
37
37
|
*/
|
|
38
|
-
var
|
|
38
|
+
var C = require("./constants");
|
|
39
|
+
var { generateToken, timingSafeEqual } = require("./crypto");
|
|
39
40
|
var safeSchema = require("./safe-schema");
|
|
40
41
|
var safeUrl = require("./safe-url");
|
|
41
42
|
var template = require("./template");
|
|
@@ -44,16 +45,24 @@ var template = require("./template");
|
|
|
44
45
|
// CSRF token primitives
|
|
45
46
|
// ============================================================
|
|
46
47
|
|
|
47
|
-
var CSRF_TOKEN_BYTES = 32;
|
|
48
|
+
var CSRF_TOKEN_BYTES = C.BYTES.bytes(32);
|
|
49
|
+
|
|
50
|
+
// RFC 5321 §4.5.3.1.3 caps an email address at 254 chars; bound the
|
|
51
|
+
// regex test on form submissions so a hostile caller can't stage a
|
|
52
|
+
// pathological-length input against the engine.
|
|
53
|
+
var MAX_EMAIL_LENGTH = 254;
|
|
54
|
+
// RFC 7230 §3.1.1 doesn't fix a URL length but the conventional cap
|
|
55
|
+
// (and what most servers / proxies enforce) is 8 KiB.
|
|
56
|
+
var MAX_URL_LENGTH = C.BYTES.kib(8);
|
|
48
57
|
|
|
49
58
|
function generateCsrfToken() {
|
|
50
|
-
return
|
|
59
|
+
return generateToken(CSRF_TOKEN_BYTES);
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
function verifyCsrfToken(submitted, expected) {
|
|
54
63
|
if (typeof submitted !== "string" || typeof expected !== "string") return false;
|
|
55
64
|
if (submitted.length === 0 || submitted.length !== expected.length) return false;
|
|
56
|
-
return
|
|
65
|
+
return timingSafeEqual(submitted, expected);
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
// ============================================================
|
|
@@ -129,7 +138,18 @@ function _renderInput(field) {
|
|
|
129
138
|
if (field.disabled) attrs.push("disabled");
|
|
130
139
|
if (field.checked) attrs.push("checked");
|
|
131
140
|
if (field.autocomplete) attrs.push('autocomplete="' + escapeAttribute(field.autocomplete) + '"');
|
|
132
|
-
if (field.pattern)
|
|
141
|
+
if (field.pattern) {
|
|
142
|
+
// Pattern is a RegExp at every boundary; render its source for the
|
|
143
|
+
// HTML attribute so client + server agree on the regex. String
|
|
144
|
+
// patterns are rejected at config time because validate() refuses
|
|
145
|
+
// them — symmetric handling avoids a silent server-validation gap.
|
|
146
|
+
if (!(field.pattern instanceof RegExp)) {
|
|
147
|
+
throw new Error("forms.render: field '" + field.name +
|
|
148
|
+
"'.pattern must be a pre-compiled RegExp; got " +
|
|
149
|
+
(typeof field.pattern) + ". Wrap the source string with `RegExp` at config time.");
|
|
150
|
+
}
|
|
151
|
+
attrs.push('pattern="' + escapeAttribute(field.pattern.source) + '"');
|
|
152
|
+
}
|
|
133
153
|
if (field.min !== undefined) attrs.push('min="' + escapeAttribute(field.min) + '"');
|
|
134
154
|
if (field.max !== undefined) attrs.push('max="' + escapeAttribute(field.max) + '"');
|
|
135
155
|
if (field.step !== undefined) attrs.push('step="' + escapeAttribute(field.step) + '"');
|
|
@@ -317,18 +337,27 @@ function validate(spec, body) {
|
|
|
317
337
|
}
|
|
318
338
|
}
|
|
319
339
|
if (f.type === "email" && typeof coerced === "string") {
|
|
340
|
+
// Length cap before the regex test (RFC 5321 §4.5.3.1.3 = 254
|
|
341
|
+
// chars). Any longer is rejected without engaging the regex.
|
|
320
342
|
// Same pragmatic check the rest of the framework uses
|
|
321
343
|
// (safeSchema.EMAIL_RE — shared so we don't carry parallel
|
|
322
344
|
// copies of the same /^[^\s@]+@[^\s@]+\.[^\s@]+$/ regex).
|
|
323
|
-
if (!safeSchema.EMAIL_RE.test(coerced)) {
|
|
345
|
+
if (coerced.length > MAX_EMAIL_LENGTH || !safeSchema.EMAIL_RE.test(coerced)) {
|
|
324
346
|
errors[f.name] = (f.label || f.name) + " must be a valid email address";
|
|
325
347
|
continue;
|
|
326
348
|
}
|
|
327
349
|
}
|
|
328
350
|
if (f.type === "url" && typeof coerced === "string") {
|
|
329
351
|
// Form `url` fields come from the request body — operator/external
|
|
330
|
-
// input.
|
|
331
|
-
//
|
|
352
|
+
// input. Length cap (RFC 7230 §3.1.1 has no fixed limit; the
|
|
353
|
+
// conventional cap is 8 KiB) bounds the work before safeUrl.parse
|
|
354
|
+
// engages. Route through safeUrl so the scheme allowlist is
|
|
355
|
+
// honored (https-only by default; operator opts in to http via
|
|
356
|
+
// field meta).
|
|
357
|
+
if (coerced.length > MAX_URL_LENGTH) {
|
|
358
|
+
errors[f.name] = (f.label || f.name) + " must be a valid URL";
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
332
361
|
try {
|
|
333
362
|
safeUrl.parse(coerced, {
|
|
334
363
|
allowedProtocols: f.allowHttp ? safeUrl.ALLOW_HTTP_ALL : safeUrl.ALLOW_HTTP_TLS,
|
|
@@ -348,10 +377,17 @@ function validate(spec, body) {
|
|
|
348
377
|
continue;
|
|
349
378
|
}
|
|
350
379
|
if (f.pattern) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
380
|
+
// Pattern accepts a pre-compiled RegExp only — taking a string
|
|
381
|
+
// and compiling it here would be a ReDoS surface against the
|
|
382
|
+
// form-render engine. Operators construct the RegExp at config
|
|
383
|
+
// time so the framework never compiles operator-supplied source
|
|
384
|
+
// on the request path.
|
|
385
|
+
if (!(f.pattern instanceof RegExp)) {
|
|
386
|
+
throw new Error("forms.validate: field '" + f.name +
|
|
387
|
+
"'.pattern must be a pre-compiled RegExp; got " +
|
|
388
|
+
(typeof f.pattern) + ". Wrap the source string with `RegExp` at config time.");
|
|
389
|
+
}
|
|
390
|
+
if (!f.pattern.test(coerced)) {
|
|
355
391
|
errors[f.name] = f.errorMessages && f.errorMessages.pattern
|
|
356
392
|
? f.errorMessages.pattern
|
|
357
393
|
: (f.label || f.name) + " has an invalid format";
|
package/lib/framework-error.js
CHANGED
|
@@ -99,8 +99,7 @@ function defineClass(name, opts) {
|
|
|
99
99
|
// call here even during framework-error's own load: observability's
|
|
100
100
|
// dependencies on metrics + tracing are themselves lazy-required
|
|
101
101
|
// and only resolve at first call (post-load).
|
|
102
|
-
|
|
103
|
-
catch (_e) { /* defensive — no observability sink should ever break a constructor */ }
|
|
102
|
+
observability.safeEvent("error.construct", 1, { class: name });
|
|
104
103
|
}
|
|
105
104
|
};
|
|
106
105
|
Object.defineProperty(GeneratedError, "name", { value: name, configurable: true });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* framework-sha1-hibp — SHA-1 hex digest, RESTRICTED to the
|
|
4
4
|
* HaveIBeenPwned k-anonymity API caller (lib/auth/password.js policy).
|
|
5
5
|
*
|
|
6
6
|
* SHA-1 is broken for collision-resistance and trivially extendable;
|
package/lib/handlers.js
CHANGED
|
@@ -63,7 +63,11 @@
|
|
|
63
63
|
var fs = require("node:fs");
|
|
64
64
|
var path = require("node:path");
|
|
65
65
|
var C = require("./constants");
|
|
66
|
+
var numericBounds = require("./numeric-bounds");
|
|
67
|
+
var safeAsync = require("./safe-async");
|
|
68
|
+
var safeJson = require("./safe-json");
|
|
66
69
|
var safeUrl = require("./safe-url");
|
|
70
|
+
var validateOpts = require("./validate-opts");
|
|
67
71
|
var { defineClass } = require("./framework-error");
|
|
68
72
|
|
|
69
73
|
var CookieJarError = defineClass("CookieJarError", { alwaysPermanent: true });
|
|
@@ -162,10 +166,7 @@ function create(opts) {
|
|
|
162
166
|
}
|
|
163
167
|
var filePath = null;
|
|
164
168
|
if (persist === "file") {
|
|
165
|
-
|
|
166
|
-
throw _err("BAD_OPT",
|
|
167
|
-
"cookieJar.create: persist: 'file' requires opts.file (absolute path)");
|
|
168
|
-
}
|
|
169
|
+
validateOpts.requireNonEmptyString(opts.file, "cookieJar.create: persist: 'file' opts.file (absolute path)", CookieJarError, "BAD_OPT");
|
|
169
170
|
filePath = opts.file;
|
|
170
171
|
// Refuse relative paths so a process running in a different cwd
|
|
171
172
|
// doesn't accidentally serialize to a sibling directory.
|
|
@@ -174,8 +175,11 @@ function create(opts) {
|
|
|
174
175
|
"cookieJar.create: opts.file must be an absolute path, got " + JSON.stringify(filePath));
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
if (opts.flushDebounceMs !== undefined && !numericBounds.isNonNegativeFiniteInt(opts.flushDebounceMs)) {
|
|
179
|
+
throw _err("BAD_OPT", "cookieJar.create: flushDebounceMs must be a non-negative finite integer; got " +
|
|
180
|
+
numericBounds.shape(opts.flushDebounceMs));
|
|
181
|
+
}
|
|
182
|
+
var flushDebounceMs = opts.flushDebounceMs !== undefined ? opts.flushDebounceMs : DEFAULTS.flushDebounceMs;
|
|
179
183
|
var clock = typeof opts.clock === "function" ? opts.clock : Date.now;
|
|
180
184
|
|
|
181
185
|
// Storage map keyed by `<domain>|<path>|<name>` so a (domain, path)
|
|
@@ -194,7 +198,8 @@ function create(opts) {
|
|
|
194
198
|
|
|
195
199
|
function _setOne(reqUrl, parsed) {
|
|
196
200
|
var u;
|
|
197
|
-
try { u =
|
|
201
|
+
try { u = safeUrl.parse(reqUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL }); }
|
|
202
|
+
catch (_e) { return; }
|
|
198
203
|
var host = u.hostname.toLowerCase();
|
|
199
204
|
var attrs = parsed.attrs || {};
|
|
200
205
|
|
|
@@ -274,7 +279,8 @@ function create(opts) {
|
|
|
274
279
|
|
|
275
280
|
function cookieHeaderFor(reqUrl) {
|
|
276
281
|
var u;
|
|
277
|
-
try { u =
|
|
282
|
+
try { u = safeUrl.parse(reqUrl, { allowedProtocols: safeUrl.ALLOW_HTTP_ALL }); }
|
|
283
|
+
catch (_e) { return null; }
|
|
278
284
|
var host = u.hostname.toLowerCase();
|
|
279
285
|
var path = u.pathname || "/";
|
|
280
286
|
var isSecure = u.protocol === "https:";
|
|
@@ -419,7 +425,6 @@ function create(opts) {
|
|
|
419
425
|
// is also passed, the file is sealed via vault.seal so the on-disk
|
|
420
426
|
// bytes are encrypted at rest; otherwise plaintext (operator chose
|
|
421
427
|
// the threat model by passing or omitting vault).
|
|
422
|
-
var flushTimer = null;
|
|
423
428
|
function _flushSync() {
|
|
424
429
|
if (!filePath) return;
|
|
425
430
|
var rows = getAll();
|
|
@@ -427,21 +432,20 @@ function create(opts) {
|
|
|
427
432
|
var blob = vault ? vault.seal(serialized) : serialized;
|
|
428
433
|
fs.writeFileSync(filePath, blob);
|
|
429
434
|
}
|
|
435
|
+
var flushScheduler = safeAsync.makeScheduledFlush(flushDebounceMs, function () {
|
|
436
|
+
if (!filePath) return;
|
|
437
|
+
try { _flushSync(); } catch (_e) { /* operator can call flush() to retry */ }
|
|
438
|
+
});
|
|
430
439
|
function _scheduleFlush() {
|
|
431
440
|
if (!filePath) return;
|
|
432
|
-
|
|
433
|
-
flushTimer = setTimeout(function () {
|
|
434
|
-
flushTimer = null;
|
|
435
|
-
try { _flushSync(); } catch (_e) { /* operator can call flush() to retry */ }
|
|
436
|
-
}, flushDebounceMs);
|
|
437
|
-
if (flushTimer.unref) flushTimer.unref();
|
|
441
|
+
flushScheduler.schedule();
|
|
438
442
|
}
|
|
439
443
|
function flush() {
|
|
440
|
-
|
|
444
|
+
flushScheduler.cancel();
|
|
441
445
|
_flushSync();
|
|
442
446
|
}
|
|
443
447
|
function close() {
|
|
444
|
-
|
|
448
|
+
flushScheduler.cancel();
|
|
445
449
|
if (filePath) try { _flushSync(); } catch (_e) { /* best-effort */ }
|
|
446
450
|
}
|
|
447
451
|
|
|
@@ -459,12 +463,15 @@ function create(opts) {
|
|
|
459
463
|
};
|
|
460
464
|
|
|
461
465
|
// Initial load from file. Missing file is fine (first run).
|
|
466
|
+
// Persist file may be operator-tampered (or vault-sealed) — route through
|
|
467
|
+
// safeJson with an explicit byte cap so a maliciously-large file can't
|
|
468
|
+
// OOM the process before the parse fails.
|
|
462
469
|
if (filePath && fs.existsSync(filePath)) {
|
|
463
470
|
try {
|
|
464
471
|
var raw = fs.readFileSync(filePath, "utf8");
|
|
465
472
|
var serialized = vault ? vault.unseal(raw) : raw;
|
|
466
473
|
if (serialized && serialized.length > 0) {
|
|
467
|
-
var rows =
|
|
474
|
+
var rows = safeJson.parse(serialized, { maxBytes: C.BYTES.mib(16) });
|
|
468
475
|
setFromSerialized(rows);
|
|
469
476
|
}
|
|
470
477
|
} catch (e) {
|