├── .gitignore ├── LICENSE ├── README.md ├── config.js ├── package.json ├── src ├── main.js ├── storage.js └── utils.js ├── static ├── assets │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-57x57.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-76x76.png │ ├── favicon-128.png │ ├── favicon-16x16.png │ ├── favicon-196x196.png │ ├── favicon-32x32.png │ └── favicon-96x96.png ├── css │ └── main.css ├── favicon.ico ├── index.html ├── js │ └── main.js └── template.html └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # secrets 2 | .env 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # next.js build output 64 | .next 65 | 66 | # database folder for linus.zone 67 | db/ 68 | 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Linus Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # linus.zone 2 | 3 | A URL shortener / note sharing service, running at [linus.zone](https://linus.zone). 4 | 5 | ## Setup 6 | 7 | To start `zone`: 8 | 9 | 1. Make sure you have `npm` installed, and install dependencies with `npm install` or `yarn install` 10 | 2. Create a `.env` file with your global password (see below note), `PASSWORD=` 11 | 3. `npm start` or `yarn start` to start the server. 12 | 13 | ### Note on a global password and spam prevention 14 | 15 | I wrote this app a while ago, and still run it in production for my personal use at linus.zone. It used to be open for anyone to use, but I started running into problems with spam, with scammers trying to use this redirector to mask their real domains. This is obviously problematic in many ways, but I didn't want to add a fully-fledged auth system to the app, so for now I've set a global secret password, defined in a `.env` file, so that only those with that single password can edit the link shortener database. 16 | 17 | It's not an elegant solution, but it took a few minutes and worked for me, without adding any complexity. 18 | 19 | This means that, if you want to run this on your own, you also either need to set a `PASSWORD` in an `.env` file, or remove the global-password-checking logic from `src/main.js` (grep for `isAuthorizedUser`). 20 | 21 | ## Todo's 22 | 23 | - [ ] Avoid sanitizing dangerous HTML tags if they're rendered inside code snippets. This may require is to restructure our sanitizer so we sanitize on render and not save. 24 | - [ ] Ability to see all created uris and notes as links under one page (`/all`), password-authenticated 25 | 26 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | PORT: 3000, 3 | DATABASE: 'db', 4 | } 5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zone", 3 | "version": "1.0.0", 4 | "description": "linus.zone", 5 | "main": "src/main.js", 6 | "repository": "git@github.com:thesephist/zone.git", 7 | "author": "Linus Lee ", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "node src/main.js" 11 | }, 12 | "dependencies": { 13 | "body-parser": "^1.20.0", 14 | "express": "^4.18.0", 15 | "marked": "^4.0.14", 16 | "shortid": "^2.2.16" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const crypto = require('crypto'); 4 | 5 | const bodyParser = require('body-parser'); 6 | const express = require('express'); 7 | const app = express(); 8 | app.use(bodyParser.urlencoded({ 9 | extended: false, 10 | })); 11 | 12 | const Config = require('../config.js'); 13 | const { StorageBackend, Record } = require('./storage.js'); 14 | 15 | const GLOBAL_PASSWORD = fs.readFileSync('.env', 'utf8').trim().split('=')[1]; 16 | 17 | // create db if not exists 18 | if (!fs.existsSync(Config.DATABASE)) { 19 | fs.mkdirSync(Config.DATABASE); 20 | } 21 | 22 | // cache template HTML 23 | const TEMPLATE = fs.readFileSync('static/template.html', 'utf8'); 24 | 25 | const storage = new StorageBackend(Config.DATABASE); 26 | 27 | const tpl = params => { 28 | let templateResult = TEMPLATE; 29 | for (const [key, value] of Object.entries(params)) { 30 | templateResult = templateResult.replace(`%${key}%`, value); 31 | } 32 | return templateResult; 33 | } 34 | 35 | app.get('/', (req, res) => { 36 | //> Hack to return a fancy ANSI terminal result 37 | // when running $ curl linus.zone 38 | if (req.header('User-Agent').includes('curl/')) { 39 | res.set('Content-Type', 'text/plain'); 40 | res.send(`Linus Lee 41 | === 42 | 43 | www: thesephist.com 44 | email: linus@thesephist.com 45 | social: @thesephist 46 | 47 | --- 48 | 49 | Product Engineer, Hack Club 50 | linus@hackclub.com 51 | 52 | Investment Partner, Dorm Room Fund 53 | linus@drf.vc 54 | 55 | Director, Cal Hacks 56 | linus@calhacks.io 57 | 58 | Computer Science, UC Berkeley 59 | l7@berkeley.edu\n`); 60 | } else { 61 | fs.readFile('static/index.html', 'utf8', (err, data) => { 62 | if (err) { 63 | throw err; 64 | } 65 | 66 | res.set('Content-Type', 'text/html'); 67 | res.send(data); 68 | }); 69 | } 70 | }); 71 | 72 | app.post('/new', async (req, res) => { 73 | if (req.body.id && (req.body.id.includes('.') || req.body.id.includes('/'))) { 74 | res.status(400); 75 | res.set('Content-Type', 'text/plain'); 76 | res.send('Record IDs cannot contain "." or "/"'); 77 | return; 78 | } 79 | 80 | try { 81 | let canCreateRecord = false; 82 | if (await storage.has(req.body.id)) { 83 | const existingRecord = await storage.get(req.body.id); 84 | if (existingRecord.isLocked()) { 85 | if (existingRecord.canUnlockWith(req.body.password)) { 86 | canCreateRecord = true; 87 | } 88 | } else { 89 | canCreateRecord = true; 90 | } 91 | } else { 92 | canCreateRecord = true; 93 | } 94 | 95 | // NOTE: This is a quick hack to ensure that only authorized 96 | // people can add to the database, because I ran into problems 97 | // with spam. 98 | const hash = crypto.createHash('sha256'); 99 | hash.update(req.body.password); 100 | const hashed = hash.digest('hex'); 101 | const isAuthorizedUser = hashed === GLOBAL_PASSWORD; 102 | 103 | if (canCreateRecord && isAuthorizedUser) { 104 | const record = new Record({ 105 | id: req.body.id || undefined, 106 | type: req.body.content_uri ? 'uri' : 'note', 107 | password: req.body.password || undefined, 108 | content: req.body.content_uri || req.body.content_note, 109 | }); 110 | 111 | if (record.isURI() && record.id === record.content) { 112 | res.status(409); 113 | res.send(`This record will create a redirect loop`); 114 | return; 115 | } else if (!record.validate()) { 116 | res.status(400); 117 | res.send(`This record is invalid`); 118 | return; 119 | } 120 | 121 | await storage.save(record); 122 | res.redirect(302, `/${record.id}`); 123 | console.log(`Created note ${record.id} as ${record.type}`); 124 | } else { 125 | res.status(401); 126 | res.set('Content-Type', 'text/plain'); 127 | res.send(`Incorrect password: could not edit record ${req.body.id}.`); 128 | console.log(`Unauthorized attempt to edit ${req.body.id}`); 129 | } 130 | } catch (e) { 131 | res.status(500); 132 | res.send(''); 133 | console.log(`Error on /new: ${e}`); 134 | } 135 | }); 136 | 137 | app.get('/:id', async (req, res) => { 138 | res.set('Content-Type', 'text/html'); 139 | 140 | const rid = req.params.id; 141 | try { 142 | if (await storage.has(rid)) { 143 | const record = await storage.get(rid); 144 | if (record.isNote()) { 145 | res.send(tpl({ 146 | title: record.id, 147 | content: record.render(), 148 | })); 149 | console.log(`Rendered note ${record.id} as HTML`); 150 | } else if (record.isURI()) { 151 | res.redirect(302, record.getRedirect()); 152 | console.log(`Redirected note ${record.id} to ${record.getRedirect()}`); 153 | } 154 | } else { 155 | res.status(404); 156 | res.send(`Record ${rid} does not exist.`); 157 | } 158 | } catch (e) { 159 | console.error(e); 160 | } 161 | }); 162 | 163 | app.get('/:id/raw', async (req, res) => { 164 | res.set('Content-Type', 'text/plain'); 165 | 166 | const rid = req.params.id; 167 | try { 168 | if (await storage.has(rid)) { 169 | const record = await storage.get(rid); 170 | if (record.isNote()) { 171 | res.send(record.getRawNote()); 172 | console.log(`Rendered raw note for ${record.id}`); 173 | } else if (record.isURI()) { 174 | res.send(record.getRedirect()); 175 | console.log(`Rendered raw uri for ${record.id}`); 176 | } 177 | } else { 178 | res.status(404); 179 | res.send(`Record ${rid} does not exist.`); 180 | } 181 | } catch (e) { 182 | console.error(e); 183 | } 184 | }); 185 | 186 | app.get('/:id/edit', async (req, res) => { 187 | res.set('Content-Type', 'text/plain'); 188 | 189 | const rid = req.params.id; 190 | try { 191 | if (await storage.has(rid)) { 192 | const record = await storage.get(rid); 193 | res.redirect(302, `/#${record.id}`); // prefilled form 194 | } else { 195 | res.status(404); 196 | res.send(`Record ${rid} does not exist.`); 197 | } 198 | } catch (e) { 199 | console.error(e); 200 | } 201 | }); 202 | 203 | app.get('/:id/content', async (req, res) => { 204 | res.set('Content-Type', 'application/json'); 205 | 206 | const rid = req.params.id; 207 | try { 208 | if (await storage.has(rid)) { 209 | const record = await storage.get(rid); 210 | if (record.isNote()) { 211 | res.send({ 212 | type: 'note', 213 | content: record.getRawNote(), 214 | }); 215 | } else if (record.isURI()) { 216 | res.send({ 217 | type: 'uri', 218 | content: record.getRedirect(), 219 | }); 220 | } 221 | } else { 222 | res.send({ 223 | type: 'none', 224 | content: `Record ${rid} does not exist.` 225 | }); 226 | } 227 | } catch (e) { 228 | console.error(e); 229 | } 230 | }); 231 | 232 | app.get('/:id/locked', async (req, res) => { 233 | res.set('Content-Type', 'text/plain'); 234 | 235 | const rid = req.params.id; 236 | try { 237 | if (await storage.has(rid)) { 238 | const record = await storage.get(rid); 239 | res.send(record.isLocked() ? '1' : '0'); 240 | } else { 241 | // doesn't exist, so this ID isn't locked 242 | res.send('0'); 243 | } 244 | } catch (e) { 245 | res.send('0'); 246 | console.error(e); 247 | } 248 | }); 249 | 250 | app.use('/static', express.static('static')); 251 | 252 | app.listen( 253 | Config.PORT, 254 | () => console.log(`Zone service running on :${Config.PORT}`) 255 | ); 256 | 257 | -------------------------------------------------------------------------------- /src/storage.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const crypto = require('crypto'); 4 | 5 | const marked = require('marked'); 6 | const { 7 | randid, 8 | validid, 9 | sanitizeDangerousHtml, 10 | } = require('./utils.js'); 11 | 12 | class StorageBackend { 13 | 14 | constructor(dirpath) { 15 | this.dirpath = dirpath; 16 | } 17 | 18 | pathFromID(id) { 19 | return path.join(this.dirpath, id); 20 | } 21 | 22 | has(id) { 23 | return new Promise((res, rej) => { 24 | if (!id) { 25 | res(false); 26 | } else { 27 | fs.access( 28 | this.pathFromID(id), 29 | fs.constants.F_OK, 30 | err => { 31 | res(!err); 32 | } 33 | ); 34 | } 35 | }).catch(e => console.error(e)); 36 | } 37 | 38 | get(id) { 39 | return new Promise((res, rej) => { 40 | fs.readFile( 41 | this.pathFromID(id), 42 | 'utf8', 43 | (err, data) => { 44 | if (err) { 45 | rej(err); 46 | } else { 47 | res(Record.parse(data)); 48 | } 49 | }); 50 | }).catch(e => console.error(e)); 51 | } 52 | 53 | save(record) { 54 | return new Promise((res, rej) => { 55 | fs.writeFile( 56 | this.pathFromID(record.id), 57 | record.serialize(), 58 | 'utf8', 59 | (err) => { 60 | if (err) { 61 | rej(err); 62 | } else { 63 | res(); 64 | } 65 | } 66 | ) 67 | }).catch(e => console.error(e)); 68 | } 69 | 70 | } 71 | 72 | class Record { 73 | 74 | constructor({ 75 | id = randid(), 76 | type = 'note', 77 | hash = undefined, 78 | password = undefined, 79 | content = '', 80 | }) { 81 | this.id = id.trim(); 82 | this.type = type; 83 | this.hash = hash || (password ? Record.hash(this.id, password) : undefined); 84 | this.content = sanitizeDangerousHtml(content.trim()); 85 | } 86 | 87 | isLocked() { 88 | return this.hash !== undefined; 89 | } 90 | 91 | canUnlockWith(password) { 92 | return this.hash === Record.hash(this.id, password); 93 | } 94 | 95 | isURI() { 96 | return this.type === 'uri'; 97 | } 98 | 99 | isNote() { 100 | return this.type === 'note'; 101 | } 102 | 103 | validate() { 104 | const VALID_TYPES = [ 105 | 'uri', 106 | 'note', 107 | ]; 108 | 109 | return [ 110 | validid(this.id), 111 | VALID_TYPES.includes(this.type), 112 | this.content !== '', 113 | ].every(x => x); 114 | } 115 | 116 | render() { 117 | if (!this.isNote()) { 118 | throw new Error(`Cannot render a record of type ${this.type}`); 119 | } 120 | 121 | return marked.parse(this.content); 122 | } 123 | 124 | getRawNote() { 125 | if (!this.isNote()) { 126 | throw new Error(`Cannot get raw note of a record of type ${this.type}`); 127 | } 128 | 129 | return this.content; 130 | } 131 | 132 | getRedirect() { 133 | if (!this.isURI()) { 134 | throw new Error(`Cannot render a redirect path for record of type ${this.type}`); 135 | } 136 | 137 | return this.content; 138 | } 139 | 140 | serialize() { 141 | if (this.isLocked()) { 142 | return `${this.type}.${this.id}.${this.hash}\n${this.content}`; 143 | } else { 144 | return `${this.type}.${this.id}\n${this.content}`; 145 | } 146 | } 147 | 148 | static hash(id, password) { 149 | const hash = crypto.createHash('sha256'); 150 | hash.update(id + password); 151 | return hash.digest('hex'); 152 | } 153 | 154 | static parse(data) { 155 | const [firstline, ...contentLines] = data.split('\n'); 156 | const [type, id, hash] = firstline.split('.'); 157 | return new Record({ 158 | id: id, 159 | type: type, 160 | hash: hash, 161 | content: contentLines.join('\n'), 162 | }); 163 | } 164 | 165 | } 166 | 167 | module.exports = { 168 | StorageBackend, 169 | Record, 170 | } 171 | 172 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const shortid = require('shortid'); 3 | 4 | /** 5 | * Get a random ID for a record 6 | */ 7 | const randid = _ => { 8 | return shortid.generate(); 9 | } 10 | 11 | /** 12 | * Is the given ID a valid ID in our system? 13 | */ 14 | const validid = id => { 15 | return ( 16 | typeof id === 'string' 17 | && id !== '' 18 | && !id.includes('.') 19 | ); 20 | } 21 | 22 | const DANGEROUS_HTML_TAGS = [ 23 | 'head', 24 | 'body', 25 | 'title', 26 | 'link', 27 | 'style', 28 | 'script', 29 | ]; 30 | 31 | const sanitizeDangerousHtml = markdownString => { 32 | let result = ''; 33 | let inOpeningTag = false; 34 | let lastTag = ''; 35 | 36 | for (let i = 0; i < markdownString.length; i ++) { 37 | const char = markdownString[i]; 38 | 39 | if (char === '<') { 40 | inOpeningTag = true; 41 | } else if ((char === '>' || char.trim() == '') && inOpeningTag) { 42 | inOpeningTag = false; 43 | if (DANGEROUS_HTML_TAGS.includes(lastTag.trim().toLowerCase())) { 44 | // read and ignore until the tag is closed 45 | const closingTag = ``; 46 | const indexOfClosingTag = i + markdownString.substr(i).toLowerCase().indexOf(closingTag); 47 | if (indexOfClosingTag === -1) { 48 | // tag is never closed, so ignore the rest of the string 49 | i = markdownString.length; 50 | } else { 51 | i = indexOfClosingTag + closingTag.length - 1; 52 | } 53 | } else { 54 | result += '<' + lastTag + char; 55 | } 56 | lastTag = ''; 57 | } else { 58 | if (inOpeningTag) { 59 | lastTag += char; 60 | } else { 61 | result += char; 62 | } 63 | } 64 | } 65 | 66 | return result; 67 | } 68 | 69 | module.exports = { 70 | randid, 71 | validid, 72 | sanitizeDangerousHtml, 73 | } 74 | 75 | -------------------------------------------------------------------------------- /static/assets/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /static/assets/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /static/assets/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /static/assets/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /static/assets/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /static/assets/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /static/assets/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /static/assets/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /static/assets/favicon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/favicon-128.png -------------------------------------------------------------------------------- /static/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/favicon-16x16.png -------------------------------------------------------------------------------- /static/assets/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/favicon-196x196.png -------------------------------------------------------------------------------- /static/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/favicon-32x32.png -------------------------------------------------------------------------------- /static/assets/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/assets/favicon-96x96.png -------------------------------------------------------------------------------- /static/css/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | scroll-behavior: smooth; 6 | } 7 | 8 | html, 9 | body, 10 | div, 11 | input, 12 | label, 13 | textarea, 14 | blockquote, 15 | code, 16 | pre { 17 | box-sizing: border-box; 18 | } 19 | 20 | h1, 21 | h2, 22 | h3, 23 | p { 24 | color: #333; 25 | } 26 | 27 | p, 28 | li { 29 | line-height: 1.5em; 30 | } 31 | 32 | p.warning { 33 | font-style: italic; 34 | color: #777; 35 | } 36 | 37 | code, pre { 38 | font-family: 'Dank Mono', 'Menlo', 'Monaco', monospace; 39 | background: #e3e3e3; 40 | border-radius: 4px; 41 | font-size: 1em; 42 | padding: 6px 8px; 43 | } 44 | 45 | pre { 46 | overflow-x: auto; 47 | } 48 | 49 | pre code { 50 | padding: 0; 51 | } 52 | 53 | code { 54 | padding: 0 5px 2px 5px; 55 | } 56 | 57 | body { 58 | font-family: system-ui, 'Helvetica', 'Segoe UI', sans-serif; 59 | background: #f8f8f8; 60 | } 61 | 62 | main { 63 | margin: 24px auto; 64 | width: 60%; 65 | max-width: 700px; 66 | } 67 | 68 | a { 69 | color: #333; 70 | } 71 | 72 | a:hover { 73 | opacity: .6; 74 | } 75 | 76 | input[type="checkbox"] { 77 | width: auto; 78 | } 79 | 80 | header { 81 | display: flex; 82 | flex-direction: row; 83 | align-items: center; 84 | justify-content: space-between; 85 | } 86 | 87 | header p { 88 | text-align: right 89 | } 90 | 91 | header p a { 92 | color: #333; 93 | position: relative; 94 | } 95 | 96 | header p a::after { 97 | content: attr(title); 98 | display: block; 99 | opacity: 0; 100 | color: #333; 101 | position: absolute; 102 | top: calc(100% + 6px); 103 | right: 0; 104 | transform: translate(0, -6px); 105 | white-space: nowrap; 106 | background: #fff; 107 | border-radius: 4px; 108 | padding: 4px 6px; 109 | box-shadow: 0px 3px 8px -1px rgba(0, 0, 0, .3); 110 | transition: opacity .2s, transform .2s; 111 | pointer-events: none; 112 | } 113 | 114 | header p a:hover::after { 115 | opacity: 1; 116 | transform: translate(0, 0); 117 | } 118 | 119 | label { 120 | color: #333; 121 | font-weight: bold; 122 | margin: 8px 0; 123 | } 124 | 125 | input, 126 | button, 127 | textarea { 128 | padding: 6px 12px; 129 | width: 100%; 130 | outline: none; 131 | background: #fff; 132 | border-radius: 4px; 133 | font-size: 16px; 134 | border: 2px solid #aaa; 135 | font-family: 'Menlo', 'Monaco', 'Courier', monospace; 136 | box-shadow: none; 137 | } 138 | 139 | input::placeholder, 140 | textarea::placeholder { 141 | color: #999; 142 | } 143 | 144 | input:focus, 145 | textarea:focus { 146 | border: 2px solid #333; 147 | } 148 | 149 | .header { 150 | display: flex; 151 | flex-direction: row; 152 | justify-content: space-between; 153 | } 154 | 155 | .header, 156 | input, 157 | textarea { 158 | width: 100%; 159 | } 160 | 161 | .inputGroup { 162 | display: flex; 163 | flex-direction: column; 164 | align-items: flex-start; 165 | justify-content: center; 166 | margin-bottom: 16px; 167 | flex-grow: 1; 168 | } 169 | 170 | .inputGroup.submit { 171 | margin-left: 16px; 172 | width: 180px; 173 | flex-grow: 0; 174 | } 175 | 176 | textarea { 177 | resize: vertical; 178 | min-height: 640px; 179 | } 180 | 181 | button, 182 | input[type="submit"] { 183 | padding: 6px 12px; 184 | background: #333; 185 | color: #fff; 186 | cursor: pointer; 187 | display: block; 188 | font-weight: bold; 189 | transition: opacity .2s; 190 | border-color: #333; 191 | } 192 | 193 | button:hover, 194 | input[type="submit"]:hover, 195 | input[type="submit"]:focus { 196 | opacity: .7; 197 | } 198 | 199 | .hidden { 200 | visibility: hidden; 201 | } 202 | 203 | blockquote { 204 | border-left: 2px solid #333; 205 | padding-left: 12px; 206 | } 207 | 208 | p a { 209 | color: #333; 210 | } 211 | 212 | img { 213 | max-width: 100%; 214 | } 215 | 216 | .orMark { 217 | color: #333; 218 | font-weight: bold; 219 | text-align: center; 220 | margin: 12px 0; 221 | } 222 | 223 | #expandNoteButton { 224 | margin: 8px 0; 225 | width: auto; 226 | } 227 | 228 | .inputHeader { 229 | width: 100%; 230 | display: flex; 231 | flex-direction: row; 232 | align-items: center; 233 | justify-content: space-between; 234 | } 235 | 236 | @media only screen and (max-width: 700px) { 237 | main { 238 | width: 96%; 239 | max-width: unset; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thesephist/zone/f4e4a04a61a6c6ec2f3fe0f268382b58afc14e31/static/favicon.ico -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | linus.zone 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |

linus.zone

31 |

32 | What's this? 33 | | 34 | Linus's website 35 |

36 |
37 | 38 |

Note: because this is a service I run for personal use, anything you create here may be deleted/overwritten without warning if you haven't checked with me beforehand. If you'd like to use this service, feel free to deploy the open-source version on your own!

39 |

Spammers will be blocked without warning.

40 | 41 |
42 |
43 |
44 | 45 | 46 |
47 |
48 | 49 | 50 |
51 |
52 | 53 |
54 | 55 | 56 |
57 | 58 |
59 | 60 |
61 | 62 | 63 |
64 | 65 |
or
66 | 67 |
68 |
69 | 70 | 71 |
72 | 73 |
74 | 75 |
76 |
77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const idInput = document.querySelector('#id'); 3 | const requiredMessage = document.querySelector('.requiredMessage'); 4 | const uriInput = document.querySelector('#content_uri'); 5 | const noteInput = document.querySelector('#content_note'); 6 | 7 | let userTypedInput = false; 8 | 9 | function setPasswordGroupVisibility(displayed) { 10 | requiredMessage.style.display = displayed ? '' : 'none'; 11 | } 12 | 13 | function submitForm() { 14 | document.querySelector('form').submit(); 15 | } 16 | 17 | async function checkIfShouldShowPasswordRequired() { 18 | const id = idInput.value; 19 | if (id) { 20 | try { 21 | const locked = await fetch(`/${id}/locked`).then(resp => resp.text()); 22 | return +locked > 0; 23 | } catch (e) { 24 | // shouldn't happen, but just in case, reveal the password field 25 | return true; 26 | } 27 | } else { 28 | return false; 29 | } 30 | } 31 | 32 | async function populateContentForExistingNote() { 33 | const id = idInput.value; 34 | if (id) { 35 | try { 36 | const content = await fetch(`/${id}/content`).then(resp => resp.json()); 37 | if (content['type'] == 'none') { 38 | if (!userTypedInput) { 39 | uriInput.value = ''; 40 | noteInput.value = ''; 41 | } 42 | return; 43 | } 44 | 45 | if (userTypedInput) { 46 | const replace = confirm(`Do you want to replace what you've typed with the existing content for id ${id}?`); 47 | if (!replace) { 48 | return; 49 | } 50 | } 51 | 52 | if (content['type'] == 'note') { 53 | uriInput.value = ''; 54 | noteInput.value = content['content']; 55 | } 56 | else if (content['type'] == 'uri') { 57 | uriInput.value = content['content']; 58 | noteInput.value = ''; 59 | } 60 | 61 | userTypedInput = false; 62 | 63 | } catch (e) { 64 | console.error(`Error fetching content for preexisting note: ${e}`); 65 | } 66 | } 67 | } 68 | 69 | function handleKeydown(evt) { 70 | if (evt.key === 'Tab' && !evt.shiftKey) { 71 | evt.preventDefault(); 72 | const idx = evt.target.selectionStart; 73 | if (idx !== null) { 74 | const front = evt.target.value.substr(0, idx); 75 | const back = evt.target.value.substr(idx); 76 | evt.target.value = front + ' ' + back; 77 | evt.target.setSelectionRange(idx + 4, idx + 4); 78 | } 79 | } 80 | 81 | // metaKey detects the command key on macOS 82 | if (evt.key === 'Enter' && (evt.ctrlKey || evt.metaKey)) { 83 | submitForm(); 84 | } 85 | } 86 | 87 | function debounce(fn, duration) { 88 | let timeout = undefined; 89 | return () => { 90 | if (timeout) clearTimeout(timeout); 91 | timeout = setTimeout(fn, duration); 92 | } 93 | } 94 | 95 | const onIdInput = async () => { 96 | const [showPasswordRequired, _] = [ 97 | await checkIfShouldShowPasswordRequired(), 98 | await populateContentForExistingNote(), 99 | ]; 100 | setPasswordGroupVisibility(showPasswordRequired); 101 | } 102 | 103 | setPasswordGroupVisibility(false); 104 | idInput.addEventListener('input', debounce(onIdInput, 400)); 105 | uriInput.addEventListener('input', () => { 106 | userTypedInput = uriInput.value.length > 0; 107 | }); 108 | noteInput.addEventListener('input', () => { 109 | userTypedInput = noteInput.value.length > 0; 110 | }); 111 | noteInput.addEventListener('keydown', handleKeydown); 112 | 113 | function expandTextarea(evt) { 114 | evt.preventDefault(); 115 | const lastHeight = noteInput.getBoundingClientRect().height; 116 | noteInput.style.height = `${lastHeight + 150}px`; 117 | } 118 | document.getElementById('expandNoteButton').addEventListener('click', expandTextarea); 119 | 120 | document.addEventListener('DOMContentLoaded', () => { 121 | // allow for linus.zone/#<:slug> to go straight to editing 122 | if (window.location.hash.length > 1) { 123 | const noteToEdit = window.location.hash.substr(1); 124 | idInput.value = noteToEdit; 125 | onIdInput(); 126 | } 127 | }); 128 | 129 | }()); 130 | 131 | -------------------------------------------------------------------------------- /static/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %title% | linus.zone 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | %content% 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.8: 6 | version "1.3.8" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 8 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 9 | dependencies: 10 | mime-types "~2.1.34" 11 | negotiator "0.6.3" 12 | 13 | array-flatten@1.1.1: 14 | version "1.1.1" 15 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 16 | integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= 17 | 18 | body-parser@1.20.0, body-parser@^1.20.0: 19 | version "1.20.0" 20 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" 21 | integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== 22 | dependencies: 23 | bytes "3.1.2" 24 | content-type "~1.0.4" 25 | debug "2.6.9" 26 | depd "2.0.0" 27 | destroy "1.2.0" 28 | http-errors "2.0.0" 29 | iconv-lite "0.4.24" 30 | on-finished "2.4.1" 31 | qs "6.10.3" 32 | raw-body "2.5.1" 33 | type-is "~1.6.18" 34 | unpipe "1.0.0" 35 | 36 | bytes@3.1.2: 37 | version "3.1.2" 38 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 39 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 40 | 41 | call-bind@^1.0.0: 42 | version "1.0.2" 43 | resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" 44 | integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== 45 | dependencies: 46 | function-bind "^1.1.1" 47 | get-intrinsic "^1.0.2" 48 | 49 | content-disposition@0.5.4: 50 | version "0.5.4" 51 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 52 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 53 | dependencies: 54 | safe-buffer "5.2.1" 55 | 56 | content-type@~1.0.4: 57 | version "1.0.4" 58 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 59 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== 60 | 61 | cookie-signature@1.0.6: 62 | version "1.0.6" 63 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 64 | integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= 65 | 66 | cookie@0.5.0: 67 | version "0.5.0" 68 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" 69 | integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 70 | 71 | debug@2.6.9: 72 | version "2.6.9" 73 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 74 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 75 | dependencies: 76 | ms "2.0.0" 77 | 78 | depd@2.0.0: 79 | version "2.0.0" 80 | resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 81 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 82 | 83 | destroy@1.2.0: 84 | version "1.2.0" 85 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" 86 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 87 | 88 | ee-first@1.1.1: 89 | version "1.1.1" 90 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 91 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= 92 | 93 | encodeurl@~1.0.2: 94 | version "1.0.2" 95 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 96 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= 97 | 98 | escape-html@~1.0.3: 99 | version "1.0.3" 100 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 101 | integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= 102 | 103 | etag@~1.8.1: 104 | version "1.8.1" 105 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 106 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= 107 | 108 | express@^4.18.0: 109 | version "4.18.0" 110 | resolved "https://registry.yarnpkg.com/express/-/express-4.18.0.tgz#7a426773325d0dd5406395220614c0db10b6e8e2" 111 | integrity sha512-EJEXxiTQJS3lIPrU1AE2vRuT7X7E+0KBbpm5GSoK524yl0K8X+er8zS2P14E64eqsVNoWbMCT7MpmQ+ErAhgRg== 112 | dependencies: 113 | accepts "~1.3.8" 114 | array-flatten "1.1.1" 115 | body-parser "1.20.0" 116 | content-disposition "0.5.4" 117 | content-type "~1.0.4" 118 | cookie "0.5.0" 119 | cookie-signature "1.0.6" 120 | debug "2.6.9" 121 | depd "2.0.0" 122 | encodeurl "~1.0.2" 123 | escape-html "~1.0.3" 124 | etag "~1.8.1" 125 | finalhandler "1.2.0" 126 | fresh "0.5.2" 127 | http-errors "2.0.0" 128 | merge-descriptors "1.0.1" 129 | methods "~1.1.2" 130 | on-finished "2.4.1" 131 | parseurl "~1.3.3" 132 | path-to-regexp "0.1.7" 133 | proxy-addr "~2.0.7" 134 | qs "6.10.3" 135 | range-parser "~1.2.1" 136 | safe-buffer "5.2.1" 137 | send "0.18.0" 138 | serve-static "1.15.0" 139 | setprototypeof "1.2.0" 140 | statuses "2.0.1" 141 | type-is "~1.6.18" 142 | utils-merge "1.0.1" 143 | vary "~1.1.2" 144 | 145 | finalhandler@1.2.0: 146 | version "1.2.0" 147 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" 148 | integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== 149 | dependencies: 150 | debug "2.6.9" 151 | encodeurl "~1.0.2" 152 | escape-html "~1.0.3" 153 | on-finished "2.4.1" 154 | parseurl "~1.3.3" 155 | statuses "2.0.1" 156 | unpipe "~1.0.0" 157 | 158 | forwarded@0.2.0: 159 | version "0.2.0" 160 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" 161 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 162 | 163 | fresh@0.5.2: 164 | version "0.5.2" 165 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 166 | integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= 167 | 168 | function-bind@^1.1.1: 169 | version "1.1.1" 170 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 171 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 172 | 173 | get-intrinsic@^1.0.2: 174 | version "1.1.1" 175 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" 176 | integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== 177 | dependencies: 178 | function-bind "^1.1.1" 179 | has "^1.0.3" 180 | has-symbols "^1.0.1" 181 | 182 | has-symbols@^1.0.1: 183 | version "1.0.3" 184 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 185 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 186 | 187 | has@^1.0.3: 188 | version "1.0.3" 189 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 190 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 191 | dependencies: 192 | function-bind "^1.1.1" 193 | 194 | http-errors@2.0.0: 195 | version "2.0.0" 196 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" 197 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 198 | dependencies: 199 | depd "2.0.0" 200 | inherits "2.0.4" 201 | setprototypeof "1.2.0" 202 | statuses "2.0.1" 203 | toidentifier "1.0.1" 204 | 205 | iconv-lite@0.4.24: 206 | version "0.4.24" 207 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 208 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 209 | dependencies: 210 | safer-buffer ">= 2.1.2 < 3" 211 | 212 | inherits@2.0.4: 213 | version "2.0.4" 214 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 215 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 216 | 217 | ipaddr.js@1.9.1: 218 | version "1.9.1" 219 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 220 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 221 | 222 | marked@^4.0.14: 223 | version "4.0.14" 224 | resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.14.tgz#7a3a5fa5c80580bac78c1ed2e3b84d7bd6fc3870" 225 | integrity sha512-HL5sSPE/LP6U9qKgngIIPTthuxC0jrfxpYMZ3LdGDD3vTnLs59m2Z7r6+LNDR3ToqEQdkKd6YaaEfJhodJmijQ== 226 | 227 | media-typer@0.3.0: 228 | version "0.3.0" 229 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 230 | integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= 231 | 232 | merge-descriptors@1.0.1: 233 | version "1.0.1" 234 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 235 | integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= 236 | 237 | methods@~1.1.2: 238 | version "1.1.2" 239 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 240 | integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= 241 | 242 | mime-db@1.52.0: 243 | version "1.52.0" 244 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 245 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 246 | 247 | mime-types@~2.1.24, mime-types@~2.1.34: 248 | version "2.1.35" 249 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 250 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 251 | dependencies: 252 | mime-db "1.52.0" 253 | 254 | mime@1.6.0: 255 | version "1.6.0" 256 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 257 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 258 | 259 | ms@2.0.0: 260 | version "2.0.0" 261 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 262 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 263 | 264 | ms@2.1.3: 265 | version "2.1.3" 266 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 267 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 268 | 269 | nanoid@^2.1.0: 270 | version "2.1.11" 271 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" 272 | integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA== 273 | 274 | negotiator@0.6.3: 275 | version "0.6.3" 276 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 277 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 278 | 279 | object-inspect@^1.9.0: 280 | version "1.12.0" 281 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" 282 | integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== 283 | 284 | on-finished@2.4.1: 285 | version "2.4.1" 286 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" 287 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 288 | dependencies: 289 | ee-first "1.1.1" 290 | 291 | parseurl@~1.3.3: 292 | version "1.3.3" 293 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 294 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 295 | 296 | path-to-regexp@0.1.7: 297 | version "0.1.7" 298 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 299 | integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= 300 | 301 | proxy-addr@~2.0.7: 302 | version "2.0.7" 303 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" 304 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 305 | dependencies: 306 | forwarded "0.2.0" 307 | ipaddr.js "1.9.1" 308 | 309 | qs@6.10.3: 310 | version "6.10.3" 311 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" 312 | integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== 313 | dependencies: 314 | side-channel "^1.0.4" 315 | 316 | range-parser@~1.2.1: 317 | version "1.2.1" 318 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 319 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 320 | 321 | raw-body@2.5.1: 322 | version "2.5.1" 323 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" 324 | integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== 325 | dependencies: 326 | bytes "3.1.2" 327 | http-errors "2.0.0" 328 | iconv-lite "0.4.24" 329 | unpipe "1.0.0" 330 | 331 | safe-buffer@5.2.1: 332 | version "5.2.1" 333 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 334 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 335 | 336 | "safer-buffer@>= 2.1.2 < 3": 337 | version "2.1.2" 338 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 339 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 340 | 341 | send@0.18.0: 342 | version "0.18.0" 343 | resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" 344 | integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== 345 | dependencies: 346 | debug "2.6.9" 347 | depd "2.0.0" 348 | destroy "1.2.0" 349 | encodeurl "~1.0.2" 350 | escape-html "~1.0.3" 351 | etag "~1.8.1" 352 | fresh "0.5.2" 353 | http-errors "2.0.0" 354 | mime "1.6.0" 355 | ms "2.1.3" 356 | on-finished "2.4.1" 357 | range-parser "~1.2.1" 358 | statuses "2.0.1" 359 | 360 | serve-static@1.15.0: 361 | version "1.15.0" 362 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" 363 | integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== 364 | dependencies: 365 | encodeurl "~1.0.2" 366 | escape-html "~1.0.3" 367 | parseurl "~1.3.3" 368 | send "0.18.0" 369 | 370 | setprototypeof@1.2.0: 371 | version "1.2.0" 372 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 373 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 374 | 375 | shortid@^2.2.16: 376 | version "2.2.16" 377 | resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.16.tgz#b742b8f0cb96406fd391c76bfc18a67a57fe5608" 378 | integrity sha512-Ugt+GIZqvGXCIItnsL+lvFJOiN7RYqlGy7QE41O3YC1xbNSeDGIRO7xg2JJXIAj1cAGnOeC1r7/T9pgrtQbv4g== 379 | dependencies: 380 | nanoid "^2.1.0" 381 | 382 | side-channel@^1.0.4: 383 | version "1.0.4" 384 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" 385 | integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== 386 | dependencies: 387 | call-bind "^1.0.0" 388 | get-intrinsic "^1.0.2" 389 | object-inspect "^1.9.0" 390 | 391 | statuses@2.0.1: 392 | version "2.0.1" 393 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 394 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 395 | 396 | toidentifier@1.0.1: 397 | version "1.0.1" 398 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 399 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 400 | 401 | type-is@~1.6.18: 402 | version "1.6.18" 403 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 404 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 405 | dependencies: 406 | media-typer "0.3.0" 407 | mime-types "~2.1.24" 408 | 409 | unpipe@1.0.0, unpipe@~1.0.0: 410 | version "1.0.0" 411 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 412 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= 413 | 414 | utils-merge@1.0.1: 415 | version "1.0.1" 416 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 417 | integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= 418 | 419 | vary@~1.1.2: 420 | version "1.1.2" 421 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 422 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 423 | --------------------------------------------------------------------------------