Compare commits

...

22 Commits

Author SHA1 Message Date
e02f7f59c6 changelog 2022-03-02 16:59:25 +01:00
e8c6467faa dont' allow empty notes 2022-03-02 16:55:10 +01:00
43f67c795d don't remove already selected files 2022-03-02 16:55:04 +01:00
83f0902291 lokalise 2022-03-02 16:32:36 +01:00
11a6621bd7 script to download locales 2022-03-02 16:15:25 +01:00
36fa451249 enforce limits 2022-03-01 16:16:02 +01:00
d112eba8fe add config for max views, expiration and advanced 2022-03-01 15:24:17 +01:00
ef39f9ec0b cleanup 2022-03-01 02:04:43 +01:00
8517c20e6c remove unused code 2022-03-01 02:01:57 +01:00
728ad56b33 enforce strict typescript 2022-03-01 02:00:01 +01:00
f185ccee03 add svelte check 2022-03-01 01:52:20 +01:00
284bbcbae2 better typings 2022-03-01 01:52:09 +01:00
7eba454f1b visual improvements 2022-03-01 01:52:04 +01:00
dcd9efaeba use native icons 2022-03-01 01:51:43 +01:00
f13bcbaf3f update robots 2022-03-01 01:51:21 +01:00
8e7e0414a6 add copy to clipboard note 2022-03-01 01:16:31 +01:00
229c8d8368 update actix to version 4 2022-03-01 00:53:47 +01:00
1adf87b884 set memcached size 2022-03-01 00:53:34 +01:00
a061b540b1 make top margin smaller 2022-01-21 17:46:13 +01:00
824603ff4a add locales 2022-01-16 14:39:45 +01:00
539d99d35f version bump 2022-01-16 14:04:49 +01:00
716034660c translate the app 2022-01-16 14:02:53 +01:00
58 changed files with 2092 additions and 1310 deletions

BIN
.github/lokalise.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

10
.vscode/settings.json vendored
View File

@@ -1,6 +1,6 @@
{ {
"cSpell.words": [ "cSpell.words": ["ciphertext", "cryptgeon"],
"ciphertext", "i18n-ally.localesPaths": ["frontend/locales"],
"cryptgeon" "i18n-ally.enabledFrameworks": ["svelte"],
] "i18n-ally.keystyle": "nested"
} }

View File

@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4.0] - 2022-01-16
### Added
- Support for multiple languages.
- Select multiple files without removing already selected ones.
- Tooltip for copy action.
- Configure maximum views, expiration and advanced options for the server.
### Changed
- Use native SVGs instead of images.
- Update robots.txt file to allow only root.
- Stronger frontend types.
## [1.3.3] - 2022-01-03 ## [1.3.3] - 2022-01-03
### Fixed ### Fixed

View File

@@ -1,4 +1,4 @@
# Client # Frontend
FROM node:16-alpine as CLIENT FROM node:16-alpine as CLIENT
WORKDIR /tmp WORKDIR /tmp
@@ -8,8 +8,8 @@ RUN npm install -g pnpm
RUN pnpm install RUN pnpm install
RUN pnpm run build RUN pnpm run build
# Rust # Backend
FROM rust:1.56-alpine as RUST FROM rust:1.59-alpine as RUST
WORKDIR /tmp WORKDIR /tmp
RUN apk add libc-dev openssl-dev alpine-sdk RUN apk add libc-dev openssl-dev alpine-sdk

View File

@@ -10,13 +10,18 @@
</a> </a>
<br/> <br/>
<a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/cryptgeon?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-cryptgeon" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=295189&theme=light" alt="Cryptgeon - Securely share self-destructing notes | Product Hunt" height="50" /></a>
<a href=""><img src="./.github/lokalise.png" height="50">
<br/> <br/>
## About? ## About?
_cryptgeon_ is a secure, open source sharing note or file service inspired by [_PrivNote_](https://privnote.com) _cryptgeon_ is a secure, open source sharing note or file service inspired by [_PrivNote_](https://privnote.com)
> 🌍 If you want to translate the project feel free to reach out to me.
>
> Thanks to [Lokalise](https://lokalise.com/) for providing free access to their platform.
## Demo ## Demo
Check out the demo and see for yourself https://cryptgeon.nicco.io. Check out the demo and see for yourself https://cryptgeon.nicco.io.

1409
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cryptgeon" name = "cryptgeon"
version = "1.3.3" version = "1.4.0"
authors = ["cupcakearmy <hi@nicco.io>"] authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2021" edition = "2021"
@@ -11,9 +11,9 @@ path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
actix-web = "3" actix-web = "4"
actix-files = "0.5" actix-files = "0.6"
serde = "1" serde = { version = "1.0", features = ["derive"] }
serde_json = "1" serde_json = "1"
lazy_static = "1" lazy_static = "1"
ring = "0.16" ring = "0.16"

12
backend/src/api.rs Normal file
View File

@@ -0,0 +1,12 @@
use actix_web::web;
use crate::note;
use crate::status;
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/api")
.service(note::init())
.service(status::init()),
);
}

View File

@@ -1,10 +1,10 @@
use actix_files::{Files, NamedFile}; use actix_files::Files;
use actix_web::{web, Responder}; use actix_web::web;
pub fn init(cfg: &mut web::ServiceConfig) { pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(Files::new("/", "./frontend/build").index_file("index.html")); cfg.service(
} Files::new("/", "./frontend/build")
.index_file("index.html")
pub async fn fallback_fn() -> impl Responder { .use_etag(true),
NamedFile::open("./frontend/build/index.html") );
} }

23
backend/src/config.rs Normal file
View File

@@ -0,0 +1,23 @@
use byte_unit::Byte;
lazy_static! {
pub static ref VERSION: String = option_env!("CARGO_PKG_VERSION")
.unwrap_or("Unknown")
.to_string();
pub static ref LIMIT: u32 =
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
.unwrap()
.get_bytes() as u32;
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
.unwrap_or("100".to_string())
.parse()
.unwrap();
pub static ref MAX_EXPIRATION: u32 = std::env::var("MAX_EXPIRATION")
.unwrap_or("360".to_string()) // 6 hours in minutes
.parse()
.unwrap();
pub static ref ALLOW_ADVANCED: bool = std::env::var("ALLOW_ADVANCED")
.unwrap_or("true".to_string())
.parse()
.unwrap();
}

View File

@@ -1,12 +1,15 @@
use actix_web::{middleware, web, App, HttpServer}; use actix_web::{middleware, App, HttpServer};
use dotenv::dotenv; use dotenv::dotenv;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
mod api;
mod client; mod client;
mod config;
mod note; mod note;
mod size; mod size;
mod status;
mod store; mod store;
#[actix_web::main] #[actix_web::main]
@@ -17,9 +20,8 @@ async fn main() -> std::io::Result<()> {
.wrap(middleware::Compress::default()) .wrap(middleware::Compress::default())
.wrap(middleware::DefaultHeaders::default()) .wrap(middleware::DefaultHeaders::default())
.configure(size::init) .configure(size::init)
.configure(note::init) .configure(api::init)
.configure(client::init) .configure(client::init)
.default_service(web::resource("").route(web::get().to(client::fallback_fn)))
}) })
.bind("0.0.0.0:5000")? .bind("0.0.0.0:5000")?
.run() .run()

View File

@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
pub struct Note { pub struct Note {
pub meta: String, pub meta: String,
pub contents: String, pub contents: String,
pub views: Option<u8>, pub views: Option<u32>,
pub expiration: Option<u32>, pub expiration: Option<u32>,
} }

View File

@@ -1,9 +1,9 @@
use actix_web::{delete, get, post, web, HttpResponse, Responder}; use actix_web::{delete, get, post, web, HttpResponse, Responder, Scope};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::SystemTime; use std::time::SystemTime;
use crate::config;
use crate::note::{generate_id, Note, NoteInfo, NotePublic}; use crate::note::{generate_id, Note, NoteInfo, NotePublic};
use crate::size::LIMIT;
use crate::store; use crate::store;
pub fn now() -> u32 { pub fn now() -> u32 {
@@ -41,17 +41,22 @@ async fn create(note: web::Json<Note>) -> impl Responder {
if n.views == None && n.expiration == None { if n.views == None && n.expiration == None {
return bad_req; return bad_req;
} }
if !*config::ALLOW_ADVANCED {
n.views = Some(1);
n.expiration = None;
}
match n.views { match n.views {
Some(v) => { Some(v) => {
if v > 100 { if v > *config::MAX_VIEWS {
return bad_req; return bad_req;
} }
n.expiration = None; // views overrides expiration
} }
_ => {} _ => {}
} }
match n.expiration { match n.expiration {
Some(e) => { Some(e) => {
if e > 360 { if e > *config::MAX_EXPIRATION {
return bad_req; return bad_req;
} }
let expiration = now() + (e * 60); let expiration = now() + (e * 60);
@@ -111,27 +116,9 @@ struct Status {
max_size: usize, max_size: usize,
} }
#[get("/status")] pub fn init() -> Scope {
async fn status() -> impl Responder { web::scope("/notes")
println!("Limit: {}", *LIMIT); .service(one)
return HttpResponse::Ok().json(Status { .service(create)
version: option_env!("CARGO_PKG_VERSION") .service(delete)
.unwrap_or("Unknown")
.to_string(),
max_size: *LIMIT,
});
}
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/api")
.service(
web::scope("/notes")
.service(one)
.service(create)
.service(delete)
.service(status),
)
.service(status),
);
} }

View File

@@ -10,11 +10,9 @@ lazy_static! {
} }
pub fn init(cfg: &mut web::ServiceConfig) { pub fn init(cfg: &mut web::ServiceConfig) {
println!("Limit: {}", *LIMIT);
let json = web::JsonConfig::default().limit(*LIMIT); let json = web::JsonConfig::default().limit(*LIMIT);
let plain = web::PayloadConfig::default() let plain = web::PayloadConfig::default()
.limit(*LIMIT) .limit(*LIMIT)
.mimetype(mime::STAR_STAR); .mimetype(mime::STAR_STAR);
cfg.data(json); cfg.app_data(json).app_data(plain);
cfg.data(plain);
} }

View File

@@ -0,0 +1,5 @@
mod model;
mod routes;
pub use model::*;
pub use routes::*;

View File

@@ -0,0 +1,10 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Status {
pub version: String,
pub max_size: u32,
pub max_views: u32,
pub max_expiration: u32,
pub allow_advanced: bool,
}

View File

@@ -0,0 +1,19 @@
use actix_web::{get, web, HttpResponse, Responder, Scope};
use crate::config;
use crate::status::Status;
#[get("/")]
async fn get_status() -> impl Responder {
return HttpResponse::Ok().json(Status {
version: config::VERSION.to_string(),
max_size: *config::LIMIT,
max_views: *config::MAX_VIEWS,
max_expiration: *config::MAX_EXPIRATION,
allow_advanced: *config::ALLOW_ADVANCED,
});
}
pub fn init() -> Scope {
web::scope("/status").service(get_status)
}

View File

@@ -7,7 +7,7 @@ services:
memcached: memcached:
image: memcached:1-alpine image: memcached:1-alpine
restart: unless-stopped restart: unless-stopped
entrypoint: memcached -m 128M -I 4M entrypoint: memcached -m 256M -I 128M
ports: ports:
- 11211:11211 - 11211:11211
@@ -16,6 +16,6 @@ services:
depends_on: depends_on:
- memcached - memcached
environment: environment:
SIZE_LIMIT: 4M SIZE_LIMIT: 128M
ports: ports:
- 80:5000 - 80:5000

View File

@@ -3,7 +3,7 @@ version: '3.8'
services: services:
memcached: memcached:
image: memcached:1-alpine image: memcached:1-alpine
entrypoint: memcached -m 128 # Limit to 128 MB Ram, customize at free will. entrypoint: memcached -m 256 -I 128 # Limit to 128 MB Ram, customize at free will. -m must be at least double than -I.
app: app:
image: cupcakearmy/cryptgeon:latest image: cupcakearmy/cryptgeon:latest

View File

@@ -1,38 +1,18 @@
# create-svelte # Cryptgeon Frontend
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte); ## Locale
## Creating a project Download with these settings:
If you're seeing this, you've probably already done this step. Congrats! ```json
{
```bash "format": "json",
# create a new project in the current directory "indentation": "tab",
npm init svelte@next "json_unescaped_slashes": true,
"export_sort": "first_added",
# create a new project in my-app "original_filenames": false,
npm init svelte@next my-app "export_empty_as": "skip",
"add_newline_eof": true,
"replace_breaks": false
}
``` ```
> Note: the `@next` is temporary
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then:
```bash
npm run build
```
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.

42
frontend/locales/de.json Normal file
View File

@@ -0,0 +1,42 @@
{
"common": {
"note": "Hinweis",
"file": "Datei",
"advanced": "erweitert",
"create": "erstellen",
"loading": "Läd...",
"mode": "Modus",
"views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}",
"minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}",
"max": "max",
"share_link": "Link teilen",
"copy_clipboard": "in die Zwischenablage kopieren"
},
"home": {
"intro": "Senden Sie ganz einfach <i>vollständig verschlüsselte</i>, sichere Notizen oder Dateien mit einem Klick. Erstellen Sie einfach eine Notiz und teilen Sie den Link.",
"explanation": "die Notiz verfällt und wird nach {type} zerstört.",
"new_note": "neue Note",
"new_note_notice": "<b>Verfügbarkeit:</b><br />es ist nicht garantiert, dass die Notiz gespeichert wird, da alles im Speicher gehalten wird. Wenn dieser voll ist, werden die ältesten Notizen entfernt.<br />(Sie werden wahrscheinlich keine Probleme haben, seien Sie nur gewarnt).",
"errors": {
"note_to_big": "Notiz konnte nicht erstellt werden. Notiz ist zu groß",
"note_error": "konnte keine Notiz erstellen. Bitte versuchen Sie es erneut.",
"max": "max: {n}",
"empty_content": "Notiz ist leer."
},
"copied_to_clipboard": "in die Zwischenablage kopiert 🔗"
},
"show": {
"errors": {
"not_found": "wurde nicht gefunden oder wurde bereits gelöscht.",
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört."
},
"explanation": "Klicken Sie unten, um die Notiz anzuzeigen und zu löschen, wenn der Zähler sein Limit erreicht hat",
"show_note": "Notiz anzeigen",
"warning_will_not_see_again": "haben Sie <b>keine</b> Gelegenheit, die Notiz noch einmal zu sehen.",
"download_all": "alle herunterladen"
},
"file_upload": {
"selected_files": "Ausgewählte Dateien",
"no_files_selected": "Keine Dateien ausgewählt"
}
}

42
frontend/locales/en.json Normal file
View File

@@ -0,0 +1,42 @@
{
"common": {
"note": "note",
"file": "file",
"advanced": "advanced",
"create": "create",
"loading": "loading...",
"mode": "mode",
"views": "{n, plural, =0 {views} =1 {1 view} other {# views}}",
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
"max": "max",
"share_link": "share link",
"copy_clipboard": "copy to clipboard"
},
"home": {
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
"explanation": "the note will expire and be destroyed after {type}.",
"new_note": "new note",
"new_note_notice": "<b>availability:</b><br />the note is not guaranteed to be stored as everything is kept in ram, if it fills up the oldest notes will be removed.<br />(you probably will be fine, just be warned.)",
"errors": {
"note_to_big": "could not create note. note is to big",
"note_error": "could not create note. please try again.",
"max": "max: {n}",
"empty_content": "note is empty."
},
"copied_to_clipboard": "copied to clipboard 🔗"
},
"show": {
"errors": {
"not_found": "note was not found or was already deleted.",
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed."
},
"explanation": "click below to show and delete the note if the counter has reached it's limit",
"show_note": "show note",
"warning_will_not_see_again": "you will <b>not</b> get the chance to see the note again.",
"download_all": "download all"
},
"file_upload": {
"selected_files": "Selected Files",
"no_files_selected": "No Files Selected"
}
}

42
frontend/locales/es.json Normal file
View File

@@ -0,0 +1,42 @@
{
"common": {
"note": "nota",
"file": "archivo",
"advanced": "avanzado",
"create": "crear",
"loading": "cargando...",
"mode": "modo",
"views": "{n, plural, =0 {vistas} =1 {1 vista} other {# vistas}}",
"minutes": "{n, plural, =0 {minutos} =1 {1 minuto} other {# minutos}}",
"max": "max",
"share_link": "compartir enlace",
"copy_clipboard": "copiar al portapapeles"
},
"home": {
"intro": "Envía fácilmente notas o archivos <i>totalmente encriptados</i> y seguros con un solo clic. Solo tienes que crear una nota y compartir el enlace.",
"explanation": "la nota expirará y se destruirá después de {type}.",
"new_note": "nueva nota",
"new_note_notice": "<b>disponibilidad:</b><br />no se garantiza que la nota se almacene, ya que todo se guarda en la memoria RAM, si se llena se eliminarán las notas más antiguas.<br />(probablemente estará bien, sólo está advertido.)",
"errors": {
"note_to_big": "no se pudo crear la nota. la nota es demasiado grande",
"note_error": "No se ha podido crear la nota. Por favor, inténtelo de nuevo.",
"max": "max: {n}",
"empty_content": "la nota está vacía."
},
"copied_to_clipboard": "copiado al portapapeles 🔗"
},
"show": {
"errors": {
"not_found": "la nota no se encontró o ya fue borrada.",
"decryption_failed": "contraseña incorrecta. no se pudo descifrar. probablemente un enlace roto. la nota fue destruida."
},
"explanation": "pulse abajo para mostrar y borrar la nota si el contador ha llegado a su límite",
"show_note": "mostrar nota",
"warning_will_not_see_again": " <b>no</b> tendrás la oportunidad de volver a ver la nota.",
"download_all": "descargar todo"
},
"file_upload": {
"selected_files": "Archivos seleccionados",
"no_files_selected": "No hay archivos seleccionados"
}
}

42
frontend/locales/fr.json Normal file
View File

@@ -0,0 +1,42 @@
{
"common": {
"note": "note",
"file": "fichier",
"advanced": "avancé",
"create": "créer",
"loading": "chargement...",
"mode": "mode",
"views": "{n, plural, =0 {vues} =1 {1 vue} other {# vues}}",
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
"max": "max",
"share_link": "partager le lien",
"copy_clipboard": "copier dans le presse-papiers"
},
"home": {
"intro": "Envoyez facilement des notes ou des fichiers <i>entièrement cryptés</i> et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.",
"explanation": "la note expirera et sera détruite après {type}.",
"new_note": "nouvelle note",
"new_note_notice": "<b>disponibilité :</b><br />la note n'est pas garantie d'être stockée car tout est conservé dans la mémoire vive, si elle se remplit les notes les plus anciennes seront supprimées.<br />(vous serez probablement bien, soyez juste averti.)",
"errors": {
"note_to_big": "Impossible de créer une note. La note est trop grande",
"note_error": "n'a pas pu créer de note. Veuillez réessayer.",
"max": "max: {n}",
"empty_content": "La note est vide."
},
"copied_to_clipboard": "copié dans le presse-papiers 🔗"
},
"show": {
"errors": {
"not_found": "La note n'a pas été trouvée ou a déjà été supprimée.",
"decryption_failed": "mauvais mot de passe. impossible à déchiffrer. probablement un lien brisé. la note a été détruite."
},
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
"show_note": "note de présentation",
"warning_will_not_see_again": "vous <b>n'aurez pas</b> la chance de revoir la note.",
"download_all": "télécharger tout"
},
"file_upload": {
"selected_files": "Fichiers sélectionnés",
"no_files_selected": "Aucun fichier sélectionné"
}
}

42
frontend/locales/it.json Normal file
View File

@@ -0,0 +1,42 @@
{
"common": {
"note": "nota",
"file": "file",
"advanced": "avanzato",
"create": "crea",
"loading": "carica...",
"mode": "modalita",
"views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}",
"minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}",
"max": "max",
"share_link": "condividi link",
"copy_clipboard": "copia negli appunti"
},
"home": {
"intro": "Invia facilmente note o file <i>completamente criptati</i> e sicuri con un solo clic. Basta creare una nota e condividere il link.",
"explanation": "la nota scadrà e sarà distrutta dopo {type}.",
"new_note": "nuova nota",
"new_note_notice": "<b>disponibilità:</b><br />la nota non è garantita per essere memorizzata come tutto è tenuto in ram, se si riempie le note più vecchie saranno rimosse.<br />(probabilmente andrà bene, basta essere avvertiti).",
"errors": {
"note_to_big": "impossibile creare una nota. la nota è troppo grande",
"note_error": "Impossibile creare la nota. Riprova.",
"max": "max: {n}",
"empty_content": "la nota è vuota."
},
"copied_to_clipboard": "copiato negli appunti 🔗"
},
"show": {
"errors": {
"not_found": "non è stata trovata o è stata già cancellata.",
"decryption_failed": "password sbagliata. non ha potuto decifrare. probabilmente un link rotto. la nota è stata distrutta."
},
"explanation": "clicca sotto per mostrare e cancellare la nota se il contatore ha raggiunto il suo limite",
"show_note": "mostra la nota",
"warning_will_not_see_again": " <b>non</b> avrete la possibilità di rivedere la nota.",
"download_all": "scarica tutti"
},
"file_upload": {
"selected_files": "File selezionati",
"no_files_selected": "Nessun file selezionato"
}
}

View File

@@ -4,17 +4,25 @@
"dev": "svelte-kit dev", "dev": "svelte-kit dev",
"build": "svelte-kit build", "build": "svelte-kit build",
"preview": "svelte-kit preview", "preview": "svelte-kit preview",
"licenses": "license-checker --summary > licenses.csv" "check": "svelte-check --tsconfig tsconfig.json",
"licenses": "license-checker --summary > licenses.csv",
"locale:download": "node scripts/locale.js"
}, },
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-static": "^1.0.0-next.24", "@lokalise/node-api": "^7.1.1",
"@sveltejs/kit": "^1.0.0-next.212", "@sveltejs/adapter-static": "^1.0.0-next.26",
"svelte": "^3.44.3", "@sveltejs/kit": "^1.0.0-next.231",
"@types/file-saver": "^2.0.5",
"adm-zip": "^0.5.9",
"dotenv": "^16.0.0",
"svelte": "^3.46.2",
"svelte-check": "^2.4.5",
"svelte-intl-precompile": "^0.8.0",
"svelte-preprocess": "^4.10.1", "svelte-preprocess": "^4.10.1",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"typescript": "^4.5.4", "typescript": "^4.5.4",
"vite": "^2.7.10" "vite": "^2.7.12"
}, },
"dependencies": { "dependencies": {
"@fontsource/fira-mono": "^4.5.0", "@fontsource/fira-mono": "^4.5.0",

935
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
import dotenv from 'dotenv'
import { LokaliseApi } from '@lokalise/node-api'
import https from 'https'
import AdmZip from 'adm-zip'
dotenv.config()
const apiKey = process.env.LOKALISE_API_KEY
const project_id = process.env.LOKALISE_PROJECT
if (!apiKey) throw new Error('No API Key set for Lokalize! Set with "LOKALISE_API_KEY"')
if (!project_id) throw new Error('No project id set for Lokalize! Set with "LOKALISE_PROJECT"')
const client = new LokaliseApi({ apiKey })
const WGet = (url) =>
new Promise((done) => {
https
.get(url, (res) => {
const data = []
res
.on('data', (chunk) => {
data.push(chunk)
})
.on('end', () => {
let buffer = Buffer.concat(data)
done(buffer)
})
})
.on('error', (err) => {
console.log('download error:', err)
})
})
async function download() {
// For details see: https://app.lokalise.com/api2docs/curl/#transition-download-files-post
const download = await client.files().download(project_id, {
format: 'json',
indentation: 'tab',
json_unescaped_slashes: true,
original_filenames: false,
bundle_structure: '%LANG_ISO%.%FORMAT%',
export_sort: 'first_added',
export_empty_as: 'skip',
add_newline_eof: true,
replace_breaks: false,
})
const buffered = await WGet(download.bundle_url)
const zip = new AdmZip(buffered)
zip.extractAllTo('./locales', true)
}
download().catch((e) => {
console.error(e)
process.exit(1)
})

View File

@@ -7,7 +7,7 @@ export class Files {
}) })
} }
static fromString(s: string): Promise<Blob> { static async fromString(s: string): Promise<Blob> {
return fetch(s).then((r) => r.blob()) return fetch(s).then((r) => r.blob())
} }
} }

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Contrast</title><path
d="M256 32C132.29 32 32 132.29 32 256s100.29 224 224 224 224-100.29 224-224S379.71 32 256 32zM128.72 383.28A180 180 0 01256 76v360a178.82 178.82 0 01-127.28-52.72z"
/></svg
>

After

Width:  |  Height:  |  Size: 287 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Copy</title><path
d="M456 480H136a24 24 0 01-24-24V128a16 16 0 0116-16h328a24 24 0 0124 24v320a24 24 0 01-24 24z"
/><path
d="M112 80h288V56a24 24 0 00-24-24H60a28 28 0 00-28 28v316a24 24 0 0024 24h24V112a32 32 0 0132-32z"
/></svg
>

After

Width:  |  Height:  |  Size: 325 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Dice</title><path
d="M48 366.92L240 480V284L48 170zM192 288c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zm-96 32c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zM272 284v196l192-113.08V170zm48 140c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm96 32c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm32 77.64zM256 32L64 144l192 112 192-112zm0 120c-13.25 0-24-7.16-24-16s10.75-16 24-16 24 7.16 24 16-10.75 16-24 16z"
/></svg
>

After

Width:  |  Height:  |  Size: 736 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Eye</title><circle cx="256" cy="256" r="64" /><path
d="M394.82 141.18C351.1 111.2 304.31 96 255.76 96c-43.69 0-86.28 13-126.59 38.48C88.52 160.23 48.67 207 16 256c26.42 44 62.56 89.24 100.2 115.18C159.38 400.92 206.33 416 255.76 416c49 0 95.85-15.07 139.3-44.79C433.31 345 469.71 299.82 496 256c-26.38-43.43-62.9-88.56-101.18-114.82zM256 352a96 96 0 1196-96 96.11 96.11 0 01-96 96z"
/></svg
>

After

Width:  |  Height:  |  Size: 483 B

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"
><title>Eye Off</title><path
d="M63.998 86.004l21.998-21.998L448 426.01l-21.998 21.998zM259.34 192.09l60.57 60.57a64.07 64.07 0 00-60.57-60.57zM252.66 319.91l-60.57-60.57a64.07 64.07 0 0060.57 60.57z"
/><path
d="M256 352a96 96 0 01-92.6-121.34l-69.07-69.08C66.12 187.42 39.24 221.14 16 256c26.42 44 62.56 89.24 100.2 115.18C159.38 400.92 206.33 416 255.76 416A233.47 233.47 0 00335 402.2l-53.61-53.6A95.84 95.84 0 01256 352zM256 160a96 96 0 0192.6 121.34L419.26 352c29.15-26.25 56.07-61.56 76.74-96-26.38-43.43-62.9-88.56-101.18-114.82C351.1 111.2 304.31 96 255.76 96a222.92 222.92 0 00-78.21 14.29l53.11 53.11A95.84 95.84 0 01256 160z"
/></svg
>

After

Width:  |  Height:  |  Size: 732 B

View File

@@ -4,13 +4,16 @@ import { writable } from 'svelte/store'
export type Status = { export type Status = {
version: string version: string
max_size: number max_size: number
max_views: number
max_expiration: number
allow_advanced: boolean
} }
export const status = writable<null | Status>(null) export const status = writable<null | Status>(null)
export async function init() { export async function init() {
const data = await call({ const data = await call({
url: 'status', url: 'status/',
method: 'get', method: 'get',
}) })
status.set(data) status.set(data)

View File

@@ -2,6 +2,8 @@
import type { FileDTO } from '$lib/api' import type { FileDTO } from '$lib/api'
import { Files } from '$lib/files' import { Files } from '$lib/files'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { t } from 'svelte-intl-precompile'
import Button from './Button.svelte'
import MaxSize from './MaxSize.svelte' import MaxSize from './MaxSize.svelte'
export let label: string = '' export let label: string = ''
@@ -11,8 +13,8 @@
async function onInput(e: Event) { async function onInput(e: Event) {
const input = e.target as HTMLInputElement const input = e.target as HTMLInputElement
if (input.files.length) { if (input?.files?.length) {
files = Array.from(input.files) files = [...files, ...Array.from(input.files)]
const data: FileDTO[] = await Promise.all( const data: FileDTO[] = await Promise.all(
files.map(async (file) => ({ files.map(async (file) => ({
name: file.name, name: file.name,
@@ -21,15 +23,17 @@
contents: await Files.toString(file), contents: await Files.toString(file),
})) }))
) )
console.debug(
'files',
data.map((d) => d.contents.length)
)
dispatch('file', JSON.stringify(data)) dispatch('file', JSON.stringify(data))
} else { } else {
dispatch('file', '') dispatch('file', '')
} }
} }
function clear(e: Event) {
e.preventDefault()
files = []
dispatch('file', '')
}
</script> </script>
<label> <label>
@@ -40,18 +44,20 @@
<div class="box"> <div class="box">
{#if files.length} {#if files.length}
<div> <div>
<b>Selected Files</b> <b>{$t('file_upload.selected_files')}</b>
{#each files as file} {#each files as file}
<div class="file"> <div class="file">
{file.name} {file.name}
</div> </div>
{/each} {/each}
<div class="spacer" />
<Button on:click={clear}>Clear</Button>
</div> </div>
{:else} {:else}
<div> <div>
<b>No Files Selected</b> <b>{$t('file_upload.no_files_selected')}</b>
<br /> <br />
<small>max: <MaxSize /></small> <small>{$t('common.max')}: <MaxSize /></small>
</div> </div>
{/if} {/if}
</div> </div>
@@ -69,4 +75,8 @@
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
} }
.spacer {
margin-top: 1rem;
}
</style> </style>

View File

@@ -1,27 +1,30 @@
<script lang="ts"> <script lang="ts" context="module">
import { onMount } from 'svelte' import IconContrast from '$lib/icons/IconContrast.svelte'
export let icon: string = '' import IconCopy from '$lib/icons/IconCopy.svelte'
export let href: string = '' import IconDice from '$lib/icons/IconDice.svelte'
import IconEye from '$lib/icons/IconEye.svelte'
import IconEyeOff from '$lib/icons/IconEyeOff.svelte'
$: src = href || `/icons/${icon}.svg` const map = {
contrast: IconContrast,
let html = null copy: IconCopy,
dice: IconDice,
onMount(async () => { eye: IconEye,
html = await fetch(src).then((res) => res.text()) 'eye-off': IconEyeOff,
}) }
</script> </script>
{#if html === null} <script lang="ts">
<img on:click {...$$restProps} {src} alt={icon} /> export let icon: keyof typeof map
{:else} </script>
<div on:click {...$$restProps}>
{@html html} <div on:click {...$$restProps}>
</div> {#if map[icon]}
{/if} <svelte:component this={map[icon]} />
{/if}
</div>
<style> <style>
img,
div { div {
display: inline-block; display: inline-block;
contain: strict; contain: strict;

View File

@@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import { status } from '$lib/stores/status' import { status } from '$lib/stores/status'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import { _ } from 'svelte-intl-precompile'
</script> </script>
<span> <span>
{#if $status !== null} {#if $status !== null}
{prettyBytes($status.max_size, { binary: true })} {prettyBytes($status.max_size, { binary: true })}
{:else} {:else}
loading... {$_('common.loading')}
{/if} {/if}
</span> </span>

View File

@@ -4,6 +4,7 @@
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import prettyBytes from 'pretty-bytes' import prettyBytes from 'pretty-bytes'
import { t } from 'svelte-intl-precompile'
import Button from './Button.svelte' import Button from './Button.svelte'
export let note: NotePublic export let note: NotePublic
@@ -28,20 +29,20 @@
} }
</script> </script>
<p class="error-text">you will <b>not</b> get the chance to see the note again.</p> <p class="error-text">{@html $t('show.warning_will_not_see_again')}</p>
{#if note.meta.type === 'text'} {#if note.meta.type === 'text'}
<div class="note" data-testid="note-result"> <div class="note">
{note.contents} {note.contents}
</div> </div>
<Button on:click={() => copy(note.contents)}>copy to clipboard</Button> <Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
{:else} {:else}
{#each files as file} {#each files as file}
<div class="note file" data-testid="note-result"> <div class="note file">
<b on:click={() => downloadFile(file)}> {file.name}</b> <b on:click={() => downloadFile(file)}> {file.name}</b>
<small> {file.type} {prettyBytes(file.size)}</small> <small> {file.type} {prettyBytes(file.size)}</small>
</div> </div>
{/each} {/each}
<Button on:click={download}>download all</Button> <Button on:click={download}>{$t('show.download_all')}</Button>
{/if} {/if}
<style> <style>
@@ -59,6 +60,9 @@
.note b { .note b {
cursor: pointer; cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.note.file { .note.file {
@@ -66,4 +70,8 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.note.file small {
padding-left: 1rem;
}
</style> </style>

View File

@@ -49,7 +49,7 @@
height: 2rem; height: 2rem;
width: 1.25rem; width: 1.25rem;
left: 0.125rem; left: 0.125rem;
bottom: 0.1rem; bottom: 0.125rem;
background-color: var(--ui-bg-1); background-color: var(--ui-bg-1);
-webkit-transition: 0.4s; -webkit-transition: 0.4s;
transition: var(--ui-anim); transition: var(--ui-anim);

View File

@@ -1,19 +1,23 @@
<script lang="ts"> <script lang="ts">
import { getRandomBytes, Hex } from '$lib/crypto' import { getRandomBytes, Hex } from '$lib/crypto'
import copyToClipboard from 'copy-to-clipboard' import copyToClipboard from 'copy-to-clipboard'
import { t } from 'svelte-intl-precompile'
import { fade } from 'svelte/transition'
import Icon from './Icon.svelte' import Icon from './Icon.svelte'
export let label: string = '' export let label: string = ''
export let value export let value: any
export let validate: (value: any) => boolean | string = () => true
export let copy: boolean = false export let copy: boolean = false
export let random: boolean = false export let random: boolean = false
const initialType = $$restProps.type const initialType = $$restProps.type
const isPassword = initialType === 'password' const isPassword = initialType === 'password'
let hidden = true let hidden = true
let notification: string | null = null
let notificationTimeout: NodeJS.Timeout | null = null
$: valid = validate(value)
$: if (isPassword) { $: if (isPassword) {
value value
@@ -24,29 +28,46 @@
hidden = !hidden hidden = !hidden
} }
function copyFN() { function copyFN() {
copyToClipboard(value) copyToClipboard(value.toString())
notify($t('home.copied_to_clipboard'))
} }
function randomFN() { function randomFN() {
value = Hex.encode(getRandomBytes(20)) value = Hex.encode(getRandomBytes(20))
} }
function notify(msg: string, delay: number = 2000) {
if (notificationTimeout) {
clearTimeout(notificationTimeout)
}
notificationTimeout = setTimeout(() => {
notification = null
}, delay)
notification = msg
}
</script> </script>
<label> <label>
<small disabled={$$restProps.disabled}> <small disabled={$$restProps.disabled}>
{label} {label}
{#if valid !== true}
<span class="error-text">{valid}</span>
{/if}
</small> </small>
<input bind:value {...$$restProps} /> <input bind:value {...$$restProps} class:valid={valid === true} />
<div class="icons"> <div class="icons">
{#if isPassword} {#if isPassword}
<Icon class="icon" icon={hidden ? 'eye-sharp' : 'eye-off-sharp'} on:click={toggle} /> <Icon class="icon" icon={hidden ? 'eye' : 'eye-off'} on:click={toggle} />
{/if} {/if}
{#if random} {#if random}
<Icon class="icon" icon="dice-sharp" on:click={randomFN} /> <Icon class="icon" icon="dice" on:click={randomFN} />
{/if} {/if}
{#if copy} {#if copy}
<Icon class="icon" icon="copy-sharp" on:click={copyFN} /> <Icon class="icon" icon="copy" on:click={copyFN} />
{/if} {/if}
</div> </div>
{#if notification}
<div class="notification" transition:fade><small>{notification}</small></div>
{/if}
</label> </label>
<style> <style>
@@ -55,6 +76,10 @@
display: block; display: block;
} }
label > small {
display: block;
}
input { input {
width: 100%; width: 100%;
margin: 0; margin: 0;
@@ -68,6 +93,10 @@
border-color: var(--ui-clr-primary); border-color: var(--ui-clr-primary);
} }
input:not(.valid) {
border-color: var(--ui-clr-error);
}
.icons { .icons {
border: 1px red; border: 1px red;
position: absolute; position: absolute;
@@ -88,4 +117,11 @@
.icons > :global(.icon:hover) { .icons > :global(.icon:hover) {
border-color: var(--ui-clr-primary); border-color: var(--ui-clr-primary);
} }
.notification {
text-align: right;
position: absolute;
right: 0;
bottom: -1.5em;
}
</style> </style>

View File

@@ -41,7 +41,7 @@
</script> </script>
<div on:click={change}> <div on:click={change}>
<Icon class="icon" icon="contrast-sharp" /> <Icon class="icon" icon="contrast" />
{$theme} {$theme}
</div> </div>

View File

@@ -1,12 +1,14 @@
<script lang="ts"> <script lang="ts">
import { create, Note, PayloadToLargeError } from '$lib/api' import { create, Note, PayloadToLargeError } from '$lib/api'
import { encrypt, getKeyFromString, getRandomBytes, Hex } from '$lib/crypto' import { encrypt, getKeyFromString, getRandomBytes, Hex } from '$lib/crypto'
import { status } from '$lib/stores/status'
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import FileUpload from '$lib/ui/FileUpload.svelte' import FileUpload from '$lib/ui/FileUpload.svelte'
import MaxSize from '$lib/ui/MaxSize.svelte' import MaxSize from '$lib/ui/MaxSize.svelte'
import Switch from '$lib/ui/Switch.svelte' import Switch from '$lib/ui/Switch.svelte'
import TextArea from '$lib/ui/TextArea.svelte' import TextArea from '$lib/ui/TextArea.svelte'
import TextInput from '$lib/ui/TextInput.svelte' import TextInput from '$lib/ui/TextInput.svelte'
import { t } from 'svelte-intl-precompile'
import { blur } from 'svelte/transition' import { blur } from 'svelte/transition'
let note: Note = { let note: Note = {
@@ -18,20 +20,24 @@
let result: { password: string; id: string } | null = null let result: { password: string; id: string } | null = null
let advanced = false let advanced = false
let file = false let file = false
let type = false let timeExpiration = false
let message = '' let message = ''
let loading = false let loading = false
let error: string | null = null let error: string | null = null
$: if (!advanced) { $: if (!advanced) {
note.views = 1 note.views = 1
type = false timeExpiration = false
} }
$: { $: {
let fraction: string message = $t('home.explanation', {
fraction = type ? `${note.expiration} minutes` : `${note.views} views` values: {
message = 'the note will expire and be destroyed after ' + fraction type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
values: { n: (timeExpiration ? note.expiration : note.views) ?? '?' },
}),
},
})
} }
$: note.meta.type = file ? 'file' : 'text' $: note.meta.type = file ? 'file' : 'text'
@@ -40,20 +46,21 @@
note.contents = '' note.contents = ''
} }
class EmptyContentError extends Error {}
async function submit() { async function submit() {
try { try {
error = null error = null
loading = true loading = true
const password = Hex.encode(getRandomBytes(32)) const password = Hex.encode(getRandomBytes(32))
const key = await getKeyFromString(password) const key = await getKeyFromString(password)
if (note.contents === '') throw new EmptyContentError()
const data: Note = { const data: Note = {
contents: await encrypt(note.contents, key), contents: await encrypt(note.contents, key),
meta: note.meta, meta: note.meta,
} }
// @ts-ignore if (timeExpiration) data.expiration = parseInt(note.expiration as any)
if (type) data.expiration = parseInt(note.expiration) else data.views = parseInt(note.views as any)
// @ts-ignore
else data.views = parseInt(note.views)
const response = await create(data) const response = await create(data)
result = { result = {
@@ -62,9 +69,11 @@
} }
} catch (e) { } catch (e) {
if (e instanceof PayloadToLargeError) { if (e instanceof PayloadToLargeError) {
error = 'could not create not. note is to big' error = $t('home.errors.note_to_big')
} else if (e instanceof EmptyContentError) {
error = $t('home.errors.empty_content')
} else { } else {
error = 'could not create note. please try again.' error = $t('home.errors.note_error')
} }
} finally { } finally {
loading = false loading = false
@@ -80,48 +89,38 @@
<TextInput <TextInput
type="text" type="text"
readonly readonly
label="share link" label={$t('common.share_link')}
value="{window.location.origin}/note/{result.id}#{result.password}" value="{window.location.origin}/note/{result.id}#{result.password}"
copy copy
data-testid="note-share-link"
/> />
<br /> <br />
<p> <p>
<b>availability:</b> {@html $t('home.new_note_notice')}
<br />
the note is not guaranteed to be stored as everything is kept in ram, if it fills up the oldest notes
will be removed.
<br />
(you probably will be fine, just be warned.)
</p> </p>
<br /> <br />
<Button on:click={reset}>new note</Button> <Button on:click={reset}>{$t('home.new_note')}</Button>
{:else} {:else}
<p> <p>
Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and {@html $t('home.intro')}
share the link.
</p> </p>
<form on:submit|preventDefault={submit}> <form on:submit|preventDefault={submit}>
<fieldset disabled={loading}> <fieldset disabled={loading}>
{#if file} {#if file}
<FileUpload label="file" on:file={(f) => (note.contents = f.detail)} /> <FileUpload label={$t('common.file')} on:file={(f) => (note.contents = f.detail)} />
{:else} {:else}
<TextArea <TextArea label={$t('common.note')} bind:value={note.contents} placeholder="..." />
label="note"
bind:value={note.contents}
placeholder="..."
data-testid="input-note"
/>
{/if} {/if}
<div class="bottom"> <div class="bottom">
<Switch class="file" label="file" bind:value={file} /> <Switch class="file" label={$t('common.file')} bind:value={file} />
<Switch label="advanced" bind:value={advanced} /> {#if $status?.allow_advanced}
<Switch label={$t('common.advanced')} bind:value={advanced} />
{/if}
<div class="grow" /> <div class="grow" />
<div class="tr"> <div class="tr">
<small>max: <MaxSize /> </small> <small>{$t('common.max')}: <MaxSize /> </small>
<br /> <br />
<Button type="submit" data-testid="button-create">create</Button> <Button type="submit">{$t('common.create')}</Button>
</div> </div>
</div> </div>
@@ -132,7 +131,7 @@
<p> <p>
<br /> <br />
{#if loading} {#if loading}
loading... {$t('common.loading')}
{:else} {:else}
{message} {message}
{/if} {/if}
@@ -144,20 +143,26 @@
<div class="fields"> <div class="fields">
<TextInput <TextInput
type="number" type="number"
label="views" label={$t('common.views', { values: { n: 0 } })}
bind:value={note.views} bind:value={note.views}
disabled={type} disabled={timeExpiration}
max={100} max={$status?.max_views}
validate={(v) =>
($status && v < $status?.max_views) ||
$t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })}
/> />
<div class="middle-switch"> <div class="middle-switch">
<Switch label="mode" bind:value={type} color={false} /> <Switch label={$t('common.mode')} bind:value={timeExpiration} color={false} />
</div> </div>
<TextInput <TextInput
type="number" type="number"
label="minutes" label={$t('common.minutes', { values: { n: 0 } })}
bind:value={note.expiration} bind:value={note.expiration}
disabled={!type} disabled={!timeExpiration}
max={360} max={$status?.max_expiration}
validate={(v) =>
($status && v < $status?.max_expiration) ||
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
/> />
</div> </div>
</div> </div>

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/ui/Icon.svelte'
import ThemeToggle from '$lib/ui/ThemeToggle.svelte' import ThemeToggle from '$lib/ui/ThemeToggle.svelte'
</script> </script>

View File

@@ -83,7 +83,7 @@
header { header {
text-align: center; text-align: center;
margin-top: 4rem; margin-top: 3rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }

View File

@@ -1,12 +1,20 @@
<script lang="ts" context="module">
import { init, waitLocale, getLocaleFromNavigator } from 'svelte-intl-precompile'
// @ts-ignore
import { registerAll } from '$locales'
registerAll()
init({ initialLocale: getLocaleFromNavigator() ?? undefined, fallbackLocale: 'en' })
</script>
<script lang="ts"> <script lang="ts">
import { init } from '$lib/stores/status' import { init as initStores } from '$lib/stores/status'
import Footer from '$lib/views/Footer.svelte' import Footer from '$lib/views/Footer.svelte'
import Header from '$lib/views/Header.svelte' import Header from '$lib/views/Header.svelte'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import '../app.css' import '../app.css'
onMount(() => { onMount(() => {
init() initStores()
}) })
</script> </script>
@@ -14,12 +22,14 @@
<title>cryptgeon</title> <title>cryptgeon</title>
</svelte:head> </svelte:head>
<main> {#await waitLocale() then _}
<Header /> <main>
<slot /> <Header />
</main> <slot />
</main>
<Footer /> <Footer />
{/await}
<style> <style>
main { main {

View File

@@ -50,6 +50,14 @@
</span> </span>
</AboutParagraph> </AboutParagraph>
<AboutParagraph title="translations">
<span
>translations are managed on <a href="https://lokalise.com/" target="_blank">Lokalise</a>,
which granted an open source license to use the paid version. If you are interested in helping
translating don't hesitate to contact me!
</span>
</AboutParagraph>
<AboutParagraph title="attribution"> <AboutParagraph title="attribution">
<span> <span>
icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from icons made by <a href="https://www.freepik.com" title="Freepik">freepik</a> from

View File

@@ -1,5 +1,7 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
export async function load({ params }) { import type { Load } from '@sveltejs/kit'
export const load: Load = async ({ params }) => {
return { return {
props: params, props: params,
} }
@@ -7,12 +9,14 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'
import { t } from 'svelte-intl-precompile'
import type { NotePublic } from '$lib/api' import type { NotePublic } from '$lib/api'
import { get, info } from '$lib/api' import { get, info } from '$lib/api'
import { decrypt, getKeyFromString } from '$lib/crypto' import { decrypt, getKeyFromString } from '$lib/crypto'
import Button from '$lib/ui/Button.svelte' import Button from '$lib/ui/Button.svelte'
import ShowNote from '$lib/ui/ShowNote.svelte' import ShowNote from '$lib/ui/ShowNote.svelte'
import { onMount } from 'svelte'
export let id: string export let id: string
@@ -26,7 +30,7 @@
onMount(async () => { onMount(async () => {
try { try {
loading = true loading = true
error = null error = false
password = window.location.hash.slice(1) password = window.location.hash.slice(1)
await info(id) await info(id)
exists = true exists = true
@@ -55,20 +59,18 @@
{#if !loading} {#if !loading}
{#if !exists} {#if !exists}
<p class="error-text" data-testid="note-not-found"> <p class="error-text">{$t('show.errors.not_found')}</p>
note was not found or was already deleted.
</p>
{:else if note && !error} {:else if note && !error}
<ShowNote {note} /> <ShowNote {note} />
{:else} {:else}
<form on:submit|preventDefault={show}> <form on:submit|preventDefault={show}>
<fieldset> <fieldset>
<p>click below to show and delete the note if the counter has reached it's limit</p> <p>{$t('show.explanation')}</p>
<Button type="submit" data-testid="button-show">show note</Button> <Button type="submit">{$t('show.show_note')}</Button>
{#if error} {#if error}
<br /> <br />
<p class="error-text"> <p class="error-text">
wrong password. could not decipher. probably a broken link. note was destroyed. {$t('show.errors.decryption_failed')}
<br /> <br />
</p> </p>
{/if} {/if}
@@ -77,5 +79,5 @@
{/if} {/if}
{/if} {/if}
{#if loading} {#if loading}
<p>loading...</p> <p>{$t('common.loading')}</p>
{/if} {/if}

View File

@@ -1 +0,0 @@
<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Contrast</title><path d='M256 32C132.29 32 32 132.29 32 256s100.29 224 224 224 224-100.29 224-224S379.71 32 256 32zM128.72 383.28A180 180 0 01256 76v360a178.82 178.82 0 01-127.28-52.72z'/></svg>

Before

Width:  |  Height:  |  Size: 279 B

View File

@@ -1 +0,0 @@
<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Copy</title><path d='M456 480H136a24 24 0 01-24-24V128a16 16 0 0116-16h328a24 24 0 0124 24v320a24 24 0 01-24 24z'/><path d='M112 80h288V56a24 24 0 00-24-24H60a28 28 0 00-28 28v316a24 24 0 0024 24h24V112a32 32 0 0132-32z'/></svg>

Before

Width:  |  Height:  |  Size: 313 B

View File

@@ -1 +0,0 @@
<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Dice</title><path d='M48 366.92L240 480V284L48 170zM192 288c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zm-96 32c8.84 0 16 10.75 16 24s-7.16 24-16 24-16-10.75-16-24 7.16-24 16-24zM272 284v196l192-113.08V170zm48 140c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm96 32c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm0-88c-8.84 0-16-10.75-16-24s7.16-24 16-24 16 10.75 16 24-7.16 24-16 24zm32 77.64zM256 32L64 144l192 112 192-112zm0 120c-13.25 0-24-7.16-24-16s10.75-16 24-16 24 7.16 24 16-10.75 16-24 16z'/></svg>

Before

Width:  |  Height:  |  Size: 728 B

View File

@@ -1 +0,0 @@
<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Eye Off</title><path d='M63.998 86.004l21.998-21.998L448 426.01l-21.998 21.998zM259.34 192.09l60.57 60.57a64.07 64.07 0 00-60.57-60.57zM252.66 319.91l-60.57-60.57a64.07 64.07 0 0060.57 60.57z'/><path d='M256 352a96 96 0 01-92.6-121.34l-69.07-69.08C66.12 187.42 39.24 221.14 16 256c26.42 44 62.56 89.24 100.2 115.18C159.38 400.92 206.33 416 255.76 416A233.47 233.47 0 00335 402.2l-53.61-53.6A95.84 95.84 0 01256 352zM256 160a96 96 0 0192.6 121.34L419.26 352c29.15-26.25 56.07-61.56 76.74-96-26.38-43.43-62.9-88.56-101.18-114.82C351.1 111.2 304.31 96 255.76 96a222.92 222.92 0 00-78.21 14.29l53.11 53.11A95.84 95.84 0 01256 160z'/></svg>

Before

Width:  |  Height:  |  Size: 720 B

View File

@@ -1 +0,0 @@
<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Eye</title><circle cx='256' cy='256' r='64'/><path d='M394.82 141.18C351.1 111.2 304.31 96 255.76 96c-43.69 0-86.28 13-126.59 38.48C88.52 160.23 48.67 207 16 256c26.42 44 62.56 89.24 100.2 115.18C159.38 400.92 206.33 416 255.76 416c49 0 95.85-15.07 139.3-44.79C433.31 345 469.71 299.82 496 256c-26.38-43.43-62.9-88.56-101.18-114.82zM256 352a96 96 0 1196-96 96.11 96.11 0 01-96 96z'/></svg>

Before

Width:  |  Height:  |  Size: 474 B

View File

@@ -1 +0,0 @@
<svg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'><title>Lock Closed</title><path d='M420 192h-68v-80a96 96 0 10-192 0v80H92a12 12 0 00-12 12v280a12 12 0 0012 12h328a12 12 0 0012-12V204a12 12 0 00-12-12zm-106 0H198v-80.75a58 58 0 11116 0z'/></svg>

Before

Width:  |  Height:  |  Size: 275 B

View File

@@ -1,3 +1,4 @@
# https://www.robotstxt.org/robotstxt.html # https://www.robotstxt.org/robotstxt.html
User-agent: * User-agent: *
Disallow: Allow: /$
Disallow: /

View File

@@ -1,5 +1,6 @@
import preprocess from 'svelte-preprocess' import preprocess from 'svelte-preprocess'
import adapter from '@sveltejs/adapter-static' import adapter from '@sveltejs/adapter-static'
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
export default { export default {
preprocess: preprocess(), preprocess: preprocess(),
@@ -9,5 +10,10 @@ export default {
fallback: 'index.html', fallback: 'index.html',
}), }),
target: '#svelte', target: '#svelte',
vite: {
plugins: [
precompileIntl('locales'), // if your translations are defined in /locales/[lang].json
],
},
}, },
} }

View File

@@ -24,7 +24,8 @@
"checkJs": true, "checkJs": true,
"paths": { "paths": {
"$lib/*": ["src/lib/*"] "$lib/*": ["src/lib/*"]
} },
"strict": true
}, },
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"] "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
} }

192
pnpm-lock.yaml generated
View File

@@ -82,21 +82,25 @@ packages:
is-arrayish: 0.2.1 is-arrayish: 0.2.1
dev: true dev: true
/es-abstract/1.18.0: /es-abstract/1.19.1:
resolution: {integrity: sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==} resolution: {integrity: sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies: dependencies:
call-bind: 1.0.2 call-bind: 1.0.2
es-to-primitive: 1.2.1 es-to-primitive: 1.2.1
function-bind: 1.1.1 function-bind: 1.1.1
get-intrinsic: 1.1.1 get-intrinsic: 1.1.1
get-symbol-description: 1.0.0
has: 1.0.3 has: 1.0.3
has-symbols: 1.0.2 has-symbols: 1.0.2
is-callable: 1.2.3 internal-slot: 1.0.3
is-negative-zero: 2.0.1 is-callable: 1.2.4
is-regex: 1.1.2 is-negative-zero: 2.0.2
is-string: 1.0.5 is-regex: 1.1.4
object-inspect: 1.10.2 is-shared-array-buffer: 1.0.1
is-string: 1.0.7
is-weakref: 1.0.2
object-inspect: 1.12.0
object-keys: 1.1.1 object-keys: 1.1.1
object.assign: 4.1.2 object.assign: 4.1.2
string.prototype.trimend: 1.0.4 string.prototype.trimend: 1.0.4
@@ -108,9 +112,9 @@ packages:
resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies: dependencies:
is-callable: 1.2.3 is-callable: 1.2.4
is-date-object: 1.0.2 is-date-object: 1.0.5
is-symbol: 1.0.3 is-symbol: 1.0.4
dev: true dev: true
/escape-string-regexp/1.0.5: /escape-string-regexp/1.0.5:
@@ -122,8 +126,8 @@ packages:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
dev: true dev: true
/follow-redirects/1.14.6: /follow-redirects/1.14.7:
resolution: {integrity: sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==} resolution: {integrity: sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
peerDependencies: peerDependencies:
debug: '*' debug: '*'
@@ -144,8 +148,16 @@ packages:
has-symbols: 1.0.2 has-symbols: 1.0.2
dev: true dev: true
/graceful-fs/4.2.6: /get-symbol-description/1.0.0:
resolution: {integrity: sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==} resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
get-intrinsic: 1.1.1
dev: true
/graceful-fs/4.2.9:
resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==}
dev: true dev: true
/has-bigints/1.0.1: /has-bigints/1.0.1:
@@ -162,6 +174,13 @@ packages:
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dev: true dev: true
/has-tostringtag/1.0.0:
resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==}
engines: {node: '>= 0.4'}
dependencies:
has-symbols: 1.0.2
dev: true
/has/1.0.3: /has/1.0.3:
resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4.0'}
@@ -178,73 +197,101 @@ packages:
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
dependencies: dependencies:
eventemitter3: 4.0.7 eventemitter3: 4.0.7
follow-redirects: 1.14.6 follow-redirects: 1.14.7
requires-port: 1.0.0 requires-port: 1.0.0
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
dev: true dev: true
/internal-slot/1.0.3:
resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==}
engines: {node: '>= 0.4'}
dependencies:
get-intrinsic: 1.1.1
has: 1.0.3
side-channel: 1.0.4
dev: true
/is-arrayish/0.2.1: /is-arrayish/0.2.1:
resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=} resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=}
dev: true dev: true
/is-bigint/1.0.1: /is-bigint/1.0.4:
resolution: {integrity: sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==} resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
dependencies:
has-bigints: 1.0.1
dev: true dev: true
/is-boolean-object/1.1.0: /is-boolean-object/1.1.2:
resolution: {integrity: sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==} resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies: dependencies:
call-bind: 1.0.2 call-bind: 1.0.2
has-tostringtag: 1.0.0
dev: true dev: true
/is-callable/1.2.3: /is-callable/1.2.4:
resolution: {integrity: sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==} resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dev: true dev: true
/is-core-module/2.3.0: /is-core-module/2.8.1:
resolution: {integrity: sha512-xSphU2KG9867tsYdLD4RWQ1VqdFl4HTO9Thf3I/3dLEfr0dbPTWKsuCKrgqMljg4nPE+Gq0VCnzT3gr0CyBmsw==} resolution: {integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==}
dependencies: dependencies:
has: 1.0.3 has: 1.0.3
dev: true dev: true
/is-date-object/1.0.2: /is-date-object/1.0.5:
resolution: {integrity: sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==} resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==}
engines: {node: '>= 0.4'}
dependencies:
has-tostringtag: 1.0.0
dev: true
/is-negative-zero/2.0.2:
resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dev: true dev: true
/is-negative-zero/2.0.1: /is-number-object/1.0.6:
resolution: {integrity: sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==} resolution: {integrity: sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies:
has-tostringtag: 1.0.0
dev: true dev: true
/is-number-object/1.0.4: /is-regex/1.1.4:
resolution: {integrity: sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==} resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==}
engines: {node: '>= 0.4'}
dev: true
/is-regex/1.1.2:
resolution: {integrity: sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies: dependencies:
call-bind: 1.0.2 call-bind: 1.0.2
has-symbols: 1.0.2 has-tostringtag: 1.0.0
dev: true dev: true
/is-string/1.0.5: /is-shared-array-buffer/1.0.1:
resolution: {integrity: sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==} resolution: {integrity: sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==}
dev: true
/is-string/1.0.7:
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies:
has-tostringtag: 1.0.0
dev: true dev: true
/is-symbol/1.0.3: /is-symbol/1.0.4:
resolution: {integrity: sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==} resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies: dependencies:
has-symbols: 1.0.2 has-symbols: 1.0.2
dev: true dev: true
/is-weakref/1.0.2:
resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
dependencies:
call-bind: 1.0.2
dev: true
/isexe/2.0.0: /isexe/2.0.0:
resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=}
dev: true dev: true
@@ -257,7 +304,7 @@ packages:
resolution: {integrity: sha1-L19Fq5HjMhYjT9U62rZo607AmTs=} resolution: {integrity: sha1-L19Fq5HjMhYjT9U62rZo607AmTs=}
engines: {node: '>=4'} engines: {node: '>=4'}
dependencies: dependencies:
graceful-fs: 4.2.6 graceful-fs: 4.2.9
parse-json: 4.0.0 parse-json: 4.0.0
pify: 3.0.0 pify: 3.0.0
strip-bom: 3.0.0 strip-bom: 3.0.0
@@ -282,7 +329,7 @@ packages:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
dependencies: dependencies:
hosted-git-info: 2.8.9 hosted-git-info: 2.8.9
resolve: 1.20.0 resolve: 1.21.0
semver: 5.7.1 semver: 5.7.1
validate-npm-package-license: 3.0.4 validate-npm-package-license: 3.0.4
dev: true dev: true
@@ -299,12 +346,12 @@ packages:
minimatch: 3.0.4 minimatch: 3.0.4
pidtree: 0.3.1 pidtree: 0.3.1
read-pkg: 3.0.0 read-pkg: 3.0.0
shell-quote: 1.7.2 shell-quote: 1.7.3
string.prototype.padend: 3.1.2 string.prototype.padend: 3.1.3
dev: true dev: true
/object-inspect/1.10.2: /object-inspect/1.12.0:
resolution: {integrity: sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==} resolution: {integrity: sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==}
dev: true dev: true
/object-keys/1.1.1: /object-keys/1.1.1:
@@ -335,8 +382,8 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/path-parse/1.0.6: /path-parse/1.0.7:
resolution: {integrity: sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==} resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
dev: true dev: true
/path-type/3.0.0: /path-type/3.0.0:
@@ -370,11 +417,13 @@ packages:
resolution: {integrity: sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=} resolution: {integrity: sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=}
dev: true dev: true
/resolve/1.20.0: /resolve/1.21.0:
resolution: {integrity: sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==} resolution: {integrity: sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==}
hasBin: true
dependencies: dependencies:
is-core-module: 2.3.0 is-core-module: 2.8.1
path-parse: 1.0.6 path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
dev: true dev: true
/semver/5.7.1: /semver/5.7.1:
@@ -394,15 +443,23 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/shell-quote/1.7.2: /shell-quote/1.7.3:
resolution: {integrity: sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==} resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==}
dev: true
/side-channel/1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
call-bind: 1.0.2
get-intrinsic: 1.1.1
object-inspect: 1.12.0
dev: true dev: true
/spdx-correct/3.1.1: /spdx-correct/3.1.1:
resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==} resolution: {integrity: sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==}
dependencies: dependencies:
spdx-expression-parse: 3.0.1 spdx-expression-parse: 3.0.1
spdx-license-ids: 3.0.7 spdx-license-ids: 3.0.11
dev: true dev: true
/spdx-exceptions/2.3.0: /spdx-exceptions/2.3.0:
@@ -413,20 +470,20 @@ packages:
resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==}
dependencies: dependencies:
spdx-exceptions: 2.3.0 spdx-exceptions: 2.3.0
spdx-license-ids: 3.0.7 spdx-license-ids: 3.0.11
dev: true dev: true
/spdx-license-ids/3.0.7: /spdx-license-ids/3.0.11:
resolution: {integrity: sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==} resolution: {integrity: sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==}
dev: true dev: true
/string.prototype.padend/3.1.2: /string.prototype.padend/3.1.3:
resolution: {integrity: sha512-/AQFLdYvePENU3W5rgurfWSMU6n+Ww8n/3cUt7E+vPBB/D7YDG8x+qjoFs4M/alR2bW7Qg6xMjVwWUOvuQ0XpQ==} resolution: {integrity: sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dependencies: dependencies:
call-bind: 1.0.2 call-bind: 1.0.2
define-properties: 1.1.3 define-properties: 1.1.3
es-abstract: 1.18.0 es-abstract: 1.19.1
dev: true dev: true
/string.prototype.trimend/1.0.4: /string.prototype.trimend/1.0.4:
@@ -455,6 +512,11 @@ packages:
has-flag: 3.0.0 has-flag: 3.0.0
dev: true dev: true
/supports-preserve-symlinks-flag/1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
dev: true
/unbox-primitive/1.0.1: /unbox-primitive/1.0.1:
resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==} resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==}
dependencies: dependencies:
@@ -474,11 +536,11 @@ packages:
/which-boxed-primitive/1.0.2: /which-boxed-primitive/1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
dependencies: dependencies:
is-bigint: 1.0.1 is-bigint: 1.0.4
is-boolean-object: 1.1.0 is-boolean-object: 1.1.2
is-number-object: 1.0.4 is-number-object: 1.0.6
is-string: 1.0.5 is-string: 1.0.7
is-symbol: 1.0.3 is-symbol: 1.0.4
dev: true dev: true
/which/1.3.1: /which/1.3.1: