@blamejs/core 0.14.26 → 0.15.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 +2 -2
- package/index.js +4 -0
- package/lib/agent-envelope-mac.js +104 -0
- package/lib/agent-event-bus.js +105 -4
- package/lib/agent-posture-chain.js +8 -42
- package/lib/ai-content-detect.js +9 -10
- package/lib/api-key.js +107 -74
- package/lib/atomic-file.js +62 -4
- package/lib/audit-chain.js +47 -11
- package/lib/audit-sign.js +77 -2
- package/lib/audit-tools.js +79 -51
- package/lib/audit.js +249 -123
- package/lib/auth/openid-federation.js +108 -47
- package/lib/backup/index.js +13 -10
- package/lib/break-glass.js +202 -144
- package/lib/cache.js +174 -105
- package/lib/chain-writer.js +38 -16
- package/lib/cli.js +19 -14
- package/lib/cluster-provider-db.js +130 -104
- package/lib/cluster-storage.js +119 -22
- package/lib/cluster.js +119 -71
- package/lib/compliance.js +169 -4
- package/lib/consent.js +73 -24
- package/lib/constants.js +16 -11
- package/lib/crypto-field.js +474 -92
- package/lib/db-declare-row-policy.js +35 -22
- package/lib/db-file-lifecycle.js +3 -2
- package/lib/db-query.js +497 -255
- package/lib/db-schema.js +209 -44
- package/lib/db.js +176 -95
- package/lib/error-page.js +14 -1
- package/lib/external-db-migrate.js +229 -139
- package/lib/external-db.js +25 -15
- package/lib/file-upload.js +52 -7
- package/lib/framework-error.js +14 -1
- package/lib/framework-files.js +73 -0
- package/lib/framework-schema.js +695 -394
- package/lib/gate-contract.js +649 -1
- package/lib/guard-agent-registry.js +26 -44
- package/lib/guard-all.js +1 -0
- package/lib/guard-auth.js +42 -112
- package/lib/guard-cidr.js +33 -154
- package/lib/guard-csv.js +46 -113
- package/lib/guard-domain.js +34 -157
- package/lib/guard-dsn.js +27 -43
- package/lib/guard-email.js +47 -69
- package/lib/guard-envelope.js +19 -32
- package/lib/guard-event-bus-payload.js +24 -42
- package/lib/guard-event-bus-topic.js +25 -43
- package/lib/guard-filename.js +42 -106
- package/lib/guard-graphql.js +42 -123
- package/lib/guard-html.js +53 -108
- package/lib/guard-idempotency-key.js +24 -42
- package/lib/guard-image.js +46 -103
- package/lib/guard-imap-command.js +18 -32
- package/lib/guard-jmap.js +16 -30
- package/lib/guard-json.js +38 -108
- package/lib/guard-jsonpath.js +38 -171
- package/lib/guard-jwt.js +49 -179
- package/lib/guard-list-id.js +25 -41
- package/lib/guard-list-unsubscribe.js +27 -43
- package/lib/guard-mail-compose.js +24 -42
- package/lib/guard-mail-move.js +26 -44
- package/lib/guard-mail-query.js +28 -46
- package/lib/guard-mail-reply.js +24 -42
- package/lib/guard-mail-sieve.js +24 -42
- package/lib/guard-managesieve-command.js +17 -31
- package/lib/guard-markdown.js +37 -104
- package/lib/guard-message-id.js +26 -45
- package/lib/guard-mime.js +39 -151
- package/lib/guard-oauth.js +54 -135
- package/lib/guard-pdf.js +45 -101
- package/lib/guard-pop3-command.js +21 -31
- package/lib/guard-posture-chain.js +24 -42
- package/lib/guard-regex.js +33 -107
- package/lib/guard-saga-config.js +24 -42
- package/lib/guard-shell.js +42 -172
- package/lib/guard-smtp-command.js +48 -54
- package/lib/guard-snapshot-envelope.js +24 -42
- package/lib/guard-sql.js +1491 -0
- package/lib/guard-stream-args.js +24 -43
- package/lib/guard-svg.js +47 -65
- package/lib/guard-template.js +35 -172
- package/lib/guard-tenant-id.js +26 -45
- package/lib/guard-time.js +32 -154
- package/lib/guard-trace-context.js +25 -44
- package/lib/guard-uuid.js +32 -153
- package/lib/guard-xml.js +38 -113
- package/lib/guard-yaml.js +51 -163
- package/lib/http-client.js +37 -9
- package/lib/inbox.js +120 -107
- package/lib/legal-hold.js +107 -50
- package/lib/log-stream-cloudwatch.js +47 -31
- package/lib/log-stream-otlp.js +32 -18
- package/lib/mail-crypto-smime.js +2 -6
- package/lib/mail-greylist.js +2 -6
- package/lib/mail-helo.js +2 -6
- package/lib/mail-journal.js +85 -64
- package/lib/mail-rbl.js +2 -6
- package/lib/mail-scan.js +2 -6
- package/lib/mail-server-jmap.js +117 -12
- package/lib/mail-spam-score.js +2 -6
- package/lib/mail-store.js +287 -154
- package/lib/middleware/body-parser.js +71 -25
- package/lib/middleware/csrf-protect.js +19 -8
- package/lib/middleware/fetch-metadata.js +17 -7
- package/lib/middleware/idempotency-key.js +54 -38
- package/lib/middleware/rate-limit.js +102 -32
- package/lib/middleware/security-headers.js +21 -5
- package/lib/migrations.js +108 -66
- package/lib/network-heartbeat.js +7 -0
- package/lib/nonce-store.js +31 -9
- package/lib/object-store/azure-blob-bucket-ops.js +9 -4
- package/lib/object-store/azure-blob.js +57 -3
- package/lib/object-store/sigv4.js +10 -0
- package/lib/observability.js +87 -0
- package/lib/otel-export.js +25 -1
- package/lib/outbox.js +136 -82
- package/lib/parsers/safe-xml.js +47 -7
- package/lib/pqc-agent.js +44 -0
- package/lib/pubsub-cluster.js +42 -20
- package/lib/queue-local.js +202 -139
- package/lib/queue-redis.js +9 -1
- package/lib/queue-sqs.js +6 -0
- package/lib/redact.js +68 -11
- package/lib/redis-client.js +160 -31
- package/lib/retention.js +82 -39
- package/lib/router.js +212 -5
- package/lib/safe-dns.js +29 -45
- package/lib/safe-ical.js +18 -33
- package/lib/safe-icap.js +27 -43
- package/lib/safe-sieve.js +21 -40
- package/lib/safe-sql.js +124 -3
- package/lib/safe-vcard.js +18 -33
- package/lib/scheduler.js +35 -12
- package/lib/seeders.js +122 -74
- package/lib/session-stores.js +42 -14
- package/lib/session.js +109 -72
- package/lib/sql.js +3885 -0
- package/lib/ssrf-guard.js +51 -4
- package/lib/static.js +177 -34
- package/lib/subject.js +55 -17
- package/lib/vault/index.js +3 -2
- package/lib/vault/passphrase-ops.js +3 -2
- package/lib/vault/rotate.js +104 -64
- package/lib/vendor-data.js +2 -0
- package/lib/websocket.js +35 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/migrations.js
CHANGED
|
@@ -41,10 +41,13 @@
|
|
|
41
41
|
var nodePath = require("node:path");
|
|
42
42
|
var atomicFile = require("./atomic-file");
|
|
43
43
|
var dbSchema = require("./db-schema");
|
|
44
|
+
var frameworkSchema = require("./framework-schema");
|
|
44
45
|
var lazyRequire = require("./lazy-require");
|
|
45
46
|
var { boot } = require("./log");
|
|
46
47
|
var migrationFiles = require("./migration-files");
|
|
47
48
|
var numericBounds = require("./numeric-bounds");
|
|
49
|
+
var safeSql = require("./safe-sql");
|
|
50
|
+
var sql = require("./sql");
|
|
48
51
|
var db = lazyRequire(function () { return require("./db"); });
|
|
49
52
|
var validateOpts = require("./validate-opts");
|
|
50
53
|
var { FrameworkError } = require("./framework-error");
|
|
@@ -60,11 +63,29 @@ class MigrationError extends FrameworkError {
|
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
66
|
+
// Logical names; the physical names resolve through
|
|
67
|
+
// frameworkSchema.tableName so a configured table prefix flows here too.
|
|
68
|
+
// SQL is composed with b.sql (quoteName: true) so the resolved name is
|
|
69
|
+
// quoted by construction — a reserved-word / whitespace-bearing name
|
|
70
|
+
// still emits a valid `"..."` identifier.
|
|
71
|
+
var MIGRATIONS_TABLE = "_blamejs_migrations"; // allow:hand-rolled-sql — logical name declaration; physical name + prefix resolve via frameworkSchema.tableName below
|
|
72
|
+
function _migrationsTable() { return frameworkSchema.tableName(MIGRATIONS_TABLE); }
|
|
73
|
+
// b.sql opts for the migration bookkeeping statements. db.prepare /
|
|
74
|
+
// runSqlOnHandle run these directly against the handle (never
|
|
75
|
+
// clusterStorage), so the dialect must match the handle: db.from()'s local
|
|
76
|
+
// node:sqlite default, or an operator's own Postgres / MySQL handle (which
|
|
77
|
+
// declares `handle.dialect`). The handle-dialect / opts / key-text-type
|
|
78
|
+
// resolution is shared with db-schema's reconciler + seeders.js, so it is
|
|
79
|
+
// composed from db-schema rather than re-derived here. The historical
|
|
80
|
+
// default (sqlite) is byte-identical for every existing local-handle caller.
|
|
81
|
+
var _handleDialect = dbSchema.handleDialect;
|
|
82
|
+
var _sqlOpts = dbSchema.sqlOpts;
|
|
83
|
+
var _keyTextType = dbSchema.keyTextType;
|
|
84
|
+
// A ms-epoch column type. Date.now() exceeds a 32-bit INTEGER, so the
|
|
85
|
+
// lock timestamp needs a 64-bit type on Postgres + MySQL (BIGINT) — b.sql's
|
|
86
|
+
// logical "int" resolves to BIGINT on both and INTEGER on SQLite, so passing
|
|
87
|
+
// the logical name through the handle dialect is enough.
|
|
88
|
+
var _MS_EPOCH_TYPE = "int";
|
|
68
89
|
// Filename grammar: leading numeric prefix (any width), then '-', then a
|
|
69
90
|
// non-empty body, then '.js'. Numeric prefix orders execution. Letters
|
|
70
91
|
// in the body include hyphens, underscores, and alphanumerics; anything
|
|
@@ -87,31 +108,36 @@ function _isMigrationFile(name) {
|
|
|
87
108
|
var _runSql = dbSchema.runSqlOnHandle;
|
|
88
109
|
|
|
89
110
|
function _ensureTable(db) {
|
|
90
|
-
_runSql(db,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"
|
|
94
|
-
|
|
95
|
-
")"
|
|
96
|
-
);
|
|
111
|
+
_runSql(db, sql.createTable(_migrationsTable(), [
|
|
112
|
+
{ name: "name", type: _keyTextType(db), primaryKey: true },
|
|
113
|
+
{ name: "description", type: "text" },
|
|
114
|
+
{ name: "appliedAt", type: "text", notNull: true },
|
|
115
|
+
], _sqlOpts(db)).sql);
|
|
97
116
|
}
|
|
98
117
|
|
|
99
118
|
// Single-row advisory-lock table. Two processes running `migrate up`
|
|
100
119
|
// concurrently against the same DB race on this table: the winner of
|
|
101
120
|
// the INSERT acquires the lock; the loser sees a UNIQUE violation and
|
|
102
121
|
// the operator gets a clear "lock held by other process" error.
|
|
103
|
-
var LOCK_TABLE
|
|
104
|
-
|
|
122
|
+
var LOCK_TABLE = "_blamejs_migrations_lock"; // allow:hand-rolled-sql — logical name declaration; physical name + prefix resolve via frameworkSchema.tableName below
|
|
123
|
+
function _lockTable() { return frameworkSchema.tableName(LOCK_TABLE); }
|
|
105
124
|
|
|
106
125
|
function _ensureLockTable(db) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
// The single-row invariant (CHECK scope = 'lock') is a static,
|
|
127
|
+
// framework-controlled column constraint — b.sql guards the verbatim
|
|
128
|
+
// fragment (allowLiterals) and quotes the column by construction. The
|
|
129
|
+
// CHECK references `scope` with the handle's identifier quoting (backtick
|
|
130
|
+
// on mysql) so the constraint parses on every dialect. lockedAt is an
|
|
131
|
+
// ms-epoch value (`int` → BIGINT on Postgres/MySQL, INTEGER on SQLite);
|
|
132
|
+
// a 32-bit INTEGER would overflow Date.now() and make the lock
|
|
133
|
+
// unacquirable on Postgres.
|
|
134
|
+
var dialect = _handleDialect(db);
|
|
135
|
+
var scopeCheck = "CHECK (" + safeSql.quoteIdentifier("scope", dialect, { allowReserved: true }) + " = 'lock')";
|
|
136
|
+
_runSql(db, sql.createTable(_lockTable(), [
|
|
137
|
+
{ name: "scope", type: _keyTextType(db), primaryKey: true, constraints: scopeCheck },
|
|
138
|
+
{ name: "lockedAt", type: _MS_EPOCH_TYPE, notNull: true },
|
|
139
|
+
{ name: "lockedBy", type: "text", notNull: true },
|
|
140
|
+
], _sqlOpts(db)).sql);
|
|
115
141
|
}
|
|
116
142
|
|
|
117
143
|
function _lockHolderId() {
|
|
@@ -139,23 +165,24 @@ function _acquireLock(db, opts) {
|
|
|
139
165
|
} else {
|
|
140
166
|
staleAfterMs = opts.staleAfterMs;
|
|
141
167
|
}
|
|
168
|
+
var insertLock = sql.insert(_lockTable(), _sqlOpts(db))
|
|
169
|
+
.values({ scope: "lock", lockedAt: nowMs, lockedBy: holder }).toSql();
|
|
142
170
|
// Try to insert; if there's a stale lock, optionally force-replace it.
|
|
143
171
|
try {
|
|
144
|
-
db.prepare(
|
|
145
|
-
|
|
146
|
-
).run(nowMs, holder);
|
|
172
|
+
var insStmt = db.prepare(insertLock.sql);
|
|
173
|
+
insStmt.run.apply(insStmt, insertLock.params);
|
|
147
174
|
return holder;
|
|
148
175
|
} catch {
|
|
149
176
|
// PRIMARY KEY conflict → existing lock. Inspect it.
|
|
150
|
-
var
|
|
151
|
-
"
|
|
152
|
-
|
|
177
|
+
var selExisting = sql.select(_lockTable(), _sqlOpts(db))
|
|
178
|
+
.columns(["lockedAt", "lockedBy"]).where("scope", "lock").toSql();
|
|
179
|
+
var selStmt = db.prepare(selExisting.sql);
|
|
180
|
+
var existing = selStmt.get.apply(selStmt, selExisting.params);
|
|
153
181
|
if (!existing) {
|
|
154
182
|
// Race window between INSERT failure and SELECT — try once more.
|
|
155
183
|
try {
|
|
156
|
-
db.prepare(
|
|
157
|
-
|
|
158
|
-
).run(nowMs, holder);
|
|
184
|
+
var retryStmt = db.prepare(insertLock.sql);
|
|
185
|
+
retryStmt.run.apply(retryStmt, insertLock.params);
|
|
159
186
|
return holder;
|
|
160
187
|
} catch (e2) {
|
|
161
188
|
throw new MigrationError("migrations/lock-busy",
|
|
@@ -165,25 +192,32 @@ function _acquireLock(db, opts) {
|
|
|
165
192
|
}
|
|
166
193
|
var ageMs = nowMs - Number(existing.lockedAt);
|
|
167
194
|
if (staleAfterMs > 0 && ageMs > staleAfterMs) {
|
|
168
|
-
// Force-replace the stale lock.
|
|
169
|
-
//
|
|
170
|
-
|
|
195
|
+
// Force-replace the stale lock. The DELETE + INSERT run in a single
|
|
196
|
+
// transaction so the next process can't slip in between. The
|
|
197
|
+
// transaction boundary is dialect-aware: only SQLite has the
|
|
198
|
+
// `BEGIN IMMEDIATE` write-lock-up-front form — Postgres + MySQL
|
|
199
|
+
// reject the `IMMEDIATE` keyword, so the shared runInTransaction
|
|
200
|
+
// helper emits a plain portable `BEGIN`/`COMMIT`/`ROLLBACK` there.
|
|
201
|
+
var lockMode = _handleDialect(db) === "sqlite" ? "IMMEDIATE" : null;
|
|
171
202
|
try {
|
|
172
|
-
|
|
173
|
-
.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
203
|
+
return dbSchema.runInTransaction(db, function () {
|
|
204
|
+
var delStale = sql.delete(_lockTable(), _sqlOpts(db))
|
|
205
|
+
.where("scope", "lock").where("lockedAt", existing.lockedAt).toSql();
|
|
206
|
+
var delStaleStmt = db.prepare(delStale.sql);
|
|
207
|
+
delStaleStmt.run.apply(delStaleStmt, delStale.params);
|
|
208
|
+
var replStmt = db.prepare(insertLock.sql);
|
|
209
|
+
replStmt.run.apply(replStmt, insertLock.params);
|
|
210
|
+
return holder;
|
|
211
|
+
}, {
|
|
212
|
+
lockMode: lockMode,
|
|
213
|
+
onRollbackFail: function (rollbackErr) {
|
|
214
|
+
log.debug("rollback-failed", {
|
|
215
|
+
op: "lock-stale-replace",
|
|
216
|
+
error: rollbackErr && rollbackErr.message,
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
});
|
|
179
220
|
} catch (forceErr) {
|
|
180
|
-
try { _runSql(db, "ROLLBACK"); }
|
|
181
|
-
catch (rollbackErr) {
|
|
182
|
-
log.debug("rollback-failed", {
|
|
183
|
-
op: "lock-stale-replace",
|
|
184
|
-
error: rollbackErr && rollbackErr.message,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
221
|
throw new MigrationError("migrations/lock-stale-replace-failed",
|
|
188
222
|
"could not replace stale lock: " + ((forceErr && forceErr.message) || String(forceErr)),
|
|
189
223
|
true);
|
|
@@ -202,9 +236,10 @@ function _releaseLock(db, holder) {
|
|
|
202
236
|
// shouldn't have its lock cleared by an unrelated next deploy unless
|
|
203
237
|
// the operator explicitly used the staleAfterMs nodePath.
|
|
204
238
|
try {
|
|
205
|
-
|
|
206
|
-
"
|
|
207
|
-
|
|
239
|
+
var rel = sql.delete(_lockTable(), _sqlOpts(db))
|
|
240
|
+
.where("scope", "lock").where("lockedBy", holder).toSql();
|
|
241
|
+
var relStmt = db.prepare(rel.sql);
|
|
242
|
+
relStmt.run.apply(relStmt, rel.params);
|
|
208
243
|
} catch (_e) { /* best-effort release; operator can DELETE manually */ }
|
|
209
244
|
}
|
|
210
245
|
|
|
@@ -271,10 +306,11 @@ function create(opts) {
|
|
|
271
306
|
function _appliedRows() {
|
|
272
307
|
var db = _resolveDb(opts);
|
|
273
308
|
_ensureTable(db);
|
|
274
|
-
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
|
|
309
|
+
var q = sql.select(_migrationsTable(), _sqlOpts(db))
|
|
310
|
+
.columns(["name", "description", "appliedAt"])
|
|
311
|
+
.orderBy("appliedAt", "asc").orderBy("name", "asc").toSql();
|
|
312
|
+
var stmt = db.prepare(q.sql);
|
|
313
|
+
return stmt.all.apply(stmt, q.params);
|
|
278
314
|
}
|
|
279
315
|
|
|
280
316
|
function status() {
|
|
@@ -293,8 +329,10 @@ function create(opts) {
|
|
|
293
329
|
var db = _resolveDb(opts);
|
|
294
330
|
_ensureTable(db);
|
|
295
331
|
return _withLock(db, opts, function () {
|
|
332
|
+
var namesQ = sql.select(_migrationsTable(), _sqlOpts(db)).columns(["name"]).toSql();
|
|
333
|
+
var namesStmt = db.prepare(namesQ.sql);
|
|
296
334
|
var appliedSet = new Set(
|
|
297
|
-
|
|
335
|
+
namesStmt.all.apply(namesStmt, namesQ.params)
|
|
298
336
|
.map(function (r) { return r.name; })
|
|
299
337
|
);
|
|
300
338
|
var files = _list(dir);
|
|
@@ -307,10 +345,11 @@ function create(opts) {
|
|
|
307
345
|
try {
|
|
308
346
|
_txn(db, function () {
|
|
309
347
|
mod.up(db);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
348
|
+
var insQ = sql.insert(_migrationsTable(), _sqlOpts(db))
|
|
349
|
+
.values({ name: file, description: mod.description || "",
|
|
350
|
+
appliedAt: new Date().toISOString() }).toSql();
|
|
351
|
+
var insStmt = db.prepare(insQ.sql);
|
|
352
|
+
insStmt.run.apply(insStmt, insQ.params);
|
|
314
353
|
});
|
|
315
354
|
} catch (e) {
|
|
316
355
|
throw new MigrationError("migrations/up-failed",
|
|
@@ -336,11 +375,12 @@ function create(opts) {
|
|
|
336
375
|
return _withLock(db, opts, function () {
|
|
337
376
|
// Most-recent applied first (reverse chronological by appliedAt
|
|
338
377
|
// then by name as a stable tiebreaker for fixtures with identical
|
|
339
|
-
// timestamps).
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
"
|
|
343
|
-
|
|
378
|
+
// timestamps). steps is a validated positive integer, so b.sql
|
|
379
|
+
// inlines the LIMIT.
|
|
380
|
+
var downQ = sql.select(_migrationsTable(), _sqlOpts(db)).columns(["name"])
|
|
381
|
+
.orderBy("appliedAt", "desc").orderBy("name", "desc").limit(steps).toSql();
|
|
382
|
+
var downStmt = db.prepare(downQ.sql);
|
|
383
|
+
var rows = downStmt.all.apply(downStmt, downQ.params);
|
|
344
384
|
|
|
345
385
|
var reverted = [];
|
|
346
386
|
for (var i = 0; i < rows.length; i++) {
|
|
@@ -355,7 +395,9 @@ function create(opts) {
|
|
|
355
395
|
try {
|
|
356
396
|
_txn(db, function () {
|
|
357
397
|
mod.down(db);
|
|
358
|
-
|
|
398
|
+
var delQ = sql.delete(_migrationsTable(), _sqlOpts(db)).where("name", file).toSql();
|
|
399
|
+
var delStmt = db.prepare(delQ.sql);
|
|
400
|
+
delStmt.run.apply(delStmt, delQ.params);
|
|
359
401
|
});
|
|
360
402
|
} catch (e) {
|
|
361
403
|
throw new MigrationError("migrations/down-failed",
|
package/lib/network-heartbeat.js
CHANGED
|
@@ -72,6 +72,13 @@ function _probeHttp(target, timeoutMs) {
|
|
|
72
72
|
url: target.url,
|
|
73
73
|
method: target.method || "GET",
|
|
74
74
|
timeoutMs: timeoutMs,
|
|
75
|
+
// Forward the target's protocol/host allowlists so an operator who
|
|
76
|
+
// opts a cleartext http:// heartbeat in (allowedProtocols:
|
|
77
|
+
// b.safeUrl.ALLOW_HTTP_ALL) is honoured. Left undefined, httpClient
|
|
78
|
+
// applies its https-only default (ALLOW_HTTP_TLS) — so an http://
|
|
79
|
+
// target with no opt-in is still rejected, not silently probed.
|
|
80
|
+
allowedProtocols: target.allowedProtocols,
|
|
81
|
+
allowedHosts: target.allowedHosts,
|
|
75
82
|
allowInternal: target.allowInternal === true ? true : target.allowInternal,
|
|
76
83
|
});
|
|
77
84
|
p.then(function (res) {
|
package/lib/nonce-store.js
CHANGED
|
@@ -43,10 +43,30 @@
|
|
|
43
43
|
|
|
44
44
|
var clusterStorage = require("./cluster-storage");
|
|
45
45
|
var C = require("./constants");
|
|
46
|
+
var frameworkSchema = require("./framework-schema");
|
|
46
47
|
var safeAsync = require("./safe-async");
|
|
48
|
+
var sql = require("./sql");
|
|
47
49
|
var { defineClass } = require("./framework-error");
|
|
48
50
|
var { boundedMap } = require("./bounded-map");
|
|
49
51
|
|
|
52
|
+
// Cluster-backend table — resolved through frameworkSchema.tableName so a
|
|
53
|
+
// configured table prefix (b.frameworkSchema.setTablePrefix) is honored.
|
|
54
|
+
// The name is identity-mapped in LOCAL_TO_EXTERNAL, so clusterStorage's
|
|
55
|
+
// resolveTables leaves it untouched at dispatch and the resolved name is
|
|
56
|
+
// what reaches the backend on both sides.
|
|
57
|
+
var NONCE_TABLE = "_blamejs_api_encrypt_nonces"; // allow:hand-rolled-sql — canonical logical table-name declaration
|
|
58
|
+
|
|
59
|
+
// b.sql opts for every cluster-backend statement: thread the ACTIVE backend
|
|
60
|
+
// dialect (clusterStorage.dialect() — "sqlite" single-node, "postgres" |
|
|
61
|
+
// "mysql" in cluster mode) so the emitted identifier quoting and dialect
|
|
62
|
+
// idioms (ON CONFLICT vs ON DUPLICATE KEY) match the backend the SQL
|
|
63
|
+
// dispatches to. Defaulting to "sqlite" works on Postgres only by accident
|
|
64
|
+
// (both double-quote identifiers) and emits invalid quoting + ON CONFLICT on
|
|
65
|
+
// MySQL. clusterStorage.execute still rewrites table names + translates `?`
|
|
66
|
+
// placeholders at dispatch; this controls only the builder-side quoting +
|
|
67
|
+
// idiom selection.
|
|
68
|
+
function _nonceSqlOpts() { return { dialect: clusterStorage.dialect() }; }
|
|
69
|
+
|
|
50
70
|
var NonceStoreError = defineClass("NonceStoreError");
|
|
51
71
|
|
|
52
72
|
var DEFAULT_SWEEP_INTERVAL_MS = C.TIME.minutes(5);
|
|
@@ -148,19 +168,21 @@ function _clusterBackend(_opts) {
|
|
|
148
168
|
// (someone else already inserted the same nonce, i.e. replay).
|
|
149
169
|
// The middleware hashes the raw nonce before passing it here so
|
|
150
170
|
// the table only ever sees hashes, not the originals.
|
|
151
|
-
var
|
|
152
|
-
"
|
|
153
|
-
|
|
154
|
-
[
|
|
155
|
-
|
|
171
|
+
var built = sql.upsert(frameworkSchema.tableName(NONCE_TABLE), _nonceSqlOpts())
|
|
172
|
+
.columns(["nonceHash", "expireAt"])
|
|
173
|
+
.values({ nonceHash: nonce, expireAt: expireAt })
|
|
174
|
+
.onConflict(["nonceHash"])
|
|
175
|
+
.doNothing()
|
|
176
|
+
.toSql();
|
|
177
|
+
var result = await clusterStorage.execute(built.sql, built.params);
|
|
156
178
|
return (result && result.rowCount > 0);
|
|
157
179
|
}
|
|
158
180
|
|
|
159
181
|
async function purgeExpired() {
|
|
160
|
-
var
|
|
161
|
-
"
|
|
162
|
-
|
|
163
|
-
);
|
|
182
|
+
var built = sql.delete(frameworkSchema.tableName(NONCE_TABLE), _nonceSqlOpts())
|
|
183
|
+
.where("expireAt", "<=", Date.now())
|
|
184
|
+
.toSql();
|
|
185
|
+
var result = await clusterStorage.execute(built.sql, built.params);
|
|
164
186
|
return (result && result.rowCount) || 0;
|
|
165
187
|
}
|
|
166
188
|
|
|
@@ -214,6 +214,11 @@ function create(config) {
|
|
|
214
214
|
var timeoutMs = config.timeoutMs;
|
|
215
215
|
var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
216
216
|
var allowInternal = config.allowInternal != null ? config.allowInternal : null;
|
|
217
|
+
// Account placement — see azure-blob.js create(). Default host-based; opt
|
|
218
|
+
// into path-style (Azurite / Azure Stack / private) with config.pathStyle:
|
|
219
|
+
// true. Default false keeps the host-based wire shape unchanged.
|
|
220
|
+
var pathStyle = config.pathStyle === true;
|
|
221
|
+
var pathPrefix = pathStyle ? ("/" + config.accountName) : "";
|
|
217
222
|
|
|
218
223
|
function _sign(method, url, headers) {
|
|
219
224
|
return azureBlob.signRequest({
|
|
@@ -260,7 +265,7 @@ function create(config) {
|
|
|
260
265
|
async function createContainer(name, opts) {
|
|
261
266
|
_validateContainerName(name);
|
|
262
267
|
opts = opts || {};
|
|
263
|
-
var url = _internalUrl(endpoint + "/" + name + "?restype=container", allowedProtocols);
|
|
268
|
+
var url = _internalUrl(endpoint + pathPrefix + "/" + name + "?restype=container", allowedProtocols);
|
|
264
269
|
var headers = { "Content-Length": "0" };
|
|
265
270
|
if (opts.publicAccess) {
|
|
266
271
|
if (opts.publicAccess !== "blob" && opts.publicAccess !== "container") {
|
|
@@ -282,7 +287,7 @@ function create(config) {
|
|
|
282
287
|
|
|
283
288
|
async function deleteContainer(name) {
|
|
284
289
|
_validateContainerName(name);
|
|
285
|
-
var url = _internalUrl(endpoint + "/" + name + "?restype=container", allowedProtocols);
|
|
290
|
+
var url = _internalUrl(endpoint + pathPrefix + "/" + name + "?restype=container", allowedProtocols);
|
|
286
291
|
var signed = _sign("DELETE", url, {});
|
|
287
292
|
var res = await _request("DELETE", url, signed, null, [HTTP_ACCEPTED, HTTP_NOT_FOUND]);
|
|
288
293
|
return res.statusCode === HTTP_ACCEPTED;
|
|
@@ -290,7 +295,7 @@ function create(config) {
|
|
|
290
295
|
|
|
291
296
|
async function listContainers(opts) {
|
|
292
297
|
opts = opts || {};
|
|
293
|
-
var url = _internalUrl(endpoint + "/?comp=list", allowedProtocols);
|
|
298
|
+
var url = _internalUrl(endpoint + pathPrefix + "/?comp=list", allowedProtocols);
|
|
294
299
|
if (opts.prefix) url.searchParams.set("prefix", opts.prefix);
|
|
295
300
|
if (opts.maxResults != null) url.searchParams.set("maxresults", String(opts.maxResults));
|
|
296
301
|
var signed = _sign("GET", url, {});
|
|
@@ -319,7 +324,7 @@ function create(config) {
|
|
|
319
324
|
rules.forEach(_validateCorsRule);
|
|
320
325
|
var xml = _buildCorsXml(rules);
|
|
321
326
|
var bodyBuf = Buffer.from(xml, "utf8");
|
|
322
|
-
var url = _internalUrl(endpoint + "/?restype=service&comp=properties", allowedProtocols);
|
|
327
|
+
var url = _internalUrl(endpoint + pathPrefix + "/?restype=service&comp=properties", allowedProtocols);
|
|
323
328
|
var headers = {
|
|
324
329
|
"Content-Type": "application/xml",
|
|
325
330
|
"Content-Length": String(bodyBuf.length),
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* accountKey: '<base64 storage key>' // required (REST shared key)
|
|
16
16
|
* container: 'my-container' // required
|
|
17
17
|
* endpoint: 'https://...' // optional override
|
|
18
|
+
* pathStyle: true // optional; account as the first
|
|
19
|
+
* // URL path segment (Azurite / Azure
|
|
20
|
+
* // Stack / private endpoints).
|
|
21
|
+
* // Default false = host-based
|
|
22
|
+
* // (<account>.blob.core.windows.net).
|
|
18
23
|
* apiVersion: '2024-08-04' // x-ms-version header
|
|
19
24
|
* timeoutMs: C.TIME.seconds(30)
|
|
20
25
|
* }
|
|
@@ -35,6 +40,7 @@ var { URL } = require("node:url");
|
|
|
35
40
|
var { Readable } = require("node:stream");
|
|
36
41
|
var safeXml = require("../parsers/safe-xml");
|
|
37
42
|
var sharedRequest = require("./http-request");
|
|
43
|
+
var sigv4 = require("./sigv4");
|
|
38
44
|
var C = require("../constants");
|
|
39
45
|
var requestHelpers = require("../request-helpers");
|
|
40
46
|
var { ObjectStoreError } = require("../framework-error");
|
|
@@ -65,6 +71,26 @@ function _arrayify(value) {
|
|
|
65
71
|
return Array.isArray(value) ? value : [value];
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
// Percent-encode a hierarchical blob name for use in a URL path. Azure
|
|
75
|
+
// blob names are `/`-delimited virtual directories, so each segment is
|
|
76
|
+
// RFC 3986 percent-encoded (via the family-shared encoder used by the
|
|
77
|
+
// S3 / GCS backends) while the `/` separators are preserved. Without
|
|
78
|
+
// this, a key containing `?`, `#`, a space, or other reserved chars is
|
|
79
|
+
// interpolated raw into the request URL — `?`/`#` start the query /
|
|
80
|
+
// fragment (so the blob path is truncated, hitting the wrong object or
|
|
81
|
+
// the container root), and spaces / control bytes corrupt the request
|
|
82
|
+
// line (CWE-20 improper input → request-smuggling-adjacent). A null
|
|
83
|
+
// byte is refused outright (it can't appear in a valid blob name and
|
|
84
|
+
// indicates a malformed / hostile key), matching the S3 / GCS guards.
|
|
85
|
+
function _encodeBlobKey(key) {
|
|
86
|
+
if (key.indexOf("\0") !== -1) {
|
|
87
|
+
throw _err("INVALID_KEY", "null byte in blob key", true);
|
|
88
|
+
}
|
|
89
|
+
return key.split("/").map(function (s) {
|
|
90
|
+
return sigv4.awsUriEncode(s, true);
|
|
91
|
+
}).join("/");
|
|
92
|
+
}
|
|
93
|
+
|
|
68
94
|
var DEFAULT_API_VERSION = "2024-08-04";
|
|
69
95
|
|
|
70
96
|
// Service SAS expiry bounds. Azure doesn't enforce a hard max, but
|
|
@@ -120,6 +146,17 @@ function buildStringToSign(opts) {
|
|
|
120
146
|
var canonicalResource = (function () {
|
|
121
147
|
// /<account>/<rest of path>
|
|
122
148
|
// Plus sorted query params, each "name:value\n"
|
|
149
|
+
// Canonicalized resource per the Shared Key spec: "/" + account + the
|
|
150
|
+
// request's absolute path + sorted query. Host-based endpoints
|
|
151
|
+
// (production <account>.blob.core.windows.net) have url.pathname
|
|
152
|
+
// "/<container>/<blob>", giving "/<account>/<container>/<blob>".
|
|
153
|
+
// Path-style endpoints (Azurite / Azure Stack / private) already carry
|
|
154
|
+
// "/<account>" as the first path segment, so the account appears twice
|
|
155
|
+
// ("/<account>/<account>/<container>/<blob>") — which is exactly what a
|
|
156
|
+
// path-style server expects: it prepends the account to the full request
|
|
157
|
+
// path it received. Verified against Azurite — the doubled form is the
|
|
158
|
+
// one that authenticates; the URL itself must carry the account in its
|
|
159
|
+
// path (see pathPrefix in create()).
|
|
123
160
|
var resourcePath = "/" + opts.accountName + url.pathname;
|
|
124
161
|
var paramPairs = [];
|
|
125
162
|
url.searchParams.forEach(function (v, k) {
|
|
@@ -204,11 +241,24 @@ function create(config) {
|
|
|
204
241
|
var allowedProtocols = config.allowedProtocols || safeUrl.ALLOW_HTTP_TLS;
|
|
205
242
|
var allowInternal = config.allowInternal != null ? config.allowInternal : null;
|
|
206
243
|
safeUrl.parse(endpoint, { allowedProtocols: allowedProtocols, errorClass: ObjectStoreError });
|
|
244
|
+
// Account placement. Default host-based — production Azure is
|
|
245
|
+
// https://<account>.blob.core.windows.net/<container>/<blob> (account in the
|
|
246
|
+
// host). Path-style endpoints (Azurite / Azure Stack / private) carry the
|
|
247
|
+
// account as the first PATH segment instead —
|
|
248
|
+
// https://<host>/<account>/<container>/<blob> — opt in with
|
|
249
|
+
// config.pathStyle:true. Default false keeps the host-based wire shape
|
|
250
|
+
// unchanged for existing deployments (no silent breaking change). The signed
|
|
251
|
+
// canonicalized resource is always "/" + account + url.pathname, so for a
|
|
252
|
+
// path-style URL the account appears twice — which is exactly what a
|
|
253
|
+
// path-style server expects (see buildStringToSign).
|
|
254
|
+
var pathStyle = config.pathStyle === true;
|
|
255
|
+
var pathPrefix = pathStyle ? ("/" + config.accountName) : "";
|
|
207
256
|
var reqOpts = { timeoutMs: timeoutMs, allowedProtocols: allowedProtocols };
|
|
208
257
|
if (allowInternal !== null) reqOpts.allowInternal = allowInternal;
|
|
209
258
|
|
|
210
259
|
function _blobUrl(key, params) {
|
|
211
|
-
var u = _internalUrl(endpoint + "/" + config.container + "/" + key,
|
|
260
|
+
var u = _internalUrl(endpoint + pathPrefix + "/" + config.container + "/" + _encodeBlobKey(key),
|
|
261
|
+
allowedProtocols);
|
|
212
262
|
if (params) {
|
|
213
263
|
Object.keys(params).forEach(function (k) { u.searchParams.set(k, params[k]); });
|
|
214
264
|
}
|
|
@@ -216,7 +266,7 @@ function create(config) {
|
|
|
216
266
|
}
|
|
217
267
|
|
|
218
268
|
function _containerUrl(params) {
|
|
219
|
-
var u = _internalUrl(endpoint + "/" + config.container, allowedProtocols);
|
|
269
|
+
var u = _internalUrl(endpoint + pathPrefix + "/" + config.container, allowedProtocols);
|
|
220
270
|
if (params) {
|
|
221
271
|
Object.keys(params).forEach(function (k) { u.searchParams.set(k, params[k]); });
|
|
222
272
|
}
|
|
@@ -425,8 +475,12 @@ function create(config) {
|
|
|
425
475
|
throw _err("INVALID_KEY", "null byte in key", true);
|
|
426
476
|
}
|
|
427
477
|
|
|
478
|
+
// _buildSasToken signs the canonicalized resource with the RAW
|
|
479
|
+
// (decoded) blob name per the Azure SAS spec; the URL PATH carries the
|
|
480
|
+
// percent-encoded key so a key with reserved chars (`?` / `#` / space)
|
|
481
|
+
// doesn't truncate the path or corrupt the request line.
|
|
428
482
|
var token = _buildSasToken(permissions, opts);
|
|
429
|
-
var url = _internalUrl(endpoint + "/" + config.container + "/" + opts.key + "?" + token.sas, allowedProtocols);
|
|
483
|
+
var url = _internalUrl(endpoint + pathPrefix + "/" + config.container + "/" + _encodeBlobKey(opts.key) + "?" + token.sas, allowedProtocols);
|
|
430
484
|
|
|
431
485
|
var clientHeaders = {};
|
|
432
486
|
if (opts.contentType) clientHeaders["Content-Type"] = opts.contentType;
|
|
@@ -661,6 +661,16 @@ function create(config) {
|
|
|
661
661
|
etag: res.headers.etag,
|
|
662
662
|
lastModified: res.headers["last-modified"] ? Date.parse(res.headers["last-modified"]) : null,
|
|
663
663
|
};
|
|
664
|
+
}, function (e) {
|
|
665
|
+
// A missing key surfaces as the framework NOT_FOUND code — the same
|
|
666
|
+
// contract local.js head() exposes and that deleteKey already maps 404
|
|
667
|
+
// to — so existence probes via head() (e.g. the backup objectStore
|
|
668
|
+
// adapter's hasKey / statKey) get the uniform missing-key signal instead
|
|
669
|
+
// of a raw HTTP 404 they don't recognize.
|
|
670
|
+
if (e && e.statusCode === 404) {
|
|
671
|
+
throw _err("NOT_FOUND", "key not found: " + key, true);
|
|
672
|
+
}
|
|
673
|
+
throw e;
|
|
664
674
|
});
|
|
665
675
|
}
|
|
666
676
|
|
package/lib/observability.js
CHANGED
|
@@ -55,6 +55,14 @@ var safeBuffer = lazyRequire(function () { return require("./safe-buffer"); });
|
|
|
55
55
|
var tracing = lazyRequire(function () { return require("./tracing"); });
|
|
56
56
|
var metrics = lazyRequire(function () { return require("./metrics"); });
|
|
57
57
|
|
|
58
|
+
// redact is the framework's central PII/secret scrubber. Lazy-loaded so
|
|
59
|
+
// the require graph stays acyclic at boot (redact lazy-pulls audit, which
|
|
60
|
+
// pulls observability back). Composed by the default telemetry redactor
|
|
61
|
+
// below so span/metric attribute VALUES are scrubbed before the OTLP
|
|
62
|
+
// exporter serializes them — CWE-532 (insertion of sensitive information
|
|
63
|
+
// into a telemetry/log egress sink).
|
|
64
|
+
var redact = lazyRequire(function () { return require("./redact"); });
|
|
65
|
+
|
|
58
66
|
// Operator-installed tap handler — wired via setTap(). When non-null,
|
|
59
67
|
// every observability event/tap dispatch routes here in addition to
|
|
60
68
|
// the framework's metrics module. Used by b.otelExport.create() so an
|
|
@@ -62,6 +70,26 @@ var metrics = lazyRequire(function () { return require("./metrics"); });
|
|
|
62
70
|
// emits internally.
|
|
63
71
|
var _externalTap = null;
|
|
64
72
|
|
|
73
|
+
// Telemetry-attribute redactor seam. Span / metric attribute VALUES are
|
|
74
|
+
// a first-class egress surface: a span attribute holding a user email,
|
|
75
|
+
// bearer token, or vault-sealed ciphertext is shipped verbatim to the
|
|
76
|
+
// OTLP collector unless it is scrubbed at the assembly boundary, the same
|
|
77
|
+
// way log-stream redacts every record before any sink sees it. Defaults
|
|
78
|
+
// ON — the default redactor composes b.redact.redact, passing the
|
|
79
|
+
// attribute key as the parent-key context so both field-name rules
|
|
80
|
+
// (authorization / token / session / password) and value-shape detectors
|
|
81
|
+
// (JWT / PEM / credit-card / SSN / connection-string) fire. CWE-532.
|
|
82
|
+
//
|
|
83
|
+
// The redactor is (value, key) → redactedValue. The exporter calls it for
|
|
84
|
+
// every attribute value; a thrown redactor drops the attribute rather
|
|
85
|
+
// than leaking it (the exporter enforces fail-toward-dropping), so a
|
|
86
|
+
// misbehaving custom redactor can never widen the egress surface.
|
|
87
|
+
function _defaultTelemetryRedactor(value, key) {
|
|
88
|
+
return redact().redact(value, { parentKey: typeof key === "string" ? key : null });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var _telemetryRedactor = _defaultTelemetryRedactor;
|
|
92
|
+
|
|
65
93
|
function _safeMetricsTap(name, value, labels) {
|
|
66
94
|
try { metrics().tap(name, value, labels); }
|
|
67
95
|
catch (_e) { /* boot-order tolerance — metrics may not be loaded */ }
|
|
@@ -106,6 +134,63 @@ function setTap(handler) {
|
|
|
106
134
|
_externalTap = handler;
|
|
107
135
|
}
|
|
108
136
|
|
|
137
|
+
/**
|
|
138
|
+
* @primitive b.observability.setRedactor
|
|
139
|
+
* @signature b.observability.setRedactor(redactor)
|
|
140
|
+
* @since 0.14.27
|
|
141
|
+
* @related b.observability.getRedactor, b.redact.redact
|
|
142
|
+
*
|
|
143
|
+
* Override the redactor applied to every span / metric attribute VALUE
|
|
144
|
+
* before the OTLP exporter serializes it onto the wire. Telemetry is a
|
|
145
|
+
* first-class egress sink: an attribute holding a user email, bearer
|
|
146
|
+
* token, or secret would otherwise reach the collector in plaintext
|
|
147
|
+
* (CWE-532). Redaction is ON by default — the default redactor composes
|
|
148
|
+
* `b.redact.redact` and fires both field-name and value-shape rules; this
|
|
149
|
+
* setter only lets an operator swap in a stricter or domain-specific
|
|
150
|
+
* scrubber.
|
|
151
|
+
*
|
|
152
|
+
* The redactor is `redactor(value, key)` and returns the value to export.
|
|
153
|
+
* It runs on the export hot path, so a throw is caught and the attribute
|
|
154
|
+
* is dropped (never exported raw) — a redactor that throws can only
|
|
155
|
+
* shrink the egress surface, never widen it. Pass `null` to restore the
|
|
156
|
+
* default `b.redact.redact`-backed redactor.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* b.observability.setRedactor(function (value, key) {
|
|
160
|
+
* if (key === "enduser.id") return "[REDACTED]";
|
|
161
|
+
* return b.redact.redact(value, { parentKey: key });
|
|
162
|
+
* });
|
|
163
|
+
* b.observability.setRedactor(null); // restore the default
|
|
164
|
+
*/
|
|
165
|
+
function setRedactor(redactor) {
|
|
166
|
+
if (redactor !== null && typeof redactor !== "function") {
|
|
167
|
+
throw new TypeError("observability.setRedactor: redactor must be a function or null, got " +
|
|
168
|
+
typeof redactor);
|
|
169
|
+
}
|
|
170
|
+
_telemetryRedactor = redactor === null ? _defaultTelemetryRedactor : redactor;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @primitive b.observability.getRedactor
|
|
175
|
+
* @signature b.observability.getRedactor()
|
|
176
|
+
* @since 0.14.27
|
|
177
|
+
* @related b.observability.setRedactor, b.redact.redact
|
|
178
|
+
*
|
|
179
|
+
* Return the redactor currently applied to span / metric attribute
|
|
180
|
+
* values on the OTLP egress path. The OTLP exporter calls this to scrub
|
|
181
|
+
* every attribute value before serialization; operators rarely need it
|
|
182
|
+
* directly. When no override has been installed it returns the default
|
|
183
|
+
* `b.redact.redact`-backed redactor.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* var redactor = b.observability.getRedactor();
|
|
187
|
+
* redactor("Bearer eyJabc.eyJdef.sig", "authorization");
|
|
188
|
+
* // → "[REDACTED]" (field-name rule on the "authorization" key)
|
|
189
|
+
*/
|
|
190
|
+
function getRedactor() {
|
|
191
|
+
return _telemetryRedactor;
|
|
192
|
+
}
|
|
193
|
+
|
|
109
194
|
/**
|
|
110
195
|
* @primitive b.observability.tap
|
|
111
196
|
* @signature b.observability.tap(name, attrs, fn)
|
|
@@ -750,6 +835,8 @@ module.exports = {
|
|
|
750
835
|
safeEvent: safeEvent,
|
|
751
836
|
timed: timed,
|
|
752
837
|
setTap: setTap,
|
|
838
|
+
setRedactor: setRedactor,
|
|
839
|
+
getRedactor: getRedactor,
|
|
753
840
|
SEMCONV: SEMCONV,
|
|
754
841
|
traceContext: traceContext,
|
|
755
842
|
baggage: baggage,
|