emailengine-app 2.68.1 → 2.69.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 (68) hide show
  1. package/.github/workflows/deploy.yml +2 -0
  2. package/.github/workflows/release.yaml +4 -0
  3. package/CHANGELOG.md +40 -0
  4. package/config/default.toml +2 -0
  5. package/data/google-crawlers.json +7 -1
  6. package/lib/account.js +62 -25
  7. package/lib/api-routes/account-routes.js +493 -75
  8. package/lib/api-routes/blocklist-routes.js +337 -0
  9. package/lib/api-routes/delivery-test-routes.js +321 -0
  10. package/lib/api-routes/export-routes.js +1 -12
  11. package/lib/api-routes/gateway-routes.js +376 -0
  12. package/lib/api-routes/license-routes.js +142 -0
  13. package/lib/api-routes/mailbox-routes.js +318 -0
  14. package/lib/api-routes/message-routes.js +21 -129
  15. package/lib/api-routes/oauth2-app-routes.js +631 -0
  16. package/lib/api-routes/outbox-routes.js +173 -0
  17. package/lib/api-routes/pubsub-routes.js +98 -0
  18. package/lib/api-routes/route-helpers.js +45 -0
  19. package/lib/api-routes/settings-routes.js +331 -0
  20. package/lib/api-routes/stats-routes.js +77 -0
  21. package/lib/api-routes/submit-routes.js +472 -0
  22. package/lib/api-routes/template-routes.js +7 -55
  23. package/lib/api-routes/token-routes.js +297 -0
  24. package/lib/api-routes/webhook-route-routes.js +152 -0
  25. package/lib/email-client/gmail-client.js +14 -0
  26. package/lib/email-client/imap/mailbox.js +34 -11
  27. package/lib/email-client/imap/subconnection.js +20 -12
  28. package/lib/email-client/imap/sync-operations.js +130 -2
  29. package/lib/email-client/imap-client.js +116 -58
  30. package/lib/email-client/outlook-client.js +85 -13
  31. package/lib/export.js +60 -19
  32. package/lib/imapproxy/imap-core/lib/commands/starttls.js +18 -0
  33. package/lib/imapproxy/imap-core/lib/imap-command.js +6 -1
  34. package/lib/imapproxy/imap-core/lib/imap-connection.js +106 -23
  35. package/lib/imapproxy/imap-core/lib/imap-server.js +24 -0
  36. package/lib/imapproxy/imap-core/lib/imap-stream.js +26 -0
  37. package/lib/message-port-stream.js +113 -16
  38. package/lib/reject-worker-calls.js +42 -0
  39. package/lib/routes-ui.js +37 -8778
  40. package/lib/schemas.js +26 -1
  41. package/lib/tools.js +68 -0
  42. package/lib/ui-routes/account-routes.js +40 -210
  43. package/lib/ui-routes/admin-config-routes.js +913 -487
  44. package/lib/ui-routes/admin-entities-routes.js +1 -0
  45. package/lib/ui-routes/auth-routes.js +1339 -0
  46. package/lib/ui-routes/dashboard-routes.js +188 -0
  47. package/lib/ui-routes/document-store-routes.js +800 -0
  48. package/lib/ui-routes/export-routes.js +217 -0
  49. package/lib/ui-routes/internals-routes.js +354 -0
  50. package/lib/ui-routes/network-config-routes.js +759 -0
  51. package/lib/ui-routes/{oauth-routes.js → oauth-config-routes.js} +371 -91
  52. package/lib/ui-routes/route-helpers.js +316 -0
  53. package/lib/ui-routes/smtp-test-routes.js +236 -0
  54. package/lib/ui-routes/unsubscribe-routes.js +234 -0
  55. package/lib/webhook-request.js +36 -0
  56. package/package.json +8 -8
  57. package/sbom.json +1 -1
  58. package/server.js +214 -16
  59. package/static/licenses.html +12 -12
  60. package/translations/messages.pot +129 -149
  61. package/views/dashboard.hbs +7 -26
  62. package/views/internals/index.hbs +15 -0
  63. package/views/tokens/index.hbs +9 -0
  64. package/workers/api.js +198 -4401
  65. package/workers/export.js +87 -54
  66. package/workers/imap.js +29 -13
  67. package/workers/submit.js +20 -11
  68. package/workers/webhooks.js +6 -20
package/server.js CHANGED
@@ -72,7 +72,8 @@ const {
72
72
  getRedisStats,
73
73
  threadStats,
74
74
  httpAgent,
75
- reloadHttpProxyAgent
75
+ reloadHttpProxyAgent,
76
+ maybeReloadHttpProxyAgent
76
77
  } = require('./lib/tools');
77
78
  const MetricsCollector = require('./lib/metrics-collector');
78
79
 
@@ -148,6 +149,8 @@ const { QueueEvents } = require('bullmq');
148
149
 
149
150
  const getSecret = require('./lib/get-secret');
150
151
 
152
+ const { rejectWorkerCalls } = require('./lib/reject-worker-calls');
153
+
151
154
  const msgpack = require('msgpack5')();
152
155
 
153
156
  // Initialize default configuration values if not set
@@ -220,6 +223,10 @@ config.dbs.redis = readEnvValue('EENGINE_REDIS') || readEnvValue('REDIS_URL') ||
220
223
  config.workers.imap = getWorkerCount(readEnvValue('EENGINE_WORKERS') || config.workers.imap) || 4;
221
224
  config.workers.webhooks = Number(readEnvValue('EENGINE_WORKERS_WEBHOOKS')) || config.workers.webhooks || 1;
222
225
  config.workers.submit = Number(readEnvValue('EENGINE_WORKERS_SUBMIT')) || config.workers.submit || 1;
226
+ // API worker count. Values >1 require SO_REUSEPORT (Linux); on unsupported platforms it falls back to 1 at startup.
227
+ // Uses getWorkerCount() for parity with EENGINE_WORKERS (supports "cpus"); Math.floor avoids a fractional
228
+ // count over-spawning, Math.max keeps at least one API worker.
229
+ config.workers.api = Math.max(1, Math.floor(getWorkerCount(readEnvValue('EENGINE_WORKERS_API') || config.workers.api)));
223
230
 
224
231
  config.api.port =
225
232
  (hasEnvValue('EENGINE_PORT') && Number(readEnvValue('EENGINE_PORT'))) || (hasEnvValue('PORT') && Number(readEnvValue('PORT'))) || config.api.port;
@@ -275,6 +282,12 @@ const NO_ACTIVE_HANDLER_RESP_ERR = new Error('No active handler for requested ac
275
282
  NO_ACTIVE_HANDLER_RESP_ERR.statusCode = 503;
276
283
  NO_ACTIVE_HANDLER_RESP_ERR.code = 'WorkerNotAvailable';
277
284
 
285
+ // Shared rejection for in-flight calls whose target worker terminated mid-request.
286
+ // Reused across concurrent rejections - never attach per-call fields to this instance.
287
+ const WORKER_DIED_RESP_ERR = new Error('Worker handling the request terminated before completion. Try again.');
288
+ WORKER_DIED_RESP_ERR.statusCode = 503;
289
+ WORKER_DIED_RESP_ERR.code = 'WorkerNotAvailable';
290
+
278
291
  // Update check intervals
279
292
  const UPGRADE_CHECK_TIMEOUT = 1 * 24 * 3600 * 1000; // 24 hours
280
293
  const LICENSE_CHECK_TIMEOUT = 20 * 60 * 1000; // 20 minutes
@@ -315,6 +328,7 @@ const THREAD_NAMES = {
315
328
  */
316
329
  const THREAD_CONFIG_VALUES = {
317
330
  imap: { key: 'EENGINE_WORKERS', value: config.workers.imap },
331
+ api: { key: 'EENGINE_WORKERS_API', value: config.workers.api },
318
332
  submit: { key: 'EENGINE_WORKERS_SUBMIT', value: config.workers.submit },
319
333
  webhooks: { key: 'EENGINE_WORKERS_WEBHOOKS', value: config.workers.webhooks },
320
334
  export: { key: 'EENGINE_WORKERS_EXPORT', value: config.workers.export || 1 }
@@ -326,6 +340,97 @@ const queueEvents = {};
326
340
  // Unique run index for this server instance
327
341
  let runIndex;
328
342
 
343
+ // Whether multiple API workers may share the listen port via SO_REUSEPORT.
344
+ // Determined once at startup by probeReusePort(); false means a single API worker.
345
+ let apiWorkerReusePort = false;
346
+
347
+ // Requested API worker count (EENGINE_WORKERS_API), surfaced on /admin/internals so operators can
348
+ // see when more workers were requested than could be started. Whether we fell back to a single
349
+ // worker is derived: `apiWorkersRequested > 1 && !apiWorkerReusePort`.
350
+ let apiWorkersRequested = 1;
351
+
352
+ // Why a multi-worker request fell back to a single API worker, surfaced on /admin/internals so the
353
+ // banner can state the real cause. Set during probeReusePort():
354
+ // - 'platform': SO_REUSEPORT is unsupported on this OS/Node build (Linux with Node < 23.1, macOS, Windows)
355
+ // - 'port': the probe could not test the port (transient conflict / permission), so support is unknown
356
+ // Stays null when SO_REUSEPORT is supported or only one worker was requested.
357
+ let apiWorkerReusePortFallbackReason = null;
358
+
359
+ /**
360
+ * Probe whether SO_REUSEPORT can load-balance across multiple sockets on this
361
+ * platform. Binds two listeners to the same host:port with reusePort enabled;
362
+ * both must succeed for load balancing to work. It is unsupported on macOS,
363
+ * Windows, and Node < 23.1, where either the first bind fails outright (e.g.
364
+ * ENOTSUP) or - when the reusePort option is silently ignored (Node < 23.1) -
365
+ * the first bind succeeds as a plain bind and the second fails with EADDRINUSE.
366
+ * The `stage` field tells the caller where the probe failed so it can separate a
367
+ * genuine platform/capability limitation from a transient port conflict:
368
+ * - 'probe': the first bind failed, so the port could not even be tested. An
369
+ * EADDRINUSE/EACCES code here means the port is unavailable (transient); any
370
+ * other code means the platform rejected reusePort.
371
+ * - 'reuse': the first bind succeeded but the second did not, so reusePort is
372
+ * not honored on this platform/Node - a capability limitation, even though
373
+ * the failing code is EADDRINUSE.
374
+ * - null: SO_REUSEPORT is supported.
375
+ * @param {string} host - Bind address
376
+ * @param {number} port - Listen port
377
+ * @returns {Promise<{supported: boolean, code: (string|null), stage: (string|null)}>}
378
+ * Whether SO_REUSEPORT load balancing works, the failing bind's error code (or
379
+ * null), and which bind failed ('probe', 'reuse', or null when supported)
380
+ */
381
+ async function probeReusePort(host, port) {
382
+ const net = require('net');
383
+
384
+ const open = () =>
385
+ new Promise(resolve => {
386
+ let srv = net.createServer();
387
+ srv.once('error', err => resolve({ srv: null, code: (err && err.code) || null }));
388
+ try {
389
+ srv.listen({ host, port, reusePort: true }, () => resolve({ srv, code: null }));
390
+ } catch (err) {
391
+ resolve({ srv: null, code: (err && err.code) || null });
392
+ }
393
+ });
394
+
395
+ const close = srv =>
396
+ new Promise(resolve => {
397
+ if (!srv) {
398
+ return resolve();
399
+ }
400
+ srv.close(() => resolve());
401
+ });
402
+
403
+ let first = await open();
404
+ if (!first.srv) {
405
+ // Could not even bind the first probe socket, so the port itself could not be tested.
406
+ return { supported: false, code: first.code, stage: 'probe' };
407
+ }
408
+
409
+ let second = await open();
410
+ await close(first.srv);
411
+ await close(second.srv);
412
+
413
+ if (second.srv) {
414
+ return { supported: true, code: null, stage: null };
415
+ }
416
+
417
+ // The first bind succeeded but a second listener on the same host:port did not, so the kernel is
418
+ // not load-balancing via SO_REUSEPORT - the option was accepted/ignored but not honored (e.g.
419
+ // Linux with Node < 23.1, where reusePort is an unrecognized listen() option). This is a platform
420
+ // limitation, not a port conflict, even though the failing code is EADDRINUSE.
421
+ return { supported: false, code: second.code, stage: 'reuse' };
422
+ }
423
+
424
+ /**
425
+ * Compute per-worker spawn options. API workers receive their index and whether to
426
+ * bind with SO_REUSEPORT; other worker types need no extra workerData. Keeps the
427
+ * startup and respawn paths deriving spawn options from a single place.
428
+ * @param {string} type - Worker type
429
+ * @param {number} workerIndex - Zero-based index within the worker type
430
+ * @returns {Object|undefined} Spawn options for spawnWorker(), or undefined
431
+ */
432
+ const workerSpawnOpts = (type, workerIndex) => (type === 'api' ? { workerData: { workerIndex, reusePort: apiWorkerReusePort } } : undefined);
433
+
329
434
  // Prepared configuration handling
330
435
  let preparedSettings = false;
331
436
  const preparedSettingsString = readEnvValue('EENGINE_SETTINGS') || config.settings;
@@ -682,7 +787,7 @@ let updateServerState = async (type, state, payload) => {
682
787
  for (let worker of workers.get('api')) {
683
788
  let callPayload = {
684
789
  cmd: 'change',
685
- type: '${type}ServerState',
790
+ type: `${type}ServerState`,
686
791
  key: state,
687
792
  payload: payload || null
688
793
  };
@@ -930,9 +1035,11 @@ async function sendWebhook(account, event, data) {
930
1035
  /**
931
1036
  * Spawn a new worker thread of the specified type
932
1037
  * @param {string} type - Worker type (imap, api, webhooks, submit, documents, smtp, imapProxy)
1038
+ * @param {Object} [opts] - Optional spawn options
1039
+ * @param {Object} [opts.workerData] - Data passed to the worker thread (e.g. API worker index)
933
1040
  * @returns {Promise<number|void>} Thread ID if successful
934
1041
  */
935
- let spawnWorker = async type => {
1042
+ let spawnWorker = async (type, opts) => {
936
1043
  // Don't spawn workers during shutdown
937
1044
  if (isClosing) {
938
1045
  return;
@@ -966,6 +1073,7 @@ let spawnWorker = async type => {
966
1073
  let worker = new WorkerThread(pathlib.join(__dirname, 'workers', `${type.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}.js`), {
967
1074
  argv,
968
1075
  env: SHARE_ENV,
1076
+ workerData: opts && opts.workerData,
969
1077
  trackUnmanagedFds: true
970
1078
  });
971
1079
  metrics.threadStarts.inc();
@@ -1013,6 +1121,13 @@ let spawnWorker = async type => {
1013
1121
  onlineWorkers.delete(worker);
1014
1122
  metrics.threadStops.inc();
1015
1123
 
1124
+ // Fail any in-flight calls routed to this worker right away, so callers
1125
+ // get a fast retryable error instead of hanging until their own timeout.
1126
+ let rejectedCalls = rejectWorkerCalls(callQueue, worker, WORKER_DIED_RESP_ERR);
1127
+ if (rejectedCalls) {
1128
+ logger.info({ msg: 'Rejected in-flight calls for exited worker', type, threadId: worker.threadId, rejectedCalls });
1129
+ }
1130
+
1016
1131
  workers.get(type).delete(worker);
1017
1132
 
1018
1133
  // Update server state for proxy servers
@@ -1117,9 +1232,9 @@ let spawnWorker = async type => {
1117
1232
  logger.error({ msg: 'Worker unexpectedly exited', exitCode, type });
1118
1233
  }
1119
1234
 
1120
- // Respawn worker after delay
1235
+ // Respawn worker after delay, preserving any spawn options (e.g. API worker index)
1121
1236
  await new Promise(r => setTimeout(r, 1000));
1122
- await spawnWorker(type);
1237
+ await spawnWorker(type, opts);
1123
1238
  };
1124
1239
 
1125
1240
  // Handle worker exit
@@ -1325,10 +1440,8 @@ let spawnWorker = async type => {
1325
1440
  }
1326
1441
 
1327
1442
  case 'settings':
1328
- // Reload HTTP proxy agent in the main thread
1329
- if (message.data && ('httpProxyEnabled' in message.data || 'httpProxyUrl' in message.data)) {
1330
- reloadHttpProxyAgent().catch(err => logger.error({ msg: 'Failed to reload HTTP proxy agent', err }));
1331
- }
1443
+ // Reload the HTTP proxy agent in the main thread when proxy settings change
1444
+ maybeReloadHttpProxyAgent(message.data);
1332
1445
 
1333
1446
  // Forward settings changes to all IMAP workers
1334
1447
  availableIMAPWorkers.forEach(worker => {
@@ -1339,8 +1452,8 @@ let spawnWorker = async type => {
1339
1452
  }
1340
1453
  });
1341
1454
 
1342
- // Forward settings changes to webhooks, submit, and export workers
1343
- for (let type of ['webhooks', 'submit', 'export']) {
1455
+ // Forward settings changes to API, webhooks, submit, and export workers
1456
+ for (let type of ['api', 'webhooks', 'submit', 'export']) {
1344
1457
  let typeWorkers = workers.get(type);
1345
1458
  if (typeWorkers) {
1346
1459
  typeWorkers.forEach(worker => {
@@ -1499,7 +1612,8 @@ async function call(worker, message, transferList) {
1499
1612
  reject(err);
1500
1613
  }, ttl);
1501
1614
 
1502
- // Store callback info
1615
+ // Store callback info. `worker` lets us reject this entry immediately if
1616
+ // the target worker terminates, instead of waiting out the timeout.
1503
1617
  callQueue.set(mid, {
1504
1618
  resolve: result => {
1505
1619
  clearTimeout(timer);
@@ -1509,7 +1623,8 @@ async function call(worker, message, transferList) {
1509
1623
  clearTimeout(timer);
1510
1624
  reject(err);
1511
1625
  },
1512
- timer
1626
+ timer,
1627
+ worker
1513
1628
  });
1514
1629
 
1515
1630
  try {
@@ -1850,7 +1965,7 @@ let licenseCheckHandler = async opts => {
1850
1965
  default:
1851
1966
  if (config.workers && config.workers[type]) {
1852
1967
  for (let i = 0; i < config.workers[type]; i++) {
1853
- await spawnWorker(type);
1968
+ await spawnWorker(type, workerSpawnOpts(type, i));
1854
1969
  }
1855
1970
  }
1856
1971
  }
@@ -2588,6 +2703,16 @@ async function onCommand(worker, message) {
2588
2703
  return await getThreadsInfo();
2589
2704
  }
2590
2705
 
2706
+ case 'apiWorkerScaling': {
2707
+ // Reported on /admin/internals so operators can see when EENGINE_WORKERS_API > 1
2708
+ // was requested but only one worker started, and why (reason: 'platform' | 'port').
2709
+ return {
2710
+ requested: apiWorkersRequested,
2711
+ fallback: apiWorkersRequested > 1 && !apiWorkerReusePort,
2712
+ reason: apiWorkerReusePortFallbackReason
2713
+ };
2714
+ }
2715
+
2591
2716
  case 'worker-accounts': {
2592
2717
  // Get accounts assigned to a specific worker thread
2593
2718
  const { threadId, page = 1, pageSize = 20 } = message;
@@ -3192,8 +3317,81 @@ const startApplication = async () => {
3192
3317
 
3193
3318
  // -- START WORKER THREADS
3194
3319
 
3195
- // Start API server first for health checks
3196
- await spawnWorker('api');
3320
+ // Start API server first for health checks.
3321
+ // Optionally run several API workers that share the listen port via SO_REUSEPORT.
3322
+ apiWorkersRequested = config.workers.api;
3323
+ // Effective count we actually start; drops to 1 if SO_REUSEPORT is unavailable. Kept as a local
3324
+ // so we don't mutate the parsed config object that is read elsewhere.
3325
+ let effectiveApiWorkers = config.workers.api;
3326
+ if (config.workers.api > 1) {
3327
+ const probe = await probeReusePort(config.api.host, config.api.port);
3328
+ apiWorkerReusePort = probe.supported;
3329
+ if (!apiWorkerReusePort) {
3330
+ // Only a first-bind EADDRINUSE/EACCES (stage 'probe') means we genuinely could not test
3331
+ // the port - a transient conflict or permission issue, which the single API worker we
3332
+ // start next will surface. A failed second bind (stage 'reuse') instead means reusePort
3333
+ // is accepted but not honored on this platform/Node (e.g. Node < 23.1), which is a
3334
+ // capability limitation rather than a port conflict, so it falls through to the
3335
+ // "not available on this platform" message below.
3336
+ if (probe.stage === 'probe' && (probe.code === 'EADDRINUSE' || probe.code === 'EACCES')) {
3337
+ apiWorkerReusePortFallbackReason = 'port';
3338
+ logger.warn({
3339
+ msg: 'Could not probe SO_REUSEPORT because the API port is unavailable; starting a single API worker',
3340
+ requested: apiWorkersRequested,
3341
+ code: probe.code,
3342
+ host: config.api.host,
3343
+ port: config.api.port
3344
+ });
3345
+ } else {
3346
+ apiWorkerReusePortFallbackReason = 'platform';
3347
+ logger.warn({
3348
+ msg: 'Multiple API workers requested but SO_REUSEPORT is not available on this platform; starting a single API worker',
3349
+ requested: apiWorkersRequested,
3350
+ code: probe.code
3351
+ });
3352
+ }
3353
+ effectiveApiWorkers = 1;
3354
+ // The /admin/internals warning banner (via the apiWorkerScaling command) tells the
3355
+ // operator only one worker started; the thread-config popover keeps showing the
3356
+ // configured EENGINE_WORKERS_API value rather than being rewritten here.
3357
+ } else {
3358
+ logger.info({ msg: 'SO_REUSEPORT is available; starting multiple API workers', workers: effectiveApiWorkers });
3359
+ }
3360
+ }
3361
+
3362
+ let apiPromises = [];
3363
+ for (let i = 0; i < effectiveApiWorkers; i++) {
3364
+ apiPromises.push(spawnWorker('api', workerSpawnOpts('api', i)));
3365
+ }
3366
+
3367
+ // Tolerate partial API-worker startup: proceed as long as at least one worker reached 'ready'
3368
+ // (the spawnWorker exit handler respawns any that failed). Only abort startup if EVERY API
3369
+ // worker failed, preserving the original single-worker "no API worker => exit" behavior.
3370
+ let apiResults = await Promise.allSettled(apiPromises);
3371
+ let apiReady = apiResults.filter(result => result.status === 'fulfilled').length;
3372
+ let apiFailed = apiResults.length - apiReady;
3373
+ if (apiFailed) {
3374
+ logger.error({
3375
+ msg: 'Some API workers failed to start; they will be respawned',
3376
+ requested: apiWorkersRequested,
3377
+ attempted: apiResults.length,
3378
+ ready: apiReady,
3379
+ failed: apiFailed,
3380
+ errors: apiResults
3381
+ .filter(result => result.status === 'rejected')
3382
+ .map(result => ({
3383
+ message: result.reason && result.reason.message,
3384
+ exitCode: result.reason && result.reason.exitCode,
3385
+ threadId: result.reason && result.reason.threadId
3386
+ }))
3387
+ });
3388
+ }
3389
+ if (!apiReady) {
3390
+ // No API worker came up at all - fatal, same as the original single-worker behavior. This
3391
+ // rejects startApplication(), which logs fatal and exits the process.
3392
+ throw new Error('No API worker thread could be started');
3393
+ }
3394
+ logger.info({ msg: 'API workers started', requested: apiWorkersRequested, ready: apiReady });
3197
3395
 
3198
3396
  // Small delay to allow API to start
3199
3397
  await new Promise(r => setTimeout(r, 100));
@@ -1,6 +1,6 @@
1
1
  <!doctype html><html><head><meta charset="utf-8"><title>EmailEngine Licenses</title><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"></head><body>
2
2
  <div class="container-fluid">
3
- <h1>EmailEngine v2.68.0</h1><p>EmailEngine includes code from the following software packages:</p>
3
+ <h1>EmailEngine v2.68.1</h1><p>EmailEngine includes code from the following software packages:</p>
4
4
  <table class="table table-sm">
5
5
  <tr><thead class="thead-dark"><th>Package</th><th>Version</th><th>License</th><th>Publisher</th><th>Publisher's Email</th><th>Package URL</th></tr>
6
6
  <tbody>
@@ -146,7 +146,7 @@
146
146
  </tr>
147
147
  <tr>
148
148
  <td><a href="https://npmjs.com/package/@bull-board/api">@bull-board/api</a></td>
149
- <td>7.1.5</td>
149
+ <td>7.2.1</td>
150
150
  <td>MIT</td>
151
151
  <td>felixmosh</td>
152
152
  <td></td>
@@ -156,7 +156,7 @@
156
156
  </tr>
157
157
  <tr>
158
158
  <td><a href="https://npmjs.com/package/@bull-board/hapi">@bull-board/hapi</a></td>
159
- <td>7.1.5</td>
159
+ <td>7.2.1</td>
160
160
  <td>MIT</td>
161
161
  <td>felixmosh</td>
162
162
  <td></td>
@@ -166,7 +166,7 @@
166
166
  </tr>
167
167
  <tr>
168
168
  <td><a href="https://npmjs.com/package/@bull-board/ui">@bull-board/ui</a></td>
169
- <td>7.1.5</td>
169
+ <td>7.2.1</td>
170
170
  <td>MIT</td>
171
171
  <td>felixmosh</td>
172
172
  <td></td>
@@ -216,7 +216,7 @@
216
216
  </tr>
217
217
  <tr>
218
218
  <td><a href="https://npmjs.com/package/@csstools/css-syntax-patches-for-csstree">@csstools/css-syntax-patches-for-csstree</a></td>
219
- <td>1.1.4</td>
219
+ <td>1.1.5</td>
220
220
  <td>MIT-0</td>
221
221
  <td></td>
222
222
  <td></td>
@@ -1806,7 +1806,7 @@
1806
1806
  </tr>
1807
1807
  <tr>
1808
1808
  <td><a href="https://npmjs.com/package/bullmq">bullmq</a></td>
1809
- <td>5.77.7</td>
1809
+ <td>5.78.0</td>
1810
1810
  <td>MIT</td>
1811
1811
  <td>Taskforce.sh Inc.</td>
1812
1812
  <td></td>
@@ -2466,7 +2466,7 @@
2466
2466
  </tr>
2467
2467
  <tr>
2468
2468
  <td><a href="https://npmjs.com/package/ejs">ejs</a></td>
2469
- <td>5.0.2</td>
2469
+ <td>6.0.1</td>
2470
2470
  <td>Apache-2.0</td>
2471
2471
  <td>Matthew Eernisse</td>
2472
2472
  <td>matthew.eernisse@gmail.com</td>
@@ -3656,7 +3656,7 @@
3656
3656
  </tr>
3657
3657
  <tr>
3658
3658
  <td><a href="https://npmjs.com/package/imapflow">imapflow</a></td>
3659
- <td>1.3.5</td>
3659
+ <td>1.4.0</td>
3660
3660
  <td>MIT</td>
3661
3661
  <td>Postal Systems O&#xDC;</td>
3662
3662
  <td></td>
@@ -3736,7 +3736,7 @@
3736
3736
  </tr>
3737
3737
  <tr>
3738
3738
  <td><a href="https://npmjs.com/package/ioredis">ioredis</a></td>
3739
- <td>5.11.0</td>
3739
+ <td>5.11.1</td>
3740
3740
  <td>MIT</td>
3741
3741
  <td>Zihua Li</td>
3742
3742
  <td>i@zihua.li</td>
@@ -5146,7 +5146,7 @@
5146
5146
  </tr>
5147
5147
  <tr>
5148
5148
  <td><a href="https://npmjs.com/package/prettier">prettier</a></td>
5149
- <td>3.8.3</td>
5149
+ <td>3.8.4</td>
5150
5150
  <td>MIT</td>
5151
5151
  <td>James Long</td>
5152
5152
  <td></td>
@@ -5696,7 +5696,7 @@
5696
5696
  </tr>
5697
5697
  <tr>
5698
5698
  <td><a href="https://npmjs.com/package/side-channel">side-channel</a></td>
5699
- <td>1.1.0</td>
5699
+ <td>1.1.1</td>
5700
5700
  <td>MIT</td>
5701
5701
  <td>Jordan Harband</td>
5702
5702
  <td>ljharb@gmail.com</td>
@@ -6086,7 +6086,7 @@
6086
6086
  </tr>
6087
6087
  <tr>
6088
6088
  <td><a href="https://npmjs.com/package/thread-stream">thread-stream</a></td>
6089
- <td>3.1.0</td>
6089
+ <td>3.2.0</td>
6090
6090
  <td>MIT</td>
6091
6091
  <td>Matteo Collina</td>
6092
6092
  <td>hello@matteocollina.com</td>