Compare commits

...

5 commits

Author SHA1 Message Date
ClaraCrazy
ed2cc90852
[Feat]: Do NOT wipe locked inboxes 2025-12-26 12:38:54 +01:00
ClaraCrazy
fc1ed35856
[Chore]: Update logging & bump version 2025-12-26 12:29:43 +01:00
ClaraCrazy
994ccb2dc3
[Style]: Update explainer for lock time 2025-12-26 12:07:27 +01:00
ClaraCrazy
a571381462
[Feat]: Add Inbox Locking
Add support for locking specific inboxes with a password for X time, configurable via .env vars. This allows for users to bridge the gap between public free tempmail services and private personal mail services.
Cheers!
2025-12-26 09:10:25 +01:00
ClaraCrazy
83a4fac4ab
[Feat]: Show total and historical email count in UI
Enhances user interface by displaying both the current number of emails and the largest UID seen, offering better visibility into historical mailbox activity. Updates backend logic and view templates to support this change, and improves maintainability by centralizing count formatting.
2025-12-26 05:53:06 +01:00
21 changed files with 1290 additions and 51 deletions

View file

@ -31,3 +31,9 @@ HTTP_DISPLAY_SORT=2 # Domain display
# 2 = alphabetical + first item shuffled, # 2 = alphabetical + first item shuffled,
# 3 = shuffle all # 3 = shuffle all
HTTP_HIDE_OTHER=false # true = only show first domain, false = show all HTTP_HIDE_OTHER=false # true = only show first domain, false = show all
# --- INBOX LOCKING (optional) ---
LOCK_ENABLED=false # Enable inbox locking with passwords
LOCK_SESSION_SECRET="change-this-secret-in-production" # Secret for session encryption
LOCK_DATABASE_PATH="./db/locked-inboxes.db" # Path to lock database
LOCK_RELEASE_HOURS=720 # Auto-release locked inboxes after X hours of inactivity (default 30 days)

View file

@ -71,7 +71,7 @@ User=user
Group=user Group=user
WorkingDirectory=/opt/48hr-email WorkingDirectory=/opt/48hr-email
ExecStart=npm run prod ExecStart=npm run start
Restart=on-failure Restart=on-failure
TimeoutStartSec=0 TimeoutStartSec=0
@ -100,7 +100,6 @@ WantedBy=multi-user.target
### TODO (PRs welcome): ### TODO (PRs welcome):
- Add user registration: - Add user registration:
- Optional "premium" domains that arent visible to the public to prevent them from being scraped and flagged - Optional "premium" domains that arent visible to the public to prevent them from being scraped and flagged
- Allow people to set a password for their email (releases X time after last login)
- Allow people to set up forwarding - Allow people to set up forwarding
#### Unsure: #### Unsure:

21
app.js
View file

@ -13,6 +13,7 @@ const ClientNotification = require('./infrastructure/web/client-notification')
const ImapService = require('./application/imap-service') const ImapService = require('./application/imap-service')
const MailProcessingService = require('./application/mail-processing-service') const MailProcessingService = require('./application/mail-processing-service')
const MailRepository = require('./domain/mail-repository') const MailRepository = require('./domain/mail-repository')
const InboxLock = require('./domain/inbox-lock')
const clientNotification = new ClientNotification() const clientNotification = new ClientNotification()
debug('Client notification service initialized') debug('Client notification service initialized')
@ -55,6 +56,26 @@ imapService.on(ImapService.EVENT_ERROR, error => {
}) })
app.set('mailProcessingService', mailProcessingService) app.set('mailProcessingService', mailProcessingService)
app.set('config', config)
// Initialize inbox locking if enabled
if (config.lock.enabled) {
const inboxLock = new InboxLock(config.lock.dbPath)
app.set('inboxLock', inboxLock)
console.log(`Inbox locking enabled (auto-release after ${config.lock.releaseHours} hours)`)
// Check for inactive locked inboxes
setInterval(() => {
const inactive = inboxLock.getInactive(config.lock.releaseHours)
if (inactive.length > 0) {
console.log(`Releasing ${inactive.length} inactive locked inbox(es)`)
inactive.forEach(address => inboxLock.release(address))
}
}, config.imap.refreshIntervalSeconds * 1000)
}
app.locals.imapService = imapService
app.locals.mailProcessingService = mailProcessingService
debug('Starting IMAP connection and message loading') debug('Starting IMAP connection and message loading')
imapService.connectAndLoadMessages().catch(error => { imapService.connectAndLoadMessages().catch(error => {

View file

@ -60,6 +60,13 @@ const config = {
branding: parseValue(process.env.HTTP_BRANDING), branding: parseValue(process.env.HTTP_BRANDING),
displaySort: Number(process.env.HTTP_DISPLAY_SORT), displaySort: Number(process.env.HTTP_DISPLAY_SORT),
hideOther: parseBool(process.env.HTTP_HIDE_OTHER) hideOther: parseBool(process.env.HTTP_HIDE_OTHER)
},
lock: {
enabled: parseBool(process.env.LOCK_ENABLED) || false,
sessionSecret: parseValue(process.env.LOCK_SESSION_SECRET) || 'change-me-in-production',
dbPath: parseValue(process.env.LOCK_DATABASE_PATH) || './db/locked-inboxes.db',
releaseHours: Number(process.env.LOCK_RELEASE_HOURS) || 720 // 30 days default
} }
}; };

View file

@ -150,6 +150,17 @@ class Helper {
return result return result
} }
} }
async getLargestUid(imapService) {
return await imapService.getLargestUid();
}
countElementBuilder(count = 0, largestUid = 0) {
const handling = `<label title="Historically managed ${largestUid} email${largestUid === 1 ? '' : 's'}">
<h4 style="display: inline;"><u><i>${count}</i></u> mail${count === 1 ? '' : 's'}</h4>
</label>`
return handling
}
} }
module.exports = Helper module.exports = Helper

View file

@ -253,19 +253,39 @@ class ImapService extends EventEmitter {
const exampleUids = this.config.email.examples.uids.map(x => parseInt(x)); const exampleUids = this.config.email.examples.uids.map(x => parseInt(x));
const headers = await this._getMailHeaders(uids); const headers = await this._getMailHeaders(uids);
// Filter out mails that are too new or whitelisted // Get locked inboxes if available
let lockedAddresses = [];
if (typeof this.config.lock !== 'undefined' && this.config.lock.enabled && this.config.lock.dbPath) {
try {
// Try to get the app instance and inboxLock
const app = require('../app');
const inboxLock = app.get('inboxLock');
if (inboxLock && typeof inboxLock.getAllLocked === 'function') {
lockedAddresses = inboxLock.getAllLocked().map(addr => addr.toLowerCase());
debug(`Locked inboxes (excluded from purge): ${lockedAddresses.join(', ')}`);
}
} catch (err) {
debug('Could not get locked inboxes for purge:', err.message);
}
}
// Filter out mails that are too new, whitelisted, or belong to locked inboxes
const toDelete = headers const toDelete = headers
.filter(mail => { .filter(mail => {
const date = mail.attributes.date; const date = mail.attributes.date;
const uid = parseInt(mail.attributes.uid); const uid = parseInt(mail.attributes.uid);
const toAddresses = Array.isArray(mail.parts[0].body.to) ?
mail.parts[0].body.to.map(a => a.toLowerCase()) :
[String(mail.parts[0].body.to).toLowerCase()];
if (exampleUids.includes(uid)) return false; if (exampleUids.includes(uid)) return false;
if (toAddresses.some(addr => lockedAddresses.includes(addr))) return false;
return date <= deleteOlderThan; return date <= deleteOlderThan;
}) })
.map(mail => parseInt(mail.attributes.uid)); .map(mail => parseInt(mail.attributes.uid));
if (toDelete.length === 0) { if (toDelete.length === 0) {
debug('No mails to delete.'); debug('No mails to delete. (after locked inbox exclusion)');
return; return;
} }
@ -421,8 +441,18 @@ class ImapService extends EventEmitter {
] ]
return this.connection.search(searchCriteria, fetchOptions) return this.connection.search(searchCriteria, fetchOptions)
} }
/*
* Get the largest UID from all messages in the mailbox.
*/
async getLargestUid() {
const uids = await this._getAllUids();
return uids.length > 0 ? Math.max(...uids) : null;
}
} }
// Consumers should use these constants: // Consumers should use these constants:
ImapService.EVENT_NEW_MAIL = 'mail' ImapService.EVENT_NEW_MAIL = 'mail'
ImapService.EVENT_DELETED_MAIL = 'mailDeleted' ImapService.EVENT_DELETED_MAIL = 'mailDeleted'

95
domain/inbox-lock.js Normal file
View file

@ -0,0 +1,95 @@
const Database = require('better-sqlite3')
const bcrypt = require('bcrypt')
const path = require('path')
class InboxLock {
constructor(dbPath = './db/locked-inboxes.db') {
// Ensure data directory exists
const fs = require('fs')
const dir = path.dirname(dbPath)
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
this.db = new Database(dbPath)
this.db.pragma('journal_mode = WAL')
this._initTable()
}
_initTable() {
this.db.exec(`
CREATE TABLE IF NOT EXISTS locked_inboxes (
address TEXT PRIMARY KEY,
password_hash TEXT NOT NULL,
locked_at INTEGER NOT NULL,
last_access INTEGER NOT NULL
)
`)
}
async lock(address, password) {
const passwordHash = await bcrypt.hash(password, 10)
const now = Date.now()
const stmt = this.db.prepare(`
INSERT INTO locked_inboxes (address, password_hash, locked_at, last_access)
VALUES (?, ?, ?, ?)
`)
try {
stmt.run(address.toLowerCase(), passwordHash, now, now)
return true
} catch (error) {
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
throw new Error('This inbox is already locked')
}
throw error
}
}
async unlock(address, password) {
const stmt = this.db.prepare('SELECT * FROM locked_inboxes WHERE address = ?')
const inbox = stmt.get(address.toLowerCase())
if (!inbox) {
return null
}
const valid = await bcrypt.compare(password, inbox.password_hash)
if (!valid) {
return null
}
// Update last access
this.updateAccess(address)
return inbox
}
isLocked(address) {
const stmt = this.db.prepare('SELECT address FROM locked_inboxes WHERE address = ?')
return stmt.get(address.toLowerCase()) !== undefined
}
updateAccess(address) {
const stmt = this.db.prepare('UPDATE locked_inboxes SET last_access = ? WHERE address = ?')
stmt.run(Date.now(), address.toLowerCase())
}
getInactive(hoursThreshold) {
const cutoff = Date.now() - (hoursThreshold * 60 * 60 * 1000)
const stmt = this.db.prepare('SELECT address FROM locked_inboxes WHERE last_access < ?')
return stmt.all(cutoff).map(row => row.address)
}
release(address) {
const stmt = this.db.prepare('DELETE FROM locked_inboxes WHERE address = ?')
stmt.run(address.toLowerCase())
}
getAllLocked() {
const stmt = this.db.prepare('SELECT address FROM locked_inboxes')
return stmt.all().map(row => row.address)
}
}
module.exports = InboxLock

View file

@ -0,0 +1,38 @@
function checkLockAccess(req, res, next) {
const inboxLock = req.app.get('inboxLock')
const address = req.params.address
if (!address || !inboxLock) {
return next()
}
const isLocked = inboxLock.isLocked(address)
const hasAccess = req.session && req.session.lockedInbox === address.toLowerCase()
// Block access to locked inbox without proper authentication
if (isLocked && !hasAccess) {
const count = req.app.get('mailProcessingService').getCount()
const unlockError = req.session ? req.session.unlockError : undefined
if (req.session) delete req.session.unlockError
return res.render('error', {
purgeTime: require('../../../application/helper').prototype.purgeTimeElemetBuilder(),
address: address,
count: count,
message: 'This inbox is locked. Please unlock it to access.',
branding: req.app.get('config').http.branding,
showUnlockButton: true,
unlockError: unlockError,
redirectTo: req.originalUrl
})
}
// Update last access if they have access
if (isLocked && hasAccess) {
inboxLock.updateAccess(address)
}
next()
}
module.exports = { checkLockAccess }

View file

@ -0,0 +1,136 @@
document.addEventListener('DOMContentLoaded', function() {
// Lock modal elements
const lockModal = document.getElementById('lockModal');
const lockBtn = document.getElementById('lockBtn');
const closeLock = document.getElementById('closeLock');
const lockForm = document.querySelector('#lockModal form');
// Unlock modal elements
const unlockModal = document.getElementById('unlockModal');
const unlockBtn = document.getElementById('unlockBtn');
const closeUnlock = document.getElementById('closeUnlock');
// Remove lock modal elements
const removeLockModal = document.getElementById('removeLockModal');
const removeLockBtn = document.getElementById('removeLockBtn');
const closeRemoveLock = document.getElementById('closeRemoveLock');
const cancelRemoveLock = document.getElementById('cancelRemoveLock');
// Open/close helpers
const openModal = function(modal) { if (modal) modal.style.display = 'block'; };
const closeModal = function(modal) { if (modal) modal.style.display = 'none'; };
// Protect / Lock modal logic
if (lockBtn) {
lockBtn.onclick = function(e) {
e.preventDefault();
openModal(lockModal);
};
}
if (closeLock) {
closeLock.onclick = function() {
closeModal(lockModal);
};
}
if (lockForm) {
lockForm.addEventListener('submit', function(e) {
const pwElement = document.getElementById('lockPassword');
const cfElement = document.getElementById('lockConfirm');
const pw = pwElement ? pwElement.value : '';
const cf = cfElement ? cfElement.value : '';
const err = document.getElementById('lockErrorInline');
const serverErr = document.getElementById('lockServerError');
if (serverErr) serverErr.style.display = 'none';
if (pw !== cf) {
e.preventDefault();
if (err) {
err.textContent = 'Passwords do not match.';
err.style.display = 'block';
}
return;
}
if (pw.length < 8) {
e.preventDefault();
if (err) {
err.textContent = 'Password must be at least 8 characters.';
err.style.display = 'block';
}
return;
}
if (err) err.style.display = 'none';
});
}
// Auto-open lock modal if server provided an error (via data attribute)
if (lockModal) {
const lockErrorValue = (lockModal.dataset.lockError || '').trim();
if (lockErrorValue) {
openModal(lockModal);
const err = document.getElementById('lockErrorInline');
const serverErr = document.getElementById('lockServerError');
if (serverErr) serverErr.style.display = 'none';
if (err) {
if (lockErrorValue === 'locking_disabled_for_example') {
err.textContent = 'Locking is disabled for the example inbox.';
} else if (lockErrorValue === 'invalid') {
err.textContent = 'Please provide a valid password.';
} else if (lockErrorValue === 'server_error') {
err.textContent = 'A server error occurred. Please try again.';
} else if (lockErrorValue === 'remove_failed') {
err.textContent = 'Failed to remove lock. Please try again.';
} else {
err.textContent = 'An error occurred. Please try again.';
}
err.style.display = 'block';
}
}
}
// Unlock modal logic
if (unlockBtn) {
unlockBtn.onclick = function(e) {
e.preventDefault();
openModal(unlockModal);
};
}
if (closeUnlock) {
closeUnlock.onclick = function() {
closeModal(unlockModal);
};
}
if (unlockModal) {
const unlockErrorValue = (unlockModal.dataset.unlockError || '').trim();
if (unlockErrorValue) {
openModal(unlockModal);
}
}
// Remove lock modal logic
if (removeLockBtn) {
removeLockBtn.onclick = function(e) {
e.preventDefault();
openModal(removeLockModal);
};
}
if (closeRemoveLock) {
closeRemoveLock.onclick = function() {
closeModal(removeLockModal);
};
}
if (cancelRemoveLock) {
cancelRemoveLock.onclick = function() {
closeModal(removeLockModal);
};
}
// Close modals when clicking outside
window.onclick = function(e) {
if (e.target === lockModal) closeModal(lockModal);
if (e.target === unlockModal) closeModal(unlockModal);
if (e.target === removeLockModal) closeModal(removeLockModal);
};
});

View file

@ -542,3 +542,113 @@ label {
transform: translateX(5px); transform: translateX(5px);
text-decoration: none; text-decoration: none;
} }
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.8);
}
.modal-content {
background-color: #131516;
margin: 10% auto;
padding: 2rem;
border: 1px solid #9b4dca;
border-radius: 0.4rem;
width: 90%;
max-width: 400px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.modal-content h3 {
margin-top: 0;
color: #cccccc;
}
.modal-description {
color: #999;
margin-bottom: 1.5rem;
font-size: 1.4rem;
}
.close {
float: right;
font-size: 2.8rem;
font-weight: bold;
cursor: pointer;
color: #cccccc;
line-height: 20px;
transition: color 0.3s;
}
.close:hover,
.close:focus {
color: #9b4dca;
}
.floating-label {
position: relative;
top: 12px;
left: 12px;
width: fit-content;
line-height: 1;
padding: 0 6px;
font-size: 1.4rem;
background-color: #131516;
z-index: 999;
color: #cccccc;
}
.modal-input {
border-radius: 0.4rem;
color: #cccccc;
font-size: 1.6rem;
height: 4.2rem;
padding: 0 1.4rem;
margin-bottom: 1rem;
background-color: transparent;
border: 1px solid #444;
width: 100%;
}
.modal-input:focus {
border-color: #9b4dca;
outline: none;
}
.modal-button {
width: 100%;
margin-top: 1rem;
background-color: #9b4dca;
}
/* Lock Error Messages */
.unlock-error {
color: #ff8c00;
margin-bottom: 1rem;
padding: 0.8rem;
background: rgba(255, 140, 0, 0.1);
border-left: 3px solid #ff8c00;
}
/* Remove Lock Button Styles */
.modal-button-danger {
background-color: #e74c3c;
}
.modal-button-cancel {
background-color: #555;
}

View file

@ -16,6 +16,8 @@ router.get('/:address/:errorCode', async(req, res, next) => {
} }
debug(`Error page requested: ${req.params.errorCode} for ${req.params.address}`) debug(`Error page requested: ${req.params.errorCode} for ${req.params.address}`)
const count = await mailProcessingService.getCount() const count = await mailProcessingService.getCount()
const largestUid = await req.app.locals.imapService.getLargestUid()
const totalcount = helper.countElementBuilder(count, largestUid)
const errorCode = parseInt(req.params.errorCode) || 404 const errorCode = parseInt(req.params.errorCode) || 404
const message = req.query.message || (req.session && req.session.errorMessage) || 'An error occurred' const message = req.query.message || (req.session && req.session.errorMessage) || 'An error occurred'
@ -26,6 +28,7 @@ router.get('/:address/:errorCode', async(req, res, next) => {
purgeTime: purgeTime, purgeTime: purgeTime,
address: req.params.address, address: req.params.address,
count: count, count: count,
totalcount: totalcount,
message: message, message: message,
status: errorCode, status: errorCode,
branding: config.http.branding branding: config.http.branding

View file

@ -6,9 +6,11 @@ const debug = require('debug')('48hr-email:routes')
const config = require('../../../application/config') const config = require('../../../application/config')
const Helper = require('../../../application/helper') const Helper = require('../../../application/helper')
const helper = new(Helper) const helper = new(Helper)
const { checkLockAccess } = require('../middleware/lock')
const purgeTime = helper.purgeTimeElemetBuilder() const purgeTime = helper.purgeTimeElemetBuilder()
const sanitizeAddress = param('address').customSanitizer( const sanitizeAddress = param('address').customSanitizer(
(value, { req }) => { (value, { req }) => {
return req.params.address return req.params.address
@ -17,22 +19,44 @@ const sanitizeAddress = param('address').customSanitizer(
} }
) )
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, next) => { router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, checkLockAccess, async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
if (!mailProcessingService) { if (!mailProcessingService) {
throw new Error('Mail processing service not available') throw new Error('Mail processing service not available')
} }
debug(`Inbox request for ${req.params.address}`) debug(`Inbox request for ${req.params.address}`)
const inboxLock = req.app.get('inboxLock')
const count = await mailProcessingService.getCount() const count = await mailProcessingService.getCount()
const largestUid = await req.app.locals.imapService.getLargestUid()
const totalcount = helper.countElementBuilder(count, largestUid)
debug(`Rendering inbox with ${count} total mails`) debug(`Rendering inbox with ${count} total mails`)
const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
const hasAccess = req.session && req.session.lockedInbox === req.params.address
// Pull any lock error from session and clear it after reading
const lockError = req.session ? req.session.lockError : undefined
const unlockErrorSession = req.session ? req.session.unlockError : undefined
if (req.session) {
delete req.session.lockError
delete req.session.unlockError
}
res.render('inbox', { res.render('inbox', {
title: `${config.http.branding[0]} | ` + req.params.address, title: `${config.http.branding[0]} | ` + req.params.address,
purgeTime: purgeTime, purgeTime: purgeTime,
address: req.params.address, address: req.params.address,
count: count, count: count,
totalcount: totalcount,
mailSummaries: mailProcessingService.getMailSummaries(req.params.address), mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
branding: config.http.branding, branding: config.http.branding,
lockEnabled: config.lock.enabled,
isLocked: isLocked,
hasAccess: hasAccess,
unlockError: unlockErrorSession,
locktimer: config.lock.releaseHours,
error: lockError,
redirectTo: req.originalUrl
}) })
} catch (error) { } catch (error) {
debug(`Error loading inbox for ${req.params.address}:`, error.message) debug(`Error loading inbox for ${req.params.address}:`, error.message)
@ -44,11 +68,14 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, next) =
router.get( router.get(
'^/:address/:uid([0-9]+)', '^/:address/:uid([0-9]+)',
sanitizeAddress, sanitizeAddress,
checkLockAccess,
async(req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
debug(`Viewing email ${req.params.uid} for ${req.params.address}`) debug(`Viewing email ${req.params.uid} for ${req.params.address}`)
const count = await mailProcessingService.getCount() const count = await mailProcessingService.getCount()
const largestUid = await req.app.locals.imapService.getLargestUid()
const totalcount = helper.countElementBuilder(count, largestUid)
const mail = await mailProcessingService.getOneFullMail( const mail = await mailProcessingService.getOneFullMail(
req.params.address, req.params.address,
req.params.uid req.params.uid
@ -61,15 +88,24 @@ router.get(
// Emails are immutable, cache if found // Emails are immutable, cache if found
res.set('Cache-Control', 'private, max-age=600') res.set('Cache-Control', 'private, max-age=600')
const inboxLock = req.app.get('inboxLock')
const isLocked = inboxLock && inboxLock.isLocked(req.params.address)
const hasAccess = req.session && req.session.lockedInbox === req.params.address
debug(`Rendering email view for UID ${req.params.uid}`) debug(`Rendering email view for UID ${req.params.uid}`)
res.render('mail', { res.render('mail', {
title: mail.subject + " | " + req.params.address, title: mail.subject + " | " + req.params.address,
purgeTime: purgeTime, purgeTime: purgeTime,
address: req.params.address, address: req.params.address,
count: count, count: count,
totalcount: totalcount,
mail, mail,
uid: req.params.uid, uid: req.params.uid,
branding: config.http.branding, branding: config.http.branding,
lockEnabled: config.lock.enabled,
isLocked: isLocked,
hasAccess: hasAccess
}) })
} else { } else {
debug(`Email ${req.params.uid} not found for ${req.params.address}`) debug(`Email ${req.params.uid} not found for ${req.params.address}`)
@ -88,6 +124,7 @@ router.get(
router.get( router.get(
'^/:address/delete-all', '^/:address/delete-all',
sanitizeAddress, sanitizeAddress,
checkLockAccess,
async(req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
@ -111,6 +148,7 @@ router.get(
router.get( router.get(
'^/:address/:uid/delete', '^/:address/:uid/delete',
sanitizeAddress, sanitizeAddress,
checkLockAccess,
async(req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
@ -129,6 +167,7 @@ router.get(
router.get( router.get(
'^/:address/:uid/:checksum([a-f0-9]+)', '^/:address/:uid/:checksum([a-f0-9]+)',
sanitizeAddress, sanitizeAddress,
checkLockAccess,
async(req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
@ -188,12 +227,15 @@ router.get(
router.get( router.get(
'^/:address/:uid/raw', '^/:address/:uid/raw',
sanitizeAddress, sanitizeAddress,
checkLockAccess,
async(req, res, next) => { async(req, res, next) => {
try { try {
const mailProcessingService = req.app.get('mailProcessingService') const mailProcessingService = req.app.get('mailProcessingService')
debug(`Fetching raw email ${req.params.uid} for ${req.params.address}`) debug(`Fetching raw email ${req.params.uid} for ${req.params.address}`)
const uid = parseInt(req.params.uid, 10) const uid = parseInt(req.params.uid, 10)
const count = await mailProcessingService.getCount() const count = await mailProcessingService.getCount()
const largestUid = await req.app.locals.imapService.getLargestUid()
const totalcount = helper.countElementBuilder(count, largestUid)
// Validate UID is a valid integer // Validate UID is a valid integer
if (isNaN(uid) || uid <= 0) { if (isNaN(uid) || uid <= 0) {
@ -214,7 +256,8 @@ router.get(
debug(`Rendering raw email view for UID ${req.params.uid}`) debug(`Rendering raw email view for UID ${req.params.uid}`)
res.render('raw', { res.render('raw', {
title: req.params.uid + " | raw | " + req.params.address, title: req.params.uid + " | raw | " + req.params.address,
mail mail,
totalcount: totalcount
}) })
} else { } else {
debug(`Raw email ${uid} not found for ${req.params.address}`) debug(`Raw email ${uid} not found for ${req.params.address}`)

View file

@ -0,0 +1,134 @@
const express = require('express')
const router = express.Router()
const debug = require('debug')('48hr-email:lock')
router.post('/lock', async(req, res) => {
const { address, password } = req.body
debug(`Lock attempt for inbox: ${address}`);
if (!address || !password || password.length < 8) {
debug(`Lock error for ${address}: invalid input`);
if (req.session) req.session.lockError = 'invalid'
return res.redirect(`/inbox/${address}`)
}
try {
const inboxLock = req.app.get('inboxLock')
const mailProcessingService = req.app.get('mailProcessingService')
const config = req.app.get('config')
// Prevent locking the example inbox; allow UI but block DB insert
if (config && config.email && config.email.examples && config.email.examples.account && address.toLowerCase() === config.email.examples.account.toLowerCase()) {
debug(`Lock error for ${address}: locking disabled for example inbox`);
if (req.session) req.session.lockError = 'locking_disabled_for_example'
return res.redirect(`/inbox/${address}`)
}
await inboxLock.lock(address, password)
debug(`Inbox locked: ${address}`);
// Clear cache for this inbox
if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) {
debug(`Clearing lock cache for: ${address}`);
mailProcessingService.cachedFetchFullMail.clear()
}
req.session.lockedInbox = address
res.redirect(`/inbox/${address}`)
} catch (error) {
debug(`Lock error for ${address}: ${error.message}`);
console.error('Lock error:', error)
if (req.session) req.session.lockError = 'server_error'
res.redirect(`/inbox/${address}`)
}
})
router.post('/unlock', async(req, res) => {
const { address, password, redirectTo } = req.body
const destination = redirectTo && redirectTo.startsWith('/') ? redirectTo : `/inbox/${address}`
debug(`Unlock attempt for inbox: ${address}`);
if (!address || !password) {
debug(`Unlock error for ${address}: missing fields`);
if (req.session) req.session.unlockError = 'missing_fields'
return res.redirect(destination)
}
try {
const inboxLock = req.app.get('inboxLock')
const inbox = await inboxLock.unlock(address, password)
if (!inbox) {
debug(`Unlock error for ${address}: invalid password`);
if (req.session) req.session.unlockError = 'invalid_password'
return res.redirect(destination)
}
debug(`Inbox unlocked: ${address}`);
req.session.lockedInbox = address
res.redirect(destination)
} catch (error) {
debug(`Unlock error for ${address}: ${error.message}`);
console.error('Unlock error:', error)
if (req.session) req.session.unlockError = 'server_error'
res.redirect(destination)
}
})
router.get('/logout', (req, res) => {
const mailProcessingService = req.app.get('mailProcessingService')
// Clear cache before logout
if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) {
debug('Clearing lock cache for logout');
mailProcessingService.cachedFetchFullMail.clear()
}
debug('Lock session destroyed (logout)');
req.session.destroy()
res.redirect('/')
})
router.post('/remove', async(req, res) => {
const { address } = req.body
debug(`Remove lock attempt for inbox: ${address}`);
if (!address) {
debug('Remove lock error: missing address');
return res.redirect('/')
}
// Check if user has access to this locked inbox
const hasAccess = req.session && req.session.lockedInbox === address.toLowerCase()
debug(`Lock middleware: ${address} - hasAccess: ${hasAccess}`);
if (!hasAccess) {
debug(`Remove lock error: no access for ${address}`);
return res.redirect(`/inbox/${address}`)
}
try {
const inboxLock = req.app.get('inboxLock')
const mailProcessingService = req.app.get('mailProcessingService')
await inboxLock.release(address)
debug(`Lock removed for inbox: ${address}`);
// Clear cache when removing lock
if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) {
debug(`Clearing lock cache for: ${address}`);
mailProcessingService.cachedFetchFullMail.clear()
}
debug('Lock session destroyed (remove)');
req.session.destroy()
res.redirect(`/inbox/${address}`)
} catch (error) {
debug(`Remove lock error for ${address}: ${error.message}`);
console.error('Remove lock error:', error)
if (req.session) req.session.lockError = 'remove_failed'
res.redirect(`/inbox/${address}`)
}
})
module.exports = router

View file

@ -18,6 +18,8 @@ router.get('/', async(req, res, next) => {
} }
debug('Login page requested') debug('Login page requested')
const count = await mailProcessingService.getCount() const count = await mailProcessingService.getCount()
const largestUid = await req.app.locals.imapService.getLargestUid()
const totalcount = helper.countElementBuilder(count, largestUid)
debug(`Rendering login page with ${count} total mails`) debug(`Rendering login page with ${count} total mails`)
res.render('login', { res.render('login', {
title: `${config.http.branding[0]} | Your temporary Inbox`, title: `${config.http.branding[0]} | Your temporary Inbox`,
@ -25,6 +27,7 @@ router.get('/', async(req, res, next) => {
purgeTime: purgeTime, purgeTime: purgeTime,
domains: helper.getDomains(), domains: helper.getDomains(),
count: count, count: count,
totalcount: totalcount,
branding: config.http.branding, branding: config.http.branding,
example: config.email.examples.account, example: config.email.examples.account,
}) })

View file

@ -2,8 +2,10 @@
{% block header %} {% block header %}
<div class="action-links"> <div class="action-links">
<a href="/inbox/{{ address }}">← Return to inbox</a> {% if showUnlockButton %}
<a href="/logout">Logout</a> <a href="#" id="unlockBtn">Unlock</a>
{% endif %}
<a href="/">Logout</a>
</div> </div>
{% endblock %} {% endblock %}
@ -11,4 +13,48 @@
<h1>{{message}}</h1> <h1>{{message}}</h1>
<h2>{{error.status}}</h2> <h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre> <pre>{{error.stack}}</pre>
{% if showUnlockButton %}
<div id="unlockModal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" id="closeUnlock">&times;</span>
<h3>Unlock Inbox</h3>
<p class="modal-description">Enter password to access this locked inbox.</p>
{% if unlockError %}
<p class="unlock-error">
{% if unlockError == 'invalid_password' %}
Invalid password. Please try again.
{% elseif unlockError == 'missing_fields' %}
Please provide a password.
{% else %}
An error occurred. Please try again.
{% endif %}
</p>
{% endif %}
<form method="POST" action="/lock/unlock">
<input type="hidden" name="address" value="{{ address }}">
<input type="hidden" name="redirectTo" value="{{ redirectTo|default(address) }}">
<fieldset>
<label for="unlockPassword" class="floating-label">Password</label>
<input type="password" id="unlockPassword" name="password" required class="modal-input">
<button type="submit" class="button-primary modal-button">Unlock</button>
</fieldset>
</form>
</div>
</div>
<script>
const modal = document.getElementById('unlockModal');
const btn = document.getElementById('unlockBtn');
const close = document.getElementById('closeUnlock');
if (btn) btn.onclick = (e) => { e.preventDefault(); modal.style.display = 'block'; };
if (close) close.onclick = () => modal.style.display = 'none';
window.onclick = (e) => { if (e.target == modal) modal.style.display = 'none'; };
// Auto-open modal if there's an unlock error
if ('{{ unlockError|default("") }}') {
modal.style.display = 'block';
}
</script>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -2,14 +2,27 @@
{% block header %} {% block header %}
<div class="action-links"> <div class="action-links">
{% if lockEnabled %}
{% if isLocked and hasAccess %}
<a href="#" id="removeLockBtn">Remove Lock</a>
{% elseif isLocked %}
<a href="#" id="unlockBtn">Unlock</a>
{% else %}
<a href="#" id="lockBtn">Protect Inbox</a>
{% endif %}
{% endif %}
<a href="/inbox/{{ address }}/delete-all">Wipe Inbox</a> <a href="/inbox/{{ address }}/delete-all">Wipe Inbox</a>
{% if lockEnabled and hasAccess %}
<a href="/lock/logout">Logout</a>
{% else %}
<a href="/logout">Logout</a> <a href="/logout">Logout</a>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<script src="/javascripts/inbox-init.js" defer data-address="{{ address }}"></script> <script src="/javascripts/inbox-init.js" defer data-address="{{ address }}"></script>
<script src="/javascripts/lock-modals.js" defer></script>
<div class="inbox-container"> <div class="inbox-container">
<div class="inbox-header"> <div class="inbox-header">
<h1 class="inbox-title">{{ address }}</h1> <h1 class="inbox-title">{{ address }}</h1>
@ -32,14 +45,89 @@
{% endfor %} {% endfor %}
{% if not mailSummaries %} {% if not mailSummaries %}
<div class="empty-state"> <blockquote>
<div class="empty-card"> There are no mails yet.
<h3>Inbox Empty</h3> </blockquote>
<p>Your emails will appear here once they arrive.</p>
</div>
</div>
{% endif %} {% endif %}
{% if lockEnabled and not isLocked %}
<!-- Lock Modal -->
<div id="lockModal" class="modal" style="display: none;" data-lock-error="{{ error|default('') }}">
<div class="modal-content">
<span class="close" id="closeLock">&times;</span>
<h3>Protect Inbox</h3>
<p class="modal-description">Password-protect this inbox. Locked emails won't be deleted. Protection active for {{ locktimer }}hrs after last login.</p>
{% if error and error == 'locking_disabled_for_example' %}
<p id="lockServerError" class="unlock-error">Locking is disabled for the example inbox.</p>
{% endif %}
<p id="lockErrorInline" class="unlock-error" style="display:none"></p>
<form method="POST" action="/lock/lock">
<input type="hidden" name="address" value="{{ address }}">
<fieldset>
<label for="lockPassword" class="floating-label">Password (min 8 characters)</label>
<input type="password" id="lockPassword" name="password" placeholder="Password" required minlength="8" class="modal-input">
<label for="lockConfirm" class="floating-label">Confirm Password</label>
<input type="password" id="lockConfirm" placeholder="Confirm" required minlength="8" class="modal-input">
<button type="submit" class="button-primary modal-button">Lock Inbox</button>
</fieldset>
</form>
</div> </div>
</div> </div>
{% endif %}
{% if lockEnabled and isLocked and not hasAccess %}
<!-- Unlock Modal -->
<div id="unlockModal" class="modal" style="display: none;" data-unlock-error="{{ unlockError|default('') }}">
<div class="modal-content">
<span class="close" id="closeUnlock">&times;</span>
<h3>Unlock Inbox</h3>
<p class="modal-description">Enter password to access this locked inbox.</p>
{% if unlockError %}
<p class="unlock-error">
{% if unlockError == 'invalid_password' %}
Invalid password. Please try again.
{% elseif unlockError == 'missing_fields' %}
Please provide a password.
{% else %}
An error occurred. Please try again.
{% endif %}
</p>
{% endif %}
<form method="POST" action="/lock/unlock">
<input type="hidden" name="address" value="{{ address }}">
<input type="hidden" name="redirectTo" value="{{ redirectTo }}">
<fieldset>
<label for="unlockPassword" class="floating-label">Password</label>
<input type="password" id="unlockPassword" name="password" placeholder="Password" required class="modal-input">
<button type="submit" class="button-primary modal-button">Unlock</button>
</fieldset>
</form>
</div>
</div>
{% endif %}
{% if lockEnabled and isLocked and hasAccess %}
<!-- Remove Lock Modal -->
<div id="removeLockModal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close" id="closeRemoveLock">&times;</span>
<h3>Remove Password Lock</h3>
<p class="modal-description">Are you sure you want to remove the password lock from this inbox? This cannot be undone.</p>
<form method="POST" action="/lock/remove">
<input type="hidden" name="address" value="{{ address }}">
<fieldset>
<button type="submit" class="button-primary modal-button modal-button-danger">Remove Lock</button>
<button type="button" class="button modal-button modal-button-cancel" id="cancelRemoveLock">Cancel</button>
</fieldset>
</form>
</div>
</div>
{# JS handled in /javascripts/lock-modals.js #}
{% endif %}
{% endblock %} {% endblock %}

View file

@ -30,7 +30,7 @@
{% block footer %} {% block footer %}
<section class="container footer"> <section class="container footer">
<hr> <hr>
<h4>{{ branding[0] }} offered by <a href="{{ branding[2] }}" style="text-decoration:underline" target="_blank">{{ branding[1] }}</a> | All Emails will be deleted after {{ purgeTime | raw }} | Currently handling <u><i>{{ count }}</i></u> Emails</h4> <h4>{{ branding[0] }} offered by <a href="{{ branding[2] }}" style="text-decoration:underline" target="_blank">{{ branding[1] }}</a> | All Emails will be deleted after {{ purgeTime | raw }} | Currently handling {{ totalcount | raw }}</h4>
<h4 class="container footer-two"> This project is <a href="https://github.com/crazyco-xyz/48hr.email" style="text-decoration:underline" target="_blank">open-source ♥</a></h4> <h4 class="container footer-two"> This project is <a href="https://github.com/crazyco-xyz/48hr.email" style="text-decoration:underline" target="_blank">open-source ♥</a></h4>
</section> </section>
{% endblock %} {% endblock %}

View file

@ -5,7 +5,11 @@
<a href="/inbox/{{ address }}">← Return to inbox</a> <a href="/inbox/{{ address }}">← Return to inbox</a>
<a href="/inbox/{{ address }}/{{ uid }}/delete">Delete Email</a> <a href="/inbox/{{ address }}/{{ uid }}/delete">Delete Email</a>
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank">View Raw</a> <a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank">View Raw</a>
{% if lockEnabled and isLocked and hasAccess %}
<a href="/lock/logout">Logout</a>
{% else %}
<a href="/logout">Logout</a> <a href="/logout">Logout</a>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -13,6 +13,7 @@ const config = require('../../application/config')
const inboxRouter = require('./routes/inbox') const inboxRouter = require('./routes/inbox')
const loginRouter = require('./routes/login') const loginRouter = require('./routes/login')
const errorRouter = require('./routes/error') const errorRouter = require('./routes/error')
const lockRouter = require('./routes/lock')
const { sanitizeHtmlTwigFilter } = require('./views/twig-filters') const { sanitizeHtmlTwigFilter } = require('./views/twig-filters')
const Helper = require('../../application/helper') const Helper = require('../../application/helper')
@ -39,13 +40,25 @@ app.use(logger('dev'))
app.use(express.json()) app.use(express.json())
app.use(express.urlencoded({ extended: false })) app.use(express.urlencoded({ extended: false }))
// Session middleware // Session support for inbox locking
app.use(session({ if (config.lock.enabled) {
secret: '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', // They will hate me for this, its temporary tho, I swear! const session = require('express-session')
app.use(session({
secret: config.lock.sessionSecret,
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: false,
cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24 hours cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours
})) }))
}
// Clear session when user goes Home so locked inboxes require password again
app.get('/', (req, res, next) => {
if (config.lock.enabled && req.session) {
req.session.destroy(() => next())
} else {
next()
}
})
// Remove trailing slash middleware (except for root) // Remove trailing slash middleware (except for root)
app.use((req, res, next) => { app.use((req, res, next) => {
@ -72,15 +85,12 @@ app.use(
) )
Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter) Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter)
/**
app.get('/', (req, res, _next) => {
res.redirect('/login')
})
**/
app.use('/', loginRouter) app.use('/', loginRouter)
app.use('/inbox', inboxRouter) app.use('/inbox', inboxRouter)
app.use('/error', errorRouter) app.use('/error', errorRouter)
if (config.lock.enabled) {
app.use('/lock', lockRouter)
}
// Catch 404 and forward to error handler // Catch 404 and forward to error handler
app.use((req, res, next) => { app.use((req, res, next) => {

457
package-lock.json generated
View file

@ -11,6 +11,8 @@
"dependencies": { "dependencies": {
"array.prototype.flatmap": "^1.3.3", "array.prototype.flatmap": "^1.3.3",
"async-retry": "^1.3.3", "async-retry": "^1.3.3",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.5.0",
"compression": "^1.8.1", "compression": "^1.8.1",
"debug": "^4.4.3", "debug": "^4.4.3",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
@ -1225,6 +1227,26 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/base64id": { "node_modules/base64id": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
@ -1262,6 +1284,83 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.3.0",
"node-gyp-build": "^4.8.4"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/better-sqlite3": {
"version": "12.5.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz",
"integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bl/node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/bl/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/bl/node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.4", "version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
@ -1390,6 +1489,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -1537,6 +1660,12 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/chrome-trace-event": { "node_modules/chrome-trace-event": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
@ -1874,6 +2003,30 @@
} }
} }
}, },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -1986,6 +2139,15 @@
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dir-glob": { "node_modules/dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -2168,6 +2330,15 @@
"semver": "bin/semver" "semver": "bin/semver"
} }
}, },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/engine.io": { "node_modules/engine.io": {
"version": "6.6.5", "version": "6.6.5",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz",
@ -3376,6 +3547,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/express": { "node_modules/express": {
"version": "4.22.1", "version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
@ -3647,6 +3827,12 @@
"node": "^10.12.0 || >=12.0.0" "node": "^10.12.0 || >=12.0.0"
} }
}, },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -3912,6 +4098,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -4086,6 +4278,12 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
} }
}, },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -4508,6 +4706,26 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -4633,6 +4851,12 @@
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@ -5835,6 +6059,18 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/min-indent": { "node_modules/min-indent": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@ -5861,12 +6097,17 @@
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/mnemonist": { "node_modules/mnemonist": {
"version": "0.27.2", "version": "0.27.2",
"resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.27.2.tgz", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.27.2.tgz",
@ -5952,6 +6193,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/natural-compare": { "node_modules/natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -5985,6 +6232,38 @@
"node": ">=4.0.0" "node": ">=4.0.0"
} }
}, },
"node_modules/node-abi": {
"version": "3.85.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
"integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.27", "version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@ -6229,7 +6508,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
@ -6623,6 +6901,32 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -6710,6 +7014,16 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -6860,6 +7174,30 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/read-pkg": { "node_modules/read-pkg": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@ -7628,6 +7966,51 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/slash": { "node_modules/slash": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
@ -8012,6 +8395,57 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tar-stream/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/tar-stream/node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/terser": { "node_modules/terser": {
"version": "5.44.1", "version": "5.44.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
@ -8182,6 +8616,18 @@
"strip-bom": "^3.0.0" "strip-bom": "^3.0.0"
} }
}, },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/twig": { "node_modules/twig": {
"version": "0.10.3", "version": "0.10.3",
"resolved": "https://registry.npmjs.org/twig/-/twig-0.10.3.tgz", "resolved": "https://registry.npmjs.org/twig/-/twig-0.10.3.tgz",
@ -8494,6 +8940,12 @@
"integrity": "sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==", "integrity": "sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -8803,7 +9255,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": { "node_modules/ws": {

View file

@ -1,11 +1,15 @@
{ {
"name": "48hr.email", "name": "48hr.email",
"version": "1.6.3", "version": "1.7.0",
"private": false, "private": false,
"description": "48hr.email is your favorite open-source tempmail client. ", "description": "48hr.email is your favorite open-source tempmail client.",
"keywords": [ "keywords": [
"node",
"mail",
"email",
"tempmail", "tempmail",
"48hr.email", "48hr.email",
"temporary-email",
"disposable-email" "disposable-email"
], ],
"homepage": "https://48hr.email/", "homepage": "https://48hr.email/",
@ -28,6 +32,8 @@
"dependencies": { "dependencies": {
"array.prototype.flatmap": "^1.3.3", "array.prototype.flatmap": "^1.3.3",
"async-retry": "^1.3.3", "async-retry": "^1.3.3",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.5.0",
"compression": "^1.8.1", "compression": "^1.8.1",
"debug": "^4.4.3", "debug": "^4.4.3",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
@ -69,8 +75,7 @@
} }
] ]
}, },
"overrides": [ "overrides": [{
{
"files": "public/javascripts/*.js", "files": "public/javascripts/*.js",
"esnext": false, "esnext": false,
"env": [ "env": [
@ -79,7 +84,6 @@
"globals": [ "globals": [
"io" "io"
] ]
} }]
]
} }
} }