@blamejs/core 0.4.26 → 0.4.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/api-key.js +1 -1
- package/lib/cli.js +6 -7
- package/lib/db-query.js +1 -1
- package/lib/db.js +1 -1
- package/lib/error-page.js +3 -10
- package/lib/forms.js +5 -4
- package/lib/http-client.js +3 -2
- package/lib/mail.js +5 -4
- package/lib/middleware/body-parser.js +9 -9
- package/lib/object-store/gcs.js +1 -1
- package/lib/object-store/sigv4-bucket-ops.js +12 -8
- package/lib/queue-local.js +2 -2
- package/lib/queue.js +4 -4
- package/lib/router.js +8 -6
- package/lib/safe-schema.js +42 -10
- package/lib/session.js +6 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.4.x
|
|
10
10
|
|
|
11
|
+
- **0.4.27** (2026-04-30) — primitive-drift sweep: regex + escape consolidation, IPv6 completion
|
|
12
|
+
- **0.4.26** (2026-04-30) — primitive-drift sweep: middleware audit context + safeUrl
|
|
11
13
|
- **0.4.25** (2026-04-30) — b.objectStore.bucketOps: bucket-level operations (SigV4)
|
|
12
14
|
- **0.4.24** (2026-04-30) — b.objectStore: multipart upload + server-side encryption
|
|
13
15
|
- **0.4.23** (2026-04-30) — b.mail.dkim signing + calendar invites
|
package/lib/api-key.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* ownerId: "user-42",
|
|
14
14
|
* scopes: ["read:users", "write:posts"],
|
|
15
15
|
* metadata: { name: "Mobile app v3" },
|
|
16
|
-
* expiresAt: Date.now() + 90
|
|
16
|
+
* expiresAt: Date.now() + b.constants.TIME.days(90),
|
|
17
17
|
* });
|
|
18
18
|
* // issued.key — "bk_live_<idHex>_<secretHex>" (returned ONCE)
|
|
19
19
|
* // issued.id — "<idHex>"
|
package/lib/cli.js
CHANGED
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
* before encryption or temporarily switch the file to plaintext.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
var fs = require("node:fs");
|
|
38
|
+
var nodeCrypto = require("node:crypto");
|
|
39
|
+
var os = require("node:os");
|
|
37
40
|
var path = require("path");
|
|
38
41
|
var apiSnapshot = require("./api-snapshot");
|
|
39
42
|
var auditTools = require("./audit-tools");
|
|
@@ -898,11 +901,7 @@ async function _runBackup(args, ctx) {
|
|
|
898
901
|
}
|
|
899
902
|
|
|
900
903
|
if (sub === "verify") {
|
|
901
|
-
var
|
|
902
|
-
var os2 = require("node:os");
|
|
903
|
-
var nodePath = require("node:path");
|
|
904
|
-
var nodeCrypto = require("node:crypto");
|
|
905
|
-
var stagingDir = nodePath.join(os2.tmpdir(),
|
|
904
|
+
var stagingDir = path.join(os.tmpdir(),
|
|
906
905
|
"blamejs-backup-verify-" + nodeCrypto.randomBytes(8).toString("hex"));
|
|
907
906
|
try {
|
|
908
907
|
var r = await restoreBundle.extract({
|
|
@@ -917,7 +916,7 @@ async function _runBackup(args, ctx) {
|
|
|
917
916
|
} catch (e) {
|
|
918
917
|
return report.error((e && e.message) || String(e));
|
|
919
918
|
} finally {
|
|
920
|
-
try {
|
|
919
|
+
try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
|
|
921
920
|
}
|
|
922
921
|
}
|
|
923
922
|
|
|
@@ -1118,7 +1117,7 @@ async function _runMtls(args, ctx) {
|
|
|
1118
1117
|
validityDays: daysP,
|
|
1119
1118
|
});
|
|
1120
1119
|
if (outPath) {
|
|
1121
|
-
|
|
1120
|
+
fs.writeFileSync(outPath, p12.p12, { mode: 0o600 });
|
|
1122
1121
|
report.write("p12 written: " + outPath);
|
|
1123
1122
|
} else {
|
|
1124
1123
|
// No --out: stream the bytes to stdout for piping. Operators
|
package/lib/db-query.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* a derived hash throws (every encryption uses a fresh nonce — the lookup
|
|
27
27
|
* would always return zero rows; failing loudly is safer).
|
|
28
28
|
*/
|
|
29
|
+
var { Readable } = require("node:stream");
|
|
29
30
|
var cryptoField = require("./crypto-field");
|
|
30
31
|
var { generateToken } = require("./crypto");
|
|
31
32
|
|
|
@@ -194,7 +195,6 @@ class Query {
|
|
|
194
195
|
var stmt = this._db.prepare(sql);
|
|
195
196
|
var table = this._table;
|
|
196
197
|
var iter;
|
|
197
|
-
var Readable = require("node:stream").Readable;
|
|
198
198
|
try { iter = stmt.iterate.apply(stmt, this._whereParams); }
|
|
199
199
|
catch (e) {
|
|
200
200
|
var r = new Readable({ objectMode: true, read: function () {} });
|
package/lib/db.js
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
var fs = require("fs");
|
|
43
43
|
var path = require("path");
|
|
44
44
|
var { DatabaseSync } = require("node:sqlite");
|
|
45
|
+
var { Readable } = require("node:stream");
|
|
45
46
|
var atomicFile = require("./atomic-file");
|
|
46
47
|
var audit = require("./audit");
|
|
47
48
|
var auditSign = require("./audit-sign");
|
|
@@ -796,7 +797,6 @@ function stream(sql) {
|
|
|
796
797
|
var table = opts && typeof opts.table === "string" ? opts.table : null;
|
|
797
798
|
var unseal = table ? cryptoField : null;
|
|
798
799
|
|
|
799
|
-
var Readable = require("node:stream").Readable;
|
|
800
800
|
var stmt;
|
|
801
801
|
var iter;
|
|
802
802
|
try {
|
package/lib/error-page.js
CHANGED
|
@@ -39,8 +39,11 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
var lazyRequire = require("./lazy-require");
|
|
42
|
+
var template = require("./template");
|
|
42
43
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
43
44
|
|
|
45
|
+
var _esc = template.escapeHtml;
|
|
46
|
+
|
|
44
47
|
// Status code → default short reason. Used when the error doesn't carry
|
|
45
48
|
// its own message (e.g. a generic Error thrown from a route).
|
|
46
49
|
var STATUS_REASONS = {
|
|
@@ -114,16 +117,6 @@ function _wantsJson(req, defaultFormat) {
|
|
|
114
117
|
return req && req.method && req.method.toUpperCase() !== "GET";
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
function _esc(s) {
|
|
118
|
-
if (s === undefined || s === null) return "";
|
|
119
|
-
return String(s)
|
|
120
|
-
.replace(/&/g, "&")
|
|
121
|
-
.replace(/</g, "<")
|
|
122
|
-
.replace(/>/g, ">")
|
|
123
|
-
.replace(/"/g, """)
|
|
124
|
-
.replace(/'/g, "'");
|
|
125
|
-
}
|
|
126
|
-
|
|
127
120
|
function _redactHeaders(headers) {
|
|
128
121
|
if (!headers || typeof headers !== "object") return {};
|
|
129
122
|
var out = {};
|
package/lib/forms.js
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
* forms.escapeHtml = template.escapeHtml (re-export for convenience)
|
|
37
37
|
*/
|
|
38
38
|
var nodeCrypto = require("crypto");
|
|
39
|
+
var safeSchema = require("./safe-schema");
|
|
39
40
|
var safeUrl = require("./safe-url");
|
|
40
41
|
var template = require("./template");
|
|
41
42
|
|
|
@@ -319,10 +320,10 @@ function validate(spec, body) {
|
|
|
319
320
|
}
|
|
320
321
|
}
|
|
321
322
|
if (f.type === "email" && typeof coerced === "string") {
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
if (
|
|
323
|
+
// Same pragmatic check the rest of the framework uses
|
|
324
|
+
// (safeSchema.EMAIL_RE — shared so we don't carry parallel
|
|
325
|
+
// copies of the same /^[^\s@]+@[^\s@]+\.[^\s@]+$/ regex).
|
|
326
|
+
if (!safeSchema.EMAIL_RE.test(coerced)) {
|
|
326
327
|
errors[f.name] = (f.label || f.name) + " must be a valid email address";
|
|
327
328
|
continue;
|
|
328
329
|
}
|
package/lib/http-client.js
CHANGED
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
var http = require("http");
|
|
65
65
|
var https = require("https");
|
|
66
66
|
var http2 = require("http2");
|
|
67
|
+
var nodeCrypto = require("node:crypto");
|
|
68
|
+
var nodeStream = require("node:stream");
|
|
67
69
|
var { URL } = require("url");
|
|
68
70
|
var C = require("./constants");
|
|
69
71
|
var pqcAgent = require("./pqc-agent");
|
|
@@ -321,7 +323,6 @@ function _attachJarCookie(headers, jar, url) {
|
|
|
321
323
|
// parser accepts so round-trip from one blamejs app's outbound to
|
|
322
324
|
// another's inbound is exact.
|
|
323
325
|
function _buildMultipartBody(spec) {
|
|
324
|
-
var nodeCrypto = require("node:crypto");
|
|
325
326
|
var boundary = "----blamejs-mp-" + nodeCrypto.randomBytes(16).toString("hex");
|
|
326
327
|
var CRLF = "\r\n";
|
|
327
328
|
var parts = [];
|
|
@@ -701,7 +702,7 @@ function _requestH1(transport, u, opts) {
|
|
|
701
702
|
// The framework's contract is to hand back the response stream
|
|
702
703
|
// unmodified; fix-up via a passthrough keeps that contract while
|
|
703
704
|
// observing the chunk sizes.
|
|
704
|
-
var passthrough = new
|
|
705
|
+
var passthrough = new nodeStream.PassThrough();
|
|
705
706
|
res.on("data", function (chunk) { _emitDownload(chunk.length); passthrough.write(chunk); });
|
|
706
707
|
res.on("end", function () { passthrough.end(); });
|
|
707
708
|
res.on("error", function (e) { passthrough.destroy(e); });
|
package/lib/mail.js
CHANGED
|
@@ -67,6 +67,7 @@ var audit = lazyRequire(function () { return require("./audit"); });
|
|
|
67
67
|
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
68
68
|
var net = lazyRequire(function () { return require("net"); });
|
|
69
69
|
var tls = lazyRequire(function () { return require("tls"); });
|
|
70
|
+
var safeSchema = require("./safe-schema");
|
|
70
71
|
var validateOpts = require("./validate-opts");
|
|
71
72
|
var { FrameworkError } = require("./framework-error");
|
|
72
73
|
|
|
@@ -80,10 +81,10 @@ class MailError extends FrameworkError {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
// Pragmatic email
|
|
84
|
-
// regex is a fool's errand; this catches obvious nonsense
|
|
85
|
-
// real-world addresses through.
|
|
86
|
-
var EMAIL_RE =
|
|
84
|
+
// Pragmatic email regex — shared with forms.validate / safe-schema.
|
|
85
|
+
// RFC 5322 in a regex is a fool's errand; this catches obvious nonsense
|
|
86
|
+
// and lets real-world addresses through.
|
|
87
|
+
var EMAIL_RE = safeSchema.EMAIL_RE;
|
|
87
88
|
|
|
88
89
|
function _normalizeRecipientList(value, label) {
|
|
89
90
|
if (value === undefined || value === null) return [];
|
|
@@ -26,23 +26,23 @@
|
|
|
26
26
|
*
|
|
27
27
|
* var bp = b.middleware.bodyParser({
|
|
28
28
|
* json: {
|
|
29
|
-
* limit:
|
|
29
|
+
* limit: b.constants.BYTES.mib(1),
|
|
30
30
|
* strict: true, // require the body to start with {/[
|
|
31
31
|
* parseHook: function (parsed) { ... return validatedShape; },
|
|
32
32
|
* },
|
|
33
33
|
* urlencoded: {
|
|
34
|
-
* limit:
|
|
34
|
+
* limit: b.constants.BYTES.mib(1),
|
|
35
35
|
* arrayLimit: 100, // for ?tag=a&tag=b → tag: ["a","b"]
|
|
36
36
|
* },
|
|
37
|
-
* text: { limit:
|
|
38
|
-
* raw: { limit: 10
|
|
37
|
+
* text: { limit: b.constants.BYTES.mib(1), charset: "utf-8" },
|
|
38
|
+
* raw: { limit: b.constants.BYTES.mib(10), contentTypes: ["application/octet-stream"] },
|
|
39
39
|
* multipart: {
|
|
40
40
|
* tmpDir: os.tmpdir(),
|
|
41
|
-
* fileSize: 10
|
|
42
|
-
* totalSize: 50
|
|
41
|
+
* fileSize: b.constants.BYTES.mib(10),
|
|
42
|
+
* totalSize: b.constants.BYTES.mib(50),
|
|
43
43
|
* fileCount: 20,
|
|
44
44
|
* fieldCount: 100,
|
|
45
|
-
* fieldSize:
|
|
45
|
+
* fieldSize: b.constants.BYTES.mib(1),
|
|
46
46
|
* mimeAllowlist: ["image/jpeg", "image/png", "application/pdf"], // null = any
|
|
47
47
|
*
|
|
48
48
|
* // Per-part predicate. Runs after sanitization + MIME checks but
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
* // global mimeAllowlist for the named field; other fields still
|
|
65
65
|
* // use the global list.
|
|
66
66
|
* fields: {
|
|
67
|
-
* avatar: { maxBytes: 2
|
|
68
|
-
* document: { maxBytes: 25
|
|
67
|
+
* avatar: { maxBytes: b.constants.BYTES.mib(2), mimeTypes: ["image/jpeg", "image/png"] },
|
|
68
|
+
* document: { maxBytes: b.constants.BYTES.mib(25) },
|
|
69
69
|
* },
|
|
70
70
|
*
|
|
71
71
|
* // When wired, fileFilter rejections emit body-parser.multipart.file_rejected
|
package/lib/object-store/gcs.js
CHANGED
|
@@ -175,7 +175,7 @@ function create(config) {
|
|
|
175
175
|
if (!tokenResp.access_token) {
|
|
176
176
|
throw _err("AUTH_FAILED", "GCS token endpoint returned no access_token: " + res.body.toString("utf8"), true);
|
|
177
177
|
}
|
|
178
|
-
var expiresInMs = (tokenResp.expires_in || 3600)
|
|
178
|
+
var expiresInMs = C.TIME.seconds(tokenResp.expires_in || 3600);
|
|
179
179
|
cachedToken = {
|
|
180
180
|
accessToken: tokenResp.access_token,
|
|
181
181
|
expiresAt: Date.now() + expiresInMs,
|
|
@@ -54,6 +54,7 @@ var nodeCrypto = require("crypto");
|
|
|
54
54
|
var sigv4 = require("./sigv4");
|
|
55
55
|
var safeXml = require("../parsers/safe-xml");
|
|
56
56
|
var safeUrl = require("../safe-url");
|
|
57
|
+
var template = require("../template");
|
|
57
58
|
var httpClient = require("../http-client");
|
|
58
59
|
var { ObjectStoreError } = require("../framework-error");
|
|
59
60
|
|
|
@@ -83,15 +84,18 @@ function _validateBucketName(name) {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.replace(/'/g, "'");
|
|
93
|
-
}
|
|
87
|
+
// XML body strings flow through template.escapeHtml — `'` (which
|
|
88
|
+
// it emits for the apostrophe) is a numeric character reference and
|
|
89
|
+
// is valid in both XML and HTML, where `'` is XML-only. AWS S3
|
|
90
|
+
// accepts both; using the shared HTML escape keeps the framework
|
|
91
|
+
// down to one canonical escape primitive.
|
|
92
|
+
var _xmlEscape = template.escapeHtml;
|
|
94
93
|
|
|
94
|
+
// AWS PutBucketLifecycle / PutBucketCors require a Content-MD5 header
|
|
95
|
+
// for body integrity (legacy AWS API requirement; SigV4 already covers
|
|
96
|
+
// integrity via x-amz-content-sha256 but the API still validates this).
|
|
97
|
+
// MD5 here is NOT a credential or security primitive — it's an AWS API
|
|
98
|
+
// shape. b.credentialHash is the wrong tool.
|
|
95
99
|
function _md5Base64(buf) {
|
|
96
100
|
return nodeCrypto.createHash("md5").update(buf).digest("base64");
|
|
97
101
|
}
|
package/lib/queue-local.js
CHANGED
|
@@ -33,8 +33,9 @@ var cluster = require("./cluster");
|
|
|
33
33
|
var clusterStorage = require("./cluster-storage");
|
|
34
34
|
var { generateToken } = require("./crypto");
|
|
35
35
|
var cryptoField = require("./crypto-field");
|
|
36
|
-
var safeJson = require("./safe-json");
|
|
37
36
|
var lazyRequire = require("./lazy-require");
|
|
37
|
+
var safeJson = require("./safe-json");
|
|
38
|
+
var scheduler = require("./scheduler");
|
|
38
39
|
var { QueueError } = require("./framework-error");
|
|
39
40
|
|
|
40
41
|
var _err = QueueError.factory;
|
|
@@ -241,7 +242,6 @@ function create(_config) {
|
|
|
241
242
|
if (row && row.repeatCron) {
|
|
242
243
|
try {
|
|
243
244
|
var unsealedRow = cryptoField.unsealRow("_blamejs_jobs", row);
|
|
244
|
-
var scheduler = require("./scheduler");
|
|
245
245
|
var cron = scheduler.parseCron(unsealedRow.repeatCron);
|
|
246
246
|
var nextMs = scheduler.nextCronFire(cron, new Date(nowMs), unsealedRow.repeatTimezone || null);
|
|
247
247
|
await enqueue(unsealedRow.queueName,
|
package/lib/queue.js
CHANGED
|
@@ -31,12 +31,13 @@
|
|
|
31
31
|
* failed (status='failed')
|
|
32
32
|
*/
|
|
33
33
|
var C = require("./constants");
|
|
34
|
-
var localProto = require("./queue-local");
|
|
35
|
-
var retryHelper = require("./retry");
|
|
36
|
-
var safeAsync = require("./safe-async");
|
|
37
34
|
var lazyRequire = require("./lazy-require");
|
|
35
|
+
var nodeCrypto = require("node:crypto");
|
|
38
36
|
var observability = require("./observability");
|
|
39
37
|
var protocolDispatcher = require("./protocol-dispatcher");
|
|
38
|
+
var localProto = require("./queue-local");
|
|
39
|
+
var retryHelper = require("./retry");
|
|
40
|
+
var safeAsync = require("./safe-async");
|
|
40
41
|
var { QueueError } = require("./framework-error");
|
|
41
42
|
|
|
42
43
|
var dispatcher = protocolDispatcher.create({
|
|
@@ -565,7 +566,6 @@ function enqueueFlow(spec) {
|
|
|
565
566
|
return Promise.reject(e);
|
|
566
567
|
}
|
|
567
568
|
|
|
568
|
-
var nodeCrypto = require("node:crypto");
|
|
569
569
|
var flowId = "flow-" + nodeCrypto.randomBytes(8).toString("hex");
|
|
570
570
|
|
|
571
571
|
return observability.tap("queue.enqueueFlow",
|
package/lib/router.js
CHANGED
|
@@ -25,6 +25,7 @@ var fs = require("fs");
|
|
|
25
25
|
var path = require("path");
|
|
26
26
|
var { URL } = require("url");
|
|
27
27
|
var C = require("./constants");
|
|
28
|
+
var safeAsync = require("./safe-async");
|
|
28
29
|
var websocket = require("./websocket");
|
|
29
30
|
var { boot } = require("./log");
|
|
30
31
|
|
|
@@ -217,12 +218,13 @@ class Router {
|
|
|
217
218
|
});
|
|
218
219
|
});
|
|
219
220
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
// Wait up to `timeoutMs` for graceful WS closes, then force-destroy
|
|
222
|
+
// any laggards. `safeAsync.sleep({ unref: true })` matches the
|
|
223
|
+
// framework's outbound-timeout convention.
|
|
224
|
+
await Promise.race([
|
|
225
|
+
Promise.all(closes),
|
|
226
|
+
safeAsync.sleep(timeoutMs, { unref: true }),
|
|
227
|
+
]);
|
|
226
228
|
// Force-destroy any laggards — at this point we've waited the full
|
|
227
229
|
// timeout and they didn't ack. The operator chose timeoutMs; honor it.
|
|
228
230
|
this._activeWsConns.forEach(function (conn) {
|
package/lib/safe-schema.js
CHANGED
|
@@ -109,6 +109,7 @@
|
|
|
109
109
|
* so the two stay distinct rather than one wrapping the other.
|
|
110
110
|
*/
|
|
111
111
|
|
|
112
|
+
var safeJson = require("./safe-json");
|
|
112
113
|
var { defineClass } = require("./framework-error");
|
|
113
114
|
|
|
114
115
|
var SafeSchemaError = defineClass("SafeSchemaError", { alwaysPermanent: true });
|
|
@@ -143,10 +144,17 @@ var ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
|
143
144
|
// base64 (standard alphabet, with optional padding). Base64url variants
|
|
144
145
|
// rejected — operators chain .regex(...) for that.
|
|
145
146
|
var BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
146
|
-
// IPv6 —
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
147
|
+
// IPv6 structural pattern — full 8-hextet, every `::`-compressed shape,
|
|
148
|
+
// `::` and `::1` literals, IPv4-mapped (`::ffff:1.2.3.4`), and 6-prefix
|
|
149
|
+
// + IPv4 tail. Adapted from validator.js (Apache-2.0); zone IDs
|
|
150
|
+
// (`fe80::1%eth0`) are deliberately omitted — the framework rejects
|
|
151
|
+
// them as non-portable, matching `safe-json.formats.ipv6`. Bounded
|
|
152
|
+
// quantifiers, no nested-quantifier alternation, ReDoS-safe.
|
|
153
|
+
//
|
|
154
|
+
// `.ipv6()` schema method delegates to `safeJson.formats.ipv6` for
|
|
155
|
+
// stricter algorithmic validation; this regex is exported as a
|
|
156
|
+
// structural pattern for operators who want it directly.
|
|
157
|
+
var IPV6_RE = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
|
150
158
|
|
|
151
159
|
// ---- helpers ----
|
|
152
160
|
|
|
@@ -424,22 +432,33 @@ function _stringMethods(schema, spec) {
|
|
|
424
432
|
_fail(p, "string/datetime", "must be an ISO-8601 datetime with timezone");
|
|
425
433
|
});
|
|
426
434
|
};
|
|
435
|
+
// IP-address validators delegate to safe-json's algorithmic format
|
|
436
|
+
// checks rather than re-running regex matches. The algorithmic path
|
|
437
|
+
// handles edge cases that pure regex misses (compressed `::` shapes,
|
|
438
|
+
// IPv4-mapped `::ffff:1.2.3.4`, multi-`::` rejection, group-count
|
|
439
|
+
// bounds) and keeps the framework's IP-validation behavior in one
|
|
440
|
+
// tested place. IPV4_RE / IPV6_RE remain exported for operators who
|
|
441
|
+
// want the structural pattern, but `.ipv4()` / `.ipv6()` / `.ip()`
|
|
442
|
+
// are the canonical validation surface.
|
|
427
443
|
schema.ipv4 = function () {
|
|
428
444
|
return chain(function (v, p) {
|
|
429
|
-
return
|
|
430
|
-
|
|
445
|
+
return (typeof v === "string" && safeJson.formats.ipv4(v))
|
|
446
|
+
? { ok: true }
|
|
447
|
+
: _fail(p, "string/ipv4", "must be a valid IPv4 address");
|
|
431
448
|
});
|
|
432
449
|
};
|
|
433
450
|
schema.ipv6 = function () {
|
|
434
451
|
return chain(function (v, p) {
|
|
435
|
-
return
|
|
436
|
-
|
|
452
|
+
return (typeof v === "string" && safeJson.formats.ipv6(v))
|
|
453
|
+
? { ok: true }
|
|
454
|
+
: _fail(p, "string/ipv6", "must be a valid IPv6 address");
|
|
437
455
|
});
|
|
438
456
|
};
|
|
439
457
|
schema.ip = function () {
|
|
440
458
|
return chain(function (v, p) {
|
|
441
|
-
return (
|
|
442
|
-
|
|
459
|
+
return (typeof v === "string" && safeJson.formats.ip(v))
|
|
460
|
+
? { ok: true }
|
|
461
|
+
: _fail(p, "string/ip", "must be a valid IP address (v4 or v6)");
|
|
443
462
|
});
|
|
444
463
|
};
|
|
445
464
|
schema.cuid = function () {
|
|
@@ -1173,4 +1192,17 @@ module.exports = {
|
|
|
1173
1192
|
|
|
1174
1193
|
// Errors
|
|
1175
1194
|
SafeSchemaError: SafeSchemaError,
|
|
1195
|
+
|
|
1196
|
+
// Validation regexes — exported so other modules don't re-declare
|
|
1197
|
+
// their own copies. Pragmatic patterns; operators wanting RFC-strict
|
|
1198
|
+
// behavior chain `.refine()` on top of the schema instead.
|
|
1199
|
+
EMAIL_RE: EMAIL_RE,
|
|
1200
|
+
URL_RE: URL_RE,
|
|
1201
|
+
UUID_RE: UUID_RE,
|
|
1202
|
+
DATE_RE: DATE_RE,
|
|
1203
|
+
DATETIME_RE: DATETIME_RE,
|
|
1204
|
+
IPV4_RE: IPV4_RE,
|
|
1205
|
+
IPV6_RE: IPV6_RE,
|
|
1206
|
+
CUID_RE: CUID_RE,
|
|
1207
|
+
ULID_RE: ULID_RE,
|
|
1176
1208
|
};
|
package/lib/session.js
CHANGED
|
@@ -35,12 +35,13 @@
|
|
|
35
35
|
* verify / count
|
|
36
36
|
* — anywhere (any node can read shared session state)
|
|
37
37
|
*/
|
|
38
|
-
var
|
|
39
|
-
var safeJson = require("./safe-json");
|
|
40
|
-
var cryptoField = require("./crypto-field");
|
|
41
|
-
var clusterStorage = require("./cluster-storage");
|
|
38
|
+
var audit = require("./audit");
|
|
42
39
|
var cluster = require("./cluster");
|
|
40
|
+
var clusterStorage = require("./cluster-storage");
|
|
43
41
|
var C = require("./constants");
|
|
42
|
+
var { generateToken, sha3Hash } = require("./crypto");
|
|
43
|
+
var cryptoField = require("./crypto-field");
|
|
44
|
+
var safeJson = require("./safe-json");
|
|
44
45
|
var { SessionError } = require("./framework-error");
|
|
45
46
|
|
|
46
47
|
var _err = SessionError.factory;
|
|
@@ -260,13 +261,12 @@ async function rotate(oldToken, opts) {
|
|
|
260
261
|
// privilege transition so post-incident review can trace which
|
|
261
262
|
// session id covered which privilege state.
|
|
262
263
|
try {
|
|
263
|
-
var audit = require("./audit");
|
|
264
264
|
audit.emit({
|
|
265
265
|
action: "auth.session.rotate",
|
|
266
266
|
outcome: "success",
|
|
267
267
|
metadata: { reason: opts.reason || "explicit" },
|
|
268
268
|
});
|
|
269
|
-
} catch (_e) { /*
|
|
269
|
+
} catch (_e) { /* audit emit best-effort — never block rotate() */ }
|
|
270
270
|
|
|
271
271
|
return { token: newSid, expiresAt: expiresAt };
|
|
272
272
|
}
|