summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/.gitignore49
-rw-r--r--app/LICENSE21
-rw-r--r--app/app.js50
-rw-r--r--app/lib/otp.js29
-rw-r--r--app/lib/pipe.js27
-rw-r--r--app/lib/router.js40
-rw-r--r--app/lib/socket.js70
-rw-r--r--app/lib/static.js78
-rw-r--r--app/node-server.service13
-rw-r--r--app/package-lock.json444
-rw-r--r--app/package.json20
-rw-r--r--app/public/404.html1
-rw-r--r--app/public/500.html1
-rw-r--r--app/public/index.html64
-rw-r--r--app/public/main.js18
-rw-r--r--app/public/manifest.webmanifest26
-rw-r--r--app/public/sock.js43
-rw-r--r--app/public/style.css14
-rw-r--r--app/public/sw1.js29
-rw-r--r--app/public/sw2.js40
20 files changed, 1077 insertions, 0 deletions
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..bfd1621
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,49 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+*.sock
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+users
+private
diff --git a/app/LICENSE b/app/LICENSE
new file mode 100644
index 0000000..e30e62b
--- /dev/null
+++ b/app/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Alexis Hovorka <[email protected]>
+
+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.
diff --git a/app/app.js b/app/app.js
new file mode 100644
index 0000000..d9cd39c
--- /dev/null
+++ b/app/app.js
@@ -0,0 +1,50 @@
+"use strict";
+
+const http = require("http");
+const fs = require("fs");
+
+const argon2 = require("argon2");
+
+const Router = require("./lib/router");
+const Static = require("./lib/static");
+//const Socket = require("./lib/socket");
+//const pipe = require("./lib/pipe");
+//const otp = require("./lib/otp");
+
+if (!fs.existsSync("./logs")) fs.mkdirSync("./logs");
+if (!fs.existsSync("./users")) fs.mkdirSync("./users");
+if (!fs.existsSync("./private")) fs.mkdirSync("./private");
+
+Math.clamp = Math.clamp || ((x,l,h) => Math.max(l,Math.min(x,h)));
+const PORT = Math.clamp(+process.env.PORT||8080, 1, 65535);
+const HOST = process.env.HOST||"0.0.0.0";
+
+const server = http.createServer();
+const stat = new Static("./public");
+//const wss = new Socket(server);
+const app = new Router();
+
+const cors = fn => (req, res, ...rest) => {
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST");
+ fn(req, res, ...rest); };
+
+const sj = (res, data) => {
+ res.setHeader("Content-Type", "application/json");
+ res.end(JSON.stringify(data)); };
+
+//app.get("/", (req, res) => {
+// res.writeHead(302, {"Location":"https://alexishovorka.com/"});
+// res.end();
+//});
+
+//await argon2.hash(password, {type: argon2.argon2id});
+//await argon2.verify(hash, password);
+
+server.on("request", (req, res) => {
+ console.log(`${Date.now()} ${req.method} ${req.url}`);
+ app.route(req, res) || stat.route(req, res);
+});
+
+server.listen(PORT, HOST);
+console.log(`${Date.now()} Running on http://${HOST}:${PORT}`);
diff --git a/app/lib/otp.js b/app/lib/otp.js
new file mode 100644
index 0000000..751edf4
--- /dev/null
+++ b/app/lib/otp.js
@@ -0,0 +1,29 @@
+"use strict";
+
+const { createHmac } = require("crypto");
+const getHMAC = (k,c) => { const h = createHmac("sha1", k);
+ h.update(c, "hex"); return h.digest("hex"); }
+
+const h2d = s => parseInt(s, 16);
+const d2h = s => Math.floor(s).toString(16).padStart(2,0);
+
+const b32a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+const b32u = s => Uint8Array.from(s.split("").reduce((a,c) =>
+ a+b32a.indexOf(c.toUpperCase()).toString(2).padStart(5,0), "")
+ .match(/.{8}/g).map(b => parseInt(b,2)));
+
+module.exports = function totp(secret, { expiry=30,
+ now=Math.round(new Date().getTime()/1000), length=6 }={}) {
+ const time = d2h(now/expiry).padStart(16,0);
+ const hmac = getHMAC(b32u(secret), time);
+ const offset = h2d(hmac.substring(hmac.length - 1));
+ const otp = (h2d(hmac.substr(2*offset, 8)) & h2d("7fffffff")) + "";
+ return otp.padStart(length,0).substr(-length);
+}
+
+module.exports.check = function check(token, secret, { expiry=30,
+ now=Math.round(new Date().getTime()/1000), length=6, window=0 }={}) {
+ const i = Array(window*2+1).fill().map((e,i) => now+(expiry*(i-window)))
+ .findIndex(n => token === this(secret, {now:n, expiry, length}));
+ return i>=0 && i-window;
+}
diff --git a/app/lib/pipe.js b/app/lib/pipe.js
new file mode 100644
index 0000000..e329b2b
--- /dev/null
+++ b/app/lib/pipe.js
@@ -0,0 +1,27 @@
+"use strict";
+
+const { spawn } = require("child_process");
+
+module.exports = function pipe({ command, flags, stdin="", buffer } = {}) {
+ return new Promise((resolve, reject) => {
+ const child = spawn(command, flags);
+ let stdout = (buffer?[]:"");
+ let stderr = "";
+
+ child.stderr.on("data", d => stderr += d);
+ child.stdout.on("data", d => (buffer?
+ stdout.push(d):stdout+=d));
+
+ child.on("close", code => {
+ const res = { code, stderr, stdout:
+ (buffer?Buffer.concat(stdout):stdout) };
+
+ if (code) reject(res);
+ else resolve(res);
+ });
+
+ //child.stdin.setEncoding("utf-8");
+ child.stdin.write(stdin);
+ child.stdin.end();
+ });
+}
diff --git a/app/lib/router.js b/app/lib/router.js
new file mode 100644
index 0000000..8c0a2ef
--- /dev/null
+++ b/app/lib/router.js
@@ -0,0 +1,40 @@
+"use strict"; // https://github.com/mixu/minimal
+
+const url = require("url");
+const degroup = path => Object.assign(path, path.groups);
+
+class Router {
+ constructor() {
+ this.routes = [];
+ }
+
+ route(req, res) {
+ const pathname = url.parse(req.url).pathname;
+ return this.routes.some(route => {
+ const isMatch = route.method === req.method && route.re.test(pathname);
+ if (isMatch) route.cb(req, res, degroup(route.re.exec(pathname)));
+ return isMatch;
+ });
+ }
+
+ gather(cb, max) { // Handle POST data
+ return (req, res, match) => {
+ let data = "";
+ req.on("data", chunk => {
+ if ((data += chunk).length > (max||1e6)) // ~1MB
+ req.connection.destroy();
+ }).on("end", () => cb(req, res, match, data));
+ };
+ }
+
+ gpost(re, cb, max) { // Laziness
+ this.post(re, this.gather(cb, max));
+ }
+}
+
+["get", "post", "put", "delete"].forEach(method =>
+ Router.prototype[method] = function(re, cb) {
+ this.routes.push({method: method.toUpperCase(), cb,
+ re: (re instanceof RegExp)? re : new RegExp(`^${re}$`)})});
+
+module.exports = Router;
diff --git a/app/lib/socket.js b/app/lib/socket.js
new file mode 100644
index 0000000..88fdd49
--- /dev/null
+++ b/app/lib/socket.js
@@ -0,0 +1,70 @@
+"use strict";
+
+const WebSocket = require("ws");
+
+class Client {
+ constructor(ws) {
+ this.ws = ws;
+ this.handlers = {};
+
+ ws.on("message", msg => { const d = JSON.parse(msg);
+ (this.handlers[d.type]||[]).forEach(cb => cb(d.data));
+ });
+
+ const closecb = () =>
+ (this.handlers["close"]||[]).forEach(cb => cb());
+
+ ws.on("close", closecb);
+ ws.on("error", closecb);
+ }
+
+ on(type, cb) {
+ (this.handlers[type]||(this.handlers[type]=[])).push(cb);
+ }
+
+ send(type, data) {
+ this.ws.send(JSON.stringify({type, data}));
+ }
+}
+
+module.exports = class Socket {
+ constructor(server) {
+ this.wss = new WebSocket.Server({server});
+ this.handlers = {};
+ this.clients = [];
+
+ this.wss.on("connection", ws => {
+ const client = new Client(ws);
+ this.clients.push(client);
+
+ ws.on("message", msg => { const d = JSON.parse(msg);
+ (this.handlers[d.type]||[]).forEach(cb => cb(client, d.data));
+ });
+
+ let pingTimeout;
+ const ping = () => { client.send("ping", {});
+ pingTimeout = setTimeout(() => ws.terminate(), 3e4); };
+ client.on("pong", () => { clearTimeout(pingTimeout);
+ setTimeout(ping, 1e3); }); ping();
+
+ const closecb = () => {
+ const i = this.clients.indexOf(client); if (i < 0) return;
+ (this.handlers["close"]||[]).forEach(cb => cb(client));
+ this.clients.splice(i, 1);
+ }
+
+ ws.on("close", closecb);
+ ws.on("error", closecb);
+
+ (this.handlers["open"]||[]).forEach(cb => cb(client));
+ });
+ }
+
+ on(type, cb) {
+ (this.handlers[type]||(this.handlers[type]=[])).push(cb);
+ }
+
+ sendAll(type, data) {
+ this.clients.forEach(c => c.send(type, data));
+ }
+};
diff --git a/app/lib/static.js b/app/lib/static.js
new file mode 100644
index 0000000..ef48ae6
--- /dev/null
+++ b/app/lib/static.js
@@ -0,0 +1,78 @@
+"use strict";
+
+const Path = require("path");
+const url = require("url");
+const fs = require("fs");
+
+const mimeTypes = {
+ ".html": "text/html",
+ ".css": "text/css",
+ ".js": "text/javascript",
+ ".json": "application/json",
+ ".wasm": "application/wasm",
+ ".pdf": "application/pdf",
+ ".txt": "text/plain",
+ ".md": "text/markdown",
+
+ ".png": "image/png",
+ ".jpg": "image/jpg",
+ ".svg": "image/svg+xml",
+ ".gif": "image/gif",
+ ".ico": "image/x-icon",
+
+ ".wav": "audio/wav",
+ ".mp3": "audio/mpeg",
+ ".mp4": "video/mp4",
+
+ ".eot": "application/vnd.ms-fontobject",
+ ".ttf": "application/font-ttf",
+ ".woff": "application/font-woff",
+
+ ".gltf": "model/gltf+json",
+ ".glb": "model/gltf-binary",
+};
+
+module.exports = class Static {
+ constructor(root) {
+ this.root = `${root||"."}`;
+ this.e404 = fs.readFileSync(`${this.root}/404.html`);
+ this.e500 = fs.readFileSync(`${this.root}/500.html`);
+ }
+
+ route(req, res) {
+ if (req.method !== "GET") {
+ res.writeHead(405, {"Content-Type": "text/plain"});
+ res.end("405 Method Not Allowed");
+ return;
+ }
+
+ const pathname = url.parse(req.url).pathname;
+ const sane = Path.normalize(pathname).replace(/^(\.\.\/)+/, "");
+ let path = `${this.root}${sane}`; //Path.join(__dirname, sane);
+
+ fs.stat(path, (err, stats) => {
+ if (err) { if (["EACCES", "ENOENT", "EPERM"].includes(err.code)) {
+ res.writeHead(404, {"Content-Type": "text/html"});
+ return res.end(this.e404);
+ } else { console.log(err);
+ res.writeHead(500, {"Content-Type": "text/html"});
+ return res.end(this.e500);
+ }}
+
+ if (stats.isDirectory()) path += "/index.html";
+ const ext = `${Path.extname(path)}`.toLowerCase();
+ const stream = fs.createReadStream(path);
+
+ stream.on("error", e => { console.log(e);
+ res.writeHead(500, {"Content-Type": "text/html"});
+ //res.end(`<pre>Error getting the file: ${e}</pre>`);
+ res.end(this.e500);
+ });
+
+ res.setHeader("Content-Type", mimeTypes[ext]||"application/octet-stream");
+ // TODO Caching?
+
+ stream.pipe(res);
+ });
+ }
+};
diff --git a/app/node-server.service b/app/node-server.service
new file mode 100644
index 0000000..42c1e8f
--- /dev/null
+++ b/app/node-server.service
@@ -0,0 +1,13 @@
+[Service]
+ExecStart=/usr/bin/node /srv/http/appname/app.js
+Restart=always
+WorkingDirectory=/srv/http/appname
+StandardOutput=syslog
+StandardError=syslog
+SyslogIdentifier=appname
+User=alexis
+Group=users
+Environment=PORT=8080
+
+[Install]
+WantedBy=multi-user.target
diff --git a/app/package-lock.json b/app/package-lock.json
new file mode 100644
index 0000000..b5eb137
--- /dev/null
+++ b/app/package-lock.json
@@ -0,0 +1,444 @@
+{
+ "name": "appname",
+ "version": "0.0.1",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@mapbox/node-pre-gyp": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz",
+ "integrity": "sha512-CMGKi28CF+qlbXh26hDe6NxCd7amqeAzEqnS6IHeO6LoaKyM/n+Xw3HT1COdq8cuioOdlKdqn/hCmqPUOMOywg==",
+ "requires": {
+ "detect-libc": "^1.0.3",
+ "https-proxy-agent": "^5.0.0",
+ "make-dir": "^3.1.0",
+ "node-fetch": "^2.6.5",
+ "nopt": "^5.0.0",
+ "npmlog": "^5.0.1",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.11"
+ }
+ },
+ "@phc/format": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
+ "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+ },
+ "agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "requires": {
+ "debug": "4"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+ },
+ "aproba": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
+ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
+ },
+ "are-we-there-yet": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+ "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ }
+ },
+ "argon2": {
+ "version": "0.28.3",
+ "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.28.3.tgz",
+ "integrity": "sha512-NkEJOImg+T7nnkx6/Fy8EbjZsF20hbBBKdVP/YUxujuLTAjIODmrFeY4vVpekKwGAGDm6roXxluFQ+CIaoVrbg==",
+ "requires": {
+ "@mapbox/node-pre-gyp": "^1.0.7",
+ "@phc/format": "^1.0.0",
+ "node-addon-api": "^4.2.0",
+ "opencollective-postinstall": "^2.0.3"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
+ },
+ "color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
+ },
+ "debug": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
+ "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
+ },
+ "detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "requires": {
+ "minipass": "^3.0.0"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+ },
+ "gauge": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+ "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+ "requires": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.2",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.1",
+ "object-assign": "^4.1.1",
+ "signal-exit": "^3.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.2"
+ }
+ },
+ "glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
+ },
+ "https-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
+ "requires": {
+ "agent-base": "6",
+ "debug": "4"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "requires": {
+ "semver": "^6.0.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
+ }
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minipass": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz",
+ "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==",
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "requires": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ }
+ },
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node-addon-api": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.2.0.tgz",
+ "integrity": "sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q=="
+ },
+ "node-fetch": {
+ "version": "2.6.6",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
+ "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
+ "requires": {
+ "whatwg-url": "^5.0.0"
+ }
+ },
+ "nopt": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+ "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "npmlog": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+ "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+ "requires": {
+ "are-we-there-yet": "^2.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^3.0.0",
+ "set-blocking": "^2.0.0"
+ }
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "opencollective-postinstall": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
+ "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q=="
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+ },
+ "readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+ },
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
+ },
+ "signal-exit": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz",
+ "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ=="
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "tar": {
+ "version": "6.1.11",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz",
+ "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==",
+ "requires": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ }
+ },
+ "tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
+ },
+ "whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
+ "requires": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "wide-align": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+ "requires": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "ws": {
+ "version": "7.5.6",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
+ "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA=="
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ }
+ }
+}
diff --git a/app/package.json b/app/package.json
new file mode 100644
index 0000000..0a4ce2d
--- /dev/null
+++ b/app/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "notes-web",
+ "version": "0.0.1",
+ "description": "Zettelkasten-inspired note taking web app",
+ "author": "Alexis Hovorka <[email protected]>",
+ "license": "MIT",
+ "main": "app.js",
+ "scripts": {
+ "start": "node app.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": ""
+ },
+ "private": true,
+ "dependencies": {
+ "argon2": "^0.28.3",
+ "ws": "^7.1.0"
+ }
+}
diff --git a/app/public/404.html b/app/public/404.html
new file mode 100644
index 0000000..1a43d0a
--- /dev/null
+++ b/app/public/404.html
@@ -0,0 +1 @@
+<pre>404 Not Found</pre>
diff --git a/app/public/500.html b/app/public/500.html
new file mode 100644
index 0000000..ee07335
--- /dev/null
+++ b/app/public/500.html
@@ -0,0 +1 @@
+<pre>500 Internal Server Error</pre>
diff --git a/app/public/index.html b/app/public/index.html
new file mode 100644
index 0000000..3c435f5
--- /dev/null
+++ b/app/public/index.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>Notes</title>
+ <meta charset="UTF-8">
+
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="user-scalable=no,width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1">
+
+ <!--link rel="icon" type="image/x-icon" href="favicon.ico">
+ <link rel="apple-touch-icon" href="img/apple-touch-icon-57.png" sizes="57x57" >
+ <link rel="apple-touch-icon" href="img/apple-touch-icon-60.png" sizes="60x60" >
+ <link rel="apple-touch-icon" href="img/apple-touch-icon-72.png" sizes="72x72" >
+ <link rel="apple-touch-icon" href="img/apple-touch-icon-76.png" sizes="76x76" >
+ <link rel="apple-touch-icon" href="img/apple-touch-icon-114.png" sizes="114x114" >
+ <link rel="apple-touch-icon" href="img/apple-touch-icon-120.png" sizes="120x120" >
+ <link rel="apple-touch-icon" href="img/apple-touch-icon-144.png" sizes="144x144" >
+ <link rel="apple-touch-icon" href="img/apple-touch-icon-152.png" sizes="152x152" >
+ <link rel="apple-touch-icon" href="img/apple-touch-icon-168.png" sizes="168x168" >
+ <link rel="apple-touch-icon" href="img/apple-touch-icon-180.png" sizes="180x180" >
+ <link rel="apple-touch-startup-image" href="img/apple-splash-2048.png" sizes="2048x2732">
+ <link rel="apple-touch-startup-image" href="img/apple-splash-1668.png" sizes="1668x2224">
+ <link rel="apple-touch-startup-image" href="img/apple-splash-1536.png" sizes="1536x2048">
+ <link rel="apple-touch-startup-image" href="img/apple-splash-1125.png" sizes="1125x2436">
+ <link rel="apple-touch-startup-image" href="img/apple-splash-1242.png" sizes="1242x2208">
+ <link rel="apple-touch-startup-image" href="img/apple-splash-750.png" sizes="750x1334" >
+ <link rel="apple-touch-startup-image" href="img/apple-splash-640.png" sizes="640x1136" >
+ <link rel="icon" type="image/png" href="img/favicon-32.png" sizes="32x32" >
+ <link rel="icon" type="image/png" href="img/favicon-192.png" sizes="192x192" ><!- - Maskable: - ->
+ <link rel="icon" type="image/png" href="img/favicon-256.png" sizes="256x256" ><!- - Safe area is circle - ->
+ <link rel="icon" type="image/png" href="img/favicon-512.png" sizes="512x512" ><!- - with 40% radius - ->
+ <link rel="icon" type="image/png" href="img/favicon-96.png" sizes="96x96" >
+ <link rel="icon" type="image/png" href="img/favicon-48.png" sizes="48x48" >
+ <link rel="icon" type="image/png" href="img/favicon-16.png" sizes="16x16" >
+ <link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
+ <link rel="manifest" href="manifest.webmanifest">
+
+ <meta name="mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-app-title" content="Notes">
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
+ <meta name="application-name" content="Notes">
+ <meta name="msapplication-TileColor" content="#000000">
+ <meta name="msapplication-TileImage" content="img/mstile-144.png">
+ <meta name="msapplication-navbutton-color" content="#000000">
+ <meta name="msapplication-tooltip" content="Notes">
+ <meta name="msapplication-starturl" content="/">
+ <meta name="msapplication-tap-highlight" content="no">
+ <meta name="theme-color" content="#000000"-->
+
+ <link rel="canonical" href="https://notes.ahov.co/">
+ <meta name="author" content="Alexis Hovorka">
+ <meta name="description" content="Zettelkasten-inspired note taking web app">
+
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:400&display=swap">
+ <link rel="stylesheet" href="style.css">
+ </head>
+ <body>
+ <h1>Notes</h1>
+
+ <!--script src="sock.js"></script-->
+ <script src="main.js"></script>
+ </body>
+</html>
diff --git a/app/public/main.js b/app/public/main.js
new file mode 100644
index 0000000..ff960c2
--- /dev/null
+++ b/app/public/main.js
@@ -0,0 +1,18 @@
+document.addEventListener("DOMContentLoaded", async () => { "use strict";
+
+const secure = location.protocol === "https:";
+const $ = (s,c) => (c||document).querySelector(s);
+function $$(x,y,z,a){a=(z||document).querySelectorAll(x);if(typeof y=="function")[].forEach.call(a,y);return a}
+function m(a,b,c){c=document;b=c.createElement(b||"p");b.innerHTML=a.trim();for(a=c.createDocumentFragment();c=b.firstChild;)a.appendChild(c);return a.firstChild}
+
+//sock.init(`ws${secure?"s":""}://${location.host}/ws`);
+//sock.on("hello", e => {
+// console.log("hello", e);
+// sock.send("world", {foo:"bar"});
+//});
+
+//if ("serviceWorker" in navigator) {
+// navigator.serviceWorker.register("sw.js")
+// .then(() => console.log("Service worker registered"));
+//}
+});
diff --git a/app/public/manifest.webmanifest b/app/public/manifest.webmanifest
new file mode 100644
index 0000000..9eb9f13
--- /dev/null
+++ b/app/public/manifest.webmanifest
@@ -0,0 +1,26 @@
+{
+ "name": "Notes",
+ "short_name": "Notes",
+ "description": "Zettelkasten-inspired note taking web app",
+ "categories": ["productivity", "utilities"],
+ "lang": "en-US",
+ "start_url": "/",
+ "scope": "/",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#000000",
+ "icons": [
+ {"type": "image/png", "sizes": "48x48", "src": "img/favicon-48.png" },
+ {"type": "image/png", "sizes": "96x96", "src": "img/favicon-96.png" },
+ {"type": "image/png", "sizes": "192x192", "src": "img/favicon-192.png", "purpose": "maskable any"},
+ {"type": "image/png", "sizes": "256x256", "src": "img/favicon-256.png", "purpose": "maskable any"},
+ {"type": "image/png", "sizes": "512x512", "src": "img/favicon-512.png", "purpose": "maskable any"},
+
+ {"type": "image/png", "sizes": "72x72", "src": "img/apple-touch-icon-72.png" },
+ {"type": "image/png", "sizes": "144x144", "src": "img/apple-touch-icon-144.png"},
+ {"type": "image/png", "sizes": "168x168", "src": "img/apple-touch-icon-168.png"}
+ ],
+ "shortcuts": [
+ {"name": "New", "url": "/new"}
+ ]
+}
diff --git a/app/public/sock.js b/app/public/sock.js
new file mode 100644
index 0000000..b4905e5
--- /dev/null
+++ b/app/public/sock.js
@@ -0,0 +1,43 @@
+const sock = (() => { "use strict";
+const refresh = () => setTimeout(() => location.reload(), 1e3);
+
+let ws, pingTimeout;
+const prequeue = [];
+const handlers = {
+ "reset": [refresh],
+ "ping": [() => {
+ clearTimeout(pingTimeout);
+ pingTimeout = setTimeout(refresh, 3e4);
+ sock.send("pong", {});
+ }],
+};
+
+const sock = {
+ init: url => {
+ ws = new WebSocket(url);
+ ws.addEventListener("close", refresh);
+ ws.addEventListener("error", refresh);
+ ws.addEventListener("message", e => {
+ const d = JSON.parse(e.data);
+ (handlers[d.type]||[]).forEach(cb => cb(d.data));
+ });
+ ws.addEventListener("open", e => {
+ while (prequeue.length) sock.send(...prequeue.shift());
+ (handlers["open"]||[]).forEach(cb => cb());
+ delete handlers["open"];
+ });
+ },
+
+ on: (type, cb) => {
+ if (type === "open" && ws && ws.readyState === WebSocket.OPEN) cb();
+ else (handlers[type]||(handlers[type]=[])).push(cb);
+ },
+
+ send: (type, data) => {
+ if (ws && ws.readyState === WebSocket.OPEN)
+ ws.send(JSON.stringify({type, data}));
+ else prequeue.push([type, data]);
+ },
+};
+
+return sock })();
diff --git a/app/public/style.css b/app/public/style.css
new file mode 100644
index 0000000..655e181
--- /dev/null
+++ b/app/public/style.css
@@ -0,0 +1,14 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ font-family: "Roboto", "Noto Sans", sans-serif;
+ transition-timing-function: ease-in-out;
+}
+
+body {
+ user-select: none;
+ -webkit-user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-tap-highlight-color: transparent;
+}
diff --git a/app/public/sw1.js b/app/public/sw1.js
new file mode 100644
index 0000000..37ae5e5
--- /dev/null
+++ b/app/public/sw1.js
@@ -0,0 +1,29 @@
+// https://gist.github.com/adactio/3717b7da007a9363ddf21f584aae34af
+
+// HTML files: try the network first, then the cache.
+// Other files: try the cache first, then the network.
+// Both: cache a fresh version if possible.
+// (beware: the cache will grow and grow; there's no cleanup)
+
+const cacheName = "notes-cache-v1";
+
+addEventListener("fetch", fetchEvent => {
+ const request = fetchEvent.request;
+ if (request.method !== "GET") return;
+ fetchEvent.respondWith(async () => {
+ const fetchPromise = fetch(request);
+ fetchEvent.waitUntil(async () => {
+ const responseFromFetch = await fetchPromise;
+ const responseCopy = responseFromFetch.clone();
+ const myCache = await caches.open(cacheName);
+ return myCache.put(request, responseCopy);
+ }());
+ if (request.headers.get("Accept").includes("text/html")) {
+ try { return await fetchPromise; }
+ catch(error) { return caches.match(request); }
+ } else {
+ const responseFromCache = await caches.match(request);
+ return responseFromCache || fetchPromise;
+ }
+ }());
+});
diff --git a/app/public/sw2.js b/app/public/sw2.js
new file mode 100644
index 0000000..0dabb13
--- /dev/null
+++ b/app/public/sw2.js
@@ -0,0 +1,40 @@
+// https://googlechrome.github.io/samples/service-worker/basic/
+
+const PRECACHE = "notes-precache-v1";
+const RUNTIME = "notes-runtime";
+
+const PRECACHE_URLS = [
+ "./", // Alias for index.html
+ "index.html",
+ "style.css",
+ "main.js"
+];
+
+self.addEventListener("install", e => e.waitUntil(
+ caches.open(PRECACHE)
+ .then(cache => cache.addAll(PRECACHE_URLS))
+ .then(self.skipWaiting())));
+
+self.addEventListener("activate", e => {
+ const currentCaches = [PRECACHE, RUNTIME];
+ e.waitUntil( // Clean up old caches
+ caches.keys().then(cacheNames =>
+ cacheNames.filter(cacheName => !currentCaches.includes(cacheName))
+ ).then(cachesToDelete => Promise.all(cachesToDelete.map(cacheToDelete =>
+ caches.delete(cacheToDelete)))
+ ).then(() => self.clients.claim())
+ );
+});
+
+self.addEventListener("fetch", e => {
+ if (e.request.url.startsWith(self.location.origin)) {
+ e.respondWith(caches.match(e.request).then(cachedResponse =>
+ cachedResponse? cachedResponse
+ : caches.open(RUNTIME).then(cache =>
+ fetch(e.request).then(res =>
+ cache.put(e.request, res.clone()).then(() => res)
+ )
+ )
+ ));
+ }
+});