@blamejs/core 0.11.21 → 0.11.23
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 +4 -0
- package/index.js +2 -0
- package/lib/acme.js +288 -0
- package/lib/asn1-der.js +68 -0
- package/lib/audit.js +1 -0
- package/lib/cert.js +763 -0
- package/lib/mail-agent.js +121 -0
- package/lib/mail-store.js +103 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-agent.js
CHANGED
|
@@ -104,6 +104,7 @@ var SCOPE_FOR_METHOD = Object.freeze({
|
|
|
104
104
|
move: "mail:move",
|
|
105
105
|
flag: "mail:move",
|
|
106
106
|
delete: "mail:move",
|
|
107
|
+
expunge: "mail:expunge",
|
|
107
108
|
"sieve.list": "mail:sieve",
|
|
108
109
|
"sieve.put": "mail:sieve",
|
|
109
110
|
"sieve.activate": "mail:sieve",
|
|
@@ -209,6 +210,7 @@ function create(opts) {
|
|
|
209
210
|
move: function (args) { return _dispatchOrLocal(ctx, "move", args, _move); },
|
|
210
211
|
flag: function (args) { return _dispatchOrLocal(ctx, "flag", args, _flag); },
|
|
211
212
|
delete: function (args) { return _dispatchOrLocal(ctx, "delete", args, _delete); },
|
|
213
|
+
expunge: function (args) { return _dispatchOrLocal(ctx, "expunge", args, _expunge); },
|
|
212
214
|
|
|
213
215
|
// Sieve — needs v0.9.26 interpreter.
|
|
214
216
|
sieve: {
|
|
@@ -484,6 +486,125 @@ async function _delete(ctx, args) {
|
|
|
484
486
|
return r;
|
|
485
487
|
}
|
|
486
488
|
|
|
489
|
+
async function _expunge(ctx, args) {
|
|
490
|
+
// Hard EXPUNGE — permanent removal of messages from the mail store.
|
|
491
|
+
// Composes two refusal gates BEFORE the destructive SQL runs:
|
|
492
|
+
//
|
|
493
|
+
// 1. b.legalHold — any message whose `legal_hold` flag is set
|
|
494
|
+
// refuses with reason "legal-hold". The mail-store layer
|
|
495
|
+
// surfaces the flag in the row metadata; this layer maps that
|
|
496
|
+
// to the operator-facing refusal.
|
|
497
|
+
//
|
|
498
|
+
// 2. b.retention.complianceFloor — given the operator's posture
|
|
499
|
+
// (e.g. "hipaa"), `complianceFloor(posture, candidateTtlMs)`
|
|
500
|
+
// returns the regulator-mandated minimum retention TTL. Any
|
|
501
|
+
// message younger than that floor refuses with reason
|
|
502
|
+
// "retention-floor".
|
|
503
|
+
//
|
|
504
|
+
// Both gates run per-message; the response shape carries an
|
|
505
|
+
// explicit refusal reason for every refused id so the wire-protocol
|
|
506
|
+
// adapter (IMAP EXPUNGE → "* N EXPUNGE" suppression, JMAP
|
|
507
|
+
// Email/set destroyed → notDestroyed[id] = SetError) can mirror the
|
|
508
|
+
// reason to operators verbatim.
|
|
509
|
+
_entry(ctx, "expunge", args);
|
|
510
|
+
if (typeof args.folder !== "string" || !Array.isArray(args.objectIds)) {
|
|
511
|
+
throw new MailAgentError("mail-agent/bad-args",
|
|
512
|
+
"agent.expunge: { folder, objectIds, [candidateTtlMs] } required");
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Look up the regulator-mandated retention floor for the operator's
|
|
516
|
+
// active posture. For expunge semantics, the floor IS the minimum
|
|
517
|
+
// TTL — messages younger than the floor MUST NOT be hard-deleted,
|
|
518
|
+
// even on operator request. Distinct from `b.retention.
|
|
519
|
+
// complianceFloor(posture, candidateTtl)` which composes the
|
|
520
|
+
// candidate TTL into a max — that primitive's "candidate must be
|
|
521
|
+
// positive" contract doesn't apply here because expunge means TTL=0.
|
|
522
|
+
// Read the floor table directly.
|
|
523
|
+
var retentionModule = require("./retention"); // allow:inline-require — lazy-load until first expunge call
|
|
524
|
+
var posture = (ctx && ctx.posture) || (args && args.posture) || null;
|
|
525
|
+
var floorMs = 0;
|
|
526
|
+
if (typeof posture === "string" && posture.length > 0) {
|
|
527
|
+
floorMs = retentionModule.COMPLIANCE_RETENTION_FLOOR_MS[posture] || 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Read message metadata BEFORE invoking hardExpunge so the per-id
|
|
531
|
+
// refusal map is built from the same row set the destructive call
|
|
532
|
+
// sees. Use the store's hardExpunge primitive in two passes:
|
|
533
|
+
// pass 1: pass an empty objectIds[] for the candidate scan? No —
|
|
534
|
+
// hardExpunge returns the metadata for the ids it was asked about,
|
|
535
|
+
// so call it ONCE with the full id set; it returns `refused` for
|
|
536
|
+
// legal-hold refusals + the metadata rows for the survivors.
|
|
537
|
+
// We then add retention-floor refusals to the response and pass
|
|
538
|
+
// the FINAL surviving id set to a second hardExpunge call? No —
|
|
539
|
+
// the surviving set is computed inline; the simpler shape is:
|
|
540
|
+
//
|
|
541
|
+
// - Filter via metadata read (using a `dryRun` flag on
|
|
542
|
+
// hardExpunge would work but adds API surface)
|
|
543
|
+
//
|
|
544
|
+
// Pragmatic v1: call hardExpunge once with the full set. It refuses
|
|
545
|
+
// legal-hold internally + returns the metadata for the rest. Then
|
|
546
|
+
// we filter the deleted set retroactively for retention-floor
|
|
547
|
+
// violations — but hardExpunge already DELETED them. That's wrong.
|
|
548
|
+
//
|
|
549
|
+
// Correct v1: call hardExpunge with an empty `objectIds` for a
|
|
550
|
+
// metadata-only pass? hardExpunge returns immediately for empty
|
|
551
|
+
// input. So we need an explicit "read metadata for these ids"
|
|
552
|
+
// query OR a hardExpunge `dryRun` flag.
|
|
553
|
+
//
|
|
554
|
+
// Use a fresh SELECT to read the gate-input data, then pass the
|
|
555
|
+
// surviving set to hardExpunge. The store exposes `queryByModseq`
|
|
556
|
+
// but that's a wide scan; for v1 expunge takes the metadata via a
|
|
557
|
+
// dedicated per-id lookup. (The store's hardExpunge SELECT is the
|
|
558
|
+
// same shape; expose it as `_selectForExpunge` via a small adapter,
|
|
559
|
+
// OR just round-trip through the existing fetchByObjectId.)
|
|
560
|
+
var nowMs = Date.now();
|
|
561
|
+
var refused = [];
|
|
562
|
+
var candidates = [];
|
|
563
|
+
for (var i = 0; i < args.objectIds.length; i += 1) {
|
|
564
|
+
var oid = args.objectIds[i];
|
|
565
|
+
var meta = ctx.store.fetchByObjectId(args.folder, oid);
|
|
566
|
+
if (!meta) {
|
|
567
|
+
refused.push({ id: oid, reason: "not-in-folder" });
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (meta.legalHold) {
|
|
571
|
+
refused.push({ id: oid, reason: "legal-hold" });
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
if (floorMs > 0) {
|
|
575
|
+
var receivedAt = meta.receivedAt || meta.internalDate || 0;
|
|
576
|
+
var ageMs = nowMs - receivedAt;
|
|
577
|
+
if (ageMs < floorMs) {
|
|
578
|
+
refused.push({ id: oid, reason: "retention-floor",
|
|
579
|
+
floorMs: floorMs, ageMs: ageMs, posture: posture });
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
candidates.push(oid);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Run the destructive SQL only on the surviving set.
|
|
587
|
+
var result = candidates.length > 0
|
|
588
|
+
? ctx.store.hardExpunge(args.folder, candidates)
|
|
589
|
+
: { rows: [], deleted: [], refused: [] };
|
|
590
|
+
|
|
591
|
+
ctx.auditEmit("mail.agent.expunge.success", args.actor, {
|
|
592
|
+
folder: args.folder,
|
|
593
|
+
requested: args.objectIds.length,
|
|
594
|
+
deleted: result.deleted.length,
|
|
595
|
+
refused: refused.length,
|
|
596
|
+
refusedReasons: refused.reduce(function (acc, r) {
|
|
597
|
+
acc[r.reason] = (acc[r.reason] || 0) + 1; return acc;
|
|
598
|
+
}, {}),
|
|
599
|
+
posture: posture,
|
|
600
|
+
floorMs: floorMs,
|
|
601
|
+
});
|
|
602
|
+
return {
|
|
603
|
+
deleted: result.deleted,
|
|
604
|
+
refused: refused,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
487
608
|
async function _sievePut(ctx, args) {
|
|
488
609
|
// Two-stage validation: agent-level shape guard for RBAC + name +
|
|
489
610
|
// size, then the full RFC 5228 grammar parse via b.safeSieve. The
|
package/lib/mail-store.js
CHANGED
|
@@ -185,6 +185,16 @@ function create(opts) {
|
|
|
185
185
|
var stmtBumpQuota = db.prepare(
|
|
186
186
|
"INSERT INTO " + qQuota + " (folder_id, used_bytes, used_count, cap_bytes, cap_count) VALUES (?, ?, ?, NULL, NULL) " +
|
|
187
187
|
"ON CONFLICT(folder_id) DO UPDATE SET used_bytes = used_bytes + excluded.used_bytes, used_count = used_count + excluded.used_count");
|
|
188
|
+
// Hard-expunge prepared statements — used by `hardExpunge` to delete
|
|
189
|
+
// a message permanently after retention-floor + legal-hold gates
|
|
190
|
+
// pass. The SELECT is the gate-input source (legal_hold flag + age);
|
|
191
|
+
// the DELETE + flag-cleanup + quota-decrement run inside a backend
|
|
192
|
+
// transaction so partial state can't survive a crash.
|
|
193
|
+
var stmtSelectForExpunge = db.prepare(
|
|
194
|
+
"SELECT objectid, folder_id, size_bytes, received_at, legal_hold FROM " + qMsgs +
|
|
195
|
+
" WHERE folder_id = ? AND objectid IN (SELECT value FROM json_each(?))");
|
|
196
|
+
var stmtDeleteMsg = db.prepare("DELETE FROM " + qMsgs + " WHERE objectid = ?");
|
|
197
|
+
var stmtDeleteFlags = db.prepare("DELETE FROM " + qFlags + " WHERE objectid = ?");
|
|
188
198
|
|
|
189
199
|
return {
|
|
190
200
|
appendMessage: function (folderName, rawBytes, appendOpts) {
|
|
@@ -285,6 +295,98 @@ function create(opts) {
|
|
|
285
295
|
objectids.forEach(function (oid) { stmtLegalHold.run(hold, oid); });
|
|
286
296
|
return { changed: objectids.length };
|
|
287
297
|
},
|
|
298
|
+
/**
|
|
299
|
+
* hardExpunge — remove messages permanently from a folder.
|
|
300
|
+
*
|
|
301
|
+
* Returns `{ rows: [{ objectid, size_bytes, received_at, legal_hold }],
|
|
302
|
+
* deleted: <ids>, refused: [{ id, reason }] }`. Per-row
|
|
303
|
+
* `legal_hold` is the column value at expunge time so the caller
|
|
304
|
+
* (typically `b.mail.agent.expunge`) can refuse messages currently
|
|
305
|
+
* under hold.
|
|
306
|
+
*
|
|
307
|
+
* The caller is responsible for:
|
|
308
|
+
* (1) Composing `b.legalHold` to refuse hold-flagged messages
|
|
309
|
+
* before passing the surviving set here, AND
|
|
310
|
+
* (2) Composing `b.retention.complianceFloor` to refuse messages
|
|
311
|
+
* whose `received_at` is inside the regulated retention window.
|
|
312
|
+
*
|
|
313
|
+
* This primitive does the destructive SQL work + transaction-
|
|
314
|
+
* scoped quota decrement + modseq bump. Refusals must happen at
|
|
315
|
+
* the agent layer; this layer is the wire-protocol-shaped backend
|
|
316
|
+
* surface.
|
|
317
|
+
*/
|
|
318
|
+
hardExpunge: function (folderName, objectids) {
|
|
319
|
+
var folder = stmtGetFolderByName.get(folderName);
|
|
320
|
+
if (!folder) {
|
|
321
|
+
throw new MailStoreError("mail-store/no-folder",
|
|
322
|
+
"hardExpunge: folder '" + folderName + "' not found");
|
|
323
|
+
}
|
|
324
|
+
if (!Array.isArray(objectids) || objectids.length === 0) {
|
|
325
|
+
return { rows: [], deleted: [], refused: [] };
|
|
326
|
+
}
|
|
327
|
+
// Deduplicate objectids before the per-id pass. Without this,
|
|
328
|
+
// `hardExpunge(folder, [id, id])` would append the same row to
|
|
329
|
+
// `toDelete` twice and drive `usedBytes` / `usedCount` negative
|
|
330
|
+
// via the double-subtract in the transaction; `deleted` would
|
|
331
|
+
// also carry the duplicate id back to the caller. Preserve
|
|
332
|
+
// first-seen ordering for stable refused/deleted output.
|
|
333
|
+
var seenIds = Object.create(null);
|
|
334
|
+
var uniqueIds = [];
|
|
335
|
+
for (var ui = 0; ui < objectids.length; ui += 1) {
|
|
336
|
+
if (!seenIds[objectids[ui]]) {
|
|
337
|
+
seenIds[objectids[ui]] = true;
|
|
338
|
+
uniqueIds.push(objectids[ui]);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
objectids = uniqueIds;
|
|
342
|
+
var rows = stmtSelectForExpunge.all(folder.id, JSON.stringify(objectids));
|
|
343
|
+
var byId = Object.create(null);
|
|
344
|
+
rows.forEach(function (r) { byId[r.objectid] = r; });
|
|
345
|
+
var refused = [];
|
|
346
|
+
var toDelete = [];
|
|
347
|
+
for (var i = 0; i < objectids.length; i += 1) {
|
|
348
|
+
var oid = objectids[i];
|
|
349
|
+
var row = byId[oid];
|
|
350
|
+
if (!row) {
|
|
351
|
+
refused.push({ id: oid, reason: "not-in-folder" });
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (row.legal_hold === 1) {
|
|
355
|
+
refused.push({ id: oid, reason: "legal-hold" });
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
toDelete.push(row);
|
|
359
|
+
}
|
|
360
|
+
if (toDelete.length === 0) return { rows: rows, deleted: [], refused: refused };
|
|
361
|
+
|
|
362
|
+
// One transaction: delete messages + their flags, bump folder
|
|
363
|
+
// modseq, decrement quota. Better-sqlite3-style `transaction`
|
|
364
|
+
// helpers wrap this; if the backend doesn't expose `transaction`,
|
|
365
|
+
// run the statements directly (atomicity falls back to per-stmt).
|
|
366
|
+
var totalBytes = 0;
|
|
367
|
+
var modseqBump = Date.now();
|
|
368
|
+
function _runTxn() {
|
|
369
|
+
for (var di = 0; di < toDelete.length; di += 1) {
|
|
370
|
+
stmtDeleteFlags.run(toDelete[di].objectid);
|
|
371
|
+
stmtDeleteMsg.run(toDelete[di].objectid);
|
|
372
|
+
totalBytes += toDelete[di].size_bytes || 0;
|
|
373
|
+
}
|
|
374
|
+
stmtBumpFolderModseq.run(modseqBump, folderName);
|
|
375
|
+
if (totalBytes > 0 || toDelete.length > 0) {
|
|
376
|
+
stmtDecrementQuota.run(totalBytes, toDelete.length, folder.id);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (typeof db.transaction === "function") {
|
|
380
|
+
db.transaction(_runTxn)();
|
|
381
|
+
} else {
|
|
382
|
+
_runTxn();
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
rows: rows,
|
|
386
|
+
deleted: toDelete.map(function (r) { return r.objectid; }),
|
|
387
|
+
refused: refused,
|
|
388
|
+
};
|
|
389
|
+
},
|
|
288
390
|
_backend: db,
|
|
289
391
|
_tablePrefix: prefix,
|
|
290
392
|
};
|
|
@@ -445,7 +547,7 @@ function _fetchByObjectId(args) {
|
|
|
445
547
|
bodyText: unsealed.body_text,
|
|
446
548
|
bodyHtml: unsealed.body_html,
|
|
447
549
|
flags: flags,
|
|
448
|
-
legalHold: row.legal_hold === 1,
|
|
550
|
+
legalHold: row.legal_hold === 1, // allow:raw-byte-literal — sqlite INTEGER column 0|1
|
|
449
551
|
};
|
|
450
552
|
}
|
|
451
553
|
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:e4f090dd-9a46-4d67-b64e-b7b9105c5254",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-21T02:10:35.980Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.11.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.11.23",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.11.
|
|
25
|
+
"version": "0.11.23",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.11.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.11.23",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.11.
|
|
57
|
+
"ref": "@blamejs/core@0.11.23",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|