mirror of
https://github.com/cupcakearmy/cryptgeon.git
synced 2025-09-05 17:00:39 +00:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
a5d98b76bd | |||
9590c9b567 | |||
|
0913a8ad0c | ||
d13c712e95 | |||
6230d2dbd0 | |||
|
dbfb383c73 | ||
a257d2cefb | |||
35ba25ba9e | |||
724dca0e69 | |||
9029f72a02 | |||
1d55d7f2d2 | |||
d09bb4e0c6 | |||
53c7c9d9e2 | |||
df9c60c29e | |||
f29b6b23f0 | |||
cc88fa6763 | |||
19022e7cb5 | |||
44f43dbc2c | |||
45f6f3af32 | |||
9bd544f0d5 |
@@ -1,2 +1,15 @@
|
|||||||
/**/target
|
*
|
||||||
/**/node_modules
|
|
||||||
|
!/backend/src
|
||||||
|
!/backend/Cargo.lock
|
||||||
|
!/backend/Cargo.toml
|
||||||
|
|
||||||
|
!/frontend/locales
|
||||||
|
!/frontend/src
|
||||||
|
!/frontend/static
|
||||||
|
!/frontend/.npmrc
|
||||||
|
!/frontend/package.json
|
||||||
|
!/frontend/pnpm-lock.yaml
|
||||||
|
!/frontend/svelte.config.js
|
||||||
|
!/frontend/tsconfig.json
|
||||||
|
!/frontend/vite.config.js
|
||||||
|
63
CHANGELOG.md
63
CHANGELOG.md
@@ -5,6 +5,69 @@ 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).
|
||||||
|
|
||||||
|
## [2.0.1] - 2022-07-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Max file size on the client now.
|
||||||
|
- Loading information.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed encoding from hex to base64.
|
||||||
|
- Chinese language code.
|
||||||
|
- Notable speed improvements for big files.
|
||||||
|
|
||||||
|
## [2.0.0] - 2022-07-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Theming for logo and description text.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved to redis.
|
||||||
|
- New html sanitizing library.
|
||||||
|
|
||||||
|
## [2.0.0-rc.0] - 2022-07-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Theming for logo and description text.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved to redis.
|
||||||
|
- New html sanitizing library.
|
||||||
|
|
||||||
|
## [1.5.3] - 2022-06-07
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Use the value from the `MEMCACHE` env variable in startup script.
|
||||||
|
|
||||||
|
## [1.5.2] - 2022-06-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Wait for script for memecached.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Updated dependencies.
|
||||||
|
|
||||||
|
## [1.5.1] - 2022-05-15
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Remove double note content.
|
||||||
|
|
||||||
|
## [1.5.0] - 2022-05-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Links in notes are not highlighted and can be directly clicked #30.
|
||||||
|
|
||||||
## [1.4.1] - 2022-03-05
|
## [1.4.1] - 2022-03-05
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
27
Dockerfile
27
Dockerfile
@@ -1,31 +1,26 @@
|
|||||||
# Frontend
|
# FRONTEND
|
||||||
FROM node:16-alpine as CLIENT
|
FROM node:16-alpine as client
|
||||||
|
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
|
RUN npm install -g pnpm@7
|
||||||
COPY ./frontend ./
|
COPY ./frontend ./
|
||||||
|
|
||||||
RUN npm install -g pnpm
|
|
||||||
RUN pnpm install
|
RUN pnpm install
|
||||||
|
RUN pnpm exec svelte-kit sync
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
# Backend
|
|
||||||
FROM rust:1.59-alpine as RUST
|
|
||||||
|
|
||||||
|
# BACKEND
|
||||||
|
FROM rust:1.61-alpine as backend
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
RUN apk add libc-dev openssl-dev alpine-sdk
|
RUN apk add libc-dev openssl-dev alpine-sdk
|
||||||
COPY ./backend ./
|
COPY ./backend ./
|
||||||
|
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
# Server
|
|
||||||
|
# RUNNER
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=RUST /tmp/target/release/cryptgeon .
|
COPY --from=backend /tmp/target/release/cryptgeon .
|
||||||
COPY --from=CLIENT /tmp/build ./frontend/build
|
COPY --from=client /tmp/build ./frontend/build
|
||||||
|
ENV REDIS=redis://redis/
|
||||||
ENV MEMCACHE=memcached:11211
|
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
ENTRYPOINT [ "/app/cryptgeon" ]
|
ENTRYPOINT [ "/app/cryptgeon" ]
|
||||||
|
73
README.md
73
README.md
@@ -9,10 +9,12 @@
|
|||||||
<img alt="Latest version" src="https://img.shields.io/github/v/release/cupcakearmy/cryptgeon?style=for-the-badge" />
|
<img alt="Latest version" src="https://img.shields.io/github/v/release/cupcakearmy/cryptgeon?style=for-the-badge" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<br/>
|
<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" height="50" /></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">
|
<a href=""><img src="./.github/lokalise.png" height="50">
|
||||||
<br/>
|
<br/><br/>
|
||||||
|
|
||||||
|
EN | [简体中文](README_zh-CN.md)
|
||||||
|
|
||||||
## About?
|
## About?
|
||||||
|
|
||||||
@@ -49,9 +51,14 @@ of the notes even if it tried to.
|
|||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
| ------------ | ----------------- | --------------------------------------------------------------------------------------- |
|
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `MEMCACHE` | `memcached:11211` | Memcached URL to connect to. |
|
| `REDIS` | `redis://redis/` | Redis URL to connect to. |
|
||||||
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/) |
|
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/). <br> `512 MiB` is the maximum allowed. <br> The frontend will show that number including the ~35% encoding overhead. |
|
||||||
|
| `MAX_VIEWS` | `100` | Maximal number of views. |
|
||||||
|
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
|
||||||
|
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
|
||||||
|
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
|
||||||
|
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
@@ -64,19 +71,18 @@ Docker is the easiest way. There is the [official image here](https://hub.docker
|
|||||||
```yaml
|
```yaml
|
||||||
# docker-compose.yml
|
# docker-compose.yml
|
||||||
|
|
||||||
version: '3.7'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
memcached:
|
redis:
|
||||||
image: memcached:1-alpine
|
image: redis:7-alpine
|
||||||
entrypoint: memcached -m 128M -I 4M # Limit to 128 MB Ram, 4M per entry, customize at free will.
|
|
||||||
|
|
||||||
app:
|
app:
|
||||||
image: cupcakearmy/cryptgeon:latest
|
image: cupcakearmy/cryptgeon:latest
|
||||||
depends_on:
|
depends_on:
|
||||||
- memcached
|
- redis
|
||||||
environment:
|
environment:
|
||||||
SIZE_LIMIT: 4M
|
SIZE_LIMIT: 4 MiB
|
||||||
ports:
|
ports:
|
||||||
- 80:5000
|
- 80:5000
|
||||||
```
|
```
|
||||||
@@ -102,16 +108,15 @@ networks:
|
|||||||
external: true
|
external: true
|
||||||
|
|
||||||
services:
|
services:
|
||||||
memcached:
|
redis:
|
||||||
image: memcached:1-alpine
|
image: redis:7-alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
entrypoint: memcached -m 128M -I 4M # Limit to 128 MB Ram, 4M per entry, customize at free will.
|
|
||||||
|
|
||||||
app:
|
app:
|
||||||
image: cupcakearmy/cryptgeon:latest
|
image: cupcakearmy/cryptgeon:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- memcached
|
- redis
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
- proxy
|
- proxy
|
||||||
@@ -124,15 +129,39 @@ services:
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
1. Clone
|
**Requirements**
|
||||||
2. run `pnpm i` in the root and and client `client/` folders.
|
|
||||||
3. Run `pnpm run dev` to start development.
|
|
||||||
|
|
||||||
Running `npm run dev` in the root folder will start the following things
|
- `pnpm`: `>=6`
|
||||||
|
- `node`: `>=16`
|
||||||
|
- `rust`: edition `2021`
|
||||||
|
|
||||||
- a memcache docker container
|
**Install**
|
||||||
- rust backend with hot reload
|
|
||||||
- client with hot reload
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm --prefix frontend install
|
||||||
|
|
||||||
|
# Also you need cargo watch if you don't already have it installed.
|
||||||
|
# https://lib.rs/crates/cargo-watch
|
||||||
|
cargo install cargo-watch
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run**
|
||||||
|
|
||||||
|
Make sure you have docker running.
|
||||||
|
|
||||||
|
> If you are on `macOS` you might need to disable AirPlay Receiver as it uses port 5000 (So stupid...)
|
||||||
|
> https://developer.apple.com/forums/thread/682332
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Running `pnpm run dev` in the root folder will start the following things:
|
||||||
|
|
||||||
|
- redis docker container
|
||||||
|
- rust backend
|
||||||
|
- client
|
||||||
|
|
||||||
You can see the app under [localhost:1234](http://localhost:1234).
|
You can see the app under [localhost:1234](http://localhost:1234).
|
||||||
|
|
||||||
|
166
README_zh-CN.md
Normal file
166
README_zh-CN.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="./design/Github_zh-CN.png" alt="logo">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a href="https://discord.gg/nuby6RnxZt">
|
||||||
|
<img alt="discord" src="https://img.shields.io/discord/252403122348097536?style=for-the-badge" />
|
||||||
|
<img alt="docker pulls" src="https://img.shields.io/docker/pulls/cupcakearmy/cryptgeon?style=for-the-badge" />
|
||||||
|
<img alt="Docker image size badge" src="https://img.shields.io/docker/image-size/cupcakearmy/cryptgeon?style=for-the-badge" />
|
||||||
|
<img alt="Latest version" src="https://img.shields.io/github/v/release/cupcakearmy/cryptgeon?style=for-the-badge" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<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" height="50" /></a>
|
||||||
|
<a href=""><img src="./.github/lokalise.png" height="50">
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
[EN](README.md) | 简体中文
|
||||||
|
|
||||||
|
## 关于本项目
|
||||||
|
|
||||||
|
_加密鸽_ 是一个受 [_PrivNote_](https://privnote.com)项目启发的安全、开源共享密信和文件共享服务器
|
||||||
|
|
||||||
|
> 🌍 如果你想翻译此项目请随时与我联系.
|
||||||
|
>
|
||||||
|
> 感谢 [Lokalise](https://lokalise.com/) 提供免费的平台服务支持
|
||||||
|
|
||||||
|
## 演示示例
|
||||||
|
|
||||||
|
查看加密鸽的在线演示 demo: https://cryptgeon.nicco.io.
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 服务端无法解密和查看客户端加密的内容
|
||||||
|
- 查看次数或时间限制,阅后即焚
|
||||||
|
- 您发送的数据将存放于内存中,不会写入到磁盘中
|
||||||
|
- 黑暗模式支持
|
||||||
|
|
||||||
|
## 加密鸽是如何工作的?
|
||||||
|
|
||||||
|
加密鸽会为每条笔记都生成一个独立的 <code>id (256bit)</code> 和 <code>key 256(bit)</code>。
|
||||||
|
|
||||||
|
其中<code>id</code>用于保存和提取密信, 在这之后这封密信将会被客户端使用 AES 算法的 GCM 模式和`key`进行加密然后发送至服务器,数据将会保存在服务器的内存中且永远不会被持久化到硬盘上,服务端永远不会得到密钥并且无法解读密信的内容。
|
||||||
|
|
||||||
|
## 屏幕截图
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
| 变量名称 | 默认值 | 描述 |
|
||||||
|
| ----------------- | ---------------- | --------------------------------------------------------------------------------- |
|
||||||
|
| `REDIS` | `redis://redis/` | Redis URL to connect to. |
|
||||||
|
| `SIZE_LIMIT` | `1 KiB` | 最大请求体(body)限制。有关支持的数值请查看 [字节单位](https://docs.rs/byte-unit/) |
|
||||||
|
| `MAX_VIEWS` | `100` | 密信最多查看次数限制 |
|
||||||
|
| ` MAX_EXPIRATION` | `360` | 密信最长过期时间限制(分钟) |
|
||||||
|
| `ALLOW_ADVANCED` | `true` | 是否允许自定义设置,该项如果设为`false`,则不会显示自定义设置模块 |
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
ℹ️ 加密鸽必须使用`https`,否则浏览器可能将不会支援加密鸽的加密算法。
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
Docker 是最简单的部署方式。这里是[官方镜像的地址](https://hub.docker.com/r/cupcakearmy/cryptgeon)。
|
||||||
|
|
||||||
|
附:译者的[部署笔记](https://www.hash070.top/archives/cryptgeon-docker-deploy.html)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
|
||||||
|
app:
|
||||||
|
image: cupcakearmy/cryptgeon:latest
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
SIZE_LIMIT: 4 MiB
|
||||||
|
ports:
|
||||||
|
- 80:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### NGINX 反向代理
|
||||||
|
|
||||||
|
查看 [examples/nginx](https://github.com/cupcakearmy/cryptgeon/tree/main/examples/nginx) 目录。那里有几个示例反代配置文件模板,其中一个是带 https 配置的反代配置模板,你需要指定服务器的名称和证书才能生效。
|
||||||
|
|
||||||
|
### Traefik 2
|
||||||
|
|
||||||
|
假设:
|
||||||
|
|
||||||
|
- 外部 Docker 代理网络 `proxy`
|
||||||
|
- 证书解析器 `le`
|
||||||
|
- 一个 https 入站点 `secure`
|
||||||
|
- 域名 `example.org`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
app:
|
||||||
|
image: cupcakearmy/cryptgeon:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- proxy
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.cryptgeon.rule=Host(`example.org`)
|
||||||
|
- traefik.http.routers.cryptgeon.entrypoints=secure
|
||||||
|
- traefik.http.routers.cryptgeon.tls.certresolver=le
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
**环境要求**
|
||||||
|
|
||||||
|
- `pnpm`: `>=6`
|
||||||
|
- `node`: `>=14`
|
||||||
|
- `rust`: edition `2021`
|
||||||
|
|
||||||
|
**安装**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm --prefix frontend install
|
||||||
|
|
||||||
|
# Also you need cargo watch if you don't already have it installed.
|
||||||
|
# https://lib.rs/crates/cargo-watch
|
||||||
|
cargo install cargo-watch
|
||||||
|
```
|
||||||
|
|
||||||
|
**运行**
|
||||||
|
|
||||||
|
确保你的 Docker 正在运行
|
||||||
|
|
||||||
|
> If you are on `macOS` you might need to disable AirPlay Receiver as it uses port 5000 (So stupid...)
|
||||||
|
> https://developer.apple.com/forums/thread/682332
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
在根目录执行 `pnpm run dev` 会开启下列服务:
|
||||||
|
|
||||||
|
- 一个 redis docker 容器
|
||||||
|
- 无热重载的 rust 后端
|
||||||
|
- 可热重载的客户端
|
||||||
|
|
||||||
|
你可以通过 1234 端口进入该应用,即 [localhost:1234](http://localhost:1234).
|
||||||
|
|
||||||
|
###### Attributions
|
||||||
|
|
||||||
|
本项目所使用的图标由<a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com 的<a href="https://www.freepik.com" title="Freepik">freepik</a>制作</a>
|
650
backend/Cargo.lock
generated
650
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cryptgeon"
|
name = "cryptgeon"
|
||||||
version = "1.4.1"
|
version = "2.0.1"
|
||||||
authors = ["cupcakearmy <hi@nicco.io>"]
|
authors = ["cupcakearmy <hi@nicco.io>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -18,7 +18,8 @@ serde_json = "1"
|
|||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
ring = "0.16"
|
ring = "0.16"
|
||||||
bs62 = "0.1"
|
bs62 = "0.1"
|
||||||
memcache = "0.16"
|
|
||||||
byte-unit = "4"
|
byte-unit = "4"
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
|
env_logger = "0.9"
|
||||||
|
redis = "0.21.5"
|
||||||
|
@@ -1,13 +1,18 @@
|
|||||||
use byte_unit::Byte;
|
use byte_unit::Byte;
|
||||||
|
|
||||||
|
// General
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref VERSION: String = option_env!("CARGO_PKG_VERSION")
|
pub static ref VERSION: String = option_env!("CARGO_PKG_VERSION")
|
||||||
.unwrap_or("Unknown")
|
.unwrap_or("Unknown")
|
||||||
.to_string();
|
.to_string();
|
||||||
pub static ref LIMIT: u32 =
|
}
|
||||||
|
|
||||||
|
// CONFIG
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref LIMIT: usize =
|
||||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get_bytes() as u32;
|
.get_bytes() as usize;
|
||||||
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
|
pub static ref MAX_VIEWS: u32 = std::env::var("MAX_VIEWS")
|
||||||
.unwrap_or("100".to_string())
|
.unwrap_or("100".to_string())
|
||||||
.parse()
|
.parse()
|
||||||
@@ -21,3 +26,15 @@ lazy_static! {
|
|||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// THEME
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref THEME_IMAGE: String = std::env::var("THEME_IMAGE")
|
||||||
|
.unwrap_or("".to_string())
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
pub static ref THEME_TEXT: String = std::env::var("THEME_TEXT")
|
||||||
|
.unwrap_or("".to_string())
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
use actix_web::{middleware, web, App, HttpServer};
|
use actix_web::{
|
||||||
|
middleware::{self, Logger},
|
||||||
|
web, App, HttpServer,
|
||||||
|
};
|
||||||
use dotenv::dotenv;
|
use dotenv::dotenv;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
@@ -15,8 +18,10 @@ mod store;
|
|||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
|
env_logger::init_from_env(env_logger::Env::new().default_filter_or("warning"));
|
||||||
return HttpServer::new(|| {
|
return HttpServer::new(|| {
|
||||||
App::new()
|
App::new()
|
||||||
|
.wrap(Logger::new("%a \"%r\" %s %b %T"))
|
||||||
.wrap(middleware::Compress::default())
|
.wrap(middleware::Compress::default())
|
||||||
.wrap(middleware::DefaultHeaders::default())
|
.wrap(middleware::DefaultHeaders::default())
|
||||||
.configure(size::init)
|
.configure(size::init)
|
||||||
|
@@ -22,9 +22,11 @@ struct NotePath {
|
|||||||
async fn one(path: web::Path<NotePath>) -> impl Responder {
|
async fn one(path: web::Path<NotePath>) -> impl Responder {
|
||||||
let p = path.into_inner();
|
let p = path.into_inner();
|
||||||
let note = store::get(&p.id);
|
let note = store::get(&p.id);
|
||||||
|
|
||||||
match note {
|
match note {
|
||||||
None => return HttpResponse::NotFound().finish(),
|
Ok(Some(_)) => HttpResponse::Ok().json(NoteInfo {}),
|
||||||
Some(_) => return HttpResponse::Ok().json(NoteInfo {}),
|
Ok(None) => HttpResponse::NotFound().finish(),
|
||||||
|
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +66,10 @@ async fn create(note: web::Json<Note>) -> impl Responder {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
store::set(&id.clone(), &n.clone());
|
match store::set(&id.clone(), &n.clone()) {
|
||||||
return HttpResponse::Ok().json(CreateResponse { id: id });
|
Ok(_) => return HttpResponse::Ok().json(CreateResponse { id: id }),
|
||||||
|
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[delete("/{id}")]
|
#[delete("/{id}")]
|
||||||
@@ -73,8 +77,9 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
|
|||||||
let p = path.into_inner();
|
let p = path.into_inner();
|
||||||
let note = store::get(&p.id);
|
let note = store::get(&p.id);
|
||||||
match note {
|
match note {
|
||||||
None => return HttpResponse::NotFound().finish(),
|
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||||
Some(note) => {
|
Ok(None) => return HttpResponse::NotFound().finish(),
|
||||||
|
Ok(Some(note)) => {
|
||||||
let mut changed = note.clone();
|
let mut changed = note.clone();
|
||||||
if changed.views == None && changed.expiration == None {
|
if changed.views == None && changed.expiration == None {
|
||||||
return HttpResponse::BadRequest().finish();
|
return HttpResponse::BadRequest().finish();
|
||||||
@@ -84,9 +89,19 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
|
|||||||
changed.views = Some(v - 1);
|
changed.views = Some(v - 1);
|
||||||
let id = p.id.clone();
|
let id = p.id.clone();
|
||||||
if v <= 1 {
|
if v <= 1 {
|
||||||
store::del(&id);
|
match store::del(&id) {
|
||||||
|
Err(e) => {
|
||||||
|
return HttpResponse::InternalServerError().body(e.to_string())
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
store::set(&id, &changed.clone());
|
match store::set(&id, &changed.clone()) {
|
||||||
|
Err(e) => {
|
||||||
|
return HttpResponse::InternalServerError().body(e.to_string())
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -96,8 +111,12 @@ async fn delete(path: web::Path<NotePath>) -> impl Responder {
|
|||||||
match changed.expiration {
|
match changed.expiration {
|
||||||
Some(e) => {
|
Some(e) => {
|
||||||
if e < n {
|
if e < n {
|
||||||
store::del(&p.id.clone());
|
match store::del(&p.id.clone()) {
|
||||||
return HttpResponse::BadRequest().finish();
|
Ok(_) => return HttpResponse::BadRequest().finish(),
|
||||||
|
Err(e) => {
|
||||||
|
return HttpResponse::InternalServerError().body(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@@ -1,18 +1,11 @@
|
|||||||
|
use crate::config;
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use byte_unit::Byte;
|
|
||||||
use mime;
|
use mime;
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
pub static ref LIMIT: usize =
|
|
||||||
Byte::from_str(std::env::var("SIZE_LIMIT").unwrap_or("1 KiB".to_string()))
|
|
||||||
.unwrap()
|
|
||||||
.get_bytes() as usize;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(cfg: &mut web::ServiceConfig) {
|
pub fn init(cfg: &mut web::ServiceConfig) {
|
||||||
let json = web::JsonConfig::default().limit(*LIMIT);
|
let json = web::JsonConfig::default().limit(*config::LIMIT);
|
||||||
let plain = web::PayloadConfig::default()
|
let plain = web::PayloadConfig::default()
|
||||||
.limit(*LIMIT)
|
.limit(*config::LIMIT)
|
||||||
.mimetype(mime::STAR_STAR);
|
.mimetype(mime::STAR_STAR);
|
||||||
cfg.app_data(json).app_data(plain);
|
cfg.app_data(json).app_data(plain);
|
||||||
}
|
}
|
||||||
|
@@ -2,9 +2,14 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
|
// General
|
||||||
pub version: String,
|
pub version: String,
|
||||||
|
// Config
|
||||||
pub max_size: u32,
|
pub max_size: u32,
|
||||||
pub max_views: u32,
|
pub max_views: u32,
|
||||||
pub max_expiration: u32,
|
pub max_expiration: u32,
|
||||||
pub allow_advanced: bool,
|
pub allow_advanced: bool,
|
||||||
|
// Theme
|
||||||
|
pub theme_image: String,
|
||||||
|
pub theme_text: String,
|
||||||
}
|
}
|
||||||
|
@@ -7,10 +7,12 @@ use crate::status::Status;
|
|||||||
async fn get_status() -> impl Responder {
|
async fn get_status() -> impl Responder {
|
||||||
return HttpResponse::Ok().json(Status {
|
return HttpResponse::Ok().json(Status {
|
||||||
version: config::VERSION.to_string(),
|
version: config::VERSION.to_string(),
|
||||||
max_size: *config::LIMIT,
|
max_size: *config::LIMIT as u32,
|
||||||
max_views: *config::MAX_VIEWS,
|
max_views: *config::MAX_VIEWS,
|
||||||
max_expiration: *config::MAX_EXPIRATION,
|
max_expiration: *config::MAX_EXPIRATION,
|
||||||
allow_advanced: *config::ALLOW_ADVANCED,
|
allow_advanced: *config::ALLOW_ADVANCED,
|
||||||
|
theme_image: config::THEME_IMAGE.to_string(),
|
||||||
|
theme_text: config::THEME_TEXT.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,36 +1,55 @@
|
|||||||
use memcache;
|
use redis;
|
||||||
|
use redis::Commands;
|
||||||
|
|
||||||
use crate::note::now;
|
use crate::note::now;
|
||||||
use crate::note::Note;
|
use crate::note::Note;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CLIENT: memcache::Client = memcache::connect(format!(
|
static ref REDIS_CLIENT: String = std::env::var("REDIS")
|
||||||
"memcache://{}?timeout=10&tcp_nodelay=true",
|
.unwrap_or("redis://127.0.0.1/".to_string())
|
||||||
std::env::var("MEMCACHE").unwrap_or("127.0.0.1:11211".to_string())
|
.parse()
|
||||||
))
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(id: &String, note: &Note) {
|
fn get_connection() -> Result<redis::Connection, &'static str> {
|
||||||
let serialized = serde_json::to_string(¬e.clone()).unwrap();
|
let client =
|
||||||
let expiration: u32 = match note.expiration {
|
redis::Client::open(REDIS_CLIENT.to_string()).map_err(|_| "Unable to connect to redis")?;
|
||||||
Some(e) => e - now(),
|
client
|
||||||
None => 0,
|
.get_connection()
|
||||||
};
|
.map_err(|_| "Unable to connect to redis")
|
||||||
CLIENT.set(id, serialized, expiration).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(id: &String) -> Option<Note> {
|
pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
|
||||||
let value: Option<String> = CLIENT.get(&id).unwrap();
|
let serialized = serde_json::to_string(¬e.clone()).unwrap();
|
||||||
|
let mut conn = get_connection()?;
|
||||||
|
|
||||||
|
conn.set(id, serialized)
|
||||||
|
.map_err(|_| "Unable to set note in redis")?;
|
||||||
|
match note.expiration {
|
||||||
|
Some(e) => {
|
||||||
|
let seconds = e - now();
|
||||||
|
conn.expire(id, seconds as usize)
|
||||||
|
.map_err(|_| "Unable to set expiration on notion")?
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(id: &String) -> Result<Option<Note>, &'static str> {
|
||||||
|
let mut conn = get_connection()?;
|
||||||
|
let value: Option<String> = conn.get(id).map_err(|_| "Could not load note in redis")?;
|
||||||
match value {
|
match value {
|
||||||
None => return None,
|
None => return Ok(None),
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
let deserialize: Note = serde_json::from_str(&s).unwrap();
|
let deserialize: Note = serde_json::from_str(&s).unwrap();
|
||||||
return Some(deserialize);
|
return Ok(Some(deserialize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn del(id: &String) {
|
pub fn del(id: &String) -> Result<(), &'static str> {
|
||||||
CLIENT.delete(id).unwrap();
|
let mut conn = get_connection()?;
|
||||||
|
conn.del(id).map_err(|_| "Unable to delete note in redis")?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
BIN
design/Github_zh-CN.png
Normal file
BIN
design/Github_zh-CN.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
@@ -1,21 +1,19 @@
|
|||||||
# DEV Compose file.
|
# DEV Compose file.
|
||||||
# For a production file see: README.md
|
# For a production file see: README.md
|
||||||
|
|
||||||
version: '3.7'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
memcached:
|
redis:
|
||||||
image: memcached:1-alpine
|
image: redis:7-alpine
|
||||||
restart: unless-stopped
|
|
||||||
entrypoint: memcached -m 256M -I 128M
|
|
||||||
ports:
|
ports:
|
||||||
- 11211:11211
|
- 6379:6379
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
depends_on:
|
depends_on:
|
||||||
- memcached
|
- redis
|
||||||
environment:
|
environment:
|
||||||
SIZE_LIMIT: 128M
|
SIZE_LIMIT: 128 MiB
|
||||||
ports:
|
ports:
|
||||||
- 80:5000
|
- 80:5000
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
memcached:
|
redis:
|
||||||
image: memcached:1-alpine
|
image: redis:7-alpine
|
||||||
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
|
||||||
depends_on:
|
depends_on:
|
||||||
- memcached
|
- redis
|
||||||
|
|
||||||
proxy:
|
proxy:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
|
160
examples/scratch/README.md
Normal file
160
examples/scratch/README.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# Install from scratch.
|
||||||
|
|
||||||
|
This is a tiny guide to install cryptgeon on (probably) any unix system (and maybe windows?) from scratch using traefik as the proxy, which will manage certificates and handle https for us.
|
||||||
|
|
||||||
|
1. Install Docker & Docker Compose.
|
||||||
|
2. Install Traefik.
|
||||||
|
3. Run the cryptgeon.
|
||||||
|
4. [Optional] install watchtower to keep up to date.
|
||||||
|
|
||||||
|
## Install Docker & Docker Compose
|
||||||
|
|
||||||
|
- [Docker](https://docs.docker.com/engine/install/)
|
||||||
|
- [Compose](https://docs.docker.com/compose/install/)
|
||||||
|
|
||||||
|
## Install Traefik 2.0
|
||||||
|
|
||||||
|
[Traefik](https://doc.traefik.io/traefik/) is a router & proxy that makes deployment of containers incredibly easy. It will manage all the https certificates, routing, etc.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/foo/bar/traefik/
|
||||||
|
├── docker-compose.yaml
|
||||||
|
└── traefik.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yaml
|
||||||
|
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
traefik:
|
||||||
|
image: traefik:2.6
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '80:80'
|
||||||
|
- '443:443'
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./traefik.yaml:/etc/traefik/traefik.yaml:ro
|
||||||
|
- ./data:/data
|
||||||
|
labels:
|
||||||
|
- 'traefik.enable=true'
|
||||||
|
|
||||||
|
# HTTP to HTTPS redirection
|
||||||
|
- 'traefik.http.routers.http_catchall.rule=HostRegexp(`{any:.+}`)'
|
||||||
|
- 'traefik.http.routers.http_catchall.entrypoints=insecure'
|
||||||
|
- 'traefik.http.routers.http_catchall.middlewares=https_redirect'
|
||||||
|
- 'traefik.http.middlewares.https_redirect.redirectscheme.scheme=https'
|
||||||
|
- 'traefik.http.middlewares.https_redirect.redirectscheme.permanent=true'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
external: true
|
||||||
|
name: proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# traefik.yaml
|
||||||
|
|
||||||
|
api:
|
||||||
|
dashboard: true
|
||||||
|
|
||||||
|
# Define HTTP and HTTPS entrypoint
|
||||||
|
entryPoints:
|
||||||
|
insecure:
|
||||||
|
address: ':80'
|
||||||
|
secure:
|
||||||
|
address: ':443'
|
||||||
|
|
||||||
|
# Dynamic configuration will come from docker labels
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
endpoint: 'unix:///var/run/docker.sock'
|
||||||
|
network: 'proxy'
|
||||||
|
exposedByDefault: false
|
||||||
|
|
||||||
|
# Enable acme with http file challenge
|
||||||
|
certificatesResolvers:
|
||||||
|
le:
|
||||||
|
acme:
|
||||||
|
email: me@example.org
|
||||||
|
storage: /data/acme.json
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: insecure
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker network create proxy
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cryptgeon
|
||||||
|
|
||||||
|
Create another docker-compose.yaml file in another folder. We will assume that the domain is `cryptgeon.example.org`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/foo/bar/cryptgeon/
|
||||||
|
└── docker-compose.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
app:
|
||||||
|
image: cupcakearmy/cryptgeon:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
SIZE_LIMIT: 4 MiB
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- proxy
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.cryptgeon.rule=Host(`cryptgeon.example.org`)
|
||||||
|
- traefik.http.routers.cryptgeon.entrypoints=secure
|
||||||
|
- traefik.http.routers.cryptgeon.tls.certresolver=le
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Watchtower
|
||||||
|
|
||||||
|
> A container-based solution for automating Docker container base image updates.
|
||||||
|
|
||||||
|
[Watchtower](https://containrrr.dev/watchtower/) will keep our containers up to date. The interval is set to once a day and also configured to delete old images to prevent cluttering.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
/foo/bar/watchtower/
|
||||||
|
└── docker-compose.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yaml
|
||||||
|
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
command: --cleanup --interval 86400
|
||||||
|
```
|
@@ -1,6 +1,7 @@
|
|||||||
├─ MIT: 46
|
├─ MIT: 12
|
||||||
├─ MIT*: 2
|
├─ BSD-3-Clause: 1
|
||||||
├─ BSD-3-Clause: 2
|
├─ (MPL-2.0 OR Apache-2.0): 1
|
||||||
|
├─ BSD-2-Clause: 1
|
||||||
├─ ISC: 1
|
├─ ISC: 1
|
||||||
├─ 0BSD: 1
|
├─ 0BSD: 1
|
||||||
└─ Apache-2.0: 1
|
└─ Apache-2.0: 1
|
||||||
|
|
@@ -4,13 +4,17 @@
|
|||||||
"file": "Datei",
|
"file": "Datei",
|
||||||
"advanced": "erweitert",
|
"advanced": "erweitert",
|
||||||
"create": "erstellen",
|
"create": "erstellen",
|
||||||
"loading": "Läd...",
|
"loading": "läd",
|
||||||
"mode": "Modus",
|
"mode": "Modus",
|
||||||
"views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}",
|
"views": "{n, plural, =0 {Ansichten} =1 {1 Ansicht} other {# Ansichten}}",
|
||||||
"minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}",
|
"minutes": "{n, plural, =0 {Minuten} =1 {1 Minute} other {# Minuten}}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
"share_link": "Link teilen",
|
"share_link": "Link teilen",
|
||||||
"copy_clipboard": "in die Zwischenablage kopieren"
|
"copy_clipboard": "in die Zwischenablage kopieren",
|
||||||
|
"encrypting": "verschlüsseln",
|
||||||
|
"decrypting": "entschlüsselt",
|
||||||
|
"uploading": "hochladen",
|
||||||
|
"downloading": "wird heruntergeladen"
|
||||||
},
|
},
|
||||||
"home": {
|
"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.",
|
"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.",
|
||||||
@@ -28,7 +32,8 @@
|
|||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "wurde nicht gefunden oder wurde bereits gelöscht.",
|
"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."
|
"decryption_failed": "falsches Passwort. konnte nicht entziffert werden. wahrscheinlich ein defekter Link. Notiz wurde zerstört.",
|
||||||
|
"unsupported_type": "nicht unterstützter Notiztyp."
|
||||||
},
|
},
|
||||||
"explanation": "Klicken Sie unten, um die Notiz anzuzeigen und zu löschen, wenn der Zähler sein Limit erreicht hat",
|
"explanation": "Klicken Sie unten, um die Notiz anzuzeigen und zu löschen, wenn der Zähler sein Limit erreicht hat",
|
||||||
"show_note": "Notiz anzeigen",
|
"show_note": "Notiz anzeigen",
|
||||||
|
@@ -4,13 +4,17 @@
|
|||||||
"file": "file",
|
"file": "file",
|
||||||
"advanced": "advanced",
|
"advanced": "advanced",
|
||||||
"create": "create",
|
"create": "create",
|
||||||
"loading": "loading...",
|
"loading": "loading",
|
||||||
"mode": "mode",
|
"mode": "mode",
|
||||||
"views": "{n, plural, =0 {views} =1 {1 view} other {# views}}",
|
"views": "{n, plural, =0 {views} =1 {1 view} other {# views}}",
|
||||||
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
"share_link": "share link",
|
"share_link": "share link",
|
||||||
"copy_clipboard": "copy to clipboard"
|
"copy_clipboard": "copy to clipboard",
|
||||||
|
"encrypting": "encrypting",
|
||||||
|
"decrypting": "decrypting",
|
||||||
|
"uploading": "uploading",
|
||||||
|
"downloading": "downloading"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
|
"intro": "Easily send <i>fully encrypted</i>, secure notes or files with one click. Just create a note and share the link.",
|
||||||
@@ -28,7 +32,8 @@
|
|||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "note was not found or was already deleted.",
|
"not_found": "note was not found or was already deleted.",
|
||||||
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed."
|
"decryption_failed": "wrong password. could not decipher. probably a broken link. note was destroyed.",
|
||||||
|
"unsupported_type": "unsupported note type."
|
||||||
},
|
},
|
||||||
"explanation": "click below to show and delete the note if the counter has reached it's limit",
|
"explanation": "click below to show and delete the note if the counter has reached it's limit",
|
||||||
"show_note": "show note",
|
"show_note": "show note",
|
||||||
|
@@ -4,13 +4,17 @@
|
|||||||
"file": "archivo",
|
"file": "archivo",
|
||||||
"advanced": "avanzado",
|
"advanced": "avanzado",
|
||||||
"create": "crear",
|
"create": "crear",
|
||||||
"loading": "cargando...",
|
"loading": "cargando",
|
||||||
"mode": "modo",
|
"mode": "modo",
|
||||||
"views": "{n, plural, =0 {vistas} =1 {1 vista} other {# vistas}}",
|
"views": "{n, plural, =0 {vistas} =1 {1 vista} other {# vistas}}",
|
||||||
"minutes": "{n, plural, =0 {minutos} =1 {1 minuto} other {# minutos}}",
|
"minutes": "{n, plural, =0 {minutos} =1 {1 minuto} other {# minutos}}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
"share_link": "compartir enlace",
|
"share_link": "compartir enlace",
|
||||||
"copy_clipboard": "copiar al portapapeles"
|
"copy_clipboard": "copiar al portapapeles",
|
||||||
|
"encrypting": "encriptando",
|
||||||
|
"decrypting": "descifrando",
|
||||||
|
"uploading": "cargando",
|
||||||
|
"downloading": "descargando"
|
||||||
},
|
},
|
||||||
"home": {
|
"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.",
|
"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.",
|
||||||
@@ -28,7 +32,8 @@
|
|||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "la nota no se encontró o ya fue borrada.",
|
"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."
|
"decryption_failed": "contraseña incorrecta. no se pudo descifrar. probablemente un enlace roto. la nota fue destruida.",
|
||||||
|
"unsupported_type": "tipo de nota no compatible."
|
||||||
},
|
},
|
||||||
"explanation": "pulse abajo para mostrar y borrar la nota si el contador ha llegado a su límite",
|
"explanation": "pulse abajo para mostrar y borrar la nota si el contador ha llegado a su límite",
|
||||||
"show_note": "mostrar nota",
|
"show_note": "mostrar nota",
|
||||||
|
@@ -4,13 +4,17 @@
|
|||||||
"file": "fichier",
|
"file": "fichier",
|
||||||
"advanced": "avancé",
|
"advanced": "avancé",
|
||||||
"create": "créer",
|
"create": "créer",
|
||||||
"loading": "chargement...",
|
"loading": "chargement",
|
||||||
"mode": "mode",
|
"mode": "mode",
|
||||||
"views": "{n, plural, =0 {vues} =1 {1 vue} other {# vues}}",
|
"views": "{n, plural, =0 {vues} =1 {1 vue} other {# vues}}",
|
||||||
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
"minutes": "{n, plural, =0 {minutes} =1 {1 minute} other {# minutes}}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
"share_link": "partager le lien",
|
"share_link": "partager le lien",
|
||||||
"copy_clipboard": "copier dans le presse-papiers"
|
"copy_clipboard": "copier dans le presse-papiers",
|
||||||
|
"encrypting": "cryptage",
|
||||||
|
"decrypting": "déchiffrer",
|
||||||
|
"uploading": "téléchargement",
|
||||||
|
"downloading": "téléchargement"
|
||||||
},
|
},
|
||||||
"home": {
|
"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.",
|
"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.",
|
||||||
@@ -28,7 +32,8 @@
|
|||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "La note n'a pas été trouvée ou a déjà été supprimée.",
|
"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."
|
"decryption_failed": "mauvais mot de passe. impossible à déchiffrer. probablement un lien brisé. la note a été détruite.",
|
||||||
|
"unsupported_type": "type de note non supporté."
|
||||||
},
|
},
|
||||||
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
|
"explanation": "Cliquez ci-dessous pour afficher et supprimer la note si le compteur a atteint sa limite.",
|
||||||
"show_note": "note de présentation",
|
"show_note": "note de présentation",
|
||||||
|
@@ -4,13 +4,17 @@
|
|||||||
"file": "file",
|
"file": "file",
|
||||||
"advanced": "avanzato",
|
"advanced": "avanzato",
|
||||||
"create": "crea",
|
"create": "crea",
|
||||||
"loading": "carica...",
|
"loading": "carica",
|
||||||
"mode": "modalita",
|
"mode": "modalita",
|
||||||
"views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}",
|
"views": "{n, plural, =0 {viste} =1 {1 vista} other {# viste}}",
|
||||||
"minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}",
|
"minutes": "{n, plural, =0 {minuti} =1 {1 minuto} other {# minuti}}",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
"share_link": "condividi link",
|
"share_link": "condividi link",
|
||||||
"copy_clipboard": "copia negli appunti"
|
"copy_clipboard": "copia negli appunti",
|
||||||
|
"encrypting": "criptando",
|
||||||
|
"decrypting": "decifrando",
|
||||||
|
"uploading": "caricamento",
|
||||||
|
"downloading": "scaricando"
|
||||||
},
|
},
|
||||||
"home": {
|
"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.",
|
"intro": "Invia facilmente note o file <i>completamente criptati</i> e sicuri con un solo clic. Basta creare una nota e condividere il link.",
|
||||||
@@ -28,7 +32,8 @@
|
|||||||
"show": {
|
"show": {
|
||||||
"errors": {
|
"errors": {
|
||||||
"not_found": "non è stata trovata o è stata già cancellata.",
|
"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."
|
"decryption_failed": "password sbagliata. non ha potuto decifrare. probabilmente un link rotto. la nota è stata distrutta.",
|
||||||
|
"unsupported_type": "tipo di nota non supportato."
|
||||||
},
|
},
|
||||||
"explanation": "clicca sotto per mostrare e cancellare la nota se il contatore ha raggiunto il suo limite",
|
"explanation": "clicca sotto per mostrare e cancellare la nota se il contatore ha raggiunto il suo limite",
|
||||||
"show_note": "mostra la nota",
|
"show_note": "mostra la nota",
|
||||||
|
47
frontend/locales/zh_CN.json
Normal file
47
frontend/locales/zh_CN.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"note": "密信",
|
||||||
|
"file": "上传文件",
|
||||||
|
"advanced": "高级设置",
|
||||||
|
"create": "创建",
|
||||||
|
"loading": "加载中",
|
||||||
|
"mode": "模式",
|
||||||
|
"views": "{n, plural, =0 {可查看次数} =1 {1 次查看} other {# 次查看}}",
|
||||||
|
"minutes": "{n, plural, =0 {有效期(分钟)} =1 {1 分钟} other {# 分钟}}",
|
||||||
|
"max": "最大值",
|
||||||
|
"share_link": "分享链接",
|
||||||
|
"copy_clipboard": "复制到剪切版",
|
||||||
|
"encrypting": "加密",
|
||||||
|
"decrypting": "解密",
|
||||||
|
"uploading": "上传",
|
||||||
|
"downloading": "下载"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"intro": "一键轻松发送 <i>完全加密的</i> 密信或者文件。只需创建一个密信然后分享链接。",
|
||||||
|
"explanation": "该密信会在{type}后失效。",
|
||||||
|
"new_note": "新建密信",
|
||||||
|
"new_note_notice": "<b>可用性警示:</b><br />由于加密鸽的所有数据是全部保存在内存中的,所以如果加密鸽的可用内存被用光了那么它将会删除最早的密信以释放内存,因此不保证该密信的可用性。<br />(一般情况下是您应该是不会遇到这个问题,只是警示一下。)",
|
||||||
|
"errors": {
|
||||||
|
"note_to_big": "无法创建密信,这个密信太大了!",
|
||||||
|
"note_error": "无法创建密信,请再试一遍。",
|
||||||
|
"max": "最大文件大小: {n}",
|
||||||
|
"empty_content": "密信为空!"
|
||||||
|
},
|
||||||
|
"copied_to_clipboard": "已复制到剪切板 🔗"
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"errors": {
|
||||||
|
"not_found": "该密信无法被找到或者它已经被删除了!",
|
||||||
|
"decryption_failed": "密钥错误!您可能不小心粘贴了一个不完整的链接或者正在尝试破解该密信!但无论如何,该密信已被销毁!",
|
||||||
|
"unsupported_type": "不支持的票据类型。"
|
||||||
|
},
|
||||||
|
"explanation": "点击下方的按钮可以查看密信,如果它到达了限制将会被删除",
|
||||||
|
"show_note": "查看密信",
|
||||||
|
"warning_will_not_see_again": "您将<b>无法</b>再次查看该密信",
|
||||||
|
"download_all": "下载全部"
|
||||||
|
},
|
||||||
|
"file_upload": {
|
||||||
|
"selected_files": "已选中的文件",
|
||||||
|
"no_files_selected": "没有文件被选中"
|
||||||
|
}
|
||||||
|
}
|
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "svelte-kit dev",
|
"dev": "vite dev",
|
||||||
"build": "svelte-kit build",
|
"build": "vite build",
|
||||||
"preview": "svelte-kit preview",
|
"preview": "svelte-kit preview",
|
||||||
"check": "svelte-check --tsconfig tsconfig.json",
|
"check": "svelte-check --tsconfig tsconfig.json",
|
||||||
"licenses": "license-checker --summary > licenses.csv",
|
"licenses": "license-checker --summary > licenses.csv",
|
||||||
@@ -10,23 +10,25 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lokalise/node-api": "^7.1.1",
|
"@lokalise/node-api": "^7.3.1",
|
||||||
"@sveltejs/adapter-static": "^1.0.0-next.28",
|
"@sveltejs/adapter-static": "^1.0.0-next.37",
|
||||||
"@sveltejs/kit": "1.0.0-next.288",
|
"@sveltejs/kit": "^1.0.0-next.377",
|
||||||
|
"@types/dompurify": "^2.3.3",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.5",
|
||||||
"adm-zip": "^0.5.9",
|
"adm-zip": "^0.5.9",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.1",
|
||||||
"svelte": "^3.46.4",
|
"svelte": "^3.49.0",
|
||||||
"svelte-check": "^2.4.5",
|
"svelte-check": "^2.8.0",
|
||||||
"svelte-intl-precompile": "^0.8.1",
|
"svelte-intl-precompile": "^0.10.1",
|
||||||
"svelte-preprocess": "^4.10.4",
|
"svelte-preprocess": "^4.10.7",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.4.0",
|
||||||
"typescript": "^4.6.2",
|
"typescript": "^4.7.4",
|
||||||
"vite": "^2.8.6"
|
"vite": "^3.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/fira-mono": "^4.5.3",
|
"@fontsource/fira-mono": "^4.5.8",
|
||||||
"copy-to-clipboard": "^3.3.1",
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
"dompurify": "^2.3.9",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"pretty-bytes": "^5.6.0"
|
"pretty-bytes": "^5.6.0"
|
||||||
}
|
}
|
||||||
|
818
frontend/pnpm-lock.yaml
generated
818
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,9 @@
|
|||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
%svelte.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="svelte">%svelte.body%</div>
|
<div id="svelte">%sveltekit.body%</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
61
frontend/src/lib/adapters.ts
Normal file
61
frontend/src/lib/adapters.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type { EncryptedFileDTO, FileDTO } from './api'
|
||||||
|
import { Crypto } from './crypto'
|
||||||
|
|
||||||
|
abstract class CryptAdapter<T> {
|
||||||
|
abstract encrypt(plaintext: T, key: CryptoKey): Promise<string>
|
||||||
|
abstract decrypt(ciphertext: string, key: CryptoKey): Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
class CryptTextAdapter implements CryptAdapter<string> {
|
||||||
|
async encrypt(plaintext: string, key: CryptoKey) {
|
||||||
|
return await Crypto.encrypt(new TextEncoder().encode(plaintext), key)
|
||||||
|
}
|
||||||
|
async decrypt(ciphertext: string, key: CryptoKey) {
|
||||||
|
const plaintext = await Crypto.decrypt(ciphertext, key)
|
||||||
|
return new TextDecoder().decode(plaintext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CryptBlobAdapter implements CryptAdapter<Blob> {
|
||||||
|
async encrypt(plaintext: Blob, key: CryptoKey) {
|
||||||
|
return await Crypto.encrypt(await plaintext.arrayBuffer(), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(ciphertext: string, key: CryptoKey) {
|
||||||
|
const plaintext = await Crypto.decrypt(ciphertext, key)
|
||||||
|
return new Blob([plaintext], { type: 'application/octet-stream' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CryptFilesAdapter implements CryptAdapter<FileDTO[]> {
|
||||||
|
async encrypt(plaintext: FileDTO[], key: CryptoKey) {
|
||||||
|
const adapter = new CryptBlobAdapter()
|
||||||
|
const data: Promise<EncryptedFileDTO>[] = plaintext.map(async (file) => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
contents: await adapter.encrypt(file.contents, key),
|
||||||
|
}))
|
||||||
|
return JSON.stringify(await Promise.all(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(ciphertext: string, key: CryptoKey) {
|
||||||
|
const adapter = new CryptBlobAdapter()
|
||||||
|
const data: EncryptedFileDTO[] = JSON.parse(ciphertext)
|
||||||
|
const files: FileDTO[] = await Promise.all(
|
||||||
|
data.map(async (file) => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
contents: await adapter.decrypt(file.contents, key),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Adapters = {
|
||||||
|
Text: new CryptTextAdapter(),
|
||||||
|
Blob: new CryptBlobAdapter(),
|
||||||
|
Files: new CryptFilesAdapter(),
|
||||||
|
}
|
@@ -11,6 +11,10 @@ export type NotePublic = Pick<Note, 'contents' | 'meta'>
|
|||||||
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
export type NoteCreate = Omit<Note, 'meta'> & { meta: string }
|
||||||
|
|
||||||
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
|
export type FileDTO = Pick<File, 'name' | 'size' | 'type'> & {
|
||||||
|
contents: Blob
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EncryptedFileDTO = Omit<FileDTO, 'contents'> & {
|
||||||
contents: string
|
contents: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,13 +19,31 @@ export class Hex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALG = 'AES-GCM'
|
export class ArrayBufferUtils {
|
||||||
|
static async toString(buffer: ArrayBuffer): Promise<string> {
|
||||||
|
const reader = new window.FileReader()
|
||||||
|
reader.readAsDataURL(new Blob([buffer]))
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
reader.onloadend = () => resolve(reader.result as string)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function getRandomBytes(size = 16): Uint8Array {
|
static async fromString(s: string): Promise<ArrayBuffer> {
|
||||||
|
return fetch(s)
|
||||||
|
.then((r) => r.blob())
|
||||||
|
.then((b) => b.arrayBuffer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Crypto {
|
||||||
|
private static ALG = 'AES-GCM'
|
||||||
|
private static DELIMITER = ':::'
|
||||||
|
|
||||||
|
public static getRandomBytes(size: number): Uint8Array {
|
||||||
return window.crypto.getRandomValues(new Uint8Array(size))
|
return window.crypto.getRandomValues(new Uint8Array(size))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getKeyFromString(password: string) {
|
public static getKeyFromString(password: string) {
|
||||||
return window.crypto.subtle.importKey(
|
return window.crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
new TextEncoder().encode(password),
|
new TextEncoder().encode(password),
|
||||||
@@ -34,8 +52,7 @@ export function getKeyFromString(password: string) {
|
|||||||
['deriveBits', 'deriveKey']
|
['deriveBits', 'deriveKey']
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
public static async getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
|
||||||
export async function getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
|
|
||||||
const iterations = 100_000
|
const iterations = 100_000
|
||||||
return window.crypto.subtle.deriveKey(
|
return window.crypto.subtle.deriveKey(
|
||||||
{
|
{
|
||||||
@@ -45,27 +62,36 @@ export async function getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) {
|
|||||||
hash: 'SHA-512',
|
hash: 'SHA-512',
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
{ name: ALG, length: 256 },
|
{ name: this.ALG, length: 256 },
|
||||||
true,
|
true,
|
||||||
['encrypt', 'decrypt']
|
['encrypt', 'decrypt']
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function encrypt(plaintext: string, key: CryptoKey) {
|
public static async encrypt(plaintext: ArrayBuffer, key: CryptoKey): Promise<string> {
|
||||||
const salt = getRandomBytes(16)
|
const salt = this.getRandomBytes(16)
|
||||||
const derived = await getDerivedForKey(key, salt)
|
const derived = await this.getDerivedForKey(key, salt)
|
||||||
const iv = getRandomBytes(16)
|
const iv = this.getRandomBytes(16)
|
||||||
const encrypted = await window.crypto.subtle.encrypt(
|
const encrypted: ArrayBuffer = await window.crypto.subtle.encrypt(
|
||||||
{ name: ALG, iv },
|
{ name: this.ALG, iv },
|
||||||
derived,
|
derived,
|
||||||
new TextEncoder().encode(plaintext)
|
plaintext
|
||||||
)
|
)
|
||||||
return [salt, iv, encrypted].map(Hex.encode).join(':')
|
const data = [
|
||||||
|
Hex.encode(salt),
|
||||||
|
Hex.encode(iv),
|
||||||
|
await ArrayBufferUtils.toString(encrypted),
|
||||||
|
].join(this.DELIMITER)
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decrypt(ciphertext: string, key: CryptoKey) {
|
public static async decrypt(ciphertext: string, key: CryptoKey): Promise<ArrayBuffer> {
|
||||||
const [salt, iv, encrypted] = ciphertext.split(':').map(Hex.decode)
|
const splitted = ciphertext.split(this.DELIMITER)
|
||||||
const derived = await getDerivedForKey(key, salt)
|
const salt = Hex.decode(splitted[0])
|
||||||
const plaintext = await window.crypto.subtle.decrypt({ name: ALG, iv }, derived, encrypted)
|
const iv = Hex.decode(splitted[1])
|
||||||
return new TextDecoder().decode(plaintext)
|
const encrypted = await ArrayBufferUtils.fromString(splitted[2])
|
||||||
|
const derived = await this.getDerivedForKey(key, salt)
|
||||||
|
const plaintext = await window.crypto.subtle.decrypt({ name: this.ALG, iv }, derived, encrypted)
|
||||||
|
return plaintext
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
export class Files {
|
|
||||||
static toString(f: File | Blob): Promise<string> {
|
|
||||||
const reader = new window.FileReader()
|
|
||||||
reader.readAsDataURL(f)
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
reader.onloadend = () => resolve(reader.result as string)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static async fromString(s: string): Promise<Blob> {
|
|
||||||
return fetch(s).then((r) => r.blob())
|
|
||||||
}
|
|
||||||
}
|
|
@@ -7,6 +7,8 @@ export type Status = {
|
|||||||
max_views: number
|
max_views: number
|
||||||
max_expiration: number
|
max_expiration: number
|
||||||
allow_advanced: boolean
|
allow_advanced: boolean
|
||||||
|
theme_image: string
|
||||||
|
theme_text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const status = writable<null | Status>(null)
|
export const status = writable<null | Status>(null)
|
||||||
|
47
frontend/src/lib/ui/AdvancedParameters.svelte
Normal file
47
frontend/src/lib/ui/AdvancedParameters.svelte
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
|
import type { Note } from '$lib/api'
|
||||||
|
import { status } from '$lib/stores/status'
|
||||||
|
import Switch from '$lib/ui/Switch.svelte'
|
||||||
|
import TextInput from '$lib/ui/TextInput.svelte'
|
||||||
|
|
||||||
|
export let note: Note
|
||||||
|
export let timeExpiration = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="fields">
|
||||||
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
label={$t('common.views', { values: { n: 0 } })}
|
||||||
|
bind:value={note.views}
|
||||||
|
disabled={timeExpiration}
|
||||||
|
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">
|
||||||
|
<Switch label={$t('common.mode')} bind:value={timeExpiration} color={false} />
|
||||||
|
</div>
|
||||||
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
label={$t('common.minutes', { values: { n: 0 } })}
|
||||||
|
bind:value={note.expiration}
|
||||||
|
disabled={!timeExpiration}
|
||||||
|
max={$status?.max_expiration}
|
||||||
|
validate={(v) =>
|
||||||
|
($status && v < $status?.max_expiration) ||
|
||||||
|
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.middle-switch {
|
||||||
|
margin: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,38 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FileDTO } from '$lib/api'
|
|
||||||
import { Files } from '$lib/files'
|
|
||||||
import { createEventDispatcher } from 'svelte'
|
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
import Button from './Button.svelte'
|
|
||||||
import MaxSize from './MaxSize.svelte'
|
import type { FileDTO } from '$lib/api'
|
||||||
|
import Button from '$lib/ui/Button.svelte'
|
||||||
|
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||||
|
|
||||||
export let label: string = ''
|
export let label: string = ''
|
||||||
let files: File[] = []
|
export let files: FileDTO[] = []
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ file: string }>()
|
function fileToDTO(file: File): FileDTO {
|
||||||
|
return {
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
contents: file,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 = [...files, ...Array.from(input.files)]
|
files = [...files, ...Array.from(input.files).map(fileToDTO)]
|
||||||
const data: FileDTO[] = await Promise.all(
|
|
||||||
files.map(async (file) => ({
|
|
||||||
name: file.name,
|
|
||||||
type: file.type,
|
|
||||||
size: file.size,
|
|
||||||
contents: await Files.toString(file),
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
dispatch('file', JSON.stringify(data))
|
|
||||||
} else {
|
|
||||||
dispatch('file', '')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear(e: Event) {
|
function clear(e: Event) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
files = []
|
files = []
|
||||||
dispatch('file', '')
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -57,7 +51,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<b>{$t('file_upload.no_files_selected')}</b>
|
<b>{$t('file_upload.no_files_selected')}</b>
|
||||||
<br />
|
<br />
|
||||||
<small>{$t('common.max')}: <MaxSize /></small>
|
<small>
|
||||||
|
{$t('common.max')}: <MaxSize />
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
41
frontend/src/lib/ui/Loader.svelte
Normal file
41
frontend/src/lib/ui/Loader.svelte
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
xml:space="preserve"
|
||||||
|
>
|
||||||
|
<rect fill="none" stroke="currentColor" stroke-width="4" x="25" y="25" width="50" height="50">
|
||||||
|
<animateTransform
|
||||||
|
attributeName="transform"
|
||||||
|
dur="0.5s"
|
||||||
|
from="0 50 50"
|
||||||
|
to="180 50 50"
|
||||||
|
type="rotate"
|
||||||
|
id="strokeBox"
|
||||||
|
attributeType="XML"
|
||||||
|
begin="rectBox.end"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
<rect x="27" y="27" fill="currentColor" width="46" height="50">
|
||||||
|
<animate
|
||||||
|
attributeName="height"
|
||||||
|
dur="1.3s"
|
||||||
|
attributeType="XML"
|
||||||
|
from="50"
|
||||||
|
to="0"
|
||||||
|
id="rectBox"
|
||||||
|
fill="freeze"
|
||||||
|
begin="0s;strokeBox.end"
|
||||||
|
/>
|
||||||
|
</rect>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
svg {
|
||||||
|
height: 2em;
|
||||||
|
position: relative;
|
||||||
|
top: 0.6em;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
After Width: | Height: | Size: 784 B |
@@ -1,12 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { status } from '$lib/stores/status'
|
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
import { _ } from 'svelte-intl-precompile'
|
import { _ } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
|
import { status } from '$lib/stores/status'
|
||||||
|
|
||||||
|
// Due to encoding overhead (~35%) with base64
|
||||||
|
// https://en.wikipedia.org/wiki/Base64
|
||||||
|
const overhead = 1 / 1.35
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{#if $status !== null}
|
{#if $status !== null}
|
||||||
{prettyBytes($status.max_size, { binary: true })}
|
{prettyBytes($status.max_size * overhead, { binary: true })}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('common.loading')}
|
{$_('common.loading')}
|
||||||
{/if}
|
{/if}
|
||||||
|
36
frontend/src/lib/ui/NoteResult.svelte
Normal file
36
frontend/src/lib/ui/NoteResult.svelte
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
export type NoteResult = {
|
||||||
|
password: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
|
import Button from '$lib/ui/Button.svelte'
|
||||||
|
import TextInput from '$lib/ui/TextInput.svelte'
|
||||||
|
|
||||||
|
export let result: NoteResult
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
readonly
|
||||||
|
label={$t('common.share_link')}
|
||||||
|
value="{window.location.origin}/note/{result.id}#{result.password}"
|
||||||
|
copy
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{@html $t('home.new_note_notice')}
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
@@ -1,18 +1,24 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
export type DecryptedNote = Omit<NotePublic, 'contents'> & { contents: any }
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FileDTO, NotePublic } from '$lib/api'
|
|
||||||
import { Files } from '$lib/files'
|
|
||||||
import copy from 'copy-to-clipboard'
|
import copy from 'copy-to-clipboard'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
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 { t } from 'svelte-intl-precompile'
|
||||||
import Button from './Button.svelte'
|
|
||||||
|
|
||||||
export let note: NotePublic
|
import type { FileDTO, NotePublic } from '$lib/api'
|
||||||
|
import Button from '$lib/ui/Button.svelte'
|
||||||
|
|
||||||
|
export let note: DecryptedNote
|
||||||
|
|
||||||
|
const RE_URL = /[A-Za-z]+:\/\/([A-Z a-z0-9\-._~:\/?#\[\]@!$&'()*+,;%=])+/g
|
||||||
let files: FileDTO[] = []
|
let files: FileDTO[] = []
|
||||||
|
|
||||||
$: if (note.meta.type === 'file') {
|
$: if (note.meta.type === 'file') {
|
||||||
files = JSON.parse(note.contents) as FileDTO[]
|
files = note.contents
|
||||||
}
|
}
|
||||||
|
|
||||||
$: download = () => {
|
$: download = () => {
|
||||||
@@ -22,17 +28,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function downloadFile(file: FileDTO) {
|
async function downloadFile(file: FileDTO) {
|
||||||
const f = new File([await Files.fromString(file.contents)], file.name, {
|
const f = new File([file.contents], file.name, {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
})
|
})
|
||||||
saveAs(f)
|
saveAs(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function contentWithLinks(content: string): string {
|
||||||
|
const replaced = content.replace(
|
||||||
|
RE_URL,
|
||||||
|
(url) => `<a href="${url}" rel="noreferrer">${url}</a>`
|
||||||
|
)
|
||||||
|
return DOMPurify.sanitize(replaced, { USE_PROFILES: { html: true } })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p class="error-text">{@html $t('show.warning_will_not_see_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">
|
<div class="note">
|
||||||
{note.contents}
|
{@html contentWithLinks(note.contents)}
|
||||||
</div>
|
</div>
|
||||||
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
|
<Button on:click={() => copy(note.contents)}>{$t('common.copy_clipboard')}</Button>
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 { t } from 'svelte-intl-precompile'
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from 'svelte/transition'
|
||||||
import Icon from './Icon.svelte'
|
|
||||||
|
import { Crypto, Hex } from '$lib/crypto'
|
||||||
|
import Icon from '$lib/ui/Icon.svelte'
|
||||||
|
|
||||||
export let label: string = ''
|
export let label: string = ''
|
||||||
export let value: any
|
export let value: any
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
notify($t('home.copied_to_clipboard'))
|
notify($t('home.copied_to_clipboard'))
|
||||||
}
|
}
|
||||||
function randomFN() {
|
function randomFN() {
|
||||||
value = Hex.encode(getRandomBytes(20))
|
value = Hex.encode(Crypto.getRandomBytes(20))
|
||||||
}
|
}
|
||||||
|
|
||||||
function notify(msg: string, delay: number = 2000) {
|
function notify(msg: string, delay: number = 2000) {
|
||||||
|
@@ -1,28 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { create, Note, PayloadToLargeError } from '$lib/api'
|
|
||||||
import { encrypt, getKeyFromString, getRandomBytes, Hex } from '$lib/crypto'
|
|
||||||
import { status } from '$lib/stores/status'
|
|
||||||
import Button from '$lib/ui/Button.svelte'
|
|
||||||
import FileUpload from '$lib/ui/FileUpload.svelte'
|
|
||||||
import MaxSize from '$lib/ui/MaxSize.svelte'
|
|
||||||
import Switch from '$lib/ui/Switch.svelte'
|
|
||||||
import TextArea from '$lib/ui/TextArea.svelte'
|
|
||||||
import TextInput from '$lib/ui/TextInput.svelte'
|
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
import { blur } from 'svelte/transition'
|
import { blur } from 'svelte/transition'
|
||||||
|
|
||||||
|
import { Adapters } from '$lib/adapters'
|
||||||
|
import type { FileDTO, Note } from '$lib/api'
|
||||||
|
import { create, PayloadToLargeError } from '$lib/api'
|
||||||
|
import { Crypto, Hex } from '$lib/crypto'
|
||||||
|
import { status } from '$lib/stores/status'
|
||||||
|
import AdvancedParameters from '$lib/ui/AdvancedParameters.svelte'
|
||||||
|
import Button from '$lib/ui/Button.svelte'
|
||||||
|
import FileUpload from '$lib/ui/FileUpload.svelte'
|
||||||
|
import Loader from '$lib/ui/Loader.svelte'
|
||||||
|
import MaxSize from '$lib/ui/MaxSize.svelte'
|
||||||
|
import Result, { type NoteResult } from '$lib/ui/NoteResult.svelte'
|
||||||
|
import Switch from '$lib/ui/Switch.svelte'
|
||||||
|
import TextArea from '$lib/ui/TextArea.svelte'
|
||||||
|
|
||||||
let note: Note = {
|
let note: Note = {
|
||||||
contents: '',
|
contents: '',
|
||||||
meta: { type: 'text' },
|
meta: { type: 'text' },
|
||||||
views: 1,
|
views: 1,
|
||||||
expiration: 60,
|
expiration: 60,
|
||||||
}
|
}
|
||||||
let result: { password: string; id: string } | null = null
|
let files: FileDTO[]
|
||||||
|
let result: NoteResult | null = null
|
||||||
let advanced = false
|
let advanced = false
|
||||||
let file = false
|
let isFile = false
|
||||||
let timeExpiration = false
|
let timeExpiration = false
|
||||||
let message = ''
|
let description = ''
|
||||||
let loading = false
|
let loading: string | null = null
|
||||||
let error: string | null = null
|
let error: string | null = null
|
||||||
|
|
||||||
$: if (!advanced) {
|
$: if (!advanced) {
|
||||||
@@ -31,7 +37,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
message = $t('home.explanation', {
|
description = $t('home.explanation', {
|
||||||
values: {
|
values: {
|
||||||
type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
|
type: $t(timeExpiration ? 'common.minutes' : 'common.views', {
|
||||||
values: { n: (timeExpiration ? note.expiration : note.views) ?? '?' },
|
values: { n: (timeExpiration ? note.expiration : note.views) ?? '?' },
|
||||||
@@ -40,9 +46,9 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$: note.meta.type = file ? 'file' : 'text'
|
$: note.meta.type = isFile ? 'file' : 'text'
|
||||||
|
|
||||||
$: if (!file) {
|
$: if (!isFile) {
|
||||||
note.contents = ''
|
note.contents = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,17 +57,26 @@
|
|||||||
async function submit() {
|
async function submit() {
|
||||||
try {
|
try {
|
||||||
error = null
|
error = null
|
||||||
loading = true
|
loading = $t('common.encrypting')
|
||||||
const password = Hex.encode(getRandomBytes(32))
|
|
||||||
const key = await getKeyFromString(password)
|
const password = Hex.encode(Crypto.getRandomBytes(32))
|
||||||
if (note.contents === '') throw new EmptyContentError()
|
const key = await Crypto.getKeyFromString(password)
|
||||||
|
|
||||||
const data: Note = {
|
const data: Note = {
|
||||||
contents: await encrypt(note.contents, key),
|
contents: '',
|
||||||
meta: note.meta,
|
meta: note.meta,
|
||||||
}
|
}
|
||||||
|
if (isFile) {
|
||||||
|
if (files.length === 0) throw new EmptyContentError()
|
||||||
|
data.contents = await Adapters.Files.encrypt(files, key)
|
||||||
|
} else {
|
||||||
|
if (note.contents === '') throw new EmptyContentError()
|
||||||
|
data.contents = await Adapters.Text.encrypt(note.contents, key)
|
||||||
|
}
|
||||||
if (timeExpiration) data.expiration = parseInt(note.expiration as any)
|
if (timeExpiration) data.expiration = parseInt(note.expiration as any)
|
||||||
else data.views = parseInt(note.views as any)
|
else data.views = parseInt(note.views as any)
|
||||||
|
|
||||||
|
loading = $t('common.uploading')
|
||||||
const response = await create(data)
|
const response = await create(data)
|
||||||
result = {
|
result = {
|
||||||
password: password,
|
password: password,
|
||||||
@@ -73,46 +88,31 @@
|
|||||||
} else if (e instanceof EmptyContentError) {
|
} else if (e instanceof EmptyContentError) {
|
||||||
error = $t('home.errors.empty_content')
|
error = $t('home.errors.empty_content')
|
||||||
} else {
|
} else {
|
||||||
|
console.error(e)
|
||||||
error = $t('home.errors.note_error')
|
error = $t('home.errors.note_error')
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if result}
|
{#if result}
|
||||||
<TextInput
|
<Result {result} />
|
||||||
type="text"
|
|
||||||
readonly
|
|
||||||
label={$t('common.share_link')}
|
|
||||||
value="{window.location.origin}/note/{result.id}#{result.password}"
|
|
||||||
copy
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
{@html $t('home.new_note_notice')}
|
|
||||||
</p>
|
|
||||||
<br />
|
|
||||||
<Button on:click={reset}>{$t('home.new_note')}</Button>
|
|
||||||
{:else}
|
{:else}
|
||||||
<p>
|
<p>
|
||||||
{@html $t('home.intro')}
|
{@html $status?.theme_text || $t('home.intro')}
|
||||||
</p>
|
</p>
|
||||||
<form on:submit|preventDefault={submit}>
|
<form on:submit|preventDefault={submit}>
|
||||||
<fieldset disabled={loading}>
|
<fieldset disabled={loading !== null}>
|
||||||
{#if file}
|
{#if isFile}
|
||||||
<FileUpload label={$t('common.file')} on:file={(f) => (note.contents = f.detail)} />
|
<FileUpload label={$t('common.file')} bind:files />
|
||||||
{:else}
|
{:else}
|
||||||
<TextArea label={$t('common.note')} bind:value={note.contents} placeholder="..." />
|
<TextArea label={$t('common.note')} bind:value={note.contents} placeholder="..." />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<Switch class="file" label={$t('common.file')} bind:value={file} />
|
<Switch class="file" label={$t('common.file')} bind:value={isFile} />
|
||||||
{#if $status?.allow_advanced}
|
{#if $status?.allow_advanced}
|
||||||
<Switch label={$t('common.advanced')} bind:value={advanced} />
|
<Switch label={$t('common.advanced')} bind:value={advanced} />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -131,40 +131,16 @@
|
|||||||
<p>
|
<p>
|
||||||
<br />
|
<br />
|
||||||
{#if loading}
|
{#if loading}
|
||||||
{$t('common.loading')}
|
{loading} <Loader />
|
||||||
{:else}
|
{:else}
|
||||||
{message}
|
{description}
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if advanced}
|
{#if advanced}
|
||||||
<div transition:blur={{ duration: 250 }}>
|
<div transition:blur={{ duration: 250 }}>
|
||||||
<br />
|
<br />
|
||||||
<div class="fields">
|
<AdvancedParameters bind:note bind:timeExpiration />
|
||||||
<TextInput
|
|
||||||
type="number"
|
|
||||||
label={$t('common.views', { values: { n: 0 } })}
|
|
||||||
bind:value={note.views}
|
|
||||||
disabled={timeExpiration}
|
|
||||||
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">
|
|
||||||
<Switch label={$t('common.mode')} bind:value={timeExpiration} color={false} />
|
|
||||||
</div>
|
|
||||||
<TextInput
|
|
||||||
type="number"
|
|
||||||
label={$t('common.minutes', { values: { n: 0 } })}
|
|
||||||
bind:value={note.expiration}
|
|
||||||
disabled={!timeExpiration}
|
|
||||||
max={$status?.max_expiration}
|
|
||||||
validate={(v) =>
|
|
||||||
($status && v < $status?.max_expiration) ||
|
|
||||||
$t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -186,15 +162,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.middle-switch {
|
|
||||||
margin: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-text {
|
.error-text {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { status } from '$lib/stores/status'
|
||||||
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<a href="/">
|
<a href="/">
|
||||||
|
{#if $status?.theme_image}
|
||||||
|
<img alt="logo" src={$status.theme_image} />
|
||||||
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
@@ -10,7 +17,8 @@
|
|||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
|
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
|
||||||
><g id="Logo"
|
><g id="Logo"
|
||||||
><clipPath id="_clip1"><rect x="6.336" y="3.225" width="193.55" height="193.55" /></clipPath
|
><clipPath id="_clip1"
|
||||||
|
><rect x="6.336" y="3.225" width="193.55" height="193.55" /></clipPath
|
||||||
><g clip-path="url(#_clip1)"
|
><g clip-path="url(#_clip1)"
|
||||||
><g
|
><g
|
||||||
><g
|
><g
|
||||||
@@ -73,6 +81,7 @@
|
|||||||
></g
|
></g
|
||||||
></svg
|
></svg
|
||||||
>
|
>
|
||||||
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -87,20 +96,26 @@
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 30rem) {
|
@media screen and (max-width: 30rem) {
|
||||||
header {
|
header {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
header svg {
|
header svg,
|
||||||
|
header img {
|
||||||
max-height: 4rem;
|
max-height: 4rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header svg {
|
header svg,
|
||||||
|
header img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 16rem;
|
max-height: 8rem;
|
||||||
transform: translateX(-1rem);
|
transform: translateX(-1rem);
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import { init, waitLocale, getLocaleFromNavigator } from 'svelte-intl-precompile'
|
import { getLocaleFromNavigator, init, waitLocale } from 'svelte-intl-precompile'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { registerAll } from '$locales'
|
import { registerAll } from '$locales'
|
||||||
registerAll()
|
registerAll()
|
||||||
|
@@ -12,47 +12,67 @@
|
|||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { t } from 'svelte-intl-precompile'
|
import { t } from 'svelte-intl-precompile'
|
||||||
|
|
||||||
import type { NotePublic } from '$lib/api'
|
import { Adapters } from '$lib/adapters'
|
||||||
import { get, info } from '$lib/api'
|
import { get, info } from '$lib/api'
|
||||||
import { decrypt, getKeyFromString } from '$lib/crypto'
|
import { Crypto } 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 Loader from '$lib/ui/Loader.svelte'
|
||||||
|
import ShowNote, { type DecryptedNote } from '$lib/ui/ShowNote.svelte'
|
||||||
|
|
||||||
export let id: string
|
export let id: string
|
||||||
|
|
||||||
let password: string
|
let password: string
|
||||||
let note: NotePublic | null = null
|
let note: DecryptedNote | null = null
|
||||||
let exists = false
|
let exists = false
|
||||||
|
|
||||||
let loading = true
|
let loading: string | null = null
|
||||||
let error = false
|
let error: string | null = null
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
// Check if note exists
|
||||||
try {
|
try {
|
||||||
loading = true
|
loading = $t('common.loading')
|
||||||
error = false
|
|
||||||
password = window.location.hash.slice(1)
|
password = window.location.hash.slice(1)
|
||||||
await info(id)
|
await info(id)
|
||||||
exists = true
|
exists = true
|
||||||
} catch {
|
} catch {
|
||||||
exists = false
|
exists = false
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the actual contents of the note and decrypt it.
|
||||||
|
*/
|
||||||
async function show() {
|
async function show() {
|
||||||
try {
|
try {
|
||||||
error = false
|
error = null
|
||||||
loading = true
|
loading = $t('common.downloading')
|
||||||
const data = note || (await get(id)) // Don't get the content twice on wrong password.
|
const data = await get(id)
|
||||||
const key = await getKeyFromString(password)
|
loading = $t('common.decrypting')
|
||||||
data.contents = await decrypt(data.contents, key)
|
const key = await Crypto.getKeyFromString(password)
|
||||||
note = data
|
switch (data.meta.type) {
|
||||||
|
case 'text':
|
||||||
|
note = {
|
||||||
|
meta: { type: 'text' },
|
||||||
|
contents: await Adapters.Text.decrypt(data.contents, key),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'file':
|
||||||
|
note = {
|
||||||
|
meta: { type: 'file' },
|
||||||
|
contents: await Adapters.Files.decrypt(data.contents, key),
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
error = $t('show.errors.unsupported_type')
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
error = true
|
error = $t('show.errors.decryption_failed')
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -70,7 +90,7 @@
|
|||||||
{#if error}
|
{#if error}
|
||||||
<br />
|
<br />
|
||||||
<p class="error-text">
|
<p class="error-text">
|
||||||
{$t('show.errors.decryption_failed')}
|
{error}
|
||||||
<br />
|
<br />
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -79,5 +99,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<p>{$t('common.loading')}</p>
|
<p class="loader">{loading} <Loader /></p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.loader {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
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'
|
import preprocess from 'svelte-preprocess'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
preprocess: preprocess(),
|
preprocess: preprocess(),
|
||||||
@@ -9,10 +8,5 @@ export default {
|
|||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
fallback: 'index.html',
|
fallback: 'index.html',
|
||||||
}),
|
}),
|
||||||
vite: {
|
|
||||||
plugins: [
|
|
||||||
precompileIntl('locales'), // if your translations are defined in /locales/[lang].json
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -1,31 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"moduleResolution": "node",
|
"strict": true,
|
||||||
"module": "es2020",
|
"allowSyntheticDefaultImports": true
|
||||||
"lib": ["es2020"],
|
}
|
||||||
"target": "es2019",
|
|
||||||
/**
|
|
||||||
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
|
|
||||||
to enforce using \`import type\` instead of \`import\` for Types.
|
|
||||||
*/
|
|
||||||
"importsNotUsedAsValues": "error",
|
|
||||||
"isolatedModules": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
/**
|
|
||||||
To have warnings/errors of the Svelte compiler at the correct position,
|
|
||||||
enable source maps by default.
|
|
||||||
*/
|
|
||||||
"sourceMap": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"paths": {
|
|
||||||
"$lib/*": ["src/lib/*"]
|
|
||||||
},
|
|
||||||
"strict": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
|
|
||||||
}
|
}
|
||||||
|
12
frontend/vite.config.js
Normal file
12
frontend/vite.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite'
|
||||||
|
import precompileIntl from 'svelte-intl-precompile/sveltekit-plugin'
|
||||||
|
|
||||||
|
/** @type {import('vite').UserConfig} */
|
||||||
|
const config = {
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
},
|
||||||
|
plugins: [sveltekit(), precompileIntl('locales')],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:docker": "docker-compose up memcached",
|
"dev:docker": "docker-compose up redis",
|
||||||
"dev:backend": "cd backend && cargo watch -x 'run --bin cryptgeon'",
|
"dev:backend": "cd backend && cargo watch -x 'run --bin cryptgeon'",
|
||||||
"dev:front": "pnpm --prefix frontend run dev",
|
"dev:front": "pnpm --prefix frontend run dev",
|
||||||
"dev:proxy": "node proxy.mjs",
|
"dev:proxy": "node proxy.mjs",
|
||||||
|
197
pnpm-lock.yaml
generated
197
pnpm-lock.yaml
generated
@@ -1,4 +1,4 @@
|
|||||||
lockfileVersion: 5.3
|
lockfileVersion: 5.4
|
||||||
|
|
||||||
specifiers:
|
specifiers:
|
||||||
http-proxy: ^1.18.1
|
http-proxy: ^1.18.1
|
||||||
@@ -32,7 +32,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
|
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.1
|
function-bind: 1.1.1
|
||||||
get-intrinsic: 1.1.1
|
get-intrinsic: 1.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/chalk/2.4.2:
|
/chalk/2.4.2:
|
||||||
@@ -51,7 +51,7 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/color-name/1.1.3:
|
/color-name/1.1.3:
|
||||||
resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=}
|
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/concat-map/0.0.1:
|
/concat-map/0.0.1:
|
||||||
@@ -69,10 +69,11 @@ packages:
|
|||||||
which: 1.3.1
|
which: 1.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/define-properties/1.1.3:
|
/define-properties/1.1.4:
|
||||||
resolution: {integrity: sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==}
|
resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
has-property-descriptors: 1.0.0
|
||||||
object-keys: 1.1.1
|
object-keys: 1.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -82,30 +83,33 @@ packages:
|
|||||||
is-arrayish: 0.2.1
|
is-arrayish: 0.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/es-abstract/1.19.1:
|
/es-abstract/1.20.1:
|
||||||
resolution: {integrity: sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==}
|
resolution: {integrity: sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==}
|
||||||
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
|
function.prototype.name: 1.1.5
|
||||||
|
get-intrinsic: 1.1.2
|
||||||
get-symbol-description: 1.0.0
|
get-symbol-description: 1.0.0
|
||||||
has: 1.0.3
|
has: 1.0.3
|
||||||
has-symbols: 1.0.2
|
has-property-descriptors: 1.0.0
|
||||||
|
has-symbols: 1.0.3
|
||||||
internal-slot: 1.0.3
|
internal-slot: 1.0.3
|
||||||
is-callable: 1.2.4
|
is-callable: 1.2.4
|
||||||
is-negative-zero: 2.0.2
|
is-negative-zero: 2.0.2
|
||||||
is-regex: 1.1.4
|
is-regex: 1.1.4
|
||||||
is-shared-array-buffer: 1.0.1
|
is-shared-array-buffer: 1.0.2
|
||||||
is-string: 1.0.7
|
is-string: 1.0.7
|
||||||
is-weakref: 1.0.2
|
is-weakref: 1.0.2
|
||||||
object-inspect: 1.12.0
|
object-inspect: 1.12.2
|
||||||
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
|
regexp.prototype.flags: 1.4.3
|
||||||
string.prototype.trimstart: 1.0.4
|
string.prototype.trimend: 1.0.5
|
||||||
unbox-primitive: 1.0.1
|
string.prototype.trimstart: 1.0.5
|
||||||
|
unbox-primitive: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/es-to-primitive/1.2.1:
|
/es-to-primitive/1.2.1:
|
||||||
@@ -118,7 +122,7 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/escape-string-regexp/1.0.5:
|
/escape-string-regexp/1.0.5:
|
||||||
resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=}
|
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
||||||
engines: {node: '>=0.8.0'}
|
engines: {node: '>=0.8.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -126,8 +130,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/follow-redirects/1.14.7:
|
/follow-redirects/1.15.1:
|
||||||
resolution: {integrity: sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==}
|
resolution: {integrity: sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
debug: '*'
|
debug: '*'
|
||||||
@@ -140,12 +144,26 @@ packages:
|
|||||||
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/get-intrinsic/1.1.1:
|
/function.prototype.name/1.1.5:
|
||||||
resolution: {integrity: sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==}
|
resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.2
|
||||||
|
define-properties: 1.1.4
|
||||||
|
es-abstract: 1.20.1
|
||||||
|
functions-have-names: 1.2.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/functions-have-names/1.2.3:
|
||||||
|
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/get-intrinsic/1.1.2:
|
||||||
|
resolution: {integrity: sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.1
|
function-bind: 1.1.1
|
||||||
has: 1.0.3
|
has: 1.0.3
|
||||||
has-symbols: 1.0.2
|
has-symbols: 1.0.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/get-symbol-description/1.0.0:
|
/get-symbol-description/1.0.0:
|
||||||
@@ -153,24 +171,30 @@ packages:
|
|||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.2
|
call-bind: 1.0.2
|
||||||
get-intrinsic: 1.1.1
|
get-intrinsic: 1.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/graceful-fs/4.2.9:
|
/graceful-fs/4.2.10:
|
||||||
resolution: {integrity: sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==}
|
resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/has-bigints/1.0.1:
|
/has-bigints/1.0.2:
|
||||||
resolution: {integrity: sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==}
|
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/has-flag/3.0.0:
|
/has-flag/3.0.0:
|
||||||
resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=}
|
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/has-symbols/1.0.2:
|
/has-property-descriptors/1.0.0:
|
||||||
resolution: {integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==}
|
resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
|
||||||
|
dependencies:
|
||||||
|
get-intrinsic: 1.1.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/has-symbols/1.0.3:
|
||||||
|
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -178,7 +202,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==}
|
resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
has-symbols: 1.0.2
|
has-symbols: 1.0.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/has/1.0.3:
|
/has/1.0.3:
|
||||||
@@ -197,7 +221,7 @@ 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.7
|
follow-redirects: 1.15.1
|
||||||
requires-port: 1.0.0
|
requires-port: 1.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
@@ -207,19 +231,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==}
|
resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: 1.1.1
|
get-intrinsic: 1.1.2
|
||||||
has: 1.0.3
|
has: 1.0.3
|
||||||
side-channel: 1.0.4
|
side-channel: 1.0.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/is-arrayish/0.2.1:
|
/is-arrayish/0.2.1:
|
||||||
resolution: {integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=}
|
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/is-bigint/1.0.4:
|
/is-bigint/1.0.4:
|
||||||
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
|
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
has-bigints: 1.0.1
|
has-bigints: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/is-boolean-object/1.1.2:
|
/is-boolean-object/1.1.2:
|
||||||
@@ -235,8 +259,8 @@ packages:
|
|||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/is-core-module/2.8.1:
|
/is-core-module/2.9.0:
|
||||||
resolution: {integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==}
|
resolution: {integrity: sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
has: 1.0.3
|
has: 1.0.3
|
||||||
dev: true
|
dev: true
|
||||||
@@ -253,8 +277,8 @@ packages:
|
|||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/is-number-object/1.0.6:
|
/is-number-object/1.0.7:
|
||||||
resolution: {integrity: sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==}
|
resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
has-tostringtag: 1.0.0
|
has-tostringtag: 1.0.0
|
||||||
@@ -268,8 +292,10 @@ packages:
|
|||||||
has-tostringtag: 1.0.0
|
has-tostringtag: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/is-shared-array-buffer/1.0.1:
|
/is-shared-array-buffer/1.0.2:
|
||||||
resolution: {integrity: sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==}
|
resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/is-string/1.0.7:
|
/is-string/1.0.7:
|
||||||
@@ -283,7 +309,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==}
|
resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
has-symbols: 1.0.2
|
has-symbols: 1.0.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/is-weakref/1.0.2:
|
/is-weakref/1.0.2:
|
||||||
@@ -293,7 +319,7 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/isexe/2.0.0:
|
/isexe/2.0.0:
|
||||||
resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/json-parse-better-errors/1.0.2:
|
/json-parse-better-errors/1.0.2:
|
||||||
@@ -301,22 +327,22 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/load-json-file/4.0.0:
|
/load-json-file/4.0.0:
|
||||||
resolution: {integrity: sha1-L19Fq5HjMhYjT9U62rZo607AmTs=}
|
resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.9
|
graceful-fs: 4.2.10
|
||||||
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
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/memorystream/0.3.1:
|
/memorystream/0.3.1:
|
||||||
resolution: {integrity: sha1-htcJCzDORV1j+64S3aUaR93K+bI=}
|
resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/minimatch/3.0.4:
|
/minimatch/3.1.2:
|
||||||
resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==}
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.11
|
brace-expansion: 1.1.11
|
||||||
dev: true
|
dev: true
|
||||||
@@ -329,7 +355,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.21.0
|
resolve: 1.22.1
|
||||||
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
|
||||||
@@ -343,15 +369,15 @@ packages:
|
|||||||
chalk: 2.4.2
|
chalk: 2.4.2
|
||||||
cross-spawn: 6.0.5
|
cross-spawn: 6.0.5
|
||||||
memorystream: 0.3.1
|
memorystream: 0.3.1
|
||||||
minimatch: 3.0.4
|
minimatch: 3.1.2
|
||||||
pidtree: 0.3.1
|
pidtree: 0.3.1
|
||||||
read-pkg: 3.0.0
|
read-pkg: 3.0.0
|
||||||
shell-quote: 1.7.3
|
shell-quote: 1.7.3
|
||||||
string.prototype.padend: 3.1.3
|
string.prototype.padend: 3.1.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/object-inspect/1.12.0:
|
/object-inspect/1.12.2:
|
||||||
resolution: {integrity: sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==}
|
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/object-keys/1.1.1:
|
/object-keys/1.1.1:
|
||||||
@@ -364,13 +390,13 @@ packages:
|
|||||||
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.4
|
||||||
has-symbols: 1.0.2
|
has-symbols: 1.0.3
|
||||||
object-keys: 1.1.1
|
object-keys: 1.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/parse-json/4.0.0:
|
/parse-json/4.0.0:
|
||||||
resolution: {integrity: sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=}
|
resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
error-ex: 1.3.2
|
error-ex: 1.3.2
|
||||||
@@ -378,7 +404,7 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/path-key/2.0.1:
|
/path-key/2.0.1:
|
||||||
resolution: {integrity: sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=}
|
resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -400,12 +426,12 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/pify/3.0.0:
|
/pify/3.0.0:
|
||||||
resolution: {integrity: sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=}
|
resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/read-pkg/3.0.0:
|
/read-pkg/3.0.0:
|
||||||
resolution: {integrity: sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=}
|
resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dependencies:
|
dependencies:
|
||||||
load-json-file: 4.0.0
|
load-json-file: 4.0.0
|
||||||
@@ -413,15 +439,24 @@ packages:
|
|||||||
path-type: 3.0.0
|
path-type: 3.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/requires-port/1.0.0:
|
/regexp.prototype.flags/1.4.3:
|
||||||
resolution: {integrity: sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=}
|
resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.2
|
||||||
|
define-properties: 1.1.4
|
||||||
|
functions-have-names: 1.2.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/resolve/1.21.0:
|
/requires-port/1.0.0:
|
||||||
resolution: {integrity: sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==}
|
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/resolve/1.22.1:
|
||||||
|
resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
is-core-module: 2.8.1
|
is-core-module: 2.9.0
|
||||||
path-parse: 1.0.7
|
path-parse: 1.0.7
|
||||||
supports-preserve-symlinks-flag: 1.0.0
|
supports-preserve-symlinks-flag: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
@@ -432,14 +467,14 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/shebang-command/1.2.0:
|
/shebang-command/1.2.0:
|
||||||
resolution: {integrity: sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=}
|
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 1.0.0
|
shebang-regex: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/shebang-regex/1.0.0:
|
/shebang-regex/1.0.0:
|
||||||
resolution: {integrity: sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=}
|
resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -451,8 +486,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.2
|
call-bind: 1.0.2
|
||||||
get-intrinsic: 1.1.1
|
get-intrinsic: 1.1.2
|
||||||
object-inspect: 1.12.0
|
object-inspect: 1.12.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/spdx-correct/3.1.1:
|
/spdx-correct/3.1.1:
|
||||||
@@ -482,26 +517,28 @@ packages:
|
|||||||
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.4
|
||||||
es-abstract: 1.19.1
|
es-abstract: 1.20.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/string.prototype.trimend/1.0.4:
|
/string.prototype.trimend/1.0.5:
|
||||||
resolution: {integrity: sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==}
|
resolution: {integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==}
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.2
|
call-bind: 1.0.2
|
||||||
define-properties: 1.1.3
|
define-properties: 1.1.4
|
||||||
|
es-abstract: 1.20.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/string.prototype.trimstart/1.0.4:
|
/string.prototype.trimstart/1.0.5:
|
||||||
resolution: {integrity: sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==}
|
resolution: {integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.2
|
call-bind: 1.0.2
|
||||||
define-properties: 1.1.3
|
define-properties: 1.1.4
|
||||||
|
es-abstract: 1.20.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/strip-bom/3.0.0:
|
/strip-bom/3.0.0:
|
||||||
resolution: {integrity: sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=}
|
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -517,12 +554,12 @@ packages:
|
|||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/unbox-primitive/1.0.1:
|
/unbox-primitive/1.0.2:
|
||||||
resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==}
|
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.1
|
call-bind: 1.0.2
|
||||||
has-bigints: 1.0.1
|
has-bigints: 1.0.2
|
||||||
has-symbols: 1.0.2
|
has-symbols: 1.0.3
|
||||||
which-boxed-primitive: 1.0.2
|
which-boxed-primitive: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@@ -538,7 +575,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-bigint: 1.0.4
|
is-bigint: 1.0.4
|
||||||
is-boolean-object: 1.1.2
|
is-boolean-object: 1.1.2
|
||||||
is-number-object: 1.0.6
|
is-number-object: 1.0.7
|
||||||
is-string: 1.0.7
|
is-string: 1.0.7
|
||||||
is-symbol: 1.0.4
|
is-symbol: 1.0.4
|
||||||
dev: true
|
dev: true
|
||||||
|
Reference in New Issue
Block a user