biome integration & cleanup
M.gitignore
| @@ -1,5 +1,5 @@ | |||
|---|---|---|---|
| 1 | 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | |
| 2 | - | ||
| 2 | + | /.idea | |
| 3 | 3 | # dependencies | |
| 4 | 4 | /node_modules | |
| 5 | 5 | /.pnp | |
| @@ -41,4 +41,4 @@ yarn-error.log* | |||
|---|---|---|---|
| 41 | 41 | package-lock.json | |
| 42 | 42 | **/*.bun | |
| 43 | 43 | db | |
| 44 | - | assets/dist | |
| 44 | + | assets/dist | |
Massets/default.css
| @@ -2,87 +2,86 @@ | |||
|---|---|---|---|
| 2 | 2 | @import "sakura-vader.css"; | |
| 3 | 3 | ||
| 4 | 4 | #jsguard { | |
| 5 | - | display: none; | |
| 5 | + | display: none; | |
| 6 | 6 | } | |
| 7 | 7 | ||
| 8 | 8 | body { | |
| 9 | - | display: flex; | |
| 9 | + | display: flex; | |
| 10 | 10 | align-items: center; | |
| 11 | 11 | flex-direction: column; | |
| 12 | 12 | min-height: 100vh; | |
| 13 | 13 | ||
| 14 | - | padding: 3rem; | |
| 14 | + | padding: 3rem; | |
| 15 | 15 | box-sizing: border-box; | |
| 16 | - | max-width: 80rem; | |
| 16 | + | max-width: 80rem; | |
| 17 | 17 | } | |
| 18 | 18 | ||
| 19 | 19 | a { | |
| 20 | - | text-wrap: nowrap; | |
| 20 | + | text-wrap: nowrap; | |
| 21 | 21 | } | |
| 22 | 22 | ||
| 23 | 23 | hr { | |
| 24 | - | width: 100%; | |
| 24 | + | width: 100%; | |
| 25 | 25 | } | |
| 26 | 26 | ||
| 27 | 27 | h1 { | |
| 28 | - | margin-bottom: 4rem; | |
| 28 | + | margin-bottom: 4rem; | |
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | 31 | form { | |
| 32 | - | display: flex; | |
| 33 | - | flex-direction: column; | |
| 34 | - | gap: 0.5rem; | |
| 35 | - | margin-bottom: 1.5rem; | |
| 36 | - | min-width: 35rem; | |
| 37 | - | width: 100%; | |
| 38 | - | ||
| 39 | - | input { | |
| 40 | - | flex-grow: 1; | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | input[type=checkbox] { | |
| 44 | - | flex-grow: 0; | |
| 45 | - | margin-bottom: 0.5rem; | |
| 46 | - | } | |
| 47 | - | ||
| 48 | - | div { | |
| 49 | - | display: flex; | |
| 50 | - | flex-direction: row; | |
| 51 | - | align-items: center; | |
| 52 | - | gap: 1rem; | |
| 53 | - | } | |
| 32 | + | display: flex; | |
| 33 | + | flex-direction: column; | |
| 34 | + | gap: 0.5rem; | |
| 35 | + | margin-bottom: 1.5rem; | |
| 36 | + | min-width: 35rem; | |
| 37 | + | width: 100%; | |
| 38 | + | ||
| 39 | + | input { | |
| 40 | + | flex-grow: 1; | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | input[type="checkbox"] { | |
| 44 | + | flex-grow: 0; | |
| 45 | + | margin-bottom: 0.5rem; | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | div { | |
| 49 | + | display: flex; | |
| 50 | + | flex-direction: row; | |
| 51 | + | align-items: center; | |
| 52 | + | gap: 1rem; | |
| 53 | + | } | |
| 54 | 54 | } | |
| 55 | 55 | ||
| 56 | 56 | select { | |
| 57 | - | overflow: auto; | |
| 58 | - | flex-grow: 1; | |
| 57 | + | overflow: auto; | |
| 58 | + | flex-grow: 1; | |
| 59 | 59 | } | |
| 60 | 60 | ||
| 61 | 61 | img { | |
| 62 | - | margin-bottom: 0; | |
| 62 | + | margin-bottom: 0; | |
| 63 | 63 | } | |
| 64 | 64 | ||
| 65 | 65 | .help-icon { | |
| 66 | - | width: 2rem; | |
| 67 | - | height: 2rem; | |
| 68 | - | filter: invert(); | |
| 69 | - | margin-left: 0.5rem; | |
| 66 | + | width: 2rem; | |
| 67 | + | height: 2rem; | |
| 68 | + | filter: invert(); | |
| 69 | + | margin-left: 0.5rem; | |
| 70 | 70 | } | |
| 71 | 71 | ||
| 72 | - | input[type=reset] { | |
| 73 | - | max-width: 25rem; | |
| 74 | - | align-self: center; | |
| 72 | + | input[type="reset"] { | |
| 73 | + | max-width: 25rem; | |
| 74 | + | align-self: center; | |
| 75 | 75 | } | |
| 76 | 76 | ||
| 77 | - | ||
| 78 | 77 | dialog { | |
| 79 | - | background-color: #120c0e; | |
| 80 | - | color: #d9d8dc; | |
| 81 | - | border-color: #eb99a1; | |
| 82 | - | ||
| 83 | - | form { | |
| 84 | - | display: flex; | |
| 85 | - | flex-direction: column; | |
| 86 | - | align-items: center; | |
| 87 | - | } | |
| 88 | - | } | |
| 78 | + | background-color: #120c0e; | |
| 79 | + | color: #d9d8dc; | |
| 80 | + | border-color: #eb99a1; | |
| 81 | + | ||
| 82 | + | form { | |
| 83 | + | display: flex; | |
| 84 | + | flex-direction: column; | |
| 85 | + | align-items: center; | |
| 86 | + | } | |
| 87 | + | } | |
Massets/show.css
| @@ -3,92 +3,91 @@ | |||
|---|---|---|---|
| 3 | 3 | @import "highlight.css"; | |
| 4 | 4 | ||
| 5 | 5 | hr { | |
| 6 | - | width: 100%; | |
| 6 | + | width: 100%; | |
| 7 | 7 | } | |
| 8 | 8 | ||
| 9 | 9 | body { | |
| 10 | - | box-sizing: border-box; | |
| 11 | - | width: 100%; | |
| 12 | - | height: 100vh; | |
| 13 | - | max-width: 100%; | |
| 14 | - | max-height: 100vh; | |
| 10 | + | box-sizing: border-box; | |
| 11 | + | width: 100%; | |
| 12 | + | height: 100vh; | |
| 13 | + | max-width: 100%; | |
| 14 | + | max-height: 100vh; | |
| 15 | 15 | ||
| 16 | - | display: flex; | |
| 17 | - | flex-direction: row; | |
| 16 | + | display: flex; | |
| 17 | + | flex-direction: row; | |
| 18 | 18 | ||
| 19 | - | #content { | |
| 20 | - | box-sizing: border-box; | |
| 21 | - | flex-grow: 1; | |
| 22 | - | height: 100%; | |
| 23 | - | overflow: auto; | |
| 24 | - | border: 1px solid #40363a; | |
| 25 | - | border-right: unset; | |
| 26 | - | display: flex; | |
| 27 | - | flex-direction: column; | |
| 19 | + | #content { | |
| 20 | + | box-sizing: border-box; | |
| 21 | + | flex-grow: 1; | |
| 22 | + | height: 100%; | |
| 23 | + | overflow: auto; | |
| 24 | + | border: 1px solid #40363a; | |
| 25 | + | border-right: unset; | |
| 26 | + | display: flex; | |
| 27 | + | flex-direction: column; | |
| 28 | 28 | ||
| 29 | - | #filename { | |
| 30 | - | padding-left: 1rem; | |
| 31 | - | } | |
| 29 | + | #filename { | |
| 30 | + | padding-left: 1rem; | |
| 31 | + | } | |
| 32 | 32 | ||
| 33 | - | pre { | |
| 34 | - | box-sizing: border-box; | |
| 35 | - | margin: 0; | |
| 36 | - | flex-grow: 1; | |
| 37 | - | overflow: auto; | |
| 38 | - | max-width: 100%; | |
| 39 | - | max-height: 100%; | |
| 40 | - | } | |
| 33 | + | pre { | |
| 34 | + | box-sizing: border-box; | |
| 35 | + | margin: 0; | |
| 36 | + | flex-grow: 1; | |
| 37 | + | overflow: auto; | |
| 38 | + | max-width: 100%; | |
| 39 | + | max-height: 100%; | |
| 40 | + | } | |
| 41 | 41 | ||
| 42 | - | #mediabox { | |
| 43 | - | flex-grow: 1; | |
| 44 | - | display: flex; | |
| 45 | - | justify-content: center; | |
| 46 | - | } | |
| 47 | - | } | |
| 42 | + | #mediabox { | |
| 43 | + | flex-grow: 1; | |
| 44 | + | display: flex; | |
| 45 | + | justify-content: center; | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | 48 | ||
| 49 | - | #sidebar { | |
| 50 | - | box-sizing: border-box; | |
| 51 | - | box-sizing: border-box; | |
| 52 | - | flex-shrink: 0; | |
| 53 | - | padding: 1rem; | |
| 54 | - | width: 30rem; | |
| 55 | - | height: 100%; | |
| 56 | - | border: 1px solid #40363a; | |
| 57 | - | display: flex; | |
| 58 | - | flex-direction: column; | |
| 49 | + | #sidebar { | |
| 50 | + | box-sizing: border-box; | |
| 51 | + | flex-shrink: 0; | |
| 52 | + | padding: 1rem; | |
| 53 | + | width: 30rem; | |
| 54 | + | height: 100%; | |
| 55 | + | border: 1px solid #40363a; | |
| 56 | + | display: flex; | |
| 57 | + | flex-direction: column; | |
| 59 | 58 | ||
| 60 | - | h3 { | |
| 61 | - | margin-top: 0; | |
| 62 | - | text-align: center; | |
| 63 | - | } | |
| 59 | + | h3 { | |
| 60 | + | margin-top: 0; | |
| 61 | + | text-align: center; | |
| 62 | + | } | |
| 64 | 63 | ||
| 65 | - | form { | |
| 66 | - | align-self: center; | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | } | |
| 64 | + | form { | |
| 65 | + | align-self: center; | |
| 66 | + | } | |
| 67 | + | } | |
| 70 | 68 | } | |
| 71 | 69 | ||
| 72 | 70 | audio { | |
| 73 | - | max-width: 40rem; | |
| 74 | - | min-width: 0; | |
| 75 | - | flex-grow: 1; | |
| 76 | - | align-self: center; | |
| 71 | + | max-width: 40rem; | |
| 72 | + | min-width: 0; | |
| 73 | + | flex-grow: 1; | |
| 74 | + | align-self: center; | |
| 77 | 75 | } | |
| 78 | 76 | ||
| 79 | 77 | img, | |
| 80 | 78 | video { | |
| 81 | - | object-fit: scale-down; | |
| 79 | + | object-fit: scale-down; | |
| 82 | 80 | } | |
| 83 | 81 | ||
| 84 | 82 | dialog { | |
| 85 | - | background-color: #120c0e; | |
| 86 | - | color: #d9d8dc; | |
| 87 | - | border-color: #eb99a1; | |
| 83 | + | background-color: #120c0e; | |
| 84 | + | color: #d9d8dc; | |
| 85 | + | border-color: #eb99a1; | |
| 88 | 86 | ||
| 89 | - | form { | |
| 90 | - | display: flex; | |
| 91 | - | flex-direction: column; | |
| 92 | - | align-items: center; | |
| 93 | - | } | |
| 94 | - | } | |
| 87 | + | /* biome-ignore lint/style/noDescendingSpecificity: targets dialog forms only, disjoint from the #sidebar form rule */ | |
| 88 | + | form { | |
| 89 | + | display: flex; | |
| 90 | + | flex-direction: column; | |
| 91 | + | align-items: center; | |
| 92 | + | } | |
| 93 | + | } | |
Abiome.json
| @@ -0,0 +1,44 @@ | |||
|---|---|---|---|
| 1 | + | { | |
| 2 | + | "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json", | |
| 3 | + | "vcs": { | |
| 4 | + | "enabled": true, | |
| 5 | + | "clientKind": "git", | |
| 6 | + | "useIgnoreFile": true | |
| 7 | + | }, | |
| 8 | + | "files": { | |
| 9 | + | "ignoreUnknown": false, | |
| 10 | + | "includes": [ | |
| 11 | + | "src/**", | |
| 12 | + | "*.json", | |
| 13 | + | "!bun.lock", | |
| 14 | + | "assets/show.css", | |
| 15 | + | "assets/default.css" | |
| 16 | + | ] | |
| 17 | + | }, | |
| 18 | + | "formatter": { | |
| 19 | + | "enabled": true, | |
| 20 | + | "indentStyle": "tab" | |
| 21 | + | }, | |
| 22 | + | "linter": { | |
| 23 | + | "enabled": true, | |
| 24 | + | "rules": { | |
| 25 | + | "recommended": true, | |
| 26 | + | "a11y": { | |
| 27 | + | "useMediaCaption": "off" | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | }, | |
| 31 | + | "javascript": { | |
| 32 | + | "formatter": { | |
| 33 | + | "quoteStyle": "double" | |
| 34 | + | } | |
| 35 | + | }, | |
| 36 | + | "assist": { | |
| 37 | + | "enabled": true, | |
| 38 | + | "actions": { | |
| 39 | + | "source": { | |
| 40 | + | "organizeImports": "on" | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | } | |
| 44 | + | } | |
Mbun.lock
| @@ -16,6 +16,7 @@ | |||
|---|---|---|---|
| 16 | 16 | "utf-8-validate": "^6.0.5", | |
| 17 | 17 | }, | |
| 18 | 18 | "devDependencies": { | |
| 19 | + | "@biomejs/biome": "2.4.16", | |
| 19 | 20 | "@types/pkcs7-padding": "^0.1.3", | |
| 20 | 21 | "@types/utf-8-validate": "^5.0.2", | |
| 21 | 22 | "bun-types": "latest", | |
| @@ -23,6 +24,24 @@ | |||
|---|---|---|---|
| 23 | 24 | }, | |
| 24 | 25 | }, | |
| 25 | 26 | "packages": { | |
| 27 | + | "@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="], | |
| 28 | + | ||
| 29 | + | "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="], | |
| 30 | + | ||
| 31 | + | "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="], | |
| 32 | + | ||
| 33 | + | "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="], | |
| 34 | + | ||
| 35 | + | "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="], | |
| 36 | + | ||
| 37 | + | "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="], | |
| 38 | + | ||
| 39 | + | "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="], | |
| 40 | + | ||
| 41 | + | "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="], | |
| 42 | + | ||
| 43 | + | "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="], | |
| 44 | + | ||
| 26 | 45 | "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="], | |
| 27 | 46 | ||
| 28 | 47 | "@chneau/elysia-compression": ["@chneau/elysia-compression@1.0.11", "", { "dependencies": { "elysia": "^1.1.9" } }, "sha512-J4wfz5Qs68p/DNvkg5DXxo8sxJKSibzaUBP4W56uvqgs9NtHG0LC6tnyv7sAPEjNKzh21l6kinMoXbtVDp/KSQ=="], | |
Mpackage.json
| @@ -1,26 +1,29 @@ | |||
|---|---|---|---|
| 1 | 1 | { | |
| 2 | - | "name": "zbin", | |
| 3 | - | "version": "1.0.0", | |
| 4 | - | "scripts": { | |
| 5 | - | "build": "bun build src/client*.ts --outdir assets/dist", | |
| 6 | - | "dev": "bun run build && bun run --watch src/index.ts", | |
| 7 | - | "prod": "bun run build && bun run src/index.ts" | |
| 8 | - | }, | |
| 9 | - | "dependencies": { | |
| 10 | - | "@chneau/elysia-compression": "^1.0.11", | |
| 11 | - | "@elysiajs/cron": "^1.2.0", | |
| 12 | - | "@elysiajs/html": "^1.2.0", | |
| 13 | - | "@elysiajs/static": "^1.2.0", | |
| 14 | - | "elysia": "^1.2.0", | |
| 15 | - | "file-type": "^20.0.1", | |
| 16 | - | "highlight.js": "^11.11.1", | |
| 17 | - | "pkcs7-padding": "^0.1.1", | |
| 18 | - | "utf-8-validate": "^6.0.5" | |
| 19 | - | }, | |
| 20 | - | "devDependencies": { | |
| 21 | - | "@types/pkcs7-padding": "^0.1.3", | |
| 22 | - | "@types/utf-8-validate": "^5.0.2", | |
| 23 | - | "bun-types": "latest" | |
| 24 | - | }, | |
| 25 | - | "module": "src/index.js" | |
| 2 | + | "name": "zbin", | |
| 3 | + | "version": "1.0.0", | |
| 4 | + | "scripts": { | |
| 5 | + | "build": "bun build src/client*.ts --outdir assets/dist", | |
| 6 | + | "dev": "bun run build && bun run --watch src/index.ts", | |
| 7 | + | "prod": "bun run build && bun run src/index.ts", | |
| 8 | + | "format": "biome format --write", | |
| 9 | + | "lint": "biome lint" | |
| 10 | + | }, | |
| 11 | + | "dependencies": { | |
| 12 | + | "@chneau/elysia-compression": "^1.0.11", | |
| 13 | + | "@elysiajs/cron": "^1.2.0", | |
| 14 | + | "@elysiajs/html": "^1.2.0", | |
| 15 | + | "@elysiajs/static": "^1.2.0", | |
| 16 | + | "elysia": "^1.2.0", | |
| 17 | + | "file-type": "^20.0.1", | |
| 18 | + | "highlight.js": "^11.11.1", | |
| 19 | + | "pkcs7-padding": "^0.1.1", | |
| 20 | + | "utf-8-validate": "^6.0.5" | |
| 21 | + | }, | |
| 22 | + | "devDependencies": { | |
| 23 | + | "@biomejs/biome": "2.4.16", | |
| 24 | + | "@types/pkcs7-padding": "^0.1.3", | |
| 25 | + | "@types/utf-8-validate": "^5.0.2", | |
| 26 | + | "bun-types": "latest" | |
| 27 | + | }, | |
| 28 | + | "module": "src/index.js" | |
| 26 | 29 | } | |
Msrc/client-index.ts
| @@ -1,78 +1,102 @@ | |||
|---|---|---|---|
| 1 | - | async function encrypt(content: Uint8Array, password: string): Promise<ArrayBuffer> { | |
| 2 | - | const iv = crypto.getRandomValues(new Uint8Array(16)) | |
| 3 | - | const salt = crypto.getRandomValues(new Uint8Array(16)) | |
| 4 | - | const keyMaterial = await crypto.subtle.importKey( | |
| 5 | - | "raw", | |
| 6 | - | new TextEncoder().encode(password), | |
| 7 | - | { name: "PBKDF2" }, | |
| 8 | - | false, | |
| 9 | - | ["deriveBits", "deriveKey"] | |
| 10 | - | ) | |
| 11 | - | const key = await crypto.subtle.deriveKey( | |
| 12 | - | { | |
| 13 | - | name: "PBKDF2", | |
| 14 | - | salt: salt, | |
| 15 | - | iterations: 100000, | |
| 16 | - | hash: "SHA-512" | |
| 17 | - | }, | |
| 18 | - | keyMaterial, | |
| 19 | - | { name: "AES-CBC", length: 256 }, | |
| 20 | - | false, | |
| 21 | - | ["encrypt"] | |
| 22 | - | ) | |
| 23 | - | const encryptedContent = await crypto.subtle.encrypt( | |
| 24 | - | { name: "AES-CBC", iv: iv }, | |
| 25 | - | key, | |
| 26 | - | content | |
| 27 | - | ) | |
| 28 | - | return new Uint8Array([...salt, ...iv, ...new Uint8Array(encryptedContent)]).buffer | |
| 1 | + | async function encrypt( | |
| 2 | + | content: Uint8Array<ArrayBuffer>, | |
| 3 | + | password: string, | |
| 4 | + | ): Promise<ArrayBuffer> { | |
| 5 | + | const iv = crypto.getRandomValues(new Uint8Array(16)); | |
| 6 | + | const salt = crypto.getRandomValues(new Uint8Array(16)); | |
| 7 | + | const keyMaterial = await crypto.subtle.importKey( | |
| 8 | + | "raw", | |
| 9 | + | new TextEncoder().encode(password), | |
| 10 | + | { name: "PBKDF2" }, | |
| 11 | + | false, | |
| 12 | + | ["deriveBits", "deriveKey"], | |
| 13 | + | ); | |
| 14 | + | const key = await crypto.subtle.deriveKey( | |
| 15 | + | { | |
| 16 | + | name: "PBKDF2", | |
| 17 | + | salt: salt, | |
| 18 | + | iterations: 100000, | |
| 19 | + | hash: "SHA-512", | |
| 20 | + | }, | |
| 21 | + | keyMaterial, | |
| 22 | + | { name: "AES-CBC", length: 256 }, | |
| 23 | + | false, | |
| 24 | + | ["encrypt"], | |
| 25 | + | ); | |
| 26 | + | const encryptedContent = await crypto.subtle.encrypt( | |
| 27 | + | { name: "AES-CBC", iv: iv }, | |
| 28 | + | key, | |
| 29 | + | content, | |
| 30 | + | ); | |
| 31 | + | return new Uint8Array([...salt, ...iv, ...new Uint8Array(encryptedContent)]) | |
| 32 | + | .buffer; | |
| 29 | 33 | } | |
| 30 | 34 | ||
| 31 | 35 | async function uploadFile() { | |
| 32 | - | (document.getElementById("upload-dialog") as HTMLDialogElement | null)?.showModal() | |
| 33 | - | const passwordInput = document.getElementById("password") as HTMLInputElement | null | |
| 34 | - | const fileInput = document.getElementById("file") as HTMLInputElement | null | |
| 35 | - | const filenameInput = document.getElementById("filename") as HTMLInputElement | null | |
| 36 | - | const encryptedInput = document.getElementById("encrypted") as HTMLInputElement | null | |
| 37 | - | const form = document.getElementById("uploadForm") as HTMLFormElement | null | |
| 36 | + | ( | |
| 37 | + | document.getElementById("upload-dialog") as HTMLDialogElement | null | |
| 38 | + | )?.showModal(); | |
| 39 | + | const passwordInput = document.getElementById( | |
| 40 | + | "password", | |
| 41 | + | ) as HTMLInputElement | null; | |
| 42 | + | const fileInput = document.getElementById("file") as HTMLInputElement | null; | |
| 43 | + | const filenameInput = document.getElementById( | |
| 44 | + | "filename", | |
| 45 | + | ) as HTMLInputElement | null; | |
| 46 | + | const encryptedInput = document.getElementById( | |
| 47 | + | "encrypted", | |
| 48 | + | ) as HTMLInputElement | null; | |
| 49 | + | const form = document.getElementById("uploadForm") as HTMLFormElement | null; | |
| 38 | 50 | ||
| 39 | - | if (!passwordInput || !fileInput || !filenameInput || !encryptedInput || !form) { | |
| 40 | - | console.error("Missing required elements.") | |
| 41 | - | console.error(passwordInput, fileInput, filenameInput, encryptedInput, form) | |
| 42 | - | return | |
| 43 | - | } | |
| 51 | + | if ( | |
| 52 | + | !passwordInput || | |
| 53 | + | !fileInput || | |
| 54 | + | !filenameInput || | |
| 55 | + | !encryptedInput || | |
| 56 | + | !form | |
| 57 | + | ) { | |
| 58 | + | console.error("Missing required elements."); | |
| 59 | + | console.error( | |
| 60 | + | passwordInput, | |
| 61 | + | fileInput, | |
| 62 | + | filenameInput, | |
| 63 | + | encryptedInput, | |
| 64 | + | form, | |
| 65 | + | ); | |
| 66 | + | return; | |
| 67 | + | } | |
| 44 | 68 | ||
| 45 | - | const password = passwordInput.value; | |
| 46 | - | if (!password) { | |
| 47 | - | form.submit() | |
| 48 | - | return | |
| 49 | - | } | |
| 69 | + | const password = passwordInput.value; | |
| 70 | + | if (!password) { | |
| 71 | + | form.submit(); | |
| 72 | + | return; | |
| 73 | + | } | |
| 50 | 74 | ||
| 51 | - | const file = fileInput.files?.[0]; | |
| 52 | - | if (!file) { | |
| 53 | - | return | |
| 54 | - | } | |
| 75 | + | const file = fileInput.files?.[0]; | |
| 76 | + | if (!file) { | |
| 77 | + | return; | |
| 78 | + | } | |
| 55 | 79 | ||
| 56 | - | const content = await file.arrayBuffer() | |
| 57 | - | const encryptedContent = await encrypt(new Uint8Array(content), password) | |
| 80 | + | const content = await file.arrayBuffer(); | |
| 81 | + | const encryptedContent = await encrypt(new Uint8Array(content), password); | |
| 58 | 82 | ||
| 59 | - | const myFile = new File([encryptedContent], file.name) | |
| 60 | - | const dataTransfer = new DataTransfer() | |
| 61 | - | dataTransfer.items.add(myFile) | |
| 83 | + | const myFile = new File([encryptedContent], file.name); | |
| 84 | + | const dataTransfer = new DataTransfer(); | |
| 85 | + | dataTransfer.items.add(myFile); | |
| 62 | 86 | ||
| 63 | - | fileInput.files = dataTransfer.files | |
| 64 | - | encryptedInput.checked = true | |
| 65 | - | console.log("encryption done") | |
| 66 | - | form.submit() | |
| 87 | + | fileInput.files = dataTransfer.files; | |
| 88 | + | encryptedInput.checked = true; | |
| 89 | + | console.log("encryption done"); | |
| 90 | + | form.submit(); | |
| 67 | 91 | } | |
| 68 | 92 | ||
| 69 | - | const jsguards = document.getElementsByClassName("jsguard") | |
| 93 | + | const jsguards = document.getElementsByClassName("jsguard"); | |
| 70 | 94 | for (let i = 0; i < jsguards.length; i++) { | |
| 71 | - | jsguards[i].remove() | |
| 95 | + | jsguards[i].remove(); | |
| 72 | 96 | } | |
| 73 | 97 | ||
| 74 | 98 | (document.getElementById("uploadForm") as HTMLFormElement | null)?.reset(); | |
| 75 | - | window.uploadFile = uploadFile | |
| 99 | + | window.uploadFile = uploadFile; | |
| 76 | 100 | //disable bfcache, otherwise dialog will stay open when navigating back | |
| 77 | - | window.addEventListener('unload', function () { }); | |
| 78 | - | window.addEventListener('beforeunload', function () { }); | |
| 101 | + | window.addEventListener("unload", () => {}); | |
| 102 | + | window.addEventListener("beforeunload", () => {}); | |
Msrc/client-show.ts
| @@ -1,139 +1,158 @@ | |||
|---|---|---|---|
| 1 | - | import { fileTypeFromBuffer } from 'file-type' | |
| 2 | - | import hljs from 'highlight.js' | |
| 3 | - | import { humanFileSize } from './shared' | |
| 1 | + | import { fileTypeFromBuffer } from "file-type"; | |
| 2 | + | import hljs from "highlight.js"; | |
| 3 | + | import { humanFileSize } from "./shared"; | |
| 4 | 4 | ||
| 5 | - | async function decrypt(encryptedContent: ArrayBuffer, password: string): Promise<ArrayBuffer> { | |
| 6 | - | const encryptedArray = new Uint8Array(encryptedContent) | |
| 7 | - | const salt = encryptedArray.slice(0, 16) | |
| 8 | - | const iv = encryptedArray.slice(16, 32) | |
| 9 | - | const keyMaterial = await crypto.subtle.importKey( | |
| 10 | - | "raw", | |
| 11 | - | new TextEncoder().encode(password), | |
| 12 | - | { name: "PBKDF2" }, | |
| 13 | - | false, | |
| 14 | - | ["deriveBits", "deriveKey"] | |
| 15 | - | ) | |
| 16 | - | const key = await crypto.subtle.deriveKey( | |
| 17 | - | { | |
| 18 | - | name: "PBKDF2", | |
| 19 | - | salt: salt, | |
| 20 | - | iterations: 100000, | |
| 21 | - | hash: "SHA-512" | |
| 22 | - | }, | |
| 23 | - | keyMaterial, | |
| 24 | - | { name: "AES-CBC", length: 256 }, | |
| 25 | - | false, | |
| 26 | - | ["decrypt"] | |
| 27 | - | ) | |
| 28 | - | const decryptedContent = await crypto.subtle.decrypt( | |
| 29 | - | { name: "AES-CBC", iv: iv }, | |
| 30 | - | key, | |
| 31 | - | encryptedArray.slice(32) | |
| 32 | - | ) | |
| 33 | - | return decryptedContent | |
| 5 | + | async function decrypt( | |
| 6 | + | encryptedContent: ArrayBuffer, | |
| 7 | + | password: string, | |
| 8 | + | ): Promise<ArrayBuffer> { | |
| 9 | + | const encryptedArray = new Uint8Array(encryptedContent); | |
| 10 | + | const salt = encryptedArray.slice(0, 16); | |
| 11 | + | const iv = encryptedArray.slice(16, 32); | |
| 12 | + | const keyMaterial = await crypto.subtle.importKey( | |
| 13 | + | "raw", | |
| 14 | + | new TextEncoder().encode(password), | |
| 15 | + | { name: "PBKDF2" }, | |
| 16 | + | false, | |
| 17 | + | ["deriveBits", "deriveKey"], | |
| 18 | + | ); | |
| 19 | + | const key = await crypto.subtle.deriveKey( | |
| 20 | + | { | |
| 21 | + | name: "PBKDF2", | |
| 22 | + | salt: salt, | |
| 23 | + | iterations: 100000, | |
| 24 | + | hash: "SHA-512", | |
| 25 | + | }, | |
| 26 | + | keyMaterial, | |
| 27 | + | { name: "AES-CBC", length: 256 }, | |
| 28 | + | false, | |
| 29 | + | ["decrypt"], | |
| 30 | + | ); | |
| 31 | + | const decryptedContent = await crypto.subtle.decrypt( | |
| 32 | + | { name: "AES-CBC", iv: iv }, | |
| 33 | + | key, | |
| 34 | + | encryptedArray.slice(32), | |
| 35 | + | ); | |
| 36 | + | return decryptedContent; | |
| 34 | 37 | } | |
| 35 | 38 | ||
| 36 | 39 | function isValidUTF8(buf: ArrayBuffer) { | |
| 37 | - | try { | |
| 38 | - | new TextDecoder("utf-8", { fatal: true }).decode(buf) | |
| 39 | - | return true | |
| 40 | - | } catch (e) { | |
| 41 | - | return false | |
| 42 | - | } | |
| 40 | + | try { | |
| 41 | + | new TextDecoder("utf-8", { fatal: true }).decode(buf); | |
| 42 | + | return true; | |
| 43 | + | } catch (_e) { | |
| 44 | + | return false; | |
| 45 | + | } | |
| 43 | 46 | } | |
| 44 | 47 | ||
| 45 | 48 | async function showContent(content: ArrayBuffer, filetype: string) { | |
| 46 | - | const filesize = document.getElementById("filesize") as HTMLDivElement | null | |
| 47 | - | if (filesize) { | |
| 48 | - | filesize.textContent = `size: ${humanFileSize(content.byteLength)}` | |
| 49 | - | } | |
| 49 | + | const filesize = document.getElementById("filesize") as HTMLDivElement | null; | |
| 50 | + | if (filesize) { | |
| 51 | + | filesize.textContent = `size: ${humanFileSize(content.byteLength)}`; | |
| 52 | + | } | |
| 50 | 53 | ||
| 51 | - | const mediaBox = document.getElementById("mediabox") as HTMLDivElement | null | |
| 52 | - | if (!mediaBox) { | |
| 53 | - | console.error("Missing mediaBox element.") | |
| 54 | - | return | |
| 55 | - | } | |
| 56 | - | let preview: HTMLElement = document.createElement("div") | |
| 57 | - | preview.textContent = "This file can't be previewed" | |
| 58 | - | if (isValidUTF8(content) && filetype !== "blob") { | |
| 59 | - | preview = document.createElement("pre") | |
| 60 | - | if (filetype === "none") { | |
| 61 | - | preview.textContent = new TextDecoder().decode(content) | |
| 62 | - | } else { | |
| 63 | - | preview.innerHTML = hljs.highlight(new TextDecoder("utf-8").decode(content), { language: filetype }).value | |
| 64 | - | } | |
| 65 | - | } else { | |
| 66 | - | const filetype = await fileTypeFromBuffer(content) | |
| 67 | - | if (filetype) { | |
| 68 | - | const blob = new Blob([content], { type: filetype.mime }) | |
| 69 | - | const url = URL.createObjectURL(blob) | |
| 70 | - | if (filetype.mime.startsWith("audio/")) { | |
| 71 | - | preview = document.createElement("audio") | |
| 72 | - | preview.setAttribute("controls", "") | |
| 73 | - | preview.setAttribute("src", url) | |
| 74 | - | } else if (filetype.mime.startsWith("video/")) { | |
| 75 | - | preview = document.createElement("video") | |
| 76 | - | preview.setAttribute("controls", "") | |
| 77 | - | preview.setAttribute("src", url) | |
| 78 | - | } else if (filetype.mime.startsWith("image/")) { | |
| 79 | - | preview = document.createElement("img") | |
| 80 | - | preview.setAttribute("src", url) | |
| 81 | - | } | |
| 82 | - | } | |
| 83 | - | } | |
| 84 | - | mediaBox.innerHTML = "" | |
| 85 | - | mediaBox.appendChild(preview) | |
| 54 | + | const mediaBox = document.getElementById("mediabox") as HTMLDivElement | null; | |
| 55 | + | if (!mediaBox) { | |
| 56 | + | console.error("Missing mediaBox element."); | |
| 57 | + | return; | |
| 58 | + | } | |
| 59 | + | let preview: HTMLElement = document.createElement("div"); | |
| 60 | + | preview.textContent = "This file can't be previewed"; | |
| 61 | + | if (isValidUTF8(content) && filetype !== "blob") { | |
| 62 | + | preview = document.createElement("pre"); | |
| 63 | + | if (filetype === "none") { | |
| 64 | + | preview.textContent = new TextDecoder().decode(content); | |
| 65 | + | } else { | |
| 66 | + | preview.innerHTML = hljs.highlight( | |
| 67 | + | new TextDecoder("utf-8").decode(content), | |
| 68 | + | { language: filetype }, | |
| 69 | + | ).value; | |
| 70 | + | } | |
| 71 | + | } else { | |
| 72 | + | const filetype = await fileTypeFromBuffer(content); | |
| 73 | + | if (filetype) { | |
| 74 | + | const blob = new Blob([content], { type: filetype.mime }); | |
| 75 | + | const url = URL.createObjectURL(blob); | |
| 76 | + | if (filetype.mime.startsWith("audio/")) { | |
| 77 | + | preview = document.createElement("audio"); | |
| 78 | + | preview.setAttribute("controls", ""); | |
| 79 | + | preview.setAttribute("src", url); | |
| 80 | + | } else if (filetype.mime.startsWith("video/")) { | |
| 81 | + | preview = document.createElement("video"); | |
| 82 | + | preview.setAttribute("controls", ""); | |
| 83 | + | preview.setAttribute("src", url); | |
| 84 | + | } else if (filetype.mime.startsWith("image/")) { | |
| 85 | + | preview = document.createElement("img"); | |
| 86 | + | preview.setAttribute("src", url); | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | } | |
| 90 | + | mediaBox.innerHTML = ""; | |
| 91 | + | mediaBox.appendChild(preview); | |
| 86 | 92 | } | |
| 87 | 93 | ||
| 88 | - | let content: ArrayBuffer | |
| 89 | - | let filetype: string | |
| 90 | - | let filename: string | |
| 94 | + | let content: ArrayBuffer; | |
| 95 | + | let filetype: string; | |
| 96 | + | let filename: string; | |
| 91 | 97 | ||
| 92 | 98 | async function onPasswordSubmit() { | |
| 93 | - | const passwordInput = document.getElementById("password") as HTMLInputElement | null | |
| 94 | - | const passwordLabel = document.getElementById("password-label") as HTMLLabelElement | null | |
| 95 | - | if (!passwordInput || !passwordLabel) { | |
| 96 | - | console.error("Missing passwordInput or passwordLabel element.") | |
| 97 | - | return | |
| 98 | - | } | |
| 99 | - | try { | |
| 100 | - | content = await decrypt(content, passwordInput.value) | |
| 101 | - | } catch (e) { | |
| 102 | - | passwordLabel.textContent = "Incorrect password" | |
| 103 | - | return | |
| 104 | - | } | |
| 105 | - | const dialog = document.getElementById("password-dialog") as HTMLDialogElement | null | |
| 106 | - | dialog?.close() | |
| 107 | - | const downloadForm = document.getElementById("download-form") as HTMLFormElement | null | |
| 108 | - | downloadForm?.setAttribute("action", "") | |
| 109 | - | downloadForm?.addEventListener("submit", (e) => { | |
| 110 | - | e.preventDefault() | |
| 111 | - | const blob = new Blob([content]) | |
| 112 | - | const url = URL.createObjectURL(blob) | |
| 113 | - | const link = document.createElement("a") | |
| 114 | - | link.download = filename | |
| 115 | - | link.href = url | |
| 116 | - | link.click(); | |
| 117 | - | }) | |
| 118 | - | showContent(content, filetype) | |
| 99 | + | const passwordInput = document.getElementById( | |
| 100 | + | "password", | |
| 101 | + | ) as HTMLInputElement | null; | |
| 102 | + | const passwordLabel = document.getElementById( | |
| 103 | + | "password-label", | |
| 104 | + | ) as HTMLLabelElement | null; | |
| 105 | + | if (!passwordInput || !passwordLabel) { | |
| 106 | + | console.error("Missing passwordInput or passwordLabel element."); | |
| 107 | + | return; | |
| 108 | + | } | |
| 109 | + | try { | |
| 110 | + | content = await decrypt(content, passwordInput.value); | |
| 111 | + | } catch (_e) { | |
| 112 | + | passwordLabel.textContent = "Incorrect password"; | |
| 113 | + | return; | |
| 114 | + | } | |
| 115 | + | const dialog = document.getElementById( | |
| 116 | + | "password-dialog", | |
| 117 | + | ) as HTMLDialogElement | null; | |
| 118 | + | dialog?.close(); | |
| 119 | + | const downloadForm = document.getElementById( | |
| 120 | + | "download-form", | |
| 121 | + | ) as HTMLFormElement | null; | |
| 122 | + | downloadForm?.setAttribute("action", ""); | |
| 123 | + | downloadForm?.addEventListener("submit", (e) => { | |
| 124 | + | e.preventDefault(); | |
| 125 | + | const blob = new Blob([content]); | |
| 126 | + | const url = URL.createObjectURL(blob); | |
| 127 | + | const link = document.createElement("a"); | |
| 128 | + | link.download = filename; | |
| 129 | + | link.href = url; | |
| 130 | + | link.click(); | |
| 131 | + | }); | |
| 132 | + | showContent(content, filetype); | |
| 119 | 133 | } | |
| 120 | 134 | ||
| 121 | - | const uuid = window.location.pathname.split("/").filter((x) => x !== "").reverse()[0] | |
| 135 | + | const uuid = window.location.pathname | |
| 136 | + | .split("/") | |
| 137 | + | .filter((x) => x !== "") | |
| 138 | + | .reverse()[0]; | |
| 122 | 139 | ||
| 123 | - | fetch("/raw/" + uuid + "?ignore_password=true").then(async (response) => { | |
| 124 | - | content = await response.arrayBuffer() | |
| 125 | - | const mediaBox = document.getElementById("mediabox") | |
| 126 | - | if(mediaBox) { | |
| 127 | - | mediaBox.innerHTML = "" | |
| 128 | - | } | |
| 129 | - | filetype = response.headers.get("filetype") || "none" | |
| 130 | - | filename = response.headers.get("filename") || "" | |
| 131 | - | if (response.headers.get("encrypted") === "true") { | |
| 132 | - | const dialog = document.getElementById("password-dialog") as HTMLDialogElement | null | |
| 133 | - | dialog?.showModal() | |
| 134 | - | } else { | |
| 135 | - | showContent(content, filetype) | |
| 136 | - | } | |
| 140 | + | fetch(`/raw/${uuid}?ignore_password=true`).then(async (response) => { | |
| 141 | + | content = await response.arrayBuffer(); | |
| 142 | + | const mediaBox = document.getElementById("mediabox"); | |
| 143 | + | if (mediaBox) { | |
| 144 | + | mediaBox.innerHTML = ""; | |
| 145 | + | } | |
| 146 | + | filetype = response.headers.get("filetype") || "none"; | |
| 147 | + | filename = response.headers.get("filename") || ""; | |
| 148 | + | if (response.headers.get("encrypted") === "true") { | |
| 149 | + | const dialog = document.getElementById( | |
| 150 | + | "password-dialog", | |
| 151 | + | ) as HTMLDialogElement | null; | |
| 152 | + | dialog?.showModal(); | |
| 153 | + | } else { | |
| 154 | + | showContent(content, filetype); | |
| 155 | + | } | |
| 137 | 156 | }); | |
| 138 | 157 | ||
| 139 | - | (window as any).onPasswordSubmit = onPasswordSubmit | |
| 158 | + | Object.assign(window, { onPasswordSubmit }); | |
Msrc/components.tsx
| @@ -1,227 +1,307 @@ | |||
|---|---|---|---|
| 1 | - | import { Html } from '@elysiajs/html' | |
| 2 | - | import { PropsWithChildren } from '@kitajs/html' | |
| 3 | - | import { escapeHTML } from 'bun' | |
| 4 | - | import { fileTypeFromBuffer } from 'file-type' | |
| 5 | - | import hljs from 'highlight.js' | |
| 6 | - | import isValidUTF8 from 'utf-8-validate' | |
| 7 | - | import { humanFileSize } from './shared' | |
| 1 | + | // biome-ignore lint/correctness/noUnusedImports: Html is the JSX factory (tsconfig jsxFactory: Html.createElement) | |
| 2 | + | import { Html } from "@elysiajs/html"; | |
| 3 | + | import type { PropsWithChildren } from "@kitajs/html"; | |
| 4 | + | import { escapeHTML } from "bun"; | |
| 5 | + | import { fileTypeFromBuffer } from "file-type"; | |
| 6 | + | import hljs from "highlight.js"; | |
| 7 | + | import isValidUTF8 from "utf-8-validate"; | |
| 8 | + | import { humanFileSize } from "./shared"; | |
| 8 | 9 | ||
| 9 | - | export const filetypes = ["none", "blob"].concat(hljs.listLanguages().sort()) | |
| 10 | + | export const filetypes = ["none", "blob"].concat(hljs.listLanguages().sort()); | |
| 10 | 11 | ||
| 11 | 12 | function Template(props: PropsWithChildren<{ css: string }>) { | |
| 12 | - | return ( | |
| 13 | - | <> | |
| 14 | - | {'<!doctype html>'} | |
| 15 | - | <html lang="en"> | |
| 16 | - | <head> | |
| 17 | - | <title>⚡ZBin⚡</title> | |
| 18 | - | <meta charset="UTF-8" /> | |
| 19 | - | <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| 20 | - | <link rel="stylesheet" href={props.css} /> | |
| 21 | - | <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📂</text></svg>" /> | |
| 22 | - | </head> | |
| 23 | - | <body>{props.children}</body> | |
| 24 | - | </html> | |
| 25 | - | </> | |
| 26 | - | ) | |
| 13 | + | return ( | |
| 14 | + | <> | |
| 15 | + | {"<!doctype html>"} | |
| 16 | + | <html lang="en"> | |
| 17 | + | <head> | |
| 18 | + | <title>⚡ZBin⚡</title> | |
| 19 | + | <meta charset="UTF-8" /> | |
| 20 | + | <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| 21 | + | <link rel="stylesheet" href={props.css} /> | |
| 22 | + | <link | |
| 23 | + | rel="icon" | |
| 24 | + | href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📂</text></svg>" | |
| 25 | + | /> | |
| 26 | + | </head> | |
| 27 | + | <body>{props.children}</body> | |
| 28 | + | </html> | |
| 29 | + | </> | |
| 30 | + | ); | |
| 27 | 31 | } | |
| 28 | 32 | ||
| 29 | 33 | export function NotFound() { | |
| 30 | - | return ( | |
| 31 | - | <Template css="/default.css"> | |
| 32 | - | <h1>404</h1> | |
| 33 | - | <p>File not found</p> | |
| 34 | - | </Template> | |
| 35 | - | ) | |
| 34 | + | return ( | |
| 35 | + | <Template css="/default.css"> | |
| 36 | + | <h1>404</h1> | |
| 37 | + | <p>File not found</p> | |
| 38 | + | </Template> | |
| 39 | + | ); | |
| 36 | 40 | } | |
| 37 | 41 | ||
| 38 | 42 | export function Index(hostname: string, withJs: boolean) { | |
| 39 | - | let body = ( | |
| 40 | - | <> | |
| 41 | - | <dialog id="upload-dialog"> | |
| 42 | - | File is being uploaded, please wait | |
| 43 | - | </dialog> | |
| 44 | - | {withJs ? | |
| 45 | - | <div style="text-align: center">JS version with client-side encryption, for the version without js an with server-side encryption, go <a href="/">here</a>.</div> | |
| 46 | - | : <div style="text-align: center">JS-less version with server-side encryption, for the version with client-side encryption, go to <a href="/js">/js</a>.</div> | |
| 47 | - | } | |
| 48 | - | <hr /> | |
| 49 | - | <form action={withJs ? "/upload?withJs=true" : "/upload"} id="uploadForm" method="post" enctype="multipart/form-data" onsubmit={withJs ? "uploadFile(); return false" : undefined}> | |
| 50 | - | <input type="reset" value="Reset form" /> | |
| 51 | - | <input required={true} type="file" id="file" name="file" /> | |
| 52 | - | <label for="filename">File name override:</label> | |
| 53 | - | <small>Optional. If empty, will use the uploaded file name.</small> | |
| 54 | - | <input type="text" id="filename" name="filename" placeholder="File name" /> | |
| 55 | - | <label for="filetype">File type:</label> | |
| 56 | - | <small>Used for syntax highlighting, if the file is only containing valid UTF-8 characters. Otherwise it will always be stored as a binary blob. Can select <code>blob</code> to force this detection.</small> | |
| 57 | - | <select size="10" required={true} id="filetype" name="filetype"> | |
| 58 | - | { | |
| 59 | - | filetypes.map((filetype, i) => ( | |
| 60 | - | <option selected={i === 0} value={filetype}>{filetype}</option> | |
| 61 | - | )) | |
| 62 | - | } | |
| 63 | - | </select> | |
| 64 | - | <label for="delete-in">Delete in:</label> | |
| 65 | - | <small>Optional. If set, file will be deleted after the given number of minutes.</small> | |
| 66 | - | <input type="number" id="delete-in" name="delete_in_minutes" min="0" placeholder="0" /> | |
| 67 | - | <label for="password">Password:</label> | |
| 68 | - | <small>Optional. If set, file will be encrypted and requires the given password to open.</small> | |
| 69 | - | <input type="password" id="password" name="password" placeholder="Password" /> | |
| 70 | - | <div> | |
| 71 | - | <label for="encrypted">Already encrypted:</label> | |
| 72 | - | <input type="checkbox" id="encrypted" name="encrypted" /> | |
| 73 | - | </div> | |
| 74 | - | <small>If set, file is already encrypted with an algorithm like in the python file available below. Password is ignored in this case.</small> | |
| 75 | - | <input type="submit" value="Upload File" /> | |
| 76 | - | <hr /> | |
| 77 | - | <h3>curl guide</h3> | |
| 78 | - | You can use curl to upload and download files<br /> | |
| 79 | - | Upload: | |
| 80 | - | <pre>curl {hostname}upload \{"\n"} | |
| 81 | - | -F file=@/path/to/file \{"\n"} | |
| 82 | - | -F filetype="plaintext" \{"\n"} | |
| 83 | - | # optional{"\n"} | |
| 84 | - | -F filename="file name" \{"\n"} | |
| 85 | - | # optional{"\n"} | |
| 86 | - | -F delete_in_minutes="60" \{"\n"} | |
| 87 | - | # optional{"\n"} | |
| 88 | - | -F password="mypassword" \{"\n"} | |
| 89 | - | # optional{"\n"} | |
| 90 | - | -F encrypted="on"</pre> | |
| 91 | - | <p> | |
| 92 | - | If you upload an already encrypted file, you should set the <code>encrypted="on"</code> parameter and encrypt it in a compatible way, so the frontend/backend can also decrypt it. | |
| 93 | - | You can use this python script to encrypt a file locally: <a href="/encrypt.py">encrypt.py</a> | |
| 94 | - | </p> | |
| 95 | - | <br /> | |
| 96 | - | Download: | |
| 97 | - | <pre>curl {hostname}raw/$uuid?ignore_password=true/false \{"\n"} | |
| 98 | - | # optional, if encrypted with password{"\n"} | |
| 99 | - | --cookie "password=mypassword"</pre> | |
| 100 | - | <p> | |
| 101 | - | If ignore_password is set to true, then encrypted files can be downloaded directly (in encrypted form) without supplying the password. | |
| 102 | - | </p> | |
| 103 | - | </form> | |
| 104 | - | </> | |
| 105 | - | ) | |
| 106 | - | if (withJs) { | |
| 107 | - | body = ( | |
| 108 | - | <> | |
| 109 | - | <div class="jsguard">JS seems to be disabled, enable it and refesh the page, or go back to<a href="/">here</a> for the JS-less version.</div> | |
| 110 | - | <div class="jsguard"> | |
| 111 | - | {body} | |
| 112 | - | </div> | |
| 113 | - | <script src="/dist/client-index.js" /> | |
| 114 | - | </> | |
| 115 | - | ) | |
| 116 | - | } | |
| 117 | - | return ( | |
| 118 | - | <Template css="/default.css"> | |
| 119 | - | <h1>⚡ZBin⚡</h1> | |
| 120 | - | {body} | |
| 121 | - | </Template> | |
| 122 | - | ) | |
| 43 | + | let body = ( | |
| 44 | + | <> | |
| 45 | + | <dialog id="upload-dialog">File is being uploaded, please wait</dialog> | |
| 46 | + | {withJs ? ( | |
| 47 | + | <div style="text-align: center"> | |
| 48 | + | JS version with client-side encryption, for the version without js an | |
| 49 | + | with server-side encryption, go <a href="/">here</a>. | |
| 50 | + | </div> | |
| 51 | + | ) : ( | |
| 52 | + | <div style="text-align: center"> | |
| 53 | + | JS-less version with server-side encryption, for the version with | |
| 54 | + | client-side encryption, go to <a href="/js">/js</a>. | |
| 55 | + | </div> | |
| 56 | + | )} | |
| 57 | + | <hr /> | |
| 58 | + | <form | |
| 59 | + | action={withJs ? "/upload?withJs=true" : "/upload"} | |
| 60 | + | id="uploadForm" | |
| 61 | + | method="post" | |
| 62 | + | enctype="multipart/form-data" | |
| 63 | + | onsubmit={withJs ? "uploadFile(); return false" : undefined} | |
| 64 | + | > | |
| 65 | + | <input type="reset" value="Reset form" /> | |
| 66 | + | <input required={true} type="file" id="file" name="file" /> | |
| 67 | + | <label for="filename">File name override:</label> | |
| 68 | + | <small>Optional. If empty, will use the uploaded file name.</small> | |
| 69 | + | <input | |
| 70 | + | type="text" | |
| 71 | + | id="filename" | |
| 72 | + | name="filename" | |
| 73 | + | placeholder="File name" | |
| 74 | + | /> | |
| 75 | + | <label for="filetype">File type:</label> | |
| 76 | + | <small> | |
| 77 | + | Used for syntax highlighting, if the file is only containing valid | |
| 78 | + | UTF-8 characters. Otherwise it will always be stored as a binary blob. | |
| 79 | + | Can select <code>blob</code> to force this detection. | |
| 80 | + | </small> | |
| 81 | + | <select size="10" required={true} id="filetype" name="filetype"> | |
| 82 | + | {filetypes.map((filetype, i) => ( | |
| 83 | + | <option selected={i === 0} value={filetype}> | |
| 84 | + | {filetype} | |
| 85 | + | </option> | |
| 86 | + | ))} | |
| 87 | + | </select> | |
| 88 | + | <label for="delete-in">Delete in:</label> | |
| 89 | + | <small> | |
| 90 | + | Optional. If set, file will be deleted after the given number of | |
| 91 | + | minutes. | |
| 92 | + | </small> | |
| 93 | + | <input | |
| 94 | + | type="number" | |
| 95 | + | id="delete-in" | |
| 96 | + | name="delete_in_minutes" | |
| 97 | + | min="0" | |
| 98 | + | placeholder="0" | |
| 99 | + | /> | |
| 100 | + | <label for="password">Password:</label> | |
| 101 | + | <small> | |
| 102 | + | Optional. If set, file will be encrypted and requires the given | |
| 103 | + | password to open. | |
| 104 | + | </small> | |
| 105 | + | <input | |
| 106 | + | type="password" | |
| 107 | + | id="password" | |
| 108 | + | name="password" | |
| 109 | + | placeholder="Password" | |
| 110 | + | /> | |
| 111 | + | <div> | |
| 112 | + | <label for="encrypted">Already encrypted:</label> | |
| 113 | + | <input type="checkbox" id="encrypted" name="encrypted" /> | |
| 114 | + | </div> | |
| 115 | + | <small> | |
| 116 | + | If set, file is already encrypted with an algorithm like in the python | |
| 117 | + | file available below. Password is ignored in this case. | |
| 118 | + | </small> | |
| 119 | + | <input type="submit" value="Upload File" /> | |
| 120 | + | <hr /> | |
| 121 | + | <h3>curl guide</h3> | |
| 122 | + | You can use curl to upload and download files | |
| 123 | + | <br /> | |
| 124 | + | Upload: | |
| 125 | + | <pre> | |
| 126 | + | curl {hostname}upload \{"\n"} | |
| 127 | + | -F file=@/path/to/file \{"\n"} | |
| 128 | + | -F filetype="plaintext" \{"\n"}# optional{"\n"} | |
| 129 | + | -F filename="file name" \{"\n"}# optional{"\n"} | |
| 130 | + | -F delete_in_minutes="60" \{"\n"}# optional{"\n"} | |
| 131 | + | -F password="mypassword" \{"\n"}# optional{"\n"} | |
| 132 | + | -F encrypted="on" | |
| 133 | + | </pre> | |
| 134 | + | <p> | |
| 135 | + | If you upload an already encrypted file, you should set the{" "} | |
| 136 | + | <code>encrypted="on"</code> parameter and encrypt it in a compatible | |
| 137 | + | way, so the frontend/backend can also decrypt it. You can use this | |
| 138 | + | python script to encrypt a file locally:{" "} | |
| 139 | + | <a href="/encrypt.py">encrypt.py</a> | |
| 140 | + | </p> | |
| 141 | + | <br /> | |
| 142 | + | Download: | |
| 143 | + | <pre> | |
| 144 | + | curl {hostname}raw/$uuid?ignore_password=true/false \{"\n"}# optional, | |
| 145 | + | if encrypted with password{"\n"} | |
| 146 | + | --cookie "password=mypassword" | |
| 147 | + | </pre> | |
| 148 | + | <p> | |
| 149 | + | If ignore_password is set to true, then encrypted files can be | |
| 150 | + | downloaded directly (in encrypted form) without supplying the | |
| 151 | + | password. | |
| 152 | + | </p> | |
| 153 | + | </form> | |
| 154 | + | </> | |
| 155 | + | ); | |
| 156 | + | if (withJs) { | |
| 157 | + | body = ( | |
| 158 | + | <> | |
| 159 | + | <div class="jsguard"> | |
| 160 | + | JS seems to be disabled, enable it and refesh the page, or go back to | |
| 161 | + | <a href="/">here</a> for the JS-less version. | |
| 162 | + | </div> | |
| 163 | + | <div class="jsguard">{body}</div> | |
| 164 | + | <script src="/dist/client-index.js" /> | |
| 165 | + | </> | |
| 166 | + | ); | |
| 167 | + | } | |
| 168 | + | return ( | |
| 169 | + | <Template css="/default.css"> | |
| 170 | + | <h1>⚡ZBin⚡</h1> | |
| 171 | + | {body} | |
| 172 | + | </Template> | |
| 173 | + | ); | |
| 123 | 174 | } | |
| 124 | 175 | ||
| 125 | - | ||
| 126 | 176 | export function DecryptFile(filename: string, uuid: string) { | |
| 127 | - | return ( | |
| 128 | - | <Template css="/default.css"> | |
| 129 | - | <h1>Encrypted file: {filename}</h1> | |
| 130 | - | <form action={`/set-cookie/${uuid}`} method="post"> | |
| 131 | - | <label for="password">Enter a Password to decrypt the file</label> | |
| 132 | - | <input type="password" name="password" /> | |
| 133 | - | <input type="submit" value="Submit" /> | |
| 134 | - | </form> | |
| 135 | - | </Template> | |
| 136 | - | ) | |
| 177 | + | return ( | |
| 178 | + | <Template css="/default.css"> | |
| 179 | + | <h1>Encrypted file: {filename}</h1> | |
| 180 | + | <form action={`/set-cookie/${uuid}`} method="post"> | |
| 181 | + | <label for="password">Enter a Password to decrypt the file</label> | |
| 182 | + | <input type="password" name="password" /> | |
| 183 | + | <input type="submit" value="Submit" /> | |
| 184 | + | </form> | |
| 185 | + | </Template> | |
| 186 | + | ); | |
| 137 | 187 | } | |
| 138 | 188 | ||
| 139 | 189 | export function WrongPassword() { | |
| 140 | - | return ( | |
| 141 | - | <Template css="/default.css"> | |
| 142 | - | <h1>Incorrect password</h1> | |
| 143 | - | <p>Reload page to try again</p> | |
| 144 | - | </Template> | |
| 145 | - | ) | |
| 190 | + | return ( | |
| 191 | + | <Template css="/default.css"> | |
| 192 | + | <h1>Incorrect password</h1> | |
| 193 | + | <p>Reload page to try again</p> | |
| 194 | + | </Template> | |
| 195 | + | ); | |
| 146 | 196 | } | |
| 147 | 197 | ||
| 148 | 198 | function humanReadableTime(minutes: number) { | |
| 149 | - | const years = Math.floor(minutes / 525600); | |
| 150 | - | minutes %= 525600; | |
| 151 | - | const days = Math.floor(minutes / 1440); | |
| 152 | - | minutes %= 1440; | |
| 153 | - | const hours = Math.floor(minutes / 60); | |
| 154 | - | minutes %= 60; | |
| 199 | + | const years = Math.floor(minutes / 525600); | |
| 200 | + | minutes %= 525600; | |
| 201 | + | const days = Math.floor(minutes / 1440); | |
| 202 | + | minutes %= 1440; | |
| 203 | + | const hours = Math.floor(minutes / 60); | |
| 204 | + | minutes %= 60; | |
| 155 | 205 | ||
| 156 | - | let result = ""; | |
| 157 | - | if (years > 0) result += `${years} year${years > 1 ? "s" : ""} ` | |
| 158 | - | if (days > 0) result += `${days} day${days > 1 ? "s" : ""} ` | |
| 159 | - | if (hours > 0) result += `${hours} hour${hours > 1 ? "s" : ""} ` | |
| 160 | - | if (minutes > 0) result += `${minutes} minute${minutes > 1 ? "s" : ""} ` | |
| 161 | - | if (result === "") result = "0 minutes" | |
| 162 | - | return result.trim(); | |
| 206 | + | let result = ""; | |
| 207 | + | if (years > 0) result += `${years} year${years > 1 ? "s" : ""} `; | |
| 208 | + | if (days > 0) result += `${days} day${days > 1 ? "s" : ""} `; | |
| 209 | + | if (hours > 0) result += `${hours} hour${hours > 1 ? "s" : ""} `; | |
| 210 | + | if (minutes > 0) result += `${minutes} minute${minutes > 1 ? "s" : ""} `; | |
| 211 | + | if (result === "") result = "0 minutes"; | |
| 212 | + | return result.trim(); | |
| 163 | 213 | } | |
| 164 | 214 | ||
| 165 | - | export async function ShowFile(filename: string, uuid: string, content: Uint8Array | null, filetype: string, delete_at: number | null) { | |
| 166 | - | //if content is null, this is for the js frontend | |
| 167 | - | let preview; | |
| 168 | - | if (!content) { | |
| 169 | - | preview = <>Please wait for the file to load</> | |
| 170 | - | } else { | |
| 171 | - | preview = <>This file can't be previewed</> | |
| 172 | - | if (isValidUTF8(Buffer.from(content)) && filetype !== "blob") { | |
| 173 | - | if (filetype === "none") { | |
| 174 | - | preview = <pre>{escapeHTML(new TextDecoder("utf-8").decode(content))}</pre> | |
| 175 | - | } else { | |
| 176 | - | preview = <pre>{hljs.highlight(new TextDecoder("utf-8").decode(content), { language: filetype }).value}</pre> | |
| 177 | - | } | |
| 178 | - | } else { | |
| 179 | - | const filetype = await fileTypeFromBuffer(content) | |
| 180 | - | if (filetype) { | |
| 181 | - | const base64Content = `data:${filetype.mime};base64,${Buffer.from(content).toString("base64")}`; | |
| 182 | - | if (filetype.mime.startsWith("audio/")) { | |
| 183 | - | preview = <audio controls="" src={base64Content} /> | |
| 184 | - | } else if (filetype.mime.startsWith("video/")) { | |
| 185 | - | preview = <video controls src={base64Content} /> | |
| 186 | - | } else if (filetype.mime.startsWith("image/")) { | |
| 187 | - | preview = <img src={base64Content} /> | |
| 188 | - | } | |
| 189 | - | } | |
| 190 | - | } | |
| 191 | - | } | |
| 215 | + | export async function ShowFile( | |
| 216 | + | filename: string, | |
| 217 | + | uuid: string, | |
| 218 | + | content: Uint8Array | null, | |
| 219 | + | filetype: string, | |
| 220 | + | delete_at: number | null, | |
| 221 | + | ) { | |
| 222 | + | //if content is null, this is for the js frontend | |
| 223 | + | let preview: JSX.Element; | |
| 224 | + | if (!content) { | |
| 225 | + | preview = <>Please wait for the file to load</>; | |
| 226 | + | } else { | |
| 227 | + | preview = <>This file can't be previewed</>; | |
| 228 | + | if (isValidUTF8(Buffer.from(content)) && filetype !== "blob") { | |
| 229 | + | if (filetype === "none") { | |
| 230 | + | preview = ( | |
| 231 | + | <pre>{escapeHTML(new TextDecoder("utf-8").decode(content))}</pre> | |
| 232 | + | ); | |
| 233 | + | } else { | |
| 234 | + | preview = ( | |
| 235 | + | <pre> | |
| 236 | + | { | |
| 237 | + | hljs.highlight(new TextDecoder("utf-8").decode(content), { | |
| 238 | + | language: filetype, | |
| 239 | + | }).value | |
| 240 | + | } | |
| 241 | + | </pre> | |
| 242 | + | ); | |
| 243 | + | } | |
| 244 | + | } else { | |
| 245 | + | const filetype = await fileTypeFromBuffer(content); | |
| 246 | + | if (filetype) { | |
| 247 | + | const base64Content = `data:${filetype.mime};base64,${Buffer.from(content).toString("base64")}`; | |
| 248 | + | if (filetype.mime.startsWith("audio/")) { | |
| 249 | + | preview = <audio controls="" src={base64Content} />; | |
| 250 | + | } else if (filetype.mime.startsWith("video/")) { | |
| 251 | + | preview = <video controls src={base64Content} />; | |
| 252 | + | } else if (filetype.mime.startsWith("image/")) { | |
| 253 | + | preview = <img src={base64Content} alt={filename} />; | |
| 254 | + | } | |
| 255 | + | } | |
| 256 | + | } | |
| 257 | + | } | |
| 192 | 258 | ||
| 193 | - | return ( | |
| 194 | - | <Template css="/show.css"> | |
| 195 | - | <dialog id="password-dialog"> | |
| 196 | - | <form onsubmit="onPasswordSubmit(); return false"> | |
| 197 | - | <label id="password-label" for="password">File is encrypted, enter password to decrypt:</label> | |
| 198 | - | <input required type="password" id="password" /> | |
| 199 | - | <input type="submit" value="Submit" /> | |
| 200 | - | </form> | |
| 201 | - | </dialog> | |
| 202 | - | <div id="content"> | |
| 203 | - | <div id="filename">{filename}</div> | |
| 204 | - | <div id="mediabox">{preview}</div> | |
| 205 | - | </div> | |
| 206 | - | <div id="sidebar"> | |
| 207 | - | <h3>File info</h3> | |
| 208 | - | <hr /> | |
| 209 | - | <p id="filesize">size: {content ? humanFileSize(content.byteLength) : "unknown"}</p> | |
| 210 | - | <p>declared type: {filetype}</p> | |
| 211 | - | {delete_at ? <p>delete at: {new Date(delete_at * 1000).toISOString()} (in {humanReadableTime(Math.floor((delete_at - (Date.now() / 1000)) / 60))})</p> : ""} | |
| 212 | - | <form id="download-form" action={`/raw/${uuid}`} method="get"> | |
| 213 | - | <button>Download</button> | |
| 214 | - | </form> | |
| 215 | - | </div> | |
| 216 | - | {content ? "" : <script src="/dist/client-show.js" />} | |
| 217 | - | </Template> | |
| 218 | - | ) | |
| 259 | + | return ( | |
| 260 | + | <Template css="/show.css"> | |
| 261 | + | <dialog id="password-dialog"> | |
| 262 | + | <form onsubmit="onPasswordSubmit(); return false"> | |
| 263 | + | <label id="password-label" for="password"> | |
| 264 | + | File is encrypted, enter password to decrypt: | |
| 265 | + | </label> | |
| 266 | + | <input required type="password" id="password" /> | |
| 267 | + | <input type="submit" value="Submit" /> | |
| 268 | + | </form> | |
| 269 | + | </dialog> | |
| 270 | + | <div id="content"> | |
| 271 | + | <div id="filename">{filename}</div> | |
| 272 | + | <div id="mediabox">{preview}</div> | |
| 273 | + | </div> | |
| 274 | + | <div id="sidebar"> | |
| 275 | + | <h3>File info</h3> | |
| 276 | + | <hr /> | |
| 277 | + | <p id="filesize"> | |
| 278 | + | size: {content ? humanFileSize(content.byteLength) : "unknown"} | |
| 279 | + | </p> | |
| 280 | + | <p>declared type: {filetype}</p> | |
| 281 | + | {delete_at ? ( | |
| 282 | + | <p> | |
| 283 | + | delete at: {new Date(delete_at * 1000).toISOString()} (in{" "} | |
| 284 | + | {humanReadableTime( | |
| 285 | + | Math.floor((delete_at - Date.now() / 1000) / 60), | |
| 286 | + | )} | |
| 287 | + | ) | |
| 288 | + | </p> | |
| 289 | + | ) : ( | |
| 290 | + | "" | |
| 291 | + | )} | |
| 292 | + | <form id="download-form" action={`/raw/${uuid}`} method="get"> | |
| 293 | + | <button type="submit">Download</button> | |
| 294 | + | </form> | |
| 295 | + | </div> | |
| 296 | + | {content ? "" : <script src="/dist/client-show.js" />} | |
| 297 | + | </Template> | |
| 298 | + | ); | |
| 219 | 299 | } | |
| 220 | 300 | ||
| 221 | 301 | export function SetCookie(uuid: string) { | |
| 222 | - | return ( | |
| 223 | - | <Template css="/default.css"> | |
| 224 | - | Redirecting you back to <a href={`/show/${uuid}`}>/show/{uuid}</a> | |
| 225 | - | </Template> | |
| 226 | - | ) | |
| 302 | + | return ( | |
| 303 | + | <Template css="/default.css"> | |
| 304 | + | Redirecting you back to <a href={`/show/${uuid}`}>/show/{uuid}</a> | |
| 305 | + | </Template> | |
| 306 | + | ); | |
| 227 | 307 | } | |
Msrc/index.ts
| @@ -1,174 +1,265 @@ | |||
|---|---|---|---|
| 1 | - | import { Elysia, StatusMap, t } from "elysia" | |
| 2 | - | import { Database } from "bun:sqlite" | |
| 3 | - | import { randomUUIDv7 } from "bun" | |
| 4 | - | import staticPlugin from "@elysiajs/static" | |
| 5 | - | import { html } from '@elysiajs/html' | |
| 6 | - | import crypto from 'crypto' | |
| 7 | - | import { DecryptFile, filetypes, Index, NotFound, SetCookie, ShowFile, WrongPassword } from "./components" | |
| 8 | - | import cron from "@elysiajs/cron" | |
| 9 | - | ||
| 10 | - | const db = new Database("./db/db.sqlite") | |
| 11 | - | db.exec("PRAGMA foreign_keys = ON") | |
| 12 | - | db.exec("PRAGMA journal_mode = WAL2") | |
| 13 | - | db.exec("CREATE TABLE IF NOT EXISTS files (uuid TEXT PRIMARY KEY, filename TEXT NOT NULL, content BLOB NOT NULL, filetype TEXT NOT NULL, encrypted INTEGER NOT NULL, delete_at INTEGER) STRICT") | |
| 14 | - | db.exec("PRAGMA optimize") | |
| 1 | + | import { Database } from "bun:sqlite"; | |
| 2 | + | import crypto from "node:crypto"; | |
| 3 | + | import cron from "@elysiajs/cron"; | |
| 4 | + | import { html } from "@elysiajs/html"; | |
| 5 | + | import staticPlugin from "@elysiajs/static"; | |
| 6 | + | import { randomUUIDv7 } from "bun"; | |
| 7 | + | import { Elysia, StatusMap, t } from "elysia"; | |
| 8 | + | import { | |
| 9 | + | DecryptFile, | |
| 10 | + | filetypes, | |
| 11 | + | Index, | |
| 12 | + | NotFound, | |
| 13 | + | SetCookie, | |
| 14 | + | ShowFile, | |
| 15 | + | WrongPassword, | |
| 16 | + | } from "./components"; | |
| 15 | 17 | ||
| 18 | + | const db = new Database("./db/db.sqlite"); | |
| 19 | + | db.run("PRAGMA foreign_keys = ON"); | |
| 20 | + | db.run("PRAGMA journal_mode = WAL2"); | |
| 21 | + | db.run( | |
| 22 | + | "CREATE TABLE IF NOT EXISTS files (uuid TEXT PRIMARY KEY, filename TEXT NOT NULL, content BLOB NOT NULL, filetype TEXT NOT NULL, encrypted INTEGER NOT NULL, delete_at INTEGER) STRICT", | |
| 23 | + | ); | |
| 24 | + | db.run("PRAGMA optimize"); | |
| 16 | 25 | ||
| 17 | 26 | function encrypt(content: Uint8Array, password: string): Buffer<ArrayBuffer> { | |
| 18 | - | let iv = crypto.randomBytes(16) | |
| 19 | - | const salt = crypto.randomBytes(16) | |
| 20 | - | const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512") | |
| 21 | - | let cipher = crypto.createCipheriv('aes-256-cbc', key, iv) | |
| 22 | - | let encrypted_content = Buffer.concat([salt, iv, cipher.update(content), cipher.final()]) | |
| 23 | - | return encrypted_content | |
| 27 | + | const iv = crypto.randomBytes(16); | |
| 28 | + | const salt = crypto.randomBytes(16); | |
| 29 | + | const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512"); | |
| 30 | + | const cipher = crypto.createCipheriv("aes-256-cbc", key, iv); | |
| 31 | + | return Buffer.concat([salt, iv, cipher.update(content), cipher.final()]); | |
| 24 | 32 | } | |
| 25 | 33 | ||
| 26 | 34 | function decrypt(encrypted_content: Uint8Array, password: string): Buffer { | |
| 27 | - | const salt = encrypted_content.slice(0, 16) | |
| 28 | - | const iv = encrypted_content.slice(16, 32) | |
| 29 | - | const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512") | |
| 30 | - | let decipher = crypto.createDecipheriv('aes-256-cbc', key, iv) | |
| 31 | - | let content = Buffer.concat([decipher.update(encrypted_content.slice(32)), decipher.final()]) | |
| 32 | - | return content | |
| 35 | + | const salt = encrypted_content.slice(0, 16); | |
| 36 | + | const iv = encrypted_content.slice(16, 32); | |
| 37 | + | const key = crypto.pbkdf2Sync(password, salt, 100000, 32, "sha512"); | |
| 38 | + | const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); | |
| 39 | + | return Buffer.concat([ | |
| 40 | + | decipher.update(encrypted_content.slice(32)), | |
| 41 | + | decipher.final(), | |
| 42 | + | ]); | |
| 33 | 43 | } | |
| 34 | 44 | ||
| 35 | 45 | function stringArrayToEnum<T extends string>( | |
| 36 | - | arr: readonly T[] | |
| 46 | + | arr: readonly T[], | |
| 37 | 47 | ): { [K in T]: K } { | |
| 38 | - | return arr.reduce((acc, key) => { | |
| 39 | - | acc[key] = key; | |
| 40 | - | return acc; | |
| 41 | - | }, Object.create(null)); | |
| 48 | + | return arr.reduce((acc, key) => { | |
| 49 | + | acc[key] = key; | |
| 50 | + | return acc; | |
| 51 | + | }, Object.create(null)); | |
| 42 | 52 | } | |
| 43 | 53 | ||
| 44 | 54 | const app = new Elysia({ | |
| 45 | - | serve: { | |
| 46 | - | maxRequestBodySize: 1024 * 1024 * 1024 // 1GB | |
| 47 | - | } | |
| 55 | + | serve: { | |
| 56 | + | maxRequestBodySize: 1024 * 1024 * 1024, // 1GB | |
| 57 | + | }, | |
| 48 | 58 | }) | |
| 49 | - | .use(staticPlugin({ assets: "./assets", prefix: "/" })) | |
| 50 | - | .use(html()) | |
| 51 | - | .use(cron({ | |
| 52 | - | name: "delete", | |
| 53 | - | pattern: "*/5 * * * * *", | |
| 54 | - | run() { | |
| 55 | - | db.exec("DELETE FROM files WHERE delete_at < strftime('%s', 'now')") | |
| 56 | - | } | |
| 57 | - | })) | |
| 58 | - | .get("/", ({ server }) => Index(server?.url.toString()!, false)) | |
| 59 | - | .get("/js", ({ server }) => Index(server?.url.toString()!, true)) | |
| 60 | - | .post("/upload", async ({ set, body, query }) => { | |
| 61 | - | ||
| 62 | - | const uuid = randomUUIDv7() | |
| 63 | - | let content = Buffer.from(await body.file.bytes()) | |
| 64 | - | let encrypted = false; | |
| 65 | - | let delete_at: number | null = null | |
| 66 | - | if (body.delete_in_minutes) { | |
| 67 | - | delete_at = Math.floor(Date.now() / 1000) + Number(body.delete_in_minutes) * 60 | |
| 68 | - | } | |
| 69 | - | if (body.encrypted === "on") { | |
| 70 | - | encrypted = true | |
| 71 | - | } else if (body.password) { | |
| 72 | - | content = encrypt(content, body.password) | |
| 73 | - | encrypted = true | |
| 74 | - | } | |
| 75 | - | db.exec("INSERT INTO files (uuid, filename, content, filetype, encrypted, delete_at) VALUES (?, ?, ?, ?, ?, ?)", | |
| 76 | - | [uuid, body.filename || body.file.name, content, body.filetype, encrypted, delete_at]) | |
| 77 | - | set.status = StatusMap["See Other"] | |
| 78 | - | if (query.withJs) { | |
| 79 | - | set.headers["location"] = `/show-js/${uuid}` | |
| 80 | - | } else { | |
| 81 | - | set.headers["location"] = `/show/${uuid}` | |
| 82 | - | } | |
| 83 | - | return `Created with id: ${uuid}` | |
| 84 | - | ||
| 85 | - | }, { | |
| 86 | - | body: t.Object({ | |
| 87 | - | file: t.File(), | |
| 88 | - | filename: t.Optional(t.String()), | |
| 89 | - | //filetype: t.String()/*todo:verifiy via t.Enum*/, | |
| 90 | - | filetype: t.Enum(stringArrayToEnum(filetypes)), | |
| 91 | - | password: t.Optional(t.String()), | |
| 92 | - | encrypted: t.Optional(t.String()), | |
| 93 | - | delete_in_minutes: t.String({ format: "regex", pattern: "(^$|^[0-9]+$)" }) | |
| 94 | - | }), | |
| 95 | - | query: t.Object({ withJs: t.Optional(t.Boolean()) }) | |
| 96 | - | }) | |
| 97 | - | .get("/show/:uuid", async ({ set, params, cookie }) => { | |
| 98 | - | ||
| 99 | - | const result = db.prepare("SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?").get(params.uuid) as | |
| 100 | - | { filename: string, content: Uint8Array, filetype: string, encrypted: boolean, delete_at: number | null } || null | |
| 101 | - | if (!result) { | |
| 102 | - | set.status = StatusMap["Not Found"] | |
| 103 | - | return NotFound() | |
| 104 | - | } | |
| 105 | - | if (result.encrypted) { | |
| 106 | - | const password = cookie.password.value | |
| 107 | - | if (!password) { | |
| 108 | - | return DecryptFile(result.filename, params.uuid) | |
| 109 | - | } else { | |
| 110 | - | try { | |
| 111 | - | result.content = decrypt(result.content, password) | |
| 112 | - | } catch (e) { | |
| 113 | - | set.status = StatusMap["Forbidden"] | |
| 114 | - | set.headers["set-cookie"] = [ | |
| 115 | - | `password=; Path=/show/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`, | |
| 116 | - | `password=; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT` | |
| 117 | - | ] | |
| 118 | - | return WrongPassword() | |
| 119 | - | } | |
| 120 | - | } | |
| 121 | - | } | |
| 122 | - | return ShowFile(result.filename, params.uuid, result.content, result.filetype, result.delete_at) | |
| 123 | - | ||
| 124 | - | }, { params: t.Object({ uuid: t.String() }) }) | |
| 125 | - | .get("/show-js/:uuid", async ({ set, params, cookie }) => { | |
| 126 | - | ||
| 127 | - | const result = db.prepare("SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?").get(params.uuid) as | |
| 128 | - | { filename: string, content: Uint8Array, filetype: string, encrypted: boolean, delete_at: number | null } || null | |
| 129 | - | if (!result) { | |
| 130 | - | set.status = StatusMap["Not Found"] | |
| 131 | - | return NotFound() | |
| 132 | - | } | |
| 133 | - | return ShowFile(result.filename, params.uuid, null, result.filetype, result.delete_at) | |
| 134 | - | ||
| 135 | - | }, { params: t.Object({ uuid: t.String() }) }) | |
| 136 | - | .post("/set-cookie/:uuid", ({ set, body, params }) => { | |
| 137 | - | ||
| 138 | - | set.headers["set-cookie"] = [ | |
| 139 | - | `password=${body.password}; Path=/show/${params.uuid}; SameSite=lax; HttpOnly`, | |
| 140 | - | `password=${body.password}; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly` | |
| 141 | - | ] | |
| 142 | - | set.headers["location"] = `/show/${params.uuid}` | |
| 143 | - | set.status = StatusMap["See Other"] | |
| 144 | - | return SetCookie(params.uuid) | |
| 145 | - | ||
| 146 | - | }, { body: t.Object({ password: t.String() }), params: t.Object({ uuid: t.String() }) }) | |
| 147 | - | .get("/raw/:uuid", ({ set, params, cookie, query }) => { | |
| 148 | - | const result = db.prepare("SELECT content, filename, encrypted, filetype FROM files WHERE uuid = ?").get(params.uuid) as { content: Uint8Array, filename: string, encrypted: boolean, filetype: string } || null | |
| 149 | - | if (!result) { | |
| 150 | - | set.status = StatusMap["Not Found"] | |
| 151 | - | return "File not found" | |
| 152 | - | } | |
| 153 | - | if (result.encrypted && query.ignore_password !== "true") { | |
| 154 | - | if (!cookie.password.value) { | |
| 155 | - | set.status = StatusMap["Unauthorized"] | |
| 156 | - | return "This file is encrypted, set the cookie \"password\" with the correct password to allow the server to decrypt it" | |
| 157 | - | } | |
| 158 | - | try { | |
| 159 | - | result.content = decrypt(result.content, cookie.password.value) | |
| 160 | - | } catch (e) { | |
| 161 | - | set.status = StatusMap["Forbidden"] | |
| 162 | - | return "Incorrect password" | |
| 163 | - | } | |
| 164 | - | } | |
| 165 | - | set.headers["encrypted"] = result.encrypted ? "true" : "false" | |
| 166 | - | set.headers["filetype"] = result.filetype | |
| 167 | - | set.headers["filename"] = result.filename | |
| 168 | - | set.headers["content-disposition"] = `inline; filename=${result.filename}` | |
| 169 | - | return result.content | |
| 170 | - | ||
| 171 | - | }, { params: t.Object({ uuid: t.String() }), cookie: t.Object({ password: t.Optional(t.String()) }), query: t.Object({ ignore_password: t.Optional(t.String()) }) }) | |
| 172 | - | .listen(3000) | |
| 59 | + | .use(staticPlugin({ assets: "./assets", prefix: "/" })) | |
| 60 | + | .use(html()) | |
| 61 | + | .use( | |
| 62 | + | cron({ | |
| 63 | + | name: "delete", | |
| 64 | + | pattern: "*/5 * * * * *", | |
| 65 | + | run() { | |
| 66 | + | db.exec("DELETE FROM files WHERE delete_at < strftime('%s', 'now')"); | |
| 67 | + | }, | |
| 68 | + | }), | |
| 69 | + | ) | |
| 70 | + | .get("/", ({ server }) => Index(server?.url.toString() ?? "", false)) | |
| 71 | + | .get("/js", ({ server }) => Index(server?.url.toString() ?? "", true)) | |
| 72 | + | .post( | |
| 73 | + | "/upload", | |
| 74 | + | async ({ set, body, query }) => { | |
| 75 | + | const uuid = randomUUIDv7(); | |
| 76 | + | let content = Buffer.from(await body.file.bytes()); | |
| 77 | + | let encrypted = false; | |
| 78 | + | let delete_at: number | null = null; | |
| 79 | + | if (body.delete_in_minutes) { | |
| 80 | + | delete_at = | |
| 81 | + | Math.floor(Date.now() / 1000) + Number(body.delete_in_minutes) * 60; | |
| 82 | + | } | |
| 83 | + | if (body.encrypted === "on") { | |
| 84 | + | encrypted = true; | |
| 85 | + | } else if (body.password) { | |
| 86 | + | content = encrypt(content, body.password); | |
| 87 | + | encrypted = true; | |
| 88 | + | } | |
| 89 | + | db.exec( | |
| 90 | + | "INSERT INTO files (uuid, filename, content, filetype, encrypted, delete_at) VALUES (?, ?, ?, ?, ?, ?)", | |
| 91 | + | [ | |
| 92 | + | uuid, | |
| 93 | + | body.filename || body.file.name, | |
| 94 | + | content, | |
| 95 | + | body.filetype, | |
| 96 | + | encrypted, | |
| 97 | + | delete_at, | |
| 98 | + | ], | |
| 99 | + | ); | |
| 100 | + | set.status = StatusMap["See Other"]; | |
| 101 | + | if (query.withJs) { | |
| 102 | + | set.headers.location = `/show-js/${uuid}`; | |
| 103 | + | } else { | |
| 104 | + | set.headers.location = `/show/${uuid}`; | |
| 105 | + | } | |
| 106 | + | return `Created with id: ${uuid}`; | |
| 107 | + | }, | |
| 108 | + | { | |
| 109 | + | body: t.Object({ | |
| 110 | + | file: t.File(), | |
| 111 | + | filename: t.Optional(t.String()), | |
| 112 | + | //filetype: t.String()/*todo:verifiy via t.Enum*/, | |
| 113 | + | filetype: t.Enum(stringArrayToEnum(filetypes)), | |
| 114 | + | password: t.Optional(t.String()), | |
| 115 | + | encrypted: t.Optional(t.String()), | |
| 116 | + | delete_in_minutes: t.String({ | |
| 117 | + | format: "regex", | |
| 118 | + | pattern: "(^$|^[0-9]+$)", | |
| 119 | + | }), | |
| 120 | + | }), | |
| 121 | + | query: t.Object({ withJs: t.Optional(t.Boolean()) }), | |
| 122 | + | }, | |
| 123 | + | ) | |
| 124 | + | .get( | |
| 125 | + | "/show/:uuid", | |
| 126 | + | async ({ set, params, cookie }) => { | |
| 127 | + | const result = | |
| 128 | + | (db | |
| 129 | + | .prepare( | |
| 130 | + | "SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?", | |
| 131 | + | ) | |
| 132 | + | .get(params.uuid) as { | |
| 133 | + | filename: string; | |
| 134 | + | content: Uint8Array; | |
| 135 | + | filetype: string; | |
| 136 | + | encrypted: boolean; | |
| 137 | + | delete_at: number | null; | |
| 138 | + | }) || null; | |
| 139 | + | if (!result) { | |
| 140 | + | set.status = StatusMap["Not Found"]; | |
| 141 | + | return NotFound(); | |
| 142 | + | } | |
| 143 | + | if (result.encrypted) { | |
| 144 | + | const password = cookie.password.value; | |
| 145 | + | if (!password) { | |
| 146 | + | return DecryptFile(result.filename, params.uuid); | |
| 147 | + | } else { | |
| 148 | + | try { | |
| 149 | + | result.content = decrypt(result.content, password); | |
| 150 | + | } catch (_e) { | |
| 151 | + | set.status = StatusMap.Forbidden; | |
| 152 | + | set.headers["set-cookie"] = [ | |
| 153 | + | `password=; Path=/show/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`, | |
| 154 | + | `password=; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT`, | |
| 155 | + | ]; | |
| 156 | + | return WrongPassword(); | |
| 157 | + | } | |
| 158 | + | } | |
| 159 | + | } | |
| 160 | + | return ShowFile( | |
| 161 | + | result.filename, | |
| 162 | + | params.uuid, | |
| 163 | + | result.content, | |
| 164 | + | result.filetype, | |
| 165 | + | result.delete_at, | |
| 166 | + | ); | |
| 167 | + | }, | |
| 168 | + | { | |
| 169 | + | params: t.Object({ uuid: t.String() }), | |
| 170 | + | cookie: t.Object({ password: t.Optional(t.String()) }), | |
| 171 | + | }, | |
| 172 | + | ) | |
| 173 | + | .get( | |
| 174 | + | "/show-js/:uuid", | |
| 175 | + | async ({ set, params }) => { | |
| 176 | + | const result = | |
| 177 | + | (db | |
| 178 | + | .prepare( | |
| 179 | + | "SELECT filename, content, filetype, encrypted, delete_at FROM files WHERE uuid = ?", | |
| 180 | + | ) | |
| 181 | + | .get(params.uuid) as { | |
| 182 | + | filename: string; | |
| 183 | + | content: Uint8Array; | |
| 184 | + | filetype: string; | |
| 185 | + | encrypted: boolean; | |
| 186 | + | delete_at: number | null; | |
| 187 | + | }) || null; | |
| 188 | + | if (!result) { | |
| 189 | + | set.status = StatusMap["Not Found"]; | |
| 190 | + | return NotFound(); | |
| 191 | + | } | |
| 192 | + | return ShowFile( | |
| 193 | + | result.filename, | |
| 194 | + | params.uuid, | |
| 195 | + | null, | |
| 196 | + | result.filetype, | |
| 197 | + | result.delete_at, | |
| 198 | + | ); | |
| 199 | + | }, | |
| 200 | + | { params: t.Object({ uuid: t.String() }) }, | |
| 201 | + | ) | |
| 202 | + | .post( | |
| 203 | + | "/set-cookie/:uuid", | |
| 204 | + | ({ set, body, params }) => { | |
| 205 | + | set.headers["set-cookie"] = [ | |
| 206 | + | `password=${body.password}; Path=/show/${params.uuid}; SameSite=lax; HttpOnly`, | |
| 207 | + | `password=${body.password}; Path=/raw/${params.uuid}; SameSite=lax; HttpOnly`, | |
| 208 | + | ]; | |
| 209 | + | set.headers.location = `/show/${params.uuid}`; | |
| 210 | + | set.status = StatusMap["See Other"]; | |
| 211 | + | return SetCookie(params.uuid); | |
| 212 | + | }, | |
| 213 | + | { | |
| 214 | + | body: t.Object({ password: t.String() }), | |
| 215 | + | params: t.Object({ uuid: t.String() }), | |
| 216 | + | }, | |
| 217 | + | ) | |
| 218 | + | .get( | |
| 219 | + | "/raw/:uuid", | |
| 220 | + | ({ set, params, cookie, query }) => { | |
| 221 | + | const result = | |
| 222 | + | (db | |
| 223 | + | .prepare( | |
| 224 | + | "SELECT content, filename, encrypted, filetype FROM files WHERE uuid = ?", | |
| 225 | + | ) | |
| 226 | + | .get(params.uuid) as { | |
| 227 | + | content: Uint8Array; | |
| 228 | + | filename: string; | |
| 229 | + | encrypted: boolean; | |
| 230 | + | filetype: string; | |
| 231 | + | }) || null; | |
| 232 | + | if (!result) { | |
| 233 | + | set.status = StatusMap["Not Found"]; | |
| 234 | + | return "File not found"; | |
| 235 | + | } | |
| 236 | + | if (result.encrypted && query.ignore_password !== "true") { | |
| 237 | + | if (!cookie.password.value) { | |
| 238 | + | set.status = StatusMap.Unauthorized; | |
| 239 | + | return 'This file is encrypted, set the cookie "password" with the correct password to allow the server to decrypt it'; | |
| 240 | + | } | |
| 241 | + | try { | |
| 242 | + | result.content = decrypt(result.content, cookie.password.value); | |
| 243 | + | } catch (_e) { | |
| 244 | + | set.status = StatusMap.Forbidden; | |
| 245 | + | return "Incorrect password"; | |
| 246 | + | } | |
| 247 | + | } | |
| 248 | + | set.headers.encrypted = result.encrypted ? "true" : "false"; | |
| 249 | + | set.headers.filetype = result.filetype; | |
| 250 | + | set.headers.filename = result.filename; | |
| 251 | + | set.headers["content-disposition"] = | |
| 252 | + | `inline; filename=${result.filename}`; | |
| 253 | + | return result.content; | |
| 254 | + | }, | |
| 255 | + | { | |
| 256 | + | params: t.Object({ uuid: t.String() }), | |
| 257 | + | cookie: t.Object({ password: t.Optional(t.String()) }), | |
| 258 | + | query: t.Object({ ignore_password: t.Optional(t.String()) }), | |
| 259 | + | }, | |
| 260 | + | ) | |
| 261 | + | .listen(3000); | |
| 173 | 262 | ||
| 174 | - | console.log(`⚡ ZBin is running at ${app.server?.hostname}:${app.server?.port} ⚡`) | |
| 263 | + | console.log( | |
| 264 | + | `⚡ ZBin is running at ${app.server?.hostname}:${app.server?.port} ⚡`, | |
| 265 | + | ); | |
Mtsconfig.json
| @@ -1,18 +1,18 @@ | |||
|---|---|---|---|
| 1 | 1 | { | |
| 2 | - | "compilerOptions": { | |
| 3 | - | /* Visit https://aka.ms/tsconfig to read more about this file */ | |
| 4 | - | "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ | |
| 5 | - | "module": "ES2022", /* Specify what module code is generated. */ | |
| 6 | - | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ | |
| 7 | - | "types": [ | |
| 8 | - | "bun-types" | |
| 9 | - | ], /* Specify type package names to be included without being referenced in a source file. */ | |
| 10 | - | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ | |
| 11 | - | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ | |
| 12 | - | "strict": true, /* Enable all strict type-checking options. */ | |
| 13 | - | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ | |
| 14 | - | "jsx": "react", | |
| 15 | - | "jsxFactory": "Html.createElement", | |
| 16 | - | "jsxFragmentFactory": "Html.Fragment" | |
| 17 | - | } | |
| 18 | - | } | |
| 2 | + | "compilerOptions": { | |
| 3 | + | /* Visit https://aka.ms/tsconfig to read more about this file */ | |
| 4 | + | "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, | |
| 5 | + | "module": "ES2022" /* Specify what module code is generated. */, | |
| 6 | + | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, | |
| 7 | + | "types": [ | |
| 8 | + | "bun-types" | |
| 9 | + | ] /* Specify type package names to be included without being referenced in a source file. */, | |
| 10 | + | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, | |
| 11 | + | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, | |
| 12 | + | "strict": true /* Enable all strict type-checking options. */, | |
| 13 | + | "skipLibCheck": true /* Skip type checking all .d.ts files. */, | |
| 14 | + | "jsx": "react", | |
| 15 | + | "jsxFactory": "Html.createElement", | |
| 16 | + | "jsxFragmentFactory": "Html.Fragment" | |
| 17 | + | } | |
| 18 | + | } | |