alpha release

update v0.8.0
This commit is contained in:
2026-03-17 22:49:38 -04:00
commit 63a91931da
157 changed files with 10951 additions and 0 deletions

19
.editorconfig Normal file
View File

@@ -0,0 +1,19 @@
# EditorConfig is awesome: https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = tab
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
[*.json]
indent_size = 2
indent_style = spaces
[*.md]
indent_style = tab
trim_trailing_whitespace = false

52
.gitignore vendored Normal file
View File

@@ -0,0 +1,52 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn setups files
.yarn
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# twtkpr data file directory
.data
# Other files
NOTES.md
TODO.md
*.bak

11
.prettierrc.json Normal file
View File

@@ -0,0 +1,11 @@
{
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto",
"quoteProps": "as-needed"
}

38
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,38 @@
{
"prettier.enable": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"prettier.requireConfig": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.unusualLineTerminators": "off",
"cSpell.words": [
"blakejs",
"dotenv",
"eslintcache",
"itsericwoodward",
"jspm",
"nodenext",
"noframe",
"noopener",
"noreferrer",
"pgpkey",
"pids",
"TWTKPR",
"twts",
"Twttr",
"twtxt",
"uuidv",
"wscript"
],
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
}
}

3
.yarnrc.yml Normal file
View File

@@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.13.0.cjs

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Eric Woodward [https://www.itsericwoodward.com]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

30
README.md Normal file
View File

@@ -0,0 +1,30 @@
# Twtkpr
An [ExpressJS](https://expressjs.com/) router for serving, sharing, and updating a
[`twtxt.txt` file](https://twtxt.dev/).
> [!WARNING]
> **STILL IN ALPHA**: Although this plugin lacks documentation, examples, tests, installation
> flexibility, and polish, it's still fully-functional and actively deployed to at least one site.
### Features
- Uses JWT (with refresh) for security.
- Allows for adding new twts and directly editing a `twtxt.txt` file from within a browser.
- Includes a ull-featured GET API for your `twtxt.txt` file backed by an in-memory cache.
- Supports optional drag-and-drop file upload handling with automatic linking.
## Installing
```sh
yarn add express-twtkpr
```
More to come!
---
## License
Copyright (c) 2026 Eric Woodward, released under the
[MIT License](https://www.itsericwoodward.com/licenses/mit/).

81
dist/package.json vendored Normal file
View File

@@ -0,0 +1,81 @@
{
"name": "express-twtkpr",
"version": "0.8.0",
"description": "An express library for hosting and maintaining a twtxt.txt file.",
"license": "MIT",
"author": {
"name": "Eric Woodward",
"email": "hey@itsericwoodward.com",
"url": "https://www.itsericwoodward.com"
},
"repository": {
"type": "git",
"url": "https://git.itsericwoodward.com/eric/express-twtkp"
},
"keywords": [],
"type": "module",
"main": "./dist/src/index.js",
"module": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"exports": {
".": {
"import": "./dist/src/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"start": "node --env-file=.env dist/index-app.js",
"build": "tsc && cp -r src/client dist/src",
"dev": "DEBUG='twtkpr:*' tsx watch --env-file=.env src/index-app.ts",
"get:hash": "tsx --env-file=.env src/cli.ts get-hash",
"lint": "eslint --fix src test",
"prepublishOnly": "yarn build",
"set:user": "tsx --env-file=.env src/cli.ts set-user",
"test": "vitest",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@cacheable/node-cache": "^2.0.2",
"@exodus/blakejs": "^1.1.1-exodus.0",
"base32.js": "^0.1.0",
"bcryptjs": "^3.0.3",
"cookie-parser": "^1.4.7",
"dayjs": "^1.11.20",
"debug": "^4.4.3",
"express": "^5.2.1",
"express-rate-limit": "^8.3.1",
"express-session": "^1.19.0",
"express-slow-down": "^3.1.0",
"formidable": "^3.5.4",
"jsonwebtoken": "^9.0.3",
"link": "^2.1.2",
"session-file-store": "^1.5.0",
"twtxt-lib": "^0.9.4",
"uuid": "^13.0.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19",
"@types/debug": "^4.1.12",
"@types/express": "^5.0.6",
"@types/express-session": "^1.18.2",
"@types/formidable": "^3.5.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/morgan": "^1.9.10",
"@types/node": "^25.5.0",
"@types/supertest": "^7.2.0",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-security": "^4.0.0",
"prettier": "^3.8.1",
"supertest": "^7.2.2",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.1.0"
},
"packageManager": "yarn@4.13.0"
}

506
dist/src/client/script.js vendored Normal file
View File

@@ -0,0 +1,506 @@
const DEBUG_ON = true;
// served from same path as TWTXT file
const TWTXT_FILE_URL = window.location.pathname;
const REMEMBER_LOGIN_STORAGE_KEY = 'rememberLogin';
const ACCESS_TOKEN_COOKIE_KEY = 'accessToken';
const debug = (...vals) => {
if (DEBUG_ON) console.log(...vals);
};
export default (async () => {
/* DOM Elements */
const twtForm = document.getElementById('twtForm'),
loginForm = document.getElementById('loginControls-form'),
fileBox = document.getElementById('fileBox'),
fileContentsSection = document.getElementById('fileContentsSection'),
toastContainer = document.getElementById('toast-container'),
twtControlsContentInput = document.getElementById(
'twtControlsContentInput'
),
twtSubmitButton = document.querySelector('.twtControls-submitButton'),
twtLogoutButton = document.getElementById('twtControlsLogoutButton'),
twtFileEditButton = document.getElementById('twtControlsEditButton'),
menuCheckbox = document.getElementById('hamburgerToggleCheckbox'),
twtxtEditFormText = document.getElementById('twtxtEditFormText'),
uploadInputs = document.querySelectorAll('.twtControls-uploadInput'),
rememberToggle = document.getElementById('loginControls-rememberToggle');
const lastModifiedDates = {};
let isEditing = false,
cookie,
fileText,
token;
const showToast = (message, type = 'success') => {
const toast = document.createElement('div');
toast.classList.add('toast');
if (type === 'error') toast.classList.add('error');
toast.textContent = message;
toastContainer.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'fadeOut 0.5s forwards';
setTimeout(() => toast.remove(), 500);
}, 3000);
};
const beginEditMode = () => {
isEditing = true;
if (menuCheckbox) menuCheckbox.checked = false;
if (twtSubmitButton) twtSubmitButton.setAttribute('disabled', 'disabled');
document.body.classList.add('js-editMode');
};
const endEditMode = () => {
isEditing = false;
if (twtSubmitButton) twtSubmitButton.removeAttribute('disabled');
document.body.classList.remove('js-editMode');
};
const getCookie = (name) => {
const cookies = document.cookie.split('; ');
for (const cookie of cookies) {
const [key, value] = cookie.split('=');
if (key === name) return decodeURIComponent(value);
}
return null;
};
const setCookie = (name, value, expireDays) => {
const isSecure = window.location.protocol === 'https';
const expireDate = new Date(); // current date
expireDate.setTime(
expireDate.getTime() + (expireDays ?? 0) * 24 * 60 * 60 * 1000
);
let expires =
expireDays !== undefined ? `expires=${expireDate.toUTCString()}; ` : '';
document.cookie = `${name}=${encodeURIComponent(value)}; ${expires}${isSecure ? 'Secure; ' : ''}SameSite=Strict; Path=/`;
};
const loadTwtxtFile = async (filePath = TWTXT_FILE_URL) => {
debug('loadTwtxtFile start');
let response;
const fileURL = `${filePath.charAt(0) !== '/' ? '/' : ''}${filePath}`;
const headers = new Headers();
headers.set(
'Accept',
'application/twtxt+txt,text/twtxt,text/plain;q=0.9,*/*;q=0.8'
);
if (lastModifiedDates[fileURL]) {
headers['If-Modified-Since'] = lastModifiedDates[fileURL];
}
try {
response = await fetch(fileURL, {
method: 'GET',
headers,
mode: 'same-origin',
});
if (response?.body) fileText = await new Response(response.body).text();
if (response?.headers.get('Last-Modified')) {
lastModifiedDates[fileURL] = response?.headers.get('Last-Modified');
}
if (fileText && fileBox) {
fileBox.textContent = fileText;
}
} catch (err) {
showToast('Unable to load file, please try again later.', 'error');
console.error('Error loading file', err);
}
debug('loadTwtxtFile end');
};
const refreshToken = async (hideToast = false) => {
debug('refreshToken start', hideToast);
const rememberToggleVal =
localStorage.getItem(REMEMBER_LOGIN_STORAGE_KEY) === 'true';
const res = await fetch(`${TWTXT_FILE_URL}`, {
method: 'POST',
body: new URLSearchParams({
rememberToggle: rememberToggleVal,
type: 'refresh',
}),
credentials: 'include', // Include cookies
});
if (res.ok && res?.body) {
token = await new Response(res.body).text();
if (rememberToggleVal) {
debug('refreshToken set new accessToken cookie');
setCookie(ACCESS_TOKEN_COOKIE_KEY, token);
}
debug('refreshToken end OK');
return;
}
// Handle refresh failure
if (!hideToast)
showToast('Unable to refresh token, please try again later.', 'error');
token = undefined;
document.body.classList.remove('js-authorized');
debug('refreshToken end error');
throw new Error('Failed to refresh token');
};
const uploadFiles = async (files, uploadRoute, secondAttempt = false) => {
if (!uploadRoute) return;
debug('uploadFiles', token, files, uploadRoute, secondAttempt);
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
try {
const res = await fetch(uploadRoute, {
method: 'POST',
body: formData,
headers: {
Authorization: `Bearer ${token}`,
},
credentials: 'include',
});
if (res.ok) {
showToast(`File${files.length !== 1 ? 's' : ''} uploaded`);
const filePath = await res.text();
twtControlsContentInput.value += filePath
.split('\n')
.map((currFilePath) =>
[
' ',
location.protocol,
'//',
location.hostname,
location.protocol !== 'https' && location.port !== 80
? ':' + location.port
: '',
currFilePath,
].join('')
)
.join('');
return;
}
if (!secondAttempt) {
await refreshToken();
return uploadFiles(files, uploadRoute, true);
}
showToast(
`Unable to upload image${files.length !== 1 ? 's' : ''} refresh token, please try again later.`,
'error'
);
} catch (err) {
console.error(err);
}
};
/* Handlers */
const dragOverHandler = (ev) => {
const files = [...ev.dataTransfer.items].filter(
(item) => item.kind === 'file'
);
if (files.length > 0) {
ev.preventDefault();
ev.dataTransfer.dropEffect = 'copy';
}
};
const dragOverWindowHandler = (ev) => {
const files = [...ev.dataTransfer.items].filter(
(item) => item.kind === 'file'
);
if (files.length > 0) {
ev.preventDefault();
if (!twtControlsContentInput.contains(ev.target)) {
ev.dataTransfer.dropEffect = 'none';
}
}
};
const dropHandler = (ev) => {
ev.preventDefault();
if (!uploadInputs.length) return;
const files = [...ev.dataTransfer.items]
.map((item) => item.getAsFile())
.filter((file) => file);
debug('dropHandler', files);
uploadFiles(files, uploadInputs[0].getAttribute('data-route'));
};
const dropWindowHandler = (ev) => {
if ([...ev.dataTransfer.items].some((item) => item.kind === 'file')) {
ev.preventDefault();
}
};
const editClickHandler = () => {
if (isEditing) return;
if (twtxtEditFormText) twtxtEditFormText.value = fileText;
beginEditMode();
};
const editResetHandler = (ev) => {
ev.preventDefault();
if (!isEditing && !confirm('Do you want to quit editing?')) return;
if (fileText && fileBox) {
fileBox.textContent = fileText;
}
endEditMode();
};
const editSubmitHandler = async (ev, secondAttempt = false) => {
ev?.preventDefault();
try {
const newFileText = twtxtEditFormText.value;
debug('edit file submit', { newFileText });
if (!newFileText) return;
const res = await fetch(`${TWTXT_FILE_URL}`, {
method: 'PUT',
body: new URLSearchParams({
fileContents: newFileText,
}),
headers: {
Authorization: `Bearer ${token}`,
},
credentials: 'include',
});
if (!res.ok && !secondAttempt) {
await refreshToken();
return editSubmitHandler(ev, true);
}
showToast('File updated');
if (newFileText && fileBox) {
fileText = newFileText;
fileBox.textContent = newFileText;
}
endEditMode();
await loadTwtxtFile();
} catch (err) {
if (!secondAttempt) {
await refreshToken();
return twtFormSubmitHandler(ev, true);
}
showToast('Unable to update file, please try again later.', 'error');
console.error('Error updating file', err);
}
return false;
};
const loginFormSubmitHandler = async (ev) => {
ev?.preventDefault();
let response;
try {
const loginData = new URLSearchParams(new FormData(loginForm));
debug('loginForm submit', { loginData });
response = await fetch(TWTXT_FILE_URL, {
method: 'POST',
body: loginData,
mode: 'same-origin',
credentials: 'include',
});
debug('loginForm submit', { response });
if (!response.ok) throw new Error(response.statusText);
showToast('Login complete');
} catch (err) {
showToast('Unable to login, please try again later.', 'error');
console.error('Error logging in', err);
return;
}
if (response?.body) token = await new Response(response.body).text();
if (token) document.body.classList.add('js-authorized');
if (token && rememberToggle.checked)
setCookie(ACCESS_TOKEN_COOKIE_KEY, token, 7);
debug('loginForm submit', { cookie, token, response });
return false;
};
const logoutHandler = async () => {
try {
const res = await fetch(`${TWTXT_FILE_URL}`, {
method: 'POST',
body: new URLSearchParams({
type: 'logout',
}),
credentials: 'include',
});
if (!res.ok) throw new Error();
document.cookie = `${ACCESS_TOKEN_COOKIE_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
if (menuCheckbox) menuCheckbox.checked = false;
window.location.reload();
} catch {
showToast('Unable to logout, please try again later.', 'error');
}
};
const rememberToggleHandler = (ev) => {
if (ev.target.checked) {
localStorage.setItem(REMEMBER_LOGIN_STORAGE_KEY, 'true');
return;
}
localStorage.removeItem(REMEMBER_LOGIN_STORAGE_KEY);
};
const twtContentKeyupHandler = (ev) => {
const hasContent =
(twtControlsContentInput.value?.trim() ?? '').length !== 0;
if (twtControlsContentInput) {
requestAnimationFrame(() => {
// Sync with browser repaint
twtControlsContentInput.style.height = 'auto';
twtControlsContentInput.style.height = `${Math.max(twtControlsContentInput.scrollHeight, 80)}px`;
});
}
if (isEditing || !hasContent)
twtSubmitButton.setAttribute('disabled', 'disabled');
else twtSubmitButton.removeAttribute('disabled');
if (hasContent && ev?.key === 'Enter' && ev?.ctrlKey)
twtFormSubmitHandler();
};
const twtFormSubmitHandler = async (ev, secondAttempt = false) => {
ev?.preventDefault();
try {
const twtData = new FormData(twtForm);
const twtContent = twtData.get('content').trim();
debug('twtForm submit data', { twtData });
if (!twtContent) return;
twtData.set('content', twtContent.replaceAll('\n', '\u2028'));
const twtBody = new URLSearchParams(twtData);
debug('twtForm submit body', { twtBody });
const res = await fetch(TWTXT_FILE_URL, {
method: 'POST',
body: twtBody,
headers: {
Authorization: `Bearer ${token}`,
},
credentials: 'include',
});
if (!res.ok && !secondAttempt) {
debug('twtForm submit - not OK, trying refresh');
await refreshToken();
return twtFormSubmitHandler(ev, true);
}
showToast('Twt sent');
twtForm.reset();
await loadTwtxtFile();
// scroll to bottom of file to show update
fileContentsSection.scrollTop = fileBox.scrollHeight;
} catch (err) {
debug('twtForm submit - error, trying refresh', err, !secondAttempt);
if (!secondAttempt) {
await refreshToken();
return twtFormSubmitHandler(ev, true);
}
showToast('Unable to twt, please try again later.', 'error');
console.error('Error POSTing twt', err);
}
return false;
};
const uploadChangeHandler = (ev) => {
uploadFiles(ev.target.files, ev.target.getAttribute('data-route'));
};
/* Attach Handlers to Listeners */
Array.from(uploadInputs).forEach((uploadInput) => {
uploadInput.addEventListener('change', uploadChangeHandler);
});
loginForm.addEventListener('submit', loginFormSubmitHandler);
twtForm.addEventListener('submit', twtFormSubmitHandler);
twtForm.addEventListener('keyup', twtContentKeyupHandler);
twtControlsContentInput.addEventListener('drop', dropHandler);
twtControlsContentInput.addEventListener('dragover', dragOverHandler);
twtLogoutButton.addEventListener('click', logoutHandler);
twtFileEditButton.addEventListener('click', editClickHandler);
twtxtEditForm.addEventListener('reset', editResetHandler);
twtxtEditForm.addEventListener('submit', editSubmitHandler);
window.addEventListener('dragover', dragOverWindowHandler);
window.addEventListener('drop', dropWindowHandler);
rememberToggle.addEventListener('change', rememberToggleHandler);
/* Start App*/
loadTwtxtFile().catch(() => {});
token = getCookie(ACCESS_TOKEN_COOKIE_KEY);
if (token) document.body.classList.add('js-authorized');
debug('client loaded');
})();

717
dist/src/client/styles.css vendored Normal file
View File

@@ -0,0 +1,717 @@
/**
* Taken from Normalize.css v12.1.1 / https://csstools.github.io/normalize.css/
*/
:where(html) {
line-height: 1.15;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
:where(b, strong) {
font-weight: bolder;
}
:where(code, kbd, pre, samp) {
font-family: monospace, monospace;
font-size: 1em;
}
:where(button, input, select) {
margin: 0;
}
:where(button) {
text-transform: none;
}
:where(
button,
input:is([type='button' i], [type='reset' i], [type='submit' i])
) {
-webkit-appearance: button;
appearance: button;
}
:where(select) {
text-transform: none;
}
:where(textarea) {
margin: 0;
}
:where(input[type='search' i]) {
-webkit-appearance: textfield;
appearance: textfield;
outline-offset: -2px;
}
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
::-webkit-input-placeholder {
color: inherit;
opacity: 0.54;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
:where(
button,
input:is(
[type='button' i],
[type='color' i],
[type='reset' i],
[type='submit' i]
)
)::-moz-focus-inner {
border-style: none;
padding: 0;
}
:where(
button,
input:is(
[type='button' i],
[type='color' i],
[type='reset' i],
[type='submit' i]
)
)::-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Local styles
*/
:root {
/* #0a0a14 / Vulcan*/
--bg: rgb(10, 10, 20);
--bg-al: rgb(10, 10, 20, 0.6);
/* #1b1b27 / Steel Gray */
--bg-hl: rgb(27, 27, 39);
/* #6e6e81 / Storm Gray */
--fg: rgb(110, 110, 129);
--fg-hl: #ccc;
/* #9f9fc1 / Logan */
--link: rgb(159, 159, 193);
}
* {
box-sizing: border-box;
}
body {
background-color: var(--bg);
color: var(--fg);
}
a {
border-radius: 0.5rem;
color: var(--link);
padding: 0 0.25rem;
transition: all 0.5s;
}
a:hover {
color: var(--fg-hl);
background-color: var(--bg-hl);
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--fg-hl);
}
input,
textarea {
background-color: var(--bg-hl);
border-color: var(--fg);
color: var(--fg);
}
button,
input[type='reset'],
input[type='submit'] {
background-color: var(--bg-hl);
border-color: var(--link);
color: var(--link);
}
button:disabled,
input:disabled {
opacity: 0.2;
}
.appInfo {
font-size: smaller;
font-style: italic;
margin: 0.5rem auto;
text-align: center;
}
.button {
background-color: var(--bg-hl);
border: 1px solid var(--link);
border-radius: 0.5rem;
color: var(--link);
padding: 0.5rem;
}
.fileContentsSection {
margin-bottom: 7rem;
max-width: 100vw;
overflow: auto;
}
.fileContentsSection-fileBox {
margin: 0;
max-height: 1000rem;
overflow-y: hidden;
transition: all 0.5s;
}
.hamburgerToggle {
grid-area: menuButton;
max-width: 3rem;
}
.hamburgerToggle-icon,
.hamburgerToggle-icon:after,
.hamburgerToggle-icon:before {
background-color: var(--fg);
height: 0.25rem;
position: absolute;
transition-duration: 0.5s;
width: 2rem;
}
.hamburgerToggle-icon {
top: 12px;
}
.hamburgerToggle-icon:before {
content: '';
top: -12px;
}
.hamburgerToggle-icon:after {
content: '';
top: 12px;
}
.hamburgerToggle-label {
cursor: pointer;
display: block;
height: 1.75rem;
left: 0;
position: relative;
top: 0;
transition-duration: 0.5s;
width: 2rem;
}
#hamburgerToggleCheckbox {
opacity: 0;
cursor: pointer;
position: absolute;
}
#hamburgerToggleCheckbox:checked
+ .hamburgerToggle-label
.hamburgerToggle-icon {
transition-duration: 0.5s;
background: transparent;
}
#hamburgerToggleCheckbox:checked
+ .hamburgerToggle-label
.hamburgerToggle-icon:before {
transform: rotateZ(45deg) scaleX(1.25) translate(8px, 8px);
}
#hamburgerToggleCheckbox:checked
+ .hamburgerToggle-label
.hamburgerToggle-icon:after {
transform: rotateZ(-45deg) scaleX(1.25) translate(8px, -8px);
}
#hamburgerToggleCheckbox:checked ~ .popupMenu {
right: 0.5rem;
}
.loginControls {
max-height: 100rem;
overflow: hidden;
}
.loginControls-fields {
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: end;
}
.loginControls-fields-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: end;
}
.loginControls-input {
max-width: 8rem;
}
.loginControls input:focus {
border: 1px solid #eee;
}
.loginControls-label {
margin-right: 1rem;
}
.loginControls-row {
align-items: center;
display: flex;
flex-direction: row;
justify-content: end;
}
.loginControls-toggle {
align-items: center;
border-radius: 8rm;
display: flex;
justify-content: end;
margin-right: 1rem;
}
.loginControls-toggle-checkbox {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
.loginControls-toggle-checkbox:not([disabled]):active
+ .loginControls-toggle-track,
.loginControls-toggle-checkbox:not([disabled]):focus
+ .loginControls-toggle-track {
/* border: 1px solid transparent; */
box-shadow: 0px 0px 0px 2px #333;
}
.loginControls-toggle-checkbox:disabled + .loginControls-toggle-track {
cursor: not-allowed;
opacity: 0.7;
}
.loginControls-toggle-track {
background: var(--bg);
border: 1px solid var(--fg);
border-radius: 100px;
cursor: pointer;
display: flex;
height: 1.5rem;
margin-left: 0.5rem;
position: relative;
width: 3rem;
}
.loginControls-toggle-indicator {
align-items: center;
background: var(--bg-hl);
border: 1px solid var(--fg);
border-radius: 1rem;
bottom: 0.1rem;
display: flex;
height: 1.25rem;
justify-content: center;
left: 0.1rem;
outline: solid 2px transparent;
position: absolute;
transition: 0.25s;
width: 1.25rem;
}
.loginControls-toggle-checkMark {
fill: var(--fg);
height: 1rem;
width: 1rem;
opacity: 0;
transition: opacity 0.25s ease-in-out;
}
.loginControls-toggle-checkbox:checked
+ .loginControls-toggle-track
.loginControls-toggle-indicator {
transform: translateX(1.4rem);
}
.loginControls-toggle-checkbox:checked
+ .loginControls-toggle-track
.loginControls-toggle-indicator
.loginControls-toggle-checkMark {
opacity: 1;
transition: opacity 0.25s ease-in-out;
}
.loginControls-submitButton {
margin-left: 0.5rem;
}
.menu {
background-color: var(--bg-al);
backdrop-filter: blur(5px) saturate(70%);
left: 0;
padding: 0.5rem;
position: fixed;
bottom: 0;
width: 100%;
}
.popupMenu {
background-color: var(--bg-hl);
border: 1px solid var(--fg);
border-radius: 0.5rem;
display: flex;
flex-direction: column;
justify-content: space-around;
right: -10rem;
padding: 0.5rem;
position: fixed;
top: -14rem;
transition: all 0.5s;
width: 5rem;
}
.popupMenu > * {
margin-bottom: 0.5rem;
}
.popupMenu > *:last-child {
margin-bottom: 0;
}
.popupMenu-appInfo {
margin-top: 0;
}
.toast {
display: flex;
align-items: center;
padding: 10px 20px;
background-color: #4caf50;
color: #ffffff;
border-radius: 5px;
font-size: 16px;
animation:
slideIn 0.5s,
fadeOut 0.5s 3s;
opacity: 1;
}
.toast.error {
background-color: #f44336;
}
.toastContainer {
position: fixed;
bottom: 8rem;
right: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
z-index: 1000;
}
.twtControls {
align-items: center;
display: flex;
flex-direction: row;
max-height: 0;
overflow: hidden;
position: relative;
}
.twtControls-appAuthor {
display: none;
}
.twtControls-appInfo {
display: none;
}
.twtControls-contentInput {
width: 100%;
min-height: 5rem;
overflow-y: auto;
resize: none;
transition: max-height 0.2s ease;
}
.twtControls-contentLabel {
align-items: center;
display: flex;
flex-direction: row;
grid-area: textarea;
width: 100%;
}
.twtControls-form {
width: 100%;
}
.twtControls-formRow {
align-items: center;
display: grid;
grid-gap: 0.5rem;
grid-template-areas:
'textarea menuButton'
'textarea postButton';
grid-template-columns: 1fr 3rem;
grid-template-rows: auto;
justify-content: space-between;
}
.twtControls-gitLink {
display: none;
}
.twtControls-uploadInputLabel {
align-items: center;
cursor: pointer;
display: flex;
justify-content: center;
max-width: 100%;
text-align: center;
transition: all 0.5s;
}
.twtControls-uploadInputLabel-normal {
display: none;
}
.twtControls-uploadInputLabel-small {
font-size: small;
}
.twtControls-uploadInputLabel:hover {
background-color: var(--bg-hl);
color: var(--fg-hl);
}
.twtControls-uploadInput {
display: none;
}
.twtControls-submitButton {
background-color: var(--bg-hl);
border: 1px solid var(--link);
border-radius: 0.5rem;
color: var(--link);
grid-area: postButton;
height: 3rem;
width: 3rem;
}
.twtxtEditForm {
max-height: 0;
overflow: hidden;
transition: all 0.5s;
width: 100%;
}
.twtxtEditForm-controls {
display: flex;
gap: 0.5rem;
justify-content: end;
margin-top: 1rem;
}
.twtxtEditForm-textarea {
font-size: large;
height: 80vh;
min-height: 6rem;
white-space: nowrap;
width: 100%;
}
/* Media-based Overrides */
@media (min-width: 24rem) {
.loginControls-input {
max-width: 10rem;
}
}
@media (min-width: 28rem) {
.loginControls-input {
max-width: 12rem;
}
}
@media (min-width: 600px) {
#hamburgerToggleCheckbox:checked ~ .popupMenu {
left: 0.5rem;
}
.popupMenu {
left: -10rem;
top: -6.5rem;
}
.twtControls {
flex-direction: row;
}
.twtControls-appInfo {
display: flex;
flex-direction: column;
grid-area: appInfo;
}
.twtControls-submitButton {
justify-self: end;
}
.twtControls-formRow {
grid-template-areas: 'menuButton appInfo textarea postButton';
grid-template-columns: 3.5rem 3.5rem 1fr 3.5rem;
grid-template-rows: auto;
}
}
@media (min-width: 900px) {
.popupMenu-appInfo {
display: none;
}
.twtControls-appAuthor {
display: inline-block;
max-width: 20rem;
}
.twtControls-appInfo {
display: block;
grid-area: appInfo;
}
.twtControls-contentInput {
min-height: 5rem;
}
.twtControls-formRow {
grid-template-areas: 'menuButton appInfo uploadButton textarea postButton';
grid-template-columns: 3.5rem 1fr 5rem 1fr 3.5rem;
grid-template-rows: auto;
}
.twtControls-gitLink {
display: block;
}
.twtControls-uploadInputLabel-normal {
display: block;
grid-area: uploadButton;
margin-right: 0.5rem;
}
.twtControls-uploadInputLabel-small {
display: none;
}
}
@media (min-width: 1200px) {
.fileContentsSection {
margin-bottom: 1rem;
margin-top: 6rem;
}
.menu {
top: 0;
bottom: auto;
}
.popupMenu {
left: -10rem;
top: 5rem;
}
.toastContainer {
bottom: 2rem;
}
}
@media screen and (-ms-high-contrast: active) {
.loginControls-toggle-track {
border-radius: 0;
}
}
/* State-Based Overrides */
.js-authorized .loginControls {
max-height: 0;
}
.js-authorized .twtControls {
max-height: 100rem;
}
.js-editMode .fileContentsSection-fileBox {
max-height: 0;
overflow: hidden;
transition: all 0.5s;
}
.js-editMode .fileContentsSection-twtxtEditForm {
max-height: 100rem;
}
/* TODO: Fix */
@media (prefers-color-scheme: dark) {
:root {
/* #6e6e81 / Storm Gray */
--bg: #ccc;
--bg-al: rgb(204, 204, 204, 0.6);
--bg-hl: #b6b6c0;
/* #0a0a14 / Vulcan*/
--fg: rgb(10, 10, 20);
/* #1b1b27 / Steel Gray */
--fg-hl: rgb(27, 27, 39);
--link: #35353e;
}
}
/* Animations */
@keyframes slideIn {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

1
dist/src/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export { default } from "./plugin.js";

2
dist/src/index.js vendored Normal file
View File

@@ -0,0 +1,2 @@
export { default } from "./plugin.js";
//# sourceMappingURL=index.js.map

1
dist/src/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"}

14
dist/src/lib/arrayDB.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
/**
* File-backed, in-memory database consisting of arrays of strings (ex: tokens) indexed by other
* strings (ex: usernames).
*
* @param name
* @param directory
* @returns
*/
export default function arrayDB(name: string, directory: string): Promise<{
get: (key?: string) => string[];
getObject: () => Record<string, string[]>;
remove: (key?: string) => void;
set: (key?: string, value?: string[]) => string[];
}>;

73
dist/src/lib/arrayDB.js vendored Normal file
View File

@@ -0,0 +1,73 @@
import Debug from 'debug';
import { join } from 'node:path';
import { loadObjectFromJson, saveToJson } from './utils.js';
const debug = Debug('twtkpr:arrayDB');
/**
* File-backed, in-memory database consisting of arrays of strings (ex: tokens) indexed by other
* strings (ex: usernames).
*
* @param name
* @param directory
* @returns
*/
export default async function arrayDB(name, directory) {
let theName;
let dataObject;
const get = (key = '') => {
debug('get', { key });
if (!theName || !dataObject)
throw new Error('DB must be initialized first');
key = key?.trim();
if (!key)
throw new Error('a valid key must be provided');
return dataObject[key];
};
const getObject = () => dataObject;
const initialize = async (dbName = '') => {
debug('initialize starting', { dbName });
dbName = dbName?.trim();
if (!dbName)
throw new Error('a valid name must be provided');
try {
dataObject = await loadObjectFromJson(join(directory, `${dbName}.json`));
}
catch (err) {
debug('initialize read error', { err });
if (err.code === 'ENOENT')
dataObject = {};
else
throw err;
}
// only initialize (and set name) if everything passes
theName = dbName;
debug('initialize complete', { dataObject, name: theName });
};
const remove = (key = '') => {
debug('remove', { key });
if (!theName || !dataObject)
throw new Error('DB must be initialized first');
key = key?.trim();
if (!key)
throw new Error('a valid key must be provided');
delete dataObject[key];
};
const set = (key = '', value = []) => {
debug('set', { key });
if (!theName || !dataObject)
throw new Error('DB must be initialized first');
key = key?.trim();
if (!key)
throw new Error('a valid key must be provided');
dataObject[key] = value;
saveToJson(dataObject, join(directory, `${name}.json`));
return value;
};
await initialize(name);
return {
get,
getObject,
remove,
set,
};
}
//# sourceMappingURL=arrayDB.js.map

1
dist/src/lib/arrayDB.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"arrayDB.js","sourceRoot":"","sources":["../../../src/lib/arrayDB.ts"],"names":[],"mappings":"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"}

20
dist/src/lib/constants.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
export declare const DEFAULT_PRIVATE_DIRECTORY = ".data";
export declare const DEFAULT_PUBLIC_DIRECTORY = "public";
export declare const DEFAULT_TWTXT_FILENAME = "twtxt.txt";
export declare const DEFAULT_ROUTE = "/twtxt.txt";
export declare const DEFAULT_POST_LIMITER_ACTIVE = true;
export declare const DEFAULT_QUERY_PARAMETER_APP = "app";
export declare const DEFAULT_QUERY_PARAMETER_CSS = "css";
export declare const DEFAULT_QUERY_PARAMETER_FOLLOWING = "following";
export declare const DEFAULT_QUERY_PARAMETER_JS = "js";
export declare const DEFAULT_QUERY_PARAMETER_LOGOUT = "logout";
export declare const DEFAULT_QUERY_PARAMETER_METADATA = "metadata";
export declare const DEFAULT_QUERY_PARAMETER_TWT = "twt";
export declare const DEFAULT_QUERY_PARAMETER_TWTS = "twts";
export declare const DEFAULT_UPLOAD_ACTIVE = true;
export declare const DEFAULT_UPLOAD_ALLOWED_MIME_TYPES = "";
export declare const DEFAULT_UPLOAD_ROUTE = "files";
export declare const DEFAULT_UPLOAD_ENCODING = "utf-8";
export declare const DEFAULT_UPLOAD_HASH_ALGORITHM = "sha256";
export declare const DEFAULT_UPLOAD_KEEP_EXTENSIONS = true;
export declare const __dirname: string;

24
dist/src/lib/constants.js vendored Normal file
View File

@@ -0,0 +1,24 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
export const DEFAULT_PRIVATE_DIRECTORY = '.data';
export const DEFAULT_PUBLIC_DIRECTORY = 'public';
export const DEFAULT_TWTXT_FILENAME = 'twtxt.txt';
export const DEFAULT_ROUTE = `/${DEFAULT_TWTXT_FILENAME}`;
export const DEFAULT_POST_LIMITER_ACTIVE = true;
export const DEFAULT_QUERY_PARAMETER_APP = 'app';
export const DEFAULT_QUERY_PARAMETER_CSS = 'css';
export const DEFAULT_QUERY_PARAMETER_FOLLOWING = 'following';
export const DEFAULT_QUERY_PARAMETER_JS = 'js';
export const DEFAULT_QUERY_PARAMETER_LOGOUT = 'logout';
export const DEFAULT_QUERY_PARAMETER_METADATA = 'metadata';
export const DEFAULT_QUERY_PARAMETER_TWT = 'twt';
export const DEFAULT_QUERY_PARAMETER_TWTS = 'twts';
export const DEFAULT_UPLOAD_ACTIVE = true;
export const DEFAULT_UPLOAD_ALLOWED_MIME_TYPES = '';
export const DEFAULT_UPLOAD_ROUTE = 'files';
export const DEFAULT_UPLOAD_ENCODING = 'utf-8';
// optional in zod
export const DEFAULT_UPLOAD_HASH_ALGORITHM = 'sha256';
export const DEFAULT_UPLOAD_KEEP_EXTENSIONS = true;
export const __dirname = resolve(dirname(fileURLToPath(import.meta.url)), '..');
//# sourceMappingURL=constants.js.map

1
dist/src/lib/constants.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/lib/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,yBAAyB,GAAG,OAAO,CAAC;AACjD,MAAM,CAAC,MAAM,wBAAwB,GAAG,QAAQ,CAAC;AACjD,MAAM,CAAC,MAAM,sBAAsB,GAAG,WAAW,CAAC;AAClD,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,sBAAsB,EAAE,CAAC;AAE1D,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;AAEhD,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AACjD,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AACjD,MAAM,CAAC,MAAM,iCAAiC,GAAG,WAAW,CAAC;AAC7D,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;AAC/C,MAAM,CAAC,MAAM,8BAA8B,GAAG,QAAQ,CAAC;AACvD,MAAM,CAAC,MAAM,gCAAgC,GAAG,UAAU,CAAC;AAC3D,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AACjD,MAAM,CAAC,MAAM,4BAA4B,GAAG,MAAM,CAAC;AAEnD,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAC1C,MAAM,CAAC,MAAM,iCAAiC,GAAG,EAAE,CAAC;AACpD,MAAM,CAAC,MAAM,oBAAoB,GAAG,OAAO,CAAC;AAC5C,MAAM,CAAC,MAAM,uBAAuB,GAAG,OAAO,CAAC;AAE/C,kBAAkB;AAClB,MAAM,CAAC,MAAM,6BAA6B,GAAG,QAAQ,CAAC;AACtD,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC;AAEnD,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC"}

56
dist/src/lib/env.d.ts vendored Normal file
View File

@@ -0,0 +1,56 @@
import { z } from 'zod/v4';
export declare const env: {
NODE_ENV: "development" | "production" | "test";
TWTKPR_REFRESH_SECRET: string;
TWTKPR_ACCESS_SECRET: string;
TWTKPR_DEFAULT_ROUTE: string;
TWTKPR_PRIVATE_DIRECTORY: string;
TWTKPR_PUBLIC_DIRECTORY: string;
TWTKPR_QUERY_PARAMETER_APP: string;
TWTKPR_QUERY_PARAMETER_CSS: string;
TWTKPR_QUERY_PARAMETER_FOLLOWING: string;
TWTKPR_QUERY_PARAMETER_JS: string;
TWTKPR_QUERY_PARAMETER_LOGOUT: string;
TWTKPR_QUERY_PARAMETER_METADATA: string;
TWTKPR_QUERY_PARAMETER_TWT: string;
TWTKPR_QUERY_PARAMETER_TWTS: string;
TWTKPR_TWTXT_FILENAME: string;
TWTKPR_POST_LIMITER_ACTIVE: boolean;
TWTKPR_UPLOAD_ACTIVE: boolean;
TWTKPR_UPLOAD_ALLOWED_MIME_TYPES: string | string[];
TWTKPR_UPLOAD_ROUTE: string;
TWTKPR_UPLOAD_ENCODING: string;
TWTKPR_UPLOAD_HASH_ALGORITHM: string | boolean;
TWTKPR_UPLOAD_KEEP_EXTENSIONS: boolean;
TWTKPR_POST_LIMITER_WINDOW_MS?: number | undefined;
TWTKPR_POST_LIMITER_LIMIT?: number | z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut> | undefined;
TWTKPR_POST_LIMITER_MESSAGE?: any;
TWTKPR_POST_LIMITER_STATUS_CODE?: number | undefined;
TWTKPR_POST_LIMITER_HANDLER?: z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut> | undefined;
TWTKPR_POST_LIMITER_LEGACY_HEADERS?: boolean | undefined;
TWTKPR_POST_LIMITER_STANDARD_HEADERS?: string | boolean | undefined;
TWTKPR_POST_LIMITER_IDENTIFIER?: string | z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut> | undefined;
TWTKPR_POST_LIMITER_STORE?: any;
TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR?: boolean | undefined;
TWTKPR_POST_LIMITER_KEY_GENERATOR?: z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut> | undefined;
TWTKPR_POST_LIMITER_IPV6_SUBNET?: number | boolean | z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut> | undefined;
TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME?: string | undefined;
TWTKPR_POST_LIMITER_SKIP?: z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut> | undefined;
TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS?: boolean | undefined;
TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS?: boolean | undefined;
TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL?: z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut> | undefined;
TWTKPR_POST_LIMITER_VALIDATE?: boolean | Record<string, never> | undefined;
TWTKPR_UPLOAD_ALLOW_EMPTY_FILES?: boolean | undefined;
TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS?: boolean | undefined;
TWTKPR_UPLOAD_DIRECTORY?: string | undefined;
TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER?: z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut> | undefined;
TWTKPR_UPLOAD_FILENAME?: z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut> | undefined;
TWTKPR_UPLOAD_FILTER?: z.core.$InferOuterFunctionType<z.core.$ZodFunctionArgs, z.core.$ZodFunctionOut> | undefined;
TWTKPR_UPLOAD_MAX_FIELDS?: number | undefined;
TWTKPR_UPLOAD_MAX_FIELDS_SIZE?: number | undefined;
TWTKPR_UPLOAD_MAX_FILE_SIZE?: number | undefined;
TWTKPR_UPLOAD_MAX_FILES?: number | undefined;
TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE?: number | undefined;
TWTKPR_UPLOAD_MIN_FILE_SIZE?: number | undefined;
};
export declare const __dirname: string;

206
dist/src/lib/env.js vendored Normal file
View File

@@ -0,0 +1,206 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { z } from 'zod/v4';
import { DEFAULT_POST_LIMITER_ACTIVE, DEFAULT_PRIVATE_DIRECTORY, DEFAULT_PUBLIC_DIRECTORY, DEFAULT_QUERY_PARAMETER_APP, DEFAULT_QUERY_PARAMETER_CSS, DEFAULT_QUERY_PARAMETER_FOLLOWING, DEFAULT_QUERY_PARAMETER_JS, DEFAULT_QUERY_PARAMETER_LOGOUT, DEFAULT_QUERY_PARAMETER_METADATA, DEFAULT_QUERY_PARAMETER_TWT, DEFAULT_QUERY_PARAMETER_TWTS, DEFAULT_ROUTE, DEFAULT_TWTXT_FILENAME, DEFAULT_UPLOAD_ACTIVE, DEFAULT_UPLOAD_ALLOWED_MIME_TYPES, DEFAULT_UPLOAD_ENCODING, DEFAULT_UPLOAD_HASH_ALGORITHM, DEFAULT_UPLOAD_KEEP_EXTENSIONS, DEFAULT_UPLOAD_ROUTE, } from './constants.js';
/*
The following keys are expected to exist in `process.env`, either as listed, or without the
`TWTKPR_` prefix
We only have listed default values for our keys, anything for other plugins (like formidable or
express-rate-limit) fall back to their own defaults (and thus are optional).
*/
const envSchema = z.object({
NODE_ENV: z
.enum(['development', 'production', 'test'])
.default('development'),
// required vars - MUST be passed via ENV
TWTKPR_REFRESH_SECRET: z.string().default(''),
TWTKPR_ACCESS_SECRET: z.string().default(''),
// vars with default values
TWTKPR_DEFAULT_ROUTE: z.string().default(DEFAULT_ROUTE),
TWTKPR_PRIVATE_DIRECTORY: z.string().default(DEFAULT_PRIVATE_DIRECTORY),
TWTKPR_PUBLIC_DIRECTORY: z.string().default(DEFAULT_PUBLIC_DIRECTORY),
TWTKPR_QUERY_PARAMETER_APP: z.string().default(DEFAULT_QUERY_PARAMETER_APP),
TWTKPR_QUERY_PARAMETER_CSS: z.string().default(DEFAULT_QUERY_PARAMETER_CSS),
TWTKPR_QUERY_PARAMETER_FOLLOWING: z
.string()
.default(DEFAULT_QUERY_PARAMETER_FOLLOWING),
TWTKPR_QUERY_PARAMETER_JS: z.string().default(DEFAULT_QUERY_PARAMETER_JS),
TWTKPR_QUERY_PARAMETER_LOGOUT: z
.string()
.default(DEFAULT_QUERY_PARAMETER_LOGOUT),
TWTKPR_QUERY_PARAMETER_METADATA: z
.string()
.default(DEFAULT_QUERY_PARAMETER_METADATA),
TWTKPR_QUERY_PARAMETER_TWT: z.string().default(DEFAULT_QUERY_PARAMETER_TWT),
TWTKPR_QUERY_PARAMETER_TWTS: z.string().default(DEFAULT_QUERY_PARAMETER_TWTS),
TWTKPR_TWTXT_FILENAME: z.string().default(DEFAULT_TWTXT_FILENAME),
/**
* Post limiter plugin
*/
// var with default value
TWTKPR_POST_LIMITER_ACTIVE: z.boolean().default(DEFAULT_POST_LIMITER_ACTIVE),
// optional vars
TWTKPR_POST_LIMITER_WINDOW_MS: z.optional(z.number()),
TWTKPR_POST_LIMITER_LIMIT: z.optional(z.union([z.number(), z.function()])),
TWTKPR_POST_LIMITER_MESSAGE: z.optional(z.any()),
TWTKPR_POST_LIMITER_STATUS_CODE: z.optional(z.number()),
TWTKPR_POST_LIMITER_HANDLER: z.optional(z.function()),
TWTKPR_POST_LIMITER_LEGACY_HEADERS: z.optional(z.boolean()),
TWTKPR_POST_LIMITER_STANDARD_HEADERS: z.optional(z.union([z.boolean(), z.string()])),
TWTKPR_POST_LIMITER_IDENTIFIER: z.optional(z.union([z.string(), z.function()])),
TWTKPR_POST_LIMITER_STORE: z.optional(z.any()),
TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR: z.optional(z.boolean()),
TWTKPR_POST_LIMITER_KEY_GENERATOR: z.optional(z.function()),
TWTKPR_POST_LIMITER_IPV6_SUBNET: z.optional(z.union([z.number(), z.function(), z.boolean()])),
TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME: z.optional(z.string()),
TWTKPR_POST_LIMITER_SKIP: z.optional(z.function()),
TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS: z.optional(z.boolean()),
TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS: z.optional(z.boolean()),
TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL: z.optional(z.function()),
TWTKPR_POST_LIMITER_VALIDATE: z.optional(z.union([z.boolean(), z.object()])),
/**
* Upload plugin
*/
// vars with default values
TWTKPR_UPLOAD_ACTIVE: z.boolean().default(DEFAULT_UPLOAD_ACTIVE),
TWTKPR_UPLOAD_ALLOWED_MIME_TYPES: z
.union([z.string(), z.array(z.string())])
.default(DEFAULT_UPLOAD_ALLOWED_MIME_TYPES),
TWTKPR_UPLOAD_ROUTE: z.string().default(DEFAULT_UPLOAD_ROUTE),
// optional vars
TWTKPR_UPLOAD_ALLOW_EMPTY_FILES: z.optional(z.boolean()),
TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS: z.optional(z.boolean()),
TWTKPR_UPLOAD_DIRECTORY: z.optional(z.string()),
TWTKPR_UPLOAD_ENCODING: z.string().default(DEFAULT_UPLOAD_ENCODING),
TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER: z.optional(z.function()),
TWTKPR_UPLOAD_FILENAME: z.optional(z.function()),
TWTKPR_UPLOAD_FILTER: z.optional(z.function()),
TWTKPR_UPLOAD_HASH_ALGORITHM: z
.union([z.boolean(), z.string()])
.default(DEFAULT_UPLOAD_HASH_ALGORITHM),
TWTKPR_UPLOAD_KEEP_EXTENSIONS: z
.boolean()
.default(DEFAULT_UPLOAD_KEEP_EXTENSIONS),
TWTKPR_UPLOAD_MAX_FIELDS: z.optional(z.number()),
TWTKPR_UPLOAD_MAX_FIELDS_SIZE: z.optional(z.number()),
TWTKPR_UPLOAD_MAX_FILE_SIZE: z.optional(z.number()),
TWTKPR_UPLOAD_MAX_FILES: z.optional(z.number()),
TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE: z.optional(z.number()),
TWTKPR_UPLOAD_MIN_FILE_SIZE: z.optional(z.number()),
});
const parseEnv = () => {
try {
/**
* there's probably an easier way to do this, spreading the keys from above and accepting either
* the app (bare) or library (`TWTKPR_`-prefixed) version of said key.
* But this should work for now.
*/
const parsedEnv = envSchema.parse({
NODE_ENV: process.env.TWTKPR_NODE_ENV || process.env.NODE_ENV,
TWTKPR_ACCESS_SECRET: process.env.TWTKPR_ACCESS_SECRET || process.env.ACCESS_SECRET,
TWTKPR_DEFAULT_ROUTE: process.env.TWTKPR_DEFAULT_ROUTE || process.env.DEFAULT_ROUTE,
TWTKPR_PRIVATE_DIRECTORY: process.env.TWTKPR_PRIVATE_DIRECTORY || process.env.PRIVATE_DIRECTORY,
TWTKPR_PUBLIC_DIRECTORY: process.env.TWTKPR_PUBLIC_DIRECTORY || process.env.PUBLIC_DIRECTORY,
TWTKPR_REFRESH_SECRET: process.env.TWTKPR_REFRESH_SECRET || process.env.REFRESH_SECRET,
TWTKPR_TWTXT_FILENAME: process.env.TWTKPR_TWTXT_FILENAME || process.env.TWTXT_FILENAME,
TWTKPR_POST_LIMITER_ACTIVE: process.env.TWTKPR_POST_LIMITER_ACTIVE ||
process.env.POST_LIMITER_ACTIVE,
TWTKPR_POST_LIMITER_WINDOW_MS: process.env.TWTKPR_POST_LIMITER_WINDOW_MS ||
process.env.POST_LIMITER_WINDOW_MS,
TWTKPR_POST_LIMITER_LIMIT: process.env.TWTKPR_POST_LIMITER_LIMIT || process.env.POST_LIMITER_LIMIT,
TWTKPR_POST_LIMITER_MESSAGE: process.env.TWTKPR_POST_LIMITER_MESSAGE ||
process.env.POST_LIMITER_MESSAGE,
TWTKPR_POST_LIMITER_STATUS_CODE: process.env.TWTKPR_POST_LIMITER_STATUS_CODE ||
process.env.POST_LIMITER_STATUS_CODE,
TWTKPR_POST_LIMITER_HANDLER: process.env.TWTKPR_POST_LIMITER_HANDLER ||
process.env.POST_LIMITER_HANDLER,
TWTKPR_POST_LIMITER_LEGACY_HEADERS: process.env.TWTKPR_POST_LIMITER_LEGACY_HEADERS ||
process.env.POST_LIMITER_LEGACY_HEADERS,
TWTKPR_POST_LIMITER_STANDARD_HEADERS: process.env.TWTKPR_POST_LIMITER_STANDARD_HEADERS ||
process.env.POST_LIMITER_STANDARD_HEADERS,
TWTKPR_POST_LIMITER_IDENTIFIER: process.env.TWTKPR_POST_LIMITER_IDENTIFIER ||
process.env.POST_LIMITER_IDENTIFIER,
TWTKPR_POST_LIMITER_STORE: process.env.TWTKPR_POST_LIMITER_STORE || process.env.POST_LIMITER_STORE,
TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR: process.env.TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR ||
process.env.POST_LIMITER_PASS_ON_STORE_ERROR,
TWTKPR_POST_LIMITER_KEY_GENERATOR: process.env.TWTKPR_POST_LIMITER_KEY_GENERATOR ||
process.env.POST_LIMITER_KEY_GENERATOR,
TWTKPR_POST_LIMITER_IPV6_SUBNET: process.env.TWTKPR_POST_LIMITER_IPV6_SUBNET ||
process.env.POST_LIMITER_IPV6_SUBNET,
TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME: process.env.TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME ||
process.env.POST_LIMITER_REQUEST_PROPERTY_NAME,
TWTKPR_POST_LIMITER_SKIP: process.env.TWTKPR_POST_LIMITER_SKIP || process.env.POST_LIMITER_SKIP,
TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS: process.env.TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS ||
process.env.POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS,
TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS: process.env.TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS ||
process.env.POST_LIMITER_SKIP_FAILED_REQUESTS,
TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL: process.env.TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL ||
process.env.POST_LIMITER_REQUEST_WAS_SUCCESSFUL,
TWTKPR_POST_LIMITER_VALIDATE: process.env.TWTKPR_POST_LIMITER_VALIDATE ||
process.env.POST_LIMITER_VALIDATE,
TWTKPR_QUERY_PARAMETER_APP: process.env.TWTKPR_QUERY_PARAMETER_APP ||
process.env.QUERY_PARAMETER_APP,
TWTKPR_QUERY_PARAMETER_CSS: process.env.TWTKPR_QUERY_PARAMETER_CSS ||
process.env.QUERY_PARAMETER_CSS,
TWTKPR_QUERY_PARAMETER_FOLLOWING: process.env.TWTKPR_QUERY_PARAMETER_FOLLOWING ||
process.env.QUERY_PARAMETER_FOLLOWING,
TWTKPR_QUERY_PARAMETER_JS: process.env.TWTKPR_QUERY_PARAMETER_JS || process.env.QUERY_PARAMETER_JS,
TWTKPR_QUERY_PARAMETER_LOGOUT: process.env.TWTKPR_QUERY_PARAMETER_LOGOUT ||
process.env.QUERY_PARAMETER_LOGOUT,
TWTKPR_QUERY_PARAMETER_METADATA: process.env.TWTKPR_QUERY_PARAMETER_METADATA ||
process.env.QUERY_PARAMETER_METADATA,
TWTKPR_QUERY_PARAMETER_TWT: process.env.TWTKPR_QUERY_PARAMETER_TWT ||
process.env.QUERY_PARAMETER_TWT,
TWTKPR_QUERY_PARAMETER_TWTS: process.env.TWTKPR_QUERY_PARAMETER_TWTS ||
process.env.QUERY_PARAMETER_TWTS,
TWTKPR_UPLOAD_ACTIVE: process.env.TWTKPR_UPLOAD_ACTIVE || process.env.UPLOAD_ACTIVE,
TWTKPR_UPLOAD_ALLOW_EMPTY_FILES: process.env.TWTKPR_UPLOAD_ALLOW_EMPTY_FILES ||
process.env.UPLOAD_ALLOW_EMPTY_FILES,
TWTKPR_UPLOAD_ALLOWED_MIME_TYPES: process.env.TWTKPR_UPLOAD_ALLOWED_MIME_TYPES ||
process.env.UPLOAD_ALLOWED_MIME_TYPES,
TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS: process.env.TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS ||
process.env.UPLOAD_CREATE_DIRS_FROM_UPLOADS,
TWTKPR_UPLOAD_DIRECTORY: process.env.TWTKPR_UPLOAD_DIRECTORY ||
process.env.UPLOAD_DIRECTORY ||
process.env.UPLOAD_DIR,
TWTKPR_UPLOAD_ENCODING: process.env.TWTKPR_UPLOAD_ENCODING || process.env.UPLOAD_ENCODING,
TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER: process.env.TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER ||
process.env.UPLOAD_FILE_WRITE_STREAM_HANDLER,
TWTKPR_UPLOAD_FILENAME: process.env.TWTKPR_UPLOAD_FILENAME || process.env.UPLOAD_FILENAME,
TWTKPR_UPLOAD_FILTER: process.env.TWTKPR_UPLOAD_FILTER || process.env.UPLOAD_FILTER,
TWTKPR_UPLOAD_HASH_ALGORITHM: process.env.TWTKPR_UPLOAD_HASH_ALGORITHM ||
process.env.UPLOAD_HASH_ALGORITHM,
TWTKPR_UPLOAD_KEEP_EXTENSIONS: process.env.TWTKPR_UPLOAD_KEEP_EXTENSIONS ||
process.env.UPLOAD_KEEP_EXTENSIONS,
TWTKPR_UPLOAD_MAX_FIELDS: process.env.TWTKPR_UPLOAD_MAX_FIELDS || process.env.UPLOAD_MAX_FIELDS,
TWTKPR_UPLOAD_MAX_FIELDS_SIZE: process.env.TWTKPR_UPLOAD_MAX_FIELDS_SIZE ||
process.env.UPLOAD_MAX_FIELDS_SIZE,
TWTKPR_UPLOAD_MAX_FILE_SIZE: process.env.TWTKPR_UPLOAD_MAX_FILE_SIZE ||
process.env.UPLOAD_MAX_FILE_SIZE,
TWTKPR_UPLOAD_MAX_FILES: process.env.TWTKPR_UPLOAD_MAX_FILES || process.env.UPLOAD_MAX_FILES,
TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE: process.env.TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE ||
process.env.UPLOAD_MAX_TOTAL_FILE_SIZE,
TWTKPR_UPLOAD_MIN_FILE_SIZE: process.env.TWTKPR_UPLOAD_MIN_FILE_SIZE ||
process.env.UPLOAD_MIN_FILE_SIZE,
TWTKPR_UPLOAD_ROUTE: process.env.TWTKPR_UPLOAD_ROUTE || process.env.UPLOAD_ROUTE,
});
if (!parsedEnv.TWTKPR_ACCESS_SECRET)
throw new Error('Either ACCESS_SECRET or TWTKPR_ACCESS_SECRET must be provided');
if (!parsedEnv.TWTKPR_REFRESH_SECRET)
throw new Error('Either REFRESH_SECRET or TWTKPR_REFRESH_SECRET must be provided');
return parsedEnv;
}
catch (error) {
if (error instanceof z.ZodError) {
console.error('Missing environment variables:', error.issues.flatMap((issue) => `${issue.path} or TWTKPR_${issue.path}`));
}
else {
console.error(error);
}
process.exit(1);
}
};
export const env = parseEnv();
export const __dirname = resolve(dirname(fileURLToPath(import.meta.url)), '..');
//# sourceMappingURL=env.js.map

1
dist/src/lib/env.js.map vendored Normal file

File diff suppressed because one or more lines are too long

7
dist/src/lib/getConfiguration.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
import { TwtKprConfiguration, TwtKprPluginConfiguration } from '../types.js';
/**
*
* @param initialConfiguration
* @returns
*/
export default function getConfiguration(initialConfiguration: TwtKprPluginConfiguration): TwtKprConfiguration;

103
dist/src/lib/getConfiguration.js vendored Normal file
View File

@@ -0,0 +1,103 @@
import { env } from './env.js';
/**
*
* @param allowedMimeTypes
* @returns
*/
const getDestinationByMimeTypeConfiguration = (allowedMimeTypes) => {
const fallback = {
audio: {
directory: 'audio',
rename: false,
},
image: {
directory: 'images',
rename: true,
},
video: {
directory: 'videos',
rename: true,
},
'*': {
directory: 'files',
rename: false,
},
};
const mimeTypeArrayReducer = (acc, curr) => {
if (fallback[curr])
acc[curr] = fallback[curr];
else
acc[curr] = {
directory: `${curr}s`,
rename: false,
};
return acc;
};
if (!allowedMimeTypes)
return fallback;
if (typeof allowedMimeTypes === 'string')
return allowedMimeTypes
.split(',')
.map((val) => val.trim())
.reduce(mimeTypeArrayReducer, {});
if (Array.isArray(allowedMimeTypes))
return allowedMimeTypes.reduce(mimeTypeArrayReducer, {});
if (typeof allowedMimeTypes === 'object')
return allowedMimeTypes;
return fallback;
};
/**
*
* @param initialConfiguration
* @returns
*/
export default function getConfiguration(initialConfiguration) {
const { mainRoute = env.TWTKPR_DEFAULT_ROUTE, privateDirectory = env.TWTKPR_PRIVATE_DIRECTORY, publicDirectory = env.TWTKPR_PUBLIC_DIRECTORY, twtxtFilename = env.TWTKPR_TWTXT_FILENAME, postLimiterConfiguration, queryParameters, uploadConfiguration, } = initialConfiguration ?? {};
const { active: postLimiterActive = env.TWTKPR_POST_LIMITER_ACTIVE, ...otherPostLimiterProps } = postLimiterConfiguration ?? {};
const { app = env.TWTKPR_QUERY_PARAMETER_APP, css = env.TWTKPR_QUERY_PARAMETER_CSS, following = env.TWTKPR_QUERY_PARAMETER_FOLLOWING, js = env.TWTKPR_QUERY_PARAMETER_JS, logout = env.TWTKPR_QUERY_PARAMETER_LOGOUT, metadata = env.TWTKPR_QUERY_PARAMETER_METADATA, twt = env.TWTKPR_QUERY_PARAMETER_TWT, twts = env.TWTKPR_QUERY_PARAMETER_TWTS, } = queryParameters ?? {};
const { active: uploadActive = env.TWTKPR_UPLOAD_ACTIVE, allowEmptyFiles = env.TWTKPR_UPLOAD_ALLOW_EMPTY_FILES, allowedMimeTypes = env.TWTKPR_UPLOAD_ALLOWED_MIME_TYPES, createDirsFromUploads = env.TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS, directory = env.TWTKPR_UPLOAD_DIRECTORY, encoding = env.TWTKPR_UPLOAD_ENCODING, fileWriteStreamHandler, filter = () => true, hashAlgorithm = env.TWTKPR_UPLOAD_HASH_ALGORITHM, keepExtensions = env.TWTKPR_UPLOAD_KEEP_EXTENSIONS, maxFields = env.TWTKPR_UPLOAD_MAX_FIELDS, maxFileSize = env.TWTKPR_UPLOAD_MAX_FIELDS_SIZE, maxFiles = env.TWTKPR_UPLOAD_MAX_FILES, maxTotalFileSize = env.TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE, minFileSize = env.TWTKPR_UPLOAD_MIN_FILE_SIZE, route = env.TWTKPR_UPLOAD_ROUTE, } = uploadConfiguration ?? {};
return {
// secrets cannot be provided through configuration file, must use ENV / .env
accessSecret: env.TWTKPR_ACCESS_SECRET,
refreshSecret: env.TWTKPR_REFRESH_SECRET,
mainRoute,
privateDirectory,
publicDirectory,
twtxtFilename,
postLimiterConfiguration: {
active: postLimiterActive,
...(otherPostLimiterProps ?? {}),
},
queryParameters: {
...queryParameters,
app,
css,
following,
js,
logout,
metadata,
twt,
twts,
},
uploadConfiguration: {
...uploadConfiguration,
active: uploadActive,
allowEmptyFiles,
allowedMimeTypes: getDestinationByMimeTypeConfiguration(allowedMimeTypes),
createDirsFromUploads,
directory,
encoding,
fileWriteStreamHandler,
filter,
hashAlgorithm: hashAlgorithm,
keepExtensions,
maxFields,
maxFileSize,
maxFiles,
maxTotalFileSize,
minFileSize,
route,
},
};
}
//# sourceMappingURL=getConfiguration.js.map

1
dist/src/lib/getConfiguration.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"getConfiguration.js","sourceRoot":"","sources":["../../../src/lib/getConfiguration.ts"],"names":[],"mappings":"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"}

13
dist/src/lib/refreshTokensDB.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
export interface RefreshTokensDB {
cleanUp: () => void;
get: (key: string) => string[];
getObject: () => Record<string, string[]>;
remove: (key?: string) => void;
set: (key?: string, value?: string[]) => string[];
}
/**
*
* @param directory
* @returns
*/
export default function refreshTokensDB(directory: string): Promise<RefreshTokensDB>;

40
dist/src/lib/refreshTokensDB.js vendored Normal file
View File

@@ -0,0 +1,40 @@
import arrayDB from './arrayDB.js';
import Debug from 'debug';
import jwt from 'jsonwebtoken';
const debug = Debug('twtkpr:simpleDB');
/**
*
* @param directory
* @returns
*/
export default async function refreshTokensDB(directory) {
const refreshTokensDB = await arrayDB('refreshTokens', directory);
const get = (key) => {
const currentTime = Math.floor(Date.now() / 1000);
debug('get', key, currentTime);
return (refreshTokensDB.get(key) ?? []).filter((token) => {
const val = jwt.decode(token);
return val && (val.exp ?? 0) >= currentTime;
});
};
const cleanUp = () => {
const currentTime = Math.floor(Date.now() / 1000);
const tokenListByUserId = refreshTokensDB.getObject();
debug('cleanup', currentTime);
Object.keys(tokenListByUserId).forEach((userId) => {
const tokens = refreshTokensDB.get(userId).filter((token) => {
const val = jwt.decode(token);
return val && (val.exp ?? 0) >= currentTime;
});
debug(`setting tokens for ${userId}`, tokens);
refreshTokensDB.set(userId, tokens);
});
};
cleanUp();
return {
...refreshTokensDB,
cleanUp,
get,
};
}
//# sourceMappingURL=refreshTokensDB.js.map

1
dist/src/lib/refreshTokensDB.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"refreshTokensDB.js","sourceRoot":"","sources":["../../../src/lib/refreshTokensDB.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,GAAG,MAAM,cAAc,CAAC;AAU/B,MAAM,KAAK,GAAG,KAAK,CAAC,iBAAiB,CAAC,CAAC;AAEvC;;;;GAIG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,eAAe,CAAC,SAAiB;IAC9D,MAAM,eAAe,GAAG,MAAM,OAAO,CAAC,eAAe,EAAE,SAAS,CAAC,CAAC;IAElE,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE;QAC3B,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAClD,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;QAE/B,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YACxD,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC9B,OAAO,GAAG,IAAI,CAAE,GAAsB,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,WAAW,CAAC;QACjE,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,MAAM,OAAO,GAAG,GAAG,EAAE;QACpB,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAClD,MAAM,iBAAiB,GAAG,eAAe,CAAC,SAAS,EAAE,CAAC;QAEtD,KAAK,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QAE9B,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE;YACjD,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;gBAC3D,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC9B,OAAO,GAAG,IAAI,CAAE,GAAsB,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,WAAW,CAAC;YACjE,CAAC,CAAC,CAAC;YAEH,KAAK,CAAC,sBAAsB,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC;YAE9C,eAAe,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC;IAEF,OAAO,EAAE,CAAC;IAEV,OAAO;QACN,GAAG,eAAe;QAClB,OAAO;QACP,GAAG;KACgB,CAAC;AACtB,CAAC"}

12
dist/src/lib/simpleDB.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
/**
*
* @param name
* @param directory
* @returns
*/
export default function simpleDB(name: string, directory: string): Promise<{
get: (key?: string) => string;
getObject: () => Record<string, string>;
remove: (key?: string) => void;
set: (key?: string, value?: string) => string;
}>;

71
dist/src/lib/simpleDB.js vendored Normal file
View File

@@ -0,0 +1,71 @@
import Debug from 'debug';
import path from 'node:path';
import { loadObjectFromJson, saveToJson } from './utils.js';
const debug = Debug('twtkpr:simpleDB');
/**
*
* @param name
* @param directory
* @returns
*/
export default async function simpleDB(name, directory) {
let theName;
let dataObject;
const get = (key = '') => {
debug('get', { key });
if (!theName || !dataObject)
throw new Error('DB must be initialized first');
key = key?.trim();
if (!key)
throw new Error('a valid key must be provided');
return dataObject[key];
};
const getObject = () => dataObject;
const initialize = async (dbName = '') => {
debug('initialize starting', { dbName });
dbName = dbName?.trim();
if (!dbName)
throw new Error('a valid name must be provided');
try {
dataObject = await loadObjectFromJson(path.join(directory, `${dbName}.json`));
}
catch (err) {
debug('initialize read error', { err });
if (err.code === 'ENOENT')
dataObject = {};
else
throw err;
}
// only initialize (and set name) if everything passes
theName = dbName;
debug('initialize complete', { dataObject, name: theName });
};
const remove = (key = '') => {
debug('remove', { key });
if (!theName || !dataObject)
throw new Error('DB must be initialized first');
key = key?.trim();
if (!key)
throw new Error('a valid key must be provided');
delete dataObject[key];
};
const set = (key = '', value = '') => {
debug('set', { key });
if (!theName || !dataObject)
throw new Error('DB must be initialized first');
key = key?.trim();
if (!key)
throw new Error('a valid key must be provided');
dataObject[key] = value;
saveToJson(dataObject, path.join(directory, `${name}.json`));
return value;
};
await initialize(name);
return {
get,
getObject,
remove,
set,
};
}
//# sourceMappingURL=simpleDB.js.map

1
dist/src/lib/simpleDB.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"simpleDB.js","sourceRoot":"","sources":["../../../src/lib/simpleDB.ts"],"names":[],"mappings":"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"}

11
dist/src/lib/twtxtCache.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
import { NodeCache } from '@cacheable/node-cache';
import { TwtKprConfiguration } from '../types.js';
/**
*
* @param param0
* @returns
*/
export default function twtxtCache({ publicDirectory, twtxtFilename, }: Pick<TwtKprConfiguration, 'publicDirectory' | 'twtxtFilename'>): {
cache: NodeCache<unknown>;
reloadCache: () => Promise<void>;
};

31
dist/src/lib/twtxtCache.js vendored Normal file
View File

@@ -0,0 +1,31 @@
import fsp from 'node:fs/promises';
import path from 'node:path';
import { NodeCache } from '@cacheable/node-cache';
import Debug from 'debug';
import { parseTwtxt } from 'twtxt-lib';
/**
*
* @param param0
* @returns
*/
export default function twtxtCache({ publicDirectory, twtxtFilename, }) {
let isLoaded = false;
const debug = Debug('twtkpr:twtxtCache');
const cache = new NodeCache();
const reloadCache = async () => {
const fileText = await fsp.readFile(path.join(publicDirectory, twtxtFilename), 'utf8');
const parsedFile = parseTwtxt(fileText);
Object.keys(parsedFile).forEach((key) => {
cache.set(key, parsedFile[key]); // 10 seconds
});
cache.set('source', fileText);
debug(`cache ${isLoaded ? 're' : ''}loaded`);
isLoaded = true;
};
reloadCache();
return {
cache,
reloadCache,
};
}
//# sourceMappingURL=twtxtCache.js.map

1
dist/src/lib/twtxtCache.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"twtxtCache.js","sourceRoot":"","sources":["../../../src/lib/twtxtCache.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,kBAAkB,CAAC;AACnC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAGvC;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAClC,eAAe,EACf,aAAa,GACmD;IAChE,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,MAAM,KAAK,GAAG,KAAK,CAAC,mBAAmB,CAAC,CAAC;IAEzC,MAAM,KAAK,GAAG,IAAI,SAAS,EAAE,CAAC;IAE9B,MAAM,WAAW,GAAG,KAAK,IAAI,EAAE;QAC9B,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,QAAQ,CAClC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,aAAa,CAAC,EACzC,MAAM,CACN,CAAC;QAEF,MAAM,UAAU,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;YACvC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,GAA8B,CAAC,CAAC,CAAC,CAAC,aAAa;QAC1E,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC9B,KAAK,CAAC,SAAS,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAE7C,QAAQ,GAAG,IAAI,CAAC;IACjB,CAAC,CAAC;IAEF,WAAW,EAAE,CAAC;IAEd,OAAO;QACN,KAAK;QACL,WAAW;KACX,CAAC;AACH,CAAC"}

12
dist/src/lib/userDB.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
export interface UserDB {
get: (key?: string) => string;
getObject: () => Record<string, string>;
remove: (key?: string) => void;
set: (key?: string, value?: string) => string;
}
/**
*
* @param directory
* @returns
*/
export default function userDB(directory: string): Promise<UserDB>;

10
dist/src/lib/userDB.js vendored Normal file
View File

@@ -0,0 +1,10 @@
import simpleDB from './simpleDB.js';
/**
*
* @param directory
* @returns
*/
export default function userDB(directory) {
return simpleDB('user', directory);
}
//# sourceMappingURL=userDB.js.map

1
dist/src/lib/userDB.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"userDB.js","sourceRoot":"","sources":["../../../src/lib/userDB.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,eAAe,CAAC;AASrC;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,MAAM,CAAC,SAAiB;IAC/C,OAAO,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAoB,CAAC;AACvD,CAAC"}

45
dist/src/lib/utils.d.ts vendored Normal file
View File

@@ -0,0 +1,45 @@
/**
*
* @param userId
* @param secret
* @returns
*/
export declare const generateAccessToken: (userId: string, secret?: string) => string;
/**
*
* @param val
* @returns
*/
export declare const generateEtag: (val: string) => string;
/**
*
* @param userId
* @param secret
* @param extendRefresh
* @returns
*/
export declare const generateRefreshToken: (userId: string, secret?: string, extendRefresh?: boolean) => string;
/**
*
* @param value
* @returns
*/
export declare const getQueryParameterArray: (value?: unknown | unknown[]) => string[];
/**
*
* @param value
* @returns
*/
export declare const getValueOrFirstEntry: (value: string | string[]) => string | string[];
/**
*
* @param filePath
* @returns
*/
export declare const loadObjectFromJson: (filePath: string) => Promise<any>;
/**
*
* @param contents
* @param filePath
*/
export declare const saveToJson: (contents: object | string, filePath: string) => Promise<void>;

67
dist/src/lib/utils.js vendored Normal file
View File

@@ -0,0 +1,67 @@
import crypto from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
/**
*
* @param userId
* @param secret
* @returns
*/
export const generateAccessToken = (userId, secret = '') => jwt.sign({ id: userId }, secret, { expiresIn: '10m' });
/**
*
* @param val
* @returns
*/
export const generateEtag = (val) => crypto.createHash('sha256').update(val).digest('hex');
/**
*
* @param userId
* @param secret
* @param extendRefresh
* @returns
*/
export const generateRefreshToken = (userId, secret = '', extendRefresh = false) => {
const tokenId = uuidv4(); // unique ID for the refresh token
const token = jwt.sign({ id: userId, tokenId }, secret, {
expiresIn: extendRefresh ? '7d' : '1h',
});
return token;
};
/**
*
* @param value
* @returns
*/
export const getQueryParameterArray = (value = []) => Array.isArray(value)
? value.map((val) => `${val}`.trim())
: [`${value}`.trim()];
/**
*
* @param value
* @returns
*/
export const getValueOrFirstEntry = (value) => Array.isArray(value) && value.length ? value[0] : value;
/**
*
* @param filePath
* @returns
*/
export const loadObjectFromJson = async (filePath) => {
const contents = await readFile(filePath, { encoding: 'utf8' });
return JSON.parse(contents);
};
/**
*
* @param contents
* @param filePath
*/
export const saveToJson = async (contents, filePath) => {
const stringContents = typeof contents === 'string' ? contents : JSON.stringify(contents, null, 2);
await writeFile(filePath, stringContents, {
encoding: 'utf8',
flag: 'w',
});
};
//# sourceMappingURL=utils.js.map

1
dist/src/lib/utils.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/lib/utils.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,GAAG,MAAM,cAAc,CAAC;AAC/B,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,MAAM,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAc,EAAE,MAAM,GAAG,EAAE,EAAE,EAAE,CAClE,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;AAExD;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,GAAW,EAAE,EAAE,CAC3C,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CACnC,MAAc,EACd,MAAM,GAAG,EAAE,EACX,aAAa,GAAG,KAAK,EACpB,EAAE;IACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,CAAC,kCAAkC;IAE5D,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE;QACvD,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;KACtC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACd,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,QAA6B,EAAE,EAAE,EAAE,CACzE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;IACnB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC;IACrC,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AAExB;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,KAAwB,EAAE,EAAE,CAChE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AAEzD;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EAAE,QAAgB,EAAE,EAAE;IAC5D,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAChE,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;AAC7B,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC9B,QAAyB,EACzB,QAAgB,EACf,EAAE;IACH,MAAM,cAAc,GACnB,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAE7E,MAAM,SAAS,CAAC,QAAQ,EAAE,cAAc,EAAE;QACzC,QAAQ,EAAE,MAAM;QAChB,IAAI,EAAE,GAAG;KACT,CAAC,CAAC;AACJ,CAAC,CAAC"}

View File

@@ -0,0 +1,9 @@
import type { Request } from 'express';
import { TwtKprConfiguration } from '../types.js';
/**
* Checks for a valid JWT, and returns a boolean indicating the result
*
* @param req
* @returns
*/
export default function authCheckJWT(req: Request, config: TwtKprConfiguration): Promise<boolean>;

32
dist/src/middlewares/authCheckJWT.js vendored Normal file
View File

@@ -0,0 +1,32 @@
import Debug from 'debug';
import jwt from 'jsonwebtoken';
const debug = Debug('twtkpr:authCheckJWT');
/**
* Checks for a valid JWT, and returns a boolean indicating the result
*
* @param req
* @returns
*/
export default async function authCheckJWT(req, config) {
debug('beginning');
const token = req.header('Authorization')?.split(' ')[1];
if (!token) {
debug('no token');
return false;
}
debug('token present');
try {
const decoded = jwt.verify(token, config.accessSecret);
debug({ decoded });
if (!decoded.id)
return false;
req.username = decoded.id;
}
catch {
debug('invalid token');
return false;
}
debug('token good');
return true;
}
//# sourceMappingURL=authCheckJWT.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"authCheckJWT.js","sourceRoot":"","sources":["../../../src/middlewares/authCheckJWT.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,GAAG,MAAM,cAAc,CAAC;AAI/B,MAAM,KAAK,GAAG,KAAK,CAAC,qBAAqB,CAAC,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,YAAY,CACzC,GAAY,EACZ,MAA2B;IAE3B,KAAK,CAAC,WAAW,CAAC,CAAC;IAEnB,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACzD,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,KAAK,CAAC,UAAU,CAAC,CAAC;QAClB,OAAO,KAAK,CAAC;IACd,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,CAAC;IAEvB,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,YAAY,CAAmB,CAAC;QACzE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;QAEnB,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,OAAO,KAAK,CAAC;QAC9B,GAAG,CAAC,QAAQ,GAAG,OAAO,CAAC,EAAE,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACR,KAAK,CAAC,eAAe,CAAC,CAAC;QACvB,OAAO,KAAK,CAAC;IACd,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,CAAC;IACpB,OAAO,IAAI,CAAC;AACb,CAAC"}

View File

@@ -0,0 +1 @@
export {};

27
dist/src/middlewares/csrfProtection.js vendored Normal file
View File

@@ -0,0 +1,27 @@
export {};
/*
import { doubleCsrf } from "csrf-csrf";
const {
invalidCsrfTokenError, // This is just for convenience if you plan on making your own middleware.
generateCsrfToken, // Use this in your routes to provide a CSRF token.
validateRequest, // Also a convenience if you plan on making your own middleware.
doubleCsrfProtection, // This is the default CSRF protection middleware.
} = doubleCsrf({
getSecret: (req) => 'return some cryptographically pseudorandom secret here',
getSessionIdentifier: (req) => req.session.id // return the requests unique identifier
});
const csrfTokenRoute = (req, res) => {
const csrfToken = generateCsrfToken(req, res);
// You could also pass the token into the context of a HTML response.
res.json({ csrfToken });
};
export {
csrfTokenRoute,
doubleCsrfProtection,
}
*/
//# sourceMappingURL=csrfProtection.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"csrfProtection.js","sourceRoot":"","sources":["../../../src/middlewares/csrfProtection.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;EAwBE"}

4
dist/src/middlewares/index.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
export { default as authCheck } from './authCheckJWT.js';
export { default as memoryCache } from './postHandler/memoryCache.js';
export { default as postHandler } from './postHandler/index.js';
export { default as queryHandler } from './queryHandler/index.js';

5
dist/src/middlewares/index.js vendored Normal file
View File

@@ -0,0 +1,5 @@
export { default as authCheck } from './authCheckJWT.js';
export { default as memoryCache } from './postHandler/memoryCache.js';
export { default as postHandler } from './postHandler/index.js';
export { default as queryHandler } from './queryHandler/index.js';
//# sourceMappingURL=index.js.map

1
dist/src/middlewares/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/middlewares/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,yBAAyB,CAAC"}

View File

@@ -0,0 +1 @@
export { default } from "./postHandler.js";

View File

@@ -0,0 +1,2 @@
export { default } from "./postHandler.js";
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC"}

View File

@@ -0,0 +1,10 @@
import type { Request, Response } from 'express';
import { TwtKprConfiguration } from '../../types.js';
/**
* Handles login request and (if successful) returns the JWT access token wile setting the refresh n the
*
* @param req
* @param res
* @returns
*/
export default function loginHandler(req: Request, res: Response, config: TwtKprConfiguration): Promise<void>;

View File

@@ -0,0 +1,67 @@
import bcrypt from 'bcryptjs';
import Debug from 'debug';
import { env } from '../../lib/env.js';
import refreshTokensDB from '../../lib/refreshTokensDB.js';
import userDB from '../../lib/userDB.js';
import { generateAccessToken, generateEtag, generateRefreshToken, } from '../../lib/utils.js';
const debug = Debug('twtkpr:login');
/**
* Handles login request and (if successful) returns the JWT access token wile setting the refresh n the
*
* @param req
* @param res
* @returns
*/
export default async function loginHandler(req, res, config) {
const { accessSecret, privateDirectory, refreshSecret } = config;
debug('starting');
try {
const tokens = await refreshTokensDB(privateDirectory);
const users = await userDB(privateDirectory);
const { username, password, rememberToggle } = req.body;
if (!username || !password || !users.get(username)) {
debug('no values found', username);
res.status(401).end();
return;
}
const isMatch = await bcrypt.compare(password, users.get(username));
if (!isMatch) {
privateDirectory;
debug('no match');
res.status(401).end();
return;
}
debug('generating tokens');
const accessToken = generateAccessToken(username, accessSecret);
debug(`access token: ${accessToken}`);
const refreshToken = generateRefreshToken(username, refreshSecret, !!rememberToggle);
debug(`refresh token: ${refreshToken}`);
debug('setting tokens');
tokens.set(username, (tokens.get(username) || []).concat([refreshToken]));
debug('setting refreshToken cookie');
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: env.NODE_ENV === 'production',
sameSite: 'strict',
// 1 hour or 7 days
maxAge: (rememberToggle ? 1 : 7 * 24) * 60 * 60 * 1000,
});
if (rememberToggle) {
debug('setting accessToken cookie');
/*
res.cookie('accessToken', accessToken, {
httpOnly: false,
secure: env.NODE_ENV === 'production',
sameSite: 'strict',
});
*/
}
debug('setting response');
res.set('etag', generateEtag(accessToken)).status(200).send(accessToken);
}
catch (err) {
console.error(err);
res.status(500).end();
}
}
//# sourceMappingURL=login.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/login.ts"],"names":[],"mappings":"AAEA,OAAO,MAAM,MAAM,UAAU,CAAC;AAC9B,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,eAAoC,MAAM,8BAA8B,CAAC;AAChF,OAAO,MAAkB,MAAM,qBAAqB,CAAC;AACrD,OAAO,EACN,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,GACpB,MAAM,oBAAoB,CAAC;AAG5B,MAAM,KAAK,GAAG,KAAK,CAAC,cAAc,CAAC,CAAC;AAEpC;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,YAAY,CACzC,GAAY,EACZ,GAAa,EACb,MAA2B;IAE3B,MAAM,EAAE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,GAAG,MAAM,CAAC;IACjE,KAAK,CAAC,UAAU,CAAC,CAAC;IAElB,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,gBAAgB,CAAC,CAAC;QACvD,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC;QAE7C,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,cAAc,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC;QAExD,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YACpD,KAAK,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;YAEnC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;YACtB,OAAO;QACR,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;QAEpE,IAAI,CAAC,OAAO,EAAE,CAAC;YACd,gBAAgB,CAAC;YACjB,KAAK,CAAC,UAAU,CAAC,CAAC;YAElB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;YACtB,OAAO;QACR,CAAC;QAED,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAE3B,MAAM,WAAW,GAAG,mBAAmB,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QAChE,KAAK,CAAC,iBAAiB,WAAW,EAAE,CAAC,CAAC;QAEtC,MAAM,YAAY,GAAG,oBAAoB,CACxC,QAAQ,EACR,aAAa,EACb,CAAC,CAAC,cAAc,CAChB,CAAC;QACF,KAAK,CAAC,kBAAkB,YAAY,EAAE,CAAC,CAAC;QAExC,KAAK,CAAC,gBAAgB,CAAC,CAAC;QACxB,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAE1E,KAAK,CAAC,6BAA6B,CAAC,CAAC;QACrC,GAAG,CAAC,MAAM,CAAC,cAAc,EAAE,YAAY,EAAE;YACxC,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,GAAG,CAAC,QAAQ,KAAK,YAAY;YACrC,QAAQ,EAAE,QAAQ;YAClB,mBAAmB;YACnB,MAAM,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;SACtD,CAAC,CAAC;QAEH,IAAI,cAAc,EAAE,CAAC;YACpB,KAAK,CAAC,4BAA4B,CAAC,CAAC;YACpC;;;;;;cAME;QACH,CAAC;QAED,KAAK,CAAC,kBAAkB,CAAC,CAAC;QAC1B,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC1E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IACvB,CAAC;AACF,CAAC"}

View File

@@ -0,0 +1,10 @@
import type { Request, Response } from 'express';
import { TwtKprConfiguration } from '../../types.js';
/**
* Handles logout request and clears the token cookies
*
* @param req
* @param res
* @returns
*/
export default function logoutHandler(req: Request, res: Response, config: TwtKprConfiguration): Promise<void>;

View File

@@ -0,0 +1,20 @@
import Debug from 'debug';
const debug = Debug('twtkpr:logout');
/**
* Handles logout request and clears the token cookies
*
* @param req
* @param res
* @returns
*/
export default async function logoutHandler(req, res, config) {
const { mainRoute } = config;
debug('logging out');
res
.status(200)
.clearCookie('refreshToken')
.clearCookie('accessToken')
.redirect(mainRoute);
return;
}
//# sourceMappingURL=logout.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"logout.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/logout.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,MAAM,KAAK,GAAG,KAAK,CAAC,eAAe,CAAC,CAAC;AAErC;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,aAAa,CAC1C,GAAY,EACZ,GAAa,EACb,MAA2B;IAE3B,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAC7B,KAAK,CAAC,aAAa,CAAC,CAAC;IAErB,GAAG;SACD,MAAM,CAAC,GAAG,CAAC;SACX,WAAW,CAAC,cAAc,CAAC;SAC3B,WAAW,CAAC,aAAa,CAAC;SAC1B,QAAQ,CAAC,SAAS,CAAC,CAAC;IAEtB,OAAO;AACR,CAAC"}

View File

@@ -0,0 +1,12 @@
import type { NextFunction, Request, Response } from 'express';
import NodeCache from '@cacheable/node-cache';
/**
*
* @param req
* @param res
* @param next
* @param cache
* @param reloadCache
* @returns
*/
export default function memoryCache(req: Request, res: Response, next: NextFunction, cache: NodeCache<unknown>, reloadCache: () => Promise<void>): Promise<void>;

View File

@@ -0,0 +1,25 @@
import Debug from 'debug';
const debug = Debug('twtkpr:memoryCache');
/**
*
* @param req
* @param res
* @param next
* @param cache
* @param reloadCache
* @returns
*/
export default async function memoryCache(req, res, next, cache, reloadCache) {
if (cache.keys().length && !['DELETE', 'POST', 'PUT'].includes(req.method)) {
next();
return;
}
reloadCache()
.then(() => {
next();
})
.catch((err) => {
console.error(err);
});
}
//# sourceMappingURL=memoryCache.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"memoryCache.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/memoryCache.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,KAAK,GAAG,KAAK,CAAC,oBAAoB,CAAC,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,WAAW,CACxC,GAAY,EACZ,GAAa,EACb,IAAkB,EAClB,KAAyB,EACzB,WAAgC;IAEhC,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,IAAI,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5E,IAAI,EAAE,CAAC;QACP,OAAO;IACR,CAAC;IAED,WAAW,EAAE;SACX,IAAI,CAAC,GAAG,EAAE;QACV,IAAI,EAAE,CAAC;IACR,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACd,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC"}

View File

@@ -0,0 +1,7 @@
import { TwtKprConfiguration } from '../../types.js';
/**
*
* @param config
* @returns
*/
export default function postHandler(config: TwtKprConfiguration): import("express-serve-static-core").Router;

View File

@@ -0,0 +1,60 @@
import Debug from 'debug';
import express from 'express';
import rateLimit from 'express-rate-limit';
import authCheck from '../../middlewares/authCheckJWT.js';
import login from './login.js';
import logout from './logout.js';
import refresh from './refresh.js';
import twt from './twt.js';
import editFile from '../putHandler/editFile.js';
const debug = Debug('twtkpr:postHandler');
/**
*
* @param config
* @returns
*/
export default function postHandler(config) {
const { postLimiterConfiguration } = config;
const { active: isLimiterActive, ...otherLimiterProps } = postLimiterConfiguration ?? {};
const postLimiter = isLimiterActive
? rateLimit({
...otherLimiterProps,
})
: (req, res, next) => {
next();
};
const { mainRoute } = config;
const router = express.Router();
router.post('/', postLimiter, async (req, res, next) => {
const { content, type } = req.body ?? {};
debug('post', { type, path: req.path });
if (type === 'logout') {
debug('logging out');
res.clearCookie('refreshToken');
res.clearCookie('accessToken');
res.redirect(mainRoute);
return;
}
if (type === 'login')
return login(req, res, config);
if (type === 'logout')
return logout(req, res, config);
if (type === 'refresh')
return refresh(req, res, config);
debug('checking auth');
const isLoggedIn = await authCheck(req, config);
if (!isLoggedIn) {
debug('auth check failed');
next();
return;
}
debug('auth check succeeded');
if (type === 'twt' || content)
return twt(req, res, config);
if (type === 'editFile')
return editFile(req, res, config);
next();
});
return router;
}
//# sourceMappingURL=postHandler.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"postHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/postHandler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,OAA4C,MAAM,SAAS,CAAC;AACnE,OAAO,SAAS,MAAM,oBAAoB,CAAC;AAE3C,OAAO,SAAS,MAAM,mCAAmC,CAAC;AAE1D,OAAO,KAAK,MAAM,YAAY,CAAC;AAC/B,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,QAAQ,MAAM,2BAA2B,CAAC;AAEjD,MAAM,KAAK,GAAG,KAAK,CAAC,oBAAoB,CAAC,CAAC;AAE1C;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,MAA2B;IAC9D,MAAM,EAAE,wBAAwB,EAAE,GAAG,MAAM,CAAC;IAC5C,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,iBAAiB,EAAE,GACtD,wBAAwB,IAAI,EAAE,CAAC;IAEhC,MAAM,WAAW,GAAG,eAAe;QAClC,CAAC,CAAC,SAAS,CAAC;YACV,GAAG,iBAAiB;SACpB,CAAC;QACH,CAAC,CAAC,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;YACpD,IAAI,EAAE,CAAC;QACR,CAAC,CAAC;IAEJ,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAE7B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACtD,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;QACzC,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QAExC,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;YACvB,KAAK,CAAC,aAAa,CAAC,CAAC;YACrB,GAAG,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC;YAChC,GAAG,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC;YAC/B,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YACxB,OAAO;QACR,CAAC;QAED,IAAI,IAAI,KAAK,OAAO;YAAE,OAAO,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QACrD,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QACvD,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAEzD,KAAK,CAAC,eAAe,CAAC,CAAC;QACvB,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAChD,IAAI,CAAC,UAAU,EAAE,CAAC;YACjB,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC3B,IAAI,EAAE,CAAC;YACP,OAAO;QACR,CAAC;QACD,KAAK,CAAC,sBAAsB,CAAC,CAAC;QAE9B,IAAI,IAAI,KAAK,KAAK,IAAI,OAAO;YAAE,OAAO,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAC5D,IAAI,IAAI,KAAK,UAAU;YAAE,OAAO,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAE3D,IAAI,EAAE,CAAC;IACR,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AACf,CAAC"}

View File

@@ -0,0 +1,9 @@
import type { Request, Response } from 'express';
import { TwtKprConfiguration } from '../../types.js';
/**
* Issues a new JWT and updates the refresh token in the cookie
*
* @param req
* @param res
*/
export default function refresh(req: Request, res: Response, config: TwtKprConfiguration): Promise<void>;

View File

@@ -0,0 +1,78 @@
import Debug from 'debug';
import jwt from 'jsonwebtoken';
import { env } from '../../lib/env.js';
import refreshTokensDB from '../../lib/refreshTokensDB.js';
import { generateAccessToken, generateEtag, generateRefreshToken, } from '../../lib/utils.js';
const debug = Debug('twtkpr:refresh');
/**
* Issues a new JWT and updates the refresh token in the cookie
*
* @param req
* @param res
*/
export default async function refresh(req, res, config) {
const send401 = (message) => {
debug(message);
res
.clearCookie('accessToken')
.clearCookie('refreshToken')
.status(401)
.send(message ?? 'Unauthorized');
return;
};
try {
const tokens = await refreshTokensDB(config.privateDirectory);
const oldToken = req.cookies.refreshToken;
debug(oldToken);
if (!oldToken)
return send401('Unauthorized');
let decoded = { id: '' };
try {
decoded = jwt.verify(oldToken, config.refreshSecret);
debug({ decoded });
}
catch (err) {
return send401('Refresh token invalid');
}
const username = req.username ?? decoded.id;
if (!username)
return send401('Missing username');
const currentTime = Math.floor(Date.now() / 1000);
// cleanup tokens on load
const validTokens = (tokens.get(decoded.id) ?? []).filter((token) => {
const val = jwt.decode(token);
return val && (val.exp ?? 0) >= currentTime;
});
// If token is invalid or not the latest one
if (!validTokens.includes(oldToken)) {
debug('token missing from list');
return send401('Invalid refresh token');
}
debug('generating new tokens');
const newAccessToken = generateAccessToken(req.username || decoded.id, config.accessSecret);
const newRefreshToken = generateRefreshToken(req.username || decoded.id, config.refreshSecret);
debug('updating token list');
tokens.set(req.username || decoded.id, validTokens
.filter((token) => token !== oldToken)
.concat([newRefreshToken]));
debug('setting httpOnly cookie with new refresh token');
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: env.NODE_ENV === 'production',
sameSite: 'strict',
// 1 hour or 7 days
maxAge: (!!req.query.rememberToggle ? 1 : 7 * 24) * 60 * 60 * 1000,
});
// Return the new access token in body
debug('generating response');
res
.set('etag', generateEtag(newAccessToken))
.status(200)
.send(newAccessToken);
}
catch (err) {
console.error(err);
res.status(500).end();
}
}
//# sourceMappingURL=refresh.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"refresh.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/refresh.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,GAAG,MAAM,cAAc,CAAC;AAE/B,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,eAAe,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EACN,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,GACpB,MAAM,oBAAoB,CAAC;AAG5B,MAAM,KAAK,GAAG,KAAK,CAAC,gBAAgB,CAAC,CAAC;AAEtC;;;;;GAKG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,OAAO,CACpC,GAAY,EACZ,GAAa,EACb,MAA2B;IAE3B,MAAM,OAAO,GAAG,CAAC,OAAe,EAAE,EAAE;QACnC,KAAK,CAAC,OAAO,CAAC,CAAC;QAEf,GAAG;aACD,WAAW,CAAC,aAAa,CAAC;aAC1B,WAAW,CAAC,cAAc,CAAC;aAC3B,MAAM,CAAC,GAAG,CAAC;aACX,IAAI,CAAC,OAAO,IAAI,cAAc,CAAC,CAAC;QAElC,OAAO;IACR,CAAC,CAAC;IAEF,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;QAC9D,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC;QAE1C,KAAK,CAAC,QAAQ,CAAC,CAAC;QAEhB,IAAI,CAAC,QAAQ;YAAE,OAAO,OAAO,CAAC,cAAc,CAAC,CAAC;QAE9C,IAAI,OAAO,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;QAEzB,IAAI,CAAC;YACJ,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,aAAa,CAElD,CAAC;YAEF,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,OAAO,OAAO,CAAC,uBAAuB,CAAC,CAAC;QACzC,CAAC;QAED,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,OAAO,CAAC,EAAE,CAAC;QAE5C,IAAI,CAAC,QAAQ;YAAE,OAAO,OAAO,CAAC,kBAAkB,CAAC,CAAC;QAElD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAElD,yBAAyB;QACzB,MAAM,WAAW,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;YACnE,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC9B,OAAO,GAAG,IAAI,CAAE,GAAsB,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,WAAW,CAAC;QACjE,CAAC,CAAC,CAAC;QAEH,4CAA4C;QAC5C,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrC,KAAK,CAAC,yBAAyB,CAAC,CAAC;YACjC,OAAO,OAAO,CAAC,uBAAuB,CAAC,CAAC;QACzC,CAAC;QAED,KAAK,CAAC,uBAAuB,CAAC,CAAC;QAE/B,MAAM,cAAc,GAAG,mBAAmB,CACzC,GAAG,CAAC,QAAQ,IAAI,OAAO,CAAC,EAAE,EAC1B,MAAM,CAAC,YAAY,CACnB,CAAC;QAEF,MAAM,eAAe,GAAG,oBAAoB,CAC3C,GAAG,CAAC,QAAQ,IAAI,OAAO,CAAC,EAAE,EAC1B,MAAM,CAAC,aAAa,CACpB,CAAC;QAEF,KAAK,CAAC,qBAAqB,CAAC,CAAC;QAC7B,MAAM,CAAC,GAAG,CACT,GAAG,CAAC,QAAQ,IAAI,OAAO,CAAC,EAAE,EAC1B,WAAW;aACT,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,QAAQ,CAAC;aACrC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAC3B,CAAC;QAEF,KAAK,CAAC,gDAAgD,CAAC,CAAC;QACxD,GAAG,CAAC,MAAM,CAAC,cAAc,EAAE,eAAe,EAAE;YAC3C,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,GAAG,CAAC,QAAQ,KAAK,YAAY;YACrC,QAAQ,EAAE,QAAQ;YAClB,mBAAmB;YACnB,MAAM,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;SAClE,CAAC,CAAC;QAEH,sCAAsC;QACtC,KAAK,CAAC,qBAAqB,CAAC,CAAC;QAC7B,GAAG;aACD,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,cAAc,CAAC,CAAC;aACzC,MAAM,CAAC,GAAG,CAAC;aACX,IAAI,CAAC,cAAc,CAAC,CAAC;IACxB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IACvB,CAAC;AACF,CAAC"}

View File

@@ -0,0 +1,9 @@
import type { Request, Response } from 'express';
import { TwtKprConfiguration } from '../../types.js';
/**
* Creates a new twt, appending it to the bottom of the TWTXT file
*
* @param req
* @param res
*/
export default function twt(req: Request, res: Response, config: TwtKprConfiguration): void;

21
dist/src/middlewares/postHandler/twt.js vendored Normal file
View File

@@ -0,0 +1,21 @@
import dayjs from 'dayjs';
import fs from 'node:fs';
import { join } from 'node:path';
/**
* Creates a new twt, appending it to the bottom of the TWTXT file
*
* @param req
* @param res
*/
export default function twt(req, res, config) {
const { content } = req.body ?? {};
const date = dayjs().format();
const twt = `${date}\t${content.trim()}\n`;
const stream = fs.createWriteStream(join(config.publicDirectory, config.twtxtFilename), {
flags: 'a',
});
stream.write(twt);
stream.end();
res.status(200).send(twt);
}
//# sourceMappingURL=twt.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"twt.js","sourceRoot":"","sources":["../../../../src/middlewares/postHandler/twt.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAIjC;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,GAAG,CAC1B,GAAY,EACZ,GAAa,EACb,MAA2B;IAE3B,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IAEnC,MAAM,IAAI,GAAG,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC;IAC9B,MAAM,GAAG,GAAG,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC;IAE3C,MAAM,MAAM,GAAG,EAAE,CAAC,iBAAiB,CAClC,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,aAAa,CAAC,EAClD;QACC,KAAK,EAAE,GAAG;KACV,CACD,CAAC;IAEF,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,MAAM,CAAC,GAAG,EAAE,CAAC;IAEb,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC"}

View File

@@ -0,0 +1,9 @@
import type { Request, Response } from "express";
import { TwtKprConfiguration } from "../../types.js";
/**
* Creates a new twt, appending it to the bottom of the TWTXT file
*
* @param req
* @param res
*/
export default function editFile(req: Request, res: Response, config: TwtKprConfiguration): void;

View File

@@ -0,0 +1,23 @@
import fs from "node:fs";
import path from "node:path";
/**
* Creates a new twt, appending it to the bottom of the TWTXT file
*
* @param req
* @param res
*/
export default function editFile(req, res, config) {
const { fileContents } = req.body ?? {};
if (!fileContents) {
res.status(400).send("Missing fileContents");
return;
}
const stream = fs.createWriteStream(path.join(config.publicDirectory, config.twtxtFilename), {
flags: "w",
start: 0,
});
stream.write(fileContents);
stream.end();
res.type("text").status(200).send(fileContents);
}
//# sourceMappingURL=editFile.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"editFile.js","sourceRoot":"","sources":["../../../../src/middlewares/putHandler/editFile.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAI7B;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,QAAQ,CAC/B,GAAY,EACZ,GAAa,EACb,MAA2B;IAE3B,MAAM,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IAExC,IAAI,CAAC,YAAY,EAAE,CAAC;QACnB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC7C,OAAO;IACR,CAAC;IAED,MAAM,MAAM,GAAG,EAAE,CAAC,iBAAiB,CAClC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,aAAa,CAAC,EACvD;QACC,KAAK,EAAE,GAAG;QACV,KAAK,EAAE,CAAC;KACR,CACD,CAAC;IAEF,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAC3B,MAAM,CAAC,GAAG,EAAE,CAAC;IAEb,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AACjD,CAAC"}

View File

@@ -0,0 +1 @@
export { default } from "./putHandler.js";

View File

@@ -0,0 +1,2 @@
export { default } from "./putHandler.js";
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/middlewares/putHandler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC"}

View File

@@ -0,0 +1,7 @@
import { TwtKprConfiguration } from '../../types.js';
/**
*
* @param config
* @returns
*/
export default function putHandler(config: TwtKprConfiguration): import("express-serve-static-core").Router;

View File

@@ -0,0 +1,26 @@
import Debug from 'debug';
import express from 'express';
import authCheck from '../../middlewares/authCheckJWT.js';
import editFile from './editFile.js';
const debug = Debug('twtkpr:putHandler');
/**
*
* @param config
* @returns
*/
export default function putHandler(config) {
const router = express.Router();
router.put('/', (req, res, next) => {
debug('put', { path: req.path });
debug('checking auth');
if (!authCheck(req, config)) {
debug('auth check failed');
next();
return;
}
debug('auth check succeeded');
return editFile(req, res, config);
});
return router;
}
//# sourceMappingURL=putHandler.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"putHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/putHandler/putHandler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,OAAO,SAAS,MAAM,mCAAmC,CAAC;AAE1D,OAAO,QAAQ,MAAM,eAAe,CAAC;AAErC,MAAM,KAAK,GAAG,KAAK,CAAC,mBAAmB,CAAC,CAAC;AAEzC;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,MAA2B;IAC7D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAClC,KAAK,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QAEjC,KAAK,CAAC,eAAe,CAAC,CAAC;QAEvB,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC;YAC7B,KAAK,CAAC,mBAAmB,CAAC,CAAC;YAC3B,IAAI,EAAE,CAAC;YACP,OAAO;QACR,CAAC;QAED,KAAK,CAAC,sBAAsB,CAAC,CAAC;QAE9B,OAAO,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AACf,CAAC"}

View File

@@ -0,0 +1,11 @@
import type { Request, Response } from 'express';
import { QueryParameters } from '../../types.js';
import NodeCache from '@cacheable/node-cache';
/**
*
* @param req
* @param res
* @param cache
* @param followingParameter
*/
export default function followingHandler(req: Request, res: Response, cache: NodeCache<unknown>, followingParameter: QueryParameters['following']): void;

View File

@@ -0,0 +1,39 @@
import { generateEtag, getQueryParameterArray, getValueOrFirstEntry, } from '../../lib/utils.js';
/**
*
* @param req
* @param res
* @param cache
* @param followingParameter
*/
export default function followingHandler(req, res, cache, followingParameter) {
const followingsToMatch = getQueryParameterArray(req.query[followingParameter]);
const nicksToMatch = getQueryParameterArray(req.query.nick);
const urlsToMatch = getQueryParameterArray(req.query.url);
const searchTermsToMatch = [
...getQueryParameterArray(req.query.search),
...getQueryParameterArray(req.query.s),
];
const wantsJson = req.is('json') ||
getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json';
if (wantsJson)
res.set('content-type', 'application/json');
else
res.set('content-type', 'text/plain');
const matchedFollowing = cache.get('following').filter(({ nick, url }) => (!followingsToMatch.length ||
(followingsToMatch.length === 1 && followingsToMatch[0] === '') ||
followingsToMatch.includes(nick) ||
followingsToMatch.includes(`@${nick}`) ||
followingsToMatch.includes(url)) &&
(!nicksToMatch.length ||
nicksToMatch.includes(nick) ||
nicksToMatch.includes(`@${nick}`)) &&
(!urlsToMatch.length || urlsToMatch.includes(url)) &&
(!searchTermsToMatch.length ||
searchTermsToMatch.some((term) => nick.includes(term) || url.includes(term))));
const result = wantsJson
? JSON.stringify(matchedFollowing)
: matchedFollowing.map(({ nick, url }) => `@${nick} ${url}`).join('\n');
res.set('etag', generateEtag(result)).send(result);
}
//# sourceMappingURL=followingHandler.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"followingHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/queryHandler/followingHandler.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,YAAY,EACZ,sBAAsB,EACtB,oBAAoB,GACpB,MAAM,oBAAoB,CAAC;AAI5B;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CACvC,GAAY,EACZ,GAAa,EACb,KAAyB,EACzB,kBAAgD;IAEhD,MAAM,iBAAiB,GAAG,sBAAsB,CAC/C,GAAG,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAC7B,CAAC;IAEF,MAAM,YAAY,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5D,MAAM,WAAW,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE1D,MAAM,kBAAkB,GAAG;QAC1B,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;QAC3C,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;KACtC,CAAC;IAEF,MAAM,SAAS,GACd,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC;QACd,oBAAoB,CAAC,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,MAAM,CAAC;IAC3E,IAAI,SAAS;QAAE,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;;QACtD,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;IAE3C,MAAM,gBAAgB,GAAI,KAAK,CAAC,GAAG,CAAC,WAAW,CAAa,CAAC,MAAM,CAClE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,CACjB,CAAC,CAAC,iBAAiB,CAAC,MAAM;QACzB,CAAC,iBAAiB,CAAC,MAAM,KAAK,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;QAC/D,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC;QAChC,iBAAiB,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC;QACtC,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QACjC,CAAC,CAAC,YAAY,CAAC,MAAM;YACpB,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC;YAC3B,YAAY,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACnC,CAAC,CAAC,WAAW,CAAC,MAAM,IAAI,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAClD,CAAC,CAAC,kBAAkB,CAAC,MAAM;YAC1B,kBAAkB,CAAC,IAAI,CACtB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CACnD,CAAC,CACJ,CAAC;IAEF,MAAM,MAAM,GAAG,SAAS;QACvB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC;QAClC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEzE,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACpD,CAAC"}

View File

@@ -0,0 +1 @@
export { default } from "./queryHandler.js";

View File

@@ -0,0 +1,2 @@
export { default } from "./queryHandler.js";
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/middlewares/queryHandler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC"}

View File

@@ -0,0 +1,17 @@
import type { Request, Response } from 'express';
import NodeCache from '@cacheable/node-cache';
import { QueryParameters } from '../../types.js';
export interface MetadataHandler {
cache: NodeCache<unknown>;
metadataParameter: QueryParameters['metadata'];
req: Request;
res: Response;
}
/**
*
* @param req
* @param res
* @param cache
* @param metadataParameter
*/
export default function metadataHandler(req: Request, res: Response, cache: NodeCache<unknown>, metadataParameter: QueryParameters['metadata']): void;

View File

@@ -0,0 +1,50 @@
import { generateEtag, getQueryParameterArray, getValueOrFirstEntry, } from '../../lib/utils.js';
/**
*
* @param req
* @param res
* @param cache
* @param metadataParameter
*/
export default function metadataHandler(req, res, cache, metadataParameter) {
const metadataToMatch = getQueryParameterArray(req.query[metadataParameter]);
const searchTermsToMatch = [
...getQueryParameterArray(req.query.search),
...getQueryParameterArray(req.query.s),
];
const metadata = cache.get('metadata') ?? {};
const wantsJson = req.is('json') ||
getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json';
if (wantsJson)
res.set('content-type', 'application/json');
else
res.set('content-type', 'text/plain');
const matchedMetadata = Object.keys(metadata)
.filter((key) => (!metadataToMatch.length ||
(metadataToMatch.length === 1 && metadataToMatch[0] === '') ||
metadataToMatch.includes(key)) &&
(!searchTermsToMatch.length ||
searchTermsToMatch.some((term) => key.includes(term) || Array.isArray(metadata[key])
? metadata[key].some((val) => val.includes(term))
: metadata[key].includes(term))))
.reduce((acc, key) => {
const value = metadata[key];
acc[key] = Array.isArray(value)
? value.filter((value) => !searchTermsToMatch.length ||
searchTermsToMatch.some((term) => key.includes(term) || value.includes(term)))
: value;
return acc;
}, {});
const result = wantsJson
? JSON.stringify(matchedMetadata)
: Object.keys(matchedMetadata)
.map((key) => {
const value = matchedMetadata[key];
return Array.isArray(value)
? value.map((rowVal) => `${key}: ${rowVal}`).join('\n')
: `${key}: ${value}`;
})
.join('\n');
res.set('etag', generateEtag(result)).send(result);
}
//# sourceMappingURL=metadataHandler.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"metadataHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/queryHandler/metadataHandler.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,11 @@
import NodeCache from '@cacheable/node-cache';
import type { NextFunction, Request, Response } from 'express';
import { TwtKprConfiguration } from '../../types.js';
/**
*
* @param config
* @param cache
* @param verifyAuthRequest
* @returns
*/
export default function queryHandler(config: TwtKprConfiguration, cache: NodeCache<unknown>, verifyAuthRequest: (r: Request) => Promise<boolean>): (req: Request, res: Response, next: NextFunction) => Promise<void>;

View File

@@ -0,0 +1,58 @@
import path from 'node:path';
import Debug from 'debug';
import { __dirname } from '../../lib/constants.js';
import { generateEtag } from '../../lib/utils.js';
import renderApp from '../renderApp/index.js';
import followingHandler from './followingHandler.js';
import metadataHandler from './metadataHandler.js';
import twtHandler from './twtHandler.js';
const debug = Debug('twtkpr:queryHandler');
/**
*
* @param config
* @param cache
* @param verifyAuthRequest
* @returns
*/
export default function queryHandler(config, cache, verifyAuthRequest) {
const { mainRoute, queryParameters, uploadConfiguration } = config;
return async (req, res, next) => {
debug({ query: JSON.stringify(req.query) });
if (!Object.keys(req.query).length) {
next();
return;
}
if (req.query[queryParameters.app] !== undefined) {
const appContent = renderApp({ mainRoute, uploadConfiguration });
res.set('etag', generateEtag(appContent)).send(appContent);
return;
}
if (req.query[queryParameters.css] !== undefined) {
res.sendFile('styles.css', {
root: path.resolve(__dirname, 'client'),
});
return;
}
if (req.query[queryParameters.js] !== undefined) {
res.sendFile('script.js', {
root: path.resolve(__dirname, 'client'),
});
return;
}
if (req.query[queryParameters.following] !== undefined &&
cache.get('following')) {
return followingHandler(req, res, cache, queryParameters.following);
}
if (req.query[queryParameters.metadata] !== undefined &&
cache.get('metadata')) {
return metadataHandler(req, res, cache, queryParameters.metadata);
}
if ((req.query[queryParameters.twt] !== undefined ||
req.query[queryParameters.twts] !== undefined) &&
cache.get('twts')) {
return twtHandler(req, res, cache.get('twts'), queryParameters.twt, queryParameters.twts);
}
next();
};
}
//# sourceMappingURL=queryHandler.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"queryHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/queryHandler/queryHandler.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,13 @@
import type { Request, Response } from 'express';
import type { Twt } from 'twtxt-lib';
import { QueryParameters } from '../../types.js';
/**
*
* @param req
* @param res
* @param twts
* @param twtParameter
* @param twtsParameter
* @returns
*/
export default function twtHandler(req: Request, res: Response, twts: Twt[] | undefined, twtParameter: QueryParameters['twt'], twtsParameter: QueryParameters['twts']): void;

View File

@@ -0,0 +1,68 @@
import { generateEtag, getQueryParameterArray, getValueOrFirstEntry, } from '../../lib/utils.js';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
dayjs.extend(utc);
/**
*
* @param req
* @param res
* @param twts
* @param twtParameter
* @param twtsParameter
* @returns
*/
export default function twtHandler(req, res, twts = [], twtParameter, twtsParameter) {
const twtsToMatch = getQueryParameterArray(req.query[twtsParameter]);
const showLastTwt = getQueryParameterArray(req.query[twtParameter]);
const hashesToMatch = getQueryParameterArray(req.query.hash);
const searchTermsToMatch = [
...getQueryParameterArray(req.query.search),
...getQueryParameterArray(req.query.s),
];
const createdDatesToMatch = getQueryParameterArray(req.query.created_date);
const createdUTCStartDatesToMatch = getQueryParameterArray(req.query.created_date_start).map((val) => dayjs.utc(val));
const createdUTCEndDatesToMatch = getQueryParameterArray(req.query.created_date_end).map((val) => dayjs.utc(val));
const wantsJson = req.is('json') ||
getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json';
if (wantsJson)
res.set('content-type', 'application/json');
else
res.set('content-type', 'text/plain');
if (showLastTwt.length === 1 && showLastTwt[0] === '') {
const lastTwt = twts.reduce((matched, curr) => matched?.createdUTC > curr.createdUTC ? matched : curr);
let result = 'No results';
if (lastTwt) {
result = wantsJson
? JSON.stringify(lastTwt)
: `${lastTwt?.created || ''}\t${lastTwt?.content || ''}\n`;
}
res.set('etag', generateEtag(result)).send(result);
return;
}
const matchedTwts = twts.filter(({ content, created, createdUTC, hash }) => {
return ((!twtsToMatch.length ||
(twtsToMatch.length === 1 && twtsToMatch[0] === '') ||
twtsToMatch.includes(created) ||
(hash &&
(twtsToMatch.includes(hash) || twtsToMatch.includes(`#${hash}`)))) &&
(!hashesToMatch.length ||
(hash &&
(hashesToMatch.includes(hash) ||
hashesToMatch.includes(`#${hash}`)))) &&
(!createdDatesToMatch.length ||
createdDatesToMatch.some((date) => created.includes(date))) &&
(!createdUTCStartDatesToMatch.length ||
createdUTCStartDatesToMatch.some((date) => date.diff(createdUTC) < 0)) &&
(!createdUTCEndDatesToMatch.length ||
createdUTCEndDatesToMatch.some((date) => date.diff(createdUTC) > 0)) &&
(!searchTermsToMatch.length ||
searchTermsToMatch.some((term) => content.includes(term))));
});
const result = wantsJson
? JSON.stringify(matchedTwts)
: matchedTwts
.map(({ content, created }) => `${created}\t${content}`)
.join('\n');
res.set('etag', generateEtag(result)).send(result);
}
//# sourceMappingURL=twtHandler.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"twtHandler.js","sourceRoot":"","sources":["../../../../src/middlewares/queryHandler/twtHandler.ts"],"names":[],"mappings":"AAGA,OAAO,EACN,YAAY,EACZ,sBAAsB,EACtB,oBAAoB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,GAAG,MAAM,qBAAqB,CAAC;AAItC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAElB;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CACjC,GAAY,EACZ,GAAa,EACb,OAAc,EAAE,EAChB,YAAoC,EACpC,aAAsC;IAEtC,MAAM,WAAW,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC;IACrE,MAAM,WAAW,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;IACpE,MAAM,aAAa,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7D,MAAM,kBAAkB,GAAG;QAC1B,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;QAC3C,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;KACtC,CAAC;IAEF,MAAM,mBAAmB,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAC3E,MAAM,2BAA2B,GAAG,sBAAsB,CACzD,GAAG,CAAC,KAAK,CAAC,kBAAkB,CAC5B,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,MAAM,yBAAyB,GAAG,sBAAsB,CACvD,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAC1B,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAE/B,MAAM,SAAS,GACd,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC;QACd,oBAAoB,CAAC,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,MAAM,CAAC;IAC3E,IAAI,SAAS;QAAE,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;;QACtD,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;IAE3C,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QACvD,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,CAC7C,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CACtD,CAAC;QACF,IAAI,MAAM,GAAG,YAAY,CAAC;QAE1B,IAAI,OAAO,EAAE,CAAC;YACb,MAAM,GAAG,SAAS;gBACjB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;gBACzB,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,IAAI,EAAE,KAAK,OAAO,EAAE,OAAO,IAAI,EAAE,IAAI,CAAC;QAC7D,CAAC;QAED,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnD,OAAO;IACR,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE;QAC1E,OAAO,CACN,CAAC,CAAC,WAAW,CAAC,MAAM;YACnB,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;YACnD,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC;YAC7B,CAAC,IAAI;gBACJ,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YACpE,CAAC,CAAC,aAAa,CAAC,MAAM;gBACrB,CAAC,IAAI;oBACJ,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC;wBAC5B,aAAa,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YACxC,CAAC,CAAC,mBAAmB,CAAC,MAAM;gBAC3B,mBAAmB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5D,CAAC,CAAC,2BAA2B,CAAC,MAAM;gBACnC,2BAA2B,CAAC,IAAI,CAC/B,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CACnC,CAAC;YACH,CAAC,CAAC,yBAAyB,CAAC,MAAM;gBACjC,yBAAyB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;YACrE,CAAC,CAAC,kBAAkB,CAAC,MAAM;gBAC1B,kBAAkB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAC3D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,SAAS;QACvB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC;QAC7B,CAAC,CAAC,WAAW;aACV,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,OAAO,KAAK,OAAO,EAAE,CAAC;aACvD,IAAI,CAAC,IAAI,CAAC,CAAC;IAEf,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACpD,CAAC"}

View File

@@ -0,0 +1 @@
export { default } from "./renderApp.js";

View File

@@ -0,0 +1,2 @@
export { default } from "./renderApp.js";
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/middlewares/renderApp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC"}

View File

@@ -0,0 +1,7 @@
import { TwtKprConfiguration } from '../../types.js';
/**
*
* @param param0
* @returns
*/
export default function renderApp({ mainRoute, uploadConfiguration, }: Pick<TwtKprConfiguration, 'mainRoute' | 'uploadConfiguration'>): string;

View File

@@ -0,0 +1,143 @@
import { version } from '../../packageInfo.js';
import renderUploadButton from './renderUploadButton.js';
/**
*
* @param param0
* @returns
*/
export default function renderApp({ mainRoute, uploadConfiguration, }) {
return `<!doctype html>
<html class="no-js" lang="en" xmlns:fb="http://ogp.me/ns/fb#">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- HTML head reference: https://github.com/joshbuchea/HEAD#recommended-minimum -->
<meta name="application-name" content="Application Name">
<meta name="theme-color" content="#6e6e81">
<title>TwtKpr</title>
<link rel="icon" href="favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="${mainRoute}?css" />
</head>
<body>
<div class="toastContainer" id="toast-container"></div>
<div class="app" id="app">
<div class="menu" id="menu">
<div class="loginControls" id="loginControls">
<form method="POST" action="${mainRoute}" id="loginControls-form">
<input type="hidden" name="type" value="login" />
<div class="loginControls-row">
<div class="loginControls-fields">
<div class="loginControls-fields-row">
<label class="loginControls-label" for="loginControls-username">
Username
<input type="text" class="loginControls-input"
id="loginControls-username" name="username" value="" />
</label>
<label class="loginControls-label" for="loginControls-password">
Password
<input type="password" class="loginControls-input"
id="loginControls-password" name="password" value="" />
</label>
<label class="loginControls-toggle" for="loginControls-rememberToggle">
<input type="checkbox" class="loginControls-toggle-checkbox"
id="loginControls-rememberToggle" />
Stay Logged In
<span class="loginControls-toggle-track">
<span class="loginControls-toggle-indicator">
<span class="loginControls-toggle-checkMark">
<svg viewBox="0 0 24 24" id="loginControls-svg-check"
role="presentation" aria-hidden="true">
<path d="M9.86 18a1 1 0 01-.73-.32l-4.86-5.17a1.001 1.001 0
011.46-1.37l4.12 4.39 8.41-9.2a1 1 0 111.48 1.34l-9.14 10a1 1 0
01-.73.33h-.01z"></path>
</svg>
</span>
</span>
</span>
</label>
</div>
</div>
<input class="loginControls-submitButton button" type="submit" value="Login" />
</div>
</form>
</div>
<div class="twtControls" id="twtControls">
<form class="twtControls-form" method="POST" action="${mainRoute}" id="twtForm">
<input type="hidden" name="type" value="twt" />
<div class="twtControls-formRow">
<div class="twtControls-appInfo appInfo">
TwtKpr v${version ?? 'Unknown'}
<div class="twtControls-appAuthor">
by Eric Woodward (<a href="https://itsericwoodward.com/twtxt.txt"
rel="noopener noreferrer" target="_blank">@itsericwoodward</a>)
</div>
<div class="twtControls-gitLink">
<a href="https://git.itsericwoodward.com/eric/express-twtkpr"
rel="noopener noreferrer">
https://git.itsericwoodward.com/eric/express-twtkpr
</a>
</div>
</div>
${renderUploadButton(uploadConfiguration)}
<label class="twtControls-contentLabel" for="twtControlsContentInput">
<textarea class="twtControls-contentInput"
id="twtControlsContentInput" name="content"
placeholder="What do you want to say?"></textarea>
</label>
<div class="button hamburgerToggle">
<input type="checkbox" name="hamburgerToggleCheckbox"
id="hamburgerToggleCheckbox" aria-label="Toggle Navigation" />
<label class="hamburgerToggle-label" for="hamburgerToggleCheckbox">
<div class="hamburgerToggle-icon"></div>
</label>
<div class="popupMenu">
<div class="popupMenu-appInfo appInfo">
TwtKpr v${version ?? 'Unknown'}
</div>
${renderUploadButton(uploadConfiguration, 'small')}
<button class="twtControls-editButton button" id="twtControlsEditButton">
Edit File
</button>
<button class="twtControls-logoutButton button" id="twtControlsLogoutButton">
Logout
</button>
</div>
</div>
<input class="twtControls-submitButton" disabled="disabled"
id="twtControlsSubmitButton" type="submit" value="Post\nMsg" />
</div>
</form>
</div>
</div>
<main class="fileContentsSection" id="fileContentsSection">
<pre class="fileContentsSection-fileBox" id="fileBox"></pre>
<form
action="/"
class="fileContentsSection-twtxtEditForm twtxtEditForm"
id="twtxtEditForm"
method="PUT"
>
<textarea class="twtxtEditForm-textarea" id="twtxtEditFormText"></textarea>
<div class="twtxtEditForm-controls">
<input class="button twtxtEditForm-button" type="reset" value="Cancel" />
<input class="button twtxtEditForm-button" type="submit" value="Update" />
<div>
</form>
</main>
</div>
<script type="module" src="${mainRoute}?js"></script>
</body>
</html>
`;
}
//# sourceMappingURL=renderApp.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"renderApp.js","sourceRoot":"","sources":["../../../../src/middlewares/renderApp/renderApp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAG/C,OAAO,kBAAkB,MAAM,yBAAyB,CAAC;AAEzD;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EACjC,SAAS,EACT,mBAAmB,GAC6C;IAChE,OAAO;;;;;;;;;;;;;;mCAc2B,SAAS;;;;;;;;8CAQE,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uEAyCgB,SAAS;;;;sCAI1C,OAAO,IAAI,SAAS;;;;;;;;;;;;0BAYhC,kBAAkB,CAAC,mBAAmB,CAAC;;;;;;;;;;;;;;8CAcnB,OAAO,IAAI,SAAS;;kCAEhC,kBAAkB,CAAC,mBAAmB,EAAE,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iCAiCjD,SAAS;;;;CAIzC,CAAC;AACF,CAAC"}

Some files were not shown because too many files have changed in this diff Show More