From 062054f45071a19265fbe7d1ba22da8a9da59e17 Mon Sep 17 00:00:00 2001 From: cupcakearmy Date: Sun, 12 Jul 2026 11:36:01 +0200 Subject: [PATCH] first v3 --- CONTRIBUTING.md | 4 +- Dockerfile | 2 +- README.md | 18 +- docker-compose.dev.yaml | 8 +- docker-compose.yaml | 8 +- package.json | 17 +- packages/backend/Cargo.lock | 22 +- packages/backend/Cargo.toml | 5 +- packages/backend/src/config.rs | 8 +- packages/backend/src/health/mod.rs | 4 +- packages/backend/src/lock.rs | 10 - packages/backend/src/main.rs | 32 +- packages/backend/src/note/mod.rs | 2 +- packages/backend/src/note/model.rs | 37 +- packages/backend/src/note/routes.rs | 216 ++- packages/backend/src/store.rs | 113 +- packages/cli/build.js | 9 +- packages/cli/package.json | 33 +- packages/cli/src/actions/download.ts | 79 +- packages/cli/src/actions/upload.ts | 63 +- packages/cli/src/cli.ts | 23 +- packages/cli/src/index.ts | 4 +- packages/cli/src/shared/adapters.ts | 61 - packages/cli/src/shared/api.ts | 141 -- packages/cli/src/shared/shared.ts | 2 - packages/cli/src/utils/utils.ts | 18 +- packages/frontend/package.json | 3 +- packages/frontend/src/lib/stores/status.ts | 25 +- .../src/lib/ui/AdvancedParameters.svelte | 3 +- .../frontend/src/lib/ui/FileUpload.svelte | 6 +- .../src/lib/ui/PastedFilesPreview.svelte | 8 +- packages/frontend/src/lib/ui/ShowNote.svelte | 20 +- packages/frontend/src/lib/ui/TextInput.svelte | 4 +- packages/frontend/src/lib/views/Create.svelte | 79 +- .../src/routes/note/[id]/+page.svelte | 64 +- packages/shared/package.json | 22 + packages/shared/src/api.ts | 58 + packages/shared/src/crypto.test.ts | 26 + packages/shared/src/crypto.ts | 41 + packages/shared/src/index.ts | 4 + packages/shared/src/types.ts | 21 + packages/shared/tsconfig.json | 10 + packages/shared/vitest.config.ts | 7 + pnpm-lock.yaml | 1304 ++++------------- test/utils.ts | 7 +- version.mjs | 16 +- 46 files changed, 976 insertions(+), 1691 deletions(-) delete mode 100644 packages/backend/src/lock.rs delete mode 100644 packages/cli/src/shared/adapters.ts delete mode 100644 packages/cli/src/shared/api.ts delete mode 100644 packages/cli/src/shared/shared.ts create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/api.ts create mode 100644 packages/shared/src/crypto.test.ts create mode 100644 packages/shared/src/crypto.ts create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/src/types.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 packages/shared/vitest.config.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65ba5f6..eada928 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ ## Requirements - [mise](https://mise.jdx.dev) — manages pnpm, rust, node (see `mise.toml`) -- docker or [colima](https://github.com/abiosoft/colima) (for redis) +- docker or [colima](https://github.com/abiosoft/colima) (for cache) ## Setup @@ -18,7 +18,7 @@ pnpm install pnpm run dev ``` -Make sure docker/colima is running. This starts redis, the rust backend, the web client, and the CLI. The app is at [localhost:3000](http://localhost:3000). +Make sure docker/colima is running. This starts the cache (valkey/redis), the rust backend, the web client, and the CLI. The app is at [localhost:3000](http://localhost:3000). ## Tests diff --git a/Dockerfile b/Dockerfile index 1c5738c..ab68bbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,6 @@ RUN apk add --no-cache curl libgcc COPY --from=backend /tmp/target/release/cryptgeon . COPY --from=client /tmp/packages/frontend/build ./frontend ENV FRONTEND_PATH="./frontend" -ENV REDIS="redis://redis/" +ENV CACHE="redis://cache/" EXPOSE 8000 ENTRYPOINT [ "/app/cryptgeon" ] diff --git a/README.md b/README.md index 31668f5..f3c2bc6 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ client side with the key and then sent to the server. data is store never persisted to disk. the server never sees the encryption key and cannot decrypt the contents of the notes even if it tried to. -> View counts are guaranteed with one running instance of cryptgeon. Multiple instances connected to the same Redis instance can run into race conditions, where a note might be retrieved more than the view count allows. +> View counts are guaranteed with one running instance of cryptgeon. Multiple instances connected to the same cache instance can run into race conditions, where a note might be retrieved more than the view count allows. ## Screenshot @@ -73,14 +73,14 @@ of the notes even if it tried to. | Variable | Default | Description | | ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `REDIS` | `redis://redis/` | Redis URL to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) | +| `CACHE` | `redis://cache/` | Cache URL (valkey or redis) to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) | | `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/).
`512 MiB` is the maximum allowed.
The frontend will show that number including the ~35% encoding overhead. | | `MAX_VIEWS` | `100` | Maximal number of views. | | `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. | | `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. | | `ALLOW_FILES` | `true` | Allow uploading files. If set to `false`, users will only be allowed to create text notes. | | `ID_LENGTH` | `32` | Set the size of the note `id` in bytes. By default this is `32` bytes. This is useful for reducing link size. _This setting does not affect encryption strength_. | -| `REDIS_PREFIX` | `""` | Optional prefix for all Redis keys. Useful when sharing a Redis instance with other apps via ACL namespaces. | +| `CACHE_PREFIX` | `""` | Optional prefix for all cache keys. Useful when sharing a cache instance with other apps via ACL namespaces. | | `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` | | `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable | | `THEME_TEXT` | `""` | Custom text for replacing the description below the logo | @@ -107,12 +107,12 @@ Docker is the easiest way. There is the [official image here](https://hub.docker version: "3.8" services: - redis: - image: redis:7-alpine + cache: + image: valkey/valkey:7-alpine # This is required to stay in RAM only. - command: redis-server --save "" --appendonly no + command: valkey-server --save "" --appendonly no # Set a size limit. See link below on how to customise. - # https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/ + # https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/ # --maxmemory 1gb --maxmemory-policy allkeys-lrulpine # This prevents the creation of an anonymous volume. tmpfs: @@ -121,7 +121,7 @@ services: app: image: cupcakearmy/cryptgeon:latest depends_on: - - redis + - cache environment: # Size limit for a single note. SIZE_LIMIT: 4 MiB @@ -130,7 +130,7 @@ services: # Optional health checks # healthcheck: - # test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"] + # test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/healthz"] # interval: 1m # timeout: 3s # retries: 2 diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index c651a8e..4f21c0d 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -2,7 +2,7 @@ # For a production file see: README.md services: - redis: + cache: image: valkey/valkey:7-alpine # This is required to stay in RAM only. command: valkey-server --save "" --appendonly no @@ -19,14 +19,14 @@ services: build: . env_file: .env.dev depends_on: - - redis + - cache restart: unless-stopped ports: - 3000:8000 healthcheck: - test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/api/live/'] + test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/healthz'] interval: 1m timeout: 3s retries: 2 - start_period: 5s + start_period: 5s \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 3a0b25f..73ef450 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,5 @@ services: - redis: + cache: image: valkey/valkey:7-alpine # This is required to stay in RAM only. command: valkey-server --save "" --appendonly no @@ -13,7 +13,7 @@ services: app: image: cupcakearmy/cryptgeon:latest depends_on: - - redis + - cache environment: # Size limit for a single note. SIZE_LIMIT: 4 MiB @@ -22,8 +22,8 @@ services: # Optional health checks # healthcheck: - # test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"] + # test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/healthz"] # interval: 1m # timeout: 3s # retries: 2 - # start_period: 5s + # start_period: 5s \ No newline at end of file diff --git a/package.json b/package.json index 115e11a..a33f46e 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,23 @@ { "scripts": { - "dev:docker": "docker compose -f docker-compose.dev.yaml up redis", + "dev:docker": "docker compose -f docker-compose.dev.yaml up cache", "dev:packages": "pnpm --parallel run dev", - "dev": "run-p dev:*", + "dev": "pnpm --parallel run dev:*", "docker:up": "docker compose -f docker-compose.dev.yaml up", "docker:build": "docker compose -f docker-compose.dev.yaml build", "test": "playwright test --project=chrome --project=firefox --project=safari", "test:local": "playwright test --project=chrome", - "test:server": "run-s docker:up", + "test:server": "docker compose -f docker-compose.dev.yaml up", "test:dl-browsers": "playwright install", - "test:prepare": "run-p test:dl-browsers build docker:build", + "test:prepare": "pnpm --parallel run test:dl-browsers build docker:build", "build": "pnpm run --recursive --filter=!@cryptgeon/backend build" }, "devDependencies": { "@playwright/test": "^1.60.0", - "@types/node": "^24.12.4", - "npm-run-all": "^4.1.5", - "shelljs": "^0.8.5" + "@types/node": "^24.12.4" }, - "packageManager": "pnpm@11.5.0" + "packageManager": "pnpm@11.5.0", + "engines": { + "node": ">=22" + } } diff --git a/packages/backend/Cargo.lock b/packages/backend/Cargo.lock index 7f5c0bd..dba5e3c 100644 --- a/packages/backend/Cargo.lock +++ b/packages/backend/Cargo.lock @@ -252,7 +252,7 @@ dependencies = [ [[package]] name = "cryptgeon" -version = "2.9.3" +version = "3.0.0" dependencies = [ "axum", "bs62", @@ -261,6 +261,7 @@ dependencies = [ "lazy_static", "redis", "ring", + "rmp-serde", "serde", "serde_json", "tokio", @@ -1004,6 +1005,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + [[package]] name = "rustix" version = "1.1.4" diff --git a/packages/backend/Cargo.toml b/packages/backend/Cargo.toml index 9cb08db..2646a8f 100644 --- a/packages/backend/Cargo.toml +++ b/packages/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cryptgeon" -version = "2.9.3" +version = "3.0.0" authors = ["cupcakearmy "] edition = "2024" rust-version = "1.95" @@ -20,8 +20,9 @@ redis = { version = "1", features = ["tls-native-tls"] } # Utility serde_json = "1" +rmp-serde = "1" lazy_static = "1" ring = "0.17" bs62 = "0.1" byte-unit = "4" -dotenv = "0.15" +dotenv = "0.15" \ No newline at end of file diff --git a/packages/backend/src/config.rs b/packages/backend/src/config.rs index 9b1d1ec..b9b4468 100644 --- a/packages/backend/src/config.rs +++ b/packages/backend/src/config.rs @@ -34,7 +34,7 @@ pub static ref ID_LENGTH: u32 = std::env::var("ID_LENGTH") .unwrap_or("32".to_string()) .parse() .unwrap(); -pub static ref REDIS_PREFIX: String = std::env::var("REDIS_PREFIX") +pub static ref CACHE_PREFIX: String = std::env::var("CACHE_PREFIX") .unwrap_or("".to_string()) .parse() .unwrap(); @@ -50,6 +50,10 @@ pub static ref IMPRINT_HTML: String = std::env::var("IMPRINT_HTML") .unwrap_or("".to_string()) .parse() .unwrap(); +pub static ref EXTRA_SIZE_LIMIT: usize = std::env::var("EXTRA_SIZE_LIMIT") + .unwrap_or("512".to_string()) + .parse() + .unwrap(); } // THEME @@ -78,4 +82,4 @@ lazy_static! { .unwrap_or("true".to_string()) .parse() .unwrap(); -} +} \ No newline at end of file diff --git a/packages/backend/src/health/mod.rs b/packages/backend/src/health/mod.rs index e54f8ac..cec0760 100644 --- a/packages/backend/src/health/mod.rs +++ b/packages/backend/src/health/mod.rs @@ -2,9 +2,9 @@ use crate::store; use axum::http::StatusCode; pub async fn report_health() -> (StatusCode,) { - if store::can_reach_redis() { + if store::can_reach_cache() { return (StatusCode::OK,); } else { return (StatusCode::SERVICE_UNAVAILABLE,); } -} +} \ No newline at end of file diff --git a/packages/backend/src/lock.rs b/packages/backend/src/lock.rs deleted file mode 100644 index 42deccf..0000000 --- a/packages/backend/src/lock.rs +++ /dev/null @@ -1,10 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::Mutex; - -#[derive(Clone)] -pub struct SharedState { - pub locks: LockMap, -} - -pub type LockMap = Arc>>>>; diff --git a/packages/backend/src/main.rs b/packages/backend/src/main.rs index e738eea..f51a59f 100644 --- a/packages/backend/src/main.rs +++ b/packages/backend/src/main.rs @@ -1,13 +1,9 @@ -use std::{collections::HashMap, sync::Arc}; - use axum::{ Router, ServiceExt, extract::{DefaultBodyLimit, Request}, routing::{delete, get, post}, }; use dotenv::dotenv; -use lock::SharedState; -use tokio::sync::Mutex; use tower::Layer; use tower_http::{ compression::CompressionLayer, @@ -21,7 +17,6 @@ extern crate lazy_static; mod config; mod csp; mod health; -mod lock; mod note; mod status; mod store; @@ -30,34 +25,30 @@ mod store; async fn main() { dotenv().ok(); - let shared_state = SharedState { - locks: Arc::new(Mutex::new(HashMap::new())), - }; - - if !store::can_reach_redis() { - println!("cannot reach redis"); - panic!("cannot reach redis"); + if !store::can_reach_cache() { + println!("cannot reach cache"); + panic!("cannot reach cache"); } let notes_routes = Router::new() .route("/", post(note::create)) - .route("/{id}", delete(note::delete)) + .route("/{id}", delete(note::view)) .route("/{id}", get(note::preview)); - let health_routes = Router::new().route("/live", get(health::report_health)); + let health_routes = Router::new().route("/healthz", get(health::report_health)); let status_routes = Router::new().route("/status", get(status::get_status)); - let api_routes = Router::new() + let v3_routes = Router::new() .nest("/notes", notes_routes) - .merge(health_routes) .merge(status_routes); + let api_routes = Router::new().nest("/v3", v3_routes); + let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html"); let serve_dir = ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index)); let app = Router::new() .nest("/api", api_routes) + .merge(health_routes) .fallback_service(serve_dir) - // Disabled for now, as svelte inlines scripts - // .layer(middleware::from_fn(csp::add_csp_header)) .layer(DefaultBodyLimit::max(*config::LIMIT)) .layer( CompressionLayer::new() @@ -65,8 +56,7 @@ async fn main() { .deflate(true) .gzip(true) .zstd(true), - ) - .with_state(shared_state); + ); let app = NormalizePathLayer::trim_trailing_slash().layer(app); @@ -77,4 +67,4 @@ async fn main() { axum::serve(listener, ServiceExt::::into_make_service(app)) .await .unwrap(); -} +} \ No newline at end of file diff --git a/packages/backend/src/note/mod.rs b/packages/backend/src/note/mod.rs index 13bc006..9efd49d 100644 --- a/packages/backend/src/note/mod.rs +++ b/packages/backend/src/note/mod.rs @@ -2,4 +2,4 @@ mod model; mod routes; pub use model::*; -pub use routes::*; +pub use routes::*; \ No newline at end of file diff --git a/packages/backend/src/note/model.rs b/packages/backend/src/note/model.rs index 8200b19..e522f98 100644 --- a/packages/backend/src/note/model.rs +++ b/packages/backend/src/note/model.rs @@ -5,22 +5,35 @@ use serde::{Deserialize, Serialize}; use crate::config; #[derive(Serialize, Deserialize, Clone)] -pub struct Note { - pub meta: String, - pub contents: String, +pub struct NoteMeta { + #[serde(skip_serializing_if = "Option::is_none")] pub views: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub expiration: Option, + #[serde(default)] + pub extra: Vec, } -#[derive(Serialize)] -pub struct NoteInfo { - pub meta: String, +#[derive(Serialize, Deserialize, Clone)] +pub struct CreateRequest { + pub meta: NoteMeta, + pub data: Vec, } -#[derive(Serialize)] -pub struct NotePublic { - pub meta: String, - pub contents: String, +#[derive(Serialize, Deserialize)] +pub struct CreateResponse { + pub id: String, +} + +#[derive(Serialize, Deserialize)] +pub struct MetaResponse { + pub meta: NoteMeta, +} + +#[derive(Serialize, Deserialize)] +pub struct NoteResponse { + pub meta: NoteMeta, + pub data: Vec, } pub fn generate_id() -> String { @@ -32,5 +45,5 @@ pub fn generate_id() -> String { let _ = sr.fill(&mut id); result.push_str(&bs62::encode_data(&id)); } - return result; -} + result +} \ No newline at end of file diff --git a/packages/backend/src/note/routes.rs b/packages/backend/src/note/routes.rs index bff47ef..de6ce11 100644 --- a/packages/backend/src/note/routes.rs +++ b/packages/backend/src/note/routes.rs @@ -2,155 +2,143 @@ use axum::{ extract::Path, http::StatusCode, response::{IntoResponse, Response}, - Json, + body::Bytes, }; -use serde::{Deserialize, Serialize}; -use std::{sync::Arc, time::SystemTime}; -use tokio::sync::Mutex; +use serde::Deserialize; +use std::time::SystemTime; -use crate::note::{generate_id, Note, NoteInfo}; +use crate::note::{CreateRequest, generate_id}; use crate::store; -use crate::{config, lock::SharedState}; +use crate::config; -use super::NotePublic; +use super::{CreateResponse, MetaResponse, NoteResponse, NoteMeta}; -pub fn now() -> u32 { +pub fn now() -> u64 { SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() - .as_secs() as u32 + .as_secs() } #[derive(Deserialize)] -pub struct OneNoteParams { +pub struct NoteParams { id: String, } -pub async fn preview(Path(OneNoteParams { id }): Path) -> Response { - let note = store::get(&id); +pub async fn create(body: Bytes) -> Response { + let req: CreateRequest = match rmp_serde::from_slice(&body) { + Ok(r) => r, + Err(_) => return (StatusCode::BAD_REQUEST, "Invalid msgpack").into_response(), + }; - match note { - Ok(Some(n)) => (StatusCode::OK, Json(NoteInfo { meta: n.meta })).into_response(), - Ok(None) => (StatusCode::NOT_FOUND).into_response(), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + if req.meta.views.is_none() && req.meta.expiration.is_none() { + return (StatusCode::BAD_REQUEST, "At least views or expiration must be set").into_response(); } -} -#[derive(Serialize, Deserialize)] -struct CreateResponse { - id: String, -} - -pub async fn create(Json(mut n): Json) -> Response { - // let mut n = note.into_inner(); - let id = generate_id(); - // let bad_req = HttpResponse::BadRequest().finish(); - if n.views == None && n.expiration == None { - return ( - StatusCode::BAD_REQUEST, - "At least views or expiration must be set", - ) - .into_response(); + if req.meta.extra.len() > *config::EXTRA_SIZE_LIMIT { + return (StatusCode::BAD_REQUEST, "Extra data too large").into_response(); } + + let mut meta = req.meta; + if !*config::ALLOW_ADVANCED { - n.views = Some(1); - n.expiration = None; + meta.views = Some(1); + meta.expiration = None; } - match n.views { + + match meta.views { Some(v) => { if v > *config::MAX_VIEWS || v < 1 { return (StatusCode::BAD_REQUEST, "Invalid views").into_response(); } - n.expiration = None; // views overrides expiration } - _ => {} + None => {} } - match n.expiration { + + let expiration_ts = match meta.expiration { Some(e) => { if e > *config::MAX_EXPIRATION || e < 1 { return (StatusCode::BAD_REQUEST, "Invalid expiration").into_response(); } - let expiration = now() + (e * 60); - n.expiration = Some(expiration); + Some(now() + (e as u64 * 60)) + } + None => None, + }; + + let id = generate_id(); + let views = meta.views.map(|v| v as i64); + + match store::set(&id, &req.data, views, expiration_ts, &meta.extra) { + Ok(_) => { + let resp = CreateResponse { id }; + let bytes = rmp_serde::to_vec_named(&resp).unwrap(); + (StatusCode::OK, Bytes::from(bytes)).into_response() } - _ => {} - } - match store::set(&id.clone(), &n.clone()) { - Ok(_) => (StatusCode::OK, Json(CreateResponse { id })).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } -pub async fn delete( - Path(OneNoteParams { id }): Path, - state: axum::extract::State, -) -> Response { - let mut locks_map = state.locks.lock().await; - let lock = locks_map - .entry(id.clone()) - .or_insert_with(|| Arc::new(Mutex::new(()))) - .clone(); - drop(locks_map); - let _guard = lock.lock().await; - - let note = store::get(&id); - match note { - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), +pub async fn preview(Path(NoteParams { id }): Path) -> Response { + match store::get_meta(&id) { + Ok(Some((views, expiration, extra))) => { + let meta = NoteMeta { + views: views.map(|v| v as u32), + expiration: expiration.map(|e| e as u32), + extra, + }; + let resp = MetaResponse { meta }; + let bytes = rmp_serde::to_vec_named(&resp).unwrap(); + (StatusCode::OK, Bytes::from(bytes)).into_response() + } Ok(None) => (StatusCode::NOT_FOUND).into_response(), - Ok(Some(note)) => { - let mut changed = note.clone(); - if changed.views == None && changed.expiration == None { - return (StatusCode::BAD_REQUEST).into_response(); - } - match changed.views { - Some(v) => { - changed.views = Some(v - 1); - let id = id.clone(); - if v <= 1 { - match store::del(&id) { - Err(e) => { - return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) - .into_response(); - } - _ => {} - } - } else { - match store::set(&id, &changed.clone()) { - Err(e) => { - return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) - .into_response(); - } - _ => {} - } - } - } - _ => {} - } - - let n = now(); - match changed.expiration { - Some(e) => { - if e < n { - match store::del(&id.clone()) { - Ok(_) => return (StatusCode::BAD_REQUEST).into_response(), - Err(e) => { - return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()) - .into_response() - } - } - } - } - _ => {} - } - - return ( - StatusCode::OK, - Json(NotePublic { - contents: changed.contents, - meta: changed.meta, - }), - ) - .into_response(); - } + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } + +pub async fn view(Path(NoteParams { id }): Path) -> Response { + let (views, expiration, extra) = match store::get_meta(&id) { + Ok(Some(v)) => v, + _ => return (StatusCode::NOT_FOUND).into_response(), + }; + + let has_views = views.is_some(); + + if has_views { + let remaining = match store::decrement_views(&id) { + Ok(r) => r, + Err(_) => return (StatusCode::NOT_FOUND).into_response(), + }; + + let data = match store::get_data(&id) { + Ok(Some(d)) => d, + _ => return (StatusCode::NOT_FOUND).into_response(), + }; + + if remaining <= 0 { + let _ = store::del(&id); + } + + let meta = NoteMeta { + views: Some(if remaining > 0 { remaining as u32 } else { 0 }), + expiration: expiration.map(|e| e as u32), + extra, + }; + let resp = NoteResponse { meta, data }; + let bytes = rmp_serde::to_vec_named(&resp).unwrap(); + (StatusCode::OK, Bytes::from(bytes)).into_response() + } else { + let data = match store::get_data(&id) { + Ok(Some(d)) => d, + _ => return (StatusCode::NOT_FOUND).into_response(), + }; + + let meta = NoteMeta { + views: None, + expiration: expiration.map(|e| e as u32), + extra, + }; + let resp = NoteResponse { meta, data }; + let bytes = rmp_serde::to_vec_named(&resp).unwrap(); + (StatusCode::OK, Bytes::from(bytes)).into_response() + } +} \ No newline at end of file diff --git a/packages/backend/src/store.rs b/packages/backend/src/store.rs index af437eb..7dbe098 100644 --- a/packages/backend/src/store.rs +++ b/packages/backend/src/store.rs @@ -1,71 +1,90 @@ -use redis; use redis::Commands; use crate::config; -use crate::note::now; -use crate::note::Note; lazy_static! { - static ref REDIS_CLIENT: String = std::env::var("REDIS") + static ref CACHE_URL: String = std::env::var("CACHE") .unwrap_or("redis://127.0.0.1/".to_string()) .parse() .unwrap(); } -fn prefixed(id: &String) -> String { - format!("{}{}", config::REDIS_PREFIX.as_str(), id) +fn prefixed(id: &str) -> String { + format!("{}{}", config::CACHE_PREFIX.as_str(), id) } -fn get_connection() -> Result { +fn conn() -> Result { let client = - redis::Client::open(REDIS_CLIENT.to_string()).map_err(|_| "Unable to connect to redis")?; - client - .get_connection() - .map_err(|_| "Unable to connect to redis") + redis::Client::open(CACHE_URL.to_string()).map_err(|_| "Unable to connect to cache")?; + client.get_connection().map_err(|_| "Unable to connect to cache") } -pub fn can_reach_redis() -> bool { - let conn = get_connection(); - return match conn { - Ok(_) => true, - Err(_) => false, - }; +pub fn can_reach_cache() -> bool { + conn().is_ok() } -pub fn set(id: &String, note: &Note) -> Result<(), &'static str> { +pub fn set(id: &str, data: &[u8], views: Option, expiration: Option, extra: &[u8]) -> Result<(), &'static str> { let key = prefixed(id); - let serialized = serde_json::to_string(¬e.clone()).unwrap(); - let mut conn = get_connection()?; + let mut c = conn()?; - conn.set::<_, _, ()>(key.as_str(), serialized) - .map_err(|_| "Unable to set note in redis")?; - match note.expiration { - Some(e) => { - let seconds = e - now(); - conn.expire::<_, ()>(key.as_str(), seconds as i64) - .map_err(|_| "Unable to set expiration on note")? - } - None => {} - }; - Ok(()) -} + c.hset::<_, _, _, ()>(&key, "data", data).map_err(|_| "Unable to set note")?; + c.hset::<_, _, _, ()>(&key, "extra", extra).map_err(|_| "Unable to set note")?; -pub fn get(id: &String) -> Result, &'static str> { - let key = prefixed(id); - let mut conn = get_connection()?; - let value: Option = conn.get(key.as_str()).map_err(|_| "Could not load note in redis")?; - match value { - None => return Ok(None), - Some(s) => { - let deserialize: Note = serde_json::from_str(&s).unwrap(); - return Ok(Some(deserialize)); - } + if let Some(v) = views { + c.hset::<_, _, _, ()>(&key, "views", v).map_err(|_| "Unable to set note")?; + } + if let Some(e) = expiration { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let ttl = e.saturating_sub(now); + c.expire::<_, ()>(&key, ttl as i64).map_err(|_| "Unable to set expiration")?; } -} -pub fn del(id: &String) -> Result<(), &'static str> { - let key = prefixed(id); - let mut conn = get_connection()?; - conn.del::<_, ()>(key.as_str()).map_err(|_| "Unable to delete note in redis")?; Ok(()) } + +pub fn get_meta(id: &str) -> Result, Option, Vec)>, &'static str> { + let key = prefixed(id); + let mut c = conn()?; + + let exists: bool = c.exists::<_, bool>(&key).map_err(|_| "Cache error")?; + if !exists { + return Ok(None); + } + + let views: Option = c.hget::<_, _, Option>(&key, "views").map_err(|_| "Cache error")?; + let expiration: Option = c.hget::<_, _, Option>(&key, "expiration").map_err(|_| "Cache error")?; + let extra: Vec = c.hget::<_, _, Vec>(&key, "extra").unwrap_or_default(); + + Ok(Some((views, expiration, extra))) +} + +pub fn get_data(id: &str) -> Result>, &'static str> { + let key = prefixed(id); + let mut c = conn()?; + let data: Option> = c.hget::<_, _, Option>>(&key, "data").map_err(|_| "Cache error")?; + Ok(data) +} + +pub fn has_views(id: &str) -> Result { + let key = prefixed(id); + let mut c = conn()?; + let has: bool = c.hexists::<_, _, bool>(&key, "views").map_err(|_| "Cache error")?; + Ok(has) +} + +pub fn decrement_views(id: &str) -> Result { + let key = prefixed(id); + let mut c = conn()?; + let result: i64 = c.hincr::<_, _, _, i64>(&key, "views", -1).map_err(|_| "Cache error")?; + Ok(result) +} + +pub fn del(id: &str) -> Result<(), &'static str> { + let key = prefixed(id); + let mut c = conn()?; + c.del::<_, ()>(&key).map_err(|_| "Unable to delete note")?; + Ok(()) +} \ No newline at end of file diff --git a/packages/cli/build.js b/packages/cli/build.js index a977c85..14f66c0 100644 --- a/packages/cli/build.js +++ b/packages/cli/build.js @@ -4,12 +4,13 @@ import pkg from './package.json' with { type: 'json' } const watch = process.argv.slice(2)[0] === '--watch' await build({ - entry: ['src/index.ts', 'src/cli.ts', 'src/shared/shared.ts'], + entry: ['src/index.ts', 'src/cli.ts'], dts: true, minify: true, - format: ['esm', 'cjs'], - target: 'es2020', + format: ['esm'], + target: 'es2022', clean: true, + noExternal: ['@cryptgeon/shared'], define: { VERSION: `"${pkg.version}"` }, watch, -}) +}) \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index 71dbeb3..f07a3bd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "cryptgeon", - "version": "2.9.3", + "version": "3.0.0", "homepage": "https://github.com/cupcakearmy/cryptgeon", "repository": { "type": "git", @@ -9,39 +9,36 @@ }, "type": "module", "exports": { - ".": "./dist/index.js", - "./shared": { - "import": "./dist/shared/shared.js", - "types": "./dist/shared/shared.d.ts" - } + ".": "./dist/index.js" }, "types": "./dist/index.d.ts", "bin": { - "cryptgeon": "./dist/cli.cjs" + "cryptgeon": "./dist/cli.js" }, "files": [ "dist" ], "scripts": { - "bin": "run-s build package", - "build": "tsc && node build.js", + "build": "node build.js", "dev": "node build.js --watch", "prepublishOnly": "run-s build" }, - "devDependencies": { + "dependencies": { + "@cryptgeon/shared": "workspace:*", + "@msgpack/msgpack": "^3.1.3", "@commander-js/extra-typings": "^12.1.0", - "@types/inquirer": "^9.0.9", - "@types/mime": "^4.0.0", - "@types/node": "^20.19.41", - "commander": "^12.1.0", "inquirer": "^9.3.8", "mime": "^4.1.0", - "occulto": "^2.0.6", - "pretty-bytes": "^6.1.1", + "pretty-bytes": "^6.1.1" + }, + "devDependencies": { + "@types/inquirer": "^9.0.9", + "@types/node": "^22.15.3", + "commander": "^12.1.0", "tsup": "^8.5.1", "typescript": "^5.9.3" }, "engines": { - "node": ">=18" + "node": ">=22" } -} +} \ No newline at end of file diff --git a/packages/cli/src/actions/download.ts b/packages/cli/src/actions/download.ts index 7aa04f1..c842104 100644 --- a/packages/cli/src/actions/download.ts +++ b/packages/cli/src/actions/download.ts @@ -1,51 +1,42 @@ import inquirer from 'inquirer' import { access, constants, writeFile } from 'node:fs/promises' import { basename, resolve } from 'node:path' -import { AES, Hex } from 'occulto' +import { decode } from '@msgpack/msgpack' import pretty from 'pretty-bytes' -import { Adapters } from '../shared/adapters.js' -import { API } from '../shared/api.js' +import { decrypt, deriveKey, setServer, getServer, info, get } from '@cryptgeon/shared' export async function download(url: URL, all: boolean, suggestedPassword?: string) { - API.setOptions({ server: url.origin }) + setServer(url.origin) const id = url.pathname.split('/')[2] - const preview = await API.info(id).catch(() => { - throw new Error('Note does not exist or is expired') - }) + const meta = await info(id) + if (!meta) throw new Error('Note does not exist or is expired') - // Password - let password: string - const derivation = preview?.meta.derivation - if (derivation) { + let key: Uint8Array + if (meta.extra && meta.extra.length > 0) { if (suggestedPassword) { - password = suggestedPassword + const derivation = decode(meta.extra) as any + key = deriveKey(suggestedPassword, new Uint8Array(derivation.salt)) } else { const response = await inquirer.prompt([ - { - type: 'password', - message: 'Note password', - name: 'password', - }, + { type: 'password', message: 'Note password', name: 'password' }, ]) - password = response.password + const derivation = decode(meta.extra) as any + key = deriveKey(response.password, new Uint8Array(derivation.salt)) } } else { - password = url.hash.slice(1) + const hex = url.hash.slice(1) + key = new Uint8Array(Buffer.from(hex, 'hex')) } - const key = derivation ? (await AES.derive(password, derivation))[0] : Hex.decode(password) - const note = await API.get(id) + const note = await get(id) + if (!note) throw new Error('Could not load note') - const couldNotDecrypt = new Error('Could not decrypt note. Probably an invalid password') - switch (note.meta.type) { - case 'file': - const files = await Adapters.Files.decrypt(note.contents, key).catch(() => { - throw couldNotDecrypt - }) - if (!files) { - throw new Error('No files found in note') - } + const decrypted = decrypt(note.data, key) + const content = decode(decrypted) as any + switch (content.type) { + case 'files': + const files: { name: string; data: Uint8Array }[] = content.data let selected: typeof files if (all) { selected = files @@ -55,36 +46,32 @@ export async function download(url: URL, all: boolean, suggestedPassword?: strin type: 'checkbox', message: 'What files should be saved?', name: 'names', - choices: files.map((file) => ({ - value: file.name, - name: `${file.name} - ${file.type} - ${pretty(file.size, { binary: true })}`, + choices: files.map((f) => ({ + value: f.name, + name: `${f.name} - ${pretty(f.data.length, { binary: true })}`, checked: true, })), }, ]) - selected = files.filter((file) => names.includes(file.name)) + selected = files.filter((f) => names.includes(f.name)) } - if (!selected.length) throw new Error('No files selected') await Promise.all( - selected.map(async (file) => { - let filename = resolve(file.name) + selected.map(async (f) => { + let filename = resolve(f.name) try { - // If exists -> prepend timestamp to not overwrite the current file await access(filename, constants.R_OK) - filename = resolve(`${Date.now()}-${file.name}`) + filename = resolve(`${Date.now()}-${f.name}`) } catch {} - await writeFile(filename, file.contents) + await writeFile(filename, f.data) console.log(`Saved: ${basename(filename)}`) }) ) - break case 'text': - const plaintext = await Adapters.Text.decrypt(note.contents, key).catch(() => { - throw couldNotDecrypt - }) - console.log(plaintext) + console.log(content.data) break + default: + throw new Error('Unknown content type') } -} +} \ No newline at end of file diff --git a/packages/cli/src/actions/upload.ts b/packages/cli/src/actions/upload.ts index 3bb1258..a4e352f 100644 --- a/packages/cli/src/actions/upload.ts +++ b/packages/cli/src/actions/upload.ts @@ -1,46 +1,43 @@ import { readFile, stat } from 'node:fs/promises' import { basename } from 'node:path' +import { encode } from '@msgpack/msgpack' import mime from 'mime' -import { AES, Hex } from 'occulto' -import { Adapters } from '../shared/adapters.js' -import { API, FileDTO, Note, NoteMeta } from '../shared/api.js' +import { encrypt, generateKey, deriveKey, randomBytes, setServer, getServer, create, utf8ToBytes } from '@cryptgeon/shared' -export type UploadOptions = Pick & { password?: string } +export type UploadOptions = { views?: number; expiration?: number; password?: string } export async function upload(input: string | string[], options: UploadOptions): Promise { const { password, ...noteOptions } = options - const derived = options.password ? await AES.derive(options.password) : undefined - const key = derived ? derived[0] : await AES.generateKey() - let contents: string - let type: NoteMeta['type'] - if (typeof input === 'string') { - contents = await Adapters.Text.encrypt(input, key) - type = 'text' + let key: Uint8Array + let extra = new Uint8Array() + if (password) { + const salt = randomBytes(16) + key = deriveKey(password, salt) + extra = encode({ salt, N: 32768, r: 8, p: 1 }) } else { - const files: FileDTO[] = await Promise.all( - input.map(async (path) => { - const data = new Uint8Array(await readFile(path)) - const stats = await stat(path) - const extension = path.substring(path.indexOf('.') + 1) - const type = mime.getType(extension) ?? 'application/octet-stream' - return { - name: basename(path), - size: stats.size, - contents: data, - type, - } satisfies FileDTO - }) - ) - contents = await Adapters.Files.encrypt(files, key) - type = 'file' + key = generateKey() } - // Create the actual note and upload it. - const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } } - const result = await API.create(note) - let url = `${API.getOptions().server}/note/${result.id}` - if (!derived) url += `#${Hex.encode(key)}` + let inner: Uint8Array + if (typeof input === 'string') { + inner = encode({ type: 'text', data: input }) + } else { + const files = await Promise.all( + input.map(async (path) => { + const data = new Uint8Array(await readFile(path)) + const extension = path.substring(path.indexOf('.') + 1) + const type = mime.getType(extension) ?? 'application/octet-stream' + return { name: basename(path), mime: type, size: data.length, data } + }) + ) + inner = encode({ type: 'files', data: files }) + } + + const data = encrypt(inner, key) + const result = await create({ meta: { ...noteOptions, extra }, data }) + let url = `${getServer()}/note/${result.id}` + if (!password) url += `#${Buffer.from(key).toString('hex')}` return url -} +} \ No newline at end of file diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index e505df3..835fd78 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -5,7 +5,7 @@ import prettyBytes from 'pretty-bytes' import { download } from './actions/download.js' import { upload } from './actions/upload.js' -import { API } from './shared/api.js' +import { setServer, status } from '@cryptgeon/shared' import { parseFile, parseNumber } from './utils/parsers.js' import { getStdin } from './utils/stdin.js' import { checkConstrains, exit } from './utils/utils.js' @@ -33,15 +33,12 @@ program .description('show information about the server') .addOption(server) .action(async (options) => { - API.setOptions({ server: options.server }) - const response = await API.status() - const formatted = { - ...response, - max_size: prettyBytes(response.max_size), - } - for (const key of Object.keys(formatted)) { - if (key.startsWith('theme_')) delete formatted[key as keyof typeof formatted] - } + setServer(options.server) + const response = await status() + const formatted = Object.fromEntries( + Object.entries({ ...response, max_size: prettyBytes(response.max_size as number) }) + .filter(([key]) => !key.startsWith('theme_')) + ) console.table(formatted) }) @@ -54,7 +51,7 @@ send .addOption(minutes) .addOption(password) .action(async (files, options) => { - API.setOptions({ server: options.server }) + setServer(options.server) await checkConstrains(options) options.password ||= await getStdin() try { @@ -72,7 +69,7 @@ send .addOption(minutes) .addOption(password) .action(async (text, options) => { - API.setOptions({ server: options.server }) + setServer(options.server) await checkConstrains(options) options.password ||= await getStdin() try { @@ -103,4 +100,4 @@ program } }) -program.parse() +program.parse() \ No newline at end of file diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a22bcf6..78d73ce 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,4 +1,2 @@ export * from './actions/download.js' -export * from './actions/upload.js' -export * from './shared/adapters.js' -export * from './shared/api.js' +export * from './actions/upload.js' \ No newline at end of file diff --git a/packages/cli/src/shared/adapters.ts b/packages/cli/src/shared/adapters.ts deleted file mode 100644 index e35b972..0000000 --- a/packages/cli/src/shared/adapters.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { AES, Bytes, type TypedArray } from 'occulto' -import type { EncryptedFileDTO, FileDTO } from './api' - -abstract class CryptAdapter { - abstract encrypt(plaintext: T, key: TypedArray): Promise - abstract decrypt(ciphertext: string, key: TypedArray): Promise -} - -class CryptTextAdapter implements CryptAdapter { - async encrypt(plaintext: string, key: TypedArray) { - return await AES.encrypt(Bytes.encode(plaintext), key) - } - async decrypt(ciphertext: string, key: TypedArray) { - return Bytes.decode(await AES.decrypt(ciphertext, key)) - } -} - -class CryptBlobAdapter implements CryptAdapter { - async encrypt(plaintext: TypedArray, key: TypedArray) { - return await AES.encrypt(plaintext, key) - } - - async decrypt(ciphertext: string, key: TypedArray) { - return await AES.decrypt(ciphertext, key) - // const plaintext = await AES.decrypt(ciphertext, key) - // return new Blob([plaintext], { type: 'application/octet-stream' }) - } -} - -class CryptFilesAdapter implements CryptAdapter { - async encrypt(plaintext: FileDTO[], key: TypedArray) { - const adapter = new CryptBlobAdapter() - const data: Promise[] = plaintext.map(async (file) => ({ - name: file.name, - size: file.size, - type: file.type, - contents: await adapter.encrypt(file.contents, key), - })) - return JSON.stringify(await Promise.all(data)) - } - - async decrypt(ciphertext: string, key: TypedArray) { - const adapter = new CryptBlobAdapter() - const data: EncryptedFileDTO[] = JSON.parse(ciphertext) - const files: FileDTO[] = await Promise.all( - data.map(async (file) => ({ - name: file.name, - size: file.size, - type: file.type, - contents: await adapter.decrypt(file.contents, key), - })) - ) - return files - } -} - -export const Adapters = { - Text: new CryptTextAdapter(), - Blob: new CryptBlobAdapter(), - Files: new CryptFilesAdapter(), -} diff --git a/packages/cli/src/shared/api.ts b/packages/cli/src/shared/api.ts deleted file mode 100644 index f8a9936..0000000 --- a/packages/cli/src/shared/api.ts +++ /dev/null @@ -1,141 +0,0 @@ -import type { KeyData, TypedArray } from 'occulto' - -export type NoteMeta = { - type: 'text' | 'file' - derivation?: KeyData -} - -export type Note = { - contents: string - meta: NoteMeta - views?: number - expiration?: number -} -export type NoteInfo = Pick -export type NotePublic = Pick -export type NoteCreate = Omit & { meta: string } - -export type FileDTO = Pick & { - contents: TypedArray -} - -export type EncryptedFileDTO = Omit & { - contents: string -} - -type ClientOptions = { - server: string -} - -type CallOptions = { - url: string - method: string - body?: any -} - -export class PayloadToLargeError extends Error {} - -export let client: ClientOptions = { - server: '', -} - -function setOptions(options: Partial) { - client = { ...client, ...options } -} - -function getOptions(): ClientOptions { - return client -} - -async function call(options: CallOptions) { - const url = client.server + '/api/' + options.url - const response = await fetch(url, { - method: options.method, - body: options.body === undefined ? undefined : JSON.stringify(options.body), - mode: 'cors', - headers: { - 'Content-Type': 'application/json', - }, - }) - - if (!response.ok) { - if (response.status === 413) throw new PayloadToLargeError() - else throw new Error('API call failed') - } - return response.json() -} - -async function create(note: Note) { - const { meta, ...rest } = note - const body: NoteCreate = { - ...rest, - meta: JSON.stringify(meta), - } - const data = await call({ - url: 'notes/', - method: 'post', - body, - }) - return data as { id: string } -} - -async function get(id: string): Promise { - const data = await call({ - url: `notes/${id}`, - method: 'delete', - }) - const { contents, meta } = data - const note = { - contents, - meta: JSON.parse(meta), - } satisfies NotePublic - if (note.meta.derivation) note.meta.derivation.salt = new Uint8Array(Object.values(note.meta.derivation.salt)) - return note -} - -async function info(id: string): Promise { - const data = await call({ - url: `notes/${id}`, - method: 'get', - }) - const { meta } = data - const note = { - meta: JSON.parse(meta), - } satisfies NoteInfo - if (note.meta.derivation) note.meta.derivation.salt = new Uint8Array(Object.values(note.meta.derivation.salt)) - return note -} - -export type Status = { - version: string - max_size: number - max_views: number - max_expiration: number - allow_advanced: boolean - allow_files: boolean - imprint_url: string - imprint_html: string - theme_image: string - theme_text: string - theme_favicon: string - theme_page_title: string - theme_new_note_notice: boolean - theme_home_link: boolean -} - -async function status() { - const data = await call({ - url: 'status/', - method: 'get', - }) - return data as Status -} - -export const API = { - setOptions, - getOptions, - create, - get, - info, - status, -} diff --git a/packages/cli/src/shared/shared.ts b/packages/cli/src/shared/shared.ts deleted file mode 100644 index 3aaabc9..0000000 --- a/packages/cli/src/shared/shared.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './adapters.js' -export * from './api.js' diff --git a/packages/cli/src/utils/utils.ts b/packages/cli/src/utils/utils.ts index 18da6a1..9b919c7 100644 --- a/packages/cli/src/utils/utils.ts +++ b/packages/cli/src/utils/utils.ts @@ -1,5 +1,5 @@ import { exit as exitNode } from 'node:process' -import { API } from '../shared/api.js' +import { status } from '@cryptgeon/shared' export function exit(message: string) { console.error(message) @@ -7,13 +7,11 @@ export function exit(message: string) { } export async function checkConstrains(constrains: { views?: number; minutes?: number }) { - const { views, minutes } = constrains - if (views && minutes) exit('cannot set view and minutes constrains simultaneously') - if (!views && !minutes) constrains.views = 1 + if (!constrains.views && !constrains.minutes) constrains.views = 1 - const response = await API.status() - if (views && views > response.max_views) - exit(`Only a maximum of ${response.max_views} views allowed. ${views} given.`) - if (minutes && minutes > response.max_expiration) - exit(`Only a maximum of ${response.max_expiration} minutes allowed. ${minutes} given.`) -} + const response = await status() + if (constrains.views && constrains.views > (response.max_views as number)) + exit(`Only a maximum of ${response.max_views} views allowed. ${constrains.views} given.`) + if (constrains.minutes && constrains.minutes > (response.max_expiration as number)) + exit(`Only a maximum of ${response.max_expiration} minutes allowed. ${constrains.minutes} given.`) +} \ No newline at end of file diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 29a0a45..50a1362 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -29,9 +29,8 @@ "vite": "^8.0.14" }, "dependencies": { + "@cryptgeon/shared": "workspace:*", "@fontsource/fira-mono": "^5.2.7", - "cryptgeon": "workspace:*", - "occulto": "^2.0.6", "pretty-bytes": "^7.1.0", "uqr": "^0.1.3" } diff --git a/packages/frontend/src/lib/stores/status.ts b/packages/frontend/src/lib/stores/status.ts index b020413..047cfc3 100644 --- a/packages/frontend/src/lib/stores/status.ts +++ b/packages/frontend/src/lib/stores/status.ts @@ -1,8 +1,25 @@ -import { API, type Status } from 'cryptgeon/shared' +import { status as apiStatus } from '@cryptgeon/shared' import { writable } from 'svelte/store' -export const status = writable(null) +export type StatusInfo = { + version: string + max_size: number + max_views: number + max_expiration: number + allow_advanced: boolean + allow_files: boolean + imprint_url: string + imprint_html: string + theme_image: string + theme_text: string + theme_page_title: string + theme_favicon: string + theme_new_note_notice: boolean + theme_home_link: boolean +} + +export const status = writable(null) export async function init() { - status.set(await API.status()) -} + status.set((await apiStatus()) as StatusInfo) +} \ No newline at end of file diff --git a/packages/frontend/src/lib/ui/AdvancedParameters.svelte b/packages/frontend/src/lib/ui/AdvancedParameters.svelte index ba755d8..a1e8b77 100644 --- a/packages/frontend/src/lib/ui/AdvancedParameters.svelte +++ b/packages/frontend/src/lib/ui/AdvancedParameters.svelte @@ -4,10 +4,9 @@ import { status } from '$lib/stores/status' import Switch from '$lib/ui/Switch.svelte' import TextInput from '$lib/ui/TextInput.svelte' - import type { Note } from 'cryptgeon/shared' interface Props { - note: Note + note: { views: number; expiration: number } timeExpiration?: boolean customPassword?: string | null } diff --git a/packages/frontend/src/lib/ui/FileUpload.svelte b/packages/frontend/src/lib/ui/FileUpload.svelte index 6ba7135..7a37a68 100644 --- a/packages/frontend/src/lib/ui/FileUpload.svelte +++ b/packages/frontend/src/lib/ui/FileUpload.svelte @@ -3,7 +3,7 @@ import Button from '$lib/ui/Button.svelte' import MaxSize from '$lib/ui/MaxSize.svelte' - import type { FileDTO } from 'cryptgeon/shared' + import type { FileDTO } from '@cryptgeon/shared' interface Props { label?: string @@ -16,9 +16,9 @@ async function fileToDTO(file: File): Promise { return { name: file.name, + mime: file.type, size: file.size, - type: file.type, - contents: new Uint8Array(await file.arrayBuffer()), + data: new Uint8Array(await file.arrayBuffer()), } } diff --git a/packages/frontend/src/lib/ui/PastedFilesPreview.svelte b/packages/frontend/src/lib/ui/PastedFilesPreview.svelte index ba72838..46c8f19 100644 --- a/packages/frontend/src/lib/ui/PastedFilesPreview.svelte +++ b/packages/frontend/src/lib/ui/PastedFilesPreview.svelte @@ -1,7 +1,7 @@ diff --git a/packages/frontend/src/lib/views/Create.svelte b/packages/frontend/src/lib/views/Create.svelte index a299a1e..6f5610e 100644 --- a/packages/frontend/src/lib/views/Create.svelte +++ b/packages/frontend/src/lib/views/Create.svelte @@ -1,5 +1,10 @@