diff --git a/.dockerignore b/.dockerignore index 49aa802..20f3c2a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ * -!dist/backend.step2.tmp.js +!src/ +!bun.lockb !bunfig.toml !LICENSE !package.json diff --git a/.env.example b/.env.example index 27b7622..caf4a9a 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ ## SERVER: # Set log verbosity [2]:integer -# 0=error <- 1=warn <- 2=info <- 3=debug +# (0=error <- 1=warn <- 2=info <- 3=debug) #LOGLEVEL=2 # Port for the server [4000]:integer diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aec1fef..c904b23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,28 +1,28 @@ -name: "CD -> Release" +name: CD -> Release on: workflow_dispatch: inputs: artifact-action: - description: "Artifact action" + description: Artifact action type: choice required: true - default: "none" + default: none options: - - "none" - - "build" - - "build-release" + - none + - build + - build-release image-action: - description: "Container image action" + description: Container image action type: choice required: true - default: "none" + default: none options: - - "none" - - "build" - - "build-release" + - none + - build + - build-release concurrency: - group: "${{ github.workflow }}-${{ github.ref }}" + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false permissions: @@ -31,7 +31,7 @@ permissions: jobs: artifact: if: ${{ github.repository_owner == 'jspaste' && inputs.artifact-action != 'none' }} - name: "Build artifact" + name: Build artifact runs-on: ubuntu-latest permissions: attestations: write @@ -39,15 +39,15 @@ jobs: id-token: write steps: - - name: "Harden Runner" + - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit - - name: "Setup Bun" + - name: Setup Bun uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 - - name: "Setup tags" + - name: Setup tags id: tags-artifact run: | TIMESTAMP="$(date +%Y.%m.%d)" @@ -62,41 +62,49 @@ jobs: echo "tag=${TAG}" >>"$GITHUB_OUTPUT" echo "extended=${TIMESTAMP}-${GITHUB_SHA_SHORT}" >>"$GITHUB_OUTPUT" - - name: "Checkout" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - name: Checkout + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false - - name: "Setup production dependencies" + - name: Setup production dependencies run: bun install --frozen-lockfile --production - - name: "Build artifact" + - name: Build artifact run: | bun run build:standalone:darwin-arm64 - tar -czf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz .env.example LICENSE README.md -C ./dist/ backend + chmod 755 ./dist/backend + tar -c --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend | gzip --best >./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz + tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz >/dev/null bun run build:standalone:linux-amd64 - tar -czf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz .env.example LICENSE README.md -C ./dist/ backend + chmod 755 ./dist/backend + tar -c --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend | gzip --best >./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz + tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz >/dev/null bun run build:standalone:linux-arm64 - tar -czf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz .env.example LICENSE README.md -C ./dist/ backend + chmod 755 ./dist/backend + tar -c --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend | gzip --best >./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz + tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz >/dev/null bun run build:standalone:windows-amd64 - zip -j -X ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_windows-amd64.zip .env.example LICENSE README.md ./dist/backend.exe + chmod 755 ./dist/backend.exe + zip -j -X -9 -l -o ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_windows-amd64.zip .env.example LICENSE README.md ./dist/backend.exe + zip -T ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_windows-amd64.zip - if: ${{ inputs.artifact-action == 'build-release' }} - name: "Release artifact" + name: Release artifact uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 with: name: ${{ steps.tags-artifact.outputs.extended }} tag: ${{ steps.tags-artifact.outputs.extended }} - artifacts: "dist/*.tar.gz,dist/*.zip" + artifacts: dist/*.tar.gz,dist/*.zip makeLatest: true prerelease: ${{ github.ref != 'refs/heads/stable' }} generateReleaseNotes: true - if: ${{ inputs.artifact-action == 'build-release' }} - name: "Attest artifact" + name: Attest artifact uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-path: | @@ -105,7 +113,7 @@ jobs: container: if: ${{ github.repository_owner == 'jspaste' && inputs.image-action != 'none' }} - name: "Build container image" + name: Build container image runs-on: ubuntu-latest env: REGISTRY: ghcr.io @@ -116,20 +124,17 @@ jobs: packages: write steps: - - name: "Harden Runner" + - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit - - name: "Setup Bun" - uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 - - - name: "Setup QEMU" + - name: Setup QEMU run: | sudo apt-get update sudo apt-get install -y qemu-user-static - - name: "Setup tags" + - name: Setup tags id: tags-image run: | TIMESTAMP="$(date +%Y.%m.%d)" @@ -148,18 +153,12 @@ jobs: echo "tags=${TAGS[*]}" >>"$GITHUB_OUTPUT" - - name: "Checkout" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - name: Checkout + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false - - name: "Setup production dependencies" - run: bun install --frozen-lockfile --production - - - name: "Run build" - run: bun run build - - - name: "Build image" + - name: Build image id: build-image uses: redhat-actions/buildah-build@7a95fa7ee0f02d552a32753e7414641a04307056 # v2.13 with: @@ -171,7 +170,7 @@ jobs: tags: ${{ steps.tags-image.outputs.tags }} - if: ${{ inputs.image-action == 'build-release' }} - name: "Login to GHCR" + name: Login to GHCR uses: redhat-actions/podman-login@4934294ad0449894bcd1e9f191899d7292469603 # v1.7 with: username: ${{ github.repository_owner }} @@ -179,7 +178,7 @@ jobs: registry: ${{ env.REGISTRY }} - if: ${{ inputs.image-action == 'build-release' }} - name: "Push to GHCR" + name: Push to GHCR id: push-image uses: redhat-actions/push-to-registry@5ed88d269cf581ea9ef6dd6806d01562096bee9c # v2.8 with: @@ -188,7 +187,7 @@ jobs: registry: ${{ env.REGISTRY }} - if: ${{ inputs.image-action == 'build-release' }} - name: "Attest image" + name: Attest image uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-name: "${{ env.REGISTRY }}/${{ steps.build-image.outputs.image }}" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c3d379c..967f063 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,87 +1,52 @@ -name: "CI -> Security" +name: CI -> Security on: branch_protection_rule: schedule: - - cron: "33 3 * * 1" + - cron: 33 3 * * 1 push: branches: - dev paths-ignore: - - "*.md" - - ".*ignore" + - '*.md' + - '.*ignore' pull_request: branches: - dev paths-ignore: - - "*.md" - - ".*ignore" + - '*.md' + - '.*ignore' permissions: read-all jobs: - codeql: - name: "CodeQL" - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - language: javascript-typescript - build-mode: none - - permissions: - security-events: write - - steps: - - name: "Harden Runner" - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 - with: - egress-policy: audit - - - name: "Checkout" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - with: - persist-credentials: false - - - name: "Setup CodeQL" - uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - - name: "Run analysis" - uses: github/codeql-action/analyze@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 - with: - category: "/language:${{matrix.language}}" - scoreboard: - name: "Scorecard" + name: Scorecard runs-on: ubuntu-latest permissions: security-events: write id-token: write steps: - - name: "Harden Runner" + - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit - - name: "Checkout" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - name: Checkout + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false - - name: "Run analysis" + - name: Run analysis uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: scoreboard.sarif results_format: sarif publish_results: true - - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + - name: Upload to code-scanning + uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: sarif_file: scoreboard.sarif diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 808f560..57c1da9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,53 +1,53 @@ -name: "CI -> Test" +name: CI -> Test on: workflow_dispatch: push: branches: - dev paths-ignore: - - "*.md" - - ".*ignore" + - '*.md' + - '.*ignore' pull_request: branches: - dev paths-ignore: - - "*.md" - - ".*ignore" + - '*.md' + - '.*ignore' concurrency: - group: "${{ github.workflow }}-${{ github.ref }}" + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: - lint: - name: "Lint" + test: + name: Test runs-on: ubuntu-latest steps: - - name: "Harden Runner" + - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit - - name: "Setup Bun" + - name: Setup Bun uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 - - name: "Checkout" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - name: Checkout + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false - - name: "Setup production dependencies" + - name: Setup production dependencies run: bun install --frozen-lockfile --production - - name: "Run build" - run: bun run build + - name: Run build:server + run: bun run build:server - - name: "Setup development dependencies" + - name: Setup development dependencies run: bun install --frozen-lockfile - - name: "Run lint" + - name: Run lint run: bun run lint diff --git a/.gitignore b/.gitignore index 8b5110c..2e6c9ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ -# JSP specific -documents/ +# JSPaste specific +storage/ + +# Databases +*.db +*.sql +*.sqlite +*.sqlite3 # Logs logs diff --git a/Dockerfile b/Dockerfile index 22f66ea..1649060 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ -# Build the bundle localy before building the image: "bun run build" FROM docker.io/oven/bun:1-alpine AS builder WORKDIR /build/ COPY . ./ -RUN bun run build:x:step3:standalone +RUN bun install --frozen-lockfile --production && \ + bun run build:standalone FROM cgr.dev/chainguard/cc-dynamic:latest WORKDIR /backend/ @@ -19,7 +19,7 @@ LABEL org.opencontainers.image.url="https://jspaste.eu" \ org.opencontainers.image.documentation="https://docs.jspaste.eu" \ org.opencontainers.image.licenses="EUPL-1.2" -VOLUME /backend/documents/ +VOLUME /backend/storage/ EXPOSE 4000 ENTRYPOINT ["./backend"] \ No newline at end of file diff --git a/README.md b/README.md index 000291a..fb561d5 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,17 @@ ## Setup -### Binaries +### Binary -- Download the [latest release](https://github.com/jspaste/backend/releases/latest) -- Uncompress to a new folder -- Modify the `.env.example` file to your needs and rename it to `.env` -- Execute the binary... +- Download the [latest release](https://github.com/jspaste/backend/releases/latest) and uncompress it to a new folder +- Edit the `.env.example` file and rename it to `.env` +- Run the binary... + +Linux & macOS: + +```shell +./backend +``` Windows: @@ -18,23 +23,21 @@ Windows: powershell -c ".\backend.exe" ``` -Linux & macOS: +### Container + +- Pull latest image and run the container: ```shell -chmod +x ./backend -./backend +docker pull ghcr.io/jspaste/backend:latest +docker run -e DOCS_ENABLED=true -d -p 127.0.0.1:4000:4000 \ + ghcr.io/jspaste/backend:latest ``` -### Container - -- Pull latest image: `docker pull ghcr.io/jspaste/backend:latest` -- Run container: `docker run -e DOCS_ENABLED=true -d -p 127.0.0.1:4000:4000 ghcr.io/jspaste/backend:latest` - ## Validate > [!IMPORTANT] -> ALL artifacts and images originate from [this](https://github.com/jspaste/backend) repository, no other artifacts or -> images built and distributed outside that repository are considered secure nor trusted by the JSPaste developers. +> ALL artifacts and images originate from GitHub `JSPaste/Backend` repository, no other artifacts or +> images built and distributed outside that repository are considered secure nor trusted by the JSPaste team. Artifacts are attested and can be verified using the following command: @@ -43,8 +46,8 @@ gh attestation verify backend.tar.gz \ --owner JSPaste ``` -Since container -version [`2024.05.06-e105023`](https://github.com/orgs/jspaste/packages/container/backend/212635273?tag=2024.05.06-e105023), +Since container version +[`2024.05.06-e105023`](https://github.com/orgs/jspaste/packages/container/backend/212635273?tag=2024.05.06-e105023), images are attested and can be verified using the following command: ```shell @@ -55,6 +58,17 @@ gh attestation verify oci://ghcr.io/jspaste/backend:latest \ You can verify the integrity and origin of an artifact and/or image using the GitHub CLI or manually at [JSPaste Attestations](https://github.com/jspaste/backend/attestations). +## Development + +### Maintenance + +Over time, local repositories can become messy with untracked files, registered hooks, and temporary files in the .git +folder. To clean up the repository (and possibly all your uncommitted work), run the following command: + +```shell +bun run clean:git:all +``` + ## License This project is licensed under the EUPL License. See the [`LICENSE`](LICENSE) file for more details. diff --git a/biome.json b/biome.json index 4284218..ebe4ebc 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,7 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.3/schema.json", + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "files": { - "ignore": ["dist/**", "documents/**", "*.spec.ts"], + "ignore": ["./dist/**", "./storage/**", "*.spec.ts"], "ignoreUnknown": true }, "formatter": { diff --git a/bun.lockb b/bun.lockb index 4d84884..6b2f710 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml index a002c04..c112426 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -3,9 +3,6 @@ telemetry = false [install] auto = "disable" -[install.scopes] -"@jsr" = "https://npm.jsr.io" - [run] bun = true silent = true diff --git a/package.json b/package.json index cc07d90..8855b44 100644 --- a/package.json +++ b/package.json @@ -5,41 +5,45 @@ "license": "EUPL-1.2", "type": "module", "scripts": { - "build": "bun run build:x:step1 && bun run build:x:step2 && bun run build:x:step3", - "build:standalone": "bun run build:x:step1 && bun run build:x:step2 && bun run build:x:step3:standalone", + "build": "bun run build:server", + "build:server": "rm -rf ./dist/ && bun build ./src/server.ts --outdir=./dist/ --target=bun --minify --sourcemap=inline", + "build:standalone": "bun build ./src/server.ts --outfile=./dist/backend --compile --minify --sourcemap=inline", "build:standalone:darwin-arm64": "bun run build:standalone -- --target=bun-darwin-arm64", "build:standalone:linux-amd64": "bun run build:standalone -- --target=bun-linux-x64-modern", "build:standalone:linux-arm64": "bun run build:standalone -- --target=bun-linux-arm64", "build:standalone:windows-amd64": "bun run build:standalone -- --target=bun-windows-x64-modern", - "build:x:step1": "bun build --target=bun src/index.ts --outfile=dist/backend.step1.tmp.js", - "build:x:step2": "bun swc --config-file=swc.json --out-file=dist/backend.step2.tmp.js dist/backend.step1.tmp.js", - "build:x:step3": "bun build --target=bun --minify --outfile=dist/backend.min.js dist/backend.step2.tmp.js", - "build:x:step3:standalone": "bun build --compile --minify --outfile=dist/backend dist/backend.step2.tmp.js", - "fix": "bun run fix:biome && bun run fix:package", + "clean:git:all": "bun run clean:git:untracked && bun run clean:git:gc && bun run clean:git:hooks", + "clean:git:all:force": "bun run clean:git:untracked:force && bun run clean:git:gc && bun run clean:git:hooks", + "clean:git:gc": "git gc --aggressive --prune", + "clean:git:hooks": "rm -rf ./.git/hooks/ && bun install -f", + "clean:git:untracked": "git clean -d -x -i", + "clean:git:untracked:force": "git clean -d -x -f", + "dev": "bun run start:dev", + "fix": "bun run fix:biome; bun run fix:package", "fix:biome": "bun biome check --write", "fix:package": "bun sort-package-json --quiet", "lint": "bun run lint:biome && bun run lint:tsc", "lint:biome": "bun biome lint", "lint:tsc": "bun tsc --noEmit", - "start": "bun run build && bun dist/backend.min.js", - "start:dev": "bun run src/index.ts" + "start": "bun run start:server", + "start:dev": "bun run ./src/server.ts", + "start:rebuild": "bun run build:server && bun run start:server", + "start:server": "bun run ./dist/server.js" }, "dependencies": { "@hono/zod-openapi": "~0.16.4", - "@scalar/hono-api-reference": "~0.5.152", - "@swc/cli": "0.4.1-nightly.20240914", - "@swc/core": "~1.7.26", - "@types/bun": "~1.1.10", - "cbor-x": "~1.6.0", + "@scalar/hono-api-reference": "~0.5.155", + "@types/bun": "~1.1.11", + "@types/node": "~22.7.7", "chalk": "~5.3.0", "env-var": "~7.5.0", - "hono": "~4.6.3", + "hono": "~4.6.5", "loglevel": "~1.9.2", - "typescript": "~5.5.4" + "typescript": "~5.6.3" }, "devDependencies": { - "@biomejs/biome": "~1.9.3", - "lefthook": "~1.7.18", + "@biomejs/biome": "~1.9.4", + "lefthook": "~1.7.22", "sort-package-json": "~2.10.1" }, "trustedDependencies": [ diff --git a/src/document/compression.ts b/src/document/compression.ts index eac7373..92a7e35 100644 --- a/src/document/compression.ts +++ b/src/document/compression.ts @@ -1,29 +1,11 @@ -import { type InputType, brotliCompress, brotliDecompress } from 'node:zlib'; -import { errorHandler } from '../server/errorHandler.ts'; -import { ErrorCode } from '../types/ErrorHandler.ts'; +import { type InputType, brotliCompressSync, brotliDecompressSync } from 'node:zlib'; export const compression = { - encode: (data: InputType): Promise => { - return new Promise((resolve, reject) => { - brotliCompress(data, (err, buffer) => { - if (err) { - reject(errorHandler.send(ErrorCode.documentCorrupted)); - } else { - resolve(buffer); - } - }); - }); + encode: (data: InputType): Buffer => { + return brotliCompressSync(data); }, - decode: (data: InputType): Promise => { - return new Promise((resolve, reject) => { - brotliDecompress(data, (err, buffer) => { - if (err) { - reject(errorHandler.send(ErrorCode.documentCorrupted)); - } else { - resolve(buffer); - } - }); - }); + decode: (data: InputType): Buffer => { + return brotliDecompressSync(data); } } as const; diff --git a/src/document/crypto.ts b/src/document/crypto.ts index 6d77bb4..da68052 100644 --- a/src/document/crypto.ts +++ b/src/document/crypto.ts @@ -1,44 +1,22 @@ -import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'; +import { randomBytes } from 'node:crypto'; -const cipherAlgorithm = 'aes-256-gcm'; const hashAlgorithm = 'blake2b256'; -const ivLength = 12; +const saltLength = 16; export const crypto = { - encrypt: (data: Uint8Array, password: string): Uint8Array => { - const iv = randomBytes(ivLength); - const key = crypto.hash(password, 'binary'); - const cipher = createCipheriv(cipherAlgorithm, key, iv); - const encrypted = Buffer.concat([cipher.update(data), cipher.final(), cipher.getAuthTag()]); + hash: (password: string): Uint8Array => { + const salt = randomBytes(saltLength); + const hasher = new Bun.CryptoHasher(hashAlgorithm, salt).update(password); - return Buffer.concat([iv, encrypted]); + return Buffer.concat([salt, hasher.digest()]); }, - decrypt: (data: Uint8Array, password: string): Uint8Array => { - const iv = data.slice(0, ivLength); - const encryptedData = data.slice(ivLength); - const key = crypto.hash(password, 'binary'); - const decipher = createDecipheriv(cipherAlgorithm, key, iv); + compare: (password: string, hash: Uint8Array): boolean => { + const salt = hash.subarray(0, saltLength); + const hasher = new Bun.CryptoHasher(hashAlgorithm, salt).update(password); - decipher.setAuthTag(encryptedData.slice(-16)); + const passwordHash = Buffer.concat([salt, hasher.digest()]); - return Buffer.concat([decipher.update(encryptedData.slice(0, -16)), decipher.final()]); - }, - - hash: (password: string, encoding: 'base64' | 'binary' = 'base64'): string | Uint8Array => { - const hasher = new Bun.CryptoHasher(hashAlgorithm).update(password); - - switch (encoding) { - case 'base64': { - return hasher.digest('base64'); - } - default: { - return hasher.digest() as Uint8Array; - } - } - }, - - compare: (password: string, hash: string, encoding: 'base64' | 'binary' = 'base64'): boolean => { - return crypto.hash(password, encoding) === hash; + return hash.every((value, index) => value === passwordHash[index]); } } as const; diff --git a/src/document/storage.ts b/src/document/storage.ts index 41b4946..3be296c 100644 --- a/src/document/storage.ts +++ b/src/document/storage.ts @@ -1,4 +1,4 @@ -import { decode, encode } from 'cbor-x'; +import { deserialize, serialize } from 'bun:jsc'; import { config } from '../server.ts'; import { errorHandler } from '../server/errorHandler.ts'; import type { Document } from '../types/Document.ts'; @@ -15,10 +15,10 @@ export const storage = { errorHandler.send(ErrorCode.documentNotFound); } - return decode(Buffer.from(await file.arrayBuffer())); + return deserialize(await file.arrayBuffer()); }, write: async (name: string, document: Document): Promise => { - await Bun.write(config.storagePath + name, encode(document)); + await Bun.write(config.storagePath + name, serialize(document)); } } as const; diff --git a/src/document/validator.ts b/src/document/validator.ts index 0368077..e09c428 100644 --- a/src/document/validator.ts +++ b/src/document/validator.ts @@ -6,11 +6,11 @@ import { ValidatorUtils } from '../utils/ValidatorUtils.ts'; import { crypto } from './crypto.ts'; export const validator = { - validateName: (key: string): void => { + validateName: (name: string): void => { if ( - !ValidatorUtils.isValidBase64URL(key) || + !ValidatorUtils.isValidBase64URL(name) || !ValidatorUtils.isLengthWithinRange( - Bun.stringWidth(key), + Bun.stringWidth(name), config.documentNameLengthMin, config.documentNameLengthMax ) diff --git a/src/endpoints/v1/access.route.ts b/src/endpoints/v1/access.route.ts index ae0f7ee..0c1ca62 100644 --- a/src/endpoints/v1/access.route.ts +++ b/src/endpoints/v1/access.route.ts @@ -51,12 +51,12 @@ export const accessRoute = (endpoint: OpenAPIHono): void => { const document = await storage.read(params.name); - // V1 Endpoint does not support Server-Side Encryption + // V1 Endpoint does not support document protected password if (document.header.passwordHash) { errorHandler.send(ErrorCode.documentPasswordNeeded); } - const buffer = await compression.decode(document.data); + const buffer = compression.decode(document.data); return ctx.json({ key: params.name, diff --git a/src/endpoints/v1/accessRaw.route.ts b/src/endpoints/v1/accessRaw.route.ts index 9e2e368..aa73091 100644 --- a/src/endpoints/v1/accessRaw.route.ts +++ b/src/endpoints/v1/accessRaw.route.ts @@ -45,13 +45,13 @@ export const accessRawRoute = (endpoint: OpenAPIHono): void => { const document = await storage.read(params.name); - // V1 Endpoint does not support Server-Side Encryption + // V1 Endpoint does not support document protected password if (document.header.passwordHash) { errorHandler.send(ErrorCode.documentPasswordNeeded); } // @ts-ignore: Return the buffer directly - return ctx.text(await compression.decode(document.data)); + return ctx.text(compression.decode(document.data)); }, (result) => { if (!result.success) { diff --git a/src/endpoints/v1/publish.route.ts b/src/endpoints/v1/publish.route.ts index 3b43265..ab45f2b 100644 --- a/src/endpoints/v1/publish.route.ts +++ b/src/endpoints/v1/publish.route.ts @@ -1,4 +1,5 @@ import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; +import { StringUtils } from '@x-util/StringUtils.ts'; import { compression } from '../../document/compression.ts'; import { crypto } from '../../document/crypto.ts'; import { storage } from '../../document/storage.ts'; @@ -6,7 +7,6 @@ import { errorHandler, schema } from '../../server/errorHandler.ts'; import { middleware } from '../../server/middleware.ts'; import { DocumentVersion } from '../../types/Document.ts'; import { ErrorCode } from '../../types/ErrorHandler.ts'; -import { StringUtils } from '../../utils/StringUtils.ts'; export const publishRoute = (endpoint: OpenAPIHono): void => { const route = createRoute({ @@ -60,10 +60,10 @@ export const publishRoute = (endpoint: OpenAPIHono): void => { const secret = StringUtils.createSecret(); await storage.write(name, { - data: await compression.encode(body), + data: compression.encode(body), header: { name: name, - secretHash: crypto.hash(secret) as string, + secretHash: crypto.hash(secret), passwordHash: null }, version: DocumentVersion.V1 diff --git a/src/endpoints/v2/access.route.ts b/src/endpoints/v2/access.route.ts index f1ebd2f..c6ef260 100644 --- a/src/endpoints/v2/access.route.ts +++ b/src/endpoints/v2/access.route.ts @@ -1,7 +1,6 @@ import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; +import { storage } from '@x-document/storage.ts'; import { compression } from '../../document/compression.ts'; -import { crypto } from '../../document/crypto.ts'; -import { storage } from '../../document/storage.ts'; import { validator } from '../../document/validator.ts'; import { config } from '../../server.ts'; import { errorHandler, schema } from '../../server/errorHandler.ts'; @@ -22,7 +21,7 @@ export const accessRoute = (endpoint: OpenAPIHono): void => { }), headers: z.object({ password: z.string().optional().openapi({ - description: 'The password to decrypt the document', + description: 'The password to access the document', example: 'aabbccdd11223344' }) }) @@ -68,20 +67,15 @@ export const accessRoute = (endpoint: OpenAPIHono): void => { const document = await storage.read(params.name); - let data: Uint8Array; - if (document.header.passwordHash) { if (!headers.password) { return errorHandler.send(ErrorCode.documentPasswordNeeded); } validator.validatePassword(headers.password, document.header.passwordHash); - data = crypto.decrypt(document.data, headers.password); - } else { - data = document.data; } - const buffer = await compression.decode(data); + const buffer = compression.decode(document.data); return ctx.json({ key: params.name, diff --git a/src/endpoints/v2/accessRaw.route.ts b/src/endpoints/v2/accessRaw.route.ts index 58ac783..7de8883 100644 --- a/src/endpoints/v2/accessRaw.route.ts +++ b/src/endpoints/v2/accessRaw.route.ts @@ -1,7 +1,6 @@ import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; +import { storage } from '@x-document/storage.ts'; import { compression } from '../../document/compression.ts'; -import { crypto } from '../../document/crypto.ts'; -import { storage } from '../../document/storage.ts'; import { validator } from '../../document/validator.ts'; import { config } from '../../server.ts'; import { errorHandler, schema } from '../../server/errorHandler.ts'; @@ -22,7 +21,7 @@ export const accessRawRoute = (endpoint: OpenAPIHono): void => { }), headers: z.object({ password: z.string().optional().openapi({ - description: 'The password to decrypt the document', + description: 'The password to access the document', example: 'aabbccdd11223344' }) }), @@ -65,22 +64,16 @@ export const accessRawRoute = (endpoint: OpenAPIHono): void => { const document = await storage.read(params.name); - let data: Uint8Array; - if (document.header.passwordHash) { if (!options.password) { return errorHandler.send(ErrorCode.documentPasswordNeeded); } validator.validatePassword(options.password, document.header.passwordHash); - - data = crypto.decrypt(document.data, options.password); - } else { - data = document.data; } // @ts-ignore: Return the buffer directly - return ctx.text(await compression.decode(data)); + return ctx.text(compression.decode(document.data)); }, (result) => { if (!result.success) { diff --git a/src/endpoints/v2/edit.route.ts b/src/endpoints/v2/edit.route.ts index 9975a9f..4332ea9 100644 --- a/src/endpoints/v2/edit.route.ts +++ b/src/endpoints/v2/edit.route.ts @@ -1,7 +1,6 @@ import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; +import { storage } from '@x-document/storage.ts'; import { compression } from '../../document/compression.ts'; -import { crypto } from '../../document/crypto.ts'; -import { storage } from '../../document/storage.ts'; import { validator } from '../../document/validator.ts'; import { config } from '../../server.ts'; import { errorHandler, schema } from '../../server/errorHandler.ts'; @@ -34,7 +33,8 @@ export const editRoute = (endpoint: OpenAPIHono): void => { }), headers: z.object({ password: z.string().optional().openapi({ - description: 'The password to decrypt the document', + deprecated: true, + description: 'The password to access the document (not used anymore)', example: 'aabbccdd11223344' }), secret: z.string().openapi({ @@ -74,17 +74,7 @@ export const editRoute = (endpoint: OpenAPIHono): void => { validator.validateSecret(headers.secret, document.header.secretHash); - if (document.header.passwordHash) { - if (!headers.password) { - return errorHandler.send(ErrorCode.documentPasswordNeeded); - } - - validator.validatePassword(headers.password, document.header.passwordHash); - } - - const data = await compression.encode(body); - - document.data = headers.password ? crypto.encrypt(data, headers.password) : data; + document.data = compression.encode(body); const result = await storage .write(params.name, document) diff --git a/src/endpoints/v2/publish.route.ts b/src/endpoints/v2/publish.route.ts index 7db14a8..cde5ab4 100644 --- a/src/endpoints/v2/publish.route.ts +++ b/src/endpoints/v2/publish.route.ts @@ -1,14 +1,14 @@ import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; +import { storage } from '@x-document/storage.ts'; +import { StringUtils } from '@x-util/StringUtils.ts'; import { compression } from '../../document/compression.ts'; import { crypto } from '../../document/crypto.ts'; -import { storage } from '../../document/storage.ts'; import { validator } from '../../document/validator.ts'; import { config } from '../../server.ts'; import { errorHandler, schema } from '../../server/errorHandler.ts'; import { middleware } from '../../server/middleware.ts'; import { DocumentVersion } from '../../types/Document.ts'; import { ErrorCode } from '../../types/ErrorHandler.ts'; -import { StringUtils } from '../../utils/StringUtils.ts'; export const publishRoute = (endpoint: OpenAPIHono): void => { const route = createRoute({ @@ -30,16 +30,16 @@ export const publishRoute = (endpoint: OpenAPIHono): void => { }, headers: z.object({ password: z.string().optional().openapi({ - description: 'The password to encrypt the document', + description: 'The password to restrict the document', example: 'aabbccdd11223344' }), key: z.string().optional().openapi({ description: 'The document name (formerly key)', example: 'abc123' }), - keylength: z.number().optional().openapi({ + keylength: z.string().optional().openapi({ description: 'The document name length (formerly key length)', - example: config.documentNameLengthDefault + example: config.documentNameLengthDefault.toString() }), secret: z.string().optional().openapi({ description: 'The document secret', @@ -111,19 +111,19 @@ export const publishRoute = (endpoint: OpenAPIHono): void => { name = headers.key; } else { - validator.validateNameLength(headers.keylength); + const nameLength = Number(headers.keylength || config.documentNameLengthDefault); - name = await StringUtils.createName(headers.keylength); + name = await StringUtils.createName(nameLength); } - const data = await compression.encode(body); + const data = compression.encode(body); await storage.write(name, { - data: headers.password ? crypto.encrypt(data, headers.password) : data, + data: data, header: { name: name, - secretHash: crypto.hash(secret) as string, - passwordHash: headers.password ? (crypto.hash(headers.password) as string) : null + secretHash: crypto.hash(secret), + passwordHash: headers.password ? crypto.hash(headers.password) : null }, version: DocumentVersion.V1 }); diff --git a/src/endpoints/v2/remove.route.ts b/src/endpoints/v2/remove.route.ts index db9c6de..57929c5 100644 --- a/src/endpoints/v2/remove.route.ts +++ b/src/endpoints/v2/remove.route.ts @@ -1,6 +1,6 @@ import { unlink } from 'node:fs/promises'; import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; -import { storage } from '../../document/storage.ts'; +import { storage } from '@x-document/storage.ts'; import { validator } from '../../document/validator.ts'; import { config } from '../../server.ts'; import { errorHandler, schema } from '../../server/errorHandler.ts'; diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 3769a96..0000000 --- a/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { env, server } from './server.ts'; - -// TODO: Support graceful shutdown -process.on('SIGTERM', () => process.exit(0)); - -export default { - port: env.port, - fetch: server().fetch -}; diff --git a/src/server.ts b/src/server.ts index d835f2f..81b2213 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,5 @@ import { OpenAPIHono } from '@hono/zod-openapi'; +import { serve } from 'bun'; import { get as envvar } from 'env-var'; import { cors } from 'hono/cors'; import { HTTPException } from 'hono/http-exception'; @@ -20,12 +21,18 @@ export const env = { export const config = { protocol: env.tls ? 'https://' : 'http://', apiPath: '/api', - storagePath: 'documents/', + storagePath: 'storage/', documentNameLengthMin: 2, documentNameLengthMax: 32, documentNameLengthDefault: 8 } as const; +logger.set(env.logLevel); + +process.on('SIGTERM', async () => { + await backend.stop(); +}); + const instance = new OpenAPIHono().basePath(config.apiPath); export const server = (): typeof instance => { @@ -49,9 +56,13 @@ export const server = (): typeof instance => { endpoints(instance); env.docsEnabled && documentation(instance); - logger.debug('Registered', instance.routes.length, 'routes'); logger.debug('Registered routes:', instance.routes); logger.info(`Listening on: http://localhost:${env.port}`); return instance; }; + +const backend = serve({ + port: env.port, + fetch: server().fetch +}); diff --git a/src/server/endpoints.ts b/src/server/endpoints.ts index 48359f7..d1394ac 100644 --- a/src/server/endpoints.ts +++ b/src/server/endpoints.ts @@ -1,6 +1,6 @@ import type { OpenAPIHono } from '@hono/zod-openapi'; -import { v1 } from '../endpoints/v1'; -import { v2 } from '../endpoints/v2'; +import { v1 } from '@x-v1/index.ts'; +import { v2 } from '@x-v2/index.ts'; import { config } from '../server.ts'; export const endpoints = (instance: OpenAPIHono): void => { diff --git a/src/server/errorHandler.ts b/src/server/errorHandler.ts index 0aa9c4a..265da5e 100644 --- a/src/server/errorHandler.ts +++ b/src/server/errorHandler.ts @@ -1,7 +1,6 @@ import type { ResponseConfig } from '@asteasolutions/zod-to-openapi/dist/openapi-registry'; import { z } from '@hono/zod-openapi'; import { HTTPException } from 'hono/http-exception'; -import type { StatusCode } from 'hono/utils/http-status'; import { ErrorCode, type Schema } from '../types/ErrorHandler.ts'; const map: Record = { @@ -106,7 +105,7 @@ export const errorHandler = { send: (code: ErrorCode) => { const { httpCode, type, message } = map[code]; - throw new HTTPException(httpCode as StatusCode, { + throw new HTTPException(httpCode, { res: new Response(JSON.stringify({ type, code, message }), { status: httpCode, headers: { diff --git a/src/types/Document.ts b/src/types/Document.ts index f84ad29..a5a1dbe 100644 --- a/src/types/Document.ts +++ b/src/types/Document.ts @@ -6,14 +6,11 @@ interface Document { data: Uint8Array; header: { name: string; - secretHash: string; - passwordHash: string | null; + secretHash: Uint8Array; + passwordHash: Uint8Array | null; }; version: DocumentVersion; } -interface DocumentV1 extends Document { - version: DocumentVersion.V1; -} - -export { DocumentVersion, type Document, type DocumentV1 }; +export type { Document }; +export { DocumentVersion }; diff --git a/src/types/ErrorHandler.ts b/src/types/ErrorHandler.ts index 5a49236..894ad33 100644 --- a/src/types/ErrorHandler.ts +++ b/src/types/ErrorHandler.ts @@ -1,3 +1,5 @@ +import type { StatusCode } from 'hono/utils/http-status'; + enum ErrorCode { // * Generic crash = 1000, @@ -24,9 +26,10 @@ enum ErrorCode { type Type = 'generic' | 'document'; type Schema = { - httpCode: number; + httpCode: StatusCode; type: Type; message: string; }; -export { ErrorCode, type Schema }; +export type { Schema }; +export { ErrorCode }; diff --git a/swc.json b/swc.json deleted file mode 100644 index b8014c5..0000000 --- a/swc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://swc.rs/schema.json", - "isModule": true, - "minify": true, - "jsc": { - "target": "esnext", - "minify": { - "compress": { - "arguments": true, - "hoist_funs": true, - "hoist_vars": true, - "unsafe": true - }, - "format": { - "comments": false - }, - "mangle": true - } - } -} diff --git a/tsconfig.json b/tsconfig.json index 3a242d1..5190678 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,6 @@ "noEmit": true, "resolveJsonModule": true, "skipLibCheck": true, - "sourceMap": true, "strict": true, "allowUnreachableCode": false, @@ -24,11 +23,21 @@ "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, + "noPropertyAccessFromIndexSignature": false, "noUncheckedIndexedAccess": true, + "noUncheckedSideEffectImports": true, "noUnusedLocals": true, "noUnusedParameters": true, - "verbatimModuleSyntax": true + "verbatimModuleSyntax": true, + + "baseUrl": ".", + "paths": { + "@x-v1/*": ["./src/endpoints/v1/*"], + "@x-v2/*": ["./src/endpoints/v2/*"], + "@x-document/*": ["./src/document/*"], + "@x-server/*": ["./src/server/*"], + "@x-util/*": ["./src/utils/*"] + } }, - "exclude": ["dist/**", "documents/**"] + "exclude": ["./dist/**", "./storage/**"] }