From 1d2f56accdebf20b3ad9cef4dc6d04a4d5db151c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:44:29 +0200 Subject: [PATCH 01/14] Bump github/codeql-action from 3.26.11 to 3.26.12 (#176) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/security.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c3d379c..63a956c 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -46,13 +46,13 @@ jobs: persist-credentials: false - name: "Setup CodeQL" - uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: "Run analysis" - uses: github/codeql-action/analyze@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: category: "/language:${{matrix.language}}" @@ -82,6 +82,6 @@ jobs: publish_results: true - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/upload-sarif@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: sarif_file: scoreboard.sarif From 9f95dd955a9585b31721b9d58dec548dab4e0c4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 14:53:33 +0200 Subject: [PATCH 02/14] Bump actions/checkout from 4.2.0 to 4.2.1 (#177) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++-- .github/workflows/security.yml | 4 ++-- .github/workflows/test.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aec1fef..a087025 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,7 +63,7 @@ jobs: echo "extended=${TIMESTAMP}-${GITHUB_SHA_SHORT}" >>"$GITHUB_OUTPUT" - name: "Checkout" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false @@ -149,7 +149,7 @@ jobs: echo "tags=${TAGS[*]}" >>"$GITHUB_OUTPUT" - name: "Checkout" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 63a956c..a7ce2c1 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -41,7 +41,7 @@ jobs: egress-policy: audit - name: "Checkout" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false @@ -70,7 +70,7 @@ jobs: egress-policy: audit - name: "Checkout" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 808f560..0fc51de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 - name: "Checkout" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false From 9aade245f168d8e2945251100f56d5151bb2e2bd Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Thu, 17 Oct 2024 14:38:06 +0200 Subject: [PATCH 03/14] Preliminar changes (#178) Co-authored-by: Mrgaton Co-authored-by: Mrgaton <68958481+Mrgaton@users.noreply.github.com> --- .dockerignore | 3 +- .env.example | 2 +- .github/workflows/release.yml | 87 ++++++++++++++-------------- .github/workflows/security.yml | 34 +++++------ .github/workflows/test.yml | 32 +++++----- .gitignore | 10 +++- Dockerfile | 6 +- README.md | 25 ++++---- biome.json | 2 +- bun.lockb | Bin 81768 -> 28656 bytes bunfig.toml | 3 - package.json | 30 +++++----- src/document/compression.ts | 28 ++------- src/document/crypto.ts | 44 ++++---------- src/document/storage.ts | 6 +- src/document/validator.ts | 6 +- src/endpoints/v1/access.route.ts | 4 +- src/endpoints/v1/accessRaw.route.ts | 4 +- src/endpoints/v1/publish.route.ts | 6 +- src/endpoints/v2/access.route.ts | 12 +--- src/endpoints/v2/accessRaw.route.ts | 13 +---- src/endpoints/v2/edit.route.ts | 18 ++---- src/endpoints/v2/publish.route.ts | 28 +++++---- src/endpoints/v2/remove.route.ts | 2 +- src/index.ts | 9 --- src/server.ts | 15 ++++- src/server/endpoints.ts | 4 +- src/server/errorHandler.ts | 3 +- src/types/Document.ts | 11 ++-- src/types/ErrorHandler.ts | 7 ++- swc.json | 20 ------- tsconfig.json | 17 ++++-- 32 files changed, 211 insertions(+), 280 deletions(-) delete mode 100644 src/index.ts delete mode 100644 swc.json 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 a087025..1a7e4c1 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" + - 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 --owner=0 --group=0 --mtime='now' --utc -c .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 --owner=0 --group=0 --mtime='now' --utc -c .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 --owner=0 --group=0 --mtime='now' --utc -c .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" + - 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 a7ce2c1..c28f2c5 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,28 +1,28 @@ -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" + name: CodeQL runs-on: ubuntu-latest strategy: fail-fast: false @@ -35,53 +35,53 @@ jobs: security-events: write steps: - - name: "Harden Runner" + - name: Harden Runner uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 with: egress-policy: audit - - name: "Checkout" + - name: Checkout uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false - - name: "Setup CodeQL" + - name: Setup CodeQL uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - - name: "Run analysis" + - name: Run analysis uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 with: - category: "/language:${{matrix.language}}" + 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" + - 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" + - 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 0fc51de..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" + - 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..e042b53 100644 --- a/README.md +++ b/README.md @@ -5,36 +5,35 @@ ## 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... - -Windows: - -```powershell -powershell -c ".\backend.exe" -``` +- Edit the `.env.example` file and rename it to `.env` +- Run the binary... Linux & macOS: ```shell -chmod +x ./backend ./backend ``` +Windows: + +```powershell +powershell -c ".\backend.exe" +``` + ### 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` +- Run the 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: diff --git a/biome.json b/biome.json index 4284218..d7cc7c2 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,7 @@ { "$schema": "https://biomejs.dev/schemas/1.9.3/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 4d84884081505bb4940116888b9f07db7a8c9709..8e3e4def495035ccbabd730463003b52a809eedf 100755 GIT binary patch delta 6055 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 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..82c031c 100644 --- a/package.json +++ b/package.json @@ -5,37 +5,35 @@ "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": "rm -rf ./dist/ && 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", + "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.154", + "@types/bun": "~1.1.11", + "@types/node": "~22.7.5", "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", 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..e2e3511 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).update(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).update(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..ca98a6f 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', @@ -110,20 +110,24 @@ export const publishRoute = (endpoint: OpenAPIHono): void => { } name = headers.key; - } else { - validator.validateNameLength(headers.keylength); + } else if (headers.keylength) { + const nameLength = Number(headers.keylength); + + validator.validateNameLength(nameLength); - name = await StringUtils.createName(headers.keylength); + name = await StringUtils.createName(nameLength); + } else { + name = await StringUtils.createName(config.documentNameLengthDefault); } - 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/**"] } From 24f7a014ff0eb09a90ed0e5052999614d4c3756c Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Fri, 18 Oct 2024 23:48:40 +0200 Subject: [PATCH 04/14] remove CodeQL, fix release & update deps --- .github/workflows/release.yml | 2 +- .github/workflows/security.yml | 35 --------------------------------- biome.json | 2 +- bun.lockb | Bin 28656 -> 28656 bytes package.json | 8 ++++---- 5 files changed, 6 insertions(+), 41 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a7e4c1..677f168 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,7 +98,7 @@ jobs: 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 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c28f2c5..967f063 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -21,41 +21,6 @@ on: 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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - with: - persist-credentials: false - - - name: Setup CodeQL - uses: github/codeql-action/init@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - - name: Run analysis - uses: github/codeql-action/analyze@c36620d31ac7c881962c3d9dd939c40ec9434f2b # v3.26.12 - with: - category: /language:${{matrix.language}} - scoreboard: name: Scorecard runs-on: ubuntu-latest diff --git a/biome.json b/biome.json index d7cc7c2..ebe4ebc 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.3/schema.json", + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "files": { "ignore": ["./dist/**", "./storage/**", "*.spec.ts"], "ignoreUnknown": true diff --git a/bun.lockb b/bun.lockb index 8e3e4def495035ccbabd730463003b52a809eedf..1a9901c42a0ef32cd8fcdad756f3c62f18fee391 100755 GIT binary patch delta 2653 zcmYk42|N`2AIE1l#}YYm%(ClseibTbKR=fSpYi7Qm-}m$R{pLNtnYmGLZWKJDCO1G1hg7Qs+H@r> zqCu}Se8M1Xw!>qx!aQy@X2*zp>P?3%9@urW(kGwlTCjZE5ttV`b@p5ED&%k&p>h<&m2tudUj>&?!R$7BF$e? z^HL^_f>q+;yUgDK@KbrncwtHTo#gHYky>i{U=hxHvX?OdEPOPnw|%U6LvG1wao3%Q zwn~x4X7iJ$$lTp-{Hyvc4{rx9`P3KGSX5t!OBncV8e6D%e0$Di0UZAH5U-~@Y?sJb z+4Ap4IxHYFI{;wfvaxCo0XW0C?s+F}Ojk^q%ED{LVpy5}(~u+wtDWljcI*Dr} z{-$!2uJK%JpG3 zU%dj$r)Yx{FPAeY7&I1qIQul9uJ-xOd9H_PV;9%X{>!J7a|C0gi_cjh98OF+Ffmx# zvFh_6ve0u`xM+(ST&xLE2l%=~D|cLN>cc&ZYsv`7ac&w8Yg!wApg&wH1h}_eD~P<5 z^U~|<9TMLAgj7j2VXk{Wps!K6`llg2%YZ=`W7DA#BEyYzrsIX#f604XCj0HZ(C?)c zqiY?EUTq0|oC-CUy?LxvC!uzyn^zmAq(M=2rsNsoY}U&}pHTD2+kPa`}koyA6MI$?y&Xd~SXP%n2&p}>e^vdB3jz4+Q15jhju9$w;csoR!; zYePH-n+cGfkQ8QTW5VQ{6t|?skslhk&YnYNT-{iO!;aNy@K1y;I&?puN?R(`zF_6DSYRukb++HzuotgxDnzn%poLRJEq)de8;`eoTi@TKVe5OB zO~-|hnQWxn^QtnD(JmEd;p-haKRgG&o7y?2CXw*+I9NZwIc~bcI{utTz~nT>(=53v zs%OS5gl~V_#YN1}TDrXO8YZZ(bcj8vDZ%W0;w3p`&_8zt5xIv9Djy ztjQVZea+=`wG7@*f~d@z2L4fZKB$B8?6N~Ms*B(C#l*lrkVX+vwvUiy<)i+cfo_Z9(~>D%pF-`m*L5>1bAdtEA);#pP4zz7NNt&Sw{N zE;bpUE3^{Dr&UjVuh2zX#ZFo=Fh!wZI4`2B2>73m@i>c`{KnlFb5!iKJCC$>#Zk+#>Ij>1?}@Qz`Z8=f8bW5fk#7D}1X0||@Jh)? z10C77S46`!jF!HOg#H+?uq_}{gzXx(&xM3PM`1+1GtacAzU$tWvtY0iwXGMOoigjw zXRKBa8*@uCNV=OKEC~QY0)t`>g@;pw=-HBB;aY zbJPJ*Ish!9AI)K>}h(fWWga^K~(Em=geiR>YA&oPF&X%I~M1EOHz;j6HRl9pJum zFp_lCo6ZRU01ElrSc~RQ^#&c=RMpRM;3juS8hK-&41tD_z;hkOE@t;3JXO-ZXUkE0QFF?AV`99 sL$%>;p|b0W2*-qC*OkrsT@^-$DYDhxFgdm=58K98i(xW|5(^*x9~FS!bpQYW delta 2653 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 r79 Date: Sat, 19 Oct 2024 00:04:31 +0200 Subject: [PATCH 05/14] fix release (again) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 677f168..612f41a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,7 +98,7 @@ jobs: 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 From 39ef2d376ed5c9de5bdf6ccad3f98f42e6327bda Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Sat, 19 Oct 2024 00:37:06 +0200 Subject: [PATCH 06/14] fix release (again x2) GHA Ubuntu images doesn't come with "gzip" preinstalled, so I revert these changes --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 612f41a..9fcc34e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,17 +74,17 @@ jobs: run: | bun run build:standalone:darwin-arm64 chmod 755 ./dist/backend - tar --owner=0 --group=0 --mtime='now' --utc -c .env.example LICENSE README.md -C ./dist/ backend | gzip --best >./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz + tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz >/dev/null bun run build:standalone:linux-amd64 chmod 755 ./dist/backend - tar --owner=0 --group=0 --mtime='now' --utc -c .env.example LICENSE README.md -C ./dist/ backend | gzip --best >./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz + tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz >/dev/null bun run build:standalone:linux-arm64 chmod 755 ./dist/backend - tar --owner=0 --group=0 --mtime='now' --utc -c .env.example LICENSE README.md -C ./dist/ backend | gzip --best >./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz + tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz >/dev/null bun run build:standalone:windows-amd64 @@ -98,7 +98,7 @@ jobs: 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 From 477f9df13d7870009c00206de5ad5a36dde69c8e Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Sat, 19 Oct 2024 00:43:37 +0200 Subject: [PATCH 07/14] fix release (again x3) missing quotes now? --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fcc34e..98ce289 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -98,7 +98,7 @@ jobs: 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 From f7c27ea96d0e4f1980bc9fda19f63a535c37d31a Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Sat, 19 Oct 2024 00:51:48 +0200 Subject: [PATCH 08/14] fix release (again x4) remove tar test --- .github/workflows/release.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 98ce289..7e9ee10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,17 +75,14 @@ jobs: bun run build:standalone:darwin-arm64 chmod 755 ./dist/backend tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend - tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz >/dev/null bun run build:standalone:linux-amd64 chmod 755 ./dist/backend tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend - tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz >/dev/null bun run build:standalone:linux-arm64 chmod 755 ./dist/backend tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend - tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz >/dev/null bun run build:standalone:windows-amd64 chmod 755 ./dist/backend.exe @@ -98,7 +95,7 @@ jobs: 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 From 7025488ac0261bbdc8c30c3db2edf598ad3b3820 Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Sat, 19 Oct 2024 12:04:41 +0200 Subject: [PATCH 09/14] fix release (again x5) maybe uid/gid setters blocked? --- .github/workflows/release.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e9ee10..4f2a66f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,15 +74,18 @@ jobs: run: | bun run build:standalone:darwin-arm64 chmod 755 ./dist/backend - tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend + tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend + tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz >/dev/null bun run build:standalone:linux-amd64 chmod 755 ./dist/backend - tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend + tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend + tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz >/dev/null bun run build:standalone:linux-arm64 chmod 755 ./dist/backend - tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend + tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend + tar -tzf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz >/dev/null bun run build:standalone:windows-amd64 chmod 755 ./dist/backend.exe @@ -95,7 +98,7 @@ jobs: 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 From 17c01b816e6317c5c9dd3942f07c4235723738c5 Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Sat, 19 Oct 2024 13:03:55 +0200 Subject: [PATCH 10/14] fix release (again x6) rmrf the dist directory is not needed in subsequent standalone builds, restores the original changes --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f2a66f..c904b23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,17 +74,17 @@ jobs: run: | bun run build:standalone:darwin-arm64 chmod 755 ./dist/backend - tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_darwin-arm64.tar.gz --mtime='now' --utc .env.example LICENSE README.md -C ./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 chmod 755 ./dist/backend - tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-amd64.tar.gz --mtime='now' --utc .env.example LICENSE README.md -C ./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 chmod 755 ./dist/backend - tar -caf ./dist/backend_${{ steps.tags-artifact.outputs.tag }}_linux-arm64.tar.gz --mtime='now' --utc .env.example LICENSE README.md -C ./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 @@ -98,7 +98,7 @@ jobs: 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 From 483716679ec5c9d06568776256424b05bac4d1ef Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Sat, 19 Oct 2024 13:20:58 +0200 Subject: [PATCH 11/14] add git cleanup scripts --- README.md | 27 +++++++++++++++++++++------ package.json | 8 +++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e042b53..fb561d5 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ ### Binary -- Download the [latest release](https://github.com/jspaste/backend/releases/latest) -- Uncompress to a new folder +- 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... @@ -26,8 +25,13 @@ powershell -c ".\backend.exe" ### Container -- Pull latest image: `docker pull ghcr.io/jspaste/backend:latest` -- Run the container: `docker run -e DOCS_ENABLED=true -d -p 127.0.0.1:4000:4000 ghcr.io/jspaste/backend:latest` +- Pull latest image and run the container: + +```shell +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 +``` ## Validate @@ -42,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 @@ -54,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/package.json b/package.json index a49eb8a..211e763 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,17 @@ "scripts": { "build": "bun run build:server", "build:server": "rm -rf ./dist/ && bun build ./src/server.ts --outdir=./dist/ --target=bun --minify --sourcemap=inline", - "build:standalone": "rm -rf ./dist/ && bun build ./src/server.ts --outfile=./dist/backend --compile --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", + "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", From e89680746d78b7de0c27a1d0706861cc78c3b365 Mon Sep 17 00:00:00 2001 From: Mrgaton Date: Sat, 19 Oct 2024 23:19:16 +0200 Subject: [PATCH 12/14] Use hmac for password salt --- src/document/crypto.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/document/crypto.ts b/src/document/crypto.ts index e2e3511..b55a350 100644 --- a/src/document/crypto.ts +++ b/src/document/crypto.ts @@ -2,18 +2,17 @@ import { randomBytes } from 'node:crypto'; const hashAlgorithm = 'blake2b256'; const saltLength = 16; - export const crypto = { hash: (password: string): Uint8Array => { const salt = randomBytes(saltLength); - const hasher = new Bun.CryptoHasher(hashAlgorithm).update(salt).update(password); + const hasher = new Bun.CryptoHasher(hashAlgorithm, salt).update(password); return Buffer.concat([salt, hasher.digest()]); }, compare: (password: string, hash: Uint8Array): boolean => { const salt = hash.subarray(0, saltLength); - const hasher = new Bun.CryptoHasher(hashAlgorithm).update(salt).update(password); + const hasher = new Bun.CryptoHasher(hashAlgorithm, salt).update(password); const passwordHash = Buffer.concat([salt, hasher.digest()]); From 4f1f8e10cafb03569a0953742cf41ebdeb7c55b9 Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Sun, 20 Oct 2024 01:19:48 +0200 Subject: [PATCH 13/14] cleanup --- bun.lockb | Bin 28656 -> 28656 bytes package.json | 2 +- src/document/crypto.ts | 1 + src/endpoints/v2/publish.route.ts | 6 ++---- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/bun.lockb b/bun.lockb index 1a9901c42a0ef32cd8fcdad756f3c62f18fee391..6b2f7103637e600de69331b7bfb4167447bbe163 100755 GIT binary patch delta 433 zcmexxpYg+e#tC`~8sdwtB~4&iTW~?(XEE;dH? z$&Re*o7hz)96V*5*6pz6*#GHy+ad$S?Uk2LmCbr!7}y+VVBR1vGAkkMsk!)BQ3)n_ zmS~gb9upnPqaG+FAJI44$6R*u*P7ywn^Wb_=+`slB$lKWmoPBoq^6Z*M=a(B zLMAn@Ot&nth~bG*m3TOJ-YtYoMt)vCPzytOuIhfxwO%0znVkIeoYb<^9J4?D>UCMS j)E6V9KsrFKo3GlSqurXC1e4hu7O;(z(R?#+)MHTqf%lcA delta 431 zcmexxpYg+e#tC`~*DdoUbk^`3HWza%W{jS|DZn_{#UlL0a@VqdB^LJ-n>YG(u}u!) z*4V_ZD&g?=-o<&g_QIlv4Nt7_-futgZ>q7G$^)^`569emgf{!6RsHX^+T|&o6r_`u z%-?#XQY%t#j^PEp-40 { const salt = randomBytes(saltLength); diff --git a/src/endpoints/v2/publish.route.ts b/src/endpoints/v2/publish.route.ts index ca98a6f..c9c855e 100644 --- a/src/endpoints/v2/publish.route.ts +++ b/src/endpoints/v2/publish.route.ts @@ -110,14 +110,12 @@ export const publishRoute = (endpoint: OpenAPIHono): void => { } name = headers.key; - } else if (headers.keylength) { - const nameLength = Number(headers.keylength); + } else { + const nameLength = Number(headers.keylength || config.documentNameLengthDefault); validator.validateNameLength(nameLength); name = await StringUtils.createName(nameLength); - } else { - name = await StringUtils.createName(config.documentNameLengthDefault); } const data = compression.encode(body); From 7529241717697d36e242b29197674772b04a1573 Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Sun, 20 Oct 2024 01:24:04 +0200 Subject: [PATCH 14/14] remove duplicate validator --- src/endpoints/v2/publish.route.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/endpoints/v2/publish.route.ts b/src/endpoints/v2/publish.route.ts index c9c855e..cde5ab4 100644 --- a/src/endpoints/v2/publish.route.ts +++ b/src/endpoints/v2/publish.route.ts @@ -113,8 +113,6 @@ export const publishRoute = (endpoint: OpenAPIHono): void => { } else { const nameLength = Number(headers.keylength || config.documentNameLengthDefault); - validator.validateNameLength(nameLength); - name = await StringUtils.createName(nameLength); }