@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.
Files changed (150) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/index.js +4 -0
  4. package/lib/agent-envelope-mac.js +104 -0
  5. package/lib/agent-event-bus.js +105 -4
  6. package/lib/agent-posture-chain.js +8 -42
  7. package/lib/ai-content-detect.js +9 -10
  8. package/lib/api-key.js +107 -74
  9. package/lib/atomic-file.js +62 -4
  10. package/lib/audit-chain.js +47 -11
  11. package/lib/audit-sign.js +77 -2
  12. package/lib/audit-tools.js +79 -51
  13. package/lib/audit.js +249 -123
  14. package/lib/auth/openid-federation.js +108 -47
  15. package/lib/backup/index.js +13 -10
  16. package/lib/break-glass.js +202 -144
  17. package/lib/cache.js +174 -105
  18. package/lib/chain-writer.js +38 -16
  19. package/lib/cli.js +19 -14
  20. package/lib/cluster-provider-db.js +130 -104
  21. package/lib/cluster-storage.js +119 -22
  22. package/lib/cluster.js +119 -71
  23. package/lib/compliance.js +169 -4
  24. package/lib/consent.js +73 -24
  25. package/lib/constants.js +16 -11
  26. package/lib/crypto-field.js +474 -92
  27. package/lib/db-declare-row-policy.js +35 -22
  28. package/lib/db-file-lifecycle.js +3 -2
  29. package/lib/db-query.js +497 -255
  30. package/lib/db-schema.js +209 -44
  31. package/lib/db.js +176 -95
  32. package/lib/error-page.js +14 -1
  33. package/lib/external-db-migrate.js +229 -139
  34. package/lib/external-db.js +25 -15
  35. package/lib/file-upload.js +52 -7
  36. package/lib/framework-error.js +14 -1
  37. package/lib/framework-files.js +73 -0
  38. package/lib/framework-schema.js +695 -394
  39. package/lib/gate-contract.js +649 -1
  40. package/lib/guard-agent-registry.js +26 -44
  41. package/lib/guard-all.js +1 -0
  42. package/lib/guard-auth.js +42 -112
  43. package/lib/guard-cidr.js +33 -154
  44. package/lib/guard-csv.js +46 -113
  45. package/lib/guard-domain.js +34 -157
  46. package/lib/guard-dsn.js +27 -43
  47. package/lib/guard-email.js +47 -69
  48. package/lib/guard-envelope.js +19 -32
  49. package/lib/guard-event-bus-payload.js +24 -42
  50. package/lib/guard-event-bus-topic.js +25 -43
  51. package/lib/guard-filename.js +42 -106
  52. package/lib/guard-graphql.js +42 -123
  53. package/lib/guard-html.js +53 -108
  54. package/lib/guard-idempotency-key.js +24 -42
  55. package/lib/guard-image.js +46 -103
  56. package/lib/guard-imap-command.js +18 -32
  57. package/lib/guard-jmap.js +16 -30
  58. package/lib/guard-json.js +38 -108
  59. package/lib/guard-jsonpath.js +38 -171
  60. package/lib/guard-jwt.js +49 -179
  61. package/lib/guard-list-id.js +25 -41
  62. package/lib/guard-list-unsubscribe.js +27 -43
  63. package/lib/guard-mail-compose.js +24 -42
  64. package/lib/guard-mail-move.js +26 -44
  65. package/lib/guard-mail-query.js +28 -46
  66. package/lib/guard-mail-reply.js +24 -42
  67. package/lib/guard-mail-sieve.js +24 -42
  68. package/lib/guard-managesieve-command.js +17 -31
  69. package/lib/guard-markdown.js +37 -104
  70. package/lib/guard-message-id.js +26 -45
  71. package/lib/guard-mime.js +39 -151
  72. package/lib/guard-oauth.js +54 -135
  73. package/lib/guard-pdf.js +45 -101
  74. package/lib/guard-pop3-command.js +21 -31
  75. package/lib/guard-posture-chain.js +24 -42
  76. package/lib/guard-regex.js +33 -107
  77. package/lib/guard-saga-config.js +24 -42
  78. package/lib/guard-shell.js +42 -172
  79. package/lib/guard-smtp-command.js +48 -54
  80. package/lib/guard-snapshot-envelope.js +24 -42
  81. package/lib/guard-sql.js +1491 -0
  82. package/lib/guard-stream-args.js +24 -43
  83. package/lib/guard-svg.js +47 -65
  84. package/lib/guard-template.js +35 -172
  85. package/lib/guard-tenant-id.js +26 -45
  86. package/lib/guard-time.js +32 -154
  87. package/lib/guard-trace-context.js +25 -44
  88. package/lib/guard-uuid.js +32 -153
  89. package/lib/guard-xml.js +38 -113
  90. package/lib/guard-yaml.js +51 -163
  91. package/lib/http-client.js +37 -9
  92. package/lib/inbox.js +120 -107
  93. package/lib/legal-hold.js +107 -50
  94. package/lib/log-stream-cloudwatch.js +47 -31
  95. package/lib/log-stream-otlp.js +32 -18
  96. package/lib/mail-crypto-smime.js +2 -6
  97. package/lib/mail-greylist.js +2 -6
  98. package/lib/mail-helo.js +2 -6
  99. package/lib/mail-journal.js +85 -64
  100. package/lib/mail-rbl.js +2 -6
  101. package/lib/mail-scan.js +2 -6
  102. package/lib/mail-server-jmap.js +117 -12
  103. package/lib/mail-spam-score.js +2 -6
  104. package/lib/mail-store.js +287 -154
  105. package/lib/middleware/body-parser.js +71 -25
  106. package/lib/middleware/csrf-protect.js +19 -8
  107. package/lib/middleware/fetch-metadata.js +17 -7
  108. package/lib/middleware/idempotency-key.js +54 -38
  109. package/lib/middleware/rate-limit.js +102 -32
  110. package/lib/middleware/security-headers.js +21 -5
  111. package/lib/migrations.js +108 -66
  112. package/lib/network-heartbeat.js +7 -0
  113. package/lib/nonce-store.js +31 -9
  114. package/lib/object-store/azure-blob-bucket-ops.js +9 -4
  115. package/lib/object-store/azure-blob.js +57 -3
  116. package/lib/object-store/sigv4.js +10 -0
  117. package/lib/observability.js +87 -0
  118. package/lib/otel-export.js +25 -1
  119. package/lib/outbox.js +136 -82
  120. package/lib/parsers/safe-xml.js +47 -7
  121. package/lib/pqc-agent.js +44 -0
  122. package/lib/pubsub-cluster.js +42 -20
  123. package/lib/queue-local.js +202 -139
  124. package/lib/queue-redis.js +9 -1
  125. package/lib/queue-sqs.js +6 -0
  126. package/lib/redact.js +68 -11
  127. package/lib/redis-client.js +160 -31
  128. package/lib/retention.js +82 -39
  129. package/lib/router.js +212 -5
  130. package/lib/safe-dns.js +29 -45
  131. package/lib/safe-ical.js +18 -33
  132. package/lib/safe-icap.js +27 -43
  133. package/lib/safe-sieve.js +21 -40
  134. package/lib/safe-sql.js +124 -3
  135. package/lib/safe-vcard.js +18 -33
  136. package/lib/scheduler.js +35 -12
  137. package/lib/seeders.js +122 -74
  138. package/lib/session-stores.js +42 -14
  139. package/lib/session.js +109 -72
  140. package/lib/sql.js +3885 -0
  141. package/lib/ssrf-guard.js +51 -4
  142. package/lib/static.js +177 -34
  143. package/lib/subject.js +55 -17
  144. package/lib/vault/index.js +3 -2
  145. package/lib/vault/passphrase-ops.js +3 -2
  146. package/lib/vault/rotate.js +104 -64
  147. package/lib/vendor-data.js +2 -0
  148. package/lib/websocket.js +35 -5
  149. package/package.json +1 -1
  150. package/sbom.cdx.json +6 -6
package/lib/legal-hold.js CHANGED
@@ -63,8 +63,19 @@ var bCrypto = require("./crypto");
63
63
  var lazyRequire = require("./lazy-require");
64
64
  var safeJson = require("./safe-json");
65
65
  var validateOpts = require("./validate-opts");
66
+ var sql = require("./sql");
66
67
  var { defineClass } = require("./framework-error");
67
68
 
69
+ // Local-SQLite framework table names. These run against the b.db() handle
70
+ // directly (single-node legal-hold registry + the audit_log read), so the
71
+ // b.sql builders carry { quoteName: true } to emit the quoted local name
72
+ // (no clusterStorage prefix rewrite on this path) and bind every value as a
73
+ // placeholder. The names are passed as literals here for the same reason
74
+ // db.js declares them as literals — they ARE the canonical local table
75
+ // identifiers.
76
+ var HOLD_TABLE = "_blamejs_legal_hold"; // allow:hand-rolled-sql — canonical local table-name; passed to b.sql with quoteName
77
+ var AUDIT_TABLE = "audit_log";
78
+
68
79
  var audit = lazyRequire(function () { return require("./audit"); });
69
80
 
70
81
  var LegalHoldError = defineClass("LegalHoldError", { alwaysPermanent: true });
@@ -99,6 +110,24 @@ function _hashSubject(subjectId) {
99
110
  return bCrypto.sha3Hash("bj-legal-hold:" + subjectId);
100
111
  }
101
112
 
113
+ // Resolve the b.sql dialect for the operator-supplied handle. The framework's
114
+ // local b.db handle is always node:sqlite (db.js pins { dialect: "sqlite",
115
+ // quoteName: true }) and exposes no .dialect, so this defaults to "sqlite" —
116
+ // the registry + the audit_log read both run against that local handle via
117
+ // .prepare(). An operator handle that DOES advertise a dialect (string or
118
+ // () -> string) has it threaded through so the emitted identifier quoting +
119
+ // idioms match the backend the handle dispatches to. quoteName stays on for
120
+ // every legal-hold statement: these are canonical local table names quoted by
121
+ // construction (no clusterStorage prefix rewrite on this path).
122
+ function _handleDialect(db) {
123
+ if (db && typeof db.dialect === "function") {
124
+ try { var d = db.dialect(); return typeof d === "string" ? d : "sqlite"; }
125
+ catch (_e) { return "sqlite"; }
126
+ }
127
+ if (db && typeof db.dialect === "string") return db.dialect;
128
+ return "sqlite";
129
+ }
130
+
102
131
  function create(opts) {
103
132
  opts = opts || {};
104
133
  validateOpts(opts, ["db", "audit", "signWith"], "legalHold");
@@ -106,6 +135,11 @@ function create(opts) {
106
135
  throw _err("BAD_OPT", "create: opts.db is required (a b.db handle)");
107
136
  }
108
137
  var db = opts.db;
138
+ // b.sql opts for every legal-hold statement built against this handle. The
139
+ // dialect tracks the handle (sqlite for the framework's local b.db); the
140
+ // local table names are quoted by construction (quoteName) with no
141
+ // clusterStorage prefix rewrite on this path.
142
+ var SQL_OPTS = { dialect: _handleDialect(db), quoteName: true };
109
143
  var auditOn = opts.audit !== false && opts.audit != null;
110
144
  var auditInstance = (opts.audit && opts.audit !== true) ? opts.audit : null;
111
145
  // signWith is reserved for future per-event detached signatures;
@@ -135,19 +169,21 @@ function create(opts) {
135
169
  // table via FRAMEWORK_SCHEMA at boot; this guard is for the
136
170
  // external-db / cluster path where schema migrations are operator-
137
171
  // driven. Either way the IF NOT EXISTS shape is safe to re-run.
172
+ // DDL built through b.sql.createTable so every identifier is quoted by
173
+ // construction (quoteName) and the type tokens come from the framework
174
+ // type map.
138
175
  var fn = db.runSql || db.execRaw;
139
176
  if (typeof fn === "function") {
140
- fn(
141
- 'CREATE TABLE IF NOT EXISTS "_blamejs_legal_hold" (' +
142
- '"subjectIdHash" TEXT PRIMARY KEY,' +
143
- '"placedAt" INTEGER NOT NULL,' +
144
- '"placedBy" TEXT,' +
145
- '"reason" TEXT NOT NULL,' +
146
- '"custodian" TEXT,' +
147
- '"citation" TEXT,' +
148
- '"retainUntil" INTEGER' +
149
- ')'
150
- );
177
+ var ddl = sql.createTable(HOLD_TABLE, [
178
+ { name: "subjectIdHash", type: "text", primaryKey: true },
179
+ { name: "placedAt", type: "int", notNull: true },
180
+ { name: "placedBy", type: "text" },
181
+ { name: "reason", type: "text", notNull: true },
182
+ { name: "custodian", type: "text" },
183
+ { name: "citation", type: "text" },
184
+ { name: "retainUntil", type: "int" },
185
+ ], SQL_OPTS);
186
+ fn(ddl.sql);
151
187
  }
152
188
  }
153
189
 
@@ -171,9 +207,12 @@ function create(opts) {
171
207
  }
172
208
  _ensureSchema();
173
209
  var hash = _hashSubject(sid);
174
- var existing = db.prepare(
175
- 'SELECT placedAt FROM "_blamejs_legal_hold" WHERE subjectIdHash = ?'
176
- ).get(hash);
210
+ var placeSelBuilt = sql.select(HOLD_TABLE, SQL_OPTS)
211
+ .columns(["placedAt"])
212
+ .where("subjectIdHash", hash)
213
+ .toSql();
214
+ var placeSelStmt = db.prepare(placeSelBuilt.sql);
215
+ var existing = placeSelStmt.get.apply(placeSelStmt, placeSelBuilt.params);
177
216
  if (existing) {
178
217
  _emit("legalhold.place_rejected",
179
218
  { subjectId: sid, reason: "already-held",
@@ -182,18 +221,19 @@ function create(opts) {
182
221
  return { error: "already-held", placedAt: existing.placedAt };
183
222
  }
184
223
  var nowMs = Date.now();
185
- db.prepare(
186
- 'INSERT INTO "_blamejs_legal_hold" ' +
187
- '(subjectIdHash, placedAt, placedBy, reason, custodian, citation, retainUntil) ' +
188
- 'VALUES (?, ?, ?, ?, ?, ?, ?)'
189
- ).run(
190
- hash, nowMs,
191
- args.placedBy || null,
192
- args.reason,
193
- args.custodian || null,
194
- args.citation || null,
195
- args.retainUntil || null
196
- );
224
+ var placeInsBuilt = sql.insert(HOLD_TABLE, SQL_OPTS)
225
+ .values({
226
+ subjectIdHash: hash,
227
+ placedAt: nowMs,
228
+ placedBy: args.placedBy || null,
229
+ reason: args.reason,
230
+ custodian: args.custodian || null,
231
+ citation: args.citation || null,
232
+ retainUntil: args.retainUntil || null,
233
+ })
234
+ .toSql();
235
+ var placeInsStmt = db.prepare(placeInsBuilt.sql);
236
+ placeInsStmt.run.apply(placeInsStmt, placeInsBuilt.params);
197
237
  _emit("legalhold.placed",
198
238
  { subjectId: sid, reason: args.reason,
199
239
  custodian: args.custodian || null,
@@ -216,18 +256,23 @@ function create(opts) {
216
256
  }
217
257
  _ensureSchema();
218
258
  var hash = _hashSubject(sid);
219
- var existing = db.prepare(
220
- 'SELECT placedAt, reason FROM "_blamejs_legal_hold" WHERE subjectIdHash = ?'
221
- ).get(hash);
259
+ var relSelBuilt = sql.select(HOLD_TABLE, SQL_OPTS)
260
+ .columns(["placedAt", "reason"])
261
+ .where("subjectIdHash", hash)
262
+ .toSql();
263
+ var relSelStmt = db.prepare(relSelBuilt.sql);
264
+ var existing = relSelStmt.get.apply(relSelStmt, relSelBuilt.params);
222
265
  if (!existing) {
223
266
  _emit("legalhold.release_rejected",
224
267
  { subjectId: sid, reason: "not-held" },
225
268
  "denied");
226
269
  return { error: "not-held" };
227
270
  }
228
- db.prepare(
229
- 'DELETE FROM "_blamejs_legal_hold" WHERE subjectIdHash = ?'
230
- ).run(hash);
271
+ var relDelBuilt = sql.delete(HOLD_TABLE, SQL_OPTS)
272
+ .where("subjectIdHash", hash)
273
+ .toSql();
274
+ var relDelStmt = db.prepare(relDelBuilt.sql);
275
+ relDelStmt.run.apply(relDelStmt, relDelBuilt.params);
231
276
  _emit("legalhold.released",
232
277
  { subjectId: sid, reason: args.reason,
233
278
  approver: args.approver,
@@ -241,9 +286,12 @@ function create(opts) {
241
286
  var sid = _subjectIdString(subjectId);
242
287
  _ensureSchema();
243
288
  var hash = _hashSubject(sid);
244
- var row = db.prepare(
245
- 'SELECT retainUntil FROM "_blamejs_legal_hold" WHERE subjectIdHash = ?'
246
- ).get(hash);
289
+ var heldBuilt = sql.select(HOLD_TABLE, SQL_OPTS)
290
+ .columns(["retainUntil"])
291
+ .where("subjectIdHash", hash)
292
+ .toSql();
293
+ var heldStmt = db.prepare(heldBuilt.sql);
294
+ var row = heldStmt.get.apply(heldStmt, heldBuilt.params);
247
295
  if (!row) return false;
248
296
  // retainUntil expiry — when the operator pinned a sunset and it
249
297
  // has passed, the hold has lapsed and isHeld returns false. The
@@ -257,10 +305,12 @@ function create(opts) {
257
305
  var sid = _subjectIdString(subjectId);
258
306
  _ensureSchema();
259
307
  var hash = _hashSubject(sid);
260
- var row = db.prepare(
261
- 'SELECT subjectIdHash, placedAt, placedBy, reason, custodian, citation, retainUntil ' +
262
- 'FROM "_blamejs_legal_hold" WHERE subjectIdHash = ?'
263
- ).get(hash);
308
+ var getBuilt = sql.select(HOLD_TABLE, SQL_OPTS)
309
+ .columns(["subjectIdHash", "placedAt", "placedBy", "reason", "custodian", "citation", "retainUntil"])
310
+ .where("subjectIdHash", hash)
311
+ .toSql();
312
+ var getStmt = db.prepare(getBuilt.sql);
313
+ var row = getStmt.get.apply(getStmt, getBuilt.params);
264
314
  if (!row) return null;
265
315
  return {
266
316
  subjectId: sid,
@@ -276,10 +326,12 @@ function create(opts) {
276
326
 
277
327
  function list() {
278
328
  _ensureSchema();
279
- var rows = db.prepare(
280
- 'SELECT subjectIdHash, placedAt, placedBy, reason, custodian, citation, retainUntil ' +
281
- 'FROM "_blamejs_legal_hold" ORDER BY placedAt'
282
- ).all();
329
+ var listBuilt = sql.select(HOLD_TABLE, SQL_OPTS)
330
+ .columns(["subjectIdHash", "placedAt", "placedBy", "reason", "custodian", "citation", "retainUntil"])
331
+ .orderBy("placedAt", "asc")
332
+ .toSql();
333
+ var listStmt = db.prepare(listBuilt.sql);
334
+ var rows = listStmt.all.apply(listStmt, listBuilt.params);
283
335
  var nowMs = Date.now();
284
336
  return rows.map(function (r) {
285
337
  return {
@@ -302,15 +354,20 @@ function create(opts) {
302
354
  var sid = _subjectIdString(subjectId);
303
355
  var rows = [];
304
356
  try {
305
- var auditQuery = db.prepare(
306
- 'SELECT recordedAt, action, metadata, outcome ' +
307
- 'FROM audit_log ' +
308
- 'WHERE action LIKE ? AND resourceKind = ? ' +
309
- 'ORDER BY recordedAt'
310
- );
357
+ // `action LIKE 'legalhold.%'` is a prefix match — b.sql's whereLike
358
+ // in prefix mode emits `"action" LIKE ? ESCAPE '~'` with the live
359
+ // trailing wildcard while escaping the term's own metacharacters
360
+ // (none here), so the wildcard stays builder-controlled.
361
+ var histBuilt = sql.select(AUDIT_TABLE, SQL_OPTS)
362
+ .columns(["recordedAt", "action", "metadata", "outcome"])
363
+ .whereLike("action", "legalhold.", "prefix")
364
+ .where("resourceKind", "legal-hold")
365
+ .orderBy("recordedAt", "asc")
366
+ .toSql();
367
+ var auditQuery = db.prepare(histBuilt.sql);
311
368
  // resourceId is sealed, so match on resourceKind + post-filter
312
369
  // by parsed metadata.
313
- var raw = auditQuery.all("legalhold.%", "legal-hold");
370
+ var raw = auditQuery.all.apply(auditQuery, histBuilt.params);
314
371
  for (var i = 0; i < raw.length; i++) {
315
372
  var meta = null;
316
373
  try { meta = safeJson.parse(raw[i].metadata || "{}"); } catch (_e) { meta = null; }
@@ -205,6 +205,10 @@ function create(config) {
205
205
  var inFlight = false;
206
206
  var closed = false;
207
207
  var sequenceToken = null;
208
+ // Captures the in-flight drain (the IIFE inside _flush) so close() can await
209
+ // the actual run before flipping `closed` — otherwise the _flush while-loop's
210
+ // `&& !closed` bails and buffered records are stranded at shutdown.
211
+ var inFlightPromise = null;
208
212
  var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
209
213
 
210
214
  function _takeBatch() {
@@ -236,40 +240,44 @@ function create(config) {
236
240
  }
237
241
 
238
242
  async function _flush() {
239
- if (inFlight) return;
243
+ if (inFlight) return inFlightPromise;
240
244
  if (buffer.length === 0) return;
241
245
  inFlight = true;
242
- try {
243
- try { await _ensureAutoCreated(); }
244
- catch (acErr) {
245
- // autoCreate failure is permanent — every subsequent batch
246
- // would hit the same error. Drop the queue with the
247
- // operator-supplied onDrop callback so they see exactly which
248
- // events were lost AND why, then bail.
249
- var allBuffered = buffer.splice(0, buffer.length);
250
- dropCount += allBuffered.length;
251
- _emitDrop("autocreate-failed", allBuffered, acErr);
252
- return;
253
- }
254
- while (buffer.length > 0 && !closed) {
255
- var batch = _takeBatch();
256
- if (batch.length === 0) break;
257
- try {
258
- await retryHelper.withRetry(function () {
259
- return _send(batch);
260
- }, Object.assign({
261
- isPermanent: _isPermanentAwsError,
262
- }, cfg.retry || {}));
263
- } catch (e) {
264
- dropCount += batch.length;
265
- _emitDrop("retry-exhausted", batch, e);
266
- break;
246
+ inFlightPromise = (async function () {
247
+ try {
248
+ try { await _ensureAutoCreated(); }
249
+ catch (acErr) {
250
+ // autoCreate failure is permanent every subsequent batch
251
+ // would hit the same error. Drop the queue with the
252
+ // operator-supplied onDrop callback so they see exactly which
253
+ // events were lost AND why, then bail.
254
+ var allBuffered = buffer.splice(0, buffer.length);
255
+ dropCount += allBuffered.length;
256
+ _emitDrop("autocreate-failed", allBuffered, acErr);
257
+ return;
258
+ }
259
+ while (buffer.length > 0 && !closed) {
260
+ var batch = _takeBatch();
261
+ if (batch.length === 0) break;
262
+ try {
263
+ await retryHelper.withRetry(function () {
264
+ return _send(batch);
265
+ }, Object.assign({
266
+ isPermanent: _isPermanentAwsError,
267
+ }, cfg.retry || {}));
268
+ } catch (e) {
269
+ dropCount += batch.length;
270
+ _emitDrop("retry-exhausted", batch, e);
271
+ break;
272
+ }
267
273
  }
274
+ } finally {
275
+ inFlight = false;
276
+ inFlightPromise = null;
277
+ if (buffer.length > 0) flushScheduler.schedule();
268
278
  }
269
- } finally {
270
- inFlight = false;
271
- if (buffer.length > 0) flushScheduler.schedule();
272
- }
279
+ })();
280
+ return inFlightPromise;
273
281
  }
274
282
 
275
283
  async function _send(batch) {
@@ -333,9 +341,17 @@ function create(config) {
333
341
  }
334
342
 
335
343
  async function close() {
336
- closed = true;
344
+ // Drain BEFORE flipping closed=true — _flush()'s `while (... && !closed)`
345
+ // loop bails on !closed, so flipping the flag first strands the records the
346
+ // operator queued just before shutdown (the b.logStream drain contract).
347
+ // Stop the timer, await any in-flight drain, drain the remainder, THEN
348
+ // refuse new enqueues. Mirrors the webhook sink.
337
349
  flushScheduler.cancel();
350
+ if (inFlightPromise) {
351
+ try { await inFlightPromise; } catch (_e) { /* surfaced via onDrop */ }
352
+ }
338
353
  await _flush();
354
+ closed = true;
339
355
  }
340
356
 
341
357
  function stats() {
@@ -210,30 +210,38 @@ function create(config) {
210
210
  var dropCount = 0;
211
211
  var inFlight = false;
212
212
  var closed = false;
213
+ // Captures the in-flight drain so close() awaits the real run before flipping
214
+ // `closed` (the _flush while-loop bails on !closed → flipping first strands
215
+ // buffered records at shutdown).
216
+ var inFlightPromise = null;
213
217
  var flushScheduler = safeAsync.makeScheduledFlush(cfg.maxBatchAgeMs, function () { return _flush(); });
214
218
 
215
219
  async function _flush() {
216
- if (inFlight) return;
220
+ if (inFlight) return inFlightPromise;
217
221
  if (buffer.length === 0) return;
218
222
  inFlight = true;
219
- try {
220
- while (buffer.length > 0 && !closed) {
221
- var batch = buffer.splice(0, cfg.batchSize);
222
- var body = _serializeBatch(batch, cfg, scopeVersion);
223
- try {
224
- await retryHelper.withRetry(function () {
225
- return _post(resolvedUrl, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
226
- }, cfg.retry);
227
- } catch (e) {
228
- dropCount += batch.length;
229
- _emitDrop("retry-exhausted", batch, e);
230
- break;
223
+ inFlightPromise = (async function () {
224
+ try {
225
+ while (buffer.length > 0 && !closed) {
226
+ var batch = buffer.splice(0, cfg.batchSize);
227
+ var body = _serializeBatch(batch, cfg, scopeVersion);
228
+ try {
229
+ await retryHelper.withRetry(function () {
230
+ return _post(resolvedUrl, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
231
+ }, cfg.retry);
232
+ } catch (e) {
233
+ dropCount += batch.length;
234
+ _emitDrop("retry-exhausted", batch, e);
235
+ break;
236
+ }
231
237
  }
238
+ } finally {
239
+ inFlight = false;
240
+ inFlightPromise = null;
241
+ if (buffer.length > 0) flushScheduler.schedule();
232
242
  }
233
- } finally {
234
- inFlight = false;
235
- if (buffer.length > 0) flushScheduler.schedule();
236
- }
243
+ })();
244
+ return inFlightPromise;
237
245
  }
238
246
 
239
247
  function emit(record) {
@@ -253,9 +261,15 @@ function create(config) {
253
261
  }
254
262
 
255
263
  async function close() {
256
- closed = true;
264
+ // Drain BEFORE flipping closed=true (see _flush's `&& !closed` guard) so
265
+ // records queued just before shutdown reach the wire. Mirrors the webhook
266
+ // + cloudwatch sinks.
257
267
  flushScheduler.cancel();
268
+ if (inFlightPromise) {
269
+ try { await inFlightPromise; } catch (_e) { /* surfaced via onDrop */ }
270
+ }
258
271
  await _flush();
272
+ closed = true;
259
273
  }
260
274
 
261
275
  function stats() {
@@ -102,6 +102,7 @@ var cms = require("./cms-codec");
102
102
  var asn1 = require("./asn1-der");
103
103
  var pqcSoftware = require("./pqc-software");
104
104
  var bCrypto = require("./crypto");
105
+ var gateContract = require("./gate-contract");
105
106
  var { defineClass } = require("./framework-error");
106
107
 
107
108
  var MailCryptoError = defineClass("MailCryptoError", { alwaysPermanent: true });
@@ -121,12 +122,7 @@ var REFUSED_HASHES = ["md5", "sha1"];
121
122
  // encryption is added, it composes the same set with the @intro EFAIL
122
123
  // defenses applied.
123
124
  var PROFILES = ["strict", "balanced", "permissive"];
124
- var COMPLIANCE_POSTURES = {
125
- hipaa: "strict",
126
- "pci-dss": "strict",
127
- gdpr: "strict",
128
- soc2: "strict",
129
- };
125
+ var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES;
130
126
 
131
127
  // ---- Public surface (v0.10.16 lights up — composes b.cms) ----
132
128
 
@@ -102,6 +102,7 @@ var { defineClass } = require("./framework-error");
102
102
  var bCrypto = require("./crypto");
103
103
  var lazyRequire = require("./lazy-require");
104
104
  var ipUtils = require("./ip-utils");
105
+ var gateContract = require("./gate-contract");
105
106
 
106
107
  var audit = lazyRequire(function () { return require("./audit"); });
107
108
 
@@ -126,12 +127,7 @@ var PROFILES = Object.freeze({
126
127
  permissive: { minDelayMs: C.TIME.seconds(30), whitelistTtlMs: C.TIME.days(30), ipv4Prefix: 32, ipv6Prefix: 128 }, // RFC 6647 §4.4 prefixes
127
128
  });
128
129
 
129
- var COMPLIANCE_POSTURES = Object.freeze({
130
- hipaa: "strict",
131
- "pci-dss": "strict",
132
- gdpr: "strict",
133
- soc2: "strict",
134
- });
130
+ var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES;
135
131
 
136
132
  var IPV4_RE = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/; // allow:regex-no-length-cap — anchored + per-octet repeat-cap
137
133
  var IPV6_RE = /^[0-9a-fA-F:]+$/; // allow:regex-no-length-cap — length-checked separately
package/lib/mail-helo.js CHANGED
@@ -104,6 +104,7 @@
104
104
  var { defineClass } = require("./framework-error");
105
105
  var lazyRequire = require("./lazy-require");
106
106
  var ipUtils = require("./ip-utils");
107
+ var gateContract = require("./gate-contract");
107
108
 
108
109
  var audit = lazyRequire(function () { return require("./audit"); });
109
110
 
@@ -145,12 +146,7 @@ var PROFILES = Object.freeze({
145
146
  },
146
147
  });
147
148
 
148
- var COMPLIANCE_POSTURES = Object.freeze({
149
- hipaa: "strict",
150
- "pci-dss": "strict",
151
- gdpr: "strict",
152
- soc2: "strict",
153
- });
149
+ var COMPLIANCE_POSTURES = gateContract.ALL_STRICT_POSTURES;
154
150
 
155
151
  // Operator-extensible default list of generic-rDNS patterns the
156
152
  // framework ships. Each is a RegExp — case-insensitive — designed