[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!
This commit is contained in:
ClaraCrazy 2025-12-26 09:10:25 +01:00
parent 83a4fac4ab
commit a571381462
No known key found for this signature in database
GPG key ID: EBBC896ACB497011
16 changed files with 1185 additions and 31 deletions

View file

@ -31,3 +31,9 @@ HTTP_DISPLAY_SORT=2 # Domain display
# 2 = alphabetical + first item shuffled,
# 3 = shuffle 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

@ -100,7 +100,6 @@ WantedBy=multi-user.target
### TODO (PRs welcome):
- Add user registration:
- 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
#### Unsure:

20
app.js
View file

@ -13,6 +13,7 @@ const ClientNotification = require('./infrastructure/web/client-notification')
const ImapService = require('./application/imap-service')
const MailProcessingService = require('./application/mail-processing-service')
const MailRepository = require('./domain/mail-repository')
const InboxLock = require('./domain/inbox-lock')
const clientNotification = new ClientNotification()
debug('Client notification service initialized')
@ -55,6 +56,23 @@ imapService.on(ImapService.EVENT_ERROR, error => {
})
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
@ -86,4 +104,4 @@ server.on('error', error => {
console.error('Fatal web server error', error)
process.exit(1)
}
})
})

View file

@ -60,6 +60,13 @@ const config = {
branding: parseValue(process.env.HTTP_BRANDING),
displaySort: Number(process.env.HTTP_DISPLAY_SORT),
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
}
};

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

@ -541,4 +541,114 @@ label {
border-color: rgba(155, 77, 202, 0.3);
transform: translateX(5px);
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

@ -6,6 +6,7 @@ const debug = require('debug')('48hr-email:routes')
const config = require('../../../application/config')
const Helper = require('../../../application/helper')
const helper = new(Helper)
const { checkLockAccess } = require('../middleware/lock')
const purgeTime = helper.purgeTimeElemetBuilder()
@ -18,17 +19,29 @@ const sanitizeAddress = param('address').customSanitizer(
}
)
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, next) => {
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, checkLockAccess, async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
if (!mailProcessingService) {
throw new Error('Mail processing service not available')
}
debug(`Inbox request for ${req.params.address}`)
const inboxLock = req.app.get('inboxLock')
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`)
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', {
title: `${config.http.branding[0]} | ` + req.params.address,
purgeTime: purgeTime,
@ -37,6 +50,12 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, next) =
totalcount: totalcount,
mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
branding: config.http.branding,
lockEnabled: config.lock.enabled,
isLocked: isLocked,
hasAccess: hasAccess,
unlockError: unlockErrorSession,
error: lockError,
redirectTo: req.originalUrl
})
} catch (error) {
debug(`Error loading inbox for ${req.params.address}:`, error.message)
@ -48,6 +67,7 @@ router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, next) =
router.get(
'^/:address/:uid([0-9]+)',
sanitizeAddress,
checkLockAccess,
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
@ -67,6 +87,11 @@ router.get(
// Emails are immutable, cache if found
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}`)
res.render('mail', {
title: mail.subject + " | " + req.params.address,
@ -77,6 +102,9 @@ router.get(
mail,
uid: req.params.uid,
branding: config.http.branding,
lockEnabled: config.lock.enabled,
isLocked: isLocked,
hasAccess: hasAccess
})
} else {
debug(`Email ${req.params.uid} not found for ${req.params.address}`)
@ -95,6 +123,7 @@ router.get(
router.get(
'^/:address/delete-all',
sanitizeAddress,
checkLockAccess,
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
@ -118,6 +147,7 @@ router.get(
router.get(
'^/:address/:uid/delete',
sanitizeAddress,
checkLockAccess,
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
@ -136,6 +166,7 @@ router.get(
router.get(
'^/:address/:uid/:checksum([a-f0-9]+)',
sanitizeAddress,
checkLockAccess,
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')
@ -195,6 +226,7 @@ router.get(
router.get(
'^/:address/:uid/raw',
sanitizeAddress,
checkLockAccess,
async(req, res, next) => {
try {
const mailProcessingService = req.app.get('mailProcessingService')

View file

@ -0,0 +1,112 @@
const express = require('express')
const router = express.Router()
router.post('/lock', async(req, res) => {
const { address, password } = req.body
if (!address || !password || password.length < 8) {
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()) {
if (req.session) req.session.lockError = 'locking_disabled_for_example'
return res.redirect(`/inbox/${address}`)
}
await inboxLock.lock(address, password)
// Clear cache for this inbox
if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) {
mailProcessingService.cachedFetchFullMail.clear()
}
req.session.lockedInbox = address
res.redirect(`/inbox/${address}`)
} catch (error) {
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}`
if (!address || !password) {
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) {
if (req.session) req.session.unlockError = 'invalid_password'
return res.redirect(destination)
}
req.session.lockedInbox = address
res.redirect(destination)
} catch (error) {
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) {
mailProcessingService.cachedFetchFullMail.clear()
}
req.session.destroy()
res.redirect('/')
})
router.post('/remove', async(req, res) => {
const { address } = req.body
if (!address) {
return res.redirect('/')
}
// Check if user has access to this locked inbox
const hasAccess = req.session && req.session.lockedInbox === address.toLowerCase()
if (!hasAccess) {
return res.redirect(`/inbox/${address}`)
}
try {
const inboxLock = req.app.get('inboxLock')
const mailProcessingService = req.app.get('mailProcessingService')
await inboxLock.release(address)
// Clear cache when removing lock
if (mailProcessingService.cachedFetchFullMail && mailProcessingService.cachedFetchFullMail.clear) {
mailProcessingService.cachedFetchFullMail.clear()
}
req.session.destroy()
res.redirect(`/inbox/${address}`)
} catch (error) {
console.error('Remove lock error:', error)
if (req.session) req.session.lockError = 'remove_failed'
res.redirect(`/inbox/${address}`)
}
})
module.exports = router

View file

@ -2,8 +2,10 @@
{% block header %}
<div class="action-links">
<a href="/inbox/{{ address }}">← Return to inbox</a>
<a href="/logout">Logout</a>
{% if showUnlockButton %}
<a href="#" id="unlockBtn">Unlock</a>
{% endif %}
<a href="/">Logout</a>
</div>
{% endblock %}
@ -11,4 +13,48 @@
<h1>{{message}}</h1>
<h2>{{error.status}}</h2>
<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 %}

View file

@ -2,14 +2,27 @@
{% block header %}
<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="/logout">Logout</a>
{% if lockEnabled and hasAccess %}
<a href="/lock/logout">Logout</a>
{% else %}
<a href="/logout">Logout</a>
{% endif %}
</div>
{% endblock %}
{% block body %}
<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-header">
<h1 class="inbox-title">{{ address }}</h1>
@ -31,15 +44,90 @@
</a>
{% endfor %}
{% if not mailSummaries %}
<div class="empty-state">
<div class="empty-card">
<h3>Inbox Empty</h3>
<p>Your emails will appear here once they arrive.</p>
</div>
</div>
{% if not mailSummaries %}
<blockquote>
There are no mails yet.
</blockquote>
{% 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.</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>
{% 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 %}

View file

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

View file

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

457
package-lock.json generated
View file

@ -11,6 +11,8 @@
"dependencies": {
"array.prototype.flatmap": "^1.3.3",
"async-retry": "^1.3.3",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.5.0",
"compression": "^1.8.1",
"debug": "^4.4.3",
"dotenv": "^17.2.3",
@ -1225,6 +1227,26 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
@ -1262,6 +1284,83 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"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": {
"version": "1.20.4",
"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_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": {
"version": "1.1.2",
"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"
}
},
"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": {
"version": "1.0.4",
"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": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -1986,6 +2139,15 @@
"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": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -2168,6 +2330,15 @@
"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": {
"version": "6.6.5",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz",
@ -3376,6 +3547,15 @@
"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": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
@ -3647,6 +3827,12 @@
"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": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -3912,6 +4098,12 @@
"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": {
"version": "1.0.0",
"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"
}
},
"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": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -4508,6 +4706,26 @@
"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": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -4633,6 +4851,12 @@
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
"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": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@ -5835,6 +6059,18 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@ -5861,12 +6097,17 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"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": {
"version": "0.27.2",
"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_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": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -5985,6 +6232,38 @@
"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": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@ -6229,7 +6508,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@ -6623,6 +6901,32 @@
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -6710,6 +7014,16 @@
"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": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -6860,6 +7174,30 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"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": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@ -7628,6 +7966,51 @@
"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": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
@ -8012,6 +8395,57 @@
"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": {
"version": "5.44.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
@ -8182,6 +8616,18 @@
"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": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/twig/-/twig-0.10.3.tgz",
@ -8494,6 +8940,12 @@
"integrity": "sha512-QXo+O/QkLP/x1nyi54uQiG0XrODxdysuQvE5dtVqv7F5K2Qb6FsN+qbr6KhF5wQ20tfcV3VQp0/2x1e1MRSPWg==",
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -8803,7 +9255,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/ws": {

View file

@ -28,6 +28,8 @@
"dependencies": {
"array.prototype.flatmap": "^1.3.3",
"async-retry": "^1.3.3",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.5.0",
"compression": "^1.8.1",
"debug": "^4.4.3",
"dotenv": "^17.2.3",