diff --git a/.gitignore b/.gitignore index 6612e9c..ac854f8 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,7 @@ dist # Built Visual Studio Code Extensions *.vsix +instances/* +!instances/default +instances/default/cert.* +instances/default/vault.json diff --git a/instances/default/config.json b/instances/default/config.json new file mode 100644 index 0000000..8f9c326 --- /dev/null +++ b/instances/default/config.json @@ -0,0 +1,66 @@ +{ + "servers": [ + { + "features": { + "api": false, + "client": true, + "create": true, + "createPath": "/" + }, + "http": { + "port": 3080 + }, + "https": { + "cert": "./cert.crt", + "key": "./cert.key", + "port": 3443 + } + }, + { + "features": { + "api": true, + "client": false, + "create": true + }, + "http": { + "port": 3081 + }, + "https": { + "cert": "./cert.crt", + "key": "./cert.key", + "port": 3444 + } + } + ], + "vault": { + "db": { + "type": "file", + "filename": "./vault.json" + }, + "crypto": { + "method": "aes-256-gcm", + "key": "Djblt6b8RQ+mQC6/ilpjC6y9bkfUEzkt", + "iv": "dsfnuo3" + }, + "publicID": { + "minLength": 37, + "maxLength": 45 + }, + "otLinkID": { + "minLength": 37, + "maxLength": 45, + "timeout": 360 + } + }, + "mailer": { + "sender": "Secret Sender ", + "server": { + "host": "127.0.0.1", + "port": 25, + "secure": false, + "tls": { + "rejectUnauthorized": false + } + } + } +} diff --git a/instances/default/gencert.sh b/instances/default/gencert.sh new file mode 100755 index 0000000..b045cf9 --- /dev/null +++ b/instances/default/gencert.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -x509 -nodes -days 3650 -out ./cert.crt -keyout ./cert.key -subj "/C=/ST=/L=/O=/OU=/CN=" diff --git a/instances/default/start.sh b/instances/default/start.sh new file mode 100755 index 0000000..17ebc9b --- /dev/null +++ b/instances/default/start.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +node ../../src/app.js diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..3c485a1 --- /dev/null +++ b/src/app.js @@ -0,0 +1,11 @@ +import path from "path" +import config from "./core/config.js" +import server from "./core/server.js" + +global.__dirname = path.dirname(process.argv[1]) + +const _config = config.get() + +_config.servers.forEach(srvConfig => { + server.start(srvConfig) +}) diff --git a/src/core/config.js b/src/core/config.js new file mode 100644 index 0000000..77e7300 --- /dev/null +++ b/src/core/config.js @@ -0,0 +1,23 @@ +import fs from "fs" + +let _configFile = "config.json" +if(process.argv[2]) _configFile = process.argv[2] + +let _config = null + +const load = () => { + if(! fs.existsSync(_configFile)) { + throw new Error('Config file not found') + } + _config = JSON.parse(fs.readFileSync(_configFile)) +} + +if(! _config) load() + +const get = () => { + return structuredClone(_config) +} + +export default { + get, +} diff --git a/src/core/mailer.js b/src/core/mailer.js new file mode 100644 index 0000000..3ad19bd --- /dev/null +++ b/src/core/mailer.js @@ -0,0 +1,29 @@ +import nodemailer from 'nodemailer' +import config from './config.js' + +const _config = config.get() + +const send = (email = { + from: null, + to: '', + subject: '', + text: null, + html: null, +}) => { + if(! email.from) email.from = _config.mailer.sender + const transporter = nodemailer.createTransport(_config.mailer.server) + const mailOptions = { + from: email.from, + to: email.to, + subject: email.subject, + } + if(email.html) { + mailOptions.html = email.html + if(email.text) mailOptions.text = email.text + } else mailOptions.text = email.text || '' + transporter.sendMail(mailOptions, function(error, info){}) +} + +export default { + send, +} \ No newline at end of file diff --git a/src/core/routes.js b/src/core/routes.js new file mode 100644 index 0000000..ca9066e --- /dev/null +++ b/src/core/routes.js @@ -0,0 +1,101 @@ +import bodyParser from "body-parser" +import vault from "./vault.js" + +const _title = "Secret Sender" + +const load = (router, features) => { + + if(features.api == true) { + + router.get("/api/getlink/:publicID", (req, res, next) => { + const link = vault.getLink(req.params.publicID) + res.json(link) + }) + + router.get("/api/createOTLink/:publicID", (req, res, next) => { + const otLink = vault.createOTLink(req.params.publicID) + res.json(otLink) + }) + + router.get("/api/read/:otLinkID", (req, res, next) => { + const secret = vault.read(req.params.otLinkID) + res.json(secret) + }) + + if(features.create == true) { + + router.post("/api/create", bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => { + const link = vault.create(req.body) + res.json(link) + }) + + } + + } + + if(features.client !== false) { + + router.get("/link/:publicID", (req, res, next) => { + const link = vault.getLink(req.params.publicID) + if(link) { + router.pug(res, "link.pug", { title : _title, link: link, linkUrl: req.protocol + "://" + req.headers.host + "/link/" + link.publicID }) + } else { + res.redirect('/') + } + }) + + router.post("/requestotl", bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => { + const link = vault.getLink(req.body.publicID) + if(link) { + const otLink = vault.createOTLink(link.publicID) + if(otLink) { + res.json(otLink) + } else { + res.json(null) + } + } else { + res.json(null) + } + }) + + router.get("/read/:otLinkID", (req, res, next) => { + const secret = vault.read(req.params.otLinkID) + if(secret) { + router.pug(res, "read.pug", { title : _title, secret: secret }) + } else { + res.redirect('/') + } + }) + + if(features.create == true) { + + router.get(features.createPath, (req, res, next) => { + router.pug(res, "create.pug", { title: _title }) + }) + + router.post(features.createPath, bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => { + const link = vault.create(req.body) + if(link) { + res.json(link) + } else { + res.json(null) + } + }) + + router.post("/newlink", bodyParser.urlencoded({ extended: false }), bodyParser.json(), (req, res, next) => { + router.pug(res, "newlink.pug", { title : _title, link: req.body, linkUrl: req.protocol + "://" + req.headers.host + "/link/" + req.body.publicID }) + }) + + } + + router.get("/", (req, res, next) => { + router.pug(res, "index.pug", { title: _title }) + }) + + } + +} + +export default { + load, +} \ No newline at end of file diff --git a/src/core/server.js b/src/core/server.js new file mode 100644 index 0000000..c36b065 --- /dev/null +++ b/src/core/server.js @@ -0,0 +1,60 @@ +import fs from "fs" +import express from "express" +import http from "http" +import https from "https" +import routes from "./routes.js" + +const defaulthandler = (req, res, next) => { + res.redirect("/") +} + +const pug = (res, file, value, allowcache) => { + //app.srv.cache(res,allowcache!=false) + res.setHeader("X-Frame-Options", "sameorigin") + res.setHeader("X-XSS-Protection", "1; mode=block") + res.render(file, value) +} +/* +const getremoteip = (req) => { + return req.ip +} +*/ + + +const start = (srvConfig) => { + const app = express() + const router = express.Router() + router.use(express.json()) + + router.pug = pug + + routes.load(router, srvConfig.features) + + app.disable("x-powered-by") + + app.set('trust proxy', true) + app.set("views", __dirname + "/pug") + app.set("view engine", "pug") + + app.use(router) + app.use(express.static(__dirname + "/html", {maxAge: 86400000000})) + app.use(defaulthandler) + + if(srvConfig.http) { + const srv = http.createServer({}, app) + srv.listen(srvConfig.http.port, '0.0.0.0') + console.log("Listen http " + srvConfig.http.port + " : " + JSON.stringify(srvConfig.features)) + } + if(srvConfig.https && fs.existsSync(srvConfig.https.cert) && fs.existsSync(srvConfig.https.key)) { + const sslsrv = https.createServer({ + cert: fs.readFileSync(srvConfig.https.cert), + key: fs.readFileSync(srvConfig.https.key), + }, app) + sslsrv.listen(srvConfig.https.port, '0.0.0.0') + console.log("Listen https " + srvConfig.https.port + " : " + JSON.stringify(srvConfig.features)) + } +} + +export default { + start, +} diff --git a/src/core/vault.js b/src/core/vault.js new file mode 100644 index 0000000..ff4d38b --- /dev/null +++ b/src/core/vault.js @@ -0,0 +1,223 @@ +import fs from 'fs' +import cron from 'node-cron' +import crypto from 'crypto' +import config from "./config.js" +import mailer from './mailer.js' + +const _config = config.get() +let _cache = null +const _readLinks = [] +const _dbFile = _config.vault.db.filename + +const init = () => { + if(fs.existsSync(_dbFile)) { + _cache = JSON.parse(fs.readFileSync(_dbFile)) + } else { + _cache = [] + } + clean() + cron.schedule('* * * * *', clean) +} +const clean = () => { + let needSave = false + for(let i = _cache.length - 1; i >= 0; i--) { + if(calcTimeLeft(_cache[i]) <= 0) { + _cache.splice(i, 1) + needSave = true + } + } + if(needSave === true) save() + for(let i = _readLinks.length - 1; i >= 0; i--) { + if(Math.round(_config.vault.otLinkID.timeout - ((new Date() - new Date(_readLinks[i].date)) / 60000)) <= 0) { + _readLinks.splice(i, 1) + } + } +} +const save = () => { + fs.writeFileSync(_dbFile, JSON.stringify(_cache)) +} + +const newRandomNumber = (min, max) => { + return Math.floor(Math.random() * (max - min) + min) +} +const newRandomString = (length) => { + const randomBytes = crypto.randomBytes(Math.ceil(length / 2)) + return randomBytes.toString('hex').slice(0, length) +} +const calcReadsLeft = (item) => { + if(item.maxReads <= 0) return -1 + return item.maxReads - item.readCount +} +const calcTimeLeft = (item) => { + return Math.round(item.expireTime - ((new Date() - new Date(item.date)) / 60000)) +} +const encrypt = (data) => { + const cipher = crypto.createCipheriv(_config.vault.crypto.method, _config.vault.crypto.key, _config.vault.crypto.iv) + let ciphertext = cipher.update(data, 'utf8', 'base64') + ciphertext += cipher.final('base64') + let value = cipher.getAuthTag().toString('hex') + '=' + ciphertext + return value +} +const decrypt = (data) => { + if(!data || data === '') return '' + const decipher = crypto.createDecipheriv( + _config.vault.crypto.method, + _config.vault.crypto.key, + _config.vault.crypto.iv + ) + const splitIndex = data.indexOf('=') + const tag = data.substring(0, splitIndex) + const value = data.substring(splitIndex + 1) + decipher.setAuthTag(Buffer.from(tag, 'hex')) + let plaintext = decipher.update(value, 'base64', 'utf8') + plaintext += decipher.final('utf8') + return plaintext +} +const emailReadNotify = (data) => { + const email = { + to: data.emailNotify, + subject: "Secret read", + text: 'Secret read' + } + if(data.subject) email.subject = email.subject + ' - ' + data.subject + if(data.message) email.text = data.message + if(data.senderName) email.text = email.text + '\n\n' + data.senderName + mailer.send(email) +} + +if(! _cache) init() + +const createData = (data = { + senderName: null, + subject: null, + message: null, + secret: null, + emailNotify: null, + expireDays: null, + expireHours: null, + expireTime: null, + maxReads: null, + emailOTL: null, +}) => { + if(! data) return null + const validatedData = {} + if(data.senderName && data.senderName != "") validatedData.senderName = data.senderName + if(data.subject && data.subject != "") validatedData.subject = data.subject + if(data.message && data.message != "") validatedData.message = data.message + if(data.secret && data.secret != "") validatedData.secret = data.secret + if(data.emailNotify && data.emailNotify != "") validatedData.emailNotify = data.emailNotify + if(data.emailOTL && data.emailOTL != "") validatedData.emailOTL = data.emailOTL + if(data.maxReads && data.maxReads != "") validatedData.maxReads = data.maxReads + let expireTime = 0 + if(data.expireDays && data.expireDays > 0) expireTime = +data.expireDays * 24 * 60 + if(data.expireHours && data.expireHours > 0) expireTime = expireTime + +data.expireHours * 60 + if(data.expireTime && data.expireTime > 0) expireTime = +data.expireTime + if(expireTime > 0) validatedData.expireTime = expireTime + + if(! validatedData.secret || validatedData.secret === '') return null + if(! validatedData.expireTime) validatedData.expireTime = 24 * 60 + else if(validatedData.expireTime < 1) validatedData.expireTime = 1 + else if(validatedData.expireTime > 560 * 60) validatedData.expireTime = 560 * 60 + if(! validatedData.maxReads || isNaN(validatedData.maxReads)) validatedData.maxReads = 1 + else if(validatedData.maxReads < 0) validatedData.maxReads = 0 + else if(validatedData.maxReads > 999) validatedData.maxReads = 999 + + return validatedData +} +const createLinkData = (data) => { + if(data) return { + date: data.date, + publicID: data.publicID, + senderName: data.senderName, + subject: data.subject, + message: data.message, + readsLeft: calcReadsLeft(data), + timeLeft: calcTimeLeft(data), + } + return null +} +const create = (secretData) => { + const data = createData(secretData) + if(! data) return null + data.date = new Date() + data.readCount = 0 + data.publicID = newRandomString(newRandomNumber(_config.vault.publicID.minLength, _config.vault.publicID.maxLength)) + data.secret = encrypt(data.secret) + _cache.push(data) + save() + const returnData = createLinkData(data) + returnData.emailNotify = data.emailNotify + returnData.emailOTL = data.emailOTL + return returnData +} +const remove = (publicID) => { + for(let i = 0; i < _cache.length; i++) { + if(_cache[i].publicID == publicID) { + _cache.splice(i, 1) + save() + for(let i = _readLinks.length - 1; i >= 0; i--) { + if(_readLinks[i].publicID == publicID) { + _readLinks.splice(i, 1) + } + } + return true + } + } + return false +} +const getLink = (publicID) => { + const data = _cache.find((item) => item.publicID === publicID) + return createLinkData(data) +} +const createOTLink = (publicID) => { + const value = _cache.find((item) => item.publicID === publicID) + if(value) { + const readLink = { + date: new Date(), + otLinkID: newRandomString(newRandomNumber(_config.vault.otLinkID.minLength, _config.vault.otLinkID.maxLength)), + publicID: publicID, + } + _readLinks.push(readLink) + return { + otLinkID: readLink.otLinkID + } + } + return null +} +const removeOTL = (otLinkID) => { + for(let i = _readLinks.length - 1; i >= 0; i--) { + if(_readLinks[i].otLinkID == otLinkID) { + _readLinks.splice(i, 1) + } + } +} +const read = (otLinkID) => { + const linkValue = _readLinks.find((item) => item.otLinkID === otLinkID) + if(linkValue) { + removeOTL(otLinkID) + const value = _cache.find((item) => item.publicID === linkValue.publicID) + if(value) { + value.readCount += 1 + if(value.readCount < value.maxReads) { + save() + } else { + remove(value.publicID) + } + value.timeLeft = calcTimeLeft(value) + value.readsLeft = calcReadsLeft(value) + if(value.emailNotify) emailReadNotify(value) + const returnValue = structuredClone(value) + returnValue.secret = decrypt(returnValue.secret) + return returnValue + } + } + return null +} + + +export default { + create, + getLink, + createOTLink, + read, +} \ No newline at end of file diff --git a/src/html/style.css b/src/html/style.css new file mode 100644 index 0000000..16e9236 --- /dev/null +++ b/src/html/style.css @@ -0,0 +1,99 @@ +* { + box-sizing: border-box; + scrollbar-color: #3eb0ec #232629; + scrollbar-width: 10px; +} +body { + font: 400 12pt "Droid Sans", sans-serif; + background-color: #454c53; + padding: 0; + margin: 0; + color: #e3d1c7; +} +input { + border: 1px solid transparent; +} +input:focus, textarea:focus { + outline: 1px solid #3eb0ec; +} +input:invalid, textarea:invalid { + border: 1px dashed red; +} + +::placeholder { + color: #e3d1c7; +} + +.left { + text-align: left; +} +.center { + text-align: center; +} +.right { + text-align: right; +} + +.fullwidth { + width: 100%; +} + +.inlineleftspace { + margin-left: 4px ; +} + +.monospace { + font-family: 'Courier New', Courier, monospace; +} + +.form { + max-width: 800px; + min-width: 200px; + margin: auto; + padding-left: 20px; + padding-right: 20px; + padding-bottom: 20px; +} + + +.panel { + margin-top: 4px; +} + +.controlpanel { + margin-top: 30px; +} + + +.titlebox { + font-size:8pt; + margin-left: 4px; + margin-bottom: 2px; +} + +.textbox { + border: none; + color: #e3d1c7; + background-color: #31363b; + padding: 4px; +} + +.messagebox { + border: none; + color: #e3d1c7; + background-color: #31363b; + padding: 4px; + resize: vertical; +} + +.button { + background-color: #31363b; + color: #e3d1c7; + padding: 4px; + border: solid 1px #2cc4db; + border-radius: 4px; +} +.button:hover { + background-color: #2cc4db; + color: #31363b; +} diff --git a/src/package-lock.json b/src/package-lock.json new file mode 100644 index 0000000..6c4f220 --- /dev/null +++ b/src/package-lock.json @@ -0,0 +1,1198 @@ +{ + "name": "secret-sender", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "secret-sender", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^4.21.1", + "node-cron": "^3.0.3", + "nodemailer": "^6.9.15", + "pug": "^3.0.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.9.tgz", + "integrity": "sha512-aI3jjAAO1fh7vY/pBGsn1i9LDbRP43+asrRlkPuTXW5yHXtd1NgTEMudbBoDDxrf1daEEfPJqR+JBMakzrR4Dg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.9.tgz", + "integrity": "sha512-OwS2CM5KocvQ/k7dFJa8i5bNGJP0hXWfVCfDkqRFP1IreH1JDC7wG6eCYCi0+McbfT8OR/kNqsI0UU0xP9H6PQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/assert-never": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.3.0.tgz", + "integrity": "sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==", + "license": "MIT" + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "license": "MIT", + "dependencies": { + "is-regex": "^1.0.3" + } + }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "license": "MIT" + }, + "node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "license": "MIT", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemailer": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", + "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "license": "MIT" + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pug": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", + "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", + "license": "MIT", + "dependencies": { + "pug-code-gen": "^3.0.3", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-code-gen": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", + "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/pug-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "license": "MIT" + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "license": "MIT", + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "license": "MIT" + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + } + } +} diff --git a/src/package.json b/src/package.json new file mode 100644 index 0000000..7f2a76e --- /dev/null +++ b/src/package.json @@ -0,0 +1,19 @@ +{ + "name": "secret-sender", + "version": "1.0.0", + "main": "app.js", + "type": "module", + "scripts": { + "start": "node app.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "express": "^4.21.1", + "node-cron": "^3.0.3", + "nodemailer": "^6.9.15", + "pug": "^3.0.3" + } +} diff --git a/src/pug/_main.pug b/src/pug/_main.pug new file mode 100644 index 0000000..a0e5f94 --- /dev/null +++ b/src/pug/_main.pug @@ -0,0 +1,12 @@ +doctype html +html(lang='fr-FR') + head + meta(name='viewport', content='width=device-width, height=device-height, initial-scale=1, maximum-scale=1') + meta(charset='utf-8') + title #{title} + link(rel="stylesheet", href="/style.css") + block style + block script + block head + body + block content diff --git a/src/pug/create.pug b/src/pug/create.pug new file mode 100644 index 0000000..6c20a80 --- /dev/null +++ b/src/pug/create.pug @@ -0,0 +1,86 @@ +extends _main + +block script + script. + const post = (path, value) => { + const form = document.createElement('form'); + form.method = 'post' + form.action = path + document.body.appendChild(form) + for (const key in value) { + const formField = document.createElement('input') + formField.type = 'hidden' + formField.name = key + formField.value = value[key] + form.appendChild(formField) + } + form.submit() + } + const createLink = () => { + const data = {} + if(SenderName.value != '') data.senderName = SenderName.value + if(Subject.value != '') data.subject = Subject.value + if(Message.value != '') data.message = Message.value + if(Secret.value != '') data.secret = Secret.value + if(EMailNotify.value != '') data.emailNotify = EMailNotify.value + data.expireDays = ExpireDays.value + data.expireHours = ExpireHours.value + data.maxReads = MaxReads.value + if(EMailOTL.value != '') data.emailOTL = EMailOTL.value + const path = window.location.pathname + fetch(path, { + method: "post", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }).then((res) => { + res.json().then((value) => { + if(value) post('/newlink', value) + }) + }) + } + +block content + div.center + div.form + h1 Nouveau lien + div.panel.left + div.titlebox Expéditeur + div + input#SenderName(type="text", placeholder="(Optionel)").textbox.fullwidth + div.panel.left + div.titlebox Sujet + div + input#Subject(type="text", placeholder="(Optionel)").textbox.fullwidth + div.panel.left + div.titlebox Message + div + textarea#Message(rows="3", placeholder="(Optionel)").messagebox.fullwidth + div.panel.left + div.titlebox Secret (*) + div + textarea#Secret(rows="4", placeholder="Votre secret", required).messagebox.fullwidth.monospace + div.panel.left + div.titlebox E-Mail (Notifier) + div + input#EMailNotify(type="email", placeholder="(Optionel) Votre e-mail pour être notifié").messagebox.fullwidth + div.panel.left + div.titlebox Expiration + table.fullwidth + tr + td + input#ExpireDays(type="number", value="2", min="0", max="13").textbox.small + span.inlineleftspace jour(s) + input#ExpireHours(type="number", value="1", min="1", max="23").textbox.small.inlineleftspace + span.inlineleftspace heure(s) + td.right + input#MaxReads(type="number", value="1", min="0", max="999").textbox.small.inlineleftspace + span.inlineleftspace affichage(s) + div.panel.left + div.titlebox E-Mail (OTL) + div + input#EMailOTL(type="email", placeholder="(Optionel) E-mail du destinatire pour OTL").messagebox.fullwidth + div.panel.controlpanel + button(onclick="createLink()").button.fullwidth Créer le lien diff --git a/src/pug/index.pug b/src/pug/index.pug new file mode 100644 index 0000000..a9c89a9 --- /dev/null +++ b/src/pug/index.pug @@ -0,0 +1,5 @@ +extends _main + +block content + div.center + h1 Secret Sender diff --git a/src/pug/link.pug b/src/pug/link.pug new file mode 100644 index 0000000..3b41ec8 --- /dev/null +++ b/src/pug/link.pug @@ -0,0 +1,54 @@ +extends _main + +block script + script. + const requestLink = (publicID) => { + const path = "/requestotl" + fetch(path, { + method: "post", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ publicID: publicID }), + }).then((res) => { + res.json().then((value) => { + window.location = '/read/' + value.otLinkID + }) + }) + } + +block content + div.center + div.form + h1 Accéder + if link.senderName + div.panel.left + div.titlebox Expéditeur + div.messagebox=link.senderName + if link.subject + div.panel.left + div.titlebox Sujet + div.messagebox=link.subject + if link.message + div.panel.left + div.titlebox Message + div.messagebox!=link.message.replaceAll('\n', '
') + div.panel + table.fullwidth + tr + td.left + if link.timeLeft > 2820 + div.infotext Expires dans #{Math.round(link.timeLeft / 60 / 24)} jours + else if link.timeLeft > 120 + div.infotext Expires dans #{Math.round(link.timeLeft / 60)} heures + else + div.infotext Expires dans #{link.timeLeft} min + td.right + if link.readsLeft > 0 + div.infotext #{link.readsLeft} affichage(s) restant(s) + div.panel.controlpanel + if link.emailOTL + button(onclick="requestLink('" + link.publicID + "')").button.fullwidth Demmander + else + button(onclick="requestLink('" + link.publicID + "')").button.fullwidth Afficher diff --git a/src/pug/newlink.pug b/src/pug/newlink.pug new file mode 100644 index 0000000..09fbca3 --- /dev/null +++ b/src/pug/newlink.pug @@ -0,0 +1,43 @@ +extends _main + +block content + div.center + div.form + h1 Nouveau lien + div.panel + div.messagebox.left.overflow.nowrap #{linkUrl} + if link.senderName + div.panel.left + div.titlebox Expéditeur + div.messagebox=link.senderName + if link.subject + div.panel.left + div.titlebox Sujet + div.messagebox=link.subject + if link.message + div.panel.left + div.titlebox Message + div.messagebox!=link.message.replaceAll('\n', '
') + if link.emailNotify + div.panel.left + div.titlebox Notifier + div.messagebox=link.emailNotify + if link.emailOTL + div.panel.left + div.titlebox OTL + div.messagebox=link.emailOTL + div.panel + table.fullwidth + tr + td.left + if link.timeLeft > 2820 + div.infotext Expires dans #{Math.round(link.timeLeft / 60 / 24)} jours + else if link.timeLeft > 120 + div.infotext Expires dans #{Math.round(link.timeLeft / 60)} heures + else + div.infotext Expires dans #{link.timeLeft} min + td.right + if link.readsLeft > 0 + div.infotext #{link.readsLeft} affichage(s) restant(s) + div.panel.controlpanel + button(onclick="navigator.clipboard.writeText('" + linkUrl + "')").button.fullwidth Copier le lien diff --git a/src/pug/read.pug b/src/pug/read.pug new file mode 100644 index 0000000..16320e4 --- /dev/null +++ b/src/pug/read.pug @@ -0,0 +1,37 @@ +extends _main + +block content + div.center + div.form + h1 Secret + if secret.senderName + div.panel.left + div.titlebox Expéditeur + div.messagebox=secret.senderName + if secret.subject + div.panel.left + div.titlebox Sujet + div.messagebox=secret.subject + if secret.message + div.panel.left + div.titlebox Message + div.messagebox!=secret.message.replaceAll('\n', '
') + div.panel.left + div.titlebox Secret + div#Secret.messagebox.monospace!=secret.secret.replaceAll('\n', '
') + if secret.readsLeft > 0 && secret.timeLeft > 0 + div.panel + table.fullwidth + tr + td.left + if secret.timeLeft > 2820 + div.infotext Expires dans #{Math.round(secret.timeLeft / 60 / 24)} jours + else if secret.timeLeft > 120 + div.infotext Expires dans #{Math.round(secret.timeLeft / 60)} heures + else + div.infotext Expires dans #{secret.timeLeft} min + td.right + if secret.readsLeft > 0 + div.infotext #{secret.readsLeft} affichage(s) restant(s) + div.panel.controlpanel + button(onclick="navigator.clipboard.writeText(Secret.innerText)").button.fullwidth Copier le secret diff --git a/test-api.sh b/test-api.sh new file mode 100755 index 0000000..c5f4205 --- /dev/null +++ b/test-api.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +HTTPPORT=3081 +EMAILNOTIFY="${1}" +EMAILOTP="${2}" + +echo "Create secret ..." +CREATEDATA='{' +CREATEDATA+=' "senderName": "Me"' +CREATEDATA+=', "subject": "Votre code"' +CREATEDATA+=', "message": "Hello"' +CREATEDATA+=', "secret": "My Secret"' +if [ "${EMAILNOTIFY}" != "" ]; then + CREATEDATA+=', "emailNotify": "'${EMAILNOTIFY}'"' +fi +if [ "${EMAILOTP}" != "" ]; then + CREATEDATA+=', "emailOTP": "'${EMAILOTP}'"' +fi +CREATEDATA+=' }' +echo "${CREATEDATA}" +NEWSECRET=$(curl -s -X POST -H "Content-Type: application/json" \ + -d "${CREATEDATA}" \ + http://localhost:${HTTPPORT}/api/create) + +echo "New secret :" +echo "${NEWSECRET}" | jq + +PUBLICID=$(echo "${NEWSECRET}" | jq -r .publicID) + +echo "" +echo "Get Link" +curl -s http://localhost:${HTTPPORT}/api/getlink/"${PUBLICID}" | jq + +echo "" +echo "Create OTLink" +OTLINKID=$(curl -s http://localhost:${HTTPPORT}/api/createOTLink/"${PUBLICID}" | jq -r .otLinkID) +echo "${OTLINKID}" + +echo "" +echo "Read secret first time :" +curl -s http://localhost:${HTTPPORT}/api/read/"${OTLINKID}" | jq + +echo "Read secret second time :" +curl -s http://localhost:${HTTPPORT}/api/read/"${OTLINKID}" | jq