From 4002a353051ae170de30c382a39edb3a88b7f25a Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 5 Sep 2026 05:39:25 +0200 Subject: [PATCH] Terminal-themed markdown diary Single-user diary served as one Rust binary with the Svelte frontend embedded. - axum 0.8 + SQLite (sqlx), migrations run at startup - stateless HMAC session cookie; credentials from .env, compared in constant time - FTS5 full-text search, user input rewritten into quoted prefix terms - media uploads linked to entries by scanning bodies for /api/media/, which scopes share-token media access and entry deletion - unguessable share links (192-bit token) serving a public read-only page - vim keybindings via CodeMirror plus a modal browse layer and : command line; disabled on touch devices --- .env.example | 22 + .gitignore | 12 + Cargo.lock | 2433 +++++++++++++++++++++++++++++++ Cargo.toml | 34 + Makefile | 24 + README.md | 94 ++ migrations/0001_init.sql | 37 + src/auth.rs | 96 ++ src/config.rs | 75 + src/error.rs | 43 + src/main.rs | 87 ++ src/routes/entries.rs | 259 ++++ src/routes/media.rs | 224 +++ src/routes/mod.rs | 28 + src/routes/session.rs | 50 + src/routes/share.rs | 121 ++ src/state.rs | 11 + src/static_files.rs | 45 + web/bun.lock | 277 ++++ web/index.html | 15 + web/package.json | 30 + web/src/App.svelte | 270 ++++ web/src/app.css | 179 +++ web/src/lib/CommandLine.svelte | 118 ++ web/src/lib/Editor.svelte | 178 +++ web/src/lib/EntryPane.svelte | 163 +++ web/src/lib/Help.svelte | 86 ++ web/src/lib/Login.svelte | 87 ++ web/src/lib/MediaBrowser.svelte | 99 ++ web/src/lib/Shared.svelte | 67 + web/src/lib/Sidebar.svelte | 156 ++ web/src/lib/StatusBar.svelte | 69 + web/src/lib/api.ts | 109 ++ web/src/lib/commands.ts | 262 ++++ web/src/lib/markdown.ts | 31 + web/src/lib/store.svelte.ts | 205 +++ web/src/lib/util.ts | 63 + web/src/main.ts | 5 + web/svelte.config.js | 3 + web/tsconfig.json | 15 + web/vite.config.ts | 13 + 41 files changed, 6195 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 README.md create mode 100644 migrations/0001_init.sql create mode 100644 src/auth.rs create mode 100644 src/config.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/routes/entries.rs create mode 100644 src/routes/media.rs create mode 100644 src/routes/mod.rs create mode 100644 src/routes/session.rs create mode 100644 src/routes/share.rs create mode 100644 src/state.rs create mode 100644 src/static_files.rs create mode 100644 web/bun.lock create mode 100644 web/index.html create mode 100644 web/package.json create mode 100644 web/src/App.svelte create mode 100644 web/src/app.css create mode 100644 web/src/lib/CommandLine.svelte create mode 100644 web/src/lib/Editor.svelte create mode 100644 web/src/lib/EntryPane.svelte create mode 100644 web/src/lib/Help.svelte create mode 100644 web/src/lib/Login.svelte create mode 100644 web/src/lib/MediaBrowser.svelte create mode 100644 web/src/lib/Shared.svelte create mode 100644 web/src/lib/Sidebar.svelte create mode 100644 web/src/lib/StatusBar.svelte create mode 100644 web/src/lib/api.ts create mode 100644 web/src/lib/commands.ts create mode 100644 web/src/lib/markdown.ts create mode 100644 web/src/lib/store.svelte.ts create mode 100644 web/src/lib/util.ts create mode 100644 web/src/main.ts create mode 100644 web/svelte.config.js create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6f2225d --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# The one and only account. Both are checked in constant time on login. +DIARY_USER=narl +DIARY_PASSWORD=change-me + +# Signs session cookies. Rotate it to log every device out. 32+ random chars. +# Generate one with: openssl rand -base64 32 +DIARY_SECRET=replace-with-32-random-characters + +# Where the SQLite database and uploaded media live. +DIARY_DATA_DIR=./data + +# Address to listen on. Put a TLS terminating reverse proxy in front of this. +DIARY_BIND=127.0.0.1:4242 + +# How long a login lasts, in days. +DIARY_SESSION_DAYS=30 + +# Largest single upload, in megabytes. +DIARY_MAX_UPLOAD_MB=64 + +# Set to 1 once the app is served over HTTPS, so the cookie gets `Secure`. +DIARY_SECURE_COOKIE=0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81212f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# rust +/target + +# runtime state: sqlite db + uploaded media +/data + +# frontend +/web/dist +/web/node_modules + +# secrets +.env diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..dcb282d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2433 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-compression" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "515a1f282e33d55983c499d7e9e87082e81cbc32974825bf9032f928392d5844" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "compression-codecs" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fe67f2944eef52fc7b106b8c9450d243a88701a0c065f7f57235e76abaed7df" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.9.3", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "narl-diary" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "base64", + "dotenvy", + "hmac", + "mime_guess", + "rand", + "regex", + "rust-embed", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx", + "subtle", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-embed" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +dependencies = [ + "sha2 0.11.0", + "walkdir", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-sqlite", + "syn 2.0.119", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "sha1", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "async-compression", + "bitflags", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..03db474 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "narl-diary" +version = "0.1.0" +edition = "2021" +description = "A terminal-themed personal diary for the browser" + +[dependencies] +axum = { version = "0.8", features = ["multipart", "macros"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "fs"] } +tower = "0.5" +tokio-util = { version = "0.7", features = ["io"] } +tower-http = { version = "0.6", features = ["trace", "compression-gzip", "limit"] } +sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite", "migrate", "macros"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +dotenvy = "0.15" +uuid = { version = "1", features = ["v4"] } +rand = "0.8" +hmac = "0.12" +sha2 = "0.10" +base64 = "0.22" +subtle = "2" +mime_guess = "2" +rust-embed = { version = "8", features = ["interpolate-folder-path"] } +regex = "1" +anyhow = "1" + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +strip = true diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b3e4d44 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: dev dev-api dev-web build run check clean + +# Two terminals: `make dev-api` and `make dev-web` (http://127.0.0.1:4243). +dev-api: + cargo run + +dev-web: + cd web && bun run dev + +# Everything, as one self-contained release binary at target/release/narl-diary. +build: + cd web && bun install && bun run build + cargo build --release + +run: build + ./target/release/narl-diary + +check: + cargo clippy --all-targets -- -D warnings + cd web && bun run check + +clean: + cargo clean + rm -rf web/dist web/node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..e31773a --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# ~/diary + +A private, terminal-themed diary that runs in the browser. One user, one SQLite +file, one binary. + +- **Rust + axum** backend, **SQLite** storage, **Svelte 5** frontend. +- Markdown entries with full-text search (SQLite FTS5). +- Drag, drop or paste images, video and audio straight into an entry. +- Share any entry behind an unguessable link — revocable at any time. +- Real vim keybindings on the desktop (CodeMirror 6 + vim mode), plain taps on + mobile. +- The whole frontend is embedded in the release binary, so deploying is copying + one file. + +## Quick start + +```sh +cp .env.example .env # set DIARY_USER, DIARY_PASSWORD and DIARY_SECRET +make build # builds the frontend, then the release binary +./target/release/narl-diary +``` + +Then open . + +`DIARY_SECRET` signs the session cookie; generate one with +`openssl rand -base64 32`. Changing it logs every device out. The database and +uploads live under `DIARY_DATA_DIR` (`./data` by default) — that directory is +the entire backup. + +### Development + +```sh +make dev-api # cargo run, serving the API on :4242 +make dev-web # vite dev server on :4243, proxying /api to :4242 +``` + +In debug builds the binary reads `web/dist` from disk, so `cargo run` alone also +works once the frontend has been built at least once. + +## Keys + +The desktop UI is modal. In the browse pane: + +| key | action | +| --- | --- | +| `j` / `k` | move down / up | +| `gg` / `G` | first / last entry | +| `Enter`, `l` | open the highlighted entry | +| `o` | new entry, straight into insert mode | +| `i`, `a` | edit the open entry | +| `Esc`, `h`, `q` | back out to the list | +| `/` | full-text search (`n` clears it) | +| `s` / `y` | toggle sharing / copy the share link | +| `x`, `dd` | delete the entry | +| `:` | command line | +| `Ctrl-S` | save from anywhere | +| `?` | help | + +Inside the editor the full vim keymap is live — `w`/`b`, `dd`, `ciw`, visual +mode, macros, `:w` to write. `:help` lists every ex command; the useful ones are +`:w`, `:wq`, `:q!`, `:new [yyyy-mm-dd]`, `:date`, `:title`, `:share`, `:link`, +`:upload`, `:media`, `:search`, `:set theme=green|amber|ice` and `:set novim`. + +On a touch device modal editing is turned off and the same actions are buttons. + +## Sharing + +`:share` mints a 192-bit random token and copies `https://your-host/s/`. +That page needs no session, renders the entry read-only, and serves only the +media files that entry currently embeds — dropping a picture out of a shared +entry immediately makes it unreachable through the link. `:unshare` destroys the +token, and a new `:share` mints a different one. + +## API + +Everything except the two share routes requires the session cookie. + +| method | path | purpose | +| --- | --- | --- | +| `POST` | `/api/login`, `/api/logout` | session | +| `GET` | `/api/me` | current user | +| `GET`/`POST` | `/api/entries` | list (`?q=` searches) / create | +| `GET`/`PUT`/`DELETE` | `/api/entries/{id}` | read / update / delete | +| `POST`/`DELETE` | `/api/entries/{id}/share` | mint / revoke a share token | +| `GET`/`POST` | `/api/media` | list / upload (multipart) | +| `GET`/`DELETE` | `/api/media/{id}` | serve / delete a file | +| `GET` | `/api/share/{token}` | public: read a shared entry | +| `GET` | `/api/share/{token}/media/{id}` | public: media inside a shared entry | + +## Deploying + +Put a TLS-terminating reverse proxy in front of it, point it at `DIARY_BIND`, +and set `DIARY_SECURE_COOKIE=1`. Nothing else is needed: no database server, no +runtime dependencies, no build tools on the host. diff --git a/migrations/0001_init.sql b/migrations/0001_init.sql new file mode 100644 index 0000000..30ce4bd --- /dev/null +++ b/migrations/0001_init.sql @@ -0,0 +1,37 @@ +CREATE TABLE entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL DEFAULT '', + body TEXT NOT NULL DEFAULT '', + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + share_token TEXT UNIQUE +); + +CREATE INDEX idx_entries_created_at ON entries (created_at DESC); +CREATE INDEX idx_entries_share_token ON entries (share_token); + +CREATE TABLE media ( + id TEXT PRIMARY KEY, + entry_id INTEGER REFERENCES entries (id) ON DELETE SET NULL, + filename TEXT NOT NULL, + mime TEXT NOT NULL, + size INTEGER NOT NULL, + created_at INTEGER NOT NULL +); + +CREATE INDEX idx_media_entry_id ON media (entry_id); + +CREATE VIRTUAL TABLE entries_fts USING fts5 ( + title, body, content = 'entries', content_rowid = 'id', tokenize = 'unicode61' +); + +CREATE TRIGGER entries_ai AFTER INSERT ON entries BEGIN + INSERT INTO entries_fts (rowid, title, body) VALUES (new.id, new.title, new.body); +END; +CREATE TRIGGER entries_ad AFTER DELETE ON entries BEGIN + INSERT INTO entries_fts (entries_fts, rowid, title, body) VALUES ('delete', old.id, old.title, old.body); +END; +CREATE TRIGGER entries_au AFTER UPDATE ON entries BEGIN + INSERT INTO entries_fts (entries_fts, rowid, title, body) VALUES ('delete', old.id, old.title, old.body); + INSERT INTO entries_fts (rowid, title, body) VALUES (new.id, new.title, new.body); +END; diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..32d3f96 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,96 @@ +use axum::{ + extract::FromRequestParts, + http::{header, request::Parts}, +}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD as B64, Engine}; +use hmac::{Hmac, Mac}; +use sha2::Sha256; +use subtle::ConstantTimeEq; + +use crate::{config::Config, error::AppError, state::AppState}; + +pub const COOKIE_NAME: &str = "diary_session"; + +type HmacSha256 = Hmac; + +fn sign(secret: &[u8], payload: &str) -> String { + let mut mac = HmacSha256::new_from_slice(secret).expect("hmac accepts any key length"); + mac.update(payload.as_bytes()); + B64.encode(mac.finalize().into_bytes()) +} + +/// A session token is `base64(expiry_unix_seconds).base64(hmac)`. There is no +/// server-side session store: rotating `DIARY_SECRET` invalidates every session. +pub fn issue_token(config: &Config) -> String { + let expires = crate::now() + config.session_days * 86_400; + let payload = B64.encode(expires.to_string()); + let signature = sign(&config.secret, &payload); + format!("{payload}.{signature}") +} + +pub fn verify_token(config: &Config, token: &str) -> bool { + let Some((payload, signature)) = token.split_once('.') else { + return false; + }; + let expected = sign(&config.secret, payload); + if !bool::from(expected.as_bytes().ct_eq(signature.as_bytes())) { + return false; + } + let Ok(decoded) = B64.decode(payload) else { + return false; + }; + let Ok(expires) = String::from_utf8_lossy(&decoded).parse::() else { + return false; + }; + expires > crate::now() +} + +pub fn credentials_match(config: &Config, username: &str, password: &str) -> bool { + let user_ok = username.as_bytes().ct_eq(config.username.as_bytes()); + let pass_ok = password.as_bytes().ct_eq(config.password.as_bytes()); + bool::from(user_ok & pass_ok) +} + +pub fn session_cookie(config: &Config, token: &str) -> String { + let max_age = config.session_days * 86_400; + let secure = if config.secure_cookie { "; Secure" } else { "" }; + format!( + "{COOKIE_NAME}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={max_age}{secure}" + ) +} + +pub fn clear_cookie(config: &Config) -> String { + let secure = if config.secure_cookie { "; Secure" } else { "" }; + format!("{COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0{secure}") +} + +fn cookie_value<'a>(parts: &'a Parts, name: &str) -> Option<&'a str> { + parts + .headers + .get_all(header::COOKIE) + .iter() + .filter_map(|v| v.to_str().ok()) + .flat_map(|v| v.split(';')) + .filter_map(|pair| pair.split_once('=')) + .find(|(k, _)| k.trim() == name) + .map(|(_, v)| v.trim()) +} + +/// Extractor that rejects the request unless a valid session cookie is present. +pub struct Session; + +impl FromRequestParts for Session { + type Rejection = AppError; + + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result { + let token = cookie_value(parts, COOKIE_NAME).ok_or(AppError::Unauthorized)?; + if verify_token(&state.config, token) { + Ok(Session) + } else { + Err(AppError::Unauthorized) + } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c0441ea --- /dev/null +++ b/src/config.rs @@ -0,0 +1,75 @@ +use std::{net::SocketAddr, path::PathBuf}; + +use anyhow::{Context, Result}; + +/// Runtime configuration. Everything comes from the environment (`.env` is +/// loaded on startup), because this is a deliberately single-user application. +#[derive(Debug, Clone)] +pub struct Config { + pub username: String, + pub password: String, + pub secret: Vec, + pub bind: SocketAddr, + pub data_dir: PathBuf, + pub session_days: i64, + pub max_upload_bytes: usize, + pub secure_cookie: bool, +} + +fn var(key: &str) -> Option { + std::env::var(key).ok().filter(|v| !v.trim().is_empty()) +} + +impl Config { + pub fn from_env() -> Result { + let username = var("DIARY_USER").context("DIARY_USER must be set (see .env.example)")?; + let password = + var("DIARY_PASSWORD").context("DIARY_PASSWORD must be set (see .env.example)")?; + let secret = var("DIARY_SECRET") + .context("DIARY_SECRET must be set (see .env.example)")? + .into_bytes(); + if secret.len() < 16 { + anyhow::bail!("DIARY_SECRET must be at least 16 characters"); + } + + let bind = var("DIARY_BIND") + .unwrap_or_else(|| "127.0.0.1:4242".to_string()) + .parse() + .context("DIARY_BIND must look like 127.0.0.1:4242")?; + + let data_dir = PathBuf::from(var("DIARY_DATA_DIR").unwrap_or_else(|| "./data".into())); + + let session_days = var("DIARY_SESSION_DAYS") + .map(|v| v.parse()) + .transpose() + .context("DIARY_SESSION_DAYS must be a number")? + .unwrap_or(30); + + let max_upload_bytes = var("DIARY_MAX_UPLOAD_MB") + .map(|v| v.parse::()) + .transpose() + .context("DIARY_MAX_UPLOAD_MB must be a number")? + .unwrap_or(64) + * 1024 + * 1024; + + let secure_cookie = var("DIARY_SECURE_COOKIE") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false); + + Ok(Self { + username, + password, + secret, + bind, + data_dir, + session_days, + max_upload_bytes, + secure_cookie, + }) + } + + pub fn uploads_dir(&self) -> PathBuf { + self.data_dir.join("uploads") + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..1bf4045 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,43 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde_json::json; + +#[derive(Debug)] +pub enum AppError { + Unauthorized, + NotFound, + BadRequest(String), + Internal(anyhow::Error), +} + +pub type AppResult = Result; + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, message) = match self { + AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".to_string()), + AppError::NotFound => (StatusCode::NOT_FOUND, "not found".to_string()), + AppError::BadRequest(m) => (StatusCode::BAD_REQUEST, m), + AppError::Internal(e) => { + tracing::error!("internal error: {e:#}"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "internal error".to_string(), + ) + } + }; + (status, Json(json!({ "error": message }))).into_response() + } +} + +impl From for AppError +where + E: Into, +{ + fn from(err: E) -> Self { + AppError::Internal(err.into()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8ddf192 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,87 @@ +mod auth; +mod config; +mod error; +mod routes; +mod state; +mod static_files; + +use std::sync::Arc; + +use anyhow::{Context, Result}; +use axum::Router; +use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}; +use tower_http::{compression::CompressionLayer, trace::TraceLayer}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + +use crate::{config::Config, state::AppState}; + +/// Seconds since the Unix epoch. Every timestamp in the database uses this. +pub fn now() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock is after 1970") + .as_secs() as i64 +} + +#[tokio::main] +async fn main() -> Result<()> { + dotenvy::dotenv().ok(); + + tracing_subscriber::registry() + .with(EnvFilter::try_from_env("DIARY_LOG").unwrap_or_else(|_| EnvFilter::new("info"))) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let config = Config::from_env()?; + tokio::fs::create_dir_all(config.uploads_dir()) + .await + .with_context(|| format!("could not create {}", config.uploads_dir().display()))?; + + let db = SqlitePoolOptions::new() + .max_connections(8) + .connect_with( + SqliteConnectOptions::new() + .filename(config.data_dir.join("diary.db")) + .create_if_missing(true) + .journal_mode(SqliteJournalMode::Wal) + .foreign_keys(true), + ) + .await + .context("could not open the diary database")?; + + sqlx::migrate!("./migrations") + .run(&db) + .await + .context("could not apply database migrations")?; + + let bind = config.bind; + let max_upload = config.max_upload_bytes; + let state = AppState { + db, + config: Arc::new(config), + }; + + let app = Router::new() + .nest("/api", routes::api_router()) + .fallback(static_files::handler) + .layer(axum::extract::DefaultBodyLimit::max(max_upload + 1024 * 1024)) + .layer(CompressionLayer::new()) + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let listener = tokio::net::TcpListener::bind(bind) + .await + .with_context(|| format!("could not bind {bind}"))?; + + tracing::info!("narl-diary listening on http://{bind}"); + + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await?; + Ok(()) +} + +async fn shutdown_signal() { + let _ = tokio::signal::ctrl_c().await; + tracing::info!("shutting down"); +} diff --git a/src/routes/entries.rs b/src/routes/entries.rs new file mode 100644 index 0000000..93dcb3e --- /dev/null +++ b/src/routes/entries.rs @@ -0,0 +1,259 @@ +use axum::{ + extract::{Path, Query, State}, + Json, +}; +use serde::{Deserialize, Serialize}; +use sqlx::Row; + +use crate::{ + auth::Session, + error::{AppError, AppResult}, + routes::media, + state::AppState, +}; + +#[derive(Serialize)] +pub struct EntrySummary { + pub id: i64, + pub title: String, + pub excerpt: String, + pub created_at: i64, + pub updated_at: i64, + pub shared: bool, + pub share_token: Option, +} + +#[derive(Serialize)] +pub struct Entry { + pub id: i64, + pub title: String, + pub body: String, + pub created_at: i64, + pub updated_at: i64, + pub shared: bool, + pub share_token: Option, +} + +#[derive(Deserialize)] +pub struct ListQuery { + #[serde(default)] + pub q: Option, + #[serde(default)] + pub limit: Option, + #[serde(default)] + pub offset: Option, +} + +#[derive(Deserialize)] +pub struct EntryInput { + #[serde(default)] + pub title: String, + #[serde(default)] + pub body: String, + /// Optional override so an entry can be backdated to the day it is about. + #[serde(default)] + pub created_at: Option, +} + +/// A short, plain-ish preview of the body for the entry list. +fn excerpt(body: &str) -> String { + let flat: String = body + .lines() + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with("![") && !line.starts_with("```")) + .map(|line| line.trim_start_matches(['#', '>', '-', '*', ' '])) + .collect::>() + .join(" "); + let mut out: String = flat.chars().take(180).collect(); + if flat.chars().count() > 180 { + out.push('…'); + } + out +} + +/// Turn free text into an FTS5 prefix query, discarding anything that could be +/// read as FTS syntax. +fn fts_query(raw: &str) -> Option { + let terms: Vec = raw + .split_whitespace() + .map(|term| term.replace(|c: char| !c.is_alphanumeric(), " ")) + .flat_map(|term| { + term.split_whitespace() + .map(str::to_string) + .collect::>() + }) + .map(|term| format!("\"{term}\"*")) + .collect(); + (!terms.is_empty()).then(|| terms.join(" AND ")) +} + +pub async fn list( + _: Session, + State(state): State, + Query(params): Query, +) -> AppResult>> { + let limit = params.limit.unwrap_or(200).clamp(1, 1000); + let offset = params.offset.unwrap_or(0).max(0); + + let search = params.q.as_deref().map(str::trim).filter(|q| !q.is_empty()); + + let rows = match search.and_then(fts_query) { + Some(query) => { + sqlx::query( + "SELECT e.id, e.title, e.body, e.created_at, e.updated_at, e.share_token + FROM entries_fts + JOIN entries e ON e.id = entries_fts.rowid + WHERE entries_fts MATCH ?1 + ORDER BY rank + LIMIT ?2 OFFSET ?3", + ) + .bind(query) + .bind(limit) + .bind(offset) + .fetch_all(&state.db) + .await? + } + None => { + sqlx::query( + "SELECT id, title, body, created_at, updated_at, share_token + FROM entries + ORDER BY created_at DESC, id DESC + LIMIT ?1 OFFSET ?2", + ) + .bind(limit) + .bind(offset) + .fetch_all(&state.db) + .await? + } + }; + + let entries = rows + .into_iter() + .map(|row| { + let body: String = row.get("body"); + let share_token: Option = row.get("share_token"); + EntrySummary { + id: row.get("id"), + title: row.get("title"), + excerpt: excerpt(&body), + created_at: row.get("created_at"), + updated_at: row.get("updated_at"), + shared: share_token.is_some(), + share_token, + } + }) + .collect(); + + Ok(Json(entries)) +} + +pub async fn get( + _: Session, + State(state): State, + Path(id): Path, +) -> AppResult> { + load(&state, id).await.map(Json) +} + +pub async fn create( + _: Session, + State(state): State, + Json(input): Json, +) -> AppResult> { + let now = crate::now(); + let created_at = input.created_at.unwrap_or(now); + let id: i64 = sqlx::query( + "INSERT INTO entries (title, body, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4) + RETURNING id", + ) + .bind(&input.title) + .bind(&input.body) + .bind(created_at) + .bind(now) + .fetch_one(&state.db) + .await? + .get("id"); + + media::link_to_entry(&state.db, id, &input.body).await?; + load(&state, id).await.map(Json) +} + +pub async fn update( + _: Session, + State(state): State, + Path(id): Path, + Json(input): Json, +) -> AppResult> { + let affected = sqlx::query( + "UPDATE entries + SET title = ?1, body = ?2, updated_at = ?3, created_at = COALESCE(?4, created_at) + WHERE id = ?5", + ) + .bind(&input.title) + .bind(&input.body) + .bind(crate::now()) + .bind(input.created_at) + .bind(id) + .execute(&state.db) + .await? + .rows_affected(); + + if affected == 0 { + return Err(AppError::NotFound); + } + + media::link_to_entry(&state.db, id, &input.body).await?; + load(&state, id).await.map(Json) +} + +pub async fn remove( + _: Session, + State(state): State, + Path(id): Path, +) -> AppResult> { + let media_ids: Vec = sqlx::query("SELECT id FROM media WHERE entry_id = ?1") + .bind(id) + .fetch_all(&state.db) + .await? + .into_iter() + .map(|row| row.get("id")) + .collect(); + + let affected = sqlx::query("DELETE FROM entries WHERE id = ?1") + .bind(id) + .execute(&state.db) + .await? + .rows_affected(); + + if affected == 0 { + return Err(AppError::NotFound); + } + + for media_id in media_ids { + media::delete_media(&state, &media_id).await?; + } + + Ok(Json(serde_json::json!({ "ok": true }))) +} + +async fn load(state: &AppState, id: i64) -> AppResult { + let row = sqlx::query( + "SELECT id, title, body, created_at, updated_at, share_token FROM entries WHERE id = ?1", + ) + .bind(id) + .fetch_optional(&state.db) + .await? + .ok_or(AppError::NotFound)?; + + let share_token: Option = row.get("share_token"); + Ok(Entry { + id: row.get("id"), + title: row.get("title"), + body: row.get("body"), + created_at: row.get("created_at"), + updated_at: row.get("updated_at"), + shared: share_token.is_some(), + share_token, + }) +} diff --git a/src/routes/media.rs b/src/routes/media.rs new file mode 100644 index 0000000..f1e894c --- /dev/null +++ b/src/routes/media.rs @@ -0,0 +1,224 @@ +use std::sync::LazyLock; + +use axum::{ + body::Body, + extract::{Multipart, Path, State}, + http::header, + response::{IntoResponse, Response}, + Json, +}; +use regex::Regex; +use serde::Serialize; +use sqlx::{Row, SqlitePool}; +use tokio_util::io::ReaderStream; +use uuid::Uuid; + +use crate::{ + auth::Session, + error::{AppError, AppResult}, + state::AppState, +}; + +/// Media referenced from an entry body always looks like `/api/media/`, +/// which is how an entry claims ownership of the files it embeds. +static MEDIA_REF: LazyLock = LazyLock::new(|| { + Regex::new(r"/api/media/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})") + .expect("static regex is valid") +}); + +#[derive(Serialize)] +pub struct Media { + pub id: String, + pub filename: String, + pub mime: String, + pub size: i64, + pub created_at: i64, + pub url: String, + pub entry_id: Option, +} + +fn row_to_media(row: &sqlx::sqlite::SqliteRow) -> Media { + let id: String = row.get("id"); + Media { + url: format!("/api/media/{id}"), + id, + filename: row.get("filename"), + mime: row.get("mime"), + size: row.get("size"), + created_at: row.get("created_at"), + entry_id: row.get("entry_id"), + } +} + +pub async fn upload( + _: Session, + State(state): State, + mut multipart: Multipart, +) -> AppResult>> { + let mut uploaded = Vec::new(); + + while let Some(field) = multipart + .next_field() + .await + .map_err(|e| AppError::BadRequest(format!("malformed upload: {e}")))? + { + let filename = field + .file_name() + .map(str::to_string) + .unwrap_or_else(|| "upload".to_string()); + let mime = field + .content_type() + .map(str::to_string) + .filter(|m| !m.is_empty()) + .unwrap_or_else(|| { + mime_guess::from_path(&filename) + .first_or_octet_stream() + .to_string() + }); + + let data = field + .bytes() + .await + .map_err(|e| AppError::BadRequest(format!("could not read upload: {e}")))?; + + if data.is_empty() { + continue; + } + if data.len() > state.config.max_upload_bytes { + return Err(AppError::BadRequest(format!( + "{filename} is larger than the {} MB limit", + state.config.max_upload_bytes / 1024 / 1024 + ))); + } + + let id = Uuid::new_v4().to_string(); + tokio::fs::write(state.config.uploads_dir().join(&id), &data).await?; + + let row = sqlx::query( + "INSERT INTO media (id, filename, mime, size, created_at) + VALUES (?1, ?2, ?3, ?4, ?5) + RETURNING id, entry_id, filename, mime, size, created_at", + ) + .bind(&id) + .bind(&filename) + .bind(&mime) + .bind(data.len() as i64) + .bind(crate::now()) + .fetch_one(&state.db) + .await?; + + uploaded.push(row_to_media(&row)); + } + + if uploaded.is_empty() { + return Err(AppError::BadRequest("no files in upload".into())); + } + Ok(Json(uploaded)) +} + +pub async fn list(_: Session, State(state): State) -> AppResult>> { + let rows = sqlx::query( + "SELECT id, entry_id, filename, mime, size, created_at + FROM media ORDER BY created_at DESC LIMIT 500", + ) + .fetch_all(&state.db) + .await?; + Ok(Json(rows.iter().map(row_to_media).collect())) +} + +pub async fn serve( + _: Session, + State(state): State, + Path(id): Path, +) -> AppResult { + stream_media(&state, &id).await +} + +pub async fn remove( + _: Session, + State(state): State, + Path(id): Path, +) -> AppResult> { + delete_media(&state, &id).await?; + Ok(Json(serde_json::json!({ "ok": true }))) +} + +/// Shared by the private and the public-share media handlers. +pub async fn stream_media(state: &AppState, id: &str) -> AppResult { + let row = sqlx::query("SELECT mime, filename, size FROM media WHERE id = ?1") + .bind(id) + .fetch_optional(&state.db) + .await? + .ok_or(AppError::NotFound)?; + + let mime: String = row.get("mime"); + let filename: String = row.get("filename"); + let size: i64 = row.get("size"); + + let file = tokio::fs::File::open(state.config.uploads_dir().join(id)) + .await + .map_err(|_| AppError::NotFound)?; + + Ok(( + [ + (header::CONTENT_TYPE, mime), + (header::CONTENT_LENGTH, size.to_string()), + (header::CACHE_CONTROL, "private, max-age=31536000".into()), + ( + header::CONTENT_DISPOSITION, + format!("inline; filename*=UTF-8''{}", urlencode(&filename)), + ), + ], + Body::from_stream(ReaderStream::new(file)), + ) + .into_response()) +} + +pub async fn delete_media(state: &AppState, id: &str) -> AppResult<()> { + sqlx::query("DELETE FROM media WHERE id = ?1") + .bind(id) + .execute(&state.db) + .await?; + let path = state.config.uploads_dir().join(id); + if let Err(e) = tokio::fs::remove_file(&path).await { + if e.kind() != std::io::ErrorKind::NotFound { + return Err(e.into()); + } + } + Ok(()) +} + +/// Attach every media file the body embeds to this entry, and detach the ones +/// it no longer mentions so they stop being reachable through its share link. +pub async fn link_to_entry(db: &SqlitePool, entry_id: i64, body: &str) -> AppResult<()> { + let referenced: Vec = MEDIA_REF + .captures_iter(body) + .map(|c| c[1].to_lowercase()) + .collect(); + + sqlx::query("UPDATE media SET entry_id = NULL WHERE entry_id = ?1") + .bind(entry_id) + .execute(db) + .await?; + + for id in referenced { + sqlx::query("UPDATE media SET entry_id = ?1 WHERE id = ?2") + .bind(entry_id) + .bind(id) + .execute(db) + .await?; + } + Ok(()) +} + +fn urlencode(value: &str) -> String { + value + .bytes() + .map(|b| match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + (b as char).to_string() + } + _ => format!("%{b:02X}"), + }) + .collect() +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..eeea88b --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,28 @@ +pub mod entries; +pub mod media; +pub mod session; +pub mod share; + +use axum::{ + routing::{delete, get, post, put}, + Router, +}; + +use crate::state::AppState; + +pub fn api_router() -> Router { + Router::new() + .route("/me", get(session::me)) + .route("/login", post(session::login)) + .route("/logout", post(session::logout)) + .route("/entries", get(entries::list).post(entries::create)) + .route("/entries/{id}", get(entries::get)) + .route("/entries/{id}", put(entries::update)) + .route("/entries/{id}", delete(entries::remove)) + .route("/entries/{id}/share", post(share::enable)) + .route("/entries/{id}/share", delete(share::disable)) + .route("/media", post(media::upload).get(media::list)) + .route("/media/{id}", get(media::serve).delete(media::remove)) + .route("/share/{token}", get(share::read)) + .route("/share/{token}/media/{id}", get(share::serve_media)) +} diff --git a/src/routes/session.rs b/src/routes/session.rs new file mode 100644 index 0000000..594a846 --- /dev/null +++ b/src/routes/session.rs @@ -0,0 +1,50 @@ +use axum::{ + extract::State, + http::{header, StatusCode}, + response::IntoResponse, + Json, +}; +use serde::Deserialize; +use serde_json::json; + +use crate::{ + auth::{self, Session}, + error::{AppError, AppResult}, + state::AppState, +}; + +#[derive(Deserialize)] +pub struct LoginRequest { + pub username: String, + pub password: String, +} + +pub async fn login( + State(state): State, + Json(body): Json, +) -> AppResult { + if !auth::credentials_match(&state.config, &body.username, &body.password) { + return Err(AppError::BadRequest("invalid credentials".into())); + } + let token = auth::issue_token(&state.config); + Ok(( + StatusCode::OK, + [( + header::SET_COOKIE, + auth::session_cookie(&state.config, &token), + )], + Json(json!({ "user": state.config.username })), + )) +} + +pub async fn logout(State(state): State) -> impl IntoResponse { + ( + StatusCode::OK, + [(header::SET_COOKIE, auth::clear_cookie(&state.config))], + Json(json!({ "ok": true })), + ) +} + +pub async fn me(_: Session, State(state): State) -> impl IntoResponse { + Json(json!({ "user": state.config.username })) +} diff --git a/src/routes/share.rs b/src/routes/share.rs new file mode 100644 index 0000000..c7be043 --- /dev/null +++ b/src/routes/share.rs @@ -0,0 +1,121 @@ +use axum::{ + extract::{Path, State}, + response::Response, + Json, +}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD as B64, Engine}; +use rand::RngCore; +use serde::Serialize; +use serde_json::json; +use sqlx::Row; + +use crate::{ + auth::Session, + error::{AppError, AppResult}, + routes::media, + state::AppState, +}; + +#[derive(Serialize)] +pub struct SharedEntry { + pub title: String, + pub body: String, + pub created_at: i64, + pub updated_at: i64, + pub token: String, +} + +fn new_token() -> String { + let mut bytes = [0u8; 24]; + rand::thread_rng().fill_bytes(&mut bytes); + B64.encode(bytes) +} + +pub async fn enable( + _: Session, + State(state): State, + Path(id): Path, +) -> AppResult> { + let existing: Option> = sqlx::query("SELECT share_token FROM entries WHERE id = ?1") + .bind(id) + .fetch_optional(&state.db) + .await? + .map(|row| row.get("share_token")); + + let token = match existing { + None => return Err(AppError::NotFound), + Some(Some(token)) => token, + Some(None) => { + let token = new_token(); + sqlx::query("UPDATE entries SET share_token = ?1 WHERE id = ?2") + .bind(&token) + .bind(id) + .execute(&state.db) + .await?; + token + } + }; + + Ok(Json(json!({ "token": token, "path": format!("/s/{token}") }))) +} + +pub async fn disable( + _: Session, + State(state): State, + Path(id): Path, +) -> AppResult> { + let affected = sqlx::query("UPDATE entries SET share_token = NULL WHERE id = ?1") + .bind(id) + .execute(&state.db) + .await? + .rows_affected(); + if affected == 0 { + return Err(AppError::NotFound); + } + Ok(Json(json!({ "ok": true }))) +} + +/// Public: no session required, the unguessable token is the credential. +pub async fn read( + State(state): State, + Path(token): Path, +) -> AppResult> { + let row = sqlx::query( + "SELECT title, body, created_at, updated_at FROM entries WHERE share_token = ?1", + ) + .bind(&token) + .fetch_optional(&state.db) + .await? + .ok_or(AppError::NotFound)?; + + Ok(Json(SharedEntry { + title: row.get("title"), + body: row.get("body"), + created_at: row.get("created_at"), + updated_at: row.get("updated_at"), + token, + })) +} + +/// Public: serves a media file only while it is embedded in a shared entry. +pub async fn serve_media( + State(state): State, + Path((token, id)): Path<(String, String)>, +) -> AppResult { + let allowed = sqlx::query( + "SELECT 1 AS ok + FROM media m + JOIN entries e ON e.id = m.entry_id + WHERE m.id = ?1 AND e.share_token = ?2", + ) + .bind(&id) + .bind(&token) + .fetch_optional(&state.db) + .await? + .is_some(); + + if !allowed { + return Err(AppError::NotFound); + } + media::stream_media(&state, &id).await +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..4da1f74 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,11 @@ +use std::sync::Arc; + +use sqlx::SqlitePool; + +use crate::config::Config; + +#[derive(Clone)] +pub struct AppState { + pub db: SqlitePool, + pub config: Arc, +} diff --git a/src/static_files.rs b/src/static_files.rs new file mode 100644 index 0000000..a3ad0c7 --- /dev/null +++ b/src/static_files.rs @@ -0,0 +1,45 @@ +use axum::{ + http::{header, StatusCode, Uri}, + response::{IntoResponse, Response}, +}; +use rust_embed::Embed; + +/// The built Svelte app. In debug builds rust-embed reads from disk, so a +/// `vite build --watch` next to `cargo run` is enough for iteration. +#[derive(Embed)] +#[folder = "$CARGO_MANIFEST_DIR/web/dist"] +struct Assets; + +pub async fn handler(uri: Uri) -> Response { + let path = uri.path().trim_start_matches('/'); + match Assets::get(path) { + Some(file) => serve(path, file), + // Unknown paths are client-side routes; hand them the shell. + None => match Assets::get("index.html") { + Some(index) => serve("index.html", index), + None => ( + StatusCode::NOT_FOUND, + "frontend not built - run `bun run build` in web/", + ) + .into_response(), + }, + } +} + +fn serve(path: &str, file: rust_embed::EmbeddedFile) -> Response { + let mime = mime_guess::from_path(path).first_or_octet_stream(); + // Vite fingerprints everything under /assets/, so it is safe to cache hard. + let cache = if path.starts_with("assets/") { + "public, max-age=31536000, immutable" + } else { + "no-cache" + }; + ( + [ + (header::CONTENT_TYPE, mime.as_ref()), + (header::CACHE_CONTROL, cache), + ], + file.data, + ) + .into_response() +} diff --git a/web/bun.lock b/web/bun.lock new file mode 100644 index 0000000..0537629 --- /dev/null +++ b/web/bun.lock @@ -0,0 +1,277 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "narl-diary-web", + "dependencies": { + "@codemirror/commands": "^6.7.1", + "@codemirror/lang-markdown": "^6.3.1", + "@codemirror/language": "^6.10.8", + "@codemirror/state": "^6.5.1", + "@codemirror/view": "^6.36.2", + "@lezer/highlight": "^1.2.1", + "@replit/codemirror-vim": "^6.2.1", + "dompurify": "^3.2.4", + "marked": "^15.0.6", + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tsconfig/svelte": "^5.0.4", + "svelte": "^5.19.0", + "svelte-check": "^4.1.4", + "typescript": "^5.7.3", + "vite": "^6.0.11", + }, + }, + }, + "packages": { + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g=="], + + "@codemirror/commands": ["@codemirror/commands@6.11.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.7.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA=="], + + "@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="], + + "@codemirror/lang-html": ["@codemirror/lang-html@6.4.12", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w=="], + + "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="], + + "@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw=="], + + "@codemirror/language": ["@codemirror/language@6.12.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A=="], + + "@codemirror/lint": ["@codemirror/lint@6.9.7", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.42.0", "crelt": "^1.0.5" } }, "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg=="], + + "@codemirror/search": ["@codemirror/search@6.7.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA=="], + + "@codemirror/state": ["@codemirror/state@6.7.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-QhQIVRY+xHZDxwOSFrJ1eUMapJBUID3IdeAjf7dHO7zBUzSkyooHiodnalz5MG3iHzwixKMlAAyn7244y537EA=="], + + "@codemirror/view": ["@codemirror/view@6.43.11", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-2+esucbQX6wB2JYi1eDvdCPFTA31BN8oSy6xCmk3G6CloV11yOvEjYk+gH7kLrP0MuHG94E8WDhjs5oMiu3+Wg=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@lezer/common": ["@lezer/common@1.5.2", "", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="], + + "@lezer/css": ["@lezer/css@1.3.6", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g=="], + + "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="], + + "@lezer/html": ["@lezer/html@1.3.13", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg=="], + + "@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="], + + "@lezer/lr": ["@lezer/lr@1.4.10", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A=="], + + "@lezer/markdown": ["@lezer/markdown@1.7.2", "", { "dependencies": { "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0" } }, "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ=="], + + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.4", "", {}, "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@replit/codemirror-vim": ["@replit/codemirror-vim@6.4.0", "", { "dependencies": { "@replit/codemirror-vim-core": "^0.1.0" }, "peerDependencies": { "@codemirror/commands": "6.x.x", "@codemirror/language": "6.x.x", "@codemirror/search": "6.x.x", "@codemirror/state": "6.x.x", "@codemirror/view": "6.x.x" } }, "sha512-t9UMDNhkmeAkl0uRbiJVotv97bGD6mf4GyJJoEbrjUqa/Pov0s9eL+4AaI0Xto3OGri17i9qwIpT0JbVlVXt8A=="], + + "@replit/codemirror-vim-core": ["@replit/codemirror-vim-core@0.1.0", "", {}, "sha512-1i6EBKpcNfDKvTmTh6N6g9lL6udD5t+uFNh4JCqozRnVlvUGOps7h/QzS2ne4zcvPUjvApKpmcP7Grc3fNbZiQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.1", "", { "os": "android", "cpu": "arm" }, "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.1", "", { "os": "android", "cpu": "arm64" }, "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.1", "", { "os": "none", "cpu": "arm64" }, "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="], + + "@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.1.1", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.17", "vitefu": "^1.0.6" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@4.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw=="], + + "@tsconfig/svelte": ["@tsconfig/svelte@5.0.8", "", {}, "sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + + "aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "crelt": ["crelt@1.0.7", "", {}, "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "devalue": ["devalue@5.9.2", "", {}, "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w=="], + + "dompurify": ["dompurify@3.4.14", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "esrap": ["esrap@2.3.7", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-n2nf7fZR3c9yXf0BPEuHuXqT+KW0SJVj4cN5FMEkpCZ3scLjOQWpiccyCxVzCC2q1wubTghuEGzngJY/7Ah0Ow=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "rollup": ["rollup@4.63.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.1", "@rollup/rollup-android-arm64": "4.63.1", "@rollup/rollup-darwin-arm64": "4.63.1", "@rollup/rollup-darwin-x64": "4.63.1", "@rollup/rollup-freebsd-arm64": "4.63.1", "@rollup/rollup-freebsd-x64": "4.63.1", "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", "@rollup/rollup-linux-arm-musleabihf": "4.63.1", "@rollup/rollup-linux-arm64-gnu": "4.63.1", "@rollup/rollup-linux-arm64-musl": "4.63.1", "@rollup/rollup-linux-loong64-gnu": "4.63.1", "@rollup/rollup-linux-loong64-musl": "4.63.1", "@rollup/rollup-linux-ppc64-gnu": "4.63.1", "@rollup/rollup-linux-ppc64-musl": "4.63.1", "@rollup/rollup-linux-riscv64-gnu": "4.63.1", "@rollup/rollup-linux-riscv64-musl": "4.63.1", "@rollup/rollup-linux-s390x-gnu": "4.63.1", "@rollup/rollup-linux-x64-gnu": "4.63.1", "@rollup/rollup-linux-x64-musl": "4.63.1", "@rollup/rollup-openbsd-x64": "4.63.1", "@rollup/rollup-openharmony-arm64": "4.63.1", "@rollup/rollup-win32-arm64-msvc": "4.63.1", "@rollup/rollup-win32-ia32-msvc": "4.63.1", "@rollup/rollup-win32-x64-gnu": "4.63.1", "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], + + "svelte": ["svelte@5.57.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg=="], + + "svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], + + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], + + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + + "zimmerframe": ["zimmerframe@1.1.5", "", {}, "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA=="], + } +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f944cfe --- /dev/null +++ b/web/index.html @@ -0,0 +1,15 @@ + + + + + + + + ~/diary + + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..96c6283 --- /dev/null +++ b/web/package.json @@ -0,0 +1,30 @@ +{ + "name": "narl-diary-web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tsconfig/svelte": "^5.0.4", + "svelte": "^5.19.0", + "svelte-check": "^4.1.4", + "typescript": "^5.7.3", + "vite": "^6.0.11" + }, + "dependencies": { + "@codemirror/commands": "^6.7.1", + "@codemirror/lang-markdown": "^6.3.1", + "@codemirror/language": "^6.10.8", + "@codemirror/state": "^6.5.1", + "@codemirror/view": "^6.36.2", + "@lezer/highlight": "^1.2.1", + "@replit/codemirror-vim": "^6.2.1", + "dompurify": "^3.2.4", + "marked": "^15.0.6" + } +} diff --git a/web/src/App.svelte b/web/src/App.svelte new file mode 100644 index 0000000..8764ae6 --- /dev/null +++ b/web/src/App.svelte @@ -0,0 +1,270 @@ + + + + +{#if shareToken} + +{:else if diary.booting} +
booting ~/diary…
+{:else if !diary.user} + +{:else} + +
+
+
+
+ {#if diary.open} + + {:else} +
+
{`  ┌─────────────────────────────┐
+  │  nothing open               │
+  │                             │
+  │  o   write a new entry      │
+  │  j/k browse, Enter opens    │
+  │  /   search everything      │
+  │  ?   help                   │
+  └─────────────────────────────┘`}
+
+ {/if} +
+
+ + + + {#if cmdline !== null} + (cmdline = null)} /> + {/if} + + {#if diary.overlay !== 'none'} + +
e.target === e.currentTarget && (diary.overlay = 'none')}> + {#if diary.overlay === 'help'}{:else}{/if} +
+ {/if} +
+{/if} + + + + diff --git a/web/src/app.css b/web/src/app.css new file mode 100644 index 0000000..2754556 --- /dev/null +++ b/web/src/app.css @@ -0,0 +1,179 @@ +/* ── palette ─────────────────────────────────────────────────────────────── */ +:root { + --bg: #0b0e0d; + --bg-alt: #101513; + --bg-lift: #161d1a; + --line: #1e2b26; + --fg: #cfe3d9; + --fg-dim: #6f8a7f; + --fg-faint: #3f5049; + --accent: #54f2a1; + --accent-dim: #2f9c69; + --warn: #f2c14e; + --error: #ff6b6b; + --select: #1c3a2c; + + --font: ui-monospace, "JetBrains Mono", "Fira Code", "SF Mono", Menlo, Consolas, monospace; + --radius: 2px; + color-scheme: dark; +} + +[data-theme='amber'] { + --bg: #0e0b06; + --bg-alt: #151009; + --bg-lift: #1d160c; + --line: #2e2413; + --fg: #e8d5ae; + --fg-dim: #8d7550; + --fg-faint: #4d3f28; + --accent: #ffb454; + --accent-dim: #b57a25; + --select: #3a2a10; +} + +[data-theme='ice'] { + --bg: #080b10; + --bg-alt: #0d1219; + --bg-lift: #121a24; + --line: #1c2836; + --fg: #cddbe8; + --fg-dim: #6d8399; + --fg-faint: #3d4f60; + --accent: #7cc7ff; + --accent-dim: #3d84b8; + --select: #17324a; +} + +/* ── base ────────────────────────────────────────────────────────────────── */ +* { box-sizing: border-box; } + +html, body { + height: 100%; + margin: 0; +} + +body { + background: var(--bg); + color: var(--fg); + font-family: var(--font); + font-size: 14px; + line-height: 1.55; + -webkit-font-smoothing: antialiased; + overflow: hidden; + overscroll-behavior: none; +} + +#app { height: 100%; } + +a { color: var(--accent); text-decoration: none; border-bottom: 1px solid var(--accent-dim); } +a:hover { background: var(--select); } + +button { + font: inherit; + color: var(--fg-dim); + background: transparent; + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 2px 8px; + cursor: pointer; +} +button:hover { color: var(--accent); border-color: var(--accent-dim); } +button:focus-visible, input:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; } + +input, textarea { + font: inherit; + color: var(--fg); + background: var(--bg-alt); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 4px 8px; +} +input::placeholder { color: var(--fg-faint); } + +::selection { background: var(--select); color: var(--fg); } + +/* thin, unobtrusive scrollbars everywhere */ +* { scrollbar-width: thin; scrollbar-color: var(--line) transparent; } +*::-webkit-scrollbar { width: 8px; height: 8px; } +*::-webkit-scrollbar-thumb { background: var(--line); } +*::-webkit-scrollbar-track { background: transparent; } + +.dim { color: var(--fg-dim); } +.faint { color: var(--fg-faint); } +.accent { color: var(--accent); } +.error { color: var(--error); } + +/* ── rendered markdown ───────────────────────────────────────────────────── */ +.md { max-width: 78ch; } +.md > :first-child { margin-top: 0; } +.md h1, .md h2, .md h3, .md h4 { + color: var(--accent); + font-size: 1em; + font-weight: 700; + margin: 1.6em 0 0.6em; + letter-spacing: 0.02em; +} +.md h1::before { content: '# '; color: var(--fg-faint); } +.md h2::before { content: '## '; color: var(--fg-faint); } +.md h3::before { content: '### '; color: var(--fg-faint); } +.md h4::before { content: '#### '; color: var(--fg-faint); } +.md p { margin: 0 0 1em; } +.md ul, .md ol { margin: 0 0 1em; padding-left: 2ch; } +.md li { margin: 0.15em 0; } +.md ul { list-style: none; } +.md ul > li::before { content: '- '; color: var(--fg-faint); margin-left: -2ch; } +.md blockquote { + margin: 0 0 1em; + padding-left: 1.5ch; + border-left: 2px solid var(--accent-dim); + color: var(--fg-dim); +} +.md code { + background: var(--bg-lift); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 0 4px; + font-size: 0.92em; +} +.md pre { + background: var(--bg-alt); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 10px 12px; + overflow-x: auto; + margin: 0 0 1em; +} +.md pre code { background: none; border: 0; padding: 0; } +.md hr { border: 0; border-top: 1px dashed var(--line); margin: 2em 0; } +.md img, .md video { + max-width: 100%; + border: 1px solid var(--line); + border-radius: var(--radius); + display: block; + margin: 0 0 1em; +} +.md audio { width: 100%; margin: 0 0 1em; } +.md table { border-collapse: collapse; margin: 0 0 1em; display: block; overflow-x: auto; } +.md th, .md td { border: 1px solid var(--line); padding: 3px 8px; text-align: left; } +.md th { color: var(--accent); } +.md input[type='checkbox'] { accent-color: var(--accent-dim); } + +/* ── codemirror ──────────────────────────────────────────────────────────── */ +.cm-editor { height: 100%; background: transparent; } +.cm-editor.cm-focused { outline: none; } +.cm-scroller { + font-family: var(--font) !important; + font-size: 14px; + line-height: 1.6; + padding-bottom: 40vh; +} +.cm-content { caret-color: var(--accent); } +.cm-gutters { background: transparent; border: 0; color: var(--fg-faint); } +.cm-activeLineGutter { background: transparent; color: var(--fg-dim); } +.cm-activeLine { background: rgb(255 255 255 / 2.5%); } +.cm-selectionBackground, .cm-content ::selection { background: var(--select) !important; } +.cm-fat-cursor { background: var(--accent) !important; opacity: 1 !important; } +.cm-animate-fat-cursor { background: var(--accent) !important; } +.cm-panels { background: var(--bg-alt); color: var(--fg); border-color: var(--line); } +.cm-vim-panel { padding: 2px 8px; background: var(--bg-alt); } +.cm-vim-panel input { background: transparent; border: 0; color: var(--accent); } diff --git a/web/src/lib/CommandLine.svelte b/web/src/lib/CommandLine.svelte new file mode 100644 index 0000000..170439f --- /dev/null +++ b/web/src/lib/CommandLine.svelte @@ -0,0 +1,118 @@ + + +
+ {#if matches.length > 0 && value.replace(/^:/, '').length > 0} +
    + {#each matches as spec (spec.name)} +
  • + +
  • + {/each} +
+ {/if} + +
+ +
+
+ + diff --git a/web/src/lib/Editor.svelte b/web/src/lib/Editor.svelte new file mode 100644 index 0000000..cdf05d3 --- /dev/null +++ b/web/src/lib/Editor.svelte @@ -0,0 +1,178 @@ + + +
+ + diff --git a/web/src/lib/EntryPane.svelte b/web/src/lib/EntryPane.svelte new file mode 100644 index 0000000..654bf45 --- /dev/null +++ b/web/src/lib/EntryPane.svelte @@ -0,0 +1,163 @@ + + +
+
+
+ entry:{entry.id} + {formatStamp(diary.draft.created_at)} {formatWeekday(diary.draft.created_at)} + · + edited {relative(entry.updated_at)} + · + {words} words + {#if diary.dirty}[+]{/if} +
+ +
+ {#if isTouchDevice} + + {/if} + {#if diary.editing} + + + + {:else} + + {/if} + + + +
+
+ + {#if shareUrl} +
+ ◉ public + {shareUrl} + +
+ {/if} + + {#if diary.editing} + (diary.dirty = true)} + onkeydown={(event) => { + if (event.key === 'Enter') { + event.preventDefault() + document.querySelector('.cm-content')?.focus() + } + }} + /> +
+ {#key entry.id} + {#await import('./Editor.svelte')} +
loading editor…
+ {:then module} + {@const Editor = module.default} + + {/await} + {/key} +
+ {#if isTouchDevice} +
+ + + +
+ {/if} + {:else} +
+
+ {#if entry.title.trim()}

{entry.title}

{/if} + + {@html html} + {#if !entry.body.trim()} +

this entry is empty — press i to write.

+ {/if} +
+
+ {/if} +
+ + diff --git a/web/src/lib/Help.svelte b/web/src/lib/Help.svelte new file mode 100644 index 0000000..b6941e1 --- /dev/null +++ b/web/src/lib/Help.svelte @@ -0,0 +1,86 @@ + + +
+
+ :help + +
+ +
+
+

keys

+
+ {#each keys as [key, description] (key)} +
{key}
+
{description}
+ {/each} +
+
+ +
+

commands

+
+ {#each commands as spec (spec.name)} +
+ :{spec.name}{#if spec.aliases?.length} ({spec.aliases.join(' ')}){/if} + {#if spec.args} {spec.args}{/if} +
+
{spec.help}
+ {/each} +
+
+
+ +
+ inside the editor the full vim keymap is live — hjkl, w/b, dd, ciw, visual mode, macros, and + :w to write. drop or paste an image, video or audio file into the + editor to attach it. +
+
+ + diff --git a/web/src/lib/Login.svelte b/web/src/lib/Login.svelte new file mode 100644 index 0000000..0e8fb57 --- /dev/null +++ b/web/src/lib/Login.svelte @@ -0,0 +1,87 @@ + + +
+
+ + +

a private log. one user. one machine.

+ + + + + +
+ + {#if error}{error}{/if} +
+
+
+ + diff --git a/web/src/lib/MediaBrowser.svelte b/web/src/lib/MediaBrowser.svelte new file mode 100644 index 0000000..6831336 --- /dev/null +++ b/web/src/lib/MediaBrowser.svelte @@ -0,0 +1,99 @@ + + +
+
+ :media + {diary.media.length} files + +
+ +
    + {#each diary.media as file (file.id)} +
  • +
    + {#if file.mime.startsWith('image/')} + {file.filename} + {:else} + {file.mime.split('/')[0]} + {/if} +
    + +
    + {file.filename} + + {formatBytes(file.size)} · {formatStamp(file.created_at)} · + {file.entry_id ? `entry:${file.entry_id}` : 'unattached'} + +
    + +
    + {#if diary.editing} + + {/if} + + +
    +
  • + {:else} +
  • nothing uploaded yet — attach a file from the editor.
  • + {/each} +
+
+ + diff --git a/web/src/lib/Shared.svelte b/web/src/lib/Shared.svelte new file mode 100644 index 0000000..d50cf09 --- /dev/null +++ b/web/src/lib/Shared.svelte @@ -0,0 +1,67 @@ + + +
+ {#if error} +

{error}

+ {:else if entry} +
+
+ {formatStamp(entry.created_at)} {formatWeekday(entry.created_at)} + shared entry +
+ {#if entry.title.trim()}

{entry.title}

{/if} + + {@html renderMarkdown(entry.body, token)} +
+
— written in ~/diary
+ {:else} +

loading…

+ {/if} +
+ + diff --git a/web/src/lib/Sidebar.svelte b/web/src/lib/Sidebar.svelte new file mode 100644 index 0000000..cfdfb2a --- /dev/null +++ b/web/src/lib/Sidebar.svelte @@ -0,0 +1,156 @@ + + + + + diff --git a/web/src/lib/StatusBar.svelte b/web/src/lib/StatusBar.svelte new file mode 100644 index 0000000..79847bd --- /dev/null +++ b/web/src/lib/StatusBar.svelte @@ -0,0 +1,69 @@ + + +
+ + {mode} + + {file} + + + {diary.flash?.text ?? ''} + + + {#if lines !== null}{lines}L{/if} + + {isTouchDevice ? 'tap : for commands' : '? help · : command · / search'} + + {diary.user} +
+ + diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts new file mode 100644 index 0000000..3ad20b0 --- /dev/null +++ b/web/src/lib/api.ts @@ -0,0 +1,109 @@ +export interface EntrySummary { + id: number + title: string + excerpt: string + created_at: number + updated_at: number + shared: boolean + share_token: string | null +} + +export interface Entry { + id: number + title: string + body: string + created_at: number + updated_at: number + shared: boolean + share_token: string | null +} + +export interface SharedEntry { + title: string + body: string + created_at: number + updated_at: number + token: string +} + +export interface MediaFile { + id: string + filename: string + mime: string + size: number + created_at: number + url: string + entry_id: number | null +} + +export class ApiError extends Error { + constructor( + readonly status: number, + message: string, + ) { + super(message) + } +} + +async function request(path: string, init: RequestInit = {}): Promise { + const response = await fetch(`/api${path}`, { + credentials: 'same-origin', + ...init, + headers: + init.body instanceof FormData + ? init.headers + : { 'content-type': 'application/json', ...init.headers }, + }) + + if (!response.ok) { + let message = response.statusText + try { + message = (await response.json()).error ?? message + } catch { + /* the body was not JSON; the status text will do */ + } + throw new ApiError(response.status, message) + } + + return response.status === 204 ? (undefined as T) : await response.json() +} + +const body = (data: unknown) => JSON.stringify(data) + +export const api = { + me: () => request<{ user: string }>('/me'), + + login: (username: string, password: string) => + request<{ user: string }>('/login', { method: 'POST', body: body({ username, password }) }), + + logout: () => request<{ ok: true }>('/logout', { method: 'POST' }), + + listEntries: (q = '') => + request(`/entries${q ? `?q=${encodeURIComponent(q)}` : ''}`), + + getEntry: (id: number) => request(`/entries/${id}`), + + createEntry: (input: { title: string; body: string; created_at?: number }) => + request('/entries', { method: 'POST', body: body(input) }), + + updateEntry: (id: number, input: { title: string; body: string; created_at?: number }) => + request(`/entries/${id}`, { method: 'PUT', body: body(input) }), + + deleteEntry: (id: number) => request<{ ok: true }>(`/entries/${id}`, { method: 'DELETE' }), + + share: (id: number) => request<{ token: string; path: string }>(`/entries/${id}/share`, { method: 'POST' }), + + unshare: (id: number) => request<{ ok: true }>(`/entries/${id}/share`, { method: 'DELETE' }), + + listMedia: () => request('/media'), + + upload: (files: File[]) => { + const form = new FormData() + for (const file of files) form.append('file', file) + return request('/media', { method: 'POST', body: form }) + }, + + deleteMedia: (id: string) => request<{ ok: true }>(`/media/${id}`, { method: 'DELETE' }), + + readShared: (token: string) => request(`/share/${encodeURIComponent(token)}`), +} diff --git a/web/src/lib/commands.ts b/web/src/lib/commands.ts new file mode 100644 index 0000000..c2f9877 --- /dev/null +++ b/web/src/lib/commands.ts @@ -0,0 +1,262 @@ +import { diary } from './store.svelte' +import { parseDay } from './util' + +/** Filled in by App.svelte so ex-commands can reach the DOM-bound bits. */ +export const hooks = { + pickFiles: () => {}, + focusEditor: () => {}, + focusList: () => {}, + insertText: (_text: string) => {}, + openCommandLine: (_initial: string) => {}, +} + +export interface CommandSpec { + name: string + aliases?: string[] + args?: string + help: string + run: (arg: string) => void | Promise +} + +const requireOpen = () => { + if (!diary.open) { + diary.say('no entry open', 'error') + return null + } + return diary.open +} + +export const commands: CommandSpec[] = [ + { + name: 'write', + aliases: ['w'], + help: 'save the open entry', + run: () => diary.guard(() => diary.save()), + }, + { + name: 'wq', + aliases: ['x'], + help: 'save and leave insert/editor mode', + run: async () => { + await diary.guard(() => diary.save()) + diary.editing = false + hooks.focusList() + }, + }, + { + name: 'quit', + aliases: ['q'], + help: 'close the editor, or the entry when already reading', + run: () => { + if (diary.dirty) { + diary.say('unsaved changes — :w to write, :q! to discard', 'error') + return + } + if (diary.editing) diary.editing = false + else { + diary.open = null + diary.pane = 'list' + } + hooks.focusList() + }, + }, + { + name: 'q!', + help: 'discard changes and close the editor', + run: async () => { + const open = diary.open + diary.editing = false + diary.dirty = false + if (open) await diary.guard(() => diary.openEntry(open.id)) + hooks.focusList() + }, + }, + { + name: 'new', + aliases: ['n', 'o'], + args: '[yyyy-mm-dd]', + help: 'start a new entry, optionally dated', + run: async (arg) => { + await diary.guard(async () => { + await diary.createEntry() + if (arg) { + const at = parseDay(arg) + if (at === null) return diary.say(`not a date: ${arg}`, 'error') + diary.draft.created_at = at + diary.dirty = true + await diary.save() + } + }) + hooks.focusEditor() + }, + }, + { + name: 'edit', + aliases: ['e'], + args: '[id]', + help: 'edit the open entry, or open an entry by id', + run: async (arg) => { + const id = arg ? Number(arg) : diary.open?.id ?? diary.selected?.id + if (!id || Number.isNaN(id)) return diary.say('usage: :e ', 'error') + await diary.guard(() => diary.openEntry(id, true)) + hooks.focusEditor() + }, + }, + { + name: 'delete', + aliases: ['d', 'rm'], + args: '[id]', + help: 'delete an entry (asks first)', + run: async (arg) => { + const id = arg ? Number(arg) : diary.open?.id ?? diary.selected?.id + if (!id || Number.isNaN(id)) return diary.say('nothing to delete', 'error') + if (!confirm(`delete entry #${id}? this cannot be undone.`)) return + await diary.guard(() => diary.deleteEntry(id)) + }, + }, + { + name: 'title', + args: '', + help: 'set the title of the open entry', + run: async (arg) => { + if (!requireOpen()) return + diary.draft.title = arg + diary.dirty = true + await diary.guard(() => diary.save()) + }, + }, + { + name: 'date', + args: '', + help: 're-date the open entry', + run: async (arg) => { + if (!requireOpen()) return + const at = parseDay(arg) + if (at === null) return diary.say('usage: :date 2026-09-05', 'error') + diary.draft.created_at = at + diary.dirty = true + await diary.guard(() => diary.save()) + }, + }, + { + name: 'share', + help: 'publish the entry behind an unguessable link and copy it', + run: async () => { + const id = diary.open?.id ?? diary.selected?.id + if (!id) return diary.say('no entry selected', 'error') + const entry = diary.open?.id === id ? diary.open : diary.selected + if (entry?.shared) return diary.say('already shared — :link to copy, :unshare to revoke') + await diary.guard(() => diary.toggleShare(id)) + }, + }, + { + name: 'unshare', + help: 'revoke the share link', + run: async () => { + const id = diary.open?.id ?? diary.selected?.id + if (!id) return diary.say('no entry selected', 'error') + const entry = diary.open?.id === id ? diary.open : diary.selected + if (!entry?.shared) return diary.say('entry is not shared') + await diary.guard(() => diary.toggleShare(id)) + }, + }, + { + name: 'link', + help: 'copy the share link of the current entry', + run: async () => { + const token = diary.open?.share_token ?? diary.selected?.share_token + if (!token) return diary.say('entry is not shared — :share first', 'error') + const url = `${location.origin}/s/${token}` + diary.say((await diary.copy(url)) ? `copied ${url}` : url) + }, + }, + { + name: 'upload', + aliases: ['up'], + help: 'pick files to attach at the cursor', + run: () => hooks.pickFiles(), + }, + { + name: 'media', + help: 'browse everything you have uploaded', + run: () => diary.guard(async () => { + await diary.loadMedia() + diary.overlay = 'media' + }), + }, + { + name: 'search', + aliases: ['se'], + args: '[text]', + help: 'full-text search (empty clears)', + run: (arg) => diary.guard(() => diary.search(arg)), + }, + { + name: 'set', + args: '