@blamejs/core 0.6.6 → 0.6.7
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/README.md +3 -3
- package/lib/audit.js +2 -0
- package/lib/external-db.js +100 -14
- package/lib/middleware/db-role-for.js +76 -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.6** (2026-05-01) — request-time DB role binding + Postgres RLS migrations
|
|
11
12
|
- **0.6.5** (2026-05-01) — b.db.declareView + b.externalDb.migrate
|
|
12
13
|
- **0.6.4** (2026-05-01) — wiki schema docs realigned with the actual lib API
|
|
13
14
|
- **0.6.3** (2026-05-01) — externalDb pool tuning + role-aware connect + read-replica routing
|
package/README.md
CHANGED
|
@@ -41,10 +41,10 @@ var b = require("@blamejs/core");
|
|
|
41
41
|
|
|
42
42
|
The framework bundles the surface a typical Node app reaches for. Every primitive listed is callable today; nothing is a stub.
|
|
43
43
|
|
|
44
|
-
- **Data layer** — SQLite with sealed-by-default columns (`b.db`), migrations, seeders, atomic-file writes; S3 / R2 / B2 / GCS / Azure object store with multipart upload + SSE + bucket ops (`b.storage`, `b.objectStore`); durable queue with priority + cron + flows (`b.queue`, `b.jobs`); cluster-shared cache (`b.cache`).
|
|
45
|
-
- **Identity & access** — passwords (Argon2id), passkeys (WebAuthn), TOTP, JWT (PQ-default), OAuth, sessions, brute-force lockout (`b.auth.*`, `b.session`); RBAC (`b.permissions`); API keys with rotation (`b.apiKey`); break-glass column gates with second-factor + audit (`b.breakGlass`).
|
|
44
|
+
- **Data layer** — SQLite with sealed-by-default columns (`b.db`), migrations, seeders, atomic-file writes; bring-your-own external Postgres / MySQL / etc. with pool tuning + role-aware connect + read-replica routing (`b.externalDb`); declarative role-narrowed views and Postgres row-level-security migrations (`b.db.declareView`, `b.db.declareRowPolicy`); S3 / R2 / B2 / GCS / Azure object store with multipart upload + SSE + bucket ops (`b.storage`, `b.objectStore`); durable queue with priority + cron + flows (`b.queue`, `b.jobs`); cluster-shared cache (`b.cache`).
|
|
45
|
+
- **Identity & access** — passwords (Argon2id), passkeys (WebAuthn), TOTP, JWT (PQ-default), OAuth, sessions, brute-force lockout (`b.auth.*`, `b.session`); RBAC with optional per-role DB binding (`b.permissions`, role-spec `dbRole` field); API keys with rotation (`b.apiKey`); break-glass column gates with second-factor + audit (`b.breakGlass`).
|
|
46
46
|
- **Crypto** — envelope-versioned PQC at rest (ML-KEM-1024 + P-384 hybrid, XChaCha20-Poly1305, SHAKE256), vault sealing, field-level crypto, signed webhooks (SLH-DSA-SHAKE-256f), ECIES API encryption (`b.crypto`, `b.vault`, `b.webhook`); pure-JS mTLS CA, PQC TLS gates inbound + outbound (`b.mtlsCa`, `b.pqcGate`, `b.pqcAgent`).
|
|
47
|
-
- **HTTP** — router with schema-validated routes + OpenAPI publication; full middleware stack (CSRF, CORS, rate-limit, security headers, CSP nonce, body parser, compression, SSE, request log) wired by `createApp`; HTTP/1.1 + HTTP/2 outbound client with SSRF gate, redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`).
|
|
47
|
+
- **HTTP** — router with schema-validated routes + OpenAPI publication; full middleware stack (CSRF, CORS, rate-limit, security headers, CSP nonce, body parser, compression, SSE, request log, request-time DB role binding via `b.middleware.dbRoleFor`) wired by `createApp`; HTTP/1.1 + HTTP/2 outbound client with SSRF gate, redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`).
|
|
48
48
|
- **Defensive parsers** — `b.safeJson`, `b.safeBuffer`, `b.safeSql`, `b.safeSchema`, `b.parsers` (XML / TOML / YAML / .env), `b.config` (schema-validated env).
|
|
49
49
|
- **Communication** — WebSockets with channel/room fan-out across cluster replicas (`b.websocket`, `b.websocketChannels`); mail with multipart + attachments + DKIM + calendar invites + bounce intake (`b.mail`, `b.mailBounce`); generic notification dispatcher with operator-supplied transports (`b.notify`).
|
|
50
50
|
- **Observability** — tamper-evident audit chain with SLH-DSA-signed checkpoints, metrics, tracing (OTel pass-through when wired), PII redaction, log-stream sinks, OTLP/HTTP-JSON exporter for any OTel-compatible backend (`b.audit`, `b.metrics`, `b.tracing`, `b.redact`, `b.logStream`, `b.otelExport`).
|
package/lib/audit.js
CHANGED
|
@@ -203,6 +203,8 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
203
203
|
"backup", // b.backup
|
|
204
204
|
"breakglass", // b.breakGlass — column-policy / row-enforcement step-up auth (audit namespace lowercased per the validator's `namespace.verb` rule, same convention as b.apiKey → apikey.*)
|
|
205
205
|
"cache", // b.cache
|
|
206
|
+
"db", // b.db / b.middleware.dbRoleFor / b.externalDb.runAs
|
|
207
|
+
// (role-switching, RLS-shaped events)
|
|
206
208
|
"dkim", // b.mail.dkim (DKIM-Signature generation events)
|
|
207
209
|
"mail", // b.mail (b.mail-bounce uses "system.mail.*")
|
|
208
210
|
"notify", // b.notify
|
package/lib/external-db.js
CHANGED
|
@@ -58,8 +58,14 @@ var safeAsync = require("./safe-async");
|
|
|
58
58
|
var safeSql = require("./safe-sql");
|
|
59
59
|
var { ExternalDbError } = require("./framework-error");
|
|
60
60
|
|
|
61
|
-
var audit
|
|
62
|
-
var db
|
|
61
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
62
|
+
var db = lazyRequire(function () { return require("./db"); });
|
|
63
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
64
|
+
|
|
65
|
+
function _emitMetric(name, value, labels) {
|
|
66
|
+
try { observability().event(name, value, labels || {}); }
|
|
67
|
+
catch (_e) { /* hot-path observability sink — drop silent by design */ }
|
|
68
|
+
}
|
|
63
69
|
|
|
64
70
|
var _err = ExternalDbError.factory;
|
|
65
71
|
|
|
@@ -104,13 +110,21 @@ class Pool {
|
|
|
104
110
|
throw e;
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
|
-
// At max — wait for a release
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
// At max — wait for a release. The waiter's clock starts now;
|
|
114
|
+
// when release() resolves the waiter we emit the wait duration so
|
|
115
|
+
// operators can see backpressure on the pool.
|
|
116
|
+
var self = this;
|
|
117
|
+
var waitStartedAt = Date.now();
|
|
118
|
+
return new Promise(function (resolve, reject) {
|
|
119
|
+
self.waiters.push({
|
|
120
|
+
resolve: function (client) {
|
|
121
|
+
_emitMetric("externaldb.pool.acquire_wait", Date.now() - waitStartedAt,
|
|
122
|
+
{ backend: self.name });
|
|
123
|
+
resolve(client);
|
|
124
|
+
},
|
|
125
|
+
reject: reject,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
release(client) {
|
|
@@ -318,6 +332,7 @@ async function query(sql, params, opts) {
|
|
|
318
332
|
_requireInit();
|
|
319
333
|
opts = opts || {};
|
|
320
334
|
var b = _pickBackend(opts);
|
|
335
|
+
var role = dbRoleContext.getRole();
|
|
321
336
|
|
|
322
337
|
var t0 = Date.now();
|
|
323
338
|
try {
|
|
@@ -344,9 +359,11 @@ async function query(sql, params, opts) {
|
|
|
344
359
|
});
|
|
345
360
|
}, b.retryConfig);
|
|
346
361
|
|
|
362
|
+
var durationMs = Date.now() - t0;
|
|
347
363
|
_emit("system.externaldb.query", "success", {
|
|
348
364
|
backend: b.name,
|
|
349
|
-
|
|
365
|
+
role: role,
|
|
366
|
+
durationMs: durationMs,
|
|
350
367
|
classification: opts.classification || null,
|
|
351
368
|
rowCount: result && result.rowCount,
|
|
352
369
|
// SQL is NOT logged by default — may contain sensitive literal values
|
|
@@ -355,14 +372,33 @@ async function query(sql, params, opts) {
|
|
|
355
372
|
// field-crypto on the audit row).
|
|
356
373
|
sql: opts.includeSqlInAudit ? sql : null,
|
|
357
374
|
});
|
|
375
|
+
_emitMetric("externaldb.query.success", 1,
|
|
376
|
+
{ backend: b.name, role: role || "(none)" });
|
|
377
|
+
_emitMetric("externaldb.query.duration_ms", durationMs,
|
|
378
|
+
{ backend: b.name, role: role || "(none)" });
|
|
358
379
|
return result;
|
|
359
380
|
} catch (e) {
|
|
381
|
+
var failureMs = Date.now() - t0;
|
|
360
382
|
_emit("system.externaldb.query", "failure", {
|
|
361
383
|
backend: b.name,
|
|
362
|
-
|
|
384
|
+
role: role,
|
|
385
|
+
durationMs: failureMs,
|
|
363
386
|
classification: opts.classification || null,
|
|
364
387
|
errorCode: e.code || null,
|
|
365
388
|
}, (e && e.message) || String(e));
|
|
389
|
+
_emitMetric("externaldb.query.failure", 1,
|
|
390
|
+
{ backend: b.name, role: role || "(none)", errorCode: e.code || "(none)" });
|
|
391
|
+
// Postgres signals authorization-denied as SQLSTATE 42501
|
|
392
|
+
// (insufficient_privilege). RLS-shaped writes that violate a
|
|
393
|
+
// policy and GRANT-denied SELECTs both surface this code. The
|
|
394
|
+
// operator's role-views recipe relies on this signal: a row of
|
|
395
|
+
// db.role.denied means a request-time role attempted something its
|
|
396
|
+
// grant or RLS policy forbids — the highest-signal compliance event
|
|
397
|
+
// the externalDb layer can emit.
|
|
398
|
+
if (e && e.code === "42501") {
|
|
399
|
+
_emitMetric("db.role.denied", 1,
|
|
400
|
+
{ backend: b.name, role: role || "(none)" });
|
|
401
|
+
}
|
|
366
402
|
throw e;
|
|
367
403
|
}
|
|
368
404
|
}
|
|
@@ -372,6 +408,7 @@ async function transaction(fn, opts) {
|
|
|
372
408
|
if (typeof fn !== "function") throw _err("INVALID_FN", "transaction requires a function", true);
|
|
373
409
|
opts = opts || {};
|
|
374
410
|
var b = _pickBackend(opts);
|
|
411
|
+
var role = dbRoleContext.getRole();
|
|
375
412
|
|
|
376
413
|
// sessionGucs — per-transaction `SET LOCAL "name" = value` plumbing.
|
|
377
414
|
// Each name validates as a SQL identifier (Postgres GUC names follow
|
|
@@ -398,16 +435,30 @@ async function transaction(fn, opts) {
|
|
|
398
435
|
var result = await fn(txClient);
|
|
399
436
|
await b.commit(client);
|
|
400
437
|
committed = true;
|
|
438
|
+
var durationMs = Date.now() - t0;
|
|
401
439
|
_emit("system.externaldb.transaction", "success", {
|
|
402
|
-
backend: b.name,
|
|
440
|
+
backend: b.name, role: role, durationMs: durationMs,
|
|
441
|
+
classification: opts.classification || null,
|
|
403
442
|
});
|
|
443
|
+
_emitMetric("externaldb.transaction.success", 1,
|
|
444
|
+
{ backend: b.name, role: role || "(none)" });
|
|
445
|
+
_emitMetric("externaldb.transaction.duration_ms", durationMs,
|
|
446
|
+
{ backend: b.name, role: role || "(none)" });
|
|
404
447
|
return result;
|
|
405
448
|
} catch (e) {
|
|
406
449
|
try { if (!committed) await b.rollback(client); } catch (_e) { /* best effort */ }
|
|
450
|
+
var failureMs = Date.now() - t0;
|
|
407
451
|
_emit("system.externaldb.transaction", "failure", {
|
|
408
|
-
backend: b.name,
|
|
452
|
+
backend: b.name, role: role, durationMs: failureMs,
|
|
453
|
+
classification: opts.classification || null,
|
|
409
454
|
errorCode: e.code || null,
|
|
410
455
|
}, (e && e.message) || String(e));
|
|
456
|
+
_emitMetric("externaldb.transaction.failure", 1,
|
|
457
|
+
{ backend: b.name, role: role || "(none)", errorCode: e.code || "(none)" });
|
|
458
|
+
if (e && e.code === "42501") {
|
|
459
|
+
_emitMetric("db.role.denied", 1,
|
|
460
|
+
{ backend: b.name, role: role || "(none)" });
|
|
461
|
+
}
|
|
411
462
|
throw e;
|
|
412
463
|
} finally {
|
|
413
464
|
b.pool.release(client);
|
|
@@ -651,6 +702,7 @@ async function _readQuery(sql, params, opts) {
|
|
|
651
702
|
throw _err("ALL_REPLICAS_UNHEALTHY",
|
|
652
703
|
"backend '" + b.name + "': all replicas unhealthy and fallback disabled", true);
|
|
653
704
|
}
|
|
705
|
+
var role = dbRoleContext.getRole();
|
|
654
706
|
var t0 = Date.now();
|
|
655
707
|
try {
|
|
656
708
|
var client = await replica.pool.acquire();
|
|
@@ -658,12 +710,18 @@ async function _readQuery(sql, params, opts) {
|
|
|
658
710
|
var res = await replica.query(client, sql, params || []);
|
|
659
711
|
replica.pool.release(client);
|
|
660
712
|
replica.consecutiveFailures = 0;
|
|
713
|
+
var durationMs = Date.now() - t0;
|
|
661
714
|
_emit("system.externaldb.read", "success", {
|
|
662
715
|
backend: b.name,
|
|
716
|
+
role: role,
|
|
663
717
|
replicaIdx: replica.index,
|
|
664
|
-
durationMs:
|
|
718
|
+
durationMs: durationMs,
|
|
665
719
|
rowCount: res && res.rowCount,
|
|
666
720
|
});
|
|
721
|
+
_emitMetric("externaldb.read.success", 1,
|
|
722
|
+
{ backend: b.name, role: role || "(none)", replicaIdx: replica.index });
|
|
723
|
+
_emitMetric("externaldb.read.duration_ms", durationMs,
|
|
724
|
+
{ backend: b.name, role: role || "(none)", replicaIdx: replica.index });
|
|
667
725
|
return res;
|
|
668
726
|
} catch (e) {
|
|
669
727
|
// Connection-shape errors mark unhealthy + destroy.
|
|
@@ -681,10 +739,17 @@ async function _readQuery(sql, params, opts) {
|
|
|
681
739
|
} catch (e) {
|
|
682
740
|
_emit("system.externaldb.read", "failure", {
|
|
683
741
|
backend: b.name,
|
|
742
|
+
role: role,
|
|
684
743
|
replicaIdx: replica.index,
|
|
685
744
|
durationMs: Date.now() - t0,
|
|
686
745
|
errorCode: e.code || null,
|
|
687
746
|
}, (e && e.message) || String(e));
|
|
747
|
+
_emitMetric("externaldb.read.failure", 1,
|
|
748
|
+
{ backend: b.name, role: role || "(none)", errorCode: e.code || "(none)" });
|
|
749
|
+
if (e && e.code === "42501") {
|
|
750
|
+
_emitMetric("db.role.denied", 1,
|
|
751
|
+
{ backend: b.name, role: role || "(none)" });
|
|
752
|
+
}
|
|
688
753
|
// Fallback to primary on a failed replica read when allowed.
|
|
689
754
|
if (b.replicaFallbackToPrimary) {
|
|
690
755
|
return query(sql, params, opts);
|
|
@@ -936,6 +1001,27 @@ function runAs(role, fn) {
|
|
|
936
1001
|
}
|
|
937
1002
|
safeSql.validateIdentifier(role, { allowReserved: false });
|
|
938
1003
|
}
|
|
1004
|
+
// Audit the role transition. runAs has no req, so the actor 5 W's
|
|
1005
|
+
// come from whatever the caller has bound on the audit-context ALS
|
|
1006
|
+
// (log.js requestId, plus any request-bound actor that was set in
|
|
1007
|
+
// an outer scope). Same audit shape as the dbRoleFor middleware
|
|
1008
|
+
// path — forensic walkers can reconstruct the role timeline whether
|
|
1009
|
+
// the binding came from request middleware or a job runner.
|
|
1010
|
+
var previousRole = dbRoleContext.getRole();
|
|
1011
|
+
var newRole = role || null;
|
|
1012
|
+
if (previousRole !== newRole) {
|
|
1013
|
+
audit().safeEmit({
|
|
1014
|
+
action: "db.role.switched",
|
|
1015
|
+
actor: {},
|
|
1016
|
+
resource: { kind: "db.role", id: newRole || "(none)" },
|
|
1017
|
+
outcome: "success",
|
|
1018
|
+
metadata: {
|
|
1019
|
+
previousRole: previousRole,
|
|
1020
|
+
newRole: newRole,
|
|
1021
|
+
source: "runAs",
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
939
1025
|
return dbRoleContext.runWithRole(role || null, fn);
|
|
940
1026
|
}
|
|
941
1027
|
|
|
@@ -62,15 +62,23 @@
|
|
|
62
62
|
* Observability event: db.role.bound { value: 1, labels: { role, source } }
|
|
63
63
|
* source ∈ "resolver" | "permissions" | "default"
|
|
64
64
|
*
|
|
65
|
-
* Audit emission
|
|
66
|
-
*
|
|
65
|
+
* Audit emission: db.role.switched is recorded once per request when a
|
|
66
|
+
* role binds. The audit row carries the actor 5 W's via
|
|
67
|
+
* requestHelpers.extractActorContext and metadata { previousRole,
|
|
68
|
+
* newRole, source }. Defaults align with the framework's "the
|
|
69
|
+
* authorization decision IS the audit-worthy event" stance — both
|
|
70
|
+
* auditFailures and auditSuccess default true. The audit sink can be
|
|
71
|
+
* pinned via opts.audit (any object exposing safeEmit), defaults to
|
|
72
|
+
* the framework's b.audit.
|
|
67
73
|
*/
|
|
68
74
|
var dbRoleContext = require("../db-role-context");
|
|
69
75
|
var lazyRequire = require("../lazy-require");
|
|
76
|
+
var requestHelpers = require("../request-helpers");
|
|
70
77
|
var safeSql = require("../safe-sql");
|
|
71
78
|
var validateOpts = require("../validate-opts");
|
|
72
79
|
var { defineClass } = require("../framework-error");
|
|
73
80
|
|
|
81
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
74
82
|
var observability = lazyRequire(function () { return require("../observability"); });
|
|
75
83
|
|
|
76
84
|
var DbRoleForError = defineClass("DbRoleForError", { alwaysPermanent: true });
|
|
@@ -79,6 +87,7 @@ var _err = function (code, message) { return new DbRoleForError(code, message);
|
|
|
79
87
|
var ALLOWED_OPTS = [
|
|
80
88
|
"resolve", "permissions", "defaultRole",
|
|
81
89
|
"requireRole", "missingRoleStatus", "responder",
|
|
90
|
+
"audit", "auditFailures", "auditSuccess",
|
|
82
91
|
];
|
|
83
92
|
|
|
84
93
|
function _emitEvent(name, value, labels) {
|
|
@@ -140,6 +149,20 @@ function create(opts) {
|
|
|
140
149
|
"middleware.dbRoleFor: missingRoleStatus must be an HTTP status code (100-599)");
|
|
141
150
|
}
|
|
142
151
|
}
|
|
152
|
+
if (opts.audit !== undefined && opts.audit !== null) {
|
|
153
|
+
if (typeof opts.audit !== "object" || typeof opts.audit.safeEmit !== "function") {
|
|
154
|
+
throw _err("db-role-for/bad-opt",
|
|
155
|
+
"middleware.dbRoleFor: audit must be a b.audit-shaped object (safeEmit fn)");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (opts.auditFailures !== undefined && typeof opts.auditFailures !== "boolean") {
|
|
159
|
+
throw _err("db-role-for/bad-opt",
|
|
160
|
+
"middleware.dbRoleFor: auditFailures must be a boolean");
|
|
161
|
+
}
|
|
162
|
+
if (opts.auditSuccess !== undefined && typeof opts.auditSuccess !== "boolean") {
|
|
163
|
+
throw _err("db-role-for/bad-opt",
|
|
164
|
+
"middleware.dbRoleFor: auditSuccess must be a boolean");
|
|
165
|
+
}
|
|
143
166
|
|
|
144
167
|
var resolveFn = opts.resolve || null;
|
|
145
168
|
var perms = opts.permissions || null;
|
|
@@ -147,6 +170,14 @@ function create(opts) {
|
|
|
147
170
|
var requireRole = !!opts.requireRole;
|
|
148
171
|
var missingRoleStatus = opts.missingRoleStatus || 401;
|
|
149
172
|
var responder = opts.responder || _defaultResponder;
|
|
173
|
+
// Audit defaults match permissions: the role-binding decision IS the
|
|
174
|
+
// audit-worthy act. Operators with extreme volume opt out via
|
|
175
|
+
// auditSuccess: false; failures stay on regardless. The audit sink
|
|
176
|
+
// defaults to the framework's b.audit; operators with multiple audit
|
|
177
|
+
// chains pass their own (matches captureAudit's shape).
|
|
178
|
+
var auditSink = opts.audit || null;
|
|
179
|
+
var auditFailures = (opts.auditFailures === undefined) ? true : opts.auditFailures;
|
|
180
|
+
var auditSuccess = (opts.auditSuccess === undefined) ? true : opts.auditSuccess;
|
|
150
181
|
|
|
151
182
|
return function dbRoleForMiddleware(req, res, next) {
|
|
152
183
|
var role = null;
|
|
@@ -182,6 +213,15 @@ function create(opts) {
|
|
|
182
213
|
if (!role) {
|
|
183
214
|
if (requireRole) {
|
|
184
215
|
_emitEvent("db.role.missing", 1, {});
|
|
216
|
+
if (auditFailures) {
|
|
217
|
+
_auditSwitch(auditSink, req, {
|
|
218
|
+
previousRole: dbRoleContext.getRole(),
|
|
219
|
+
newRole: null,
|
|
220
|
+
source: "middleware",
|
|
221
|
+
outcome: "failure",
|
|
222
|
+
reason: "no-role",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
185
225
|
return responder(req, res, missingRoleStatus, {
|
|
186
226
|
error: "missing_db_role",
|
|
187
227
|
status: missingRoleStatus,
|
|
@@ -206,12 +246,46 @@ function create(opts) {
|
|
|
206
246
|
return next(e);
|
|
207
247
|
}
|
|
208
248
|
|
|
249
|
+
var previousRole = dbRoleContext.getRole();
|
|
209
250
|
req.dbRole = role;
|
|
210
251
|
_emitEvent("db.role.bound", 1, { role: role, source: source });
|
|
252
|
+
if (auditSuccess) {
|
|
253
|
+
_auditSwitch(auditSink, req, {
|
|
254
|
+
previousRole: previousRole,
|
|
255
|
+
newRole: role,
|
|
256
|
+
source: "middleware",
|
|
257
|
+
outcome: "success",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
211
260
|
dbRoleContext.runWithRole(role, function () { next(); });
|
|
212
261
|
};
|
|
213
262
|
}
|
|
214
263
|
|
|
264
|
+
// Emit the db.role.switched audit row. Fire-and-forget — the audit
|
|
265
|
+
// handler's own try/catch keeps a momentary outage from breaking the
|
|
266
|
+
// request. The actor 5 W's come from extractActorContext (req-driven);
|
|
267
|
+
// metadata carries the previous + new role + binding source so a
|
|
268
|
+
// forensic walker can reconstruct "which role read which row when."
|
|
269
|
+
// The sink defaults to the framework's b.audit when the operator
|
|
270
|
+
// didn't pass an explicit instance.
|
|
271
|
+
function _auditSwitch(sink, req, info) {
|
|
272
|
+
try {
|
|
273
|
+
var emitter = sink || audit();
|
|
274
|
+
emitter.safeEmit({
|
|
275
|
+
action: "db.role.switched",
|
|
276
|
+
actor: requestHelpers.extractActorContext(req),
|
|
277
|
+
resource: { kind: "db.role", id: info.newRole || "(none)" },
|
|
278
|
+
outcome: info.outcome || "success",
|
|
279
|
+
reason: info.reason || null,
|
|
280
|
+
metadata: {
|
|
281
|
+
previousRole: info.previousRole || null,
|
|
282
|
+
newRole: info.newRole || null,
|
|
283
|
+
source: info.source,
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
} catch (_e) { /* audit best-effort */ }
|
|
287
|
+
}
|
|
288
|
+
|
|
215
289
|
module.exports = {
|
|
216
290
|
create: create,
|
|
217
291
|
DbRoleForError: DbRoleForError,
|