Compare commits

..

No commits in common. "253fccaf7879f5dfa2ca34694513e851fec9096b" and "c73af508571dd71897f750a05669c765ac50ca2c" have entirely different histories.

19 changed files with 654 additions and 902 deletions

631
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -113,4 +113,5 @@ There are a few reasons to not use OmegaUpload:
- Cannot download files larger than 512 MiB through the web frontend—this - Cannot download files larger than 512 MiB through the web frontend—this
is a technical limitation of the current web frontend not using a web worker is a technical limitation of the current web frontend not using a web worker
in addition to the fact that browsers are not optimized for XChaCha20. in addition to the fact that browsers are not optimized for XChaCha20.
- Right now, you must upload via the CLI tool.
- The frontend uses WASM, which is a novel attack surface. - The frontend uses WASM, which is a novel attack surface.

View file

@ -22,6 +22,6 @@ CUR_DIR=$(pwd)
PROJECT_TOP_LEVEL=$(git rev-parse --show-toplevel) PROJECT_TOP_LEVEL=$(git rev-parse --show-toplevel)
cd "$PROJECT_TOP_LEVEL" || exit 1 cd "$PROJECT_TOP_LEVEL" || exit 1
git submodule update --remote git submodule foreach git pull
cd "$CUR_DIR" cd "$CUR_DIR"

View file

@ -9,9 +9,10 @@ license = "GPL-3.0-or-later"
# 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]
omegaupload-common = { path = "../common" } omegaupload-common = "0.2"
anyhow = "1.0.58"
atty = "0.2.14" anyhow = "1"
clap = { version = "3.2.15", features = ["derive"] } atty = "0.2"
reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls", "blocking"] } clap = { version = "3", features = ["derive"] }
rpassword = "7.0.0" reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "blocking"] }
rpassword = "5"

View file

@ -1,4 +1,4 @@
use crate::secrecy::{ExposeSecret, SecretString}; use omegaupload_common::secrecy::{ExposeSecret, SecretString};
pub struct Builder { pub struct Builder {
decryption_key: SecretString, decryption_key: SecretString,
@ -8,7 +8,6 @@ pub struct Builder {
} }
impl Builder { impl Builder {
#[must_use]
pub fn new(decryption_key: SecretString) -> Self { pub fn new(decryption_key: SecretString) -> Self {
Self { Self {
decryption_key, decryption_key,
@ -18,7 +17,6 @@ impl Builder {
} }
} }
#[must_use]
pub const fn needs_password(mut self) -> Self { pub const fn needs_password(mut self) -> Self {
self.needs_password = true; self.needs_password = true;
self self
@ -26,7 +24,6 @@ impl Builder {
// False positive // False positive
#[allow(clippy::missing_const_for_fn)] #[allow(clippy::missing_const_for_fn)]
#[must_use]
pub fn file_name(mut self, name: String) -> Self { pub fn file_name(mut self, name: String) -> Self {
self.file_name = Some(name); self.file_name = Some(name);
self self
@ -34,13 +31,11 @@ impl Builder {
// False positive // False positive
#[allow(clippy::missing_const_for_fn)] #[allow(clippy::missing_const_for_fn)]
#[must_use]
pub fn language(mut self, language: String) -> Self { pub fn language(mut self, language: String) -> Self {
self.language = Some(language); self.language = Some(language);
self self
} }
#[must_use]
pub fn build(self) -> SecretString { pub fn build(self) -> SecretString {
if !self.needs_password && self.file_name.is_none() && self.language.is_none() { if !self.needs_password && self.file_name.is_none() && self.language.is_none() {
return self.decryption_key; return self.decryption_key;

View file

@ -24,7 +24,6 @@ use anyhow::{anyhow, bail, Context, Result};
use atty::Stream; use atty::Stream;
use clap::Parser; use clap::Parser;
use omegaupload_common::crypto::{open_in_place, seal_in_place}; use omegaupload_common::crypto::{open_in_place, seal_in_place};
use omegaupload_common::fragment::Builder;
use omegaupload_common::secrecy::{ExposeSecret, SecretString, SecretVec}; use omegaupload_common::secrecy::{ExposeSecret, SecretString, SecretVec};
use omegaupload_common::{ use omegaupload_common::{
base64, Expiration, ParsedUrl, Url, API_ENDPOINT, EXPIRATION_HEADER_NAME, base64, Expiration, ParsedUrl, Url, API_ENDPOINT, EXPIRATION_HEADER_NAME,
@ -32,7 +31,11 @@ use omegaupload_common::{
use reqwest::blocking::Client; use reqwest::blocking::Client;
use reqwest::header::EXPIRES; use reqwest::header::EXPIRES;
use reqwest::StatusCode; use reqwest::StatusCode;
use rpassword::prompt_password; use rpassword::prompt_password_stderr;
use crate::fragment::Builder;
mod fragment;
#[derive(Parser)] #[derive(Parser)]
struct Opts { struct Opts {
@ -117,7 +120,8 @@ fn handle_upload(
} }
let password = if password { let password = if password {
let maybe_password = prompt_password("Please set the password for this paste: ")?; let maybe_password =
prompt_password_stderr("Please set the password for this paste: ")?;
Some(SecretVec::new(maybe_password.into_bytes())) Some(SecretVec::new(maybe_password.into_bytes()))
} else { } else {
None None
@ -196,7 +200,8 @@ fn handle_download(mut url: ParsedUrl) -> Result<()> {
let password = if url.needs_password { let password = if url.needs_password {
// Only print prompt on interactive, else it messes with output // Only print prompt on interactive, else it messes with output
let maybe_password = prompt_password("Please enter the password to access this paste: ")?; let maybe_password =
prompt_password_stderr("Please enter the password to access this paste: ")?;
Some(SecretVec::new(maybe_password.into_bytes())) Some(SecretVec::new(maybe_password.into_bytes()))
} else { } else {
None None

View file

@ -9,24 +9,24 @@ license = "MIT"
# 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]
base64 = "0.13.0" base64 = "0.13"
bytes = { version = "1.2.0", features = ["serde"] } bytes = { version = "1", features = ["serde"] }
chacha20poly1305 = { version = "0.9.1", features = ["stream", "std"] } chacha20poly1305 = { version = "0.9", features = ["stream", "std"] }
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
headers = "0.3.7" headers = "0.3"
lazy_static = "1.4.0" lazy_static = "1"
rand = "0.8.5" rand = "0.8"
secrecy = "0.8.0" secrecy = "0.8"
serde = { version = "1.0.140", features = ["derive"] } serde = { version = "1", features = ["derive"] }
thiserror = "1.0.31" thiserror = "1"
typenum = "1.15.0" typenum = "1"
url = "2.2.2" url = "2"
argon2 = "0.4.1" argon2 = "0.3.1"
# Wasm features # Wasm features
gloo-console = { version = "0.2.1", optional = true } gloo-console = { version = "0.2", optional = true }
reqwasm = { version = "0.5.0", optional = true } http = { version = "0.2", optional = true }
http = { version = "0.2.8", optional = true } web-sys = { version = "0.3", features = ["Headers"], optional = true }
[features] [features]
wasm = ["gloo-console", "reqwasm", "http"] wasm = ["gloo-console", "http", "web-sys"]

View file

@ -294,43 +294,3 @@ fn get_argon2() -> Argon2<'static> {
pub fn get_csrng() -> impl CryptoRng + Rng { pub fn get_csrng() -> impl CryptoRng + Rng {
rand::thread_rng() rand::thread_rng()
} }
#[cfg(test)]
mod test {
use super::open_in_place;
use super::seal_in_place;
use crate::crypto::SecretVec;
macro_rules! test_encryption {
($($name:ident, $content:expr, $password:expr),*) => {
$(
#[test]
fn $name() {
let mut m = $content;
let n: Vec<u8> = $content;
let key = seal_in_place(&mut m, $password).unwrap();
assert_ne!(m, n);
assert!(open_in_place(&mut m, &key, $password).is_ok());
assert_eq!(m, n);
}
)*
};
}
test_encryption!(empty, vec![], None);
test_encryption!(
empty_password,
vec![],
Some(SecretVec::from(b"password".to_vec()))
);
test_encryption!(
normal,
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
None
);
test_encryption!(
normal_password,
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
Some(SecretVec::from(b"password".to_vec()))
);
}

View file

@ -1,6 +1,4 @@
#![warn(clippy::nursery, clippy::pedantic)] #![warn(clippy::nursery, clippy::pedantic)]
// False positive: https://github.com/rust-lang/rust-clippy/issues/6902
#![allow(clippy::use_self)]
//! Contains common functions and structures used by multiple projects //! Contains common functions and structures used by multiple projects
@ -41,7 +39,6 @@ use crate::crypto::Key;
pub mod base64; pub mod base64;
pub mod crypto; pub mod crypto;
pub mod fragment;
pub const API_ENDPOINT: &str = "/api"; pub const API_ENDPOINT: &str = "/api";
@ -233,10 +230,10 @@ expiration_from_str! {
impl Display for Expiration { impl Display for Expiration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::BurnAfterReading | Self::BurnAfterReadingWithDeadline(_) => { Expiration::BurnAfterReading | Expiration::BurnAfterReadingWithDeadline(_) => {
write!(f, "This item has been burned. You now have the only copy.") write!(f, "This item has been burned. You now have the only copy.")
} }
Self::UnixTime(time) => write!( Expiration::UnixTime(time) => write!(
f, f,
"{}", "{}",
time.format("This item will expire on %A, %B %-d, %Y at %T %Z.") time.format("This item will expire on %A, %B %-d, %Y at %T %Z.")
@ -251,7 +248,7 @@ lazy_static! {
impl Header for Expiration { impl Header for Expiration {
fn name() -> &'static HeaderName { fn name() -> &'static HeaderName {
&EXPIRATION_HEADER_NAME &*EXPIRATION_HEADER_NAME
} }
fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error> fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
@ -285,8 +282,6 @@ impl From<&Expiration> for HeaderValue {
} }
impl From<Expiration> for HeaderValue { impl From<Expiration> for HeaderValue {
// False positive: https://github.com/rust-lang/rust-clippy/issues/9095
#[allow(clippy::needless_borrow)]
fn from(expiration: Expiration) -> Self { fn from(expiration: Expiration) -> Self {
(&expiration).into() (&expiration).into()
} }
@ -295,12 +290,14 @@ impl From<Expiration> for HeaderValue {
pub struct ParseHeaderValueError; pub struct ParseHeaderValueError;
#[cfg(feature = "wasm")] #[cfg(feature = "wasm")]
impl TryFrom<reqwasm::http::Headers> for Expiration { impl TryFrom<web_sys::Headers> for Expiration {
type Error = ParseHeaderValueError; type Error = ParseHeaderValueError;
fn try_from(headers: reqwasm::http::Headers) -> Result<Self, Self::Error> { fn try_from(headers: web_sys::Headers) -> Result<Self, Self::Error> {
headers headers
.get(http::header::EXPIRES.as_str()) .get(http::header::EXPIRES.as_str())
.ok()
.flatten()
.as_deref() .as_deref()
.and_then(|v| Self::try_from(v).ok()) .and_then(|v| Self::try_from(v).ok())
.ok_or(ParseHeaderValueError) .ok_or(ParseHeaderValueError)

View file

@ -17,8 +17,7 @@
"highlight.js": "^11.4.0", "highlight.js": "^11.4.0",
"highlightjs-line-numbers.js": "^2.8.0", "highlightjs-line-numbers.js": "^2.8.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2"
"source-map-loader": "^4.0.0"
}, },
"scripts": { "scripts": {
"build": "webpack --mode production", "build": "webpack --mode production",

View file

@ -7,24 +7,24 @@ edition = "2021"
[dependencies] [dependencies]
omegaupload-common = { path = "../common" } omegaupload-common = { path = "../common" }
anyhow = "1.0.58" anyhow = "1"
axum = { version = "0.5.14", features = ["http2", "headers"] } axum = { version = "0.4", features = ["http2", "headers"] }
bincode = "1.3.3" bincode = "1"
# We don't care about which version (We want to match with axum), we just need # We don't care about which version (We want to match with axum), we just need
# to enable the feature # to enable the feature
bytes = { version = "1.2.0", features = ["serde"] } bytes = { version = "*", features = ["serde"] }
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
futures = "0.3.21" futures = "0.3"
# We just need to pull in whatever axum is pulling in # We just need to pull in whatever axum is pulling in
headers = "0.3.7" headers = "*"
lazy_static = "1.4.0" lazy_static = "1"
# Disable `random()` and `thread_rng()` # Disable `random()` and `thread_rng()`
rand = { version = "0.8.5", default-features = false } rand = { version = "0.8", default_features = false }
rocksdb = { version = "0.18.0", default-features = false, features = ["zstd"] } rocksdb = { version = "0.18", default_features = false, features = ["zstd"] }
serde = { version = "1.0.140", features = ["derive"] } serde = { version = "1", features = ["derive"] }
signal-hook = "0.3.14" signal-hook = "0.3"
signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
tokio = { version = "1.20.1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.3.4", features = ["fs"] } tower-http = { version = "0.2", features = ["fs"] }
tracing = "0.1.35" tracing = { version = "0.1" }
tracing-subscriber = "0.3.15" tracing-subscriber = "0.3"

View file

@ -27,7 +27,7 @@ use axum::extract::{Extension, Path, TypedHeader};
use axum::http::header::EXPIRES; use axum::http::header::EXPIRES;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::routing::{get, get_service, post}; use axum::routing::{get, get_service, post};
use axum::Router; use axum::{AddExtensionLayer, Router};
use chrono::Utc; use chrono::Utc;
use futures::stream::StreamExt; use futures::stream::StreamExt;
use headers::HeaderMap; use headers::HeaderMap;
@ -103,7 +103,7 @@ async fn main() -> Result<()> {
&format!("{API_ENDPOINT}/:code"), &format!("{API_ENDPOINT}/:code"),
get(paste::<SHORT_CODE_SIZE>).delete(delete::<SHORT_CODE_SIZE>), get(paste::<SHORT_CODE_SIZE>).delete(delete::<SHORT_CODE_SIZE>),
) )
.layer(axum::Extension(db)) .layer(AddExtensionLayer::new(db))
.into_make_service() .into_make_service()
}) })
.await?; .await?;

View file

@ -9,27 +9,27 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
omegaupload-common = { path = "../common", features = ["wasm"] } omegaupload-common = { path = "../common", features = ["wasm"] }
# Enables wasm support # Enables wasm support
getrandom = { version = "0.2.7", features = ["js"] } getrandom = { version = "*", features = ["js"] }
anyhow = "1.0.58" anyhow = "1"
bytes = "1.2.0" bytes = "1"
byte-unit = "4.0.14" byte-unit = "4"
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1"
gloo-console = "0.2.1" gloo-console = "0.2"
http = "0.2.8" http = "0.2"
js-sys = "0.3.59" js-sys = "0.3"
mime_guess = "2.0.4" mime_guess = "2"
reqwasm = "0.5.0" reqwasm = "0.4"
tree_magic_mini = { version = "3.0.3", features = ["with-gpl-data"] } tree_magic_mini = { version = "3", features = ["with-gpl-data"] }
serde = { version = "1.0.140", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2.82", features = ["serde-serialize"] } wasm-bindgen = { version = "0.2", features = ["serde-serialize"]}
wasm-bindgen-futures = "0.4.32" wasm-bindgen-futures = "0.4"
zip = { version = "0.6.2", default-features = false, features = ["deflate"] } zip = { version = "0.5", default-features = false, features = ["deflate"] }
flate2 = "1.0.24" flate2 = "1.0.22"
tar = "0.4.38" tar = "0.4.38"
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3.59" version = "0.3"
features = [ features = [
"BlobPropertyBag", "BlobPropertyBag",
"TextDecoder", "TextDecoder",

View file

@ -25,12 +25,9 @@ use gloo_console::{error, log};
use http::uri::PathAndQuery; use http::uri::PathAndQuery;
use http::{StatusCode, Uri}; use http::{StatusCode, Uri};
use js_sys::{Array, JsString, Object, Uint8Array}; use js_sys::{Array, JsString, Object, Uint8Array};
use omegaupload_common::base64;
use omegaupload_common::crypto::seal_in_place;
use omegaupload_common::crypto::{Error as CryptoError, Key}; use omegaupload_common::crypto::{Error as CryptoError, Key};
use omegaupload_common::fragment::Builder; use omegaupload_common::secrecy::{Secret, SecretVec};
use omegaupload_common::secrecy::{ExposeSecret, Secret, SecretString, SecretVec}; use omegaupload_common::{Expiration, PartialParsedUrl};
use omegaupload_common::{Expiration, PartialParsedUrl, Url};
use reqwasm::http::Request; use reqwasm::http::Request;
use wasm_bindgen::prelude::{wasm_bindgen, Closure}; use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
@ -53,8 +50,6 @@ extern "C" {
pub fn load_from_db(mime_type: JsString, name: Option<JsString>, language: Option<JsString>); pub fn load_from_db(mime_type: JsString, name: Option<JsString>, language: Option<JsString>);
#[wasm_bindgen(js_name = renderMessage)] #[wasm_bindgen(js_name = renderMessage)]
pub fn render_message(message: JsString); pub fn render_message(message: JsString);
#[wasm_bindgen(js_name = createUploadUi)]
pub fn create_upload_ui();
} }
fn window() -> Window { fn window() -> Window {
@ -80,7 +75,7 @@ pub fn start() {
std::panic::set_hook(Box::new(console_error_panic_hook::hook)); std::panic::set_hook(Box::new(console_error_panic_hook::hook));
if location().pathname().unwrap() == "/" { if location().pathname().unwrap() == "/" {
create_upload_ui(); render_message("Go away".into());
return; return;
} }
@ -171,55 +166,6 @@ pub fn start() {
}); });
} }
#[wasm_bindgen]
#[allow(clippy::future_not_send)]
pub fn encrypt_string(data: String) {
spawn_local(async move {
if let Err(e) = do_encrypt(data.into_bytes()).await {
log!(format!("[rs] Error encrypting string: {}", e));
}
});
}
#[wasm_bindgen]
#[allow(clippy::future_not_send)]
pub fn encrypt_array_buffer(data: Vec<u8>) {
spawn_local(async move {
if let Err(e) = do_encrypt(data).await {
log!(format!("[rs] Error encrypting array buffer: {}", e));
}
});
}
#[allow(clippy::future_not_send)]
async fn do_encrypt(mut data: Vec<u8>) -> Result<()> {
let (data, key) = {
let enc_key = seal_in_place(&mut data, None)?;
let key = SecretString::new(base64::encode(&enc_key.expose_secret().as_ref()));
(data, key)
};
let s: String = location().to_string().into();
let mut url = Url::from_str(&s)?;
let fragment = Builder::new(key);
let js_data = Uint8Array::new_with_length(u32::try_from(data.len()).expect("Data too large"));
js_data.copy_from(&data);
let short_code = Request::post(url.as_ref())
.body(js_data)
.send()
.await?
.text()
.await?;
url.set_path(&short_code);
url.set_fragment(Some(fragment.build().expose_secret()));
location()
.set_href(url.as_ref())
.expect("Unable to navigate to encrypted upload");
Ok(())
}
#[allow(clippy::future_not_send)] #[allow(clippy::future_not_send)]
async fn fetch_resources( async fn fetch_resources(
request_uri: Uri, request_uri: Uri,

View file

@ -95,14 +95,6 @@ img, audio, video {
max-width: 75vw; max-width: 75vw;
} }
textarea {
width: 100%;
height: 100%;
min-width: 75vw;
min-height: 75vh;
box-sizing: border-box;
}
.primary { .primary {
@extend .hljs; @extend .hljs;
} }

View file

@ -16,64 +16,14 @@
import './main.scss'; import './main.scss';
import ReactDom from 'react-dom'; import ReactDom from 'react-dom';
import React, { useState } from 'react'; import React from 'react';
import { encrypt_string, encrypt_array_buffer } from '../pkg';
import hljs from 'highlight.js' const hljs = require('highlight.js');
(window as any).hljs = hljs; (window as any).hljs = hljs;
require('highlightjs-line-numbers.js'); require('highlightjs-line-numbers.js');
const FileForm = () => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const fr = new FileReader();
fr.onload = (e) => {
const { result } = e.target;
if (result instanceof ArrayBuffer) {
let data = new Uint8Array(result);
encrypt_array_buffer(data);
}
}
fr.readAsArrayBuffer((event.target as HTMLInputElement).files[0]);
}
return (
<input type="file" onChange={handleChange} />
)
}
const PasteForm = () => {
const [value, setValue] = useState("");
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
encrypt_string(value);
}
return (
<pre className='paste'>
<form className='hljs centered' onSubmit={handleSubmit}>
<textarea
placeholder="Sample text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<input type="submit" value="submit" />
</form>
</pre>
)
}
function createUploadUi() {
const html = <main className='hljs centered fullscreen'>
<FileForm />
<PasteForm />
</main>;
ReactDom.render(html, document.body);
}
function loadFromDb(mimeType: string, name?: string, language?: string) { function loadFromDb(mimeType: string, name?: string, language?: string) {
let resolvedName: string; let resolvedName;
if (name) { if (name) {
resolvedName = name; resolvedName = name;
} else { } else {
@ -337,4 +287,4 @@ function getObjectUrl(data, mimeType?: string) {
window.addEventListener("hashchange", () => location.reload()); window.addEventListener("hashchange", () => location.reload());
export { renderMessage, createUploadUi, loadFromDb }; export { renderMessage, loadFromDb };

@ -1 +1 @@
Subproject commit 8690be3625964d9992e7be4bc3e1a61a80161cc6 Subproject commit a1268635894c5ee23dfdece570418ca07b66c3fc

View file

@ -2,7 +2,6 @@ const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const { SourceMapDevToolPlugin } = require('webpack');
module.exports = { module.exports = {
entry: './web/src/index.js', entry: './web/src/index.js',
@ -22,8 +21,6 @@ module.exports = {
"css-loader", "css-loader",
// Compiles Sass to CSS // Compiles Sass to CSS
"sass-loader", "sass-loader",
// source map for debugging
"source-map-loader"
], ],
}, },
], ],
@ -44,7 +41,6 @@ module.exports = {
crateDirectory: path.resolve(__dirname, "web"), crateDirectory: path.resolve(__dirname, "web"),
outDir: path.resolve(__dirname, "web/pkg"), outDir: path.resolve(__dirname, "web/pkg"),
}), }),
new SourceMapDevToolPlugin({}),
], ],
experiments: { experiments: {
asyncWebAssembly: true, asyncWebAssembly: true,

587
yarn.lock

File diff suppressed because it is too large Load diff