@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/mail-journal.js
CHANGED
|
@@ -72,7 +72,7 @@ var C = require("./constants");
|
|
|
72
72
|
var validateOpts = require("./validate-opts");
|
|
73
73
|
var numericBounds = require("./numeric-bounds");
|
|
74
74
|
var safeJson = require("./safe-json");
|
|
75
|
-
var
|
|
75
|
+
var sql = require("./sql");
|
|
76
76
|
var { defineClass } = require("./framework-error");
|
|
77
77
|
var lazyRequire = require("./lazy-require");
|
|
78
78
|
|
|
@@ -213,33 +213,40 @@ function create(opts) {
|
|
|
213
213
|
// / `messageId` / `archivedAt` / `sizeBytes` / `regimes` / `legalHold`
|
|
214
214
|
// queryable without unsealing the payload. The payload (headers +
|
|
215
215
|
// body) lives in the WORM bucket sealed via b.cryptoField.sealRow.
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
// `
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
var
|
|
225
|
-
var
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
216
|
+
//
|
|
217
|
+
// The journal table is an operator-namespaced local table (NOT a
|
|
218
|
+
// framework `_blamejs_` table that clusterStorage rewrites), so every
|
|
219
|
+
// statement composes b.sql with quoteName:true — b.sql validates the
|
|
220
|
+
// identifier through b.safeSql and emits the dialect-quoted form,
|
|
221
|
+
// running against opts.db (the local sqlite handle) directly. `_t()`
|
|
222
|
+
// opens each verb builder pre-bound to this table so the name resolves
|
|
223
|
+
// in exactly one place.
|
|
224
|
+
var rawTable = "_mail_journal_" + namespace.replace(/-/g, "_");
|
|
225
|
+
var TBL_OPTS = { dialect: "sqlite", quoteName: true };
|
|
226
|
+
function _t(verb) { return sql[verb](rawTable, TBL_OPTS); }
|
|
227
|
+
|
|
228
|
+
// Bootstrap DDL — CREATE TABLE + the archived_at / message_id indexes.
|
|
229
|
+
// runSql is a multi-statement helper, so the three b.sql DDL strings
|
|
230
|
+
// join with `;` into one call (each b.sql build is a single validated
|
|
231
|
+
// statement; the join is the multi-statement boundary runSql expects).
|
|
232
|
+
var ddl = [
|
|
233
|
+
sql.createTable(rawTable, [
|
|
234
|
+
{ name: "journal_id", type: "text", primaryKey: true },
|
|
235
|
+
{ name: "direction", type: "text", notNull: true },
|
|
236
|
+
{ name: "actor_id", type: "text" },
|
|
237
|
+
{ name: "message_id", type: "text" },
|
|
238
|
+
{ name: "archived_at", type: "int", notNull: true },
|
|
239
|
+
{ name: "size_bytes", type: "int", notNull: true },
|
|
240
|
+
{ name: "regimes", type: "text", notNull: true },
|
|
241
|
+
{ name: "floor_until", type: "int", notNull: true },
|
|
242
|
+
{ name: "legal_hold", type: "int", notNull: true, default: 0 },
|
|
243
|
+
{ name: "storage_key", type: "text", notNull: true, unique: true },
|
|
244
|
+
{ name: "sealed_payload", type: "blob", notNull: true },
|
|
245
|
+
], TBL_OPTS).sql,
|
|
246
|
+
sql.createIndex(rawTable + "_archived_at_idx", rawTable, ["archived_at"], TBL_OPTS).sql,
|
|
247
|
+
sql.createIndex(rawTable + "_message_id_idx", rawTable, ["message_id"], TBL_OPTS).sql,
|
|
248
|
+
].join(";");
|
|
249
|
+
opts.db.runSql(ddl);
|
|
243
250
|
|
|
244
251
|
async function record(req) {
|
|
245
252
|
validateOpts.requireObject(req, "mail.journal.record",
|
|
@@ -292,13 +299,24 @@ function create(opts) {
|
|
|
292
299
|
|
|
293
300
|
var regimesJson = JSON.stringify(opts.regimes);
|
|
294
301
|
var floorUntil = archivedAt + floorMs;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
+
// legal_hold is omitted from the INSERT so the column's
|
|
303
|
+
// `NOT NULL DEFAULT 0` applies (the prior inline `0` SQL literal) — b.sql
|
|
304
|
+
// binds every value it is given, so leaving the column out keeps the row
|
|
305
|
+
// unsealed-at-rest default without binding a redundant constant. b.sql
|
|
306
|
+
// quotes every column + binds every remaining value as a placeholder.
|
|
307
|
+
var insBuilt = _t("insert").values({
|
|
308
|
+
journal_id: journalId,
|
|
309
|
+
direction: req.direction,
|
|
310
|
+
actor_id: req.actorId,
|
|
311
|
+
message_id: req.messageId,
|
|
312
|
+
archived_at: archivedAt,
|
|
313
|
+
size_bytes: sizeBytes,
|
|
314
|
+
regimes: regimesJson,
|
|
315
|
+
floor_until: floorUntil,
|
|
316
|
+
storage_key: storageKey,
|
|
317
|
+
sealed_payload: sealedBlob,
|
|
318
|
+
}).toSql();
|
|
319
|
+
opts.db.runSql(insBuilt.sql, insBuilt.params);
|
|
302
320
|
|
|
303
321
|
_emit("mail.journal.record", "success", {
|
|
304
322
|
journalId: journalId,
|
|
@@ -317,11 +335,12 @@ function create(opts) {
|
|
|
317
335
|
throw new MailJournalError("mail-journal/bad-id",
|
|
318
336
|
"mail.journal.getById: journalId must be a non-empty string");
|
|
319
337
|
}
|
|
320
|
-
var
|
|
321
|
-
"
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
338
|
+
var gbBuilt = _t("select")
|
|
339
|
+
.columns(["direction", "message_id", "archived_at", "size_bytes", "regimes",
|
|
340
|
+
"floor_until", "legal_hold", "storage_key", "sealed_payload"])
|
|
341
|
+
.where("journal_id", journalId)
|
|
342
|
+
.toSql();
|
|
343
|
+
var rows = opts.db.runSql(gbBuilt.sql, gbBuilt.params);
|
|
325
344
|
if (!rows || rows.length === 0) return null;
|
|
326
345
|
var r = rows[0];
|
|
327
346
|
var unsealed = safeJson.parse(opts.vault.unseal(r.sealed_payload));
|
|
@@ -344,30 +363,27 @@ function create(opts) {
|
|
|
344
363
|
|
|
345
364
|
function list(filter) {
|
|
346
365
|
filter = filter || {};
|
|
366
|
+
var limit = numericBounds.isPositiveFiniteInt(filter.limit) ? Math.min(filter.limit, 1000) : 100; // list page cap
|
|
367
|
+
// Each filter term is an optional .where() leaf (AND-composed); b.sql
|
|
368
|
+
// quotes the columns + binds the values. A diagnostic clause list is
|
|
369
|
+
// kept for the audit metadata (the prior `filter: clauses` field).
|
|
347
370
|
var clauses = [];
|
|
348
|
-
var
|
|
371
|
+
var qb = _t("select").columns(["journal_id", "direction", "actor_id", "message_id",
|
|
372
|
+
"archived_at", "size_bytes", "regimes", "floor_until", "legal_hold", "storage_key"]);
|
|
349
373
|
if (filter.direction && ALLOWED_DIRECTIONS[filter.direction]) {
|
|
350
|
-
clauses.push("direction = ?");
|
|
351
|
-
args.push(filter.direction);
|
|
374
|
+
qb.where("direction", filter.direction); clauses.push("direction = ?");
|
|
352
375
|
}
|
|
353
376
|
if (typeof filter.since === "number" && numericBounds.isPositiveFiniteInt(filter.since)) {
|
|
354
|
-
clauses.push("archived_at >= ?");
|
|
355
|
-
args.push(filter.since);
|
|
377
|
+
qb.whereOp("archived_at", ">=", filter.since); clauses.push("archived_at >= ?");
|
|
356
378
|
}
|
|
357
379
|
if (typeof filter.until === "number" && numericBounds.isPositiveFiniteInt(filter.until)) {
|
|
358
|
-
clauses.push("archived_at < ?");
|
|
359
|
-
args.push(filter.until);
|
|
380
|
+
qb.whereOp("archived_at", "<", filter.until); clauses.push("archived_at < ?");
|
|
360
381
|
}
|
|
361
382
|
if (filter.actorId && typeof filter.actorId === "string") {
|
|
362
|
-
clauses.push("actor_id = ?");
|
|
363
|
-
args.push(filter.actorId);
|
|
383
|
+
qb.where("actor_id", filter.actorId); clauses.push("actor_id = ?");
|
|
364
384
|
}
|
|
365
|
-
var
|
|
366
|
-
var
|
|
367
|
-
var sql = "SELECT journal_id, direction, actor_id, message_id, archived_at, " +
|
|
368
|
-
"size_bytes, regimes, floor_until, legal_hold, storage_key FROM " +
|
|
369
|
-
qTable + where + " ORDER BY archived_at DESC LIMIT " + limit + ";";
|
|
370
|
-
var rows = opts.db.runSql(sql, args);
|
|
385
|
+
var listBuilt = qb.orderBy("archived_at", "desc").limit(limit).toSql();
|
|
386
|
+
var rows = opts.db.runSql(listBuilt.sql, listBuilt.params);
|
|
371
387
|
_emit("mail.journal.list", "success", { count: rows ? rows.length : 0, filter: clauses });
|
|
372
388
|
return (rows || []).map(function (r) {
|
|
373
389
|
return {
|
|
@@ -387,11 +403,15 @@ function create(opts) {
|
|
|
387
403
|
|
|
388
404
|
function expireSurface(now) {
|
|
389
405
|
if (now === undefined) now = Date.now();
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
406
|
+
// legal_hold = 0 binds as a value (the prior inline `0` literal).
|
|
407
|
+
var esBuilt = _t("select")
|
|
408
|
+
.columns(["journal_id", "archived_at", "floor_until", "message_id", "regimes"])
|
|
409
|
+
.whereOp("floor_until", "<", now)
|
|
410
|
+
.where("legal_hold", 0)
|
|
411
|
+
.orderBy("archived_at", "asc")
|
|
412
|
+
.limit(1000) // expiry-surface cap
|
|
413
|
+
.toSql();
|
|
414
|
+
var rows = opts.db.runSql(esBuilt.sql, esBuilt.params);
|
|
395
415
|
_emit("mail.journal.expire_surface", "success", { count: rows ? rows.length : 0, now: now });
|
|
396
416
|
return (rows || []).map(function (r) {
|
|
397
417
|
return {
|
|
@@ -409,10 +429,11 @@ function create(opts) {
|
|
|
409
429
|
throw new MailJournalError("mail-journal/bad-id",
|
|
410
430
|
"mail.journal.setLegalHold: journalId required");
|
|
411
431
|
}
|
|
412
|
-
|
|
413
|
-
"
|
|
414
|
-
|
|
415
|
-
|
|
432
|
+
var lhBuilt = _t("update")
|
|
433
|
+
.set("legal_hold", onHold ? 1 : 0)
|
|
434
|
+
.where("journal_id", journalId)
|
|
435
|
+
.toSql();
|
|
436
|
+
opts.db.runSql(lhBuilt.sql, lhBuilt.params);
|
|
416
437
|
_emit("mail.journal.legal_hold_change", "success", { journalId: journalId, onHold: !!onHold });
|
|
417
438
|
}
|
|
418
439
|
|
package/lib/mail-rbl.js
CHANGED
|
@@ -86,6 +86,7 @@ var C = require("./constants");
|
|
|
86
86
|
var { defineClass } = require("./framework-error");
|
|
87
87
|
var lazyRequire = require("./lazy-require");
|
|
88
88
|
var ipUtils = require("./ip-utils");
|
|
89
|
+
var gateContract = require("./gate-contract");
|
|
89
90
|
|
|
90
91
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
91
92
|
|
|
@@ -105,12 +106,7 @@ var PROFILES = Object.freeze({
|
|
|
105
106
|
permissive: { maxConcurrent: 32, perListTimeoutMs: C.TIME.seconds(20), maxListsPerQuery: 64 }, // list-count cap
|
|
106
107
|
});
|
|
107
108
|
|
|
108
|
-
var COMPLIANCE_POSTURES =
|
|
109
|
-
hipaa: "strict",
|
|
110
|
-
"pci-dss": "strict",
|
|
111
|
-
gdpr: "strict",
|
|
112
|
-
soc2: "strict",
|
|
113
|
-
});
|
|
109
|
+
var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES;
|
|
114
110
|
|
|
115
111
|
/**
|
|
116
112
|
* @primitive b.mail.rbl.create
|
package/lib/mail-scan.js
CHANGED
|
@@ -85,6 +85,7 @@ var lazyRequire = require("./lazy-require");
|
|
|
85
85
|
var validateOpts = require("./validate-opts");
|
|
86
86
|
var numericBounds = require("./numeric-bounds");
|
|
87
87
|
var safeIcap = require("./safe-icap");
|
|
88
|
+
var gateContract = require("./gate-contract");
|
|
88
89
|
|
|
89
90
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
90
91
|
var guardArchive = lazyRequire(function () { return require("./guard-archive"); });
|
|
@@ -107,12 +108,7 @@ var PROFILES = Object.freeze({
|
|
|
107
108
|
permissive: { timeoutMs: C.TIME.seconds(120), maxMessageBytes: C.BYTES.mib(150), maxResponseBytes: C.BYTES.mib(300) }, // operator-facing default mailbox cap
|
|
108
109
|
});
|
|
109
110
|
|
|
110
|
-
var COMPLIANCE_POSTURES =
|
|
111
|
-
hipaa: "strict",
|
|
112
|
-
"pci-dss": "strict",
|
|
113
|
-
gdpr: "strict",
|
|
114
|
-
soc2: "strict",
|
|
115
|
-
});
|
|
111
|
+
var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES;
|
|
116
112
|
|
|
117
113
|
var ALLOWED_PROTOCOLS = Object.freeze({
|
|
118
114
|
"icap": true,
|
package/lib/mail-server-jmap.js
CHANGED
|
@@ -316,6 +316,33 @@ function create(opts) {
|
|
|
316
316
|
return cur;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
// ---- Account authorization (RFC 8620 §3.6.1 accountNotFound) ------------
|
|
320
|
+
//
|
|
321
|
+
// The set of accountIds an actor may touch is whatever
|
|
322
|
+
// `opts.accountsFor(actor)` enumerates in its `accounts` map — the same
|
|
323
|
+
// source the session resource advertises. Resolving it ONCE per request
|
|
324
|
+
// and rejecting any client-supplied accountId outside that set is the
|
|
325
|
+
// cross-tenant authorization control: without it, a tenant's request can
|
|
326
|
+
// name another tenant's accountId and reach the operator's method/blob
|
|
327
|
+
// handler, which must then independently re-check or leak. The listener
|
|
328
|
+
// owns this gate so every account-scoped op (method dispatch + blob
|
|
329
|
+
// upload/download) is covered uniformly.
|
|
330
|
+
async function _permittedAccountIds(actor) {
|
|
331
|
+
var info = await opts.accountsFor(actor);
|
|
332
|
+
info = info || {};
|
|
333
|
+
var accounts = info.accounts || {};
|
|
334
|
+
// A Set of the accountIds the actor is enumerated for. An empty/garbage
|
|
335
|
+
// accounts map yields an empty set → every account-scoped reference is
|
|
336
|
+
// rejected (fail-closed), which is the correct posture for an actor the
|
|
337
|
+
// operator declined to grant any account.
|
|
338
|
+
var set = Object.create(null);
|
|
339
|
+
if (accounts && typeof accounts === "object") {
|
|
340
|
+
var ids = Object.keys(accounts);
|
|
341
|
+
for (var i = 0; i < ids.length; i += 1) set[ids[i]] = true;
|
|
342
|
+
}
|
|
343
|
+
return set;
|
|
344
|
+
}
|
|
345
|
+
|
|
319
346
|
// ---- Dispatch ------------------------------------------------------------
|
|
320
347
|
//
|
|
321
348
|
// `dispatch(actor, body)` is the operator-callable form — accepts a
|
|
@@ -340,6 +367,20 @@ function create(opts) {
|
|
|
340
367
|
return _refusalResponse(errType, (e && e.message) || "request refused");
|
|
341
368
|
}
|
|
342
369
|
|
|
370
|
+
// Resolve the actor's permitted accountId set ONCE for the whole
|
|
371
|
+
// request (RFC 8620 §3.6.1). Every account-scoped method call is gated
|
|
372
|
+
// against it below, BEFORE the operator handler runs, so a client can't
|
|
373
|
+
// name another tenant's accountId and reach the backend.
|
|
374
|
+
var permittedAccounts;
|
|
375
|
+
try {
|
|
376
|
+
permittedAccounts = await _permittedAccountIds(actor);
|
|
377
|
+
} catch (e) {
|
|
378
|
+
_emit("mail.server.jmap.accounts_for_threw",
|
|
379
|
+
{ error: (e && e.message) || String(e) }, "failure");
|
|
380
|
+
return _refusalResponse("urn:ietf:params:jmap:error:serverFail",
|
|
381
|
+
"account authorization unavailable");
|
|
382
|
+
}
|
|
383
|
+
|
|
343
384
|
var methodResponses = [];
|
|
344
385
|
var byClientId = Object.create(null);
|
|
345
386
|
for (var i = 0; i < parsed.methodCalls.length; i += 1) {
|
|
@@ -361,6 +402,24 @@ function create(opts) {
|
|
|
361
402
|
description: "Method '" + methodName + "' not implemented on this server" }, clientId]);
|
|
362
403
|
continue;
|
|
363
404
|
}
|
|
405
|
+
// Cross-tenant gate (RFC 8620 §3.6.1): if the call names an accountId,
|
|
406
|
+
// it MUST be one the actor is enumerated for. Rejected BEFORE the
|
|
407
|
+
// operator handler runs so a forged/foreign accountId never reaches
|
|
408
|
+
// the backend. Calls without an accountId (account-agnostic methods)
|
|
409
|
+
// pass through unchanged.
|
|
410
|
+
if (resolvedArgs && typeof resolvedArgs === "object" &&
|
|
411
|
+
resolvedArgs.accountId !== undefined && resolvedArgs.accountId !== null) {
|
|
412
|
+
var callAccountId = resolvedArgs.accountId;
|
|
413
|
+
if (typeof callAccountId !== "string" || !permittedAccounts[callAccountId]) {
|
|
414
|
+
_emit("mail.server.jmap.account_not_found",
|
|
415
|
+
{ method: methodName, accountId: typeof callAccountId === "string" ? callAccountId : null,
|
|
416
|
+
clientId: clientId }, "denied");
|
|
417
|
+
methodResponses.push(["error",
|
|
418
|
+
{ type: "urn:ietf:params:jmap:error:accountNotFound",
|
|
419
|
+
description: "accountId is not accessible to this actor" }, clientId]);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
364
423
|
if (!_legacyDeprecationEmitted && registry.source(methodName) === "builtin") {
|
|
365
424
|
_legacyDeprecationEmitted = true;
|
|
366
425
|
_emit("mail.server.jmap.methods_opt_deprecated",
|
|
@@ -813,6 +872,40 @@ function create(opts) {
|
|
|
813
872
|
if (refused) return;
|
|
814
873
|
var bytes = collector.result();
|
|
815
874
|
Promise.resolve()
|
|
875
|
+
// Cross-tenant gate (RFC 8620 §3.6.1): the accountId in the upload
|
|
876
|
+
// URL must be one the actor is enumerated for, else accountNotFound
|
|
877
|
+
// — the foreign accountId never reaches uploadBlob.
|
|
878
|
+
.then(function () { return _permittedAccountIds(actor); })
|
|
879
|
+
.then(function (permitted) {
|
|
880
|
+
if (!permitted[accountId]) {
|
|
881
|
+
_emit("mail.server.jmap.account_not_found",
|
|
882
|
+
{ op: "upload", accountId: accountId }, "denied");
|
|
883
|
+
res.statusCode = 404;
|
|
884
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
885
|
+
res.end(JSON.stringify({
|
|
886
|
+
type: "urn:ietf:params:jmap:error:accountNotFound",
|
|
887
|
+
description: "accountId is not accessible to this actor",
|
|
888
|
+
}));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
return _completeUpload(bytes);
|
|
892
|
+
})
|
|
893
|
+
.catch(function (err) {
|
|
894
|
+
_emit("mail.server.jmap.upload_threw",
|
|
895
|
+
{ accountId: accountId, error: (err && err.message) || String(err) }, "failure");
|
|
896
|
+
if (!res.headersSent) {
|
|
897
|
+
res.statusCode = 500;
|
|
898
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
899
|
+
res.end(JSON.stringify({
|
|
900
|
+
type: "urn:ietf:params:jmap:error:serverFail",
|
|
901
|
+
description: "Upload failed",
|
|
902
|
+
}));
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
function _completeUpload(bytes) {
|
|
908
|
+
return Promise.resolve()
|
|
816
909
|
.then(function () { return opts.mailStore.uploadBlob(actor, accountId, contentType, bytes); })
|
|
817
910
|
.then(function (meta) {
|
|
818
911
|
if (!meta || typeof meta !== "object" || typeof meta.blobId !== "string") {
|
|
@@ -827,18 +920,10 @@ function create(opts) {
|
|
|
827
920
|
type: meta.type || contentType,
|
|
828
921
|
size: typeof meta.size === "number" ? meta.size : bytes.length,
|
|
829
922
|
}));
|
|
830
|
-
})
|
|
831
|
-
.catch(function (err) {
|
|
832
|
-
_emit("mail.server.jmap.upload_threw",
|
|
833
|
-
{ accountId: accountId, error: (err && err.message) || String(err) }, "failure");
|
|
834
|
-
res.statusCode = 500;
|
|
835
|
-
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
836
|
-
res.end(JSON.stringify({
|
|
837
|
-
type: "urn:ietf:params:jmap:error:serverFail",
|
|
838
|
-
description: "Upload failed",
|
|
839
|
-
}));
|
|
840
923
|
});
|
|
841
|
-
|
|
924
|
+
// Errors from uploadBlob propagate to the req.on("end") chain's
|
|
925
|
+
// .catch (single serverFail responder, headersSent-guarded).
|
|
926
|
+
}
|
|
842
927
|
req.on("error", function () {
|
|
843
928
|
if (!refused) {
|
|
844
929
|
refused = true;
|
|
@@ -944,9 +1029,29 @@ function create(opts) {
|
|
|
944
1029
|
}));
|
|
945
1030
|
return;
|
|
946
1031
|
}
|
|
1032
|
+
var downloadDenied = false;
|
|
947
1033
|
Promise.resolve()
|
|
948
|
-
|
|
1034
|
+
// Cross-tenant gate (RFC 8620 §3.6.1): the accountId in the download
|
|
1035
|
+
// URL must be one the actor is enumerated for, else accountNotFound —
|
|
1036
|
+
// the foreign accountId never reaches downloadBlob.
|
|
1037
|
+
.then(function () { return _permittedAccountIds(actor); })
|
|
1038
|
+
.then(function (permitted) {
|
|
1039
|
+
if (!permitted[accountId]) {
|
|
1040
|
+
downloadDenied = true;
|
|
1041
|
+
_emit("mail.server.jmap.account_not_found",
|
|
1042
|
+
{ op: "download", accountId: accountId, blobId: blobId }, "denied");
|
|
1043
|
+
res.statusCode = 404;
|
|
1044
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
1045
|
+
res.end(JSON.stringify({
|
|
1046
|
+
type: "urn:ietf:params:jmap:error:accountNotFound",
|
|
1047
|
+
description: "accountId is not accessible to this actor",
|
|
1048
|
+
}));
|
|
1049
|
+
return undefined;
|
|
1050
|
+
}
|
|
1051
|
+
return opts.mailStore.downloadBlob(actor, accountId, blobId);
|
|
1052
|
+
})
|
|
949
1053
|
.then(function (result) {
|
|
1054
|
+
if (downloadDenied) return;
|
|
950
1055
|
if (!result || (typeof result !== "object" && !Buffer.isBuffer(result))) {
|
|
951
1056
|
res.statusCode = 404;
|
|
952
1057
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
package/lib/mail-spam-score.js
CHANGED
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
var { defineClass } = require("./framework-error");
|
|
71
71
|
var lazyRequire = require("./lazy-require");
|
|
72
72
|
var validateOpts = require("./validate-opts");
|
|
73
|
+
var gateContract = require("./gate-contract");
|
|
73
74
|
|
|
74
75
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
75
76
|
|
|
@@ -90,12 +91,7 @@ var PROFILES = Object.freeze({
|
|
|
90
91
|
permissive: { threshold: 10.0, maxReasons: MAX_REASONS, maxReasonBytes: MAX_REASON_BYTES },
|
|
91
92
|
});
|
|
92
93
|
|
|
93
|
-
var COMPLIANCE_POSTURES =
|
|
94
|
-
hipaa: "strict",
|
|
95
|
-
"pci-dss": "strict",
|
|
96
|
-
gdpr: "strict",
|
|
97
|
-
soc2: "strict",
|
|
98
|
-
});
|
|
94
|
+
var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES;
|
|
99
95
|
|
|
100
96
|
/**
|
|
101
97
|
* @primitive b.mail.spamScore.create
|