serving app and .ng_bootstrap

Niko PLP 10 months ago
parent 72d71eb3d6
commit 8efdac469d
  1. 528
      Cargo.lock
  2. 11
      README.md
  3. 3
      ng-app/package.json
  4. 30
      ng-app/prepare-app-file.cjs
  5. 8
      ng-app/src/routes/WalletCreate.svelte
  6. 9
      ng-wallet/src/types.rs
  7. 5
      ngd/src/main.rs
  8. 11
      ngone/README.md
  9. 4
      p2p-broker/Cargo.toml
  10. 180
      p2p-broker/src/server_ws.rs
  11. 2
      p2p-client-ws/Cargo.toml
  12. 67
      p2p-net/src/types.rs
  13. 73
      pnpm-lock.yaml

528
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -108,6 +108,17 @@ cargo test --package p2p-client-ws --lib -- --nocapture
### Build release binaries
First you will need to build the single-file release of ng-app.
```
// uncomment line 14 of src/App.svelte: import * as api from "ng-sdk-js";
cd ng-app
pnpm filebuild
cd ..
```
then build the ngd daemon
```
cargo build -r -p ngd
```

@ -7,7 +7,7 @@
"dev": "vite",
"webdev": "cross-env NG_APP_WEB=1 TAURI_DEBUG=1 vite",
"webbuild": "cross-env NG_APP_WEB=1 vite build",
"filebuild": "cross-env NG_APP_WEB=1 NG_APP_FILE=1 vite build",
"filebuild": "cross-env NG_APP_WEB=1 NG_APP_FILE=1 vite build && node prepare-app-file.cjs",
"filebuilddebug": "cross-env NG_APP_WEB=1 NG_APP_FILE=1 TAURI_DEBUG=1 vite build -m debug",
"build": "vite build",
"preview": "vite preview",
@ -33,6 +33,7 @@
"autoprefixer": "^10.4.14",
"cross-env": "^7.0.3",
"internal-ip": "^7.0.0",
"node-gzip": "^1.1.2",
"postcss": "^8.4.23",
"postcss-load-config": "^4.0.1",
"svelte": "^3.54.0",

@ -0,0 +1,30 @@
var crypto = require('crypto')
, fs = require('fs')
const {gzip, } = require('node-gzip');
var algorithm = 'sha256'
, shasum = crypto.createHash(algorithm)
const sha_file = './dist-file/index.sha256';
const gzip_file = './dist-file/index.gzip';
var filename = './dist-file/index.html'
, s = fs.ReadStream(filename)
var bufs = [];
s.on('data', function(data) {
shasum.update(data)
bufs.push(data);
})
s.on('end', function() {
var hash = shasum.digest('hex')
console.log(hash + ' ' + filename)
fs.writeFileSync(sha_file, hash, 'utf8');
var buf = Buffer.concat(bufs);
gzip(buf).then((compressed) => {fs.writeFileSync(gzip_file, compressed);});
})

@ -557,10 +557,10 @@
This security measure will prevent you from entering your pazzle and PIN
on malicious sites and apps.
<Alert color="red" class="mt-5">
When you will use you wallet, if you do not see and recognize your
own security phrase and image before entering your pazzle, please
stop and DO NOT enter your pazzle, you are being the victim of a
phishing attempt.
Every time you will use your wallet, if you do not see and recognize
your own security phrase and image before entering your pazzle,
please stop and DO NOT enter your pazzle, as you would be the victim
of a phishing attempt.
</Alert>
</p>
<p class="text-left mt-5">

@ -12,7 +12,7 @@ use std::fmt;
use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
use p2p_net::types::{BrokerServerV0, NetAddr};
use p2p_net::types::{BootstrapContentV0, BrokerServerV0};
use p2p_repo::types::*;
/// WalletId is a PubKey
@ -21,13 +21,6 @@ pub type WalletId = PubKey;
/// BootstrapId is a WalletId
pub type BootstrapId = WalletId;
/// Bootstrap content Version 0
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BootstrapContentV0 {
/// list of servers, in order of preference
pub servers: Vec<BrokerServerV0>,
}
/// Bootstrap Version 0
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BootstrapV0 {

@ -577,6 +577,7 @@ async fn main_inner() -> Result<(), ()> {
} else {
listener.refuse_clients = true;
}
listener.serve_app = false;
listeners.push(listener);
}
}
@ -645,7 +646,7 @@ async fn main_inner() -> Result<(), ()> {
port: private_part.1,
discoverable: false,
refuse_clients: args.public_without_clients,
serve_app: true,
serve_app: false,
accept_direct: false,
accept_forward_for: AcceptForwardForV0::PublicStatic((
BindAddress {
@ -720,6 +721,7 @@ async fn main_inner() -> Result<(), ()> {
let mut listener =
ListenerV0::new_direct(inter, !args.no_ipv6, arg_value.1);
listener.accept_direct = false;
listener.serve_app = false;
listener.accept_forward_for =
AcceptForwardForV0::PublicDyn((public_port, 60, "".to_string()));
listeners.push(listener);
@ -829,6 +831,7 @@ async fn main_inner() -> Result<(), ()> {
{
let r = listeners.last_mut().unwrap();
r.accept_direct = true;
r.serve_app = true;
r.ipv6 = !args.no_ipv6;
} else {
listeners.push(ListenerV0::new_direct(inter, !args.no_ipv6, arg_value.1));

@ -23,6 +23,17 @@ cargo watch -c -w src -x run
## Build
First you will need to build the single-file release of ng-app.
```
// uncomment line 14 of src/App.svelte: import * as api from "ng-sdk-js";
cd ../ng-app
pnpm filebuild
cd ../ngone
```
then, in ngone:
```
cd web
pnpm run build

@ -22,9 +22,11 @@ async-channel = "1.7.1"
tempfile = "3"
hex = "0.4.3"
async-trait = "0.1.64"
async-tungstenite = { version = "0.22.2", features = ["async-std-runtime"] }
async-tungstenite = { git = "https://git.nextgraph.org/NextGraph/async-tungstenite.git", branch = "nextgraph", features = ["async-std-runtime"] }
blake3 = "1.3.1"
once_cell = "1.17.1"
rust-embed= { version = "6.7.0", features=["include-exclude"] }
serde_json = "1.0.96"
[target.'cfg(target_arch = "wasm32")'.dependencies.getrandom]
version = "0.2.7"

@ -21,11 +21,15 @@ use async_tungstenite::accept_hdr_async;
use async_tungstenite::tungstenite::handshake::server::{
Callback, ErrorResponse, Request, Response,
};
use async_tungstenite::tungstenite::http::header::{CONNECTION, HOST, ORIGIN, UPGRADE};
use async_tungstenite::tungstenite::http::HeaderValue;
use async_tungstenite::tungstenite::http::StatusCode;
use async_tungstenite::tungstenite::http::{
header::{CONNECTION, HOST, ORIGIN, UPGRADE},
HeaderValue, Method, StatusCode, Uri, Version,
};
use async_tungstenite::tungstenite::protocol::Message;
use futures::{SinkExt, StreamExt};
use once_cell::sync::Lazy;
use once_cell::sync::OnceCell;
use p2p_client_ws::remote_ws::ConnectionWebSocket;
use p2p_net::broker::*;
@ -37,11 +41,14 @@ use p2p_net::utils::{get_domain_without_port, Sensitive, U8Array};
use p2p_repo::log::*;
use p2p_repo::types::{PrivKey, PubKey};
use p2p_repo::utils::generate_keypair;
use rust_embed::RustEmbed;
use serde_json::json;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::net::SocketAddr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::num::NonZeroU8;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@ -52,6 +59,9 @@ use tempfile::Builder;
static LISTENERS_INFO: OnceCell<(HashMap<String, ListenerInfo>, HashMap<BindAddress, String>)> =
OnceCell::new();
static BOOTSTRAP_STRING: OnceCell<String> = OnceCell::new();
struct SecurityCallback {
remote_bind_address: BindAddress,
local_bind_address: BindAddress,
@ -178,25 +188,69 @@ fn prepare_urls_from_private_addrs(addrs: &Vec<BindAddress>, port: u16) -> Vec<S
res
}
#[derive(RustEmbed)]
#[folder = "../ng-app/dist-file/"]
#[include = "*.sha256"]
#[include = "*.gzip"]
struct App;
fn upgrade_ws_or_serve_app(
upgrade: Option<&HeaderValue>,
connection: Option<&HeaderValue>,
remote: IP,
serve_app: bool,
response: Response,
) -> Result<Response, ErrorResponse> {
if upgrade.is_some()
&& upgrade
uri: &Uri,
last_etag: Option<&HeaderValue>,
) -> Result<(), ErrorResponse> {
if connection.is_some()
&& connection
.unwrap()
.to_str()
.unwrap()
.split(|c| c == ' ' || c == ',')
.any(|p| p.eq_ignore_ascii_case("Upgrade"))
{
return Ok(response);
return Ok(());
}
if serve_app && (remote.is_private() || remote.is_loopback()) {
return Err(make_error(StatusCode::OK));
if uri == "/" {
log_debug!("Serving the app");
let sha_file = App::get("index.sha256").unwrap();
let sha = format!(
"\"{}\"",
std::str::from_utf8(sha_file.data.as_ref()).unwrap()
);
if last_etag.is_some() && last_etag.unwrap().to_str().unwrap() == sha {
// return 304
let res = Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header("Cache-Control", "max-age=31536000, must-revalidate")
.header("ETag", sha)
.body(None)
.unwrap();
return Err(res);
}
let file = App::get("index.gzip").unwrap();
let res = Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.header("Cache-Control", "max-age=31536000, must-revalidate")
.header("Content-Encoding", "gzip")
.header("ETag", sha)
.body(Some(file.data.to_vec()))
.unwrap();
return Err(res);
} else if uri == "/.ng_bootstrap" {
log_debug!("Serving bootstrap");
let res = Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/json")
.header("Cache-Control", "max-age=3600, must-revalidate")
.body(Some(BOOTSTRAP_STRING.get().unwrap().as_bytes().to_vec()))
.unwrap();
return Err(res);
}
}
Err(make_error(StatusCode::FORBIDDEN))
@ -207,7 +261,7 @@ const LOCAL_URLS: [&str; 3] = ["http://localhost", "http://127.0.0.1", "http://:
const APP_NG_ONE_URL: &str = "https://app.nextgraph.one";
impl Callback for SecurityCallback {
fn on_request(self, request: &Request, response: Response) -> Result<Response, ErrorResponse> {
fn on_request(self, request: &Request) -> Result<(), ErrorResponse> {
let local_urls = LOCAL_URLS
.to_vec()
.iter()
@ -236,21 +290,30 @@ impl Callback for SecurityCallback {
.get(listener_id)
.ok_or(make_error(StatusCode::FORBIDDEN))?;
if request.method() != Method::GET {
return Err(make_error(StatusCode::METHOD_NOT_ALLOWED));
}
if request.version() != Version::HTTP_11 {
return Err(make_error(StatusCode::HTTP_VERSION_NOT_SUPPORTED));
}
let xff = request.headers().get("X-Forwarded-For");
let upgrade = request.headers().get(CONNECTION);
let connection = request.headers().get(CONNECTION);
let host = request.headers().get(HOST);
let origin = request.headers().get(ORIGIN);
let remote = self.remote_bind_address.ip;
let xff = request.headers().get("X-Forwarded-For");
let last_etag = request.headers().get("If-None-Match");
let uri = request.uri();
log_debug!(
"upgrade:{:?} origin:{:?} host:{:?} xff:{:?} remote:{:?} local:{:?}",
upgrade,
"connection:{:?} origin:{:?} host:{:?} xff:{:?} remote:{:?} local:{:?} uri:{:?}",
connection,
origin,
host,
xff,
remote,
self.local_bind_address
self.local_bind_address,
uri
);
match listener.config.if_type {
@ -270,7 +333,13 @@ impl Callback for SecurityCallback {
"accepted core with refuse_clients {}",
listener.config.refuse_clients
);
return Ok(response);
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
);
}
InterfaceType::Loopback => {
if !remote.is_loopback() {
@ -293,10 +362,11 @@ impl Callback for SecurityCallback {
listener.config.accept_direct
);
return upgrade_ws_or_serve_app(
upgrade,
connection,
remote,
listener.config.serve_app,
response,
uri,
last_etag,
);
} else if listener.config.accept_forward_for.is_private_domain() {
let (hosts_str, urls_str) =
@ -305,14 +375,26 @@ impl Callback for SecurityCallback {
check_host(host, hosts_str)?;
check_xff_is_public_or_private(xff, false, false)?;
log_debug!("accepted loopback PRIVATE_DOMAIN");
return Ok(response);
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
);
} else if listener.config.accept_forward_for == AcceptForwardForV0::No {
check_host(host, local_hosts)?;
check_no_xff(xff)?;
// TODO local_urls might need a trailing :port, but it is ok for now as we do starts_with
check_origin_is_url(origin, local_urls)?;
log_debug!("accepted loopback DIRECT");
return Ok(response);
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
);
}
}
InterfaceType::Private => {
@ -347,7 +429,13 @@ impl Callback for SecurityCallback {
check_origin_is_url(origin, urls_str)?;
check_host_in_addrs(host, &addrs)?;
log_debug!("accepted private PUBLIC_STATIC or PUBLIC_DYN with direct {} with refuse_clients {}",listener.config.accept_direct, listener.config.refuse_clients);
return Ok(response);
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
);
} else if listener.config.accept_forward_for.is_public_domain() {
if !remote.is_private() {
return Err(make_error(StatusCode::FORBIDDEN));
@ -373,7 +461,13 @@ impl Callback for SecurityCallback {
"accepted private PUBLIC_DOMAIN with direct {}",
listener.config.accept_direct
);
return Ok(response);
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
);
} else if listener.config.accept_forward_for == AcceptForwardForV0::No {
if !remote.is_private() {
return Err(make_error(StatusCode::FORBIDDEN));
@ -387,7 +481,13 @@ impl Callback for SecurityCallback {
prepare_urls_from_private_addrs(&listener.addrs, listener.config.port),
)?;
log_debug!("accepted private DIRECT");
return Ok(response);
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
);
}
}
_ => {}
@ -410,10 +510,7 @@ pub async fn accept(tcp: TcpStream, peer_priv_key: Sensitive<[u8; 32]>) {
)
.await;
if ws.is_err() {
log_debug!("websocket rejected {:?}", ws.err());
//let mut buffer = Vec::new();
//tcp.read_to_end(&mut buffer).await;
//log_debug!("{:?}", buffer);
log_debug!("websocket rejected");
return;
}
@ -459,7 +556,7 @@ pub async fn run_server_accept_one(
pub async fn run_server_v0(
peer_priv_key: Sensitive<[u8; 32]>,
peer_pub_key: PubKey,
peer_id: PubKey,
wallet_master_key: Sensitive<[u8; 32]>,
config: DaemonConfigV0,
mut path: PathBuf,
@ -512,9 +609,12 @@ pub async fn run_server_v0(
let mut listeners_addrs: Vec<(Vec<SocketAddr>, String)> = vec![];
let mut listeners: Vec<TcpListener> = vec![];
let mut accept_clients = false;
//let mut serve_app = false;
// TODO: check that there is only one PublicDyn or one PublicStatic or one Core
let mut servers: Vec<BrokerServerV0> = vec![];
// Preparing the listeners addrs and infos
for listener in config.listeners {
if !listener.accept_direct && listener.accept_forward_for == AcceptForwardForV0::No {
@ -576,11 +676,26 @@ pub async fn run_server_v0(
listener.interface_name
);
}
// if listener.serve_app {
// serve_app = true;
// }
let bind_addresses: Vec<BindAddress> =
addrs.iter().map(|addr| addr.into()).collect();
let server_types = listener.get_bootstraps(bind_addresses.clone());
for server_type in server_types {
servers.push(BrokerServerV0 {
peer_id,
server_type,
})
}
let listener_id: String = listener.to_string();
let listener_info = ListenerInfo {
config: listener,
addrs: addrs.iter().map(|addr| addr.into()).collect(),
addrs: bind_addresses,
};
listener_infos.insert(listener_id, listener_info);
@ -598,11 +713,14 @@ pub async fn run_server_v0(
log_warn!("There isn't any listener that accept clients. This is a misconfiguration as a core server that cannot receive client connections is useless");
}
let bootstrap = BootstrapContent::V0(BootstrapContentV0 { servers });
BOOTSTRAP_STRING.set(json!(bootstrap).to_string()).unwrap();
// saving the infos in the broker. This needs to happen before we start listening, as new incoming connections can happen anytime after that.
// and we need those infos for permission checking.
{
let mut broker = BROKER.write().await;
broker.set_my_peer_id(peer_pub_key);
broker.set_my_peer_id(peer_id);
LISTENERS_INFO
.set(broker.set_listeners(listener_infos))
.unwrap();

@ -33,4 +33,4 @@ features = ["js"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
getrandom = "0.2.7"
xactor = "0.7.11"
async-tungstenite = { version = "0.22.2", features = ["async-std-runtime"] }
async-tungstenite = { git = "https://git.nextgraph.org/NextGraph/async-tungstenite.git", branch = "nextgraph", features = ["async-std-runtime"] }

@ -118,6 +118,18 @@ pub struct BrokerServerV0 {
pub peer_id: PubKey,
}
/// Bootstrap content Version 0
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BootstrapContentV0 {
/// list of servers, in order of preference
pub servers: Vec<BrokerServerV0>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum BootstrapContent {
V0(BootstrapContentV0),
}
/// ListenerInfo
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -274,6 +286,61 @@ impl ListenerV0 {
accept_forward_for: AcceptForwardForV0::No,
}
}
pub fn get_bootstraps(&self, addrs: Vec<BindAddress>) -> Vec<BrokerServerTypeV0> {
let mut res: Vec<BrokerServerTypeV0> = vec![];
match self.accept_forward_for {
AcceptForwardForV0::PublicStatic(_) => {
if !self.refuse_clients {
res.push(BrokerServerTypeV0::BoxPublic(
self.accept_forward_for.get_public_bind_addresses(),
));
}
if self.accept_direct {
res.push(BrokerServerTypeV0::BoxPrivate(addrs));
}
}
AcceptForwardForV0::PublicDyn(_) => {
if !self.refuse_clients {
res.push(BrokerServerTypeV0::BoxPublicDyn(
// self.accept_forward_for.get_public_bind_addresses(), //FIXME. we should use this, but for now it isnt implemented
vec![],
));
}
if self.accept_direct {
res.push(BrokerServerTypeV0::BoxPrivate(addrs));
}
}
AcceptForwardForV0::PublicDomain(_) | AcceptForwardForV0::PublicDomainPeer(_) => {
res.push(BrokerServerTypeV0::Domain(
self.accept_forward_for.get_domain().to_string(),
));
if self.accept_direct {
if self.if_type == InterfaceType::Private {
res.push(BrokerServerTypeV0::BoxPrivate(addrs));
} else if self.if_type == InterfaceType::Loopback {
res.push(BrokerServerTypeV0::Localhost(addrs[0].port));
}
}
}
AcceptForwardForV0::PrivateDomain(_) => {
res.push(BrokerServerTypeV0::Domain(
self.accept_forward_for.get_domain().to_string(),
));
}
AcceptForwardForV0::No => {
if self.if_type == InterfaceType::Loopback {
res.push(BrokerServerTypeV0::Localhost(addrs[0].port));
} else if self.if_type == InterfaceType::Public && !self.refuse_clients {
res.push(BrokerServerTypeV0::BoxPublic(addrs));
} else if self.if_type == InterfaceType::Private {
res.push(BrokerServerTypeV0::BoxPrivate(addrs));
}
}
_ => panic!("get_bootstrap missing"),
}
res
}
}
#[cfg(not(target_arch = "wasm32"))]
impl fmt::Display for ListenerV0 {

@ -21,6 +21,7 @@ importers:
flowbite-svelte: ^0.37.1
internal-ip: ^7.0.0
ng-sdk-js: workspace:^0.1.0
node-gzip: ^1.1.2
postcss: ^8.4.23
postcss-load-config: ^4.0.1
svelte: ^3.54.0
@ -53,6 +54,7 @@ importers:
autoprefixer: 10.4.14_postcss@8.4.24
cross-env: 7.0.3
internal-ip: 7.0.0
node-gzip: 1.1.2
postcss: 8.4.24
postcss-load-config: 4.0.1_postcss@8.4.24
svelte: 3.59.1
@ -72,12 +74,31 @@ importers:
ngone/web:
specifiers:
'@sveltejs/vite-plugin-svelte': ^2.0.4
autoprefixer: ^10.4.14
flowbite: ^1.6.5
flowbite-svelte: ^0.37.1
postcss: ^8.4.23
postcss-load-config: ^4.0.1
svelte: ^3.58.0
svelte-preprocess: ^5.0.3
svelte-spa-router: ^3.3.0
tailwindcss: ^3.3.1
vite: ^4.3.9
vite-plugin-svelte-svg: ^2.2.1
dependencies:
flowbite: 1.6.5
flowbite-svelte: 0.37.3_svelte@3.59.1
svelte-spa-router: 3.3.0
devDependencies:
'@sveltejs/vite-plugin-svelte': 2.4.1_svelte@3.59.1+vite@4.3.9
autoprefixer: 10.4.14_postcss@8.4.24
postcss: 8.4.24
postcss-load-config: 4.0.1_postcss@8.4.24
svelte: 3.59.1
svelte-preprocess: 5.0.4_sxhny56dlbcmwov4vk7qwrzshi
tailwindcss: 3.3.2
vite: 4.3.9
vite-plugin-svelte-svg: 2.2.1_svelte@3.59.1+vite@4.3.9
packages:
@ -1227,6 +1248,10 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/node-gzip/1.1.2:
resolution: {integrity: sha512-ZB6zWpfZHGtxZnPMrJSKHVPrRjURoUzaDbLFj3VO70mpLTW5np96vXyHwft4Id0o+PYIzgDkBUjIzaNHhQ8srw==}
dev: true
/node-releases/2.0.12:
resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==}
dev: true
@ -1631,6 +1656,54 @@ packages:
typescript: 5.1.3
dev: true
/svelte-preprocess/5.0.4_sxhny56dlbcmwov4vk7qwrzshi:
resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==}
engines: {node: '>= 14.10.0'}
requiresBuild: true
peerDependencies:
'@babel/core': ^7.10.2
coffeescript: ^2.5.1
less: ^3.11.3 || ^4.0.0
postcss: ^7 || ^8
postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0
pug: ^3.0.0
sass: ^1.26.8
stylus: ^0.55.0
sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0
svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0
typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0'
peerDependenciesMeta:
'@babel/core':
optional: true
coffeescript:
optional: true
less:
optional: true
postcss:
optional: true
postcss-load-config:
optional: true
pug:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
typescript:
optional: true
dependencies:
'@types/pug': 2.0.6
detect-indent: 6.1.0
magic-string: 0.27.0
postcss: 8.4.24
postcss-load-config: 4.0.1_postcss@8.4.24
sorcery: 0.11.0
strip-indent: 3.0.0
svelte: 3.59.1
dev: true
/svelte-preprocess/5.0.4_vmz4xia4c7tzh4ii3qac2x3tom:
resolution: {integrity: sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==}
engines: {node: '>= 14.10.0'}

Loading…
Cancel
Save