From 0b3a5f40e424249fccfc5451e0c3c4f8fe21c307 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Apr 2025 21:51:15 +0000 Subject: [PATCH 01/47] Update step-security/harden-runner action to v2.11.1 (#212) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a20ae755..85e0a2d6 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -150,7 +150,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b2b17cf..ec7699f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: sha_short: ${{ steps.ctx.outputs.sha_short }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit From a318f191b2a42e6ba5b3b61d889b7c3e84e60804 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:15:53 +0200 Subject: [PATCH 02/47] Update step-security/harden-runner action to v2.12.0 (#214) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 85e0a2d6..21ee626f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit @@ -150,7 +150,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec7699f0..032c9bf9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: sha_short: ${{ steps.ctx.outputs.sha_short }} steps: - name: Harden Runner - uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit From e368c178a422c7b936457eaafbf9441273100ae7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 14:36:33 +0000 Subject: [PATCH 03/47] Update oven-sh/setup-bun action to v2.0.2 (#215) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 21ee626f..0dd8bc97 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -46,7 +46,7 @@ jobs: egress-policy: audit - name: Setup Bun - uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - name: Save context id: ctx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 032c9bf9..1a5a8666 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: echo "sha_short=${CTX_SHA::7}" >>"$GITHUB_OUTPUT" - name: Setup Bun - uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1 + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 From 17943e9afda2994f523fe8458df8c6e62ddb0f4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 01:23:50 +0200 Subject: [PATCH 04/47] Update actions/attest-build-provenance action to v2.3.0 (#216) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 0dd8bc97..5a15180d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -130,7 +130,7 @@ jobs: - if: inputs.artifact-action == 'build-release' name: Attest artifact - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 + uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 with: subject-path: | dist/*.tar.xz @@ -229,7 +229,7 @@ jobs: - if: inputs.image-action == 'build-release' name: Attest image - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3 + uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 with: subject-name: "${{ env.REGISTRY }}/${{ steps.build-image.outputs.image }}" subject-digest: ${{ steps.push-image.outputs.digest }} From 0d33a3d9c415acffdbb405787609d74aa424745d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:45:46 +0000 Subject: [PATCH 05/47] Update step-security/harden-runner action to v2.12.1 (#218) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 5a15180d..6840939f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 with: egress-policy: audit @@ -150,7 +150,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 with: egress-policy: audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a5a8666..26c441b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: sha_short: ${{ steps.ctx.outputs.sha_short }} steps: - name: Harden Runner - uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 with: egress-policy: audit From 4e32eb556a130768bba41d0c5cfccc5c7cd873bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 10:07:02 +0200 Subject: [PATCH 06/47] Update actions/attest-build-provenance action to v2.4.0 (#219) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6840939f..92c03cec 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -130,7 +130,7 @@ jobs: - if: inputs.artifact-action == 'build-release' name: Attest artifact - uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-path: | dist/*.tar.xz @@ -229,7 +229,7 @@ jobs: - if: inputs.image-action == 'build-release' name: Attest image - uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 with: subject-name: "${{ env.REGISTRY }}/${{ steps.build-image.outputs.image }}" subject-digest: ${{ steps.push-image.outputs.digest }} From 954b92d860424054ec0ce3f282be74f9d93559fc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Jun 2025 09:17:51 +0200 Subject: [PATCH 07/47] Update ncipollo/release-action action to v1.17.0 (#222) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 92c03cec..fe138d13 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -119,7 +119,7 @@ jobs: - if: inputs.artifact-action == 'build-release' name: Release artifact - uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0 + uses: ncipollo/release-action@9128f238eeeeed39b82af6636303729079d51730 # v1.17.0 with: name: ${{ steps.tags.outputs.extended }} tag: ${{ steps.tags.outputs.extended }} From 87bd5a2a86b0388f4dccef9ad1bee4be80b928ff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 08:45:30 +0200 Subject: [PATCH 08/47] Update ncipollo/release-action action to v1.18.0 (#223) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index fe138d13..81ef2d2d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -119,7 +119,7 @@ jobs: - if: inputs.artifact-action == 'build-release' name: Release artifact - uses: ncipollo/release-action@9128f238eeeeed39b82af6636303729079d51730 # v1.17.0 + uses: ncipollo/release-action@bcfe5470707e8832e12347755757cec0eb3c22af # v1.18.0 with: name: ${{ steps.tags.outputs.extended }} tag: ${{ steps.tags.outputs.extended }} From 83c673eac316ea472439d35758120c15b7677723 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 06:46:47 +0000 Subject: [PATCH 09/47] Update step-security/harden-runner action to v2.12.2 (#224) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 81ef2d2d..4f20adae 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit @@ -150,7 +150,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26c441b7..6b9ad06c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: sha_short: ${{ steps.ctx.outputs.sha_short }} steps: - name: Harden Runner - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit From 3cc270259f22ad009f81b7442cd47491e6016240 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:44:27 +0200 Subject: [PATCH 10/47] Update dependency lefthook to ~1.12.0 (#225) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 24 ++++++++++++------------ package.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/bun.lock b/bun.lock index 2b18e6fc..8face684 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "devDependencies": { "@biomejs/biome": "~1.9.0", "@types/bun": "^1.2.0", - "lefthook": "~1.11.0", + "lefthook": "~1.12.0", "sort-package-json": "^3.0.0", }, "peerDependencies": { @@ -71,27 +71,27 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "lefthook": ["lefthook@1.11.3", "", { "optionalDependencies": { "lefthook-darwin-arm64": "1.11.3", "lefthook-darwin-x64": "1.11.3", "lefthook-freebsd-arm64": "1.11.3", "lefthook-freebsd-x64": "1.11.3", "lefthook-linux-arm64": "1.11.3", "lefthook-linux-x64": "1.11.3", "lefthook-openbsd-arm64": "1.11.3", "lefthook-openbsd-x64": "1.11.3", "lefthook-windows-arm64": "1.11.3", "lefthook-windows-x64": "1.11.3" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-HJp37y62j3j8qzAOODWuUJl4ysLwsDvCTBV6odr3jIRHR/a5e+tI14VQGIBcpK9ysqC3pGWyW5Rp9Jv1YDubyw=="], + "lefthook": ["lefthook@1.12.0", "", { "optionalDependencies": { "lefthook-darwin-arm64": "1.12.0", "lefthook-darwin-x64": "1.12.0", "lefthook-freebsd-arm64": "1.12.0", "lefthook-freebsd-x64": "1.12.0", "lefthook-linux-arm64": "1.12.0", "lefthook-linux-x64": "1.12.0", "lefthook-openbsd-arm64": "1.12.0", "lefthook-openbsd-x64": "1.12.0", "lefthook-windows-arm64": "1.12.0", "lefthook-windows-x64": "1.12.0" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-+lJSdsNcKzxv4TcIcXrd21lBXI7wrVXZ080wiJzz4sHz52KyKVPfALDPMm7+VWBtATpnCtHQe2ZPQwiBmy6VRw=="], - "lefthook-darwin-arm64": ["lefthook-darwin-arm64@1.11.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IYzAOf8Qwqk7q+LoRyy7kSk9vzpUZ5wb/vLzEAH/F86Vay9AUaWe1f2pzeLwFg18qEc1QNklT69h9p/uLQMojA=="], + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@1.12.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XMwneWj+Ux/6lI+T8ZmXwPkPS9b2n9DgMcjR4kNDTzPClBTNjKngQkh/SOzOKV/w//fkkQCdfCRMvV9f9FLS3A=="], - "lefthook-darwin-x64": ["lefthook-darwin-x64@1.11.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-z/Wp7UMjE1Vyl+x9sjN3NvN6qKdwgHl+cDf98MKKDg/WyPE5XnzqLm9rLLJgImjyClfH7ptTfZxEyhTG3M3XvQ=="], + "lefthook-darwin-x64": ["lefthook-darwin-x64@1.12.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-juGzUr41z8y8zhLhZhvqN9WeqHPoovFTymGg3gmj0bkrYj2V+dSRtl+nbbz5EZTeK2T32BvrYINQJjV9rmnjwg=="], - "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@1.11.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-QevwQ7lrv5wBCkk7LLTzT5KR3Bk/5nttSxT1UH2o0EsgirS/c2K5xSgQmV6m3CiZYuCe2Pja4BSIwN3zt17SMw=="], + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@1.12.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2YUGdslUhzjpHLmXeOX8xhuZkYokotIMjnhN198Vly0aC+kq6ognRnUjBHOSjlIf1i9gAU7sV81yEuofNYv8Mw=="], - "lefthook-freebsd-x64": ["lefthook-freebsd-x64@1.11.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PYbcyNgdJJ4J2pEO9Ss4oYo5yq4vmQGTKm3RTYbRx4viSWR65hvKCP0C4LnIqspMvmR05SJi2bqe7UBP2t60EA=="], + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@1.12.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zzPu+6oD4oEh+oS0ayChlb+Y6i5momSM8gBcJKNmuze0xdFt3O4Z104ogXznRGh8bFgrPmtJgeLpwemfG/5fKg=="], - "lefthook-linux-arm64": ["lefthook-linux-arm64@1.11.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0pBMBAoafOAEg345eOPozsmRjWR0zCr6k+m5ZxwRBZbZx1bQFDqBakQ3TpFCphhcykmgFyaa1KeZJZUOrEsezA=="], + "lefthook-linux-arm64": ["lefthook-linux-arm64@1.12.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oMFkBZS7ax/upTFXfnSrqkiiwPtn1rp6vIEQhjWTxQnNcjd8dbZSlVUcpn0yolSpVAnPSKN+2pbLZEonD24YEw=="], - "lefthook-linux-x64": ["lefthook-linux-x64@1.11.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eiezheZ/bisBCMB2Ur0mctug/RDFyu39B5wzoE8y4z0W1yw6jHGrWMJ4Y8+5qKZ7fmdZg+7YPuMHZ2eFxOnhQA=="], + "lefthook-linux-x64": ["lefthook-linux-x64@1.12.0", "", { "os": "linux", "cpu": "x64" }, "sha512-IKPJcu14FjvefaXC4VZ9NSxZicVoTGZ86OlqiYvT+1hov850YEyusIEo/J8fEzZzQ2F16tQ24iqzrOLtXpp1gQ=="], - "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@1.11.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DRLTzXdtCj/TizpLcGSqXcnrqvgxeXgn/6nqzclIGqNdKCsNXDzpI0D3sP13Vwwmyoqv2etoTak2IHqZiXZDqg=="], + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@1.12.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-pZWid2jTvBgKEHJEKr6bAR9dXXTpA+2XKtxRFSLoI0KXDI9BH8KxH50zO6Y83siB96YygHlsjWbSsObIYC8Tgw=="], - "lefthook-openbsd-x64": ["lefthook-openbsd-x64@1.11.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-l7om+ZjWpYrVZyDuElwnucZhEqa7YfwlRaKBenkBxEh2zMje8O6Zodeuma1KmyDbSFvnvEjARo/Ejiot4gLXEw=="], + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@1.12.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-gHHqgcuJHzWThnhUUFxk8lm2pSL0z/xGHgZakmBct5vzItfEmXFmlnKHFbWbam4fvNIWKGFKAVCWH3O7x0IQVA=="], - "lefthook-windows-arm64": ["lefthook-windows-arm64@1.11.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-X0iTrql2gfPAkU2dzRwuHWgW5RcqCPbzJtKQ41X6Y/F7iQacRknmuYUGyC81funSvzGAsvlusMVLUvaFjIKnbA=="], + "lefthook-windows-arm64": ["lefthook-windows-arm64@1.12.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-SXbD5zCCpMSorgr14h4IzgFfBkxJsvso15VTFy1YrIAaM3jhrjlinm/Zk5gBZi82D8J+JbAlbhWC4Z93ombgoA=="], - "lefthook-windows-x64": ["lefthook-windows-x64@1.11.3", "", { "os": "win32", "cpu": "x64" }, "sha512-F+ORMn6YJXoS0EXU5LtN1FgV4QX9rC9LucZEkRmK6sKmS7hcb9IHpyb7siRGytArYzJvXVjPbxPBNSBdN4egZQ=="], + "lefthook-windows-x64": ["lefthook-windows-x64@1.12.0", "", { "os": "win32", "cpu": "x64" }, "sha512-cUOpmq8qJMhrOTqAXklsSbSaIxABVPBcENOD84N/pyqQpCdR3LAxIcq5cHLLap6ek8zPYwvFeBe1hVQC/X/xvQ=="], "openapi3-ts": ["openapi3-ts@4.4.0", "", { "dependencies": { "yaml": "^2.5.0" } }, "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw=="], diff --git a/package.json b/package.json index 4e5d72d6..5064569d 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "devDependencies": { "@biomejs/biome": "~1.9.0", "@types/bun": "^1.2.0", - "lefthook": "~1.11.0", + "lefthook": "~1.12.0", "sort-package-json": "^3.0.0" }, "peerDependencies": { From 8a53c6d087e9f867762b54ec3e84c959f5d030ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 09:46:47 +0200 Subject: [PATCH 11/47] Update step-security/harden-runner action to v2.13.0 (#226) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4f20adae..2e87fbd8 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -150,7 +150,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b9ad06c..402c2a62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: sha_short: ${{ steps.ctx.outputs.sha_short }} steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit From ee0cddc0d46635e295e3df47f81c90b828b19698 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 09:02:33 +0200 Subject: [PATCH 12/47] Update dependency hono to ~4.9.0 (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 8face684..8ff63deb 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "dependencies": { "@hono/zod-openapi": "~0.19.0", "env-var": "~7.5.0", - "hono": "~4.7.0", + "hono": "~4.9.0", }, "devDependencies": { "@biomejs/biome": "~1.9.0", @@ -67,7 +67,7 @@ "git-hooks-list": ["git-hooks-list@3.2.0", "", {}, "sha512-ZHG9a1gEhUMX1TvGrLdyWb9kDopCBbTnI8z4JgRMYxsijWipgjSEYoPWqBuIB0DnRnvqlQSEeVmzpeuPm7NdFQ=="], - "hono": ["hono@4.7.4", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="], + "hono": ["hono@4.9.0", "", {}, "sha512-JAUc4Sqi3lhby2imRL/67LMcJFKiCu7ZKghM7iwvltVZzxEC5bVJCsAa4NTnSfmWGb+N2eOVtFE586R+K3fejA=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], diff --git a/package.json b/package.json index 5064569d..65d0517b 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "dependencies": { "@hono/zod-openapi": "~0.19.0", "env-var": "~7.5.0", - "hono": "~4.7.0" + "hono": "~4.9.0" }, "devDependencies": { "@biomejs/biome": "~1.9.0", From e18164506256b5319ee6ec1d083ce9ea309e239d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:01:43 +0200 Subject: [PATCH 13/47] Update actions/checkout action to v5 (#231) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 2e87fbd8..d7baa91f 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -76,7 +76,7 @@ jobs: echo "extended=${TIMESTAMP}-${SHA_SHORT}" >>"$GITHUB_OUTPUT" - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false @@ -189,7 +189,7 @@ jobs: echo "list=${TAGS[*]}" >>"$GITHUB_OUTPUT" - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 402c2a62..e0eccd67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false From 8fc9a978c092542c2a4b482f982f58e2396bca04 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:22:29 +0200 Subject: [PATCH 14/47] Update actions/attest-build-provenance action to v3 (#232) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index d7baa91f..6c44c55a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -130,7 +130,7 @@ jobs: - if: inputs.artifact-action == 'build-release' name: Attest artifact - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 with: subject-path: | dist/*.tar.xz @@ -229,7 +229,7 @@ jobs: - if: inputs.image-action == 'build-release' name: Attest image - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 with: subject-name: "${{ env.REGISTRY }}/${{ steps.build-image.outputs.image }}" subject-digest: ${{ steps.push-image.outputs.digest }} From c33a1b642f83ecb90e3e27589dfe970beb4b169f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 13:23:52 +0200 Subject: [PATCH 15/47] Update ncipollo/release-action action to v1.20.0 (#233) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6c44c55a..aa33ab53 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -119,7 +119,7 @@ jobs: - if: inputs.artifact-action == 'build-release' name: Release artifact - uses: ncipollo/release-action@bcfe5470707e8832e12347755757cec0eb3c22af # v1.18.0 + uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 with: name: ${{ steps.tags.outputs.extended }} tag: ${{ steps.tags.outputs.extended }} From baee594ab9062a1794cd475f6d393f660be6e054 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 20:46:35 +0000 Subject: [PATCH 16/47] Update step-security/harden-runner action to v2.13.1 (#234) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index aa33ab53..7a8001e2 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit @@ -150,7 +150,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0eccd67..36f448e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: sha_short: ${{ steps.ctx.outputs.sha_short }} steps: - name: Harden Runner - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 with: egress-policy: audit From fb528d1a9da423d1308b928feb0065c78c898738 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:36:25 +0200 Subject: [PATCH 17/47] Update dependency lefthook to ~1.13.0 (#235) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 24 ++++++++++++------------ package.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/bun.lock b/bun.lock index 8ff63deb..20b044a4 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "devDependencies": { "@biomejs/biome": "~1.9.0", "@types/bun": "^1.2.0", - "lefthook": "~1.12.0", + "lefthook": "~1.13.0", "sort-package-json": "^3.0.0", }, "peerDependencies": { @@ -71,27 +71,27 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "lefthook": ["lefthook@1.12.0", "", { "optionalDependencies": { "lefthook-darwin-arm64": "1.12.0", "lefthook-darwin-x64": "1.12.0", "lefthook-freebsd-arm64": "1.12.0", "lefthook-freebsd-x64": "1.12.0", "lefthook-linux-arm64": "1.12.0", "lefthook-linux-x64": "1.12.0", "lefthook-openbsd-arm64": "1.12.0", "lefthook-openbsd-x64": "1.12.0", "lefthook-windows-arm64": "1.12.0", "lefthook-windows-x64": "1.12.0" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-+lJSdsNcKzxv4TcIcXrd21lBXI7wrVXZ080wiJzz4sHz52KyKVPfALDPMm7+VWBtATpnCtHQe2ZPQwiBmy6VRw=="], + "lefthook": ["lefthook@1.13.0", "", { "optionalDependencies": { "lefthook-darwin-arm64": "1.13.0", "lefthook-darwin-x64": "1.13.0", "lefthook-freebsd-arm64": "1.13.0", "lefthook-freebsd-x64": "1.13.0", "lefthook-linux-arm64": "1.13.0", "lefthook-linux-x64": "1.13.0", "lefthook-openbsd-arm64": "1.13.0", "lefthook-openbsd-x64": "1.13.0", "lefthook-windows-arm64": "1.13.0", "lefthook-windows-x64": "1.13.0" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-6pno+NjfBrKKt3XQmFUvwDdKXzBVh5JvzAIwcCOu9mqg81nAMCZd2FtTuU1fmDzXFNdsxjW8mwwKB+S8t5ucOQ=="], - "lefthook-darwin-arm64": ["lefthook-darwin-arm64@1.12.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XMwneWj+Ux/6lI+T8ZmXwPkPS9b2n9DgMcjR4kNDTzPClBTNjKngQkh/SOzOKV/w//fkkQCdfCRMvV9f9FLS3A=="], + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@1.13.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mhD4zOj2VRx34tptEc/lP643n5YAAVP95f/TiP6geQz4kpLwUrsTwQxzoXUIauU2DGSNbFtp9hVSE++0e4ESEA=="], - "lefthook-darwin-x64": ["lefthook-darwin-x64@1.12.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-juGzUr41z8y8zhLhZhvqN9WeqHPoovFTymGg3gmj0bkrYj2V+dSRtl+nbbz5EZTeK2T32BvrYINQJjV9rmnjwg=="], + "lefthook-darwin-x64": ["lefthook-darwin-x64@1.13.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-uspgWrhh9Xoyb+x0hVeMnYkSA1K/cEov4QHxcBBTIvTvjEuijSLIQEzULsHvg7a6xNM/8E3SBzOwBRK44jM2Mw=="], - "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@1.12.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2YUGdslUhzjpHLmXeOX8xhuZkYokotIMjnhN198Vly0aC+kq6ognRnUjBHOSjlIf1i9gAU7sV81yEuofNYv8Mw=="], + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@1.13.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UUY+UlGuwAkO8hEY4+SGYfM1OeXSI4i2/8ROwBpu6fz0LrTL1OUYRVhLIRNJvWrF2XabfgXVUrnjGY7YSq4zpg=="], - "lefthook-freebsd-x64": ["lefthook-freebsd-x64@1.12.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zzPu+6oD4oEh+oS0ayChlb+Y6i5momSM8gBcJKNmuze0xdFt3O4Z104ogXznRGh8bFgrPmtJgeLpwemfG/5fKg=="], + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@1.13.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wdF/Cwmbiblz+UaLb3a0trSKEmaY5z20latrmhim98M1H48iBHhUyUUJWaSEauyFMJWPwu7rSVZl5KktPxCxVA=="], - "lefthook-linux-arm64": ["lefthook-linux-arm64@1.12.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-oMFkBZS7ax/upTFXfnSrqkiiwPtn1rp6vIEQhjWTxQnNcjd8dbZSlVUcpn0yolSpVAnPSKN+2pbLZEonD24YEw=="], + "lefthook-linux-arm64": ["lefthook-linux-arm64@1.13.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tpg4pA0JTeLxGAZDFJVOGyIMjQAE7F8HcM31tj+3KOogahspOffpmSoS1SlHzUSZ8Jm+Bvoqcis/sW68HkmWHw=="], - "lefthook-linux-x64": ["lefthook-linux-x64@1.12.0", "", { "os": "linux", "cpu": "x64" }, "sha512-IKPJcu14FjvefaXC4VZ9NSxZicVoTGZ86OlqiYvT+1hov850YEyusIEo/J8fEzZzQ2F16tQ24iqzrOLtXpp1gQ=="], + "lefthook-linux-x64": ["lefthook-linux-x64@1.13.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5JUhlDaYqt9vBTSQ5gkA00+0ktUSRyL60AhZID6OR4ML39SidzMTu/GrgHscPT4sD3TfSODEdGZ28sNKdLg6jA=="], - "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@1.12.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-pZWid2jTvBgKEHJEKr6bAR9dXXTpA+2XKtxRFSLoI0KXDI9BH8KxH50zO6Y83siB96YygHlsjWbSsObIYC8Tgw=="], + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@1.13.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-UNCoKrbH0Yv61jCCUIPRr7ErS3yYt2VNCFdzLf752O9K0yrfn9FzYUsyxQFEn1Ah/kq+TNgZw90gVLg5fv1t4g=="], - "lefthook-openbsd-x64": ["lefthook-openbsd-x64@1.12.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-gHHqgcuJHzWThnhUUFxk8lm2pSL0z/xGHgZakmBct5vzItfEmXFmlnKHFbWbam4fvNIWKGFKAVCWH3O7x0IQVA=="], + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@1.13.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-iyvE+jgHYnLvOoHsLykgf98lftewsQzEBciYxygna9sLZ9nLvfbwp9mWUk09yMRmPCFGDeeDecERaUa2SICWLA=="], - "lefthook-windows-arm64": ["lefthook-windows-arm64@1.12.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-SXbD5zCCpMSorgr14h4IzgFfBkxJsvso15VTFy1YrIAaM3jhrjlinm/Zk5gBZi82D8J+JbAlbhWC4Z93ombgoA=="], + "lefthook-windows-arm64": ["lefthook-windows-arm64@1.13.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+u0GyvZouKGcecFsayIbzq1KIoDcrSqVhivLfJUq7vpMXbSHV5HbhrkdkfqkuGjGgGnWulQY29/bDubTQoqfOA=="], - "lefthook-windows-x64": ["lefthook-windows-x64@1.12.0", "", { "os": "win32", "cpu": "x64" }, "sha512-cUOpmq8qJMhrOTqAXklsSbSaIxABVPBcENOD84N/pyqQpCdR3LAxIcq5cHLLap6ek8zPYwvFeBe1hVQC/X/xvQ=="], + "lefthook-windows-x64": ["lefthook-windows-x64@1.13.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RG8dfOkszk6BaOA7k26NO0R1/vy1tno7/wgdg+Wjt0pYFiBo0DhmPMoAVB4kzjObqBKDd1KWidzsEv4/R0oFIg=="], "openapi3-ts": ["openapi3-ts@4.4.0", "", { "dependencies": { "yaml": "^2.5.0" } }, "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw=="], diff --git a/package.json b/package.json index 65d0517b..072a4b19 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "devDependencies": { "@biomejs/biome": "~1.9.0", "@types/bun": "^1.2.0", - "lefthook": "~1.12.0", + "lefthook": "~1.13.0", "sort-package-json": "^3.0.0" }, "peerDependencies": { From 7bae5750303c00679eaa73e5807f1d53bbb5ad18 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:04:46 +0200 Subject: [PATCH 18/47] Update dependency hono to ~4.10.0 (#236) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 20b044a4..0263d563 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "dependencies": { "@hono/zod-openapi": "~0.19.0", "env-var": "~7.5.0", - "hono": "~4.9.0", + "hono": "~4.10.0", }, "devDependencies": { "@biomejs/biome": "~1.9.0", @@ -67,7 +67,7 @@ "git-hooks-list": ["git-hooks-list@3.2.0", "", {}, "sha512-ZHG9a1gEhUMX1TvGrLdyWb9kDopCBbTnI8z4JgRMYxsijWipgjSEYoPWqBuIB0DnRnvqlQSEeVmzpeuPm7NdFQ=="], - "hono": ["hono@4.9.0", "", {}, "sha512-JAUc4Sqi3lhby2imRL/67LMcJFKiCu7ZKghM7iwvltVZzxEC5bVJCsAa4NTnSfmWGb+N2eOVtFE586R+K3fejA=="], + "hono": ["hono@4.10.0", "", {}, "sha512-V/S2IyKL6fk5+bEjiQzg74r5BglqAwU20IX3WjdTUFgvmtSqAZjSxN/Zb5lr6/JXVmH0aqkqOq++3UgzOi9+4Q=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], diff --git a/package.json b/package.json index 072a4b19..1f818a68 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "dependencies": { "@hono/zod-openapi": "~0.19.0", "env-var": "~7.5.0", - "hono": "~4.9.0" + "hono": "~4.10.0" }, "devDependencies": { "@biomejs/biome": "~1.9.0", From 8afee66dc3f7b3a7dd8e977ef62bc79038670f70 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:06:59 +0100 Subject: [PATCH 19/47] Update dependency lefthook to v2 (#237) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 24 ++++++++++++------------ package.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/bun.lock b/bun.lock index 0263d563..687231e8 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "devDependencies": { "@biomejs/biome": "~1.9.0", "@types/bun": "^1.2.0", - "lefthook": "~1.13.0", + "lefthook": "~2.0.0", "sort-package-json": "^3.0.0", }, "peerDependencies": { @@ -71,27 +71,27 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "lefthook": ["lefthook@1.13.0", "", { "optionalDependencies": { "lefthook-darwin-arm64": "1.13.0", "lefthook-darwin-x64": "1.13.0", "lefthook-freebsd-arm64": "1.13.0", "lefthook-freebsd-x64": "1.13.0", "lefthook-linux-arm64": "1.13.0", "lefthook-linux-x64": "1.13.0", "lefthook-openbsd-arm64": "1.13.0", "lefthook-openbsd-x64": "1.13.0", "lefthook-windows-arm64": "1.13.0", "lefthook-windows-x64": "1.13.0" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-6pno+NjfBrKKt3XQmFUvwDdKXzBVh5JvzAIwcCOu9mqg81nAMCZd2FtTuU1fmDzXFNdsxjW8mwwKB+S8t5ucOQ=="], + "lefthook": ["lefthook@2.0.2", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.2", "lefthook-darwin-x64": "2.0.2", "lefthook-freebsd-arm64": "2.0.2", "lefthook-freebsd-x64": "2.0.2", "lefthook-linux-arm64": "2.0.2", "lefthook-linux-x64": "2.0.2", "lefthook-openbsd-arm64": "2.0.2", "lefthook-openbsd-x64": "2.0.2", "lefthook-windows-arm64": "2.0.2", "lefthook-windows-x64": "2.0.2" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-2lrSva53G604ZWjK5kHYvDdwb5GzbhciIPWhebv0A8ceveqSsnG2JgVEt+DnhOPZ4VfNcXvt3/ohFBPNpuAlVw=="], - "lefthook-darwin-arm64": ["lefthook-darwin-arm64@1.13.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mhD4zOj2VRx34tptEc/lP643n5YAAVP95f/TiP6geQz4kpLwUrsTwQxzoXUIauU2DGSNbFtp9hVSE++0e4ESEA=="], + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-x/4AOinpMS2abZyA/krDd50cRPZit/6P670Z1mJjfS0+fPZkFw7AXpjxroiN0rgglg78vD7BwcA5331z4YZa5g=="], - "lefthook-darwin-x64": ["lefthook-darwin-x64@1.13.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-uspgWrhh9Xoyb+x0hVeMnYkSA1K/cEov4QHxcBBTIvTvjEuijSLIQEzULsHvg7a6xNM/8E3SBzOwBRK44jM2Mw=="], + "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-MSb8XZBfmlNvCpuLiQqrJS+sPiSEAyuoHOMZOHjlceYqO0leVVw9YfePVcb4Vi/PqOYngTdJk83MmYvqhsSNTQ=="], - "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@1.13.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-UUY+UlGuwAkO8hEY4+SGYfM1OeXSI4i2/8ROwBpu6fz0LrTL1OUYRVhLIRNJvWrF2XabfgXVUrnjGY7YSq4zpg=="], + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gewPsUPc3J/n2/RrhHLS9jtL3qK4HcTED25vfExhvFRW3eT1SDYaBbXnUUmB8SE0zE8Bl6AfEdT2zzZcPbOFuA=="], - "lefthook-freebsd-x64": ["lefthook-freebsd-x64@1.13.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wdF/Cwmbiblz+UaLb3a0trSKEmaY5z20latrmhim98M1H48iBHhUyUUJWaSEauyFMJWPwu7rSVZl5KktPxCxVA=="], + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fsLlaChiKAWiSavQO2LXPR8Z9OcBnyMDvmkIlXC0lG3SjBb9xbVdBdDVlcrsUyDCs5YstmGYHuzw6DfJYpAE1g=="], - "lefthook-linux-arm64": ["lefthook-linux-arm64@1.13.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tpg4pA0JTeLxGAZDFJVOGyIMjQAE7F8HcM31tj+3KOogahspOffpmSoS1SlHzUSZ8Jm+Bvoqcis/sW68HkmWHw=="], + "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-vNl3HiZud9T2nGHMngvLw3hSJgutjlN/Lzf5/5jKt/2IIuyd9L3UYktWC9HLUb03Zukr7jeaxG3+VxdAohQwAw=="], - "lefthook-linux-x64": ["lefthook-linux-x64@1.13.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5JUhlDaYqt9vBTSQ5gkA00+0ktUSRyL60AhZID6OR4ML39SidzMTu/GrgHscPT4sD3TfSODEdGZ28sNKdLg6jA=="], + "lefthook-linux-x64": ["lefthook-linux-x64@2.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0ghHMPu4fixIieS8V2k2yZHvcFd9pP0q+sIAIaWo8x7ce/AOQIXFCPHGPAOc8/wi5uVtfyEvCnhxIDKf+lHA2A=="], - "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@1.13.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-UNCoKrbH0Yv61jCCUIPRr7ErS3yYt2VNCFdzLf752O9K0yrfn9FzYUsyxQFEn1Ah/kq+TNgZw90gVLg5fv1t4g=="], + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-qfXnDM8jffut9rylvi3T+HOqlNRkFYqIDUXeVXlY7dmwCW4u2K46p0W4M3BmAVUeL/MRxBRnjze//Yy6aCbGQw=="], - "lefthook-openbsd-x64": ["lefthook-openbsd-x64@1.13.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-iyvE+jgHYnLvOoHsLykgf98lftewsQzEBciYxygna9sLZ9nLvfbwp9mWUk09yMRmPCFGDeeDecERaUa2SICWLA=="], + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RXqR0FiDTwsQv1X3QVsuBFneWeNXS+tmPFIX8F6Wz9yDPHF8+vBnkWCju6HdkTVTY71Ba5HbYGKEVDvscJkU7Q=="], - "lefthook-windows-arm64": ["lefthook-windows-arm64@1.13.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+u0GyvZouKGcecFsayIbzq1KIoDcrSqVhivLfJUq7vpMXbSHV5HbhrkdkfqkuGjGgGnWulQY29/bDubTQoqfOA=="], + "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-KfLKhiUPHP9Aea+9D7or2hgL9wtKEV+GHpx7LBg82ZhCXkAml6rop7mWsBgL80xPYLqMahKolZGO+8z5H6W4HQ=="], - "lefthook-windows-x64": ["lefthook-windows-x64@1.13.0", "", { "os": "win32", "cpu": "x64" }, "sha512-RG8dfOkszk6BaOA7k26NO0R1/vy1tno7/wgdg+Wjt0pYFiBo0DhmPMoAVB4kzjObqBKDd1KWidzsEv4/R0oFIg=="], + "lefthook-windows-x64": ["lefthook-windows-x64@2.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-TdysWxGRNtuRg5bN6Uj00tZJIsHTrF/7FavoR5rp1sq21QJhJi36M4I3UVlmOKAUCKhibAIAauZWmX7yaW3eHA=="], "openapi3-ts": ["openapi3-ts@4.4.0", "", { "dependencies": { "yaml": "^2.5.0" } }, "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw=="], diff --git a/package.json b/package.json index 1f818a68..a3cd1aef 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "devDependencies": { "@biomejs/biome": "~1.9.0", "@types/bun": "^1.2.0", - "lefthook": "~1.13.0", + "lefthook": "~2.0.0", "sort-package-json": "^3.0.0" }, "peerDependencies": { From f59a7bfb525a32d5ae8cb6cc5af18c15e5fc28a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:07:26 +0100 Subject: [PATCH 20/47] Update dependency typescript to ~5.8.0 || ~5.9.0 (#228) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index 687231e8..f202ee1c 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ "sort-package-json": "^3.0.0", }, "peerDependencies": { - "typescript": "~5.8.0", + "typescript": "~5.8.0 || ~5.9.0", }, }, }, diff --git a/package.json b/package.json index a3cd1aef..105ab5ce 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "sort-package-json": "^3.0.0" }, "peerDependencies": { - "typescript": "~5.8.0" + "typescript": "~5.8.0 || ~5.9.0" }, "trustedDependencies": [ "@biomejs/biome", From 5fc03783c66370c119ed787c14c20ca2043b680b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:15:47 +0000 Subject: [PATCH 21/47] Update step-security/harden-runner action to v2.13.2 (#238) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 7a8001e2..a0c683f0 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit @@ -150,7 +150,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36f448e8..a86f7b15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: sha_short: ${{ steps.ctx.outputs.sha_short }} steps: - name: Harden Runner - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 with: egress-policy: audit From 1fbd76029e68ea1458798d90845a7e8ad26a0419 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 01:10:11 +0000 Subject: [PATCH 22/47] Lock file maintenance (#239) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 67 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/bun.lock b/bun.lock index f202ee1c..eee86732 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 1, "workspaces": { "": { "dependencies": { @@ -23,7 +24,7 @@ "lefthook", ], "packages": { - "@asteasolutions/zod-to-openapi": ["@asteasolutions/zod-to-openapi@7.3.0", "", { "dependencies": { "openapi3-ts": "^4.1.2" }, "peerDependencies": { "zod": "^3.20.2" } }, "sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q=="], + "@asteasolutions/zod-to-openapi": ["@asteasolutions/zod-to-openapi@7.3.4", "", { "dependencies": { "openapi3-ts": "^4.1.2" }, "peerDependencies": { "zod": "^3.20.2" } }, "sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA=="], "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], @@ -43,74 +44,74 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - "@hono/zod-openapi": ["@hono/zod-openapi@0.19.2", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^7.1.0", "@hono/zod-validator": "^0.4.1" }, "peerDependencies": { "hono": ">=4.3.6", "zod": "3.*" } }, "sha512-lkFa6wdQVgY7d7/m++Ixr3hvKCF5Y+zjTIPM37fex5ylCfX53A/W28gZRDuFZx3aR+noKob7lHfwdk9dURLzxw=="], + "@hono/zod-openapi": ["@hono/zod-openapi@0.19.10", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@hono/zod-validator": "^0.7.1", "openapi3-ts": "^4.5.0" }, "peerDependencies": { "hono": ">=4.3.6", "zod": ">=3.0.0" } }, "sha512-dpoS6DenvoJyvxtQ7Kd633FRZ/Qf74+4+o9s+zZI8pEqnbjdF/DtxIib08WDpCaWabMEJOL5TXpMgNEZvb7hpA=="], - "@hono/zod-validator": ["@hono/zod-validator@0.4.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ=="], + "@hono/zod-validator": ["@hono/zod-validator@0.7.4", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-biKGn3BRJVaftZlIPMyK+HCe/UHAjJ6sH0UyXe3+v0OcgVr9xfImDROTJFLtn9e3XEEAHGZIM9U6evu85abm8Q=="], - "@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="], + "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="], - "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + "@types/react": ["@types/react@19.2.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw=="], - "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], + "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="], - "detect-indent": ["detect-indent@7.0.1", "", {}, "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g=="], + "csstype": ["csstype@3.2.2", "", {}, "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g=="], + + "detect-indent": ["detect-indent@7.0.2", "", {}, "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A=="], "detect-newline": ["detect-newline@4.0.1", "", {}, "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog=="], "env-var": ["env-var@7.5.0", "", {}, "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA=="], - "fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="], - - "get-stdin": ["get-stdin@9.0.0", "", {}, "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - "git-hooks-list": ["git-hooks-list@3.2.0", "", {}, "sha512-ZHG9a1gEhUMX1TvGrLdyWb9kDopCBbTnI8z4JgRMYxsijWipgjSEYoPWqBuIB0DnRnvqlQSEeVmzpeuPm7NdFQ=="], + "git-hooks-list": ["git-hooks-list@4.1.1", "", {}, "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA=="], - "hono": ["hono@4.10.0", "", {}, "sha512-V/S2IyKL6fk5+bEjiQzg74r5BglqAwU20IX3WjdTUFgvmtSqAZjSxN/Zb5lr6/JXVmH0aqkqOq++3UgzOi9+4Q=="], + "hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "lefthook": ["lefthook@2.0.2", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.2", "lefthook-darwin-x64": "2.0.2", "lefthook-freebsd-arm64": "2.0.2", "lefthook-freebsd-x64": "2.0.2", "lefthook-linux-arm64": "2.0.2", "lefthook-linux-x64": "2.0.2", "lefthook-openbsd-arm64": "2.0.2", "lefthook-openbsd-x64": "2.0.2", "lefthook-windows-arm64": "2.0.2", "lefthook-windows-x64": "2.0.2" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-2lrSva53G604ZWjK5kHYvDdwb5GzbhciIPWhebv0A8ceveqSsnG2JgVEt+DnhOPZ4VfNcXvt3/ohFBPNpuAlVw=="], + "lefthook": ["lefthook@2.0.4", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.4", "lefthook-darwin-x64": "2.0.4", "lefthook-freebsd-arm64": "2.0.4", "lefthook-freebsd-x64": "2.0.4", "lefthook-linux-arm64": "2.0.4", "lefthook-linux-x64": "2.0.4", "lefthook-openbsd-arm64": "2.0.4", "lefthook-openbsd-x64": "2.0.4", "lefthook-windows-arm64": "2.0.4", "lefthook-windows-x64": "2.0.4" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-GNCU2vQWM/UWjiEF23601aILi1aMbPke6viortH7wIO/oVGOCW0H6FdLez4XZDyqnHL9XkTnd0BBVrBbYVMLpA=="], - "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-x/4AOinpMS2abZyA/krDd50cRPZit/6P670Z1mJjfS0+fPZkFw7AXpjxroiN0rgglg78vD7BwcA5331z4YZa5g=="], + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AR63/O5UkM7Sc6x5PhP4vTuztTYRBeBroXApeWGM/8e5uZyoQug/7KTh7xhbCMDf8WJv6vdFeXAQCPSmDyPU3Q=="], - "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-MSb8XZBfmlNvCpuLiQqrJS+sPiSEAyuoHOMZOHjlceYqO0leVVw9YfePVcb4Vi/PqOYngTdJk83MmYvqhsSNTQ=="], + "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-618DVUttSzV9egQiqTQoxGfnR240JoPWYmqRVHhiegnQKZ2lp5XJ+7NMxeRk/ih93VVOLzFO5ky3PbpxTmJgjQ=="], - "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gewPsUPc3J/n2/RrhHLS9jtL3qK4HcTED25vfExhvFRW3eT1SDYaBbXnUUmB8SE0zE8Bl6AfEdT2zzZcPbOFuA=="], + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mTAQym1BK38fKglHBQ/0GXPznVC4LoStHO5lAI3ZxaEC0FQetqGHYFzhWbIH5sde9JhztE2rL/aBzMHDoAtzSw=="], - "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fsLlaChiKAWiSavQO2LXPR8Z9OcBnyMDvmkIlXC0lG3SjBb9xbVdBdDVlcrsUyDCs5YstmGYHuzw6DfJYpAE1g=="], + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sy02aSxd8UMd6XmiPFVl/Em0b78jdZcDSsLwg+bweJQQk0l+vJhOfqFiG11mbnpo+EBIZmRe6OH5LkxeSU36+w=="], - "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-vNl3HiZud9T2nGHMngvLw3hSJgutjlN/Lzf5/5jKt/2IIuyd9L3UYktWC9HLUb03Zukr7jeaxG3+VxdAohQwAw=="], + "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-W0Nlr/Cz2QTH9n4k5zNrk3LSsg1C4wHiJi8hrAiQVTaAV/N1XrKqd0DevqQuouuapG6pw/6B1xCgiNPebv9oyw=="], - "lefthook-linux-x64": ["lefthook-linux-x64@2.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0ghHMPu4fixIieS8V2k2yZHvcFd9pP0q+sIAIaWo8x7ce/AOQIXFCPHGPAOc8/wi5uVtfyEvCnhxIDKf+lHA2A=="], + "lefthook-linux-x64": ["lefthook-linux-x64@2.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-N6ySVCtB/DrOZ1ZgPL8WBZTgtoVHvcPKI+LV5wbcGrvA/dzDZFvniadrbDWZg7Tm705efiQzyENjwhhqNkwiww=="], - "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-qfXnDM8jffut9rylvi3T+HOqlNRkFYqIDUXeVXlY7dmwCW4u2K46p0W4M3BmAVUeL/MRxBRnjze//Yy6aCbGQw=="], + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-VmOhJO3pYzZ/1C2WFXtL/n5pq4/eYOroqJJpwTJfmCHyw4ceLACu8MDyU5AMJhGMkbL8mPxGInJKxg5xhYgGRw=="], - "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RXqR0FiDTwsQv1X3QVsuBFneWeNXS+tmPFIX8F6Wz9yDPHF8+vBnkWCju6HdkTVTY71Ba5HbYGKEVDvscJkU7Q=="], + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-U8MZz1xlHUdflkQQ2hkMQsei6fSZbs8tuE4EjCIHWnNdnAF4V8sZ6n1KbxsJcoZXPyBZqxZSMu1o/Ye8IAMVKg=="], - "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-KfLKhiUPHP9Aea+9D7or2hgL9wtKEV+GHpx7LBg82ZhCXkAml6rop7mWsBgL80xPYLqMahKolZGO+8z5H6W4HQ=="], + "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-543H3y2JAwNdvwUQ6nlNBG7rdKgoOUgzAa6pYcl6EoqicCRrjRmGhkJu7vUudkkrD2Wjm7tr9hU9poP2g5fRFQ=="], - "lefthook-windows-x64": ["lefthook-windows-x64@2.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-TdysWxGRNtuRg5bN6Uj00tZJIsHTrF/7FavoR5rp1sq21QJhJi36M4I3UVlmOKAUCKhibAIAauZWmX7yaW3eHA=="], + "lefthook-windows-x64": ["lefthook-windows-x64@2.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UDEPK9RWKm60xsNOdS/DQOdFba0SFa4w3tpFMXK1AJzmRHhosoKrorXGhtTr6kcM0MGKOtYi8GHsm++ArZ9wvQ=="], - "openapi3-ts": ["openapi3-ts@4.4.0", "", { "dependencies": { "yaml": "^2.5.0" } }, "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw=="], + "openapi3-ts": ["openapi3-ts@4.5.0", "", { "dependencies": { "yaml": "^2.8.0" } }, "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ=="], - "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "sort-object-keys": ["sort-object-keys@1.1.3", "", {}, "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg=="], - "sort-package-json": ["sort-package-json@3.0.0", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.1", "get-stdin": "^9.0.0", "git-hooks-list": "^3.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.7.1", "sort-object-keys": "^1.1.3", "tinyglobby": "^0.2.12" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-vfZWx4DnFNB8R9Vg4Dnx21s20auNzWH15ZaCBfADAiyrCwemRmhWstTgvLjMek1DW3+MHcNaqkp86giCF24rMA=="], + "sort-package-json": ["sort-package-json@3.4.0", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.1", "git-hooks-list": "^4.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.7.1", "sort-object-keys": "^1.1.3", "tinyglobby": "^0.2.12" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-97oFRRMM2/Js4oEA9LJhjyMlde+2ewpZQf53pgue27UkbEXfHJnDzHlUxQ/DWUkzqmp7DFwJp8D+wi/TYeQhpA=="], - "tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], + "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], - "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + "zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], } } From 0ec8654cc324143d206144602c5fa6e38e339545 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 03:02:35 +0000 Subject: [PATCH 23/47] Update actions/checkout action to v5.0.1 (#240) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a0c683f0..57146414 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -76,7 +76,7 @@ jobs: echo "extended=${TIMESTAMP}-${SHA_SHORT}" >>"$GITHUB_OUTPUT" - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false @@ -189,7 +189,7 @@ jobs: echo "list=${TAGS[*]}" >>"$GITHUB_OUTPUT" - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a86f7b15..8f727709 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: persist-credentials: false From 6dde48221fcf16cfff233acc54b477cca8284ea8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 20:37:41 +0000 Subject: [PATCH 24/47] Update actions/checkout action to v6 (#241) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 57146414..da3f7e96 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -76,7 +76,7 @@ jobs: echo "extended=${TIMESTAMP}-${SHA_SHORT}" >>"$GITHUB_OUTPUT" - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false @@ -189,7 +189,7 @@ jobs: echo "list=${TAGS[*]}" >>"$GITHUB_OUTPUT" - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f727709..e027d45e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - name: Checkout - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false From 5cb032b33c9d30f830a0a8886f526de92d36d9ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:50:07 +0000 Subject: [PATCH 25/47] Lock file maintenance (#242) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index eee86732..845a01ac 100644 --- a/bun.lock +++ b/bun.lock @@ -46,17 +46,13 @@ "@hono/zod-openapi": ["@hono/zod-openapi@0.19.10", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@hono/zod-validator": "^0.7.1", "openapi3-ts": "^4.5.0" }, "peerDependencies": { "hono": ">=4.3.6", "zod": ">=3.0.0" } }, "sha512-dpoS6DenvoJyvxtQ7Kd633FRZ/Qf74+4+o9s+zZI8pEqnbjdF/DtxIib08WDpCaWabMEJOL5TXpMgNEZvb7hpA=="], - "@hono/zod-validator": ["@hono/zod-validator@0.7.4", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-biKGn3BRJVaftZlIPMyK+HCe/UHAjJ6sH0UyXe3+v0OcgVr9xfImDROTJFLtn9e3XEEAHGZIM9U6evu85abm8Q=="], + "@hono/zod-validator": ["@hono/zod-validator@0.7.5", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-n4l4hutkfYU07PzRUHBOVzUEn38VSfrh+UVE5d0w4lyfWDOEhzxIupqo5iakRiJL44c3vTuFJBvcmUl8b9agIA=="], - "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="], + "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - "@types/react": ["@types/react@19.2.5", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw=="], - - "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="], - - "csstype": ["csstype@3.2.2", "", {}, "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g=="], + "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], "detect-indent": ["detect-indent@7.0.2", "", {}, "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A=="], From 21972230cf1f0f4e7b2b7cdce1c69f6ec4de7ebc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 01:56:23 +0000 Subject: [PATCH 26/47] Lock file maintenance (#243) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 845a01ac..3e89c8b1 100644 --- a/bun.lock +++ b/bun.lock @@ -64,7 +64,7 @@ "git-hooks-list": ["git-hooks-list@4.1.1", "", {}, "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA=="], - "hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="], + "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], @@ -96,9 +96,9 @@ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "sort-object-keys": ["sort-object-keys@1.1.3", "", {}, "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg=="], + "sort-object-keys": ["sort-object-keys@2.0.1", "", {}, "sha512-R89fO+z3x7hiKPXX5P0qim+ge6Y60AjtlW+QQpRozrrNcR1lw9Pkpm5MLB56HoNvdcLHL4wbpq16OcvGpEDJIg=="], - "sort-package-json": ["sort-package-json@3.4.0", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.1", "git-hooks-list": "^4.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.7.1", "sort-object-keys": "^1.1.3", "tinyglobby": "^0.2.12" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-97oFRRMM2/Js4oEA9LJhjyMlde+2ewpZQf53pgue27UkbEXfHJnDzHlUxQ/DWUkzqmp7DFwJp8D+wi/TYeQhpA=="], + "sort-package-json": ["sort-package-json@3.5.0", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.1", "git-hooks-list": "^4.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.7.1", "sort-object-keys": "^2.0.0", "tinyglobby": "^0.2.12" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-moY4UtptUuP5sPuu9H9dp8xHNel7eP5/Kz/7+90jTvC0IOiPH2LigtRM/aSFSxreaWoToHUVUpEV4a2tAs2oKQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], @@ -106,8 +106,8 @@ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - "zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], + "zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], } } From 9b380f2215cfc5b604fd911f9be78a94d92b3b1a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 06:11:29 +0000 Subject: [PATCH 27/47] Update step-security/harden-runner action to v2.13.3 (#244) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index da3f7e96..eb18f06b 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit @@ -150,7 +150,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e027d45e..b1e3f702 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: sha_short: ${{ steps.ctx.outputs.sha_short }} steps: - name: Harden Runner - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 + uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 with: egress-policy: audit From 78379bb88f546adceb2f84b26f700934e53477ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:32:16 +0000 Subject: [PATCH 28/47] Update actions/checkout action to v6.0.1 (#245) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index eb18f06b..c462704e 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -76,7 +76,7 @@ jobs: echo "extended=${TIMESTAMP}-${SHA_SHORT}" >>"$GITHUB_OUTPUT" - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false @@ -189,7 +189,7 @@ jobs: echo "list=${TAGS[*]}" >>"$GITHUB_OUTPUT" - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1e3f702..19d6ec7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - name: Checkout - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false From 31529b6e017fc8eae376602920ddadcb79e09109 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 01:36:57 +0000 Subject: [PATCH 29/47] Lock file maintenance (#247) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/bun.lock b/bun.lock index 3e89c8b1..4194e015 100644 --- a/bun.lock +++ b/bun.lock @@ -48,11 +48,11 @@ "@hono/zod-validator": ["@hono/zod-validator@0.7.5", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-n4l4hutkfYU07PzRUHBOVzUEn38VSfrh+UVE5d0w4lyfWDOEhzxIupqo5iakRiJL44c3vTuFJBvcmUl8b9agIA=="], - "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], + "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], "detect-indent": ["detect-indent@7.0.2", "", {}, "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A=="], @@ -68,27 +68,27 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "lefthook": ["lefthook@2.0.4", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.4", "lefthook-darwin-x64": "2.0.4", "lefthook-freebsd-arm64": "2.0.4", "lefthook-freebsd-x64": "2.0.4", "lefthook-linux-arm64": "2.0.4", "lefthook-linux-x64": "2.0.4", "lefthook-openbsd-arm64": "2.0.4", "lefthook-openbsd-x64": "2.0.4", "lefthook-windows-arm64": "2.0.4", "lefthook-windows-x64": "2.0.4" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-GNCU2vQWM/UWjiEF23601aILi1aMbPke6viortH7wIO/oVGOCW0H6FdLez4XZDyqnHL9XkTnd0BBVrBbYVMLpA=="], + "lefthook": ["lefthook@2.0.8", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.8", "lefthook-darwin-x64": "2.0.8", "lefthook-freebsd-arm64": "2.0.8", "lefthook-freebsd-x64": "2.0.8", "lefthook-linux-arm64": "2.0.8", "lefthook-linux-x64": "2.0.8", "lefthook-openbsd-arm64": "2.0.8", "lefthook-openbsd-x64": "2.0.8", "lefthook-windows-arm64": "2.0.8", "lefthook-windows-x64": "2.0.8" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-FozDCKeSI+m3BP0cvyPgHch+yf7ClS3hDy1JsRUrbNmlyjqBcmlygnRXsZzpH+wHoNnF2fmfhJhkx/7S7IpaVw=="], - "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AR63/O5UkM7Sc6x5PhP4vTuztTYRBeBroXApeWGM/8e5uZyoQug/7KTh7xhbCMDf8WJv6vdFeXAQCPSmDyPU3Q=="], + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nu52qmqhSP+DKKuKYKDkMkPbgvgTZv+ueEo1LVXidTcgxEwvrbe2balcdqdulQTsPfYtm3pCPvv8ikalHrH+Qg=="], - "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-618DVUttSzV9egQiqTQoxGfnR240JoPWYmqRVHhiegnQKZ2lp5XJ+7NMxeRk/ih93VVOLzFO5ky3PbpxTmJgjQ=="], + "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-EGNBw1vuXzphs/KyDchkglwnYNkKQH3EpptIPXcQCRC3WKiz87PSrwkOxjGtgDg6nLYWru3YUzgcFrIGUXjWPw=="], - "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mTAQym1BK38fKglHBQ/0GXPznVC4LoStHO5lAI3ZxaEC0FQetqGHYFzhWbIH5sde9JhztE2rL/aBzMHDoAtzSw=="], + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ZPua6y7y7l/0PpMJhU1ZAt4jl0dC3F+EGlSzy9v0vqzyoixk0HRqsz9nxN7wmJo/5vHhHJBjsE5/sEYS9Z8tsQ=="], - "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sy02aSxd8UMd6XmiPFVl/Em0b78jdZcDSsLwg+bweJQQk0l+vJhOfqFiG11mbnpo+EBIZmRe6OH5LkxeSU36+w=="], + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ab9M5gCsMeYzOeBoHIOz+zyVSnEZowwV2jn3Am+x625ZNcqU0T3eNf+a7ppopvkQjrehfmO3y5HiMVAkSAs1Vw=="], - "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-W0Nlr/Cz2QTH9n4k5zNrk3LSsg1C4wHiJi8hrAiQVTaAV/N1XrKqd0DevqQuouuapG6pw/6B1xCgiNPebv9oyw=="], + "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-BaoUKmwnAbWssSwVHoA0HyJFX3m+Mp6xJhxD4YAu8H1mo8DNOWBG5J7DGXJRIiBTm6YjAXlerq8Pjfx4lycfYQ=="], - "lefthook-linux-x64": ["lefthook-linux-x64@2.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-N6ySVCtB/DrOZ1ZgPL8WBZTgtoVHvcPKI+LV5wbcGrvA/dzDZFvniadrbDWZg7Tm705efiQzyENjwhhqNkwiww=="], + "lefthook-linux-x64": ["lefthook-linux-x64@2.0.8", "", { "os": "linux", "cpu": "x64" }, "sha512-oNXcoGWsGy/U9gqE6PJpLtiNlGlAgoYtVmfc2gauNPRJehaQBaifD5/5aXPiWhRukUTQ1p9kuShFDpT2jOYn5Q=="], - "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-VmOhJO3pYzZ/1C2WFXtL/n5pq4/eYOroqJJpwTJfmCHyw4ceLACu8MDyU5AMJhGMkbL8mPxGInJKxg5xhYgGRw=="], + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.8", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-pxUgnilqsnDEWF7J5uNViHJ+Q4gSEQbRbrcIEdluBzjW34E20WK4UPk0bxZDQZAeaXTubNQEvyafmfY7dWe4Gg=="], - "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-U8MZz1xlHUdflkQQ2hkMQsei6fSZbs8tuE4EjCIHWnNdnAF4V8sZ6n1KbxsJcoZXPyBZqxZSMu1o/Ye8IAMVKg=="], + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.8", "", { "os": "openbsd", "cpu": "x64" }, "sha512-p50cpkWImLwU330JJuJaioNVT1X/Z56iqPOLEgBt2+1BlljmPe/eGrMArF4iIKfdZ4wFJ9f2h0gq+jyvQGFjSg=="], - "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-543H3y2JAwNdvwUQ6nlNBG7rdKgoOUgzAa6pYcl6EoqicCRrjRmGhkJu7vUudkkrD2Wjm7tr9hU9poP2g5fRFQ=="], + "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-dStshOCvmg9sQSUmWNiLMLv52HFTVxC9JE2HGxCiHcK5oqVZS2v9cCZdFdiDZ1Xldi3ozLi2y7/Xpzul8Oqv5Q=="], - "lefthook-windows-x64": ["lefthook-windows-x64@2.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UDEPK9RWKm60xsNOdS/DQOdFba0SFa4w3tpFMXK1AJzmRHhosoKrorXGhtTr6kcM0MGKOtYi8GHsm++ArZ9wvQ=="], + "lefthook-windows-x64": ["lefthook-windows-x64@2.0.8", "", { "os": "win32", "cpu": "x64" }, "sha512-2YgT6feliy6CCDwbkT3pg1ylKD1b9lj+O5NdLsrxvZGRmO6ftXleWB4xfWKGGY8FrzAD2Y3eEVDv5n3NvGHDzw=="], "openapi3-ts": ["openapi3-ts@4.5.0", "", { "dependencies": { "yaml": "^2.8.0" } }, "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ=="], From c69ebd87e5eec2fa16bfa5c1b8e9dc7ea9fc1267 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 01:15:24 +0000 Subject: [PATCH 30/47] Lock file maintenance (#250) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/bun.lock b/bun.lock index 4194e015..37ac6932 100644 --- a/bun.lock +++ b/bun.lock @@ -50,7 +50,7 @@ "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], - "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + "@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="], "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], @@ -64,31 +64,31 @@ "git-hooks-list": ["git-hooks-list@4.1.1", "", {}, "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA=="], - "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], + "hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "lefthook": ["lefthook@2.0.8", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.8", "lefthook-darwin-x64": "2.0.8", "lefthook-freebsd-arm64": "2.0.8", "lefthook-freebsd-x64": "2.0.8", "lefthook-linux-arm64": "2.0.8", "lefthook-linux-x64": "2.0.8", "lefthook-openbsd-arm64": "2.0.8", "lefthook-openbsd-x64": "2.0.8", "lefthook-windows-arm64": "2.0.8", "lefthook-windows-x64": "2.0.8" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-FozDCKeSI+m3BP0cvyPgHch+yf7ClS3hDy1JsRUrbNmlyjqBcmlygnRXsZzpH+wHoNnF2fmfhJhkx/7S7IpaVw=="], + "lefthook": ["lefthook@2.0.11", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.11", "lefthook-darwin-x64": "2.0.11", "lefthook-freebsd-arm64": "2.0.11", "lefthook-freebsd-x64": "2.0.11", "lefthook-linux-arm64": "2.0.11", "lefthook-linux-x64": "2.0.11", "lefthook-openbsd-arm64": "2.0.11", "lefthook-openbsd-x64": "2.0.11", "lefthook-windows-arm64": "2.0.11", "lefthook-windows-x64": "2.0.11" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-/91k4dt9MRNkzeSr1iMjNi/z8dNuh+XvNfXrWA6PV+M1ZxiNY6uN6bGnr13n+j7N89f4h7YWBhCqhzhK33M5cA=="], - "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nu52qmqhSP+DKKuKYKDkMkPbgvgTZv+ueEo1LVXidTcgxEwvrbe2balcdqdulQTsPfYtm3pCPvv8ikalHrH+Qg=="], + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RfpdcJJQXstdgDiIBDRffncayKiXx+0LyMUCunIxDEO2JMXPpYK2hIdpUU0rkitzptAADchG7u1OXJ31rrtIAA=="], - "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-EGNBw1vuXzphs/KyDchkglwnYNkKQH3EpptIPXcQCRC3WKiz87PSrwkOxjGtgDg6nLYWru3YUzgcFrIGUXjWPw=="], + "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-D013UNKQa4FKgpxDMqdaU109U2/Pidtrt9CobQoq8te4eGUglcwxMzuYVTgaYnenz0FgKxSfVaCZsZgwqeMWqA=="], - "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ZPua6y7y7l/0PpMJhU1ZAt4jl0dC3F+EGlSzy9v0vqzyoixk0HRqsz9nxN7wmJo/5vHhHJBjsE5/sEYS9Z8tsQ=="], + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mgfNqG1tiJkCuGNwPG0LEfnAHGJA+Qzl6KidOtX/Zhxmj/sM+6hxiP4LOeEAhCnaZF5kuPtQgbFzShFHc2BK6A=="], - "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ab9M5gCsMeYzOeBoHIOz+zyVSnEZowwV2jn3Am+x625ZNcqU0T3eNf+a7ppopvkQjrehfmO3y5HiMVAkSAs1Vw=="], + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-rnHOlQbJfLGCibr7yHM44kPNgf/tFpEbj/cWVHRhjRdbgYSCAjJk0uKd/EVo3v/vjfId2na0AhWbLvO/aY3wQQ=="], - "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-BaoUKmwnAbWssSwVHoA0HyJFX3m+Mp6xJhxD4YAu8H1mo8DNOWBG5J7DGXJRIiBTm6YjAXlerq8Pjfx4lycfYQ=="], + "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-1XjDo2/4fM0TbJBwxZh8w+WMOFueg9oYHkryM8vc3vp8wTajdWBazg1K37JIS3FUco3tcOs+eWHQg0ekVjpWoA=="], - "lefthook-linux-x64": ["lefthook-linux-x64@2.0.8", "", { "os": "linux", "cpu": "x64" }, "sha512-oNXcoGWsGy/U9gqE6PJpLtiNlGlAgoYtVmfc2gauNPRJehaQBaifD5/5aXPiWhRukUTQ1p9kuShFDpT2jOYn5Q=="], + "lefthook-linux-x64": ["lefthook-linux-x64@2.0.11", "", { "os": "linux", "cpu": "x64" }, "sha512-OKOcfEvozXhO7+y2xgUzvc2kkqfhluql/sjQSzd8Ka+iK3hM4KCfbfgYx9q61Pjr34a0+i03cuH5DF2dlq/rrg=="], - "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.8", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-pxUgnilqsnDEWF7J5uNViHJ+Q4gSEQbRbrcIEdluBzjW34E20WK4UPk0bxZDQZAeaXTubNQEvyafmfY7dWe4Gg=="], + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-n1KEx196M3SKaWVNTQXGgxzBsiYAsdAy6Of6I6TAZwPhG7yoRrKGkQrhOlPgMzYl36udG1Lk4D+mfY9T0oOUYQ=="], - "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.8", "", { "os": "openbsd", "cpu": "x64" }, "sha512-p50cpkWImLwU330JJuJaioNVT1X/Z56iqPOLEgBt2+1BlljmPe/eGrMArF4iIKfdZ4wFJ9f2h0gq+jyvQGFjSg=="], + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-WAEtKpYUVvuJMVLA38IBoaPnTNSiaEzvUYxjTBlYTLHJwn7HC2GG6P1cnvoua8rfxb9/Bfi7C3D3IPa9VmB33Q=="], - "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-dStshOCvmg9sQSUmWNiLMLv52HFTVxC9JE2HGxCiHcK5oqVZS2v9cCZdFdiDZ1Xldi3ozLi2y7/Xpzul8Oqv5Q=="], + "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-HBqW1qfAnmmbpet7gSWatB6H5YIFdGxCqzolMCLwY/0o8oPFiMwdNE5RGp5JMmhZdz/h3XlbaUlIhnxoW8dk5g=="], - "lefthook-windows-x64": ["lefthook-windows-x64@2.0.8", "", { "os": "win32", "cpu": "x64" }, "sha512-2YgT6feliy6CCDwbkT3pg1ylKD1b9lj+O5NdLsrxvZGRmO6ftXleWB4xfWKGGY8FrzAD2Y3eEVDv5n3NvGHDzw=="], + "lefthook-windows-x64": ["lefthook-windows-x64@2.0.11", "", { "os": "win32", "cpu": "x64" }, "sha512-e5TYmV5cBZfRrhPVFCqjauegLI5CjdAd8exyAbMzGHkiwp3ZK145Su/pntgEP3d+ayS9mpgYPJmXYOSL7WHlyg=="], "openapi3-ts": ["openapi3-ts@4.5.0", "", { "dependencies": { "yaml": "^2.8.0" } }, "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ=="], @@ -98,7 +98,7 @@ "sort-object-keys": ["sort-object-keys@2.0.1", "", {}, "sha512-R89fO+z3x7hiKPXX5P0qim+ge6Y60AjtlW+QQpRozrrNcR1lw9Pkpm5MLB56HoNvdcLHL4wbpq16OcvGpEDJIg=="], - "sort-package-json": ["sort-package-json@3.5.0", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.1", "git-hooks-list": "^4.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.7.1", "sort-object-keys": "^2.0.0", "tinyglobby": "^0.2.12" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-moY4UtptUuP5sPuu9H9dp8xHNel7eP5/Kz/7+90jTvC0IOiPH2LigtRM/aSFSxreaWoToHUVUpEV4a2tAs2oKQ=="], + "sort-package-json": ["sort-package-json@3.6.0", "", { "dependencies": { "detect-indent": "^7.0.2", "detect-newline": "^4.0.1", "git-hooks-list": "^4.1.1", "is-plain-obj": "^4.1.0", "semver": "^7.7.3", "sort-object-keys": "^2.0.1", "tinyglobby": "^0.2.15" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-fyJsPLhWvY7u2KsKPZn1PixbXp+1m7V8NWqU8CvgFRbMEX41Ffw1kD8n0CfJiGoaSfoAvbrqRRl/DcHO8omQOQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], From 498ae7f050dbe286533c9775248798b8a081be56 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 00:43:48 +0000 Subject: [PATCH 31/47] Lock file maintenance (#252) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/bun.lock b/bun.lock index 37ac6932..1dbbf6b5 100644 --- a/bun.lock +++ b/bun.lock @@ -46,13 +46,13 @@ "@hono/zod-openapi": ["@hono/zod-openapi@0.19.10", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@hono/zod-validator": "^0.7.1", "openapi3-ts": "^4.5.0" }, "peerDependencies": { "hono": ">=4.3.6", "zod": ">=3.0.0" } }, "sha512-dpoS6DenvoJyvxtQ7Kd633FRZ/Qf74+4+o9s+zZI8pEqnbjdF/DtxIib08WDpCaWabMEJOL5TXpMgNEZvb7hpA=="], - "@hono/zod-validator": ["@hono/zod-validator@0.7.5", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-n4l4hutkfYU07PzRUHBOVzUEn38VSfrh+UVE5d0w4lyfWDOEhzxIupqo5iakRiJL44c3vTuFJBvcmUl8b9agIA=="], + "@hono/zod-validator": ["@hono/zod-validator@0.7.6", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw=="], - "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], - "@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="], + "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], - "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], "detect-indent": ["detect-indent@7.0.2", "", {}, "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A=="], @@ -68,27 +68,27 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "lefthook": ["lefthook@2.0.11", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.11", "lefthook-darwin-x64": "2.0.11", "lefthook-freebsd-arm64": "2.0.11", "lefthook-freebsd-x64": "2.0.11", "lefthook-linux-arm64": "2.0.11", "lefthook-linux-x64": "2.0.11", "lefthook-openbsd-arm64": "2.0.11", "lefthook-openbsd-x64": "2.0.11", "lefthook-windows-arm64": "2.0.11", "lefthook-windows-x64": "2.0.11" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-/91k4dt9MRNkzeSr1iMjNi/z8dNuh+XvNfXrWA6PV+M1ZxiNY6uN6bGnr13n+j7N89f4h7YWBhCqhzhK33M5cA=="], + "lefthook": ["lefthook@2.0.12", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.12", "lefthook-darwin-x64": "2.0.12", "lefthook-freebsd-arm64": "2.0.12", "lefthook-freebsd-x64": "2.0.12", "lefthook-linux-arm64": "2.0.12", "lefthook-linux-x64": "2.0.12", "lefthook-openbsd-arm64": "2.0.12", "lefthook-openbsd-x64": "2.0.12", "lefthook-windows-arm64": "2.0.12", "lefthook-windows-x64": "2.0.12" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-I2FdA9cdnq1icwlNz4RADs7exuqe47q1N9+p2LmcP/WfchWh16mvTB82OAD7w7zK9GxblS9GpF7pASaOSl4c7A=="], - "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RfpdcJJQXstdgDiIBDRffncayKiXx+0LyMUCunIxDEO2JMXPpYK2hIdpUU0rkitzptAADchG7u1OXJ31rrtIAA=="], + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tuBz1sNLien+nKKb8BDopKjS6EnbXU8rQzhMVBY+bnVfsTiYDfbBr4wo/IzA5TcwoTL/b5somCJhljEw6DvSyg=="], - "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-D013UNKQa4FKgpxDMqdaU109U2/Pidtrt9CobQoq8te4eGUglcwxMzuYVTgaYnenz0FgKxSfVaCZsZgwqeMWqA=="], + "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-FnuUMPPRMJyTEPXg6PotSrFJ8qf8FDLhhD1zLh74D+9Cye5j9n3lcrCQEjXubPT8du/GZLxMBjjffRbcZ8eYDA=="], - "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mgfNqG1tiJkCuGNwPG0LEfnAHGJA+Qzl6KidOtX/Zhxmj/sM+6hxiP4LOeEAhCnaZF5kuPtQgbFzShFHc2BK6A=="], + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DXElB0qR5e6a8cXkFNYakhwCieypbfh6Y4QG39pzMnLsG03g/nhe093o6owfiUZ4mUFyDM6+0xmy0steOooF2g=="], - "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-rnHOlQbJfLGCibr7yHM44kPNgf/tFpEbj/cWVHRhjRdbgYSCAjJk0uKd/EVo3v/vjfId2na0AhWbLvO/aY3wQQ=="], + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iJN1ZxFeaDi4Fi3b9jcW9wgyNl19LOv2NaVOaAi/tG6mlIn196cmSdXkOA3+943ZbqbdfV9I+bBcIKwneXDA3Q=="], - "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-1XjDo2/4fM0TbJBwxZh8w+WMOFueg9oYHkryM8vc3vp8wTajdWBazg1K37JIS3FUco3tcOs+eWHQg0ekVjpWoA=="], + "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-byvmO4Iri6P0COwM8c3lGgeCV3Q0hh1XJpRfrcZDr4Wslq9O63t6J3T6i87oOtY+UjC9pXLl6xGk6hlUcHZ3BQ=="], - "lefthook-linux-x64": ["lefthook-linux-x64@2.0.11", "", { "os": "linux", "cpu": "x64" }, "sha512-OKOcfEvozXhO7+y2xgUzvc2kkqfhluql/sjQSzd8Ka+iK3hM4KCfbfgYx9q61Pjr34a0+i03cuH5DF2dlq/rrg=="], + "lefthook-linux-x64": ["lefthook-linux-x64@2.0.12", "", { "os": "linux", "cpu": "x64" }, "sha512-KBaiinmf336rA+/dmYs7H7TTeAOByB0CyLA7k8IecTCuaiuKr6ez7ktSjht19poa5G+V0mts4GgEGcx6HViR0w=="], - "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-n1KEx196M3SKaWVNTQXGgxzBsiYAsdAy6Of6I6TAZwPhG7yoRrKGkQrhOlPgMzYl36udG1Lk4D+mfY9T0oOUYQ=="], + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-1QBMXX1UW5rtgC4TB52OKWB7Rz/kCBRB+bKKLT/gDD79aPzLgJANTitQQzgFNIWoa7aM9UvzvIAJzOo6FcFIbg=="], - "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-WAEtKpYUVvuJMVLA38IBoaPnTNSiaEzvUYxjTBlYTLHJwn7HC2GG6P1cnvoua8rfxb9/Bfi7C3D3IPa9VmB33Q=="], + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-zPcvUzs65GexRA37UHmaZqWuEGSU/zpBaPIY98MybXzzcJfCIf+O0oUQe2riMllwYGvNW0B1y3NOYRziDNe/vA=="], - "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-HBqW1qfAnmmbpet7gSWatB6H5YIFdGxCqzolMCLwY/0o8oPFiMwdNE5RGp5JMmhZdz/h3XlbaUlIhnxoW8dk5g=="], + "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-kgwxguS2GssoHM4SMTp+ArD/Gjg9q5MinD6iI5vSFpuJygD13ZWiXQQfESMHq9y/v1XkD0BdHTJej49dx8P+Vw=="], - "lefthook-windows-x64": ["lefthook-windows-x64@2.0.11", "", { "os": "win32", "cpu": "x64" }, "sha512-e5TYmV5cBZfRrhPVFCqjauegLI5CjdAd8exyAbMzGHkiwp3ZK145Su/pntgEP3d+ayS9mpgYPJmXYOSL7WHlyg=="], + "lefthook-windows-x64": ["lefthook-windows-x64@2.0.12", "", { "os": "win32", "cpu": "x64" }, "sha512-Tf/VtSOtF3rBTc9dzRWROa+HuhqaiIV+Xp+1gzlx5+uCueLM0m87Rz6yd4IN5mL7TrDaNkiRXI3FvjCp0dUE4Q=="], "openapi3-ts": ["openapi3-ts@4.5.0", "", { "dependencies": { "yaml": "^2.8.0" } }, "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ=="], @@ -108,6 +108,6 @@ "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - "zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], + "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], } } From fec85a81bdd9bdfff5980eae009845a2e6edd3c4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 27 Dec 2025 02:28:29 +0100 Subject: [PATCH 32/47] Update actions/attest-build-provenance action to v3.1.0 (#251) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index c462704e..08b63ee6 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -130,7 +130,7 @@ jobs: - if: inputs.artifact-action == 'build-release' name: Attest artifact - uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 + uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 with: subject-path: | dist/*.tar.xz @@ -229,7 +229,7 @@ jobs: - if: inputs.image-action == 'build-release' name: Attest image - uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 + uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 with: subject-name: "${{ env.REGISTRY }}/${{ steps.build-image.outputs.image }}" subject-digest: ${{ steps.push-image.outputs.digest }} From f889f303004245c607096588ee903f9ca8c2f192 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 27 Dec 2025 02:28:37 +0100 Subject: [PATCH 33/47] Update step-security/harden-runner action to v2.14.0 (#248) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 08b63ee6..3d252664 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: egress-policy: audit @@ -150,7 +150,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: egress-policy: audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19d6ec7e..5eff10e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: sha_short: ${{ steps.ctx.outputs.sha_short }} steps: - name: Harden Runner - uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 + uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: egress-policy: audit From 8e1b1d02da256ece32ee915e043284ac78b0cb6e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Dec 2025 00:46:47 +0000 Subject: [PATCH 34/47] Lock file maintenance (#253) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bun.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bun.lock b/bun.lock index 1dbbf6b5..e339f1ca 100644 --- a/bun.lock +++ b/bun.lock @@ -68,27 +68,27 @@ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "lefthook": ["lefthook@2.0.12", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.12", "lefthook-darwin-x64": "2.0.12", "lefthook-freebsd-arm64": "2.0.12", "lefthook-freebsd-x64": "2.0.12", "lefthook-linux-arm64": "2.0.12", "lefthook-linux-x64": "2.0.12", "lefthook-openbsd-arm64": "2.0.12", "lefthook-openbsd-x64": "2.0.12", "lefthook-windows-arm64": "2.0.12", "lefthook-windows-x64": "2.0.12" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-I2FdA9cdnq1icwlNz4RADs7exuqe47q1N9+p2LmcP/WfchWh16mvTB82OAD7w7zK9GxblS9GpF7pASaOSl4c7A=="], + "lefthook": ["lefthook@2.0.13", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.13", "lefthook-darwin-x64": "2.0.13", "lefthook-freebsd-arm64": "2.0.13", "lefthook-freebsd-x64": "2.0.13", "lefthook-linux-arm64": "2.0.13", "lefthook-linux-x64": "2.0.13", "lefthook-openbsd-arm64": "2.0.13", "lefthook-openbsd-x64": "2.0.13", "lefthook-windows-arm64": "2.0.13", "lefthook-windows-x64": "2.0.13" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-D39rCVl7/GpqakvhQvqz07SBpzUWTvWjXKnBZyIy8O6D+Lf9xD6tnbHtG5nWXd9iPvv1AKGQwL9R/e5rNtV6SQ=="], - "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tuBz1sNLien+nKKb8BDopKjS6EnbXU8rQzhMVBY+bnVfsTiYDfbBr4wo/IzA5TcwoTL/b5somCJhljEw6DvSyg=="], + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-KbQqpNSNTugjtPzt97CNcy/XZy5asJ0+uSLoHc4ML8UCJdsXKYJGozJHNwAd0Xfci/rQlj82A7rPOuTdh0jY0Q=="], - "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-FnuUMPPRMJyTEPXg6PotSrFJ8qf8FDLhhD1zLh74D+9Cye5j9n3lcrCQEjXubPT8du/GZLxMBjjffRbcZ8eYDA=="], + "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-s/vI6sEE8/+rE6CONZzs59LxyuSc/KdU+/3adkNx+Q13R1+p/AvQNeszg3LAHzXmF3NqlxYf8jbj/z5vBzEpRw=="], - "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DXElB0qR5e6a8cXkFNYakhwCieypbfh6Y4QG39pzMnLsG03g/nhe093o6owfiUZ4mUFyDM6+0xmy0steOooF2g=="], + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.13", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-iQeJTU7Zl8EJlCMQxNZQpJFAQ9xl40pydUIv5SYnbJ4nqIr9ONuvrioNv6N2LtKP5aBl1nIWQQ9vMjgVyb3k+A=="], - "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iJN1ZxFeaDi4Fi3b9jcW9wgyNl19LOv2NaVOaAi/tG6mlIn196cmSdXkOA3+943ZbqbdfV9I+bBcIKwneXDA3Q=="], + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-99cAXKRIzpq/u3obUXbOQJCHP+0ZkJbN3TF+1ZQZlRo3Y6+mPSCg9fh/oi6dgbtu4gTI5Ifz3o5p2KZzAIF9ZQ=="], - "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-byvmO4Iri6P0COwM8c3lGgeCV3Q0hh1XJpRfrcZDr4Wslq9O63t6J3T6i87oOtY+UjC9pXLl6xGk6hlUcHZ3BQ=="], + "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-RWarenY3kLy/DT4/8dY2bwDlYwlELRq9MIFq+FiMYmoBHES3ckWcLX2JMMlM49Y672paQc7MbneSrNUn/FQWhg=="], - "lefthook-linux-x64": ["lefthook-linux-x64@2.0.12", "", { "os": "linux", "cpu": "x64" }, "sha512-KBaiinmf336rA+/dmYs7H7TTeAOByB0CyLA7k8IecTCuaiuKr6ez7ktSjht19poa5G+V0mts4GgEGcx6HViR0w=="], + "lefthook-linux-x64": ["lefthook-linux-x64@2.0.13", "", { "os": "linux", "cpu": "x64" }, "sha512-QZRcxXGf8Uj/75ITBqoBh0zWhJE7+uFoRxEHwBq0Qjv55Q4KcFm7FBN/IFQCSd14reY5pmY3kDaWVVy60cAGJA=="], - "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-1QBMXX1UW5rtgC4TB52OKWB7Rz/kCBRB+bKKLT/gDD79aPzLgJANTitQQzgFNIWoa7aM9UvzvIAJzOo6FcFIbg=="], + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.13", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-LAuOWwnNmOlRE0RxKMOhIz5Kr9tXi0rCjzXtDARW9lvfAV6Br2wP+47q0rqQ8m/nVwBYoxfJ/RDunLbb86O1nA=="], - "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-zPcvUzs65GexRA37UHmaZqWuEGSU/zpBaPIY98MybXzzcJfCIf+O0oUQe2riMllwYGvNW0B1y3NOYRziDNe/vA=="], + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.13", "", { "os": "openbsd", "cpu": "x64" }, "sha512-n9TIN3QLncyxOHomiKKwzDFHKOCm5H28CVNAZFouKqDwEaUGCs5TJI88V85j4/CgmLVUU8uUn4ClVCxIWYG59w=="], - "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-kgwxguS2GssoHM4SMTp+ArD/Gjg9q5MinD6iI5vSFpuJygD13ZWiXQQfESMHq9y/v1XkD0BdHTJej49dx8P+Vw=="], + "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-sdSC4F9Di7y0t43Of9MOA5g/0CmvkM4juQ3sKfUhRcoygetLJn4PR2/pvuDOIaGf4mNMXBP5IrcKaeDON9HrcA=="], - "lefthook-windows-x64": ["lefthook-windows-x64@2.0.12", "", { "os": "win32", "cpu": "x64" }, "sha512-Tf/VtSOtF3rBTc9dzRWROa+HuhqaiIV+Xp+1gzlx5+uCueLM0m87Rz6yd4IN5mL7TrDaNkiRXI3FvjCp0dUE4Q=="], + "lefthook-windows-x64": ["lefthook-windows-x64@2.0.13", "", { "os": "win32", "cpu": "x64" }, "sha512-ccl1v7Fl10qYoghEtjXN+JC1x/y/zLM/NSHf3NFGeKEGBNd1P5d/j6w8zVmhfzi+ekS8whXrcNbRAkLdAqUrSw=="], "openapi3-ts": ["openapi3-ts@4.5.0", "", { "dependencies": { "yaml": "^2.8.0" } }, "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ=="], From ab17fcc046eb29b3de773aada1332c01d1d7b41d Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Sun, 4 Jan 2026 18:18:56 +0100 Subject: [PATCH 35/47] Database subsystem (#189) --- .dockerignore | 13 +- .editorconfig | 22 + .env.example | 67 ++- .github/renovate.json | 24 +- .github/workflows/cd.yml | 125 +++--- .github/workflows/ci.yml | 31 +- .gitignore | 39 +- .npmrc | 1 + .zed/settings.json | 53 +++ CONTRIBUTING.md | 90 +++- Dockerfile | 66 +-- README.md | 12 +- biome.json | 206 ++++++--- bun.lock | 113 ----- bunfig.toml | 9 - deno.json | 53 +++ deno.lock | 410 ++++++++++++++++++ lefthook.json | 13 - mise.toml | 209 +++++++++ package.json | 99 ++--- rolldown.config.ts | 31 ++ src/config.ts | 10 - src/database/database.ts | 127 ++++++ src/database/migrations.ts | 17 + src/database/migrations/0001.base.sql | 18 + src/database/query.ts | 133 ++++++ src/document/compression.ts | 15 +- src/document/crypto.ts | 22 - src/document/storage.ts | 64 ++- src/document/validator.ts | 58 --- src/endpoints/document/v1/delete.ts | 59 +++ src/endpoints/document/v1/get.ts | 116 +++++ src/endpoints/document/v1/index.ts | 13 + src/endpoints/document/v1/patch.ts | 121 ++++++ src/endpoints/document/v1/post.ts | 116 +++++ .../legacy/v2/documents/access.route.ts | 99 +++++ .../legacy/v2/documents/accessRaw.route.ts | 98 +++++ .../legacy/v2/documents/edit.route.ts | 80 ++++ .../legacy/v2/documents/exists.route.ts | 47 ++ src/endpoints/legacy/v2/documents/index.ts | 17 + .../legacy/v2/documents/publish.route.ts | 109 +++++ .../legacy/v2/documents/remove.route.ts | 58 +++ src/endpoints/user/v1/create.ts | 49 +++ src/endpoints/user/v1/index.ts | 7 + src/endpoints/v1/access.route.ts | 72 --- src/endpoints/v1/accessRaw.route.ts | 62 --- src/endpoints/v1/index.ts | 20 - src/endpoints/v1/publish.route.ts | 80 ---- src/endpoints/v1/remove.route.ts | 72 --- src/endpoints/v2/access.route.ts | 93 ---- src/endpoints/v2/accessRaw.route.ts | 84 ---- src/endpoints/v2/edit.route.ts | 94 ---- src/endpoints/v2/exists.route.ts | 63 --- src/endpoints/v2/index.ts | 24 - src/endpoints/v2/publish.route.ts | 144 ------ src/endpoints/v2/remove.route.ts | 71 --- src/global.ts | 89 ++++ src/http/middleware/authorization.ts | 27 ++ src/http/middleware/bodyCheck.ts | 50 +++ src/http/middleware/bodySize.ts | 50 +++ src/http/router.ts | 125 ++++++ src/http/server.ts | 43 ++ src/http/type.ts | 6 + src/index.ts | 21 + src/init.ts | 96 ++++ src/server.ts | 47 -- src/server/endpoints.ts | 13 - src/server/errorHandler.ts | 139 ------ src/server/middleware.ts | 15 - src/server/oas.ts | 31 -- src/task.ts | 42 ++ src/tasks/sweeper.ts | 95 ++++ src/types/Document.ts | 13 - src/types/ErrorHandler.ts | 32 -- src/types/Range.ts | 9 - src/utils/StringUtils.ts | 38 -- src/utils/ValidatorUtils.ts | 31 -- src/utils/btree.ts | 4 + src/utils/colors.ts | 22 - src/utils/console.test.ts | 24 + src/utils/console.ts | 86 ++++ src/utils/document.ts | 34 ++ src/utils/env.ts | 9 - src/utils/error.ts | 128 ++++++ src/utils/humanize.test.ts | 62 +++ src/utils/humanize.ts | 49 +++ src/utils/ipq.ts | 80 ++++ src/utils/logger.ts | 41 -- src/utils/validator/document.ts | 67 +++ src/utils/validator/handler.ts | 8 + src/utils/validator/user.ts | 33 ++ tsconfig.json | 47 -- 92 files changed, 3816 insertions(+), 1908 deletions(-) create mode 100644 .editorconfig create mode 100644 .npmrc create mode 100644 .zed/settings.json delete mode 100644 bun.lock delete mode 100644 bunfig.toml create mode 100644 deno.json create mode 100644 deno.lock delete mode 100644 lefthook.json create mode 100644 mise.toml create mode 100644 rolldown.config.ts delete mode 100644 src/config.ts create mode 100644 src/database/database.ts create mode 100644 src/database/migrations.ts create mode 100644 src/database/migrations/0001.base.sql create mode 100644 src/database/query.ts delete mode 100644 src/document/crypto.ts delete mode 100644 src/document/validator.ts create mode 100644 src/endpoints/document/v1/delete.ts create mode 100644 src/endpoints/document/v1/get.ts create mode 100644 src/endpoints/document/v1/index.ts create mode 100644 src/endpoints/document/v1/patch.ts create mode 100644 src/endpoints/document/v1/post.ts create mode 100644 src/endpoints/legacy/v2/documents/access.route.ts create mode 100644 src/endpoints/legacy/v2/documents/accessRaw.route.ts create mode 100644 src/endpoints/legacy/v2/documents/edit.route.ts create mode 100644 src/endpoints/legacy/v2/documents/exists.route.ts create mode 100644 src/endpoints/legacy/v2/documents/index.ts create mode 100644 src/endpoints/legacy/v2/documents/publish.route.ts create mode 100644 src/endpoints/legacy/v2/documents/remove.route.ts create mode 100644 src/endpoints/user/v1/create.ts create mode 100644 src/endpoints/user/v1/index.ts delete mode 100644 src/endpoints/v1/access.route.ts delete mode 100644 src/endpoints/v1/accessRaw.route.ts delete mode 100644 src/endpoints/v1/index.ts delete mode 100644 src/endpoints/v1/publish.route.ts delete mode 100644 src/endpoints/v1/remove.route.ts delete mode 100644 src/endpoints/v2/access.route.ts delete mode 100644 src/endpoints/v2/accessRaw.route.ts delete mode 100644 src/endpoints/v2/edit.route.ts delete mode 100644 src/endpoints/v2/exists.route.ts delete mode 100644 src/endpoints/v2/index.ts delete mode 100644 src/endpoints/v2/publish.route.ts delete mode 100644 src/endpoints/v2/remove.route.ts create mode 100644 src/global.ts create mode 100644 src/http/middleware/authorization.ts create mode 100644 src/http/middleware/bodyCheck.ts create mode 100644 src/http/middleware/bodySize.ts create mode 100644 src/http/router.ts create mode 100644 src/http/server.ts create mode 100644 src/http/type.ts create mode 100644 src/index.ts create mode 100644 src/init.ts delete mode 100644 src/server.ts delete mode 100644 src/server/endpoints.ts delete mode 100644 src/server/errorHandler.ts delete mode 100644 src/server/middleware.ts delete mode 100644 src/server/oas.ts create mode 100644 src/task.ts create mode 100644 src/tasks/sweeper.ts delete mode 100644 src/types/Document.ts delete mode 100644 src/types/ErrorHandler.ts delete mode 100644 src/types/Range.ts delete mode 100644 src/utils/StringUtils.ts delete mode 100644 src/utils/ValidatorUtils.ts create mode 100644 src/utils/btree.ts delete mode 100644 src/utils/colors.ts create mode 100644 src/utils/console.test.ts create mode 100644 src/utils/console.ts create mode 100644 src/utils/document.ts delete mode 100644 src/utils/env.ts create mode 100644 src/utils/error.ts create mode 100644 src/utils/humanize.test.ts create mode 100644 src/utils/humanize.ts create mode 100644 src/utils/ipq.ts delete mode 100644 src/utils/logger.ts create mode 100644 src/utils/validator/document.ts create mode 100644 src/utils/validator/handler.ts create mode 100644 src/utils/validator/user.ts delete mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore index 03faf034..62d0536e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,11 +1,10 @@ * -# Project files -!/bun.lock -!/bunfig.toml +!/src/** +!/.npmrc +!/deno.json +!/deno.lock !/LICENSE +!/mise.toml !/package.json -!/tsconfig.json - -# SRC -!/src/** \ No newline at end of file +!/rolldown.config.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..1d10a882 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# https://spec.editorconfig.org/#supported-pairs + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 2 +trim_trailing_whitespace = true + +[{*.json,*.jsonc}] +insert_final_newline = false + +[{*.yaml,*.yml}] +insert_final_newline = false + +[*.html] +insert_final_newline = false diff --git a/.env.example b/.env.example index 7ca898c3..8b4dbf4f 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ -#? Rename or copy this file to ".env" and set the variables there. +#? +#? Rename this file to ".env" and edit the values as needed. #? #?#################### #? VARIABLE STRUCTURE: @@ -6,35 +7,61 @@ #? [ default ] : type < min - max > #? ^ ^ ^ #? | | | -#? | | +---- RANGE between two values (these included) +#? | | +---- RANGE between two values (inclusive) #? | +-------------- TYPE of the variable -#? +------------------------ DEFAULT value applied if not set +#? +------------------------ DEFAULT value if not set #? #?################### #? COMMENT STRUCTURE: #?################### -#? "#?#..." or "###..." are used to comment a section line. -#? "#?" is used to comment a help line. -#? "##" is used to comment a description line. -#? "#" is used to comment a variable line. +#? "#?#...", "###..." for section headers +#? "#?" for help +#? "##" for description +#? "#" for variable definitions #? -#? You should remove the comment on variable lines only if you want to set the variable. -########## -## SERVER: -########## -## Set log verbosity [3]:integer -#? (0=none <- 1=error <- 2=warn <- 3=info <- 4=debug) -#LOGLEVEL=3 +## Log level: [3]:integer<0-4> +#? 0=none, 1=error, 2=warn, 3=info, 4=debug +#JSPB_LOG_VERBOSITY=3 + +## Include timestamps in logs?: [true]:boolean +#JSPB_LOG_TIME=true -## Port for the server [4000]:integer -#PORT=4000 +## Hostname to bind: [::]:string +#JSPB_HOSTNAME=:: -## Is website served over HTTPS? [true]:boolean -#TLS=true +## Port to bind: [4000]:integer<0-65535> +#JSPB_PORT=4000 ############ ## DOCUMENT: ############ -## Maximum document size in kilobytes [1024]:integer -#DOCUMENT_MAXSIZE=1024 \ No newline at end of file +## Maximum size per document: [1mb]:string +#? 0=disabled, units: b/k(i)b/m(i)b/g(i)b/t(i)b +#JSPB_DOCUMENT_SIZE=1mb + +## Delete documents older than: [0]:string +#? 0=disabled, units: s/m/h/d/w/M/y +#JSPB_DOCUMENT_AGE=0 + +## Delete anonymous documents older than: [7d]:string +#? 0=disabled, units: s/m/h/d/w/M/y +#JSPB_DOCUMENT_ANONYMOUS_AGE=7d + +######## +## USER: +######## +## Allow user registration: [true]:boolean +#? Root user can always create new users. +#JSPB_USER_REGISTER=true + +## Authentication token for root user: []:string +#? Will overwrite existing root user token on first run. +#JSPB_USER_ROOT_TOKEN= + +######## +## TASK: +######## +## Cleanup task cron schedule: [0 1 * * *]:string +#? https://crontab.guru/#0_1_*_*_* +#JSPB_TASK_SWEEPER=0 1 * * * diff --git a/.github/renovate.json b/.github/renovate.json index ba56533d..95ed7709 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,14 +1,14 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended"], - "lockFileMaintenance": { - "enabled": true, - "automerge": true - }, - "packageRules": [ - { - "matchUpdateTypes": ["patch"], - "automerge": true - } - ] + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended", "customManagers:biomeVersions"], + "lockFileMaintenance": { + "enabled": true, + "automerge": true + }, + "packageRules": [ + { + "matchUpdateTypes": ["patch"], + "automerge": true + } + ] } diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 3d252664..47e5babc 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -23,7 +23,7 @@ on: - build-release concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }} cancel-in-progress: false permissions: @@ -43,16 +43,16 @@ jobs: - name: Harden Runner uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: - egress-policy: audit + egress-policy: "audit" - - name: Setup Bun - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + - name: Setup mise-en-place + uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1 - name: Save context id: ctx env: - CTX_BRANCH: ${{ github.head_ref || github.ref_name }} - CTX_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CTX_BRANCH: "${{ github.head_ref || github.ref_name }}" + CTX_SHA: "${{ github.event.pull_request.head.sha || github.sha }}" run: | echo "branch=${CTX_BRANCH}" >>"$GITHUB_OUTPUT" echo "sha=${CTX_SHA}" >>"$GITHUB_OUTPUT" @@ -61,8 +61,8 @@ jobs: - name: Save tags id: tags env: - BRANCH: ${{ steps.ctx.outputs.branch }} - SHA_SHORT: ${{ steps.ctx.outputs.sha_short }} + BRANCH: "${{ steps.ctx.outputs.branch }}" + SHA_SHORT: "${{ steps.ctx.outputs.sha_short }}" run: | TIMESTAMP="$(date +%Y.%m.%d)" @@ -78,55 +78,40 @@ jobs: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - persist-credentials: false - - - name: Install deps - run: bun install --frozen-lockfile + persist-credentials: "false" - name: Build artifact run: | - bun run build:server - - bun run build:standalone:darwin-arm64 - chmod 755 ./dist/server - tar -c --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ server | xz -z -6 >./dist/backend_${{ steps.tags.outputs.tag }}_darwin-arm64.tar.xz - tar -tJf ./dist/backend_${{ steps.tags.outputs.tag }}_darwin-arm64.tar.xz >/dev/null - - bun run build:standalone:linux-amd64-glibc - chmod 755 ./dist/server - tar -c --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ server | xz -z -6 >./dist/backend_${{ steps.tags.outputs.tag }}_linux-amd64-glibc.tar.xz - tar -tJf ./dist/backend_${{ steps.tags.outputs.tag }}_linux-amd64-glibc.tar.xz >/dev/null - - bun run build:standalone:linux-amd64-musl - chmod 755 ./dist/server - tar -c --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ server | xz -z -6 >./dist/backend_${{ steps.tags.outputs.tag }}_linux-amd64-musl.tar.xz - tar -tJf ./dist/backend_${{ steps.tags.outputs.tag }}_linux-amd64-musl.tar.xz >/dev/null - - bun run build:standalone:linux-arm64-glibc - chmod 755 ./dist/server - tar -c --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ server | xz -z -6 >./dist/backend_${{ steps.tags.outputs.tag }}_linux-arm64-glibc.tar.xz - tar -tJf ./dist/backend_${{ steps.tags.outputs.tag }}_linux-arm64-glibc.tar.xz >/dev/null - - bun run build:standalone:linux-arm64-musl - chmod 755 ./dist/server - tar -c --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ server | xz -z -6 >./dist/backend_${{ steps.tags.outputs.tag }}_linux-arm64-musl.tar.xz - tar -tJf ./dist/backend_${{ steps.tags.outputs.tag }}_linux-arm64-musl.tar.xz >/dev/null - - bun run build:standalone:windows-amd64 - chmod 755 ./dist/server.exe - zip -j -X -9 -l -o ./dist/backend_${{ steps.tags.outputs.tag }}_windows-amd64.zip .env.example LICENSE README.md ./dist/server.exe - zip -T ./dist/backend_${{ steps.tags.outputs.tag }}_windows-amd64.zip + mise run build:standalone:darwin-arm64 + chmod 755 ./dist/backend.darwin-arm64 + tar -c --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend.darwin-arm64 | xz -z -6 >./dist/backend-${{ steps.tags.outputs.tag }}_darwin-arm64.tar.xz + tar -tJf ./dist/backend-${{ steps.tags.outputs.tag }}_darwin-arm64.tar.xz >/dev/null + + mise run build:standalone:linux-amd64 + chmod 755 ./dist/backend.linux-amd64 + tar -c --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend.linux-amd64 | xz -z -6 >./dist/backend-${{ steps.tags.outputs.tag }}_linux-amd64.tar.xz + tar -tJf ./dist/backend-${{ steps.tags.outputs.tag }}_linux-amd64.tar.xz >/dev/null + + mise run build:standalone:linux-arm64 + chmod 755 ./dist/backend.linux-arm64 + tar -c --owner=0 --group=0 --mtime='now' --utc .env.example LICENSE README.md -C ./dist/ backend.linux-arm64 | xz -z -6 >./dist/backend-${{ steps.tags.outputs.tag }}_linux-arm64.tar.xz + tar -tJf ./dist/backend-${{ steps.tags.outputs.tag }}_linux-arm64.tar.xz >/dev/null + + mise run build:standalone:windows-amd64 + chmod 755 ./dist/backend.windows-amd64.exe + zip -j -X -9 -l -o ./dist/backend-${{ steps.tags.outputs.tag }}_windows-amd64.zip .env.example LICENSE README.md ./dist/backend.windows-amd64.exe + zip -T ./dist/backend-${{ steps.tags.outputs.tag }}_windows-amd64.zip - if: inputs.artifact-action == 'build-release' name: Release artifact uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 with: - name: ${{ steps.tags.outputs.extended }} - tag: ${{ steps.tags.outputs.extended }} - artifacts: dist/*.tar.xz,dist/*.zip - makeLatest: true - prerelease: ${{ steps.ctx.outputs.branch != 'stable' }} - generateReleaseNotes: ${{ steps.ctx.outputs.branch == 'stable' }} + name: "${{ steps.tags.outputs.extended }}" + tag: "${{ steps.tags.outputs.extended }}" + artifacts: "dist/*.tar.xz,dist/*.zip" + makeLatest: "true" + prerelease: "${{ steps.ctx.outputs.branch != 'stable' }}" + generateReleaseNotes: "${{ steps.ctx.outputs.branch == 'stable' }}" - if: inputs.artifact-action == 'build-release' name: Attest artifact @@ -152,13 +137,13 @@ jobs: - name: Harden Runner uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: - egress-policy: audit + egress-policy: "audit" - name: Save context id: ctx env: - CTX_BRANCH: ${{ github.head_ref || github.ref_name }} - CTX_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CTX_BRANCH: "${{ github.head_ref || github.ref_name }}" + CTX_SHA: "${{ github.event.pull_request.head.sha || github.sha }}" run: | echo "branch=${CTX_BRANCH}" >>"$GITHUB_OUTPUT" echo "sha=${CTX_SHA}" >>"$GITHUB_OUTPUT" @@ -167,9 +152,9 @@ jobs: - name: Save tags id: tags env: - BRANCH: ${{ steps.ctx.outputs.branch }} - SHA: ${{ steps.ctx.outputs.sha }} - SHA_SHORT: ${{ steps.ctx.outputs.sha_short }} + BRANCH: "${{ steps.ctx.outputs.branch }}" + SHA: "${{ steps.ctx.outputs.sha }}" + SHA_SHORT: "${{ steps.ctx.outputs.sha_short }}" run: | TIMESTAMP="$(date +%Y.%m.%d)" TIMESTAMP_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)" @@ -191,18 +176,18 @@ jobs: - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - persist-credentials: false + persist-credentials: "false" - name: Build image id: build-image uses: redhat-actions/buildah-build@7a95fa7ee0f02d552a32753e7414641a04307056 # v2.13 with: - containerfiles: Dockerfile - platforms: linux/amd64,linux/arm64 - image: ${{ github.repository }} - layers: true - oci: true - tags: ${{ steps.tags.outputs.list }} + containerfiles: "Dockerfile" + platforms: "linux/amd64,linux/arm64" + image: "${{ github.repository }}" + layers: "true" + oci: "true" + tags: "${{ steps.tags.outputs.list }}" extra-args: | --squash --identity-label=false @@ -214,23 +199,23 @@ jobs: name: Login to GHCR uses: redhat-actions/podman-login@4934294ad0449894bcd1e9f191899d7292469603 # v1.7 with: - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: ${{ env.REGISTRY }} + username: "${{ github.repository_owner }}" + password: "${{ secrets.GITHUB_TOKEN }}" + registry: "${{ env.REGISTRY }}" - if: inputs.image-action == 'build-release' name: Push to GHCR id: push-image uses: redhat-actions/push-to-registry@5ed88d269cf581ea9ef6dd6806d01562096bee9c # v2.8 with: - image: ${{ steps.build-image.outputs.image }} - tags: ${{ steps.build-image.outputs.tags }} - registry: ${{ env.REGISTRY }} + image: "${{ steps.build-image.outputs.image }}" + tags: "${{ steps.build-image.outputs.tags }}" + registry: "${{ env.REGISTRY }}" - if: inputs.image-action == 'build-release' name: Attest image uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0 with: subject-name: "${{ env.REGISTRY }}/${{ steps.build-image.outputs.image }}" - subject-digest: ${{ steps.push-image.outputs.digest }} - push-to-registry: false + subject-digest: "${{ steps.push-image.outputs.digest }}" + push-to-registry: "false" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5eff10e0..7b91f662 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: - '*.md' concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }} cancel-in-progress: false permissions: @@ -23,42 +23,43 @@ jobs: branch: ${{ steps.ctx.outputs.branch }} sha: ${{ steps.ctx.outputs.sha }} sha_short: ${{ steps.ctx.outputs.sha_short }} + steps: - name: Harden Runner uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: - egress-policy: audit + egress-policy: "audit" - name: Save context information id: ctx env: - CTX_BRANCH: ${{ github.head_ref || github.ref_name }} - CTX_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CTX_BRANCH: "${{ github.head_ref || github.ref_name }}" + CTX_SHA: "${{ github.event.pull_request.head.sha || github.sha }}" run: | echo "branch=${CTX_BRANCH}" >>"$GITHUB_OUTPUT" echo "sha=${CTX_SHA}" >>"$GITHUB_OUTPUT" echo "sha_short=${CTX_SHA::7}" >>"$GITHUB_OUTPUT" - - name: Setup Bun - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - - name: Checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: - persist-credentials: false + persist-credentials: "false" - - name: Install deps - run: bun install --frozen-lockfile + - name: Setup mise-en-place + uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1 - name: Run lint - run: bun run lint + run: mise run lint + + - name: Run tests + run: mise run test - name: Build server - run: bun run build:server + run: mise run build - - name: Test run server + - name: Run server run: | - bun run start:server & + mise run start:server & SERVER_PID=$! - sleep 10 + sleep 5 kill $SERVER_PID diff --git a/.gitignore b/.gitignore index 931c7046..8abd6b55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,25 @@ * -# Project files +!/.github/ +!/.github/renovate.json +!/.github/workflows/*.yml +!/.zed/ +!/.zed/settings.json +!/src/ +!/src/** +!/.dockerignore +!/.editorconfig !/.env.example +!/.gitattributes +!/.gitignore +!/.npmrc !/biome.json -!/bun.lock -!/bunfig.toml !/CONTRIBUTING.md -!/lefthook.json +!/deno.json +!/deno.lock +!/Dockerfile !/LICENSE +!/mise.toml !/package.json !/README.md -!/tsconfig.json - -# SRC -!/src/ -!/src/** - -# GIT -!/.gitattributes -!/.gitignore - -# GitHub -!/.github/ -!/.github/** -!/.github/workflows/*.yml - -# Docker -!/.dockerignore -!/Dockerfile \ No newline at end of file +!/rolldown.config.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..41583e36 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000..f87bbd2e --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,53 @@ +// -*- mode: jsonc -*- + +{ + // lsp + "lsp": { + "deno": { + "settings": { + "deno": { + "enable": true + } + } + }, + "biome": { + "settings": { + "require_config_file": true, + "config_path": "./biome.json" + } + }, + "yaml-language-server": { + "settings": { + "yaml": { + "keyOrdering": false, + "format": { + "singleQuote": false + } + } + } + } + }, + + // language specific (overrides global) + "languages": { + "JavaScript": { + "formatter": "language_server", + "language_servers": ["biome", "deno"] + }, + "TypeScript": { + "formatter": "language_server", + "language_servers": ["biome", "deno"] + }, + "JSON": { + "language_servers": ["biome", "json-language-server"], + "formatter": "language_server" + }, + "JSONC": { + "language_servers": ["biome", "json-language-server"], + "formatter": "language_server" + }, + "YAML": { + "format_on_save": "off" + } + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4969752..77d0484c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,25 +1,97 @@ -### API +## Dependencies + +Everything is managed by..: + +- [mise-en-place](https://mise.jdx.dev/installing-mise.html) + +After install you need to trust the project..: + +```shell +mise trust +``` + +## Scripts + +The project uses `mise` to manage scripts. To list all available scripts..: + +```shell +mise run +``` + +Scripts are grouped, meaning that a script such as `mise run build` will run +other scripts under its name to fulfil its function, in this case building the +backend and compiling the server. + +This may not be desired in every case, so it is recommended that scripts be run +in a more granular way..: + +```shell +# Build and start +mise run build:server start:server + +# Better, we can run the development server +mise run start:dev +``` + +All scripts will run from any location within the project as if you were in the +main directory, no fear. + +## Build + +Building the Backend is very straightforward..: + +```shell +mise run build +``` + +It will prepare the server bundle ready to be run in `dist/backend.js`. + +You can also build a standalone binary for different platforms..: + +```shell +# Build the server bundle +mise run build:server + +# Build standalone binary for current platform (requires server bundle) +mise run build:standalone + +# ...or other platforms (requires server bundle) +mise run build:standalone:linux-amd64 +mise run build:standalone:linux-arm64 +mise run build:standalone:darwin-arm64 +mise run build:standalone:windows-amd64 +``` + +## API The API is documented under OpenAPI specification and can be found at the following path: ```shell -/:apipath/oas.json +/api/oas.json ``` There are several ways to interact with the API, we will cover its use with [Scalar](https://scalar.com). -We recommend using the desktop application, however, -you can also use the [web-based environment](https://client.scalar.com). (you may need to disable the CORS Proxy) +We recommend using the desktop application, however, you can also use the +[web-based environment](https://client.scalar.com). (you may need to disable the CORS Proxy) Follow these steps to import the instance's `oas.json` to Scalar..: ![](https://static.x.inetol.net/jspaste/backend/scalar-t1.gif) -### Maintenance +## 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..: +If you want to clear the entire project of dependencies and build remnants..: ```shell -bun run clean:git:all -``` \ No newline at end of file +mise run clean +``` + +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 +# Careful with this one! +mise run clean:git +``` diff --git a/Dockerfile b/Dockerfile index aa22e5e6..a52d8042 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,44 +1,58 @@ -FROM --platform=$BUILDPLATFORM docker.io/oven/bun:1-alpine AS builder-standalone +FROM --platform=$BUILDPLATFORM cgr.dev/chainguard/glibc-dynamic:latest-dev AS builder +USER root + +RUN set -euxo pipefail; \ + wget -qO- https://mise.run | sh; \ + ln -s $HOME/.local/bin/mise /usr/bin/mise WORKDIR /build/ COPY . ./ -RUN bun install --frozen-lockfile \ - && bun run build:server +RUN set -euxo pipefail; \ + mise trust; \ + GITHUB_ACTIONS=true mise run build:server -RUN addgroup jspaste \ - && adduser -G jspaste -u 7777 -s /bin/false -D jspaste \ - && grep jspaste /etc/passwd > /tmp/.backend.passwd +RUN echo "root:x:0:root" >/tmp/.group \ + && echo "root:x:0:0:root:/backend:/bin/ash" >/tmp/.passwd \ + && echo "jspaste:x:7777:jspaste" >>/tmp/.group \ + && echo "jspaste:x:7777:7777:jspaste:/backend:/bin/ash" >>/tmp/.passwd ARG TARGETOS ARG TARGETARCH -RUN bun run build:standalone - -FROM --platform=$BUILDPLATFORM docker.io/library/alpine:3.21 +RUN set -euxo pipefail; \ + mise run build:standalone -RUN apk add --no-cache libstdc++ +FROM --platform=$BUILDPLATFORM scratch AS dist -COPY --from=builder-standalone /tmp/.backend.passwd /etc/passwd -COPY --from=builder-standalone /etc/group /etc/group +COPY --from=builder /tmp/.passwd /etc/passwd +COPY --from=builder /tmp/.group /etc/group +COPY --chown=root:root --from=cgr.dev/chainguard/wolfi-base:latest / / +COPY --chown=root:root --from=builder /tmp/.passwd /etc/passwd +COPY --chown=root:root --from=builder /tmp/.group /etc/group +RUN rm -rf /home/ -WORKDIR /backend/ -COPY --chown=jspaste:jspaste --from=builder-standalone /build/dist/server ./ -COPY --chown=jspaste:jspaste --from=builder-standalone /build/LICENSE ./ +COPY --chown=7777:7777 --from=builder /build/dist/backend /backend/server +COPY --chown=7777:7777 --from=builder /build/LICENSE /backend/ LABEL org.opencontainers.image.created="0001-01-01T00:00:00Z" \ - org.opencontainers.image.description="JSPaste Backend" \ - org.opencontainers.image.licenses="EUPL-1.2" \ - org.opencontainers.image.revision="unspecified" \ - org.opencontainers.image.source="https://github.com/jspaste/backend" \ - org.opencontainers.image.title="jspaste-backend" \ - org.opencontainers.image.url="https://github.com/jspaste/backend" \ - org.opencontainers.image.version="unspecified" + org.opencontainers.image.description="JSPaste Backend" \ + org.opencontainers.image.licenses="EUPL-1.2" \ + org.opencontainers.image.revision="unspecified" \ + org.opencontainers.image.source="https://github.com/jspaste/backend" \ + org.opencontainers.image.title="jspaste-backend" \ + org.opencontainers.image.url="https://github.com/jspaste/backend" \ + org.opencontainers.image.version="unspecified" + +ENV PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ + SSL_CERT_DIR="/etc/ssl/certs" \ + SSL_CERT_FILE="/etc/ssl/certs/ca-certificates.crt" \ + HISTFILE="/dev/null" \ + STORAGE_PATH="/backend/storage/" EXPOSE 4000 -VOLUME /backend/storage/ +VOLUME $STORAGE_PATH -USER jspaste:jspaste - -ENTRYPOINT ["/backend/server"] \ No newline at end of file +WORKDIR /backend/ +ENTRYPOINT ["/backend/server"] diff --git a/README.md b/README.md index 7183c5a4..0251763e 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ Linux & macOS: ```shell -./server +./backend.- ``` Windows: ```powershell -powershell -c ".\server.exe" +powershell -c ".\backend.windows-.exe" ``` ### Container @@ -33,11 +33,11 @@ docker run --env-file=.env -d -p 127.0.0.1:4000:4000 \ ## Validate > [!IMPORTANT] -> 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. +> 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. -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). +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). Artifacts are attested and can be verified using the following command: diff --git a/biome.json b/biome.json index d6ca3679..cd179e29 100644 --- a/biome.json +++ b/biome.json @@ -1,56 +1,154 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "files": { - "ignore": ["./dist/**", "./storage/**", "*.spec.ts"], - "ignoreUnknown": true - }, - "formatter": { - "enabled": true, - "formatWithErrors": false, - "indentStyle": "tab", - "indentWidth": 4, - "lineEnding": "lf", - "lineWidth": 120 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "complexity": { - "noStaticOnlyClass": "off" - }, - "style": { - "noParameterAssign": "off" - }, - "suspicious": { - "noConsoleLog": "error" - } - } - }, - "css": { - "formatter": { - "enabled": true - } - }, - "javascript": { - "formatter": { - "arrowParentheses": "always", - "bracketSameLine": false, - "bracketSpacing": true, - "enabled": true, - "jsxQuoteStyle": "single", - "quoteProperties": "asNeeded", - "quoteStyle": "single", - "semicolons": "always", - "trailingCommas": "none" - } - }, - "json": { - "formatter": { - "enabled": true - } - }, - "organizeImports": { - "enabled": true - } + "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "files": { + "ignoreUnknown": true, + "includes": ["**", "!!dist/*", "!node_modules/*", "!!storage/*"] + }, + "assist": { + "enabled": true, + "actions": { + "recommended": true, + "source": { + "useSortedAttributes": "on", + "useSortedProperties": "on" + } + } + }, + "formatter": { + "enabled": true, + "bracketSameLine": false, + "bracketSpacing": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 120 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noForEach": "error", + "noImplicitCoercions": "error", + "useSimplifiedLogicExpression": "error" + }, + "correctness": { + "noGlobalDirnameFilename": "error", + "useImportExtensions": "error", + "useJsonImportAttributes": "error", + "useSingleJsDocAsterisk": "error" + }, + "nursery": { + "noContinue": "warn", + "noDeprecatedImports": "warn", + "noEqualsToNull": "warn", + "noFloatingPromises": "warn", + "noForIn": "warn", + "noImportCycles": "warn", + "noIncrementDecrement": "warn", + "noMisusedPromises": "warn", + "noMultiAssign": "warn", + "noMultiStr": "warn", + "noParametersOnlyUsedInRecursion": "warn", + "noReturnAssign": "warn", + "noUselessCatchBinding": "warn", + "noUselessUndefined": "warn", + "useAwaitThenable": "off", + "useDestructuring": "warn", + "useExhaustiveSwitchCases": "warn", + "useExplicitType": "off", + "useFind": "warn", + "useRegexpExec": "warn" + }, + "performance": { + "noAwaitInLoops": "error", + "noBarrelFile": "error", + "noDelete": "error", + "noNamespaceImport": "error", + "noReExportAll": "error", + "useTopLevelRegex": "error" + }, + "style": { + "noCommonJs": "error", + "noEnum": "error", + "noImplicitBoolean": "error", + "noInferrableTypes": "error", + "noNamespace": "error", + "noNegationElse": "error", + "noNestedTernary": "error", + "noParameterAssign": "error", + "noParameterProperties": "error", + "noSubstr": "error", + "noUnusedTemplateLiteral": "error", + "noUselessElse": "error", + "noYodaExpression": "error", + "useAsConstAssertion": "error", + "useAtIndex": "error", + "useCollapsedElseIf": "error", + "useCollapsedIf": "error", + "useConsistentArrayType": { + "level": "error", + "options": { + "syntax": "shorthand" + } + }, + "useConsistentBuiltinInstantiation": "error", + "useConsistentMemberAccessibility": { + "level": "error", + "options": { + "accessibility": "explicit" + } + }, + "useConsistentObjectDefinitions": { + "level": "error", + "options": { + "syntax": "explicit" + } + }, + "useConsistentTypeDefinitions": { + "level": "error", + "options": { + "style": "type" + } + }, + "useDefaultSwitchClause": "error", + "useExplicitLengthCheck": "error", + "useForOf": "error", + "useGroupedAccessorPairs": "error", + "useNumberNamespace": "error", + "useNumericSeparators": "error", + "useObjectSpread": "error", + "useReadonlyClassProperties": "error", + "useSelfClosingElements": "error", + "useShorthandAssign": "error", + "useSingleVarDeclarator": "error", + "useThrowNewError": "error", + "useThrowOnlyError": "error", + "useTrimStartEnd": "error", + "useUnifiedTypeSignatures": "error" + }, + "suspicious": { + "noConsole": "error", + "noAlert": "error", + "noConstantBinaryExpressions": "error", + "noEmptyBlockStatements": "error", + "noEvolvingTypes": "error", + "noUnassignedVariables": "error", + "noVar": "error", + "useNumberToFixedDigitsArgument": "error", + "useStaticResponseMethods": "error" + } + } + }, + "javascript": { + "formatter": { + "arrowParentheses": "always", + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "none" + } + } } diff --git a/bun.lock b/bun.lock deleted file mode 100644 index e339f1ca..00000000 --- a/bun.lock +++ /dev/null @@ -1,113 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "dependencies": { - "@hono/zod-openapi": "~0.19.0", - "env-var": "~7.5.0", - "hono": "~4.10.0", - }, - "devDependencies": { - "@biomejs/biome": "~1.9.0", - "@types/bun": "^1.2.0", - "lefthook": "~2.0.0", - "sort-package-json": "^3.0.0", - }, - "peerDependencies": { - "typescript": "~5.8.0 || ~5.9.0", - }, - }, - }, - "trustedDependencies": [ - "@biomejs/biome", - "lefthook", - ], - "packages": { - "@asteasolutions/zod-to-openapi": ["@asteasolutions/zod-to-openapi@7.3.4", "", { "dependencies": { "openapi3-ts": "^4.1.2" }, "peerDependencies": { "zod": "^3.20.2" } }, "sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA=="], - - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], - - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], - - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], - - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], - - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], - - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], - - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], - - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - - "@hono/zod-openapi": ["@hono/zod-openapi@0.19.10", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@hono/zod-validator": "^0.7.1", "openapi3-ts": "^4.5.0" }, "peerDependencies": { "hono": ">=4.3.6", "zod": ">=3.0.0" } }, "sha512-dpoS6DenvoJyvxtQ7Kd633FRZ/Qf74+4+o9s+zZI8pEqnbjdF/DtxIib08WDpCaWabMEJOL5TXpMgNEZvb7hpA=="], - - "@hono/zod-validator": ["@hono/zod-validator@0.7.6", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw=="], - - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], - - "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], - - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], - - "detect-indent": ["detect-indent@7.0.2", "", {}, "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A=="], - - "detect-newline": ["detect-newline@4.0.1", "", {}, "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog=="], - - "env-var": ["env-var@7.5.0", "", {}, "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "git-hooks-list": ["git-hooks-list@4.1.1", "", {}, "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA=="], - - "hono": ["hono@4.10.8", "", {}, "sha512-DDT0A0r6wzhe8zCGoYOmMeuGu3dyTAE40HHjwUsWFTEy5WxK1x2WDSsBPlEXgPbRIFY6miDualuUDbasPogIww=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "lefthook": ["lefthook@2.0.13", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.13", "lefthook-darwin-x64": "2.0.13", "lefthook-freebsd-arm64": "2.0.13", "lefthook-freebsd-x64": "2.0.13", "lefthook-linux-arm64": "2.0.13", "lefthook-linux-x64": "2.0.13", "lefthook-openbsd-arm64": "2.0.13", "lefthook-openbsd-x64": "2.0.13", "lefthook-windows-arm64": "2.0.13", "lefthook-windows-x64": "2.0.13" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-D39rCVl7/GpqakvhQvqz07SBpzUWTvWjXKnBZyIy8O6D+Lf9xD6tnbHtG5nWXd9iPvv1AKGQwL9R/e5rNtV6SQ=="], - - "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-KbQqpNSNTugjtPzt97CNcy/XZy5asJ0+uSLoHc4ML8UCJdsXKYJGozJHNwAd0Xfci/rQlj82A7rPOuTdh0jY0Q=="], - - "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-s/vI6sEE8/+rE6CONZzs59LxyuSc/KdU+/3adkNx+Q13R1+p/AvQNeszg3LAHzXmF3NqlxYf8jbj/z5vBzEpRw=="], - - "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.13", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-iQeJTU7Zl8EJlCMQxNZQpJFAQ9xl40pydUIv5SYnbJ4nqIr9ONuvrioNv6N2LtKP5aBl1nIWQQ9vMjgVyb3k+A=="], - - "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-99cAXKRIzpq/u3obUXbOQJCHP+0ZkJbN3TF+1ZQZlRo3Y6+mPSCg9fh/oi6dgbtu4gTI5Ifz3o5p2KZzAIF9ZQ=="], - - "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-RWarenY3kLy/DT4/8dY2bwDlYwlELRq9MIFq+FiMYmoBHES3ckWcLX2JMMlM49Y672paQc7MbneSrNUn/FQWhg=="], - - "lefthook-linux-x64": ["lefthook-linux-x64@2.0.13", "", { "os": "linux", "cpu": "x64" }, "sha512-QZRcxXGf8Uj/75ITBqoBh0zWhJE7+uFoRxEHwBq0Qjv55Q4KcFm7FBN/IFQCSd14reY5pmY3kDaWVVy60cAGJA=="], - - "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.13", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-LAuOWwnNmOlRE0RxKMOhIz5Kr9tXi0rCjzXtDARW9lvfAV6Br2wP+47q0rqQ8m/nVwBYoxfJ/RDunLbb86O1nA=="], - - "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.13", "", { "os": "openbsd", "cpu": "x64" }, "sha512-n9TIN3QLncyxOHomiKKwzDFHKOCm5H28CVNAZFouKqDwEaUGCs5TJI88V85j4/CgmLVUU8uUn4ClVCxIWYG59w=="], - - "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-sdSC4F9Di7y0t43Of9MOA5g/0CmvkM4juQ3sKfUhRcoygetLJn4PR2/pvuDOIaGf4mNMXBP5IrcKaeDON9HrcA=="], - - "lefthook-windows-x64": ["lefthook-windows-x64@2.0.13", "", { "os": "win32", "cpu": "x64" }, "sha512-ccl1v7Fl10qYoghEtjXN+JC1x/y/zLM/NSHf3NFGeKEGBNd1P5d/j6w8zVmhfzi+ekS8whXrcNbRAkLdAqUrSw=="], - - "openapi3-ts": ["openapi3-ts@4.5.0", "", { "dependencies": { "yaml": "^2.8.0" } }, "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ=="], - - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "sort-object-keys": ["sort-object-keys@2.0.1", "", {}, "sha512-R89fO+z3x7hiKPXX5P0qim+ge6Y60AjtlW+QQpRozrrNcR1lw9Pkpm5MLB56HoNvdcLHL4wbpq16OcvGpEDJIg=="], - - "sort-package-json": ["sort-package-json@3.6.0", "", { "dependencies": { "detect-indent": "^7.0.2", "detect-newline": "^4.0.1", "git-hooks-list": "^4.1.1", "is-plain-obj": "^4.1.0", "semver": "^7.7.3", "sort-object-keys": "^2.0.1", "tinyglobby": "^0.2.15" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-fyJsPLhWvY7u2KsKPZn1PixbXp+1m7V8NWqU8CvgFRbMEX41Ffw1kD8n0CfJiGoaSfoAvbrqRRl/DcHO8omQOQ=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], - - "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], - } -} diff --git a/bunfig.toml b/bunfig.toml deleted file mode 100644 index 4f06890a..00000000 --- a/bunfig.toml +++ /dev/null @@ -1,9 +0,0 @@ -telemetry = false - -[install] -auto = "disable" -saveTextLockfile = true - -[run] -bun = true -silent = true \ No newline at end of file diff --git a/deno.json b/deno.json new file mode 100644 index 00000000..5e1ae0f3 --- /dev/null +++ b/deno.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://raw.githubusercontent.com/denoland/deno/main/cli/schemas/config-file.v1.json", + "license": "EUPL-1.2", + "lock": true, + "nodeModulesDir": "manual", + "unstable": ["cron", "temporal", "raw-imports"], + "compilerOptions": { + "lib": ["deno.window", "deno.unstable"], + "types": ["node"], + "module": "esnext", + "moduleResolution": "bundler", + + "checkJs": false, + "skipLibCheck": true, + + "strict": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "exactOptionalPropertyTypes": false, + "isolatedDeclarations": false, + "noErrorTruncation": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + + "baseUrl": ".", + "paths": { + "#/*": ["./src/*"], + "#db/*": ["./src/database/*"], + "#document/*": ["./src/document/*"], + "#endpoint/*": ["./src/endpoints/*"], + "#http/*": ["./src/http/*"], + "#task/*": ["./src/tasks/*"], + "#util/*": ["./src/utils/*"] + } + }, + "fmt": { + "exclude": ["**"] + }, + "lint": { + "exclude": ["**"] + }, + "exclude": ["./dist/", "./node_modules/", "./storage/"], + "allowScripts": [] +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 00000000..c1fdf103 --- /dev/null +++ b/deno.lock @@ -0,0 +1,410 @@ +{ + "version": "5", + "specifiers": { + "npm:@biomejs/biome@2.3.11": "2.3.11", + "npm:@jsr/hono__hono@^4.11.3": "4.11.3", + "npm:@jsr/hono__standard-validator@~0.2.1": "0.2.1", + "npm:@jsr/std__assert@^1.0.16": "1.0.16", + "npm:@jsr/std__async@^1.0.16": "1.0.16", + "npm:@jsr/std__cache@~0.2.1": "0.2.1", + "npm:@jsr/std__collections@^1.1.3": "1.1.3", + "npm:@jsr/std__crypto@^1.0.5": "1.0.5", + "npm:@jsr/std__dotenv@~0.225.6": "0.225.6", + "npm:@jsr/std__fmt@^1.0.8": "1.0.8", + "npm:@jsr/std__fs@^1.0.21": "1.0.21", + "npm:@jsr/std__streams@^1.0.16": "1.0.16", + "npm:@jsr/std__ulid@1": "1.0.0", + "npm:@types/node@^25.0.3": "25.0.3", + "npm:arkenv@~0.8.1": "0.8.1_arktype@2.1.29", + "npm:arktype@^2.1.29": "2.1.29", + "npm:hono-openapi@^1.1.2": "1.1.2_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.1.29__quansync@0.2.11_@standard-community+standard-openapi@0.2.9__@standard-community+standard-json@0.3.5___@standard-schema+spec@1.1.0___@types+json-schema@7.0.15___arktype@2.1.29___quansync@0.2.11__@standard-schema+spec@1.1.0__arktype@2.1.29__openapi-types@12.1.3__@types+json-schema@7.0.15_@types+json-schema@7.0.15_openapi-types@12.1.3_arktype@2.1.29", + "npm:nanoid@^5.1.6": "5.1.6", + "npm:rolldown@1.0.0-beta.58": "1.0.0-beta.58", + "npm:sorted-btree@^2.1.0": "2.1.0" + }, + "npm": { + "@ark/schema@0.56.0": { + "integrity": "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==", + "dependencies": [ + "@ark/util" + ] + }, + "@ark/util@0.56.0": { + "integrity": "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==" + }, + "@biomejs/biome@2.3.11": { + "integrity": "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==", + "optionalDependencies": [ + "@biomejs/cli-darwin-arm64", + "@biomejs/cli-darwin-x64", + "@biomejs/cli-linux-arm64", + "@biomejs/cli-linux-arm64-musl", + "@biomejs/cli-linux-x64", + "@biomejs/cli-linux-x64-musl", + "@biomejs/cli-win32-arm64", + "@biomejs/cli-win32-x64" + ], + "bin": true + }, + "@biomejs/cli-darwin-arm64@2.3.11": { + "integrity": "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@biomejs/cli-darwin-x64@2.3.11": { + "integrity": "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@biomejs/cli-linux-arm64-musl@2.3.11": { + "integrity": "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@biomejs/cli-linux-arm64@2.3.11": { + "integrity": "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@biomejs/cli-linux-x64-musl@2.3.11": { + "integrity": "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@biomejs/cli-linux-x64@2.3.11": { + "integrity": "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@biomejs/cli-win32-arm64@2.3.11": { + "integrity": "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@biomejs/cli-win32-x64@2.3.11": { + "integrity": "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@emnapi/core@1.8.1": { + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dependencies": [ + "@emnapi/wasi-threads", + "tslib" + ] + }, + "@emnapi/runtime@1.8.1": { + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dependencies": [ + "tslib" + ] + }, + "@emnapi/wasi-threads@1.1.0": { + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dependencies": [ + "tslib" + ] + }, + "@jsr/hono__hono@4.11.3": { + "integrity": "sha512-1K5jN5tabn9NzylJUQBdYuz25Nv3WarXRXfkSZeiCZK05ahzGZW2aXtKx1odkE1ztTIdkVGDkfht1CQHdGh4iA==", + "tarball": "https://npm.jsr.io/~/11/@jsr/hono__hono/4.11.3.tgz" + }, + "@jsr/hono__standard-validator@0.2.1": { + "integrity": "sha512-93mG2IHjrCzb8A2705N03SW3LyWaNT7EkVwcZ4TqmRDzvX6OZJSe2fCeI3+39qJKon+SkgsdoTqRt00deNg0Gw==", + "dependencies": [ + "@jsr/hono__hono", + "@jsr/standard-schema__spec" + ], + "tarball": "https://npm.jsr.io/~/11/@jsr/hono__standard-validator/0.2.1.tgz" + }, + "@jsr/standard-schema__spec@1.1.0": { + "integrity": "sha512-mWncLgOE1ZVd/xXG+SPmmDmYPQ9Q3OcIbkCn/3oPpp4WOw3RpbHWdxk/jiG8m4Bd9utE2b1yyfynSStpuhdXew==", + "tarball": "https://npm.jsr.io/~/11/@jsr/standard-schema__spec/1.1.0.tgz" + }, + "@jsr/std__assert@1.0.16": { + "integrity": "sha512-bX9ih0nR1kQ12/cnQRCQU0ppTCV7MFkP0qjyWxJRoDI8RC5cpTAmLFH/KcFgxmdN4flKkRbub8VtLuyKq+4OxA==", + "dependencies": [ + "@jsr/std__internal" + ], + "tarball": "https://npm.jsr.io/~/11/@jsr/std__assert/1.0.16.tgz" + }, + "@jsr/std__async@1.0.16": { + "integrity": "sha512-WoYmNEPSh+Bs09HvVceERknVX813wQjSb2D9Z0KdxGTMYl5Pm13e5xqa3mYu9QBRlxIxpTivGhIQYaslEezrhw==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__async/1.0.16.tgz" + }, + "@jsr/std__bytes@1.0.6": { + "integrity": "sha512-St6yKggjFGhxS52IFLJWvkchRFbAKg2Xh8UxA4S1EGz7GJ2Ui+ssDDldj/w2c8vCxvl6qgR0HaYbKeFJNqujmA==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__bytes/1.0.6.tgz" + }, + "@jsr/std__cache@0.2.1": { + "integrity": "sha512-K4qXWEOWiwo04zyJNxBogPdoXHcmmKjX+O8aKFEbVAd22/AiD9JFK25ZQ85/jCRZu+tDq99mSUz4x8NKqLsISQ==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__cache/0.2.1.tgz" + }, + "@jsr/std__collections@1.1.3": { + "integrity": "sha512-jGG6mv3IjOyxm6PyT1YVbLyAlZL+Gow6LOpBw+84qb1nkdJY0+t6bi7ICEqAwUz87cNjBS0P+yZQ5HHclJhsfw==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__collections/1.1.3.tgz" + }, + "@jsr/std__crypto@1.0.5": { + "integrity": "sha512-iqFCkjeGeQccLgmxH9m1d7abjZcFMW0XrYZu1itNz8vVHzH9crObalonjVQaVDdKHCrNNOklMN1t0u3k46dirA==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__crypto/1.0.5.tgz" + }, + "@jsr/std__dotenv@0.225.6": { + "integrity": "sha512-rqh5RrHccbyzmP4v1/vqUyYy4dqopjTRgW8bJqk2ZXTKBbvpmMjPxJ+xy+YAk6XnEvtPCPAgqbFhHWcomjnX+w==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__dotenv/0.225.6.tgz" + }, + "@jsr/std__fmt@1.0.8": { + "integrity": "sha512-miZHzj9OgjuajrcMKzpqNVwFb9O71UHZzV/FHVq0E0Uwmv/1JqXgmXAoBNPrn+MP0fHT3mMgaZ6XvQO7dam67Q==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.8.tgz" + }, + "@jsr/std__fs@1.0.21": { + "integrity": "sha512-k/agrcKGm6KD89ci3AEyRmu3wRWf9JZNliOF4ZUxagTHiySmxjiKU3Lk+d2ksRtwEi7oWlLGS0AVM9Lciwc/xg==", + "dependencies": [ + "@jsr/std__internal", + "@jsr/std__path" + ], + "tarball": "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.21.tgz" + }, + "@jsr/std__internal@1.0.12": { + "integrity": "sha512-6xReMW9p+paJgqoFRpOE2nogJFvzPfaLHLIlyADYjKMUcwDyjKZxryIbgcU+gxiTygn8yCjld1HoI0ET4/iZeA==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz" + }, + "@jsr/std__path@1.1.4": { + "integrity": "sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA==", + "dependencies": [ + "@jsr/std__internal" + ], + "tarball": "https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz" + }, + "@jsr/std__streams@1.0.16": { + "integrity": "sha512-8vQHEDIpAr5m9upZEcF1UO2ylZCJsOs5mlsXaJNehQmSm8Iiz/XaKtb73Fh6fj8Ybc2jNw2zyi9CLfqd3Ph6mA==", + "dependencies": [ + "@jsr/std__bytes" + ], + "tarball": "https://npm.jsr.io/~/11/@jsr/std__streams/1.0.16.tgz" + }, + "@jsr/std__ulid@1.0.0": { + "integrity": "sha512-RvVolUwRoFtoSuYZROBmhCYEIuI4xsxeHjGGbJQZVDCBnDBJwfm16vOJyE1Va9+BnTl1iW7o1nCloBI+EQAWVg==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__ulid/1.0.0.tgz" + }, + "@napi-rs/wasm-runtime@1.1.1": { + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dependencies": [ + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util" + ] + }, + "@oxc-project/types@0.106.0": { + "integrity": "sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg==" + }, + "@rolldown/binding-android-arm64@1.0.0-beta.58": { + "integrity": "sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@rolldown/binding-darwin-arm64@1.0.0-beta.58": { + "integrity": "sha512-wFxUymI/5R8bH8qZFYDfAxAN9CyISEIYke+95oZPiv6EWo88aa5rskjVcCpKA532R+klFmdqjbbaD56GNmTF4Q==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@rolldown/binding-darwin-x64@1.0.0-beta.58": { + "integrity": "sha512-ybp3MkPj23VDV9PhtRwdU5qrGhlViWRV5BjKwO6epaSlUD5lW0WyY+roN3ZAzbma/9RrMTgZ/a/gtQq8YXOcqw==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@rolldown/binding-freebsd-x64@1.0.0-beta.58": { + "integrity": "sha512-Evxj3yh7FWvyklUYZa0qTVT9N2zX9TPDqGF056hl8hlCZ9/ndQ2xMv6uw9PD1VlLpukbsqL+/C6M0qwipL0QMg==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.58": { + "integrity": "sha512-tYeXprDOrEgVHUbPXH6MPso4cM/c6RTkmJNICMQlYdki4hGMh92aj3yU6CKs+4X5gfG0yj5kVUw/L4M685SYag==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rolldown/binding-linux-arm64-gnu@1.0.0-beta.58": { + "integrity": "sha512-N78vmZzP6zG967Ohr+MasCjmKtis0geZ1SOVmxrA0/bklTQSzH5kHEjW5Qn+i1taFno6GEre1E40v0wuWsNOQw==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rolldown/binding-linux-arm64-musl@1.0.0-beta.58": { + "integrity": "sha512-l+p4QVtG72C7wI2SIkNQw/KQtSjuYwS3rV6AKcWrRBF62ClsFUcif5vLaZIEbPrCXu5OFRXigXFJnxYsVVZqdQ==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rolldown/binding-linux-x64-gnu@1.0.0-beta.58": { + "integrity": "sha512-urzJX0HrXxIh0FfxwWRjfPCMeInU9qsImLQxHBgLp5ivji1EEUnOfux8KxPPnRQthJyneBrN2LeqUix9DYrNaQ==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rolldown/binding-linux-x64-musl@1.0.0-beta.58": { + "integrity": "sha512-7ijfVK3GISnXIwq/1FZo+KyAUJjL3kWPJ7rViAL6MWeEBhEgRzJ0yEd9I8N9aut8Y8ab+EKFJyRNMWZuUBwQ0A==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rolldown/binding-openharmony-arm64@1.0.0-beta.58": { + "integrity": "sha512-/m7sKZCS+cUULbzyJTIlv8JbjNohxbpAOA6cM+lgWgqVzPee3U6jpwydrib328JFN/gF9A99IZEnuGYqEDJdww==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@rolldown/binding-wasm32-wasi@1.0.0-beta.58": { + "integrity": "sha512-6SZk7zMgv+y3wFFQ9qE5P9NnRHcRsptL1ypmudD26PDY+PvFCvfHRkJNfclWnvacVGxjowr7JOL3a9fd1wWhUw==", + "dependencies": [ + "@napi-rs/wasm-runtime" + ], + "cpu": ["wasm32"] + }, + "@rolldown/binding-win32-arm64-msvc@1.0.0-beta.58": { + "integrity": "sha512-sFqfYPnBZ6xBhMkadB7UD0yjEDRvs7ipR3nCggblN+N4ODCXY6qhg/bKL39+W+dgQybL7ErD4EGERVbW9DAWvg==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@rolldown/binding-win32-x64-msvc@1.0.0-beta.58": { + "integrity": "sha512-AnFWJdAqB8+IDPcGrATYs67Kik/6tnndNJV2jGRmwlbeNiQQ8GhRJU8ETRlINfII0pqi9k4WWLnb00p1QCxw/Q==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@rolldown/pluginutils@1.0.0-beta.58": { + "integrity": "sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==" + }, + "@standard-community/standard-json@0.3.5_@standard-schema+spec@1.1.0_@types+json-schema@7.0.15_arktype@2.1.29_quansync@0.2.11": { + "integrity": "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==", + "dependencies": [ + "@standard-schema/spec", + "@types/json-schema", + "arktype", + "quansync" + ], + "optionalPeers": [ + "arktype" + ] + }, + "@standard-community/standard-openapi@0.2.9_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.1.29__quansync@0.2.11_@standard-schema+spec@1.1.0_arktype@2.1.29_openapi-types@12.1.3_@types+json-schema@7.0.15": { + "integrity": "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg==", + "dependencies": [ + "@standard-community/standard-json", + "@standard-schema/spec", + "arktype", + "openapi-types" + ], + "optionalPeers": [ + "arktype" + ] + }, + "@standard-schema/spec@1.1.0": { + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" + }, + "@tybys/wasm-util@0.10.1": { + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dependencies": [ + "tslib" + ] + }, + "@types/json-schema@7.0.15": { + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "@types/node@25.0.3": { + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dependencies": [ + "undici-types" + ] + }, + "arkenv@0.8.1_arktype@2.1.29": { + "integrity": "sha512-6aGadHN2nuWuRNWcmNc/CfdlUak6jeZi/WBLLWN/LvBFY8aR+FSMJE6x9SUS9L1m+qp94MTVyKpi3tDzo+SoJw==", + "dependencies": [ + "arktype" + ] + }, + "arkregex@0.0.5": { + "integrity": "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==", + "dependencies": [ + "@ark/util" + ] + }, + "arktype@2.1.29": { + "integrity": "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ==", + "dependencies": [ + "@ark/schema", + "@ark/util", + "arkregex" + ] + }, + "hono-openapi@1.1.2_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.1.29__quansync@0.2.11_@standard-community+standard-openapi@0.2.9__@standard-community+standard-json@0.3.5___@standard-schema+spec@1.1.0___@types+json-schema@7.0.15___arktype@2.1.29___quansync@0.2.11__@standard-schema+spec@1.1.0__arktype@2.1.29__openapi-types@12.1.3__@types+json-schema@7.0.15_@types+json-schema@7.0.15_openapi-types@12.1.3_arktype@2.1.29": { + "integrity": "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g==", + "dependencies": [ + "@standard-community/standard-json", + "@standard-community/standard-openapi", + "@types/json-schema", + "openapi-types" + ] + }, + "nanoid@5.1.6": { + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "bin": true + }, + "openapi-types@12.1.3": { + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" + }, + "quansync@0.2.11": { + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==" + }, + "rolldown@1.0.0-beta.58": { + "integrity": "sha512-v1FCjMZCan7f+xGAHBi+mqiE4MlH7I+SXEHSQSJoMOGNNB2UYtvMiejsq9YuUOiZjNeUeV/a21nSFbrUR+4ZCQ==", + "dependencies": [ + "@oxc-project/types", + "@rolldown/pluginutils" + ], + "optionalDependencies": [ + "@rolldown/binding-android-arm64", + "@rolldown/binding-darwin-arm64", + "@rolldown/binding-darwin-x64", + "@rolldown/binding-freebsd-x64", + "@rolldown/binding-linux-arm-gnueabihf", + "@rolldown/binding-linux-arm64-gnu", + "@rolldown/binding-linux-arm64-musl", + "@rolldown/binding-linux-x64-gnu", + "@rolldown/binding-linux-x64-musl", + "@rolldown/binding-openharmony-arm64", + "@rolldown/binding-wasm32-wasi", + "@rolldown/binding-win32-arm64-msvc", + "@rolldown/binding-win32-x64-msvc" + ], + "bin": true + }, + "sorted-btree@2.1.0": { + "integrity": "sha512-AtYXy3lL+5jrATpbymC2bM8anN/3maLkmVCd94MzypnKjokfCid/zeS3rvXedv7W6ffSfqKIGdz3UaJPWRBZ0g==" + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "undici-types@7.16.0": { + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" + } + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@biomejs/biome@2.3.11", + "npm:@jsr/hono__hono@^4.11.3", + "npm:@jsr/hono__standard-validator@~0.2.1", + "npm:@jsr/std__assert@^1.0.16", + "npm:@jsr/std__async@^1.0.16", + "npm:@jsr/std__cache@~0.2.1", + "npm:@jsr/std__collections@^1.1.3", + "npm:@jsr/std__crypto@^1.0.5", + "npm:@jsr/std__dotenv@~0.225.6", + "npm:@jsr/std__fmt@^1.0.8", + "npm:@jsr/std__fs@^1.0.21", + "npm:@jsr/std__streams@^1.0.16", + "npm:@jsr/std__ulid@1", + "npm:@types/node@^25.0.3", + "npm:arkenv@~0.8.1", + "npm:arktype@^2.1.29", + "npm:hono-openapi@^1.1.2", + "npm:nanoid@^5.1.6", + "npm:rolldown@1.0.0-beta.58", + "npm:sorted-btree@^2.1.0" + ] + } + } +} diff --git a/lefthook.json b/lefthook.json deleted file mode 100644 index 2e8038ab..00000000 --- a/lefthook.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/lefthook.json", - "pre-commit": { - "commands": { - "fix": { - "env": { - "PATH": "$PATH:$HOME/.bun/bin" - }, - "run": "bun run fix && git update-index --again" - } - } - } -} diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000..54abd52a --- /dev/null +++ b/mise.toml @@ -0,0 +1,209 @@ +[tools] +deno = "latest" + +[tasks."install"] +description = "Install all dependencies" +run = [ + { task = "install:deno" } +] + +[tasks."install:deno"] +description = "Install Deno dependencies" +run = ''' +if [ "${GITHUB_ACTIONS}" = "true" ]; then + mise exec -- deno install --frozen; +else + mise exec -- deno install; +fi +''' + +[tasks."clean"] +description = "Clean project environment" +run = [ + { task = "clean:devel" } +] + +[tasks."clean:deno"] +description = "Clean deno artifacts" +run = [ + "rm -rf ./node_modules/ ./deno.lock" +] + +[tasks."clean:devel"] +description = "Clean development artifacts" +run = [ + "rm -rf ./dist/" +] + +[tasks."clean:storage"] +description = "Clean storage (careful)" +run = [ + "rm -rf ./storage/" +] + +[tasks."clean:git"] +description = "Clean git (careful)" +run = [ + { task = "clean:git:untracked" }, + { task = "clean:git:gc" }, + { task = "clean:git:hooks" }, +] + +[tasks."clean:git:gc"] +run = [ + "git gc --aggressive --prune" +] + +[tasks."clean:git:hooks"] +run = [ + "rm -rf ./.git/hooks/" +] + +[tasks."clean:git:untracked"] +run = [ + "git clean -d -x -i" +] + +[tasks."build"] +description = "Build (server)" +run = [ + { task = "build:server" }, +] + +[tasks."build:server"] +description = "Build server" +sources = ['src/**/*.ts', "deno.lock", 'rolldown.config.ts'] +outputs = ['dist/backend.js', 'dist/backend.js.map'] +run = [ + { task = "install" }, + "mise exec -- deno x -A rolldown -c rolldown.config.ts" +] + +[tasks."build:standalone_"] +hide = true +run = [ + { task = "build:server" }, + "mise exec -- deno compile --no-check --allow-all --exclude=./node_modules/ --exclude=./package.json --include=./dist/backend.js.map --output=${STANDALONE_OUTPUT:-./dist/backend} ${STANDALONE_TARGET:+--target=${STANDALONE_TARGET}} ./dist/backend.js" +] + +[tasks."build:standalone"] +description = "Build standalone binary (current os/arch)" +run = [ + { task = "build:standalone_" } +] + +[tasks."build:standalone:darwin-arm64"] +description = "Build standalone binary" +env = { STANDALONE_OUTPUT = "./dist/backend.darwin-arm64", STANDALONE_TARGET = "aarch64-apple-darwin" } +run = [ + { task = "build:standalone_" } +] + +[tasks."build:standalone:linux-amd64"] +description = "Build standalone binary" +env = { STANDALONE_OUTPUT = "./dist/backend.linux-amd64", STANDALONE_TARGET = "x86_64-unknown-linux-gnu" } +run = [ + { task = "build:standalone_" } +] + +[tasks."build:standalone:linux-arm64"] +description = "Build standalone binary" +env = { STANDALONE_OUTPUT = "./dist/backend.linux-arm64", STANDALONE_TARGET = "aarch64-unknown-linux-gnu" } +run = [ + { task = "build:standalone_" } +] + +[tasks."build:standalone:windows-amd64"] +description = "Build standalone binary" +env = { STANDALONE_OUTPUT = "./dist/backend.windows-amd64.exe", STANDALONE_TARGET = "x86_64-pc-windows-msvc" } +run = [ + { task = "build:standalone_" } +] + +[tasks."test"] +description = "Run all tests" +run = [ + "mise exec -- deno test -A" +] + +[tasks."fix"] +description = "Run all formatters" +run = [ + { task = "fix:biome" }, +] + +[tasks."fix:biome"] +description = "Run Biome formater" +run = [ + { task = "install" }, + "mise exec -- deno x -A biome check --write" +] + +[tasks."lint"] +description = "Run all linters" +run = [ + { task = "lint:deno" }, + { task = "lint:biome" }, +] + +[tasks."lint:deno"] +description = "Run Deno linter" +run = [ + { task = "install" }, + "mise exec -- deno check --quiet" +] + +[tasks."lint:biome"] +description = "Run Biome linter" +run = [ + { task = "install" }, + "mise exec -- deno x -A biome check" +] + +[tasks."tidy"] +description = "Tidy all" +run = [ + { task = "tidy:deno" }, +] + +[tasks."tidy:deno"] +description = "Tidy Deno dependencies" +run = [ + { task = "clean:deno" }, + { task = "install:deno" }, +] + +[tasks."start"] +description = "Start backend" +run = [ + { task = "start:server" }, +] + +[tasks."start:dev"] +alias = "dev" +description = "Start devel server" +run = [ + { task = "install" }, + "JSPB_DEBUG_DATABASE_EPHEMERAL=true JSPB_LOG_VERBOSITY=4 mise exec -- deno run -A ./src/index.ts" +] + +[tasks."start:build"] +description = "Start dedicated server" +run = [ + { task = "build:server" }, + { task = "start:server" }, +] + +[tasks."start:server"] +description = "Start dedicated server (requires built backend)" +dir = "{{ config_root }}/dist/" +run = [ + "JSPB_LOG_VERBOSITY=4 mise exec -- deno run -A ./backend.js" +] + +[tasks."start:server:inspector"] +description = "Start dedicated server (requires built backend)" +dir = "{{ config_root }}/dist/" +run = [ + "JSPB_LOG_VERBOSITY=4 mise exec -- deno run -A --inspect-brk ./backend.js" +] diff --git a/package.json b/package.json index 105ab5ce..e2bac7fb 100644 --- a/package.json +++ b/package.json @@ -1,53 +1,50 @@ { - "$schema": "https://json.schemastore.org/package.json", - "private": true, - "license": "EUPL-1.2", - "type": "module", - "scripts": { - "build": "bun run build:server", - "build:all": "bun run build:server && bun run build:standalone", - "build:server": "bun build ./src/server.ts --target=bun --minify --sourcemap=inline --outfile=./dist/server.js", - "build:standalone": "bun build ./dist/server.js --compile --minify --sourcemap=inline --outfile=./dist/server", - "build:standalone:darwin-arm64": "bun run build:standalone -- --target=bun-darwin-arm64", - "build:standalone:linux-amd64-glibc": "bun run build:standalone -- --target=bun-linux-x64-modern", - "build:standalone:linux-amd64-musl": "bun run build:standalone -- --target=bun-linux-x64-modern-musl", - "build:standalone:linux-arm64-glibc": "bun run build:standalone -- --target=bun-linux-arm64", - "build:standalone:linux-arm64-musl": "bun run build:standalone -- --target=bun-linux-arm64-musl", - "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", - "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 start:server", - "start:dev": "mkdir -p ./dist/ && LOGLEVEL=4 bun run --cwd=./dist/ ../src/server.ts", - "start:rebuild": "bun run build:server && bun run start:server", - "start:server": "mkdir -p ./dist/ && bun run --cwd=./dist/ ./server.js" - }, - "dependencies": { - "@hono/zod-openapi": "~0.19.0", - "env-var": "~7.5.0", - "hono": "~4.10.0" - }, - "devDependencies": { - "@biomejs/biome": "~1.9.0", - "@types/bun": "^1.2.0", - "lefthook": "~2.0.0", - "sort-package-json": "^3.0.0" - }, - "peerDependencies": { - "typescript": "~5.8.0 || ~5.9.0" - }, - "trustedDependencies": [ - "@biomejs/biome", - "lefthook" - ] + "$schema": "https://www.schemastore.org/package.json", + "license": "EUPL-1.2", + "type": "module", + "dependencies": { + "@hono/hono": "npm:@jsr/hono__hono@^4.11.3", + "@hono/openapi": "npm:hono-openapi@^1.1.2", + "@hono/standard-validator": "npm:@jsr/hono__standard-validator@~0.2.1", + "@std/assert": "npm:@jsr/std__assert@^1.0.16", + "@std/async": "npm:@jsr/std__async@^1.0.16", + "@std/cache": "npm:@jsr/std__cache@~0.2.1", + "@std/collections": "npm:@jsr/std__collections@^1.1.3", + "@std/crypto": "npm:@jsr/std__crypto@^1.0.5", + "@std/dotenv": "npm:@jsr/std__dotenv@~0.225.6", + "@std/fmt": "npm:@jsr/std__fmt@^1.0.8", + "@std/fs": "npm:@jsr/std__fs@^1.0.21", + "@std/streams": "npm:@jsr/std__streams@^1.0.16", + "@std/ulid": "npm:@jsr/std__ulid@^1.0.0", + "@types/node": "npm:@types/node@^25.0.3", + "arkenv": "npm:arkenv@~0.8.1", + "arktype": "npm:arktype@^2.1.29", + "biome": "npm:@biomejs/biome@2.3.11", + "btree": "npm:sorted-btree@^2.1.0", + "nanoid": "npm:nanoid@^5.1.6", + "rolldown": "npm:rolldown@1.0.0-beta.58" + }, + "imports": { + "#/*": [ + "./src/*" + ], + "#db/*": [ + "./src/database/*" + ], + "#document/*": [ + "./src/document/*" + ], + "#endpoint/*": [ + "./src/endpoints/*" + ], + "#http/*": [ + "./src/http/*" + ], + "#task/*": [ + "./src/tasks/*" + ], + "#util/*": [ + "./src/utils/*" + ] + } } diff --git a/rolldown.config.ts b/rolldown.config.ts new file mode 100644 index 00000000..2fe4876e --- /dev/null +++ b/rolldown.config.ts @@ -0,0 +1,31 @@ +import type { RolldownOptions } from "rolldown"; + +export default { + input: "./src/index.ts", + output: { + file: "./dist/backend.js", + format: "es", + inlineDynamicImports: true, + legalComments: "none", + minify: true, + sourcemap: true + }, + resolve: { + conditionNames: ["import", "require", "node", "default"], + mainFields: ["main", "module"] + }, + platform: "neutral", + external: [/^(node:)/], + moduleTypes: { + ".sql": "text" + }, + optimization: { + inlineConst: true + }, + transform: { + // deno.json compilerOptions + typescript: { + onlyRemoveTypeImports: true + } + } +} satisfies RolldownOptions; diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index 9e76dc66..00000000 --- a/src/config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { env } from '#util/env.ts'; - -export const config = { - protocol: env.tls ? 'https://' : 'http://', - apiPath: '/api', - storagePath: 'storage/', - documentNameLengthMin: 2, - documentNameLengthMax: 32, - documentNameLengthDefault: 8 -} as const; diff --git a/src/database/database.ts b/src/database/database.ts new file mode 100644 index 00000000..73e2ea32 --- /dev/null +++ b/src/database/database.ts @@ -0,0 +1,127 @@ +import { DatabaseSync, type StatementSync } from "node:sqlite"; +import { monotonicUlid } from "@std/ulid"; +import { constant } from "#/global.ts"; +import { Logger } from "#util/console.ts"; +import { migrations } from "./migrations.ts"; +import { DocumentQuery, UserQuery } from "./query.ts"; + +const log: Logger = new Logger("database"); + +type Options = { + ephemeral?: boolean; +}; + +export class Database { + public readonly document = new DocumentQuery(this); + public readonly user = new UserQuery(this); + + private readonly database: DatabaseSync; + + public constructor(options?: Options) { + const ephemeral = options?.ephemeral ?? constant.env.JSPB_DEBUG_DATABASE_EPHEMERAL; + + this.database = new DatabaseSync(ephemeral ? ":memory:" : constant.path.databaseFile); + + if (ephemeral) { + log.warn("Using ephemeral. No changes will persist."); + return; + } + + this.exec(`PRAGMA journal_mode = WAL; + PRAGMA wal_autocheckpoint = 1024;`); + } + + public migration(): void { + const query = this.prepare("PRAGMA user_version;", false).get(); + if (typeof query?.user_version !== "number") { + throw new Deno.errors.InvalidData("Failed to get version."); + } + if (query.user_version === migrations.length) { + log.debug("Already up to date."); + return; + } + if (query.user_version > migrations.length) { + throw new Deno.errors.InvalidData("Version is higher than available migrations. Update your JSPaste instance."); + } + + migrations.slice(query.user_version).forEach((migration, delta) => { + try { + this.transaction(() => { + this.exec(migration.sql); + this.exec(`PRAGMA user_version = ${(query.user_version as number) + delta + 1};`); + }); + } catch (error) { + log.error(`Error while running migration "${migration.id}"..:`); + throw error; + } + + log.info(`Migration "${migration.id}" ran successfully.`); + }); + + if (query.user_version === 0) { + try { + const token = this.user.create(constant.ulid.userRoot, constant.env.JSPB_USER_ROOT_TOKEN); + + if (!constant.env.JSPB_USER_ROOT_TOKEN) { + log.warn("Note the root user token as it won't be shown again", ` >> "${token}" <<`); + } + } catch (error) { + log.error("Failed to create root user..:"); + throw error; + } + } + } + + public exec(sql: string): void { + this.database.exec(sql); + } + + public prepare(sql: string, cache = true): StatementSync { + if (!cache) { + return this.database.prepare(sql); + } + + let statement = constant.store.statements.get(sql); + if (!statement) { + statement = this.database.prepare(sql); + constant.store.statements.set(sql, statement); + } + + return statement; + } + + public transaction(callback: () => T): T { + if (this.database.isTransaction) { + const name = monotonicUlid(); + + this.exec(`SAVEPOINT ${name};`); + try { + return callback(); + } catch (error) { + this.exec(`ROLLBACK TO ${name};`); + + throw error; + } finally { + this.exec(`RELEASE ${name};`); + } + } + + this.exec("BEGIN IMMEDIATE;"); + try { + const result = callback(); + + this.exec("COMMIT;"); + + return result; + } catch (error) { + this.exec("ROLLBACK;"); + + throw error; + } + } + + public [Symbol.dispose](): void { + constant.store.statements.clear(); + this.database.close(); + } +} diff --git a/src/database/migrations.ts b/src/database/migrations.ts new file mode 100644 index 00000000..9f543e9a --- /dev/null +++ b/src/database/migrations.ts @@ -0,0 +1,17 @@ +type Migration = { + id: string; + sql: string; +}; + +export const migrations: Migration[] = [ + /** + * @description + * Base schema. + * + * @date 2025-12-27 + */ + { + id: "0001.base", + sql: (await import("./migrations/0001.base.sql", { with: { type: "text" } })).default + } +] as const; diff --git a/src/database/migrations/0001.base.sql b/src/database/migrations/0001.base.sql new file mode 100644 index 00000000..bfa6be50 --- /dev/null +++ b/src/database/migrations/0001.base.sql @@ -0,0 +1,18 @@ +CREATE TABLE user +( + id TEXT NOT NULL PRIMARY KEY, + token TEXT NOT NULL +) STRICT; + +CREATE UNIQUE INDEX idx_user_token ON USER (token); + +CREATE TABLE document +( + id TEXT NOT NULL PRIMARY KEY, + user_id TEXT REFERENCES USER (id) ON DELETE CASCADE, + version INTEGER NOT NULL, + name TEXT NOT NULL, + password TEXT +) STRICT; + +CREATE UNIQUE INDEX idx_document_name ON DOCUMENT (name) diff --git a/src/database/query.ts b/src/database/query.ts new file mode 100644 index 00000000..0234d666 --- /dev/null +++ b/src/database/query.ts @@ -0,0 +1,133 @@ +import type { SQLInputValue } from "node:sqlite"; +import { chunk } from "@std/collections"; +import { monotonicUlid } from "@std/ulid"; +import { constant } from "#/global.ts"; +import { generateToken } from "#util/document.ts"; +import type { Database } from "./database.ts"; + +export const DocumentVersion = { + V1: 1 +} as const; +export type DocumentVersionType = (typeof DocumentVersion)[keyof typeof DocumentVersion]; + +export type Document = { + id: string; + user_id: string | null; + version: DocumentVersionType; + name: string; + password: string | null; +}; +export type DocumentColumn = Pick; +export type DocumentIndex = DocumentColumn<"id" | "name">; + +export type User = { + id: string; + token: string; +}; +export type UserColumn = Pick; +export type UserIndex = UserColumn<"id" | "token">; + +abstract class Query> { + protected readonly database: Database; + private readonly table: string; + + protected constructor(database: Database, table: string) { + this.database = database; + this.table = table; + } + + protected deleteByColumn(column: K, values: Iterable): void { + let defaultValues: Iterable; + if (typeof values !== "object") { + defaultValues = [values]; + } else { + defaultValues = values; + } + + this.database.transaction(() => { + for (const batch of chunk(defaultValues, constant.databaseMaxElements)) { + this.database + .prepare(`DELETE FROM ${this.table} WHERE ${column} IN (${batch.map(() => "?").join(", ")})`, false) + .run(...batch); + } + }); + } + + protected updateByColumn( + whereColumn: K, + whereValue: Table[K], + setColumn: K, + setValue: Table[K] + ): void { + this.database.prepare(`UPDATE ${this.table} SET ${setColumn} = :setValue WHERE ${whereColumn} = :whereValue`).run({ + setValue: setValue, + whereValue: whereValue + }); + } + + protected selectByColumn(column: K, value: Table[K]): Table | undefined { + return this.database.prepare(`SELECT * FROM ${this.table} WHERE ${column} = :value`).get({ + value: value + }) as Table | undefined; + } + + protected selectColumns(columns: K[]): Pick[] { + return this.database.prepare(`SELECT ${columns.join(", ")} FROM ${this.table}`).all() as Pick[]; + } +} + +export class DocumentQuery extends Query { + public constructor(database: Database) { + super(database, "document"); + } + + public create(params: Document): void { + this.database + .prepare( + "INSERT INTO document (id, user_id, version, name, password) VALUES (:id, :user_id, :version, :name, :password)" + ) + .run({ + id: params.id, + user_id: params.user_id, + version: params.version, + name: params.name, + password: params.password + }); + } + + public delete = this.deleteByColumn; + public update = this.updateByColumn; + public get = this.selectByColumn; + public getAll = this.selectColumns; +} + +export class UserQuery extends Query { + public constructor(database: Database) { + super(database, "user"); + } + + public create(id: string = monotonicUlid(), token: string = generateToken()): string { + this.database.prepare("INSERT INTO user (id, token) VALUES (:id, :token)").run({ id: id, token: token }); + return token; + } + + public delete = this.deleteByColumn; + public update = this.updateByColumn; + public get = this.selectByColumn; + + public getDocuments(id: string): DocumentColumn<"id" | "name">[] { + return this.database + .prepare("SELECT document.id, document.name FROM document WHERE document.user_id = :id") + .all({ id: id }) as DocumentColumn<"id" | "name">[]; + } + + public getAll = this.selectColumns; + + public getAllWithoutDocuments(): UserColumn<"id">[] { + return this.database + .prepare(`SELECT user.id FROM user WHERE NOT EXISTS ( + SELECT 1 FROM document WHERE document.user_id = user.id + )`) + .all() as UserColumn<"id">[]; + } +} diff --git a/src/document/compression.ts b/src/document/compression.ts index 92a7e359..94ade5a2 100644 --- a/src/document/compression.ts +++ b/src/document/compression.ts @@ -1,11 +1,10 @@ -import { type InputType, brotliCompressSync, brotliDecompressSync } from 'node:zlib'; - +// node:zlib buffers the stream into memory export const compression = { - encode: (data: InputType): Buffer => { - return brotliCompressSync(data); - }, + encode: (readable: ReadableStream>): ReadableStream => { + return readable.pipeThrough(new CompressionStream("deflate")); + }, - decode: (data: InputType): Buffer => { - return brotliDecompressSync(data); - } + decode: (readable: ReadableStream>): ReadableStream => { + return readable.pipeThrough(new DecompressionStream("deflate")); + } } as const; diff --git a/src/document/crypto.ts b/src/document/crypto.ts deleted file mode 100644 index da680523..00000000 --- a/src/document/crypto.ts +++ /dev/null @@ -1,22 +0,0 @@ -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, 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, salt).update(password); - - const passwordHash = Buffer.concat([salt, hasher.digest()]); - - return hash.every((value, index) => value === passwordHash[index]); - } -} as const; diff --git a/src/document/storage.ts b/src/document/storage.ts index 4310dd55..b05f233f 100644 --- a/src/document/storage.ts +++ b/src/document/storage.ts @@ -1,24 +1,54 @@ -import { deserialize, serialize } from 'bun:jsc'; -import { validator } from '#document/validator.ts'; -import { errorHandler } from '#server/errorHandler.ts'; -import type { Document } from '#type/Document.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { config } from '../config.ts'; +import { constant } from "#/global.ts"; export const storage = { - read: async (name: string): Promise => { - validator.validateName(name); + delete: async (id: string): Promise => { + try { + await Deno.remove(constant.path.struct.storageData + id); + } catch { + // already deleted (probably) + } + }, - const file = Bun.file(config.storagePath + name); + read: async (id: string): Promise => { + return Deno.open(constant.path.struct.storageData + id); + }, - if (!(await file.exists())) { - errorHandler.send(ErrorCode.documentNotFound); - } + write: async (id: string, data: ReadableStream): Promise => { + await using handle = await Deno.open(constant.path.struct.storageData + id, { + createNew: true, + write: true + }); - return deserialize(await file.arrayBuffer()); - }, + await data.pipeTo(handle.writable, { preventClose: true }); + }, - write: async (name: string, document: Document): Promise => { - await Bun.write(config.storagePath + name, serialize(document)); - } + overwrite: async (id: string, data: ReadableStream): Promise => { + await using handle = await Deno.open(constant.path.struct.storageData + id, { + write: true, + truncate: true + }); + + await data.pipeTo(handle.writable, { preventClose: true }); + }, + + // relaxed exists because races between fs/db may ocurr + list: function* (relaxed?: boolean): Iterable { + for (const entry of Deno.readDirSync(constant.path.struct.storageData)) { + if (entry.isFile) { + if (relaxed) { + const info = Deno.statSync(constant.path.struct.storageData + entry.name); + + if ( + info.mtime && + constant.temporal.utc().epochMilliseconds - + info.mtime.toTemporalInstant().toZonedDateTimeISO("Etc/UTC").epochMilliseconds < + 10_000 + ) + continue; + } + + yield entry.name; + } + } + } } as const; diff --git a/src/document/validator.ts b/src/document/validator.ts deleted file mode 100644 index 188d6138..00000000 --- a/src/document/validator.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { crypto } from '#document/crypto.ts'; -import { errorHandler } from '#server/errorHandler.ts'; -import type { Document } from '#type/Document.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { ValidatorUtils } from '#util/ValidatorUtils.ts'; -import { config } from '../config.ts'; - -export const validator = { - validateName: (name: string): void => { - if ( - !ValidatorUtils.isValidBase64URL(name) || - !ValidatorUtils.isLengthWithinRange( - Bun.stringWidth(name), - config.documentNameLengthMin, - config.documentNameLengthMax - ) - ) { - errorHandler.send(ErrorCode.documentInvalidName); - } - }, - - validateNameLength: (length: number | undefined): void => { - if ( - length && - !ValidatorUtils.isLengthWithinRange(length, config.documentNameLengthMin, config.documentNameLengthMax) - ) { - errorHandler.send(ErrorCode.documentInvalidNameLength); - } - }, - - validatePassword: (password: string, dataHash: Document['header']['passwordHash']): void => { - if (dataHash && !crypto.compare(password, dataHash)) { - errorHandler.send(ErrorCode.documentInvalidPassword); - } - }, - - validatePasswordLength: (password: string | undefined): void => { - if ( - password && - (ValidatorUtils.isEmptyString(password) || - !ValidatorUtils.isLengthWithinRange(Bun.stringWidth(password), 1, 255)) - ) { - errorHandler.send(ErrorCode.documentInvalidPasswordLength); - } - }, - - validateSecret: (secret: string, secretHash: Document['header']['secretHash']): void => { - if (!crypto.compare(secret, secretHash)) { - errorHandler.send(ErrorCode.documentInvalidSecret); - } - }, - - validateSecretLength: (secret: string): void => { - if (!ValidatorUtils.isLengthWithinRange(Bun.stringWidth(secret), 1, 255)) { - errorHandler.send(ErrorCode.documentInvalidSecretLength); - } - } -} as const; diff --git a/src/endpoints/document/v1/delete.ts b/src/endpoints/document/v1/delete.ts new file mode 100644 index 00000000..18ba8d8e --- /dev/null +++ b/src/endpoints/document/v1/delete.ts @@ -0,0 +1,59 @@ +import { Hono } from "@hono/hono"; +import { describeRoute, validator } from "@hono/openapi"; +import { type } from "arktype"; +import { constant, mutable } from "#/global.ts"; +import { storage } from "#document/storage.ts"; +import { authMiddleware } from "#http/middleware/authorization.ts"; +import type { Env } from "#http/type.ts"; +import { isOwner } from "#util/document.ts"; +import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { validatorDocumentName } from "#util/validator/document.ts"; +import { validatorHandler } from "#util/validator/handler.ts"; + +const schemaParam = type({ + name: validatorDocumentName +}); + +export default new Hono().delete( + "/:name", + describeRoute({ + tags: ["DOCUMENT (v1)"], + summary: "Delete document", + description: "Deletes a published document in the instance", + security: [{}, { bearer: [] }], + responses: { + 200: { + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] }, + + // auth middleware + 401: { ...genericErrorResponse, description: constant.http[401] } + } + }), + validator("param", schemaParam, validatorHandler), + authMiddleware, + async (ctx) => { + const { + name + // @ts-expect-error upstream + } = ctx.req.valid("param") as typeof schemaParam.infer; + + const document = mutable.database.document.get("name", name); + if (!document?.id) { + return error.throw(ErrorCode.documentNotFound); + } + + const userId = ctx.get("userId"); + const owner = isOwner(userId, document.user_id); + if (!owner) { + return error.throw(ErrorCode.userInvalidToken); + } + + mutable.database.document.delete("name", name); + await storage.delete(document.id); + + return ctx.body(null); + } +); diff --git a/src/endpoints/document/v1/get.ts b/src/endpoints/document/v1/get.ts new file mode 100644 index 00000000..3370918c --- /dev/null +++ b/src/endpoints/document/v1/get.ts @@ -0,0 +1,116 @@ +import { Hono } from "@hono/hono"; +import { stream } from "@hono/hono/streaming"; +import { describeRoute, resolver, validator } from "@hono/openapi"; +import { decodeTime } from "@std/ulid"; +import { type } from "arktype"; +import { constant, mutable } from "#/global.ts"; +import { compression } from "#document/compression.ts"; +import { storage } from "#document/storage.ts"; +import type { Env } from "#http/type.ts"; +import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { + validatorDocumentDownload, + validatorDocumentName, + validatorDocumentPassword +} from "#util/validator/document.ts"; +import { validatorHandler } from "#util/validator/handler.ts"; + +const schemaParam = type({ + name: validatorDocumentName +}); + +const schemaHeader = type({ + "x-jspaste-password?": validatorDocumentPassword +}); + +const schemaQuery = type({ + "dl?": validatorDocumentDownload +}); + +const schemaResponse = resolver(type(type.unknown)); + +export default new Hono().get( + "/:name", + describeRoute({ + tags: ["DOCUMENT (v1)"], + summary: "Get document", + description: `Get the content/metadata of a published document in the instance. + +Note: If you only need to query the document metadata, you should use HEAD method instead`, + responses: { + 200: { + content: { + "text/plain": { + schema: schemaResponse + }, + "application/octet-stream": { + schema: schemaResponse + } + }, + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] } + } + }), + validator("param", schemaParam, validatorHandler), + validator("header", schemaHeader, validatorHandler), + validator("query", schemaQuery, validatorHandler), + async (ctx) => { + const { + name + // @ts-expect-error upstream + } = ctx.req.valid("param") as typeof schemaParam.infer; + const { + "x-jspaste-password": password + // @ts-expect-error upstream + } = ctx.req.valid("header") as typeof schemaHeader.infer; + const { + dl + // @ts-expect-error upstream + } = ctx.req.valid("query") as typeof schemaQuery.infer; + + const document = mutable.database.document.get("name", name); + if (!document?.id) { + return error.throw(ErrorCode.documentNotFound); + } + if (document.password) { + if (!password) { + return error.throw(ErrorCode.documentPasswordNeeded); + } + + if (password !== document.password) { + return error.throw(ErrorCode.documentInvalidPassword); + } + } + + ctx.res.headers.set( + "x-jspaste-created", + Temporal.Instant.fromEpochMilliseconds(decodeTime(document.id)).toString() + ); + + // https://github.com/honojs/hono/issues/1130 + if (ctx.req.method === "HEAD") { + return ctx.body(null); + } + + const fileHandle = await storage.read(document.id); + + let fileContent: ReadableStream; + if (ctx.req.header("accept-encoding")?.includes("deflate")) { + fileContent = fileHandle.readable; + ctx.res.headers.set("content-encoding", "deflate"); + } else { + fileContent = compression.decode(fileHandle.readable); + } + + if (typeof dl !== "undefined") { + ctx.res.headers.set("content-disposition", `attachment; filename="jspaste_${name}"`); + } + + ctx.res.headers.set("content-type", "text/plain"); + ctx.res.headers.set("transfer-encoding", "chunked"); + + return stream(ctx, async (stream) => await stream.pipe(fileContent)); + } +); diff --git a/src/endpoints/document/v1/index.ts b/src/endpoints/document/v1/index.ts new file mode 100644 index 00000000..008712d9 --- /dev/null +++ b/src/endpoints/document/v1/index.ts @@ -0,0 +1,13 @@ +import { Hono } from "@hono/hono"; +import type { Env } from "#http/type.ts"; +import delete_ from "./delete.ts"; +import get from "./get.ts"; +import patch from "./patch.ts"; +import post from "./post.ts"; + +export const v1DocumentRouter = new Hono(); + +v1DocumentRouter.route("/", delete_); +v1DocumentRouter.route("/", get); +v1DocumentRouter.route("/", patch); +v1DocumentRouter.route("/", post); diff --git a/src/endpoints/document/v1/patch.ts b/src/endpoints/document/v1/patch.ts new file mode 100644 index 00000000..94575bdc --- /dev/null +++ b/src/endpoints/document/v1/patch.ts @@ -0,0 +1,121 @@ +import { Hono } from "@hono/hono"; +import { describeRoute, resolver, validator } from "@hono/openapi"; +import { type } from "arktype"; +import { constant, mutable } from "#/global.ts"; +import { compression } from "#document/compression.ts"; +import { storage } from "#document/storage.ts"; +import { authMiddleware } from "#http/middleware/authorization.ts"; +import { bodyCheck } from "#http/middleware/bodyCheck.ts"; +import { bodySize } from "#http/middleware/bodySize.ts"; +import type { Env } from "#http/type.ts"; +import { isOwner } from "#util/document.ts"; +import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; +import { validatorHandler } from "#util/validator/handler.ts"; + +const schemaBody = await resolver( + type.unknown.configure({ + description: "Document content.", + examples: ["Hello, World!"] + }) +).toOpenAPISchema(); + +const schemaParam = type({ + actualName: validatorDocumentName +}); + +const schemaHeader = type({ + "x-jspaste-name?": validatorDocumentName, + "x-jspaste-password?": validatorDocumentPassword +}); + +export default new Hono().patch( + "/:actualName", + describeRoute({ + tags: ["DOCUMENT (v1)"], + summary: "Alter document", + description: `Edit the content/metadata of a published document in the instance + +Note: You can't move the ownership of a document, duplicate the document instead + +Note: To remove (nullify) a value, send the header with an empty value`, + security: [{}, { bearer: [] }], + requestBody: { + content: { + "text/plain": schemaBody, + "application/octet-stream": schemaBody + } + }, + responses: { + 200: { + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] }, + + // auth middleware + 401: { ...genericErrorResponse, description: constant.http[401] }, + + // document name already exists + 409: { ...genericErrorResponse, description: constant.http[409] }, + + // bodyLimit middleware + 413: { ...genericErrorResponse, description: constant.http[413] } + } + }), + validator("param", schemaParam, validatorHandler), + validator("header", schemaHeader, validatorHandler), + authMiddleware, + bodySize, + bodyCheck, + async (ctx) => { + let { + actualName + // @ts-expect-error upstream + } = ctx.req.valid("param") as typeof schemaParam.infer; + const { + "x-jspaste-password": newPassword, + "x-jspaste-name": newName + // @ts-expect-error upstream + } = ctx.req.valid("header") as typeof schemaHeader.infer; + + const document = mutable.database.document.get("name", actualName); + if (!document?.id) { + return error.throw(ErrorCode.documentNotFound); + } + + const userId = ctx.get("userId"); + const owner = isOwner(userId, document.user_id); + if (!owner) { + return error.throw(ErrorCode.userInvalidToken); + } + + if (newPassword !== undefined) { + if (newPassword === "") { + mutable.database.document.update("name", actualName, "password", null); + } else { + mutable.database.document.update("name", actualName, "password", newPassword); + } + } + + // keep newName last thing to alter in case of race conditions + if (newName) { + if (mutable.database.document.get("name", newName)?.name) { + return error.throw(ErrorCode.documentNameAlreadyExists); + } + + mutable.database.document.update("name", actualName, "name", newName); + actualName = newName; + } + + if (ctx.get("hasBody")) { + await storage.overwrite( + document.id, + // ctx.req.raw.body is only null on GET/HEAD + compression.encode(ctx.req.raw.body as NonNullable) + ); + } + + return ctx.body(null); + } +); diff --git a/src/endpoints/document/v1/post.ts b/src/endpoints/document/v1/post.ts new file mode 100644 index 00000000..3dfef49f --- /dev/null +++ b/src/endpoints/document/v1/post.ts @@ -0,0 +1,116 @@ +import { Hono } from "@hono/hono"; +import { describeRoute, resolver, validator } from "@hono/openapi"; +import { monotonicUlid } from "@std/ulid"; +import { type } from "arktype"; +import { constant, mutable } from "#/global.ts"; +import { DocumentVersion } from "#db/query.ts"; +import { compression } from "#document/compression.ts"; +import { storage } from "#document/storage.ts"; +import { authMiddleware } from "#http/middleware/authorization.ts"; +import { bodySize } from "#http/middleware/bodySize.ts"; +import type { Env } from "#http/type.ts"; +import { generateName } from "#util/document.ts"; +import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { + validatorDocumentName, + validatorDocumentNameLength, + validatorDocumentPassword +} from "#util/validator/document.ts"; +import { validatorHandler } from "#util/validator/handler.ts"; + +const schemaBody = await resolver( + type.unknown.configure({ + description: "Document content.", + examples: ["Hello, World!"] + }) +).toOpenAPISchema(); + +const schemaHeader = type({ + "x-jspaste-name-length?": validatorDocumentNameLength, + "x-jspaste-name?": validatorDocumentName, + "x-jspaste-password?": validatorDocumentPassword +}); + +const schemaResponse = resolver( + type({ + name: validatorDocumentName + }) +); + +export default new Hono().post( + "/", + describeRoute({ + tags: ["DOCUMENT (v1)"], + summary: "Post document", + description: "Publish a document to the instance", + security: [{}, { bearer: [] }], + requestBody: { + content: { + "text/plain": schemaBody, + "application/octet-stream": schemaBody + } + }, + responses: { + 200: { + content: { + "application/json": { + schema: schemaResponse + } + }, + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] }, + + // auth middleware + 401: { ...genericErrorResponse, description: constant.http[401] }, + + // document name already exists + 409: { ...genericErrorResponse, description: constant.http[409] }, + + // bodyLimit middleware + 413: { ...genericErrorResponse, description: constant.http[413] } + } + }), + validator("header", schemaHeader, validatorHandler), + authMiddleware, + bodySize, + async (ctx) => { + const { + "x-jspaste-password": password = null, + "x-jspaste-name": name, + "x-jspaste-name-length": nameLength + // @ts-expect-error upstream + } = ctx.req.valid("header") as typeof schemaHeader.infer; + + let setName: string; + if (name) { + if (mutable.database.document.get("name", name)?.name) { + return error.throw(ErrorCode.documentNameAlreadyExists); + } + + setName = name; + } else { + setName = generateName(nameLength); + } + + const setId = monotonicUlid(); + await storage.write( + setId, + // ctx.req.raw.body is only null on GET/HEAD + compression.encode(ctx.req.raw.body as NonNullable) + ); + + mutable.database.document.create({ + id: setId, + user_id: ctx.get("userId") ?? null, + version: DocumentVersion.V1, + name: setName, + password: password + }); + + return ctx.json({ + name: setName + }); + } +); diff --git a/src/endpoints/legacy/v2/documents/access.route.ts b/src/endpoints/legacy/v2/documents/access.route.ts new file mode 100644 index 00000000..7f9b15a1 --- /dev/null +++ b/src/endpoints/legacy/v2/documents/access.route.ts @@ -0,0 +1,99 @@ +import { Hono } from "@hono/hono"; +import { describeRoute, resolver, validator } from "@hono/openapi"; +import { toText } from "@std/streams"; +import { type } from "arktype"; +import { constant, mutable } from "#/global.ts"; +import { compression } from "#document/compression.ts"; +import { storage } from "#document/storage.ts"; +import type { Env } from "#http/type.ts"; +import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; +import { validatorHandler } from "#util/validator/handler.ts"; + +const schemaParam = type({ + name: validatorDocumentName +}); + +const schemaHeader = type({ + "password?": validatorDocumentPassword +}); + +const schemaResponse = resolver( + type({ + key: type.string.configure({ + description: "The document name (formerly key)", + examples: ["abc123"] + }), + data: type.string.configure({ + description: "The document data", + examples: ["Hello, World!"] + }), + url: type.string.configure({ + deprecated: true, + description: "The document URL", + examples: ["https://jspaste.eu/abc123"] + }), + expirationTimestamp: type.number.configure({ + deprecated: true, + description: "The document expiration timestamp (always will be 0)", + examples: [0] + }) + }) +); + +export default new Hono().get( + "/:name", + describeRoute({ + deprecated: true, + tags: ["DOCUMENT (legacy)"], + summary: "Get document", + responses: { + 200: { + content: { + "application/json": { + schema: schemaResponse + } + }, + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] } + } + }), + validator("param", schemaParam, validatorHandler), + validator("header", schemaHeader, validatorHandler), + async (ctx) => { + // https://github.com/honojs/hono/issues/1130 + if (ctx.req.method === "HEAD") { + return ctx.body(null); + } + + // @ts-expect-error upstream + const param = ctx.req.valid("param") as typeof schemaParam.infer; + // @ts-expect-error upstream + const header = ctx.req.valid("header") as typeof schemaHeader.infer; + + const document = mutable.database.document.get("name", param.name); + if (!document?.id) { + return error.throw(ErrorCode.documentNotFound); + } + if (document.password) { + if (!header.password) { + return error.throw(ErrorCode.documentPasswordNeeded); + } + + if (header.password !== document.password) { + return error.throw(ErrorCode.documentInvalidPassword); + } + } + + await using file = await storage.read(document.id); + + return ctx.json({ + key: param.name, + data: await toText(compression.decode(file.readable)), + url: new URL(ctx.req.url).host.concat("/", param.name), + expirationTimestamp: 0 + }); + } +); diff --git a/src/endpoints/legacy/v2/documents/accessRaw.route.ts b/src/endpoints/legacy/v2/documents/accessRaw.route.ts new file mode 100644 index 00000000..c831298e --- /dev/null +++ b/src/endpoints/legacy/v2/documents/accessRaw.route.ts @@ -0,0 +1,98 @@ +import { Hono } from "@hono/hono"; +import { stream } from "@hono/hono/streaming"; +import { describeRoute, resolver, validator } from "@hono/openapi"; +import { type } from "arktype"; +import { constant, mutable } from "#/global.ts"; +import { compression } from "#document/compression.ts"; +import { storage } from "#document/storage.ts"; +import type { Env } from "#http/type.ts"; +import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; +import { validatorHandler } from "#util/validator/handler.ts"; + +const schemaParam = type({ + name: validatorDocumentName +}); + +const schemaHeader = type({ + "password?": validatorDocumentPassword +}); + +const schemaQuery = type({ + "p?": validatorDocumentPassword +}); + +const schemaResponse = resolver(type(type.unknown)); + +export default new Hono().get( + "/:name/raw", + describeRoute({ + deprecated: true, + tags: ["DOCUMENT (legacy)"], + summary: "Get document data", + responses: { + 200: { + content: { + "text/plain": { + schema: schemaResponse + }, + "application/octet-stream": { + schema: schemaResponse + } + }, + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] } + } + }), + validator("param", schemaParam, validatorHandler), + validator("header", schemaHeader, validatorHandler), + validator("query", schemaQuery, validatorHandler), + async (ctx) => { + // https://github.com/honojs/hono/issues/1130 + if (ctx.req.method === "HEAD") { + return ctx.body(null); + } + + // @ts-expect-error upstream + const param = ctx.req.valid("param") as typeof schemaParam.infer; + // @ts-expect-error upstream + const header = ctx.req.valid("header") as typeof schemaHeader.infer; + // @ts-expect-error upstream + const query = ctx.req.valid("query") as typeof schemaQuery.infer; + const options = { + password: header.password || query.p + }; + + const document = mutable.database.document.get("name", param.name); + if (!document?.id) { + return error.throw(ErrorCode.documentNotFound); + } + if (document.password) { + if (!options.password) { + return error.throw(ErrorCode.documentPasswordNeeded); + } + + if (options.password !== document.password) { + return error.throw(ErrorCode.documentInvalidPassword); + } + } + + const file = await storage.read(document.id); + + let streamData: ReadableStream; + if (ctx.req.header("Accept-Encoding")?.includes("deflate")) { + streamData = file.readable; + ctx.res.headers.set("Content-Encoding", "deflate"); + } else { + streamData = compression.decode(file.readable); + } + + ctx.res.headers.append("Cache-Control", "no-cache"); + ctx.res.headers.set("Content-Type", "text/plain"); + ctx.res.headers.set("Transfer-Encoding", "chunked"); + + return stream(ctx, async (stream) => await stream.pipe(streamData)); + } +); diff --git a/src/endpoints/legacy/v2/documents/edit.route.ts b/src/endpoints/legacy/v2/documents/edit.route.ts new file mode 100644 index 00000000..a1c70097 --- /dev/null +++ b/src/endpoints/legacy/v2/documents/edit.route.ts @@ -0,0 +1,80 @@ +import { Hono } from "@hono/hono"; +import { describeRoute, resolver, validator } from "@hono/openapi"; +import { type } from "arktype"; +import { constant, mutable } from "#/global.ts"; +import { compression } from "#document/compression.ts"; +import { storage } from "#document/storage.ts"; +import { bodySize } from "#http/middleware/bodySize.ts"; +import type { Env } from "#http/type.ts"; +import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { validatorDocumentName } from "#util/validator/document.ts"; +import { validatorHandler } from "#util/validator/handler.ts"; + +const schemaParam = type({ + name: validatorDocumentName +}); + +const schemaRequest = await resolver( + type( + type.string.configure({ + description: "Data to replace in the document", + examples: ["Hello world!"] + }) + ) +).toOpenAPISchema(); + +const schemaResponse = resolver( + type({ + edited: type.boolean.configure({ + description: "Confirmation of edition", + examples: [true] + }) + }) +); + +export default new Hono().patch( + "/:name", + describeRoute({ + deprecated: true, + tags: ["DOCUMENT (legacy)"], + summary: "Edit document", + requestBody: { + content: { + "text/plain": schemaRequest + } + }, + responses: { + 200: { + content: { + "application/json": { + schema: schemaResponse + } + }, + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] } + } + }), + validator("param", schemaParam, validatorHandler), + bodySize, + async (ctx) => { + // @ts-expect-error upstream + const param = ctx.req.valid("param") as typeof schemaParam.infer; + + const document = mutable.database.document.get("name", param.name); + if (!document?.id || document.user_id) { + return error.throw(ErrorCode.documentNotFound); + } + + await storage.write( + document.id, + // ctx.req.raw.body is only null on GET/HEAD + compression.encode(ctx.req.raw.body as NonNullable) + ); + + return ctx.json({ + edited: true + }); + } +); diff --git a/src/endpoints/legacy/v2/documents/exists.route.ts b/src/endpoints/legacy/v2/documents/exists.route.ts new file mode 100644 index 00000000..2d4795da --- /dev/null +++ b/src/endpoints/legacy/v2/documents/exists.route.ts @@ -0,0 +1,47 @@ +import { Hono } from "@hono/hono"; +import { describeRoute, resolver, validator } from "@hono/openapi"; +import { type } from "arktype"; +import { constant, mutable } from "#/global.ts"; +import type { Env } from "#http/type.ts"; +import { genericErrorResponse } from "#util/error.ts"; +import { validatorDocumentName } from "#util/validator/document.ts"; +import { validatorHandler } from "#util/validator/handler.ts"; + +const schemaParam = type({ + name: validatorDocumentName +}); + +const schemaResponse = resolver(type(type.boolean)); + +export default new Hono().get( + "/:name/exists", + describeRoute({ + deprecated: true, + tags: ["DOCUMENT (legacy)"], + summary: "Check document", + responses: { + 200: { + content: { + "text/plain": { + schema: schemaResponse + } + }, + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] } + } + }), + validator("param", schemaParam, validatorHandler), + (ctx) => { + // https://github.com/honojs/hono/issues/1130 + if (ctx.req.method === "HEAD") { + return ctx.body(null); + } + + // @ts-expect-error upstream + const param = ctx.req.valid("param") as typeof schemaParam.infer; + + return ctx.text(mutable.database.document.get("name", param.name)?.name ? "true" : "false"); + } +); diff --git a/src/endpoints/legacy/v2/documents/index.ts b/src/endpoints/legacy/v2/documents/index.ts new file mode 100644 index 00000000..0f563c3c --- /dev/null +++ b/src/endpoints/legacy/v2/documents/index.ts @@ -0,0 +1,17 @@ +import { Hono } from "@hono/hono"; +import type { Env } from "#http/type.ts"; +import access from "./access.route.ts"; +import accessRaw from "./accessRaw.route.ts"; +import edit from "./edit.route.ts"; +import exists from "./exists.route.ts"; +import publish from "./publish.route.ts"; +import remove from "./remove.route.ts"; + +export const v2LegacyDocumentRouter = new Hono(); + +v2LegacyDocumentRouter.route("/", access); +v2LegacyDocumentRouter.route("/", accessRaw); +v2LegacyDocumentRouter.route("/", edit); +v2LegacyDocumentRouter.route("/", exists); +v2LegacyDocumentRouter.route("/", publish); +v2LegacyDocumentRouter.route("/", remove); diff --git a/src/endpoints/legacy/v2/documents/publish.route.ts b/src/endpoints/legacy/v2/documents/publish.route.ts new file mode 100644 index 00000000..d46824bb --- /dev/null +++ b/src/endpoints/legacy/v2/documents/publish.route.ts @@ -0,0 +1,109 @@ +import { Hono } from "@hono/hono"; +import { describeRoute, resolver, validator } from "@hono/openapi"; +import { monotonicUlid } from "@std/ulid"; +import { type } from "arktype"; +import { constant, mutable } from "#/global.ts"; +import { DocumentVersion } from "#db/query.ts"; +import { compression } from "#document/compression.ts"; +import { storage } from "#document/storage.ts"; +import { bodySize } from "#http/middleware/bodySize.ts"; +import type { Env } from "#http/type.ts"; +import { generateName } from "#util/document.ts"; +import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; +import { validatorHandler } from "#util/validator/handler.ts"; + +const schemaHeader = type({ + "password?": validatorDocumentPassword, + "key?": validatorDocumentName, + "keylength?": type.number.atLeast(constant.documentNameLengthMin).atMost(constant.documentNameLengthMax).configure({ + description: "The document name length" + }) +}); + +const schemaBody = await resolver( + type( + type.string.configure({ + description: "Data to replace in the document", + examples: ["Hello world!"] + }) + ) +).toOpenAPISchema(); + +const schemaResponse = resolver( + type({ + key: type.string.configure({ + description: "The document name (formerly key)", + examples: ["abc123"] + }) + }) +); + +export default new Hono().post( + "/", + describeRoute({ + deprecated: true, + tags: ["DOCUMENT (legacy)"], + summary: "Publish document", + requestBody: { + content: { + "text/plain": schemaBody + } + }, + responses: { + 200: { + content: { + "application/json": { + schema: schemaResponse + } + }, + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] } + } + }), + validator("header", schemaHeader, validatorHandler), + bodySize, + async (ctx) => { + const { + password = null, + key: name, + keylength: nameLength + // @ts-expect-error upstream + } = ctx.req.valid("header") as typeof schemaHeader.infer; + + let setName: string; + if (name) { + if (mutable.database.document.get("name", name)?.name) { + return error.throw(ErrorCode.documentNameAlreadyExists); + } + + setName = name; + } else { + setName = generateName(nameLength); + } + + const id = monotonicUlid(); + + await storage.write( + id, + // ctx.req.raw.body is only null on GET/HEAD + compression.encode(ctx.req.raw.body as NonNullable) + ); + mutable.database.document.create({ + id: id, + user_id: null, + version: DocumentVersion.V1, + name: setName, + password: password + }); + + return ctx.json({ + key: setName, + secret: "", + url: new URL(ctx.req.url).host.concat("/", setName), + expirationTimestamp: 0 + }); + } +); diff --git a/src/endpoints/legacy/v2/documents/remove.route.ts b/src/endpoints/legacy/v2/documents/remove.route.ts new file mode 100644 index 00000000..214f2c81 --- /dev/null +++ b/src/endpoints/legacy/v2/documents/remove.route.ts @@ -0,0 +1,58 @@ +import { Hono } from "@hono/hono"; +import { describeRoute, resolver, validator } from "@hono/openapi"; +import { type } from "arktype"; +import { constant, mutable } from "#/global.ts"; +import { storage } from "#document/storage.ts"; +import type { Env } from "#http/type.ts"; +import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { validatorDocumentName } from "#util/validator/document.ts"; +import { validatorHandler } from "#util/validator/handler.ts"; + +const schemaParam = type({ + name: validatorDocumentName +}); + +const schemaResponse = resolver( + type({ + removed: type.true.configure({ + description: "Confirmation of deletion", + examples: [true] + }) + }) +); + +export default new Hono().delete( + "/:name", + describeRoute({ + deprecated: true, + tags: ["DOCUMENT (legacy)"], + summary: "Remove document", + responses: { + 200: { + content: { + "application/json": { + schema: schemaResponse + } + }, + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] } + } + }), + validator("param", schemaParam, validatorHandler), + async (ctx) => { + // @ts-expect-error upstream + const param = ctx.req.valid("param") as typeof schemaParam.infer; + + const document = mutable.database.document.get("name", param.name); + if (!document?.id || document.user_id) { + return error.throw(ErrorCode.documentNotFound); + } + + await storage.delete(document.id); + mutable.database.document.delete("name", param.name); + + return ctx.json({ removed: true }); + } +); diff --git a/src/endpoints/user/v1/create.ts b/src/endpoints/user/v1/create.ts new file mode 100644 index 00000000..2cba0423 --- /dev/null +++ b/src/endpoints/user/v1/create.ts @@ -0,0 +1,49 @@ +import { Hono } from "@hono/hono"; +import { describeRoute, resolver } from "@hono/openapi"; +import { type } from "arktype"; +import { constant, mutable } from "#/global.ts"; +import type { Env } from "#http/type.ts"; +import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { validatorUserToken } from "#util/validator/user.ts"; +import { authMiddleware } from "../../../http/middleware/authorization.ts"; + +const schemaResponse = resolver( + type({ + token: validatorUserToken + }) +); + +export default new Hono().post( + "/", + describeRoute({ + tags: ["USER (v1)"], + summary: "Create user", + description: "Create a user to the instance", + security: [{}, { bearer: [] }], + responses: { + 200: { + content: { + "application/json": { + schema: schemaResponse + } + }, + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] }, + + // auth middleware + 401: { ...genericErrorResponse, description: constant.http[401] } + } + }), + authMiddleware, + async (ctx) => { + if (!constant.env.JSPB_USER_REGISTER && ctx.get("userId") !== constant.ulid.userRoot) { + return error.throw(ErrorCode.userInvalidToken); + } + + return ctx.json({ + token: mutable.database.user.create() + }); + } +); diff --git a/src/endpoints/user/v1/index.ts b/src/endpoints/user/v1/index.ts new file mode 100644 index 00000000..4c089455 --- /dev/null +++ b/src/endpoints/user/v1/index.ts @@ -0,0 +1,7 @@ +import { Hono } from "@hono/hono"; +import type { Env } from "#http/type.ts"; +import create from "./create.ts"; + +export const v1UserRouter = new Hono(); + +v1UserRouter.route("/", create); diff --git a/src/endpoints/v1/access.route.ts b/src/endpoints/v1/access.route.ts deleted file mode 100644 index ef91dc36..00000000 --- a/src/endpoints/v1/access.route.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; -import { compression } from '#document/compression.ts'; -import { storage } from '#document/storage.ts'; -import { errorHandler, schema } from '#server/errorHandler.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { config } from '../../config.ts'; - -export const accessRoute = (endpoint: OpenAPIHono): void => { - const route = createRoute({ - method: 'get', - path: '/{name}', - tags: ['v1'], - summary: 'Get document', - deprecated: true, - request: { - params: z.object({ - name: z.string().min(config.documentNameLengthMin).max(config.documentNameLengthMax).openapi({ - description: 'The document name', - example: 'abc123' - }) - }) - }, - responses: { - 200: { - content: { - 'application/json': { - schema: z.object({ - key: z.string().openapi({ - description: 'The document name (formerly key)', - example: 'abc123' - }), - data: z.string().openapi({ - description: 'The document data', - example: 'Hello, World!' - }) - }) - } - }, - description: 'The document object' - }, - 400: schema, - 404: schema, - 500: schema - } - }); - - endpoint.openapi( - route, - async (ctx) => { - const params = ctx.req.valid('param'); - - const document = await storage.read(params.name); - - // V1 Endpoint does not support document protected password - if (document.header.passwordHash) { - errorHandler.send(ErrorCode.documentPasswordNeeded); - } - - const buffer = compression.decode(document.data); - - return ctx.json({ - key: params.name, - data: buffer.toString('binary') - }); - }, - (result) => { - if (!result.success) { - return errorHandler.send(ErrorCode.validation); - } - } - ); -}; diff --git a/src/endpoints/v1/accessRaw.route.ts b/src/endpoints/v1/accessRaw.route.ts deleted file mode 100644 index 0df2ac83..00000000 --- a/src/endpoints/v1/accessRaw.route.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; -import { compression } from '#document/compression.ts'; -import { storage } from '#document/storage.ts'; -import { errorHandler, schema } from '#server/errorHandler.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { config } from '../../config.ts'; - -export const accessRawRoute = (endpoint: OpenAPIHono): void => { - const route = createRoute({ - method: 'get', - path: '/{name}/raw', - tags: ['v1'], - summary: 'Get document data', - deprecated: true, - request: { - params: z.object({ - name: z.string().min(config.documentNameLengthMin).max(config.documentNameLengthMax).openapi({ - description: 'The document name', - example: 'abc123' - }) - }) - }, - responses: { - 200: { - content: { - 'text/plain': { - schema: z.any().openapi({ - description: 'The document data' - }), - example: 'Hello, World!' - } - }, - description: 'The document data' - }, - 400: schema, - 404: schema, - 500: schema - } - }); - - endpoint.openapi( - route, - async (ctx) => { - const params = ctx.req.valid('param'); - - const document = await storage.read(params.name); - - // 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(compression.decode(document.data)); - }, - (result) => { - if (!result.success) { - return errorHandler.send(ErrorCode.validation); - } - } - ); -}; diff --git a/src/endpoints/v1/index.ts b/src/endpoints/v1/index.ts deleted file mode 100644 index 6a5aa2a9..00000000 --- a/src/endpoints/v1/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { OpenAPIHono } from '@hono/zod-openapi'; -import { accessRoute } from './access.route.ts'; -import { accessRawRoute } from './accessRaw.route.ts'; -import { publishRoute } from './publish.route.ts'; -import { removeRoute } from './remove.route.ts'; - -export const v1 = (): typeof endpoint => { - const endpoint = new OpenAPIHono(); - - endpoint.get('/', (ctx) => { - return ctx.text('Welcome to JSPaste API v1'); - }); - - accessRoute(endpoint); - accessRawRoute(endpoint); - publishRoute(endpoint); - removeRoute(endpoint); - - return endpoint; -}; diff --git a/src/endpoints/v1/publish.route.ts b/src/endpoints/v1/publish.route.ts deleted file mode 100644 index a9fa3d67..00000000 --- a/src/endpoints/v1/publish.route.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; -import { compression } from '#document/compression.ts'; -import { crypto } from '#document/crypto.ts'; -import { storage } from '#document/storage.ts'; -import { errorHandler, schema } from '#server/errorHandler.ts'; -import { middleware } from '#server/middleware.ts'; -import { DocumentVersion } from '#type/Document.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { StringUtils } from '#util/StringUtils.ts'; - -export const publishRoute = (endpoint: OpenAPIHono): void => { - const route = createRoute({ - method: 'post', - path: '/', - tags: ['v1'], - summary: 'Publish document', - deprecated: true, - middleware: [middleware.bodyLimit()], - request: { - body: { - content: { - 'text/plain': { - schema: z.string().openapi({ - description: 'Data to publish in the document', - example: 'Hello, World!' - }) - } - } - } - }, - responses: { - 200: { - content: { - 'application/json': { - schema: z.object({ - key: z.string().openapi({ - description: 'The document name (formerly key)', - example: 'abc123' - }), - secret: z.string().openapi({ - description: 'The document secret', - example: 'aaaaa-bbbbb-ccccc-ddddd' - }) - }) - } - }, - description: 'An object with a "name" and "secret" parameters of the created document' - }, - 400: schema, - 404: schema, - 500: schema - } - }); - - endpoint.openapi( - route, - async (ctx) => { - const body = await ctx.req.arrayBuffer(); - const name = await StringUtils.createName(); - const secret = StringUtils.createSecret(); - - await storage.write(name, { - data: compression.encode(body), - header: { - name: name, - secretHash: crypto.hash(secret), - passwordHash: null - }, - version: DocumentVersion.V1 - }); - - return ctx.json({ key: name, secret: secret }); - }, - (result) => { - if (!result.success) { - return errorHandler.send(ErrorCode.validation); - } - } - ); -}; diff --git a/src/endpoints/v1/remove.route.ts b/src/endpoints/v1/remove.route.ts deleted file mode 100644 index 153ab2e7..00000000 --- a/src/endpoints/v1/remove.route.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { unlink } from 'node:fs/promises'; -import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; -import { storage } from '#document/storage.ts'; -import { validator } from '#document/validator.ts'; -import { errorHandler, schema } from '#server/errorHandler.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { config } from '../../config.ts'; - -export const removeRoute = (endpoint: OpenAPIHono): void => { - const route = createRoute({ - method: 'delete', - path: '/{name}', - tags: ['v1'], - summary: 'Remove document', - deprecated: true, - request: { - params: z.object({ - name: z.string().min(config.documentNameLengthMin).max(config.documentNameLengthMax).openapi({ - description: 'The document name', - example: 'abc123' - }) - }), - headers: z.object({ - secret: z.string().min(1).openapi({ - description: 'The document secret', - example: 'aaaaa-bbbbb-ccccc-ddddd' - }) - }) - }, - responses: { - 200: { - content: { - 'application/json': { - schema: z.object({ - removed: z.boolean().openapi({ - description: 'Confirmation of deletion', - example: true - }) - }) - } - }, - description: 'An object with a "removed" parameter of the deleted document' - }, - 400: schema, - 404: schema, - 500: schema - } - }); - - endpoint.openapi( - route, - async (ctx) => { - const params = ctx.req.valid('param'); - const headers = ctx.req.valid('header'); - - const document = await storage.read(params.name); - - validator.validateSecret(headers.secret, document.header.secretHash); - - const result = await unlink(config.storagePath + params.name) - .then(() => true) - .catch(() => false); - - return ctx.json({ removed: result }); - }, - (result) => { - if (!result.success) { - return errorHandler.send(ErrorCode.validation); - } - } - ); -}; diff --git a/src/endpoints/v2/access.route.ts b/src/endpoints/v2/access.route.ts deleted file mode 100644 index 7b940768..00000000 --- a/src/endpoints/v2/access.route.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; -import { compression } from '#document/compression.ts'; -import { storage } from '#document/storage.ts'; -import { validator } from '#document/validator.ts'; -import { errorHandler, schema } from '#server/errorHandler.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { config } from '../../config.ts'; - -export const accessRoute = (endpoint: OpenAPIHono): void => { - const route = createRoute({ - method: 'get', - path: '/{name}', - tags: ['v2'], - summary: 'Get document', - request: { - params: z.object({ - name: z.string().min(config.documentNameLengthMin).max(config.documentNameLengthMax).openapi({ - description: 'The document name', - example: 'abc123' - }) - }), - headers: z.object({ - password: z.string().optional().openapi({ - description: 'The password to access the document', - example: 'aabbccdd11223344' - }) - }) - }, - responses: { - 200: { - content: { - 'application/json': { - schema: z.object({ - key: z.string().openapi({ - description: 'The document name (formerly key)', - example: 'abc123' - }), - data: z.string().openapi({ - description: 'The document data', - example: 'Hello, World!' - }), - url: z.string().openapi({ - description: 'The document URL', - example: 'https://jspaste.eu/abc123' - }), - expirationTimestamp: z.number().openapi({ - deprecated: true, - description: 'The document expiration timestamp (always will be 0)', - example: 0 - }) - }) - } - }, - description: 'The document object' - }, - 400: schema, - 404: schema, - 500: schema - } - }); - - endpoint.openapi( - route, - async (ctx) => { - const params = ctx.req.valid('param'); - const headers = ctx.req.valid('header'); - - const document = await storage.read(params.name); - - if (document.header.passwordHash) { - if (!headers.password) { - return errorHandler.send(ErrorCode.documentPasswordNeeded); - } - - validator.validatePassword(headers.password, document.header.passwordHash); - } - - const buffer = compression.decode(document.data); - - return ctx.json({ - key: params.name, - data: buffer.toString('binary'), - url: config.protocol.concat(new URL(ctx.req.url).host.concat('/', params.name)), - expirationTimestamp: 0 - }); - }, - (result) => { - if (!result.success) { - return errorHandler.send(ErrorCode.validation); - } - } - ); -}; diff --git a/src/endpoints/v2/accessRaw.route.ts b/src/endpoints/v2/accessRaw.route.ts deleted file mode 100644 index 75171802..00000000 --- a/src/endpoints/v2/accessRaw.route.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; -import { compression } from '#document/compression.ts'; -import { storage } from '#document/storage.ts'; -import { validator } from '#document/validator.ts'; -import { errorHandler, schema } from '#server/errorHandler.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { config } from '../../config.ts'; - -export const accessRawRoute = (endpoint: OpenAPIHono): void => { - const route = createRoute({ - method: 'get', - path: '/{name}/raw', - tags: ['v2'], - summary: 'Get document data', - request: { - params: z.object({ - name: z.string().min(config.documentNameLengthMin).max(config.documentNameLengthMax).openapi({ - description: 'The document name', - example: 'abc123' - }) - }), - headers: z.object({ - password: z.string().optional().openapi({ - description: 'The password to access the document', - example: 'aabbccdd11223344' - }) - }), - query: z.object({ - p: z.string().optional().openapi({ - description: - 'The password to decrypt the document. It is preferred to pass the password through headers, only use this method for support of web browsers.', - example: 'aabbccdd11223344' - }) - }) - }, - responses: { - 200: { - content: { - 'text/plain': { - schema: z.any().openapi({ - description: 'The document data' - }), - example: 'Hello, World!' - } - }, - description: 'The document data' - }, - 400: schema, - 404: schema, - 500: schema - } - }); - - endpoint.openapi( - route, - async (ctx) => { - const params = ctx.req.valid('param'); - const headers = ctx.req.valid('header'); - const query = ctx.req.valid('query'); - - const options = { - password: headers.password || query.p - }; - - const document = await storage.read(params.name); - - if (document.header.passwordHash) { - if (!options.password) { - return errorHandler.send(ErrorCode.documentPasswordNeeded); - } - - validator.validatePassword(options.password, document.header.passwordHash); - } - - // @ts-ignore: Return the buffer directly - return ctx.text(compression.decode(document.data)); - }, - (result) => { - if (!result.success) { - return errorHandler.send(ErrorCode.validation); - } - } - ); -}; diff --git a/src/endpoints/v2/edit.route.ts b/src/endpoints/v2/edit.route.ts deleted file mode 100644 index bdcb7e8e..00000000 --- a/src/endpoints/v2/edit.route.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; -import { compression } from '#document/compression.ts'; -import { storage } from '#document/storage.ts'; -import { validator } from '#document/validator.ts'; -import { errorHandler, schema } from '#server/errorHandler.ts'; -import { middleware } from '#server/middleware.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { config } from '../../config.ts'; - -export const editRoute = (endpoint: OpenAPIHono): void => { - const route = createRoute({ - method: 'patch', - path: '/{name}', - tags: ['v2'], - summary: 'Edit document', - middleware: [middleware.bodyLimit()], - request: { - body: { - content: { - 'text/plain': { - schema: z.string().openapi({ - description: 'Data to replace in the document', - example: 'Hello, World!' - }) - } - } - }, - params: z.object({ - name: z.string().min(config.documentNameLengthMin).max(config.documentNameLengthMax).openapi({ - description: 'The document name', - example: 'abc123' - }) - }), - headers: z.object({ - password: z.string().optional().openapi({ - deprecated: true, - description: 'The password to access the document (not used anymore)', - example: 'aabbccdd11223344' - }), - secret: z.string().openapi({ - description: 'The document secret', - example: 'aaaaa-bbbbb-ccccc-ddddd' - }) - }) - }, - responses: { - 200: { - content: { - 'application/json': { - schema: z.object({ - edited: z.boolean().openapi({ - description: 'Confirmation of edition', - example: true - }) - }) - } - }, - description: 'Confirmation of edition' - }, - 400: schema, - 404: schema, - 500: schema - } - }); - - endpoint.openapi( - route, - async (ctx) => { - const body = await ctx.req.arrayBuffer(); - const params = ctx.req.valid('param'); - const headers = ctx.req.valid('header'); - - const document = await storage.read(params.name); - - validator.validateSecret(headers.secret, document.header.secretHash); - - document.data = compression.encode(body); - - const result = await storage - .write(params.name, document) - .then(() => true) - .catch(() => false); - - return ctx.json({ - edited: result - }); - }, - (result) => { - if (!result.success) { - return errorHandler.send(ErrorCode.validation); - } - } - ); -}; diff --git a/src/endpoints/v2/exists.route.ts b/src/endpoints/v2/exists.route.ts deleted file mode 100644 index 7d853afd..00000000 --- a/src/endpoints/v2/exists.route.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; -import { validator } from '#document/validator.ts'; -import { errorHandler, schema } from '#server/errorHandler.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { config } from '../../config.ts'; - -export const existsRoute = (endpoint: OpenAPIHono): void => { - const route = createRoute({ - method: 'get', - path: '/{name}/exists', - tags: ['v2'], - summary: 'Check document', - request: { - params: z.object({ - name: z.string().min(config.documentNameLengthMin).max(config.documentNameLengthMax).openapi({ - description: 'The document name', - example: 'abc123' - }) - }) - }, - responses: { - 200: { - content: { - 'text/plain': { - schema: z.string().openapi({ - description: 'The document existence result' - }), - examples: { - true: { - summary: 'Document exists', - value: 'true' - }, - false: { - summary: 'Document does not exist', - value: 'false' - } - } - } - }, - description: 'The document existence result' - }, - 400: schema, - 404: schema, - 500: schema - } - }); - - endpoint.openapi( - route, - async (ctx) => { - const params = ctx.req.valid('param'); - - validator.validateName(params.name); - - return ctx.text(String(await Bun.file(config.storagePath + params.name).exists())); - }, - (result) => { - if (!result.success) { - return errorHandler.send(ErrorCode.validation); - } - } - ); -}; diff --git a/src/endpoints/v2/index.ts b/src/endpoints/v2/index.ts deleted file mode 100644 index 4354ef4d..00000000 --- a/src/endpoints/v2/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { OpenAPIHono } from '@hono/zod-openapi'; -import { accessRoute } from './access.route.ts'; -import { accessRawRoute } from './accessRaw.route.ts'; -import { editRoute } from './edit.route.ts'; -import { existsRoute } from './exists.route.ts'; -import { publishRoute } from './publish.route.ts'; -import { removeRoute } from './remove.route.ts'; - -export const v2 = (): typeof endpoint => { - const endpoint = new OpenAPIHono(); - - endpoint.get('/', (ctx) => { - return ctx.text('Welcome to JSPaste API v2'); - }); - - accessRoute(endpoint); - accessRawRoute(endpoint); - editRoute(endpoint); - existsRoute(endpoint); - publishRoute(endpoint); - removeRoute(endpoint); - - return endpoint; -}; diff --git a/src/endpoints/v2/publish.route.ts b/src/endpoints/v2/publish.route.ts deleted file mode 100644 index f894f48e..00000000 --- a/src/endpoints/v2/publish.route.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; -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 { errorHandler, schema } from '#server/errorHandler.ts'; -import { middleware } from '#server/middleware.ts'; -import { DocumentVersion } from '#type/Document.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { StringUtils } from '#util/StringUtils.ts'; -import { config } from '../../config.ts'; - -export const publishRoute = (endpoint: OpenAPIHono): void => { - const route = createRoute({ - method: 'post', - path: '/', - tags: ['v2'], - summary: 'Publish document', - middleware: [middleware.bodyLimit()], - request: { - body: { - content: { - 'text/plain': { - schema: z.string().openapi({ - description: 'Data to publish in the document', - example: 'Hello, World!' - }) - } - } - }, - headers: z.object({ - password: z.string().optional().openapi({ - description: 'The password to restrict the document', - example: 'aabbccdd11223344' - }), - key: z.string().optional().openapi({ - description: 'The document name (formerly key)', - example: 'abc123' - }), - keylength: z.string().optional().openapi({ - description: 'The document name length (formerly key length)', - example: config.documentNameLengthDefault.toString() - }), - secret: z.string().optional().openapi({ - description: 'The document secret', - example: 'aaaaa-bbbbb-ccccc-ddddd' - }) - }) - }, - responses: { - 200: { - content: { - 'application/json': { - schema: z.object({ - key: z.string().openapi({ - description: 'The document name (formerly key)', - example: 'abc123' - }), - secret: z.string().openapi({ - description: 'The document secret', - example: 'aaaaa-bbbbb-ccccc-ddddd' - }), - url: z.string().openapi({ - description: 'The document URL', - example: 'https://jspaste.eu/abc123' - }), - expirationTimestamp: z.number().openapi({ - deprecated: true, - description: 'The document expiration timestamp (always will be 0)', - example: 0 - }) - }) - } - }, - description: 'An object with a "key", "secret" and "url" parameters of the created document' - }, - 400: schema, - 404: schema, - 500: schema - } - }); - - endpoint.openapi( - route, - async (ctx) => { - const body = await ctx.req.arrayBuffer(); - const headers = ctx.req.valid('header'); - - if (headers.password) { - validator.validatePasswordLength(headers.password); - } - - let secret: string; - - if (headers.secret) { - validator.validateSecretLength(headers.secret); - - secret = headers.secret; - } else { - secret = StringUtils.createSecret(); - } - - let name: string; - - if (headers.key) { - validator.validateName(headers.key); - - if (await StringUtils.nameExists(headers.key)) { - errorHandler.send(ErrorCode.documentNameAlreadyExists); - } - - name = headers.key; - } else { - const nameLength = Number(headers.keylength || config.documentNameLengthDefault); - - name = await StringUtils.createName(nameLength); - } - - const data = compression.encode(body); - - await storage.write(name, { - data: data, - header: { - name: name, - secretHash: crypto.hash(secret), - passwordHash: headers.password ? crypto.hash(headers.password) : null - }, - version: DocumentVersion.V1 - }); - - return ctx.json({ - key: name, - secret: secret, - url: config.protocol.concat(new URL(ctx.req.url).host.concat('/', name)), - expirationTimestamp: 0 - }); - }, - (result) => { - if (!result.success) { - return errorHandler.send(ErrorCode.validation); - } - } - ); -}; diff --git a/src/endpoints/v2/remove.route.ts b/src/endpoints/v2/remove.route.ts deleted file mode 100644 index 49587291..00000000 --- a/src/endpoints/v2/remove.route.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { unlink } from 'node:fs/promises'; -import { type OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; -import { storage } from '#document/storage.ts'; -import { validator } from '#document/validator.ts'; -import { errorHandler, schema } from '#server/errorHandler.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { config } from '../../config.ts'; - -export const removeRoute = (endpoint: OpenAPIHono): void => { - const route = createRoute({ - method: 'delete', - path: '/{name}', - tags: ['v2'], - summary: 'Remove document', - request: { - params: z.object({ - name: z.string().min(config.documentNameLengthMin).max(config.documentNameLengthMax).openapi({ - description: 'The document name', - example: 'abc123' - }) - }), - headers: z.object({ - secret: z.string().min(1).openapi({ - description: 'The document secret', - example: 'aaaaa-bbbbb-ccccc-ddddd' - }) - }) - }, - responses: { - 200: { - content: { - 'application/json': { - schema: z.object({ - removed: z.boolean().openapi({ - description: 'Confirmation of deletion', - example: true - }) - }) - } - }, - description: 'An object with a "removed" parameter of the deleted document' - }, - 400: schema, - 404: schema, - 500: schema - } - }); - - endpoint.openapi( - route, - async (ctx) => { - const params = ctx.req.valid('param'); - const headers = ctx.req.valid('header'); - - const document = await storage.read(params.name); - - validator.validateSecret(headers.secret, document.header.secretHash); - - const result = await unlink(config.storagePath + params.name) - .then(() => true) - .catch(() => false); - - return ctx.json({ removed: result }); - }, - (result) => { - if (!result.success) { - return errorHandler.send(ErrorCode.validation); - } - } - ); -}; diff --git a/src/global.ts b/src/global.ts new file mode 100644 index 00000000..e4a175b3 --- /dev/null +++ b/src/global.ts @@ -0,0 +1,89 @@ +import { STATUS_CODES } from "node:http"; +import type { StatementSync } from "node:sqlite"; +import type { StatusCode } from "@hono/hono/utils/http-status"; +import { LruCache } from "@std/cache"; +import env from "arkenv"; +import { type } from "arktype"; +import { customAlphabet } from "nanoid"; +import type { Database } from "#db/database"; +import { humanizeSize, humanizeTime } from "#util/humanize.ts"; +import { IPQ } from "#util/ipq.ts"; + +export const mutable = { + database: undefined as unknown as Database, + http: undefined as Deno.HttpServer | undefined, + shutdown: false +}; + +export const constant = { + databaseMaxElements: 10_000, + documentNameLengthDefault: 8, + documentNameLengthMax: 32, + documentNameLengthMin: 2, + documentPasswordLengthMax: 128, + documentPasswordLengthMin: 2, + userTokenLengthDefault: 16, + userTokenLengthMax: 64, + userTokenLengthMin: 16, + env: env( + { + JSPB_LOG_VERBOSITY: type.keywords.number.integer.atLeast(0).atMost(4).default(3), + JSPB_LOG_TIME: type.boolean.default(true), + JSPB_HOSTNAME: type.keywords.string.ip.root + .pipe((hostname) => { + return { + isIPv6: hostname.includes(":"), + root: hostname + }; + }) + .default("::"), + JSPB_PORT: type.keywords.number.integer.atLeast(0).atMost(65_535).default(4000), + + // debug + JSPB_DEBUG_DATABASE_EPHEMERAL: type.boolean.default(false), + + // document + JSPB_DOCUMENT_SIZE: type.string.pipe(humanizeSize).default("1mb"), + JSPB_DOCUMENT_AGE: type.string.pipe(humanizeTime).default("0"), + JSPB_DOCUMENT_ANONYMOUS_AGE: type.string.pipe(humanizeTime).default("7d"), + + // user + "JSPB_USER_ROOT_TOKEN?": type.string, + JSPB_USER_REGISTER: type.boolean.default(true), + + // task + JSPB_TASK_SWEEPER: type( + /^(?:\*|[0-5]?\d(?:-[0-5]?\d)?)(?:\/[1-9]\d*)?(?:,(?:\*|[0-5]?\d(?:-[0-5]?\d)?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[01]?\d|2[0-3])(?:-(?:[01]?\d|2[0-3]))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[01]?\d|2[0-3])(?:-(?:[01]?\d|2[0-3]))?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[1-9]|[12]\d|3[01])(?:-(?:[1-9]|[12]\d|3[01]))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[1-9]|[12]\d|3[01])(?:-(?:[1-9]|[12]\d|3[01]))?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)(?:-(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)(?:-(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec))?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[0-7]|sun|mon|tue|wed|thu|fri|sat)(?:-(?:[0-7]|sun|mon|tue|wed|thu|fri|sat))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[0-7]|sun|mon|tue|wed|thu|fri|sat)(?:-(?:[0-7]|sun|mon|tue|wed|thu|fri|sat))?)(?:\/[1-9]\d*)?)*$/i + ) + .describe("a valid unix based cron: https://man7.org/linux/man-pages/man5/crontab.5.html") + .default("0 1 * * *") + }, + { + env: Deno.env.toObject() + } + ), + nanoid: customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"), + path: { + struct: { + storage: "./storage/", + storageData: "./storage/data/" + }, + databaseFile: "./storage/database.db" + }, + store: { + statements: new LruCache(200), + dispose: new IPQ Promise>() + }, + temporal: { + utc: () => Temporal.Now.zonedDateTimeISO("Etc/UTC"), + instant: Temporal.Now.instant + }, + http: STATUS_CODES as Record, + text: { + decode: new TextDecoder().decode, + encode: new TextEncoder().encode + }, + ulid: { + userRoot: "0000000000FFFF000000000000" + } +} as const; diff --git a/src/http/middleware/authorization.ts b/src/http/middleware/authorization.ts new file mode 100644 index 00000000..a7a84e66 --- /dev/null +++ b/src/http/middleware/authorization.ts @@ -0,0 +1,27 @@ +import { createMiddleware } from "@hono/hono/factory"; +import { type } from "arktype"; +import { mutable } from "#/global.ts"; +import { ErrorCode, error } from "#util/error.ts"; +import { validatorUserHeader } from "#util/validator/user.ts"; +import type { Env } from "../type.ts"; + +export const authMiddleware = createMiddleware(async (ctx, next) => { + const authorization = ctx.req.header("authorization"); + if (!authorization) { + return next(); + } + + const token = validatorUserHeader(authorization); + if (token instanceof type.errors) { + return error.throw(ErrorCode.validation, token.summary); + } + + const userId = mutable.database.user.get("token", token)?.id; + if (!userId) { + return error.throw(ErrorCode.userInvalidToken); + } + + ctx.set("userId", userId); + + await next(); +}); diff --git a/src/http/middleware/bodyCheck.ts b/src/http/middleware/bodyCheck.ts new file mode 100644 index 00000000..9dc34bf5 --- /dev/null +++ b/src/http/middleware/bodyCheck.ts @@ -0,0 +1,50 @@ +import { createMiddleware } from "@hono/hono/factory"; +import type { Env } from "../type.ts"; + +export const bodyCheck = createMiddleware(async (ctx, next) => { + if (!ctx.req.raw.body) { + ctx.set("hasBody", false); + return next(); + } + + const contentLength = ctx.req.raw.headers.get("content-length"); + const transferEncoding = ctx.req.raw.headers.has("transfer-encoding"); + + if (contentLength !== null && !transferEncoding) { + ctx.set("hasBody", Number.parseInt(contentLength, 10) > 0); + return next(); + } + + const stream = ctx.req.raw.body.getReader(); + const chunk = await stream.read(); + if (chunk.done || chunk.value.length === 0) { + ctx.set("hasBody", false); + return next(); + } + + ctx.set("hasBody", true); + + const reader = new ReadableStream({ + start: (controller) => { + // reinsert the validation chunk + controller.enqueue(chunk.value); + }, + pull: async (controller) => { + const { done, value } = await stream.read(); + if (done) { + controller.close(); + return; + } + + controller.enqueue(value); + }, + cancel: () => { + stream.cancel(); + } + }); + + const requestInit: RequestInit & { duplex: "half" } = { body: reader, duplex: "half" }; + ctx.req.raw = new Request(ctx.req.raw, requestInit as RequestInit); + + await next(); +}); diff --git a/src/http/middleware/bodySize.ts b/src/http/middleware/bodySize.ts new file mode 100644 index 00000000..f6ea37d0 --- /dev/null +++ b/src/http/middleware/bodySize.ts @@ -0,0 +1,50 @@ +import { createMiddleware } from "@hono/hono/factory"; +import { constant } from "#/global.ts"; +import { ErrorCode, error } from "#util/error.ts"; +import type { Env } from "../type.ts"; + +// https://github.com/honojs/hono/blob/main/src/middleware/body-limit/index.ts +export const bodySize = createMiddleware(async (ctx, next) => { + if (!ctx.req.raw.body) { + return next(); + } + + const contentLength = ctx.req.raw.headers.get("content-length"); + const transferEncoding = ctx.req.raw.headers.has("transfer-encoding"); + + if (contentLength !== null && !transferEncoding) { + if (Number.parseInt(contentLength, 10) > constant.env.JSPB_DOCUMENT_SIZE) { + return error.throw(ErrorCode.documentInvalidSize); + } + + return next(); + } + + let size = 0; + const stream = ctx.req.raw.body.getReader(); + const reader = new ReadableStream({ + pull: async (controller) => { + const { done, value } = await stream.read(); + if (done) { + controller.close(); + return; + } + + size += value.length; + if (size > constant.env.JSPB_DOCUMENT_SIZE) { + controller.error(error.get(ErrorCode.documentInvalidSize)); + return; + } + + controller.enqueue(value); + }, + cancel: () => { + stream.cancel(); + } + }); + + const requestInit: RequestInit & { duplex: "half" } = { body: reader, duplex: "half" }; + ctx.req.raw = new Request(ctx.req.raw, requestInit as RequestInit); + + await next(); +}); diff --git a/src/http/router.ts b/src/http/router.ts new file mode 100644 index 00000000..384839a0 --- /dev/null +++ b/src/http/router.ts @@ -0,0 +1,125 @@ +import { Hono } from "@hono/hono"; +import { cors } from "@hono/hono/cors"; +import { HTTPException } from "@hono/hono/http-exception"; +import { openAPIRouteHandler } from "@hono/openapi"; +import { v1DocumentRouter } from "#endpoint/document/v1/index.ts"; +import { v2LegacyDocumentRouter } from "#endpoint/legacy/v2/documents/index.ts"; +import { Logger } from "#util/console.ts"; +import { ErrorCode, error } from "#util/error.ts"; +import { v1UserRouter } from "../endpoints/user/v1/index.ts"; +import { constant } from "../global.ts"; +import type { Env } from "./type.ts"; + +const log: Logger = new Logger("http"); + +export const router = (): Hono => { + const router = new Hono().basePath("/api"); + + router.notFound((ctx) => { + return ctx.body(null, 404); + }); + + router.onError((instance, ctx) => { + if (instance instanceof HTTPException) { + return instance.getResponse(); + } + + // some of them may be triggered by a race condition + if ( + // IO + instance instanceof Deno.errors.NotFound || + instance instanceof Deno.errors.AlreadyExists || + instance instanceof Deno.errors.BadResource || + // corrupted stream (probably) + instance instanceof Deno.errors.Http + ) { + log.debug(instance); + + return ctx.json(error.get(ErrorCode.documentCorrupted)); + } + + log.error(instance); + + return ctx.json(error.get(ErrorCode.crash)); + }); + + router.use("*", cors()); + router.use(async (ctx, next) => { + await next(); + + // disable compression + // https://docs.deno.com/runtime/fundamentals/http_server/#automatic-body-compression + ctx.res.headers.append("Cache-Control", "no-transform"); + }); + + router.get( + "/oas.json", + openAPIRouteHandler(router, { + documentation: { + openapi: "3.1.0", + info: { + version: "rolling", + title: "JSPaste API", + summary: "Create and share code with JSPaste! The developer website for easy code sharing.", + description: `The API endpoints documented here are stable. However, the OpenAPI spec used to generate this documentation is unstable. It may change or break without notice. + +## User class +- **Anonymous:** Can alter anonymous documents, everyone can alter their documents. +- **Registered:** Can alter their own and anonymous documents, only they and "root" can alter their documents. +- **"root":** Can alter every document, no one can alter their documents except "root" itself. + +## Restrictions +Each instance can impose restrictions to the API usage. These restrictions may include, but not limited to: + +(the following values might change without notice) +- Instance registration policy: ${constant.env.JSPB_USER_REGISTER ? "OPEN" : "CLOSED"} +- Document size limit: ${constant.env.JSPB_DOCUMENT_SIZE === 0 ? "unlimited" : (constant.env.JSPB_DOCUMENT_SIZE ?? "unknown")} +- Document lifetime: ${constant.env.JSPB_DOCUMENT_AGE.total("minutes") === 0 ? "unlimited" : (constant.env.JSPB_DOCUMENT_AGE.total("minutes") ?? "unknown")} +- Document anonymous lifetime: ${constant.env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("minutes") === 0 ? "unlimited" : (constant.env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("minutes") ?? "unknown")} +`, + license: { + name: "EUPL-1.2", + url: "https://eur-lex.europa.eu/eli/dec_impl/2017/863" + } + }, + externalDocs: { + description: "Source code", + url: "https://github.com/jspaste/backend" + }, + components: { + securitySchemes: { + bearer: { + bearerFormat: "base64url", + type: "http", + scheme: "bearer", + description: "Registered user in the instance." + } + } + }, + servers: [ + { + url: "https://jspaste.eu", + description: "Official JSPaste instance" + }, + { + url: "http://localhost:4000", + description: "Local instance" + } + ] + } + }) + ); + + // deprecated + router.get("/documents/*", (ctx) => { + return ctx.redirect(ctx.req.path.replace(/\/documents\//g, "/v2/documents/"), 307); + }); + + router.route("/document/v1", v1DocumentRouter); + router.route("/user/v1", v1UserRouter); + + // deprecated + router.route("/v2/documents", v2LegacyDocumentRouter); + + return router; +}; diff --git a/src/http/server.ts b/src/http/server.ts new file mode 100644 index 00000000..5700e0d7 --- /dev/null +++ b/src/http/server.ts @@ -0,0 +1,43 @@ +import { constant } from "#/global.ts"; +import { Logger } from "#util/console.ts"; +import { ErrorCode, error } from "../utils/error.ts"; + +const log: Logger = new Logger("http"); + +const dummyHandler = (): Response => { + return Response.json( + { + ...error.get(ErrorCode.unknown) + }, + { + status: 503, + headers: { + // disable compression + // https://docs.deno.com/runtime/fundamentals/http_server/#automatic-body-compression + "Cache-Control": "no-transform" + } + } + ); +}; + +type Options = { + handler?: Deno.ServeHandler; +}; + +export const server = (options?: Options): Deno.HttpServer => { + const handlerDefault: Deno.ServeHandler = options?.handler ?? dummyHandler; + + return Deno.serve({ + transport: "tcp", + hostname: constant.env.JSPB_HOSTNAME.root, + port: constant.env.JSPB_PORT, + handler: handlerDefault, + onListen: () => { + if (options?.handler) { + log.info( + `Listening on ${constant.env.JSPB_HOSTNAME.isIPv6 ? `[${constant.env.JSPB_HOSTNAME.root}]` : constant.env.JSPB_HOSTNAME.root}:${constant.env.JSPB_PORT}` + ); + } + } + }); +}; diff --git a/src/http/type.ts b/src/http/type.ts new file mode 100644 index 00000000..6cee3e56 --- /dev/null +++ b/src/http/type.ts @@ -0,0 +1,6 @@ +export type Env = { + Variables: { + userId: string | undefined; + hasBody: boolean | undefined; + }; +}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..1bbcd836 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,21 @@ +import { configure } from "arktype/config"; +import "@std/dotenv/load"; + +declare global { + // biome-ignore lint/style/useConsistentTypeDefinitions: expected + interface ArkEnv { + meta(): { + ref?: string; + }; + } +} + +configure({ + toJsonSchema: { + fallback: { + morph: (ctx) => ctx.out ?? ctx.base + } + } +}); + +void import("./init.ts").then(({ init }) => init()); diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 00000000..2f937c26 --- /dev/null +++ b/src/init.ts @@ -0,0 +1,96 @@ +import { abortable } from "@std/async"; +import { ensureDir } from "@std/fs"; +import { Database } from "#db/database.ts"; +import { router } from "#http/router.ts"; +import { server } from "#http/server.ts"; +import { sweeper } from "#task/sweeper.ts"; +import { Logger } from "#util/console.ts"; +import { constant, mutable } from "./global.ts"; +import { taskRegister } from "./task.ts"; + +const log: Logger = new Logger(); + +const initDirStruct = async (): Promise => { + const paths = Object.values(constant.path.struct); + + await Promise.all(paths.map((path) => ensureDir(path))); +}; + +const initHTTPServer = async (handler?: Deno.ServeHandler): Promise => { + const id = "__httpServer"; + + await constant.store.dispose.get(id)?.(); + + mutable.http = server({ + handler: handler + }); + + constant.store.dispose.set(10, id, async () => { + mutable.http?.unref(); + + // Deno.serve will deadlock on shutdown under pressure + await mutable.http?.shutdown(); + }); +}; + +const initDatabase = async (): Promise => { + const id = "__databaseServer"; + + await constant.store.dispose.get(id)?.(); + + mutable.database = new Database(); + + constant.store.dispose.set(0, id, async () => mutable.database[Symbol.dispose]()); + + mutable.database.migration(); + + if (constant.env.JSPB_USER_ROOT_TOKEN) { + mutable.database.user.update("id", constant.ulid.userRoot, "token", constant.env.JSPB_USER_ROOT_TOKEN); + } +}; + +const initTask = async (): Promise => { + taskRegister(constant.env.JSPB_TASK_SWEEPER, sweeper, { + name: "sweeper" + }); +}; + +export const init = async () => { + for (const signal of ["SIGINT", "SIGTERM", "SIGHUP", "SIGUSR1", "SIGUSR2"] satisfies Deno.Signal[]) { + Deno.addSignalListener(signal, async () => { + if (mutable.shutdown) return; + mutable.shutdown = true; + + log.debug(`Received ${signal}.`); + + try { + for (const [, key, dispose] of constant.store.dispose.drain()) { + log.debug(`Closing "${key}"...`); + + try { + // biome-ignore lint/performance/noAwaitInLoops: serialized + await abortable(dispose(), AbortSignal.timeout(3000)); + } catch { + log.warn(`Couldn't close "${key}" on time.`); + } + } + } catch (error) { + log.error("Failed to gracefully shutdown (bad state)..:", error); + Deno.exit(1); + } + + if (Deno.exitCode === 0) { + log.info("Bye."); + } + }); + } + + try { + await Promise.all([initDirStruct(), initHTTPServer()]); + await Promise.all([initDatabase()]); + await Promise.all([initTask(), initHTTPServer(router().fetch)]); + } catch (error) { + log.error(error); + Deno.kill(Deno.pid, "SIGTERM"); + } +}; diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index 2bd58c50..00000000 --- a/src/server.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { OpenAPIHono } from '@hono/zod-openapi'; -import { serve } from 'bun'; -import { cors } from 'hono/cors'; -import { HTTPException } from 'hono/http-exception'; -import { oas } from '#server/oas.ts'; -import { env } from '#util/env.ts'; -import { logger } from '#util/logger.ts'; -import { config } from './config.ts'; -import { endpoints } from './server/endpoints.ts'; -import { errorHandler } from './server/errorHandler.ts'; -import { ErrorCode } from './types/ErrorHandler.ts'; - -process.on('SIGTERM', async () => await backend.stop()); - -logger.set(env.logLevel); - -const instance = new OpenAPIHono().basePath(config.apiPath); - -export const server = (): typeof instance => { - instance.use('*', cors()); - - instance.onError((err) => { - if (err instanceof HTTPException) { - return err.getResponse(); - } - - logger.error(err); - throw errorHandler.send(ErrorCode.unknown); - }); - - instance.notFound((ctx) => { - return ctx.body(null, 404); - }); - - oas(instance); - endpoints(instance); - - logger.debug('Registered routes:', instance.routes.map((route) => route.path).join(', ')); - logger.info(`Listening on: http://localhost:${env.port}`); - - return instance; -}; - -const backend = serve({ - fetch: server().fetch, - port: env.port -}); diff --git a/src/server/endpoints.ts b/src/server/endpoints.ts deleted file mode 100644 index 76ece7dd..00000000 --- a/src/server/endpoints.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { OpenAPIHono } from '@hono/zod-openapi'; -import { v1 } from '#v1/index.ts'; -import { v2 } from '#v2/index.ts'; -import { config } from '../config.ts'; - -export const endpoints = (instance: OpenAPIHono): void => { - instance.get('/documents/*', (ctx) => { - return ctx.redirect(`${config.apiPath}/v2/documents`.concat(ctx.req.path.split('/documents').pop() ?? ''), 307); - }); - - instance.route('/v2/documents', v2()); - instance.route('/v1/documents', v1()); -}; diff --git a/src/server/errorHandler.ts b/src/server/errorHandler.ts deleted file mode 100644 index d42ec927..00000000 --- a/src/server/errorHandler.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { ResponseConfig } from '@asteasolutions/zod-to-openapi/dist/openapi-registry'; -import { z } from '@hono/zod-openapi'; -import { HTTPException } from 'hono/http-exception'; -import { ErrorCode, type Schema } from '#type/ErrorHandler.ts'; - -const map: Record = { - [ErrorCode.unknown]: { - httpCode: 500, - type: 'generic', - message: - 'An unknown error occurred. This may be due to an unexpected condition in the server. If it happens again, please report it here: https://github.com/jspaste/backend/issues/new/choose' - }, - [ErrorCode.notFound]: { - httpCode: 404, - type: 'generic', - message: 'The requested resource does not exist.' - }, - [ErrorCode.validation]: { - httpCode: 400, - type: 'generic', - message: - 'Validation of the request data failed. Check the entered data according to our documentation: https://jspaste.eu/docs' - }, - [ErrorCode.crash]: { - httpCode: 500, - type: 'generic', - message: - 'An internal server error occurred. This may be due to an unhandled exception. If it happens again, please report it here: https://github.com/jspaste/backend/issues/new/choose' - }, - [ErrorCode.parse]: { - httpCode: 400, - type: 'generic', - message: - 'The request could not be parsed. This may be due to a malformed input or an unsupported data format. Check the entered data and try again.' - }, - [ErrorCode.dummy]: { - httpCode: 200, - type: 'generic', - message: 'This is a dummy error.' - }, - [ErrorCode.documentNotFound]: { - httpCode: 404, - type: 'document', - message: 'The requested document does not exist. Check the document name and try again.' - }, - [ErrorCode.documentPasswordNeeded]: { - httpCode: 401, - type: 'document', - message: 'This document is protected. Provide the document password and try again.' - }, - [ErrorCode.documentInvalidPassword]: { - httpCode: 403, - type: 'document', - message: 'The credentials provided for the document are invalid.' - }, - [ErrorCode.documentInvalidPasswordLength]: { - httpCode: 400, - type: 'document', - message: 'The password length provided for the document is invalid.' - }, - [ErrorCode.documentInvalidSize]: { - httpCode: 413, - type: 'document', - message: 'The body size provided for the document is too large.' - }, - [ErrorCode.documentInvalidSecret]: { - httpCode: 403, - type: 'document', - message: 'The credentials provided for the document are invalid.' - }, - [ErrorCode.documentInvalidSecretLength]: { - httpCode: 400, - type: 'document', - message: 'The secret length provided for the document is invalid.' - }, - [ErrorCode.documentInvalidNameLength]: { - httpCode: 400, - type: 'document', - message: 'The name length provided for the document is out of range.' - }, - [ErrorCode.documentNameAlreadyExists]: { - httpCode: 400, - type: 'document', - message: 'The name provided for the document already exists. Use another one and try again.' - }, - [ErrorCode.documentInvalidName]: { - httpCode: 400, - type: 'document', - message: 'The name provided for the document is invalid. Use another one and try again.' - }, - [ErrorCode.documentCorrupted]: { - httpCode: 500, - type: 'document', - message: 'The document is corrupted. It may have been tampered with or uses an unsupported format.' - } -} as const; - -export const errorHandler = { - get: (code: ErrorCode) => { - const { type, message } = map[code]; - - return { type, code, message }; - }, - - send: (code: ErrorCode) => { - const { httpCode, type, message } = map[code]; - - throw new HTTPException(httpCode, { - res: new Response(JSON.stringify({ type, code, message }), { - status: httpCode, - headers: { - 'Content-Type': 'application/json' - } - }) - }); - } -} as const; - -export const schema: ResponseConfig = { - content: { - 'application/json': { - schema: z.object({ - type: z.string().openapi({ - description: 'The message type', - example: errorHandler.get(ErrorCode.dummy).type - }), - code: z.number().openapi({ - description: 'The message code', - example: errorHandler.get(ErrorCode.dummy).code - }), - message: z.string().openapi({ - description: 'The message description', - example: errorHandler.get(ErrorCode.dummy).message - }) - }) - } - }, - description: 'Generic error object' -} as const; diff --git a/src/server/middleware.ts b/src/server/middleware.ts deleted file mode 100644 index 5df085c0..00000000 --- a/src/server/middleware.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { bodyLimit as middlewareBodyLimit } from 'hono/body-limit'; -import { errorHandler } from '#server/errorHandler.ts'; -import { ErrorCode } from '#type/ErrorHandler.ts'; -import { env } from '#util/env.ts'; - -export const middleware = { - bodyLimit: (maxSize: number = env.documentMaxSize) => { - return middlewareBodyLimit({ - maxSize: maxSize * 1024, - onError: () => { - throw errorHandler.send(ErrorCode.documentInvalidSize); - } - }); - } -} as const; diff --git a/src/server/oas.ts b/src/server/oas.ts deleted file mode 100644 index 64db10a6..00000000 --- a/src/server/oas.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { OpenAPIHono } from '@hono/zod-openapi'; -import { config } from '../config.ts'; - -export const oas = (instance: OpenAPIHono): void => { - instance.doc31('/oas.json', (ctx) => ({ - openapi: '3.1.0', - info: { - title: 'JSPaste API', - version: 'rolling', - description: `Note: The latest API version can be accessed with "${config.apiPath}/documents" alias route.`, - license: { - name: 'EUPL-1.2', - url: 'https://eur-lex.europa.eu/eli/dec_impl/2017/863' - } - }, - servers: [ - { - url: config.protocol.concat(new URL(ctx.req.url).host), - description: 'This instance' - }, - { - url: 'https://jspaste.eu', - description: 'Official JSPaste instance' - }, - { - url: 'https://paste.inetol.net', - description: 'Inetol Infrastructure instance' - } - ].filter((server, index, self) => self.findIndex((x) => x.url === server.url) === index) - })); -}; diff --git a/src/task.ts b/src/task.ts new file mode 100644 index 00000000..1994358c --- /dev/null +++ b/src/task.ts @@ -0,0 +1,42 @@ +import { Logger } from "#util/console.ts"; +import { constant } from "./global.ts"; + +const log: Logger = new Logger("task"); + +type TaskRegisterOptions = { + name: string; +}; + +const trigger = async (callback: () => Promise | void, options: TaskRegisterOptions): Promise => { + log.debug(`Running "${options.name}".`); + + try { + await callback(); + } catch (error) { + log.error(`Error while running "${options.name}"..:`, error); + } + + log.debug(`Finished "${options.name}".`); +}; + +export const taskRegister = ( + expression: string, + callback: () => Promise | void, + options: TaskRegisterOptions +): void => { + const abort = new AbortController(); + + const id = `__task-${options.name}`; + + constant.store.dispose.get(id)?.(); + + try { + Deno.cron(options.name, expression, { signal: abort.signal }, () => trigger(callback, options)); + } catch (error) { + log.error(`Failed to register "${options.name}"..:`, error); + } + + constant.store.dispose.set(100, id, async () => abort.abort()); + + log.debug(`Registered "${options.name}".`); +}; diff --git a/src/tasks/sweeper.ts b/src/tasks/sweeper.ts new file mode 100644 index 00000000..bd60b67c --- /dev/null +++ b/src/tasks/sweeper.ts @@ -0,0 +1,95 @@ +import { mapNotNullish } from "@std/collections"; +import { decodeTime } from "@std/ulid"; +import { constant } from "#/global.ts"; +import { Database } from "#db/database.ts"; +import { storage } from "#document/storage.ts"; +import { Logger } from "#util/console.ts"; + +const log = new Logger("task::sweeper"); + +export const sweeper = async (): Promise => { + sweeperDatabaseUser(); + sweeperDatabaseDocument(); + + // sweeper will remove everything in storage on ephemeral + if (!constant.env.JSPB_DEBUG_DATABASE_EPHEMERAL) { + await sweeperDangling(); + } +}; + +const sweeperDatabaseUser = (): void => { + using database = new Database(); + + const temporalFuture = constant.temporal.utc().add({ days: 3 }); + + const users = mapNotNullish(database.user.getAllWithoutDocuments(), ({ id }) => { + if (!id) return; + if (id === constant.ulid.userRoot) return; + + if (temporalFuture.epochMilliseconds > decodeTime(id)) { + return id; + } + + return; + }); + + if (users.length > 0) { + database.user.delete("id", users); + log.debug(`Removed ${users.length} unused user records.`); + } +}; + +const sweeperDatabaseDocument = (): void => { + using database = new Database(); + + const temporalNow = constant.temporal.utc(); + + const documents = mapNotNullish(database.document.getAll(["id", "user_id"]), ({ id, user_id }) => { + if (!id) return; + + const ageType = user_id + ? constant.env.JSPB_DOCUMENT_AGE.total("milliseconds") + : constant.env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("milliseconds"); + + if (ageType > 0 && temporalNow.epochMilliseconds - decodeTime(id) > ageType) { + return id; + } + + return; + }); + + if (documents.length > 0) { + database.document.delete("id", documents); + log.debug(`Removed ${documents.length} expired document records.`); + } +}; + +const sweeperDangling = async (): Promise => { + using database = new Database(); + + const databaseDocuments = mapNotNullish(database.document.getAll(["id"]), ({ id }) => id); + const storageDocuments = storage.list(true); + + const databaseDocumentsSet = new Set(databaseDocuments); + const storageDocumentsSet = new Set(storageDocuments); + + const databaseDocumentsDangling = databaseDocumentsSet.difference(storageDocumentsSet); + const storageDocumentsDangling = storageDocumentsSet.difference(databaseDocumentsSet); + + const queue: Promise[] = []; + + if (databaseDocumentsDangling.size > 0) { + database.document.delete("id", databaseDocumentsDangling); + log.debug(`Removed ${databaseDocumentsDangling.size} dangling records.`); + } + + if (storageDocumentsDangling.size > 0) { + for (const id of storageDocumentsDangling) { + queue.push(storage.delete(id)); + } + + await Promise.all(queue); + + log.debug(`Removed ${storageDocumentsDangling.size} dangling files.`); + } +}; diff --git a/src/types/Document.ts b/src/types/Document.ts deleted file mode 100644 index db05eb42..00000000 --- a/src/types/Document.ts +++ /dev/null @@ -1,13 +0,0 @@ -export enum DocumentVersion { - V1 = 1 -} - -export type Document = { - data: Uint8Array; - header: { - name: string; - secretHash: Uint8Array; - passwordHash: Uint8Array | null; - }; - version: DocumentVersion; -}; diff --git a/src/types/ErrorHandler.ts b/src/types/ErrorHandler.ts deleted file mode 100644 index 3e16dc2f..00000000 --- a/src/types/ErrorHandler.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ContentfulStatusCode } from 'hono/utils/http-status'; - -export enum ErrorCode { - // * Generic - crash = 1000, - unknown = 1001, - validation = 1002, - parse = 1003, - notFound = 1004, - dummy = 1005, - - // * Document - documentNotFound = 1200, - documentNameAlreadyExists = 1201, - documentPasswordNeeded = 1202, - documentInvalidSize = 1203, - documentInvalidNameLength = 1204, - documentInvalidPassword = 1205, - documentInvalidPasswordLength = 1206, - documentInvalidSecret = 1207, - documentInvalidSecretLength = 1208, - documentInvalidName = 1209, - documentCorrupted = 1210 -} - -type Type = 'generic' | 'document'; - -export type Schema = { - httpCode: ContentfulStatusCode; - type: Type; - message: string; -}; diff --git a/src/types/Range.ts b/src/types/Range.ts deleted file mode 100644 index 612a2bb7..00000000 --- a/src/types/Range.ts +++ /dev/null @@ -1,9 +0,0 @@ -// https://github.com/microsoft/TypeScript/issues/43505 -export type Range< - START extends number, - END extends number, - ARR extends unknown[] = [], - ACC extends number = never -> = ARR['length'] extends END - ? ACC | START | END - : Range; diff --git a/src/utils/StringUtils.ts b/src/utils/StringUtils.ts deleted file mode 100644 index 7ceba70f..00000000 --- a/src/utils/StringUtils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { config } from '../config.ts'; -import type { Range } from '../types/Range.ts'; -import { ValidatorUtils } from './ValidatorUtils.ts'; - -export class StringUtils { - public static readonly BASE64URL = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_'; - - public static random(length: number, base: Range<2, 64> = 62): string { - const baseSet = StringUtils.BASE64URL.slice(0, base); - let string = ''; - - while (length--) string += baseSet.charAt(Math.floor(Math.random() * baseSet.length)); - - return string; - } - - public static generateName(length: number = config.documentNameLengthDefault): string { - if (!ValidatorUtils.isLengthWithinRange(length, config.documentNameLengthMin, config.documentNameLengthMax)) { - length = config.documentNameLengthDefault; - } - - return StringUtils.random(length, 64); - } - - public static async nameExists(name: string): Promise { - return Bun.file(config.storagePath + name).exists(); - } - - public static async createName(length: number = config.documentNameLengthDefault): Promise { - const key = StringUtils.generateName(length); - - return (await StringUtils.nameExists(key)) ? StringUtils.createName(length + 1) : key; - } - - public static createSecret(chunkLength = 5, chunks = 4): string { - return Array.from({ length: chunks }, () => StringUtils.random(chunkLength)).join('-'); - } -} diff --git a/src/utils/ValidatorUtils.ts b/src/utils/ValidatorUtils.ts deleted file mode 100644 index e7ef03b0..00000000 --- a/src/utils/ValidatorUtils.ts +++ /dev/null @@ -1,31 +0,0 @@ -export class ValidatorUtils { - // biome-ignore lint/suspicious/noExplicitAny: We don't know the type of the value - public static isInstanceOf(value: unknown, type: new (...args: any[]) => T): value is T { - return value instanceof type; - } - - public static isTypeOf(value: unknown, type: string): value is T { - // biome-ignore lint/suspicious/useValidTypeof: We are checking the type of the value - return typeof value === type; - } - - public static isEmptyString(value: string): boolean { - return value.trim().length === 0; - } - - public static isValidArray(value: T[], validator: (value: T) => boolean): boolean { - return Array.isArray(value) && value.every(validator); - } - - public static isValidDomain(value: string): boolean { - return /\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/.test(value); - } - - public static isValidBase64URL(value: string): boolean { - return /^[\w-]+$/.test(value); - } - - public static isLengthWithinRange(value: number, min: number, max: number): boolean { - return value >= min && value <= max; - } -} diff --git a/src/utils/btree.ts b/src/utils/btree.ts new file mode 100644 index 00000000..8f40bf02 --- /dev/null +++ b/src/utils/btree.ts @@ -0,0 +1,4 @@ +import __BTree from "btree"; + +// https://github.com/qwertie/btree-typescript/issues/36 +export const BTree = (__BTree as unknown as { default: typeof __BTree }).default; diff --git a/src/utils/colors.ts b/src/utils/colors.ts deleted file mode 100644 index c4cb40ec..00000000 --- a/src/utils/colors.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { type ColorInput, color as bunColor } from 'bun'; - -const colorString = - (color: ColorInput) => - (...text: unknown[]): string => { - return bunColor(color, 'ansi') + text.join(' ') + colors.reset; - }; - -export const colors = { - red: colorString('#ef5454'), - orange: colorString('#ef8354'), - yellow: colorString('#efd554'), - green: colorString('#70ef54'), - turquoise: colorString('#54efef'), - blue: colorString('#5954ef'), - purple: colorString('#a454ef'), - pink: colorString('#ef54d5'), - gray: colorString('#888'), - black: colorString('#000'), - white: colorString('#fff'), - reset: '\x1b[0m' -} as const; diff --git a/src/utils/console.test.ts b/src/utils/console.test.ts new file mode 100644 index 00000000..fda8c8c4 --- /dev/null +++ b/src/utils/console.test.ts @@ -0,0 +1,24 @@ +import { Logger } from "./console.ts"; + +Deno.test("Logger#", () => { + const log = new Logger("test"); + const message = "Message here!"; + const extended = [ + "Extended here!", + undefined, + { + class: Logger, + log: "logloglogloglogloglogloglogloglogloglogloglogloglogloglogloghidden" + }, + null + ]; + + log.debug(message); + log.debug(extended, ...extended); + log.info(message); + log.info(extended, ...extended); + log.warn(message); + log.warn(extended, ...extended); + log.error(message); + log.error(extended, ...extended); +}); diff --git a/src/utils/console.ts b/src/utils/console.ts new file mode 100644 index 00000000..c0eb277f --- /dev/null +++ b/src/utils/console.ts @@ -0,0 +1,86 @@ +import { mapNotNullish } from "@std/collections"; +import { blue, gray, red, yellow } from "@std/fmt/colors"; +import { constant } from "#/global.ts"; + +export class Logger { + public static readonly level = { + none: [0, null], + error: [1, red], + warn: [2, yellow], + info: [3, blue], + debug: [4, gray] + } as const; + + public readonly source: string; + + public constructor(source = "common") { + this.source = source; + } + + public error(...message: unknown[]): void { + this.flush("error", message); + } + + public warn(...message: unknown[]): void { + this.flush("warn", message); + } + + public info(...message: unknown[]): void { + this.flush("info", message); + } + + public debug(...message: unknown[]): void { + this.flush("debug", message); + } + + private flush(level: Exclude, message: unknown[]): void { + const [levelNumber, color] = Logger.level[level]; + + if (levelNumber > constant.env.JSPB_LOG_VERBOSITY) return; + + const prefix: string[] = []; + + if (constant.env.JSPB_LOG_TIME) { + const temporalLocal = Temporal.Now.zonedDateTimeISO(); + const temporalYear = temporalLocal.year; + const temporalMonth = temporalLocal.month.toString().padStart(2, "0"); + const temporalDay = temporalLocal.day.toString().padStart(2, "0"); + const temporalHour = temporalLocal.hour.toString().padStart(2, "0"); + const temporalMinute = temporalLocal.minute.toString().padStart(2, "0"); + const temporalSecond = temporalLocal.second.toString().padStart(2, "0"); + const temporalMillisecond = temporalLocal.millisecond.toString().padStart(3, "0"); + const temporalOffset = temporalLocal.offset; + + prefix.push( + gray( + `${temporalYear}-${temporalMonth}-${temporalDay}T${temporalHour}:${temporalMinute}:${temporalSecond}.${temporalMillisecond + temporalOffset}` + ) + ); + } + + prefix.push(color(level.toUpperCase().padEnd(5))); + prefix.push(gray(`[${this.source}]`)); + + const prefixString = prefix.join(" "); + + const render = mapNotNullish(message, (item) => { + // biome-ignore lint/nursery/noEqualsToNull: expected + if (item == null) return; + + if (typeof item === "string") { + return `${prefixString} ${item}`; + } + + return `${prefixString} ${Deno.inspect(item, { + colors: true, + strAbbreviateSize: 60, + iterableLimit: 10 + })}`; + }); + + for (const line of render) { + // biome-ignore lint/suspicious/noConsole: logger + console[level](line); + } + } +} diff --git a/src/utils/document.ts b/src/utils/document.ts new file mode 100644 index 00000000..23bcc89a --- /dev/null +++ b/src/utils/document.ts @@ -0,0 +1,34 @@ +import { nanoid } from "nanoid"; +import { constant, mutable } from "#/global.ts"; + +export const generateToken = (): string => nanoid(32); + +export const generateName = (length = 8): string => { + let name: string; + do { + name = nanoid(length); + } while (mutable.database.document.get("name", name)?.name); + + return name; +}; + +export const isOwner = (userId?: string | null, documentUserId?: string | null): boolean => { + // the document is not owned, everyone can alter + if (!documentUserId) { + return true; + } + + if (userId) { + // the document is owned by the user + if (userId === documentUserId) { + return true; + } + + // the root user can alter everything + if (userId === constant.ulid.userRoot) { + return true; + } + } + + return false; +}; diff --git a/src/utils/env.ts b/src/utils/env.ts deleted file mode 100644 index 0e081cde..00000000 --- a/src/utils/env.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { get } from 'env-var'; -import { LogLevels } from '#util/logger.ts'; - -export const env = { - documentMaxSize: get('DOCUMENT_MAXSIZE').default(1024).asIntPositive(), - logLevel: get('LOGLEVEL').default(LogLevels.info).asIntPositive(), - port: get('PORT').default(4000).asPortNumber(), - tls: get('TLS').asBoolStrict() ?? true -} as const; diff --git a/src/utils/error.ts b/src/utils/error.ts new file mode 100644 index 00000000..8ea9795b --- /dev/null +++ b/src/utils/error.ts @@ -0,0 +1,128 @@ +import { HTTPException } from "@hono/hono/http-exception"; +import type { ContentfulStatusCode } from "@hono/hono/utils/http-status"; +import { resolver } from "@hono/openapi"; +import { type } from "arktype"; + +export const ErrorCode = { + crash: 1000, + unknown: 1001, + validation: 1002, + // parse: 1003, // moved to 1002 + notFound: 1004, + dummy: 1005, + + // document + documentNotFound: 1200, + documentNameAlreadyExists: 1201, + documentPasswordNeeded: 1202, + documentInvalidSize: 1203, + // documentInvalidNameLength: 1204, // moved to 1002 + documentInvalidPassword: 1205, + // documentInvalidPasswordLength: 1206, // moved to 1002 + // documentInvalidSecret: 1207, // deprecated + // documentInvalidSecretLength: 1208, // deprecated + // documentInvalidName: 1209, // moved to 1002 + documentCorrupted: 1210, + + // user + userInvalidToken: 1300 +} as const; + +export type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode]; + +export type Schema = { + httpCode: ContentfulStatusCode; + message: string; +}; + +const errorDefinition: Record = { + [ErrorCode.crash]: { + httpCode: 500, + message: + "An unexpected server error occurred. If this persists, open an issue at: https://github.com/jspaste/backend/issues" + }, + [ErrorCode.unknown]: { + httpCode: 503, + message: "Server handler has not loaded yet. Wait..." + }, + [ErrorCode.validation]: { + httpCode: 400, + message: "The request contains invalid or malformed data." + }, + [ErrorCode.notFound]: { + httpCode: 404, + message: "The requested resource could not be found." + }, + [ErrorCode.dummy]: { + httpCode: 200, + message: "Placeholder response for documentation purposes." + }, + + // document + [ErrorCode.documentNotFound]: { + httpCode: 404, + message: "No document exists with the specified name." + }, + [ErrorCode.documentNameAlreadyExists]: { + httpCode: 409, + message: "A document with this name already exists. Choose a different name." + }, + [ErrorCode.documentPasswordNeeded]: { + httpCode: 401, + message: "This document is password protected. Include the password in your request." + }, + [ErrorCode.documentInvalidSize]: { + httpCode: 413, + message: "The document content exceeds the maximum allowed size." + }, + [ErrorCode.documentInvalidPassword]: { + httpCode: 403, + message: "The provided password is incorrect." + }, + [ErrorCode.documentCorrupted]: { + httpCode: 500, + message: "The document content is corrupted and cannot be retrieved." + }, + + // user + [ErrorCode.userInvalidToken]: { + httpCode: 401, + message: "The provided authorization token is invalid or missing privileges." + } +} as const; + +export const error = { + get: (code: ErrorCodeType, overrideMessage?: string) => { + const { message } = errorDefinition[code]; + + return { code: code, message: overrideMessage ?? message }; + }, + + throw: (code: ErrorCodeType, overrideMessage?: string): never => { + const { httpCode, message } = errorDefinition[code]; + + throw new HTTPException(httpCode, { + res: Response.json({ code: code, message: overrideMessage ?? message }) + }); + } +} as const; + +export const genericErrorResponse = { + content: { + "application/json": { + schema: resolver( + type({ + code: type.number.configure({ + description: "The error code", + examples: [ErrorCode.dummy] + }), + message: type.string.configure({ + description: "The error description" + }) + }).configure({ + ref: "GenericError" + }) + ) + } + } +} as const; diff --git a/src/utils/humanize.test.ts b/src/utils/humanize.test.ts new file mode 100644 index 00000000..527ef63f --- /dev/null +++ b/src/utils/humanize.test.ts @@ -0,0 +1,62 @@ +import { assertStrictEquals, assertThrows } from "@std/assert"; +import { humanizeSize, humanizeTime } from "#util/humanize.ts"; + +Deno.test("humanizeTime", () => { + const basic = humanizeTime("1d"); + assertStrictEquals(basic.total("seconds"), 86_400); + + // case sensitive + const sensitiveMinutes = humanizeTime("1m"); + const sensitiveMonths = humanizeTime("1M"); + assertStrictEquals(sensitiveMinutes.minutes, 1); + assertStrictEquals(sensitiveMonths.months, 1); + + const zero = humanizeTime("0"); + assertStrictEquals(zero.total("seconds"), 0); + + const zeroUnit = humanizeTime("0d"); + assertStrictEquals(zeroUnit.total("milliseconds"), 0); + + // invalid unit + assertThrows(() => humanizeTime("1x")); + + // spaces in between + assertThrows(() => humanizeTime("1 d")); + + // float + assertThrows(() => humanizeTime("1.9d")); + + // multiple units + assertThrows(() => humanizeTime("1d 50m")); +}); + +Deno.test("humanizeSize", () => { + const basic = humanizeSize("1gb"); + assertStrictEquals(basic, 1_000_000_000); + + const basicBinary = humanizeSize("1gib"); + assertStrictEquals(basicBinary, 1_073_741_824); + + // case insensitive + const insensitive = humanizeSize("1kIb"); + assertStrictEquals(insensitive, 1024); + + // float + const floatValue = humanizeSize("1.5mb"); + assertStrictEquals(floatValue, 1_500_000); + + const zero = humanizeSize("0"); + assertStrictEquals(zero, 0); + + const zeroUnit = humanizeSize("0mb"); + assertStrictEquals(zeroUnit, 0); + + // invalid unit + assertThrows(() => humanizeSize("1xib")); + + // spaces in between + assertThrows(() => humanizeSize("1 gb")); + + // multiple units + assertThrows(() => humanizeTime("1gb 50mb")); +}); diff --git a/src/utils/humanize.ts b/src/utils/humanize.ts new file mode 100644 index 00000000..e97c6e69 --- /dev/null +++ b/src/utils/humanize.ts @@ -0,0 +1,49 @@ +const timeUnitsRegex = /^(\d+)([smhdwMy])$/; +const timeUnitMap: Record = { + s: "seconds", + m: "minutes", + h: "hours", + d: "days", + w: "weeks", + M: "months", + y: "years" +} as const; + +const sizeUnitsRegex = /^(\d+(?:\.\d+)?)(b|[kmgtp]i?b)$/i; +const sizeUnits: Record = { + b: 1, + kb: 1000, + kib: 1024, + mb: 1000 ** 2, + mib: 1024 ** 2, + gb: 1000 ** 3, + gib: 1024 ** 3, + tb: 1000 ** 4, + tib: 1024 ** 4 +} as const; + +export const humanizeTime = (input: string): Temporal.Duration => { + if (input === "0") { + return Temporal.Duration.from({ seconds: 0 }); + } + + const [, value, unit] = timeUnitsRegex.exec(input) ?? []; + if (!(value && unit)) { + throw new Error(`Invalid time "${input}"`); + } + + return Temporal.Duration.from({ [timeUnitMap[unit] as string]: Number.parseInt(value, 10) }); +}; + +export const humanizeSize = (input: string): number => { + if (input === "0") { + return 0; + } + + const [, value, unit] = sizeUnitsRegex.exec(input) ?? []; + if (!(value && unit)) { + throw new Error(`Invalid size "${input}"`); + } + + return Number.parseFloat(value) * (sizeUnits[unit.toLowerCase()] as number); +}; diff --git a/src/utils/ipq.ts b/src/utils/ipq.ts new file mode 100644 index 00000000..47e9e01c --- /dev/null +++ b/src/utils/ipq.ts @@ -0,0 +1,80 @@ +import { BTree } from "./btree.ts"; + +export class IPQ { + private readonly tree = new BTree>(); + private readonly priority = new Map(); + + public set(priority: number, key: K, value: V): void { + const oldPriority = this.priority.get(key); + if (oldPriority !== undefined) { + const entries = this.tree.get(oldPriority); + if (entries) { + entries.delete(key); + if (entries.size === 0) { + this.tree.delete(oldPriority); + } + } + } + + let entries = this.tree.get(priority); + if (!entries) { + entries = new Map(); + this.tree.set(priority, entries); + } + entries.set(key, value); + this.priority.set(key, priority); + } + + public get(key: K): V | undefined { + const priority = this.priority.get(key); + if (priority === undefined) return; + + return this.tree.get(priority)?.get(key); + } + + public has(key: K): boolean { + return this.priority.has(key); + } + + public delete(key: K): boolean { + const priority = this.priority.get(key); + if (priority === undefined) return false; + + const entries = this.tree.get(priority); + if (entries) { + entries.delete(key); + if (entries.size === 0) { + this.tree.delete(priority); + } + } + this.priority.delete(key); + return true; + } + + public *entries(): Generator<[number, K, V], void, unknown> { + for (const [priority, entries] of this.tree.entries()) { + for (const [key, value] of entries) { + yield [priority, key, value]; + } + } + } + + public *drain(): Generator<[number, K, V], void, unknown> { + while (this.tree.size > 0) { + const priority = this.tree.maxKey()!; + const entries = this.tree.get(priority)!; + + for (const [key, value] of entries) { + this.priority.delete(key); + yield [priority, key, value]; + } + + this.tree.delete(priority); + } + } + + public [Symbol.dispose](): void { + this.tree.clear(); + this.priority.clear(); + } +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts deleted file mode 100644 index df201d66..00000000 --- a/src/utils/logger.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { colors } from '#util/colors.ts'; - -export enum LogLevels { - none = 0, - error = 1, - warn = 2, - info = 3, - debug = 4 -} - -let logLevel: LogLevels = LogLevels.info; - -export const logger = { - set: (level: LogLevels): void => { - logLevel = level; - }, - - error: (...text: unknown[]): void => { - if (logLevel >= LogLevels.error) { - console.error(colors.gray('[BACKEND]'), colors.red('[ERROR]'), text.join(' ')); - } - }, - - warn: (...text: unknown[]): void => { - if (logLevel >= LogLevels.warn) { - console.warn(colors.gray('[BACKEND]'), colors.yellow('[WARN]'), text.join(' ')); - } - }, - - info: (...text: unknown[]): void => { - if (logLevel >= LogLevels.info) { - console.info(colors.gray('[BACKEND]'), colors.blue('[INFO]'), text.join(' ')); - } - }, - - debug: (...text: unknown[]): void => { - if (logLevel >= LogLevels.debug) { - console.debug(colors.gray('[BACKEND]'), colors.gray('[DEBUG]'), text.join(' ')); - } - } -} as const; diff --git a/src/utils/validator/document.ts b/src/utils/validator/document.ts new file mode 100644 index 00000000..08a160e1 --- /dev/null +++ b/src/utils/validator/document.ts @@ -0,0 +1,67 @@ +import { type } from "arktype"; +import { constant } from "#/global.ts"; + +export const validatorDocumentName = type(/^[A-Za-z0-9_-]+$/) + .atLeastLength(constant.documentNameLengthMin) + .atMostLength(constant.documentNameLengthMax) + .configure({ + ref: "DocumentName", + description: "The document name", + examples: ["abc123"], + expected: (ctx) => { + switch (ctx.code) { + case "pattern": { + return "a valid Base64URL"; + } + case "minLength": { + return `more than ${ctx.rule} characters`; + } + case "maxLength": { + return `less than ${ctx.rule} characters`; + } + default: { + return "valid"; + } + } + } + }); + +export const validatorDocumentNameLength = type.string.pipe( + (string) => Number.parseInt(string, 10), + type.number + .atLeast(constant.documentNameLengthMin) + .atMost(constant.documentNameLengthMax) + .configure({ + ref: "DocumentNameLength", + description: "The document name length", + expected: (ctx) => { + switch (ctx.code) { + case "domain": { + return "a valid integer"; + } + case "min": { + return `more than ${ctx.rule} length`; + } + case "max": { + return `less than ${ctx.rule} length`; + } + default: { + return "valid"; + } + } + } + }) +); + +// an empty string is the same as null here +export const validatorDocumentPassword = type("''") + .or(type.string.atLeastLength(constant.documentPasswordLengthMin).atMostLength(constant.documentPasswordLengthMax)) + .configure({ + ref: "DocumentPassword", + description: "The password to access the document" + }); + +export const validatorDocumentDownload = type.unknown.configure({ + ref: "DocumentDownload", + description: "The response will be treated as a file download" +}); diff --git a/src/utils/validator/handler.ts b/src/utils/validator/handler.ts new file mode 100644 index 00000000..34a9a69e --- /dev/null +++ b/src/utils/validator/handler.ts @@ -0,0 +1,8 @@ +import type { sValidator } from "@hono/standard-validator"; +import { ErrorCode, error } from "../error.ts"; + +export const validatorHandler: Parameters[2] = (res) => { + if (res.success) return; + + return error.throw(ErrorCode.validation, res.error[0]?.message); +}; diff --git a/src/utils/validator/user.ts b/src/utils/validator/user.ts new file mode 100644 index 00000000..57250cfa --- /dev/null +++ b/src/utils/validator/user.ts @@ -0,0 +1,33 @@ +import { type } from "arktype"; +import { constant } from "#/global.ts"; + +export const validatorUserToken = type(/^[A-Za-z0-9_-]+$/) + .atLeastLength(constant.userTokenLengthMin) + .atMostLength(constant.userTokenLengthMax) + .configure({ + description: "The user token", + examples: ["CW41t9I218GiXyFQtLpKJQ76In-CVK3H"], + expected: (ctx) => { + switch (ctx.code) { + case "pattern": { + return "a valid Base64URL"; + } + case "minLength": { + return `more than ${ctx.rule} characters`; + } + case "maxLength": { + return `less than ${ctx.rule} characters`; + } + default: { + return "valid"; + } + } + } + }); + +export const validatorUserHeader = type(/^Bearer .+$/) + .configure({ + description: "The Bearer token", + expected: "a valid header" + }) + .pipe((string) => string.split(" ")[1], validatorUserToken); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 9ade037c..00000000 --- a/tsconfig.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig.json", - "compilerOptions": { - "lib": ["ESNext"], - "module": "ESNext", - "moduleResolution": "Bundler", - "target": "ESNext", - - "allowImportingTsExtensions": true, - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "incremental": true, - "noEmit": true, - "resolveJsonModule": true, - "skipLibCheck": true, - - "strict": true, - "allowUnreachableCode": false, - "allowUnusedLabels": false, - "exactOptionalPropertyTypes": false, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "noFallthroughCasesInSwitch": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": false, - "noUncheckedIndexedAccess": true, - "noUncheckedSideEffectImports": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "verbatimModuleSyntax": true, - - "baseUrl": ".", - "paths": { - "#v1/*": ["./src/endpoints/v1/*"], - "#v2/*": ["./src/endpoints/v2/*"], - "#document/*": ["./src/document/*"], - "#server/*": ["./src/server/*"], - "#type/*": ["./src/types/*"], - "#util/*": ["./src/utils/*"] - }, - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo" - }, - "include": ["./"], - "exclude": ["./dist/", "./node_modules/", "./storage/"] -} From 5631017961a1ed16ced3a234330aa5676d94c605 Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Mon, 5 Jan 2026 00:25:53 +0100 Subject: [PATCH 36/47] Misc improvements (#256) --- src/document/storage.ts | 23 +++--- src/endpoints/document/v1/delete.ts | 2 +- src/endpoints/document/v1/patch.ts | 10 ++- src/endpoints/document/v1/post.ts | 11 +-- .../legacy/v2/documents/access.route.ts | 4 +- .../legacy/v2/documents/accessRaw.route.ts | 17 ++--- .../legacy/v2/documents/publish.route.ts | 11 +-- .../legacy/v2/documents/remove.route.ts | 2 +- src/http/middleware/bodySize.ts | 3 +- src/utils/document.ts | 5 +- src/utils/validator/document.ts | 74 ++++++++++--------- src/utils/validator/regex.ts | 2 + src/utils/validator/user.ts | 11 +-- 13 files changed, 95 insertions(+), 80 deletions(-) create mode 100644 src/utils/validator/regex.ts diff --git a/src/document/storage.ts b/src/document/storage.ts index b05f233f..334ec9eb 100644 --- a/src/document/storage.ts +++ b/src/document/storage.ts @@ -1,4 +1,5 @@ import { constant } from "#/global.ts"; +import { ErrorCode, error } from "../utils/error.ts"; export const storage = { delete: async (id: string): Promise => { @@ -15,20 +16,22 @@ export const storage = { write: async (id: string, data: ReadableStream): Promise => { await using handle = await Deno.open(constant.path.struct.storageData + id, { - createNew: true, - write: true - }); - - await data.pipeTo(handle.writable, { preventClose: true }); - }, - - overwrite: async (id: string, data: ReadableStream): Promise => { - await using handle = await Deno.open(constant.path.struct.storageData + id, { + create: true, write: true, truncate: true }); - await data.pipeTo(handle.writable, { preventClose: true }); + try { + await data.pipeTo(handle.writable, { preventClose: true }); + } catch (why) { + void storage.delete(id); + + if (why instanceof Deno.errors.BrokenPipe) { + return error.throw(ErrorCode.documentInvalidSize); + } + + throw why; + } }, // relaxed exists because races between fs/db may ocurr diff --git a/src/endpoints/document/v1/delete.ts b/src/endpoints/document/v1/delete.ts index 18ba8d8e..36225628 100644 --- a/src/endpoints/document/v1/delete.ts +++ b/src/endpoints/document/v1/delete.ts @@ -52,7 +52,7 @@ export default new Hono().delete( } mutable.database.document.delete("name", name); - await storage.delete(document.id); + void storage.delete(document.id); return ctx.body(null); } diff --git a/src/endpoints/document/v1/patch.ts b/src/endpoints/document/v1/patch.ts index 94575bdc..3f409511 100644 --- a/src/endpoints/document/v1/patch.ts +++ b/src/endpoints/document/v1/patch.ts @@ -10,7 +10,11 @@ import { bodySize } from "#http/middleware/bodySize.ts"; import type { Env } from "#http/type.ts"; import { isOwner } from "#util/document.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; -import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; +import { + validatorDocumentName, + validatorDocumentPassword, + validatorDocumentPasswordEmpty +} from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; const schemaBody = await resolver( @@ -26,7 +30,7 @@ const schemaParam = type({ const schemaHeader = type({ "x-jspaste-name?": validatorDocumentName, - "x-jspaste-password?": validatorDocumentPassword + "x-jspaste-password?": validatorDocumentPassword.or(validatorDocumentPasswordEmpty) }); export default new Hono().patch( @@ -109,7 +113,7 @@ Note: To remove (nullify) a value, send the header with an empty value`, } if (ctx.get("hasBody")) { - await storage.overwrite( + await storage.write( document.id, // ctx.req.raw.body is only null on GET/HEAD compression.encode(ctx.req.raw.body as NonNullable) diff --git a/src/endpoints/document/v1/post.ts b/src/endpoints/document/v1/post.ts index 3dfef49f..59019544 100644 --- a/src/endpoints/document/v1/post.ts +++ b/src/endpoints/document/v1/post.ts @@ -95,11 +95,6 @@ export default new Hono().post( } const setId = monotonicUlid(); - await storage.write( - setId, - // ctx.req.raw.body is only null on GET/HEAD - compression.encode(ctx.req.raw.body as NonNullable) - ); mutable.database.document.create({ id: setId, @@ -109,6 +104,12 @@ export default new Hono().post( password: password }); + await storage.write( + setId, + // ctx.req.raw.body is only null on GET/HEAD + compression.encode(ctx.req.raw.body as NonNullable) + ); + return ctx.json({ name: setName }); diff --git a/src/endpoints/legacy/v2/documents/access.route.ts b/src/endpoints/legacy/v2/documents/access.route.ts index 7f9b15a1..167058e4 100644 --- a/src/endpoints/legacy/v2/documents/access.route.ts +++ b/src/endpoints/legacy/v2/documents/access.route.ts @@ -87,11 +87,11 @@ export default new Hono().get( } } - await using file = await storage.read(document.id); + await using fileHandle = await storage.read(document.id); return ctx.json({ key: param.name, - data: await toText(compression.decode(file.readable)), + data: await toText(compression.decode(fileHandle.readable)), url: new URL(ctx.req.url).host.concat("/", param.name), expirationTimestamp: 0 }); diff --git a/src/endpoints/legacy/v2/documents/accessRaw.route.ts b/src/endpoints/legacy/v2/documents/accessRaw.route.ts index c831298e..a6d20a33 100644 --- a/src/endpoints/legacy/v2/documents/accessRaw.route.ts +++ b/src/endpoints/legacy/v2/documents/accessRaw.route.ts @@ -79,20 +79,19 @@ export default new Hono().get( } } - const file = await storage.read(document.id); + const fileHandle = await storage.read(document.id); - let streamData: ReadableStream; - if (ctx.req.header("Accept-Encoding")?.includes("deflate")) { - streamData = file.readable; + let fileContent: ReadableStream; + if (ctx.req.header("accept-encoding")?.includes("deflate")) { + fileContent = fileHandle.readable; ctx.res.headers.set("Content-Encoding", "deflate"); } else { - streamData = compression.decode(file.readable); + fileContent = compression.decode(fileHandle.readable); } - ctx.res.headers.append("Cache-Control", "no-cache"); - ctx.res.headers.set("Content-Type", "text/plain"); - ctx.res.headers.set("Transfer-Encoding", "chunked"); + ctx.res.headers.set("content-type", "text/plain"); + ctx.res.headers.set("transfer-encoding", "chunked"); - return stream(ctx, async (stream) => await stream.pipe(streamData)); + return stream(ctx, async (stream) => await stream.pipe(fileContent)); } ); diff --git a/src/endpoints/legacy/v2/documents/publish.route.ts b/src/endpoints/legacy/v2/documents/publish.route.ts index d46824bb..e485619f 100644 --- a/src/endpoints/legacy/v2/documents/publish.route.ts +++ b/src/endpoints/legacy/v2/documents/publish.route.ts @@ -86,11 +86,6 @@ export default new Hono().post( const id = monotonicUlid(); - await storage.write( - id, - // ctx.req.raw.body is only null on GET/HEAD - compression.encode(ctx.req.raw.body as NonNullable) - ); mutable.database.document.create({ id: id, user_id: null, @@ -99,6 +94,12 @@ export default new Hono().post( password: password }); + await storage.write( + id, + // ctx.req.raw.body is only null on GET/HEAD + compression.encode(ctx.req.raw.body as NonNullable) + ); + return ctx.json({ key: setName, secret: "", diff --git a/src/endpoints/legacy/v2/documents/remove.route.ts b/src/endpoints/legacy/v2/documents/remove.route.ts index 214f2c81..34f1e53b 100644 --- a/src/endpoints/legacy/v2/documents/remove.route.ts +++ b/src/endpoints/legacy/v2/documents/remove.route.ts @@ -50,8 +50,8 @@ export default new Hono().delete( return error.throw(ErrorCode.documentNotFound); } - await storage.delete(document.id); mutable.database.document.delete("name", param.name); + void storage.delete(document.id); return ctx.json({ removed: true }); } diff --git a/src/http/middleware/bodySize.ts b/src/http/middleware/bodySize.ts index f6ea37d0..8115be2a 100644 --- a/src/http/middleware/bodySize.ts +++ b/src/http/middleware/bodySize.ts @@ -32,7 +32,8 @@ export const bodySize = createMiddleware(async (ctx, next) => { size += value.length; if (size > constant.env.JSPB_DOCUMENT_SIZE) { - controller.error(error.get(ErrorCode.documentInvalidSize)); + stream.cancel(); + controller.error(new Deno.errors.BrokenPipe()); return; } diff --git a/src/utils/document.ts b/src/utils/document.ts index 23bcc89a..848b1b9a 100644 --- a/src/utils/document.ts +++ b/src/utils/document.ts @@ -1,12 +1,11 @@ -import { nanoid } from "nanoid"; import { constant, mutable } from "#/global.ts"; -export const generateToken = (): string => nanoid(32); +export const generateToken = (): string => constant.nanoid(32); export const generateName = (length = 8): string => { let name: string; do { - name = nanoid(length); + name = constant.nanoid(length); } while (mutable.database.document.get("name", name)?.name); return name; diff --git a/src/utils/validator/document.ts b/src/utils/validator/document.ts index 08a160e1..9e49a67e 100644 --- a/src/utils/validator/document.ts +++ b/src/utils/validator/document.ts @@ -1,23 +1,24 @@ import { type } from "arktype"; import { constant } from "#/global.ts"; +import { regexBase64URL } from "./regex.ts"; -export const validatorDocumentName = type(/^[A-Za-z0-9_-]+$/) +export const validatorDocumentName = type(regexBase64URL) .atLeastLength(constant.documentNameLengthMin) .atMostLength(constant.documentNameLengthMax) .configure({ ref: "DocumentName", - description: "The document name", - examples: ["abc123"], + description: "A name for the document", + examples: ["myDocumentNameHere"], expected: (ctx) => { switch (ctx.code) { case "pattern": { return "a valid Base64URL"; } case "minLength": { - return `more than ${ctx.rule} characters`; + return `at least ${ctx.rule} characters long`; } case "maxLength": { - return `less than ${ctx.rule} characters`; + return `at most ${ctx.rule} characters long`; } default: { return "valid"; @@ -26,42 +27,45 @@ export const validatorDocumentName = type(/^[A-Za-z0-9_-]+$/) } }); -export const validatorDocumentNameLength = type.string.pipe( - (string) => Number.parseInt(string, 10), - type.number - .atLeast(constant.documentNameLengthMin) - .atMost(constant.documentNameLengthMax) - .configure({ - ref: "DocumentNameLength", - description: "The document name length", - expected: (ctx) => { - switch (ctx.code) { - case "domain": { - return "a valid integer"; - } - case "min": { - return `more than ${ctx.rule} length`; - } - case "max": { - return `less than ${ctx.rule} length`; - } - default: { - return "valid"; - } +export const validatorDocumentNameLength = type.keywords.string.integer.parse + .to(type.number.atLeast(constant.documentNameLengthMin).atMost(constant.documentNameLengthMax)) + .configure({ + ref: "DocumentNameLength", + description: "The name length for a document", + expected: (ctx) => { + switch (ctx.code) { + case "domain": { + return "a valid integer"; + } + case "min": { + return `must be greater than ${ctx.rule}`; + } + case "max": { + return `must be less than ${ctx.rule}`; + } + default: { + return "valid"; } } - }) -); + } + }); -// an empty string is the same as null here -export const validatorDocumentPassword = type("''") - .or(type.string.atLeastLength(constant.documentPasswordLengthMin).atMostLength(constant.documentPasswordLengthMax)) +export const validatorDocumentPassword = type.string + .atLeastLength(constant.documentPasswordLengthMin) + .atMostLength(constant.documentPasswordLengthMax) .configure({ - ref: "DocumentPassword", - description: "The password to access the document" + ref: "DocumentPassword.default", + description: "A password for the document (for read access)", + examples: ["myDocumentPasswordHere"] }); +export const validatorDocumentPasswordEmpty = type.string.exactlyLength(0).configure({ + ref: "DocumentPassword.empty", + description: "A blank password for the document", + examples: [""] +}); + export const validatorDocumentDownload = type.unknown.configure({ ref: "DocumentDownload", - description: "The response will be treated as a file download" + description: "Indicate the client that downloads the document as a file attachment (only useful in web browsers)" }); diff --git a/src/utils/validator/regex.ts b/src/utils/validator/regex.ts new file mode 100644 index 00000000..315286e6 --- /dev/null +++ b/src/utils/validator/regex.ts @@ -0,0 +1,2 @@ +export const regexBase64URL = /^[A-Za-z0-9_-]+$/; +export const regexHeaderBearer = /^Bearer .+$/; diff --git a/src/utils/validator/user.ts b/src/utils/validator/user.ts index 57250cfa..52aecb5c 100644 --- a/src/utils/validator/user.ts +++ b/src/utils/validator/user.ts @@ -1,12 +1,13 @@ import { type } from "arktype"; import { constant } from "#/global.ts"; +import { regexBase64URL, regexHeaderBearer } from "./regex.ts"; -export const validatorUserToken = type(/^[A-Za-z0-9_-]+$/) +export const validatorUserToken = type(regexBase64URL) .atLeastLength(constant.userTokenLengthMin) .atMostLength(constant.userTokenLengthMax) .configure({ - description: "The user token", - examples: ["CW41t9I218GiXyFQtLpKJQ76In-CVK3H"], + description: "A user token", + examples: ["myUserTokenHere"], expected: (ctx) => { switch (ctx.code) { case "pattern": { @@ -25,9 +26,9 @@ export const validatorUserToken = type(/^[A-Za-z0-9_-]+$/) } }); -export const validatorUserHeader = type(/^Bearer .+$/) +export const validatorUserHeader = type(regexHeaderBearer) .configure({ - description: "The Bearer token", + description: "A RFC 6750 structured Bearer header", expected: "a valid header" }) .pipe((string) => string.split(" ")[1], validatorUserToken); From aa603ae833a626d2ba61e34acccd5b8ebdc74037 Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Mon, 5 Jan 2026 15:43:55 +0100 Subject: [PATCH 37/47] Misc improvements (#258) --- CONTRIBUTING.md | 12 ++++++------ deno.lock | 14 ++++++++++---- package.json | 3 ++- src/global.ts | 6 ++---- src/utils/crypto.ts | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 src/utils/crypto.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 77d0484c..282485a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,14 +70,14 @@ The API is documented under OpenAPI specification and can be found at the follow /api/oas.json ``` -There are several ways to interact with the API, we will cover its use with [Scalar](https://scalar.com). +You can get a quick overview with: +- [Swagger Editor](https://editor.swagger.io/?url=https://jspaste.eu/api/oas.json) +- [Scalar Client](https://client.scalar.com/?url=https://jspaste.eu/api/oas.json) -We recommend using the desktop application, however, you can also use the -[web-based environment](https://client.scalar.com). (you may need to disable the CORS Proxy) +If using Scalar Client, disable the CORS proxy and follow these steps to import the +instance `oas.json`..: -Follow these steps to import the instance's `oas.json` to Scalar..: - -![](https://static.x.inetol.net/jspaste/backend/scalar-t1.gif) +![](https://static.x.inetol.net/jspaste/backend/scalar-t1.webp) ## Maintenance diff --git a/deno.lock b/deno.lock index c1fdf103..faec8a2d 100644 --- a/deno.lock +++ b/deno.lock @@ -10,12 +10,13 @@ "npm:@jsr/std__collections@^1.1.3": "1.1.3", "npm:@jsr/std__crypto@^1.0.5": "1.0.5", "npm:@jsr/std__dotenv@~0.225.6": "0.225.6", + "npm:@jsr/std__encoding@^1.0.10": "1.0.10", "npm:@jsr/std__fmt@^1.0.8": "1.0.8", "npm:@jsr/std__fs@^1.0.21": "1.0.21", "npm:@jsr/std__streams@^1.0.16": "1.0.16", "npm:@jsr/std__ulid@1": "1.0.0", "npm:@types/node@^25.0.3": "25.0.3", - "npm:arkenv@~0.8.1": "0.8.1_arktype@2.1.29", + "npm:arkenv@~0.8.2": "0.8.2_arktype@2.1.29", "npm:arktype@^2.1.29": "2.1.29", "npm:hono-openapi@^1.1.2": "1.1.2_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.1.29__quansync@0.2.11_@standard-community+standard-openapi@0.2.9__@standard-community+standard-json@0.3.5___@standard-schema+spec@1.1.0___@types+json-schema@7.0.15___arktype@2.1.29___quansync@0.2.11__@standard-schema+spec@1.1.0__arktype@2.1.29__openapi-types@12.1.3__@types+json-schema@7.0.15_@types+json-schema@7.0.15_openapi-types@12.1.3_arktype@2.1.29", "npm:nanoid@^5.1.6": "5.1.6", @@ -152,6 +153,10 @@ "integrity": "sha512-rqh5RrHccbyzmP4v1/vqUyYy4dqopjTRgW8bJqk2ZXTKBbvpmMjPxJ+xy+YAk6XnEvtPCPAgqbFhHWcomjnX+w==", "tarball": "https://npm.jsr.io/~/11/@jsr/std__dotenv/0.225.6.tgz" }, + "@jsr/std__encoding@1.0.10": { + "integrity": "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz" + }, "@jsr/std__fmt@1.0.8": { "integrity": "sha512-miZHzj9OgjuajrcMKzpqNVwFb9O71UHZzV/FHVq0E0Uwmv/1JqXgmXAoBNPrn+MP0fHT3mMgaZ6XvQO7dam67Q==", "tarball": "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.8.tgz" @@ -309,8 +314,8 @@ "undici-types" ] }, - "arkenv@0.8.1_arktype@2.1.29": { - "integrity": "sha512-6aGadHN2nuWuRNWcmNc/CfdlUak6jeZi/WBLLWN/LvBFY8aR+FSMJE6x9SUS9L1m+qp94MTVyKpi3tDzo+SoJw==", + "arkenv@0.8.2_arktype@2.1.29": { + "integrity": "sha512-FD5p4G0TcgeFXm6aMDEUVGd1hL6Hytz8bzynnwTXkwQ1s9QZPGKZiZHq6H7G0tWx/UryvgE5QNFp1S7eWU5lKQ==", "dependencies": [ "arktype" ] @@ -393,12 +398,13 @@ "npm:@jsr/std__collections@^1.1.3", "npm:@jsr/std__crypto@^1.0.5", "npm:@jsr/std__dotenv@~0.225.6", + "npm:@jsr/std__encoding@^1.0.10", "npm:@jsr/std__fmt@^1.0.8", "npm:@jsr/std__fs@^1.0.21", "npm:@jsr/std__streams@^1.0.16", "npm:@jsr/std__ulid@1", "npm:@types/node@^25.0.3", - "npm:arkenv@~0.8.1", + "npm:arkenv@~0.8.2", "npm:arktype@^2.1.29", "npm:hono-openapi@^1.1.2", "npm:nanoid@^5.1.6", diff --git a/package.json b/package.json index e2bac7fb..4e5652ad 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,13 @@ "@std/collections": "npm:@jsr/std__collections@^1.1.3", "@std/crypto": "npm:@jsr/std__crypto@^1.0.5", "@std/dotenv": "npm:@jsr/std__dotenv@~0.225.6", + "@std/encoding": "npm:@jsr/std__encoding@^1.0.10", "@std/fmt": "npm:@jsr/std__fmt@^1.0.8", "@std/fs": "npm:@jsr/std__fs@^1.0.21", "@std/streams": "npm:@jsr/std__streams@^1.0.16", "@std/ulid": "npm:@jsr/std__ulid@^1.0.0", "@types/node": "npm:@types/node@^25.0.3", - "arkenv": "npm:arkenv@~0.8.1", + "arkenv": "npm:arkenv@~0.8.2", "arktype": "npm:arktype@^2.1.29", "biome": "npm:@biomejs/biome@2.3.11", "btree": "npm:sorted-btree@^2.1.0", diff --git a/src/global.ts b/src/global.ts index e4a175b3..e8fe10ca 100644 --- a/src/global.ts +++ b/src/global.ts @@ -79,10 +79,8 @@ export const constant = { instant: Temporal.Now.instant }, http: STATUS_CODES as Record, - text: { - decode: new TextDecoder().decode, - encode: new TextEncoder().encode - }, + textEncoder: new TextEncoder(), + textDecoder: new TextDecoder(), ulid: { userRoot: "0000000000FFFF000000000000" } diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts new file mode 100644 index 00000000..e258a336 --- /dev/null +++ b/src/utils/crypto.ts @@ -0,0 +1,32 @@ +import { crypto } from "@std/crypto"; +import { decodeAscii85, encodeAscii85 } from "@std/encoding"; +import { constant } from "../global.ts"; + +export const generateHash = async (input: string, salt?: Uint8Array) => { + const defaultSalt = salt ?? crypto.getRandomValues(new Uint8Array(4)); + + const dataBytes = constant.textEncoder.encode(input); + const combo = new Uint8Array(defaultSalt.length + dataBytes.length); + combo.set(defaultSalt, 0); + combo.set(dataBytes, defaultSalt.length); + + const hash = await crypto.subtle.digest("BLAKE3", combo); + const encodedHash = encodeAscii85(hash, { standard: "Z85" }); + + return { + combo: `${encodedHash} ${encodeAscii85(defaultSalt, { standard: "Z85" })}`, + hash: encodedHash, + salt: defaultSalt + }; +}; + +export const verifyHash = async (input: string, combo: string): Promise => { + const [hash, salt] = combo.split(" "); + if (!(hash && salt)) { + throw new Error("Invalid hash combo"); + } + + const { hash: inputHash } = await generateHash(input, decodeAscii85(salt, { standard: "Z85" })); + + return inputHash === hash; +}; From 7b38eef9630f00b7008a0b7f788768390fc588f3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:22:33 +0000 Subject: [PATCH 38/47] Update dependency rolldown to v1.0.0-beta.59 (#260) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- deno.lock | 68 ++++++++++++++++++++++++++-------------------------- package.json | 2 +- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/deno.lock b/deno.lock index faec8a2d..c5abb0d9 100644 --- a/deno.lock +++ b/deno.lock @@ -20,7 +20,7 @@ "npm:arktype@^2.1.29": "2.1.29", "npm:hono-openapi@^1.1.2": "1.1.2_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.1.29__quansync@0.2.11_@standard-community+standard-openapi@0.2.9__@standard-community+standard-json@0.3.5___@standard-schema+spec@1.1.0___@types+json-schema@7.0.15___arktype@2.1.29___quansync@0.2.11__@standard-schema+spec@1.1.0__arktype@2.1.29__openapi-types@12.1.3__@types+json-schema@7.0.15_@types+json-schema@7.0.15_openapi-types@12.1.3_arktype@2.1.29", "npm:nanoid@^5.1.6": "5.1.6", - "npm:rolldown@1.0.0-beta.58": "1.0.0-beta.58", + "npm:rolldown@1.0.0-beta.59": "1.0.0-beta.59", "npm:sorted-btree@^2.1.0": "2.1.0" }, "npm": { @@ -199,78 +199,78 @@ "@tybys/wasm-util" ] }, - "@oxc-project/types@0.106.0": { - "integrity": "sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg==" + "@oxc-project/types@0.107.0": { + "integrity": "sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==" }, - "@rolldown/binding-android-arm64@1.0.0-beta.58": { - "integrity": "sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug==", + "@rolldown/binding-android-arm64@1.0.0-beta.59": { + "integrity": "sha512-6yLLgyswYwiCfls9+hoNFY9F8TQdwo15hpXDHzlAR0X/GojeKF+AuNcXjYNbOJ4zjl/5D6lliE8CbpB5t1OWIQ==", "os": ["android"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-arm64@1.0.0-beta.58": { - "integrity": "sha512-wFxUymI/5R8bH8qZFYDfAxAN9CyISEIYke+95oZPiv6EWo88aa5rskjVcCpKA532R+klFmdqjbbaD56GNmTF4Q==", + "@rolldown/binding-darwin-arm64@1.0.0-beta.59": { + "integrity": "sha512-hqGXRc162qCCIOAcHN2Cw4eXiVTwYsMFLOhAy1IG2CxY+dwc/l4Ga+dLPkLor3Ikqy5WDn+7kxHbbh6EmshEpQ==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-x64@1.0.0-beta.58": { - "integrity": "sha512-ybp3MkPj23VDV9PhtRwdU5qrGhlViWRV5BjKwO6epaSlUD5lW0WyY+roN3ZAzbma/9RrMTgZ/a/gtQq8YXOcqw==", + "@rolldown/binding-darwin-x64@1.0.0-beta.59": { + "integrity": "sha512-ezvvGuhteE15JmMhJW0wS7BaXmhwLy1YHeEwievYaPC1PgGD86wgBKfOpHr9tSKllAXbCe0BeeMvasscWLhKdA==", "os": ["darwin"], "cpu": ["x64"] }, - "@rolldown/binding-freebsd-x64@1.0.0-beta.58": { - "integrity": "sha512-Evxj3yh7FWvyklUYZa0qTVT9N2zX9TPDqGF056hl8hlCZ9/ndQ2xMv6uw9PD1VlLpukbsqL+/C6M0qwipL0QMg==", + "@rolldown/binding-freebsd-x64@1.0.0-beta.59": { + "integrity": "sha512-4fhKVJiEYVd5n6no/mrL3LZ9kByfCGwmONOrdtvx8DJGDQhehH/q3RfhG3V/4jGKhpXgbDjpIjkkFdybCTcgew==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.58": { - "integrity": "sha512-tYeXprDOrEgVHUbPXH6MPso4cM/c6RTkmJNICMQlYdki4hGMh92aj3yU6CKs+4X5gfG0yj5kVUw/L4M685SYag==", + "@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59": { + "integrity": "sha512-T3Y52sW6JAhvIqArBw+wtjNU1Ieaz4g0NBxyjSJoW971nZJBZygNlSYx78G4cwkCmo1dYTciTPDOnQygLV23pA==", "os": ["linux"], "cpu": ["arm"] }, - "@rolldown/binding-linux-arm64-gnu@1.0.0-beta.58": { - "integrity": "sha512-N78vmZzP6zG967Ohr+MasCjmKtis0geZ1SOVmxrA0/bklTQSzH5kHEjW5Qn+i1taFno6GEre1E40v0wuWsNOQw==", + "@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59": { + "integrity": "sha512-NIW40jQDSQap2KDdmm9z3B/4OzWJ6trf8dwx3FD74kcQb3v34ThsBFTtzE5KjDuxnxgUlV+DkAu+XgSMKrgufw==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-arm64-musl@1.0.0-beta.58": { - "integrity": "sha512-l+p4QVtG72C7wI2SIkNQw/KQtSjuYwS3rV6AKcWrRBF62ClsFUcif5vLaZIEbPrCXu5OFRXigXFJnxYsVVZqdQ==", + "@rolldown/binding-linux-arm64-musl@1.0.0-beta.59": { + "integrity": "sha512-CCKEk+H+8c0WGe/8n1E20n85Tq4Pv+HNAbjP1KfUXW+01aCWSMjU56ChNrM2tvHnXicfm7QRNoZyfY8cWh7jLQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-x64-gnu@1.0.0-beta.58": { - "integrity": "sha512-urzJX0HrXxIh0FfxwWRjfPCMeInU9qsImLQxHBgLp5ivji1EEUnOfux8KxPPnRQthJyneBrN2LeqUix9DYrNaQ==", + "@rolldown/binding-linux-x64-gnu@1.0.0-beta.59": { + "integrity": "sha512-VlfwJ/HCskPmQi8R0JuAFndySKVFX7yPhE658o27cjSDWWbXVtGkSbwaxstii7Q+3Rz87ZXN+HLnb1kd4R9Img==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-linux-x64-musl@1.0.0-beta.58": { - "integrity": "sha512-7ijfVK3GISnXIwq/1FZo+KyAUJjL3kWPJ7rViAL6MWeEBhEgRzJ0yEd9I8N9aut8Y8ab+EKFJyRNMWZuUBwQ0A==", + "@rolldown/binding-linux-x64-musl@1.0.0-beta.59": { + "integrity": "sha512-kuO92hTRyGy0Ts3Nsqll0rfO8eFsEJe9dGQGktkQnZ2hrJrDVN0y419dMgKy/gB2S2o7F2dpWhpfQOBehZPwVA==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-openharmony-arm64@1.0.0-beta.58": { - "integrity": "sha512-/m7sKZCS+cUULbzyJTIlv8JbjNohxbpAOA6cM+lgWgqVzPee3U6jpwydrib328JFN/gF9A99IZEnuGYqEDJdww==", + "@rolldown/binding-openharmony-arm64@1.0.0-beta.59": { + "integrity": "sha512-PXAebvNL4sYfCqi8LdY4qyFRacrRoiPZLo3NoUmiTxm7MPtYYR8CNtBGNokqDmMuZIQIecRaD/jbmFAIDz7DxQ==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@rolldown/binding-wasm32-wasi@1.0.0-beta.58": { - "integrity": "sha512-6SZk7zMgv+y3wFFQ9qE5P9NnRHcRsptL1ypmudD26PDY+PvFCvfHRkJNfclWnvacVGxjowr7JOL3a9fd1wWhUw==", + "@rolldown/binding-wasm32-wasi@1.0.0-beta.59": { + "integrity": "sha512-yJoklQg7XIZq8nAg0bbkEXcDK6sfpjxQGxpg2Nd6ERNtvg+eOaEBRgPww0BVTrYFQzje1pB5qPwC2VnJHT3koQ==", "dependencies": [ "@napi-rs/wasm-runtime" ], "cpu": ["wasm32"] }, - "@rolldown/binding-win32-arm64-msvc@1.0.0-beta.58": { - "integrity": "sha512-sFqfYPnBZ6xBhMkadB7UD0yjEDRvs7ipR3nCggblN+N4ODCXY6qhg/bKL39+W+dgQybL7ErD4EGERVbW9DAWvg==", + "@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59": { + "integrity": "sha512-ljZ4+McmCbIuZwEBaoGtiG8Rq2nJjaXEnLEIx+usWetXn1ECjXY0LAhkELxOV6ytv4ensEmoJJ8nXg47hRMjlw==", "os": ["win32"], "cpu": ["arm64"] }, - "@rolldown/binding-win32-x64-msvc@1.0.0-beta.58": { - "integrity": "sha512-AnFWJdAqB8+IDPcGrATYs67Kik/6tnndNJV2jGRmwlbeNiQQ8GhRJU8ETRlINfII0pqi9k4WWLnb00p1QCxw/Q==", + "@rolldown/binding-win32-x64-msvc@1.0.0-beta.59": { + "integrity": "sha512-bMY4tTIwbdZljW+xe/ln1hvs0SRitahQSXfWtvgAtIzgSX9Ar7KqJzU7lRm33YTRFIHLULRi53yNjw9nJGd6uQ==", "os": ["win32"], "cpu": ["x64"] }, - "@rolldown/pluginutils@1.0.0-beta.58": { - "integrity": "sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==" + "@rolldown/pluginutils@1.0.0-beta.59": { + "integrity": "sha512-aoh6LAJRyhtazs98ydgpNOYstxUlsOV1KJXcpf/0c0vFcUA8uyd/hwKRhqE/AAPNqAho9RliGsvitCoOzREoVA==" }, "@standard-community/standard-json@0.3.5_@standard-schema+spec@1.1.0_@types+json-schema@7.0.15_arktype@2.1.29_quansync@0.2.11": { "integrity": "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==", @@ -353,8 +353,8 @@ "quansync@0.2.11": { "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==" }, - "rolldown@1.0.0-beta.58": { - "integrity": "sha512-v1FCjMZCan7f+xGAHBi+mqiE4MlH7I+SXEHSQSJoMOGNNB2UYtvMiejsq9YuUOiZjNeUeV/a21nSFbrUR+4ZCQ==", + "rolldown@1.0.0-beta.59": { + "integrity": "sha512-Slm000Gd8/AO9z4Kxl4r8mp/iakrbAuJ1L+7ddpkNxgQ+Vf37WPvY63l3oeyZcfuPD1DRrUYBsRPIXSOhvOsmw==", "dependencies": [ "@oxc-project/types", "@rolldown/pluginutils" @@ -408,7 +408,7 @@ "npm:arktype@^2.1.29", "npm:hono-openapi@^1.1.2", "npm:nanoid@^5.1.6", - "npm:rolldown@1.0.0-beta.58", + "npm:rolldown@1.0.0-beta.59", "npm:sorted-btree@^2.1.0" ] } diff --git a/package.json b/package.json index 4e5652ad..e047226e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "biome": "npm:@biomejs/biome@2.3.11", "btree": "npm:sorted-btree@^2.1.0", "nanoid": "npm:nanoid@^5.1.6", - "rolldown": "npm:rolldown@1.0.0-beta.58" + "rolldown": "npm:rolldown@1.0.0-beta.59" }, "imports": { "#/*": [ From 8dafb17c7289a1e46ab5b0999ca7ae524be2cdbb Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Wed, 7 Jan 2026 12:05:38 +0100 Subject: [PATCH 39/47] Optimizar store.dispose (#261) --- deno.lock | 9 ++---- package.json | 1 - src/global.ts | 3 +- src/init.ts | 28 ++++++++++------ src/task.ts | 4 +-- src/utils/btree.ts | 4 --- src/utils/ipq.ts | 80 ---------------------------------------------- 7 files changed, 24 insertions(+), 105 deletions(-) delete mode 100644 src/utils/btree.ts delete mode 100644 src/utils/ipq.ts diff --git a/deno.lock b/deno.lock index c5abb0d9..1f6da611 100644 --- a/deno.lock +++ b/deno.lock @@ -20,8 +20,7 @@ "npm:arktype@^2.1.29": "2.1.29", "npm:hono-openapi@^1.1.2": "1.1.2_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.1.29__quansync@0.2.11_@standard-community+standard-openapi@0.2.9__@standard-community+standard-json@0.3.5___@standard-schema+spec@1.1.0___@types+json-schema@7.0.15___arktype@2.1.29___quansync@0.2.11__@standard-schema+spec@1.1.0__arktype@2.1.29__openapi-types@12.1.3__@types+json-schema@7.0.15_@types+json-schema@7.0.15_openapi-types@12.1.3_arktype@2.1.29", "npm:nanoid@^5.1.6": "5.1.6", - "npm:rolldown@1.0.0-beta.59": "1.0.0-beta.59", - "npm:sorted-btree@^2.1.0": "2.1.0" + "npm:rolldown@1.0.0-beta.59": "1.0.0-beta.59" }, "npm": { "@ark/schema@0.56.0": { @@ -376,9 +375,6 @@ ], "bin": true }, - "sorted-btree@2.1.0": { - "integrity": "sha512-AtYXy3lL+5jrATpbymC2bM8anN/3maLkmVCd94MzypnKjokfCid/zeS3rvXedv7W6ffSfqKIGdz3UaJPWRBZ0g==" - }, "tslib@2.8.1": { "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, @@ -408,8 +404,7 @@ "npm:arktype@^2.1.29", "npm:hono-openapi@^1.1.2", "npm:nanoid@^5.1.6", - "npm:rolldown@1.0.0-beta.59", - "npm:sorted-btree@^2.1.0" + "npm:rolldown@1.0.0-beta.59" ] } } diff --git a/package.json b/package.json index e047226e..a0cd7a5e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "arkenv": "npm:arkenv@~0.8.2", "arktype": "npm:arktype@^2.1.29", "biome": "npm:@biomejs/biome@2.3.11", - "btree": "npm:sorted-btree@^2.1.0", "nanoid": "npm:nanoid@^5.1.6", "rolldown": "npm:rolldown@1.0.0-beta.59" }, diff --git a/src/global.ts b/src/global.ts index e8fe10ca..2b6258ec 100644 --- a/src/global.ts +++ b/src/global.ts @@ -7,7 +7,6 @@ import { type } from "arktype"; import { customAlphabet } from "nanoid"; import type { Database } from "#db/database"; import { humanizeSize, humanizeTime } from "#util/humanize.ts"; -import { IPQ } from "#util/ipq.ts"; export const mutable = { database: undefined as unknown as Database, @@ -72,7 +71,7 @@ export const constant = { }, store: { statements: new LruCache(200), - dispose: new IPQ Promise>() + dispose: new Map Promise]>() }, temporal: { utc: () => Temporal.Now.zonedDateTimeISO("Etc/UTC"), diff --git a/src/init.ts b/src/init.ts index 2f937c26..cfa0f034 100644 --- a/src/init.ts +++ b/src/init.ts @@ -19,28 +19,31 @@ const initDirStruct = async (): Promise => { const initHTTPServer = async (handler?: Deno.ServeHandler): Promise => { const id = "__httpServer"; - await constant.store.dispose.get(id)?.(); + await constant.store.dispose.get(id)?.[1](); mutable.http = server({ handler: handler }); - constant.store.dispose.set(10, id, async () => { - mutable.http?.unref(); + constant.store.dispose.set(id, [ + 10, + async () => { + mutable.http?.unref(); - // Deno.serve will deadlock on shutdown under pressure - await mutable.http?.shutdown(); - }); + // Deno.serve will deadlock on shutdown under pressure + await mutable.http?.shutdown(); + } + ]); }; const initDatabase = async (): Promise => { const id = "__databaseServer"; - await constant.store.dispose.get(id)?.(); + await constant.store.dispose.get(id)?.[1](); mutable.database = new Database(); - constant.store.dispose.set(0, id, async () => mutable.database[Symbol.dispose]()); + constant.store.dispose.set(id, [0, async () => mutable.database[Symbol.dispose]()]); mutable.database.migration(); @@ -63,8 +66,13 @@ export const init = async () => { log.debug(`Received ${signal}.`); + const storeDispose = constant.store.dispose + .entries() + .toArray() + .sort(([, [pa]], [, [pb]]) => pb - pa); + try { - for (const [, key, dispose] of constant.store.dispose.drain()) { + for (const [key, [, dispose]] of storeDispose) { log.debug(`Closing "${key}"...`); try { @@ -91,6 +99,8 @@ export const init = async () => { await Promise.all([initTask(), initHTTPServer(router().fetch)]); } catch (error) { log.error(error); + + Deno.exitCode = 1; Deno.kill(Deno.pid, "SIGTERM"); } }; diff --git a/src/task.ts b/src/task.ts index 1994358c..1d26f702 100644 --- a/src/task.ts +++ b/src/task.ts @@ -28,7 +28,7 @@ export const taskRegister = ( const id = `__task-${options.name}`; - constant.store.dispose.get(id)?.(); + constant.store.dispose.get(id)?.[1](); try { Deno.cron(options.name, expression, { signal: abort.signal }, () => trigger(callback, options)); @@ -36,7 +36,7 @@ export const taskRegister = ( log.error(`Failed to register "${options.name}"..:`, error); } - constant.store.dispose.set(100, id, async () => abort.abort()); + constant.store.dispose.set(id, [100, async () => abort.abort()]); log.debug(`Registered "${options.name}".`); }; diff --git a/src/utils/btree.ts b/src/utils/btree.ts deleted file mode 100644 index 8f40bf02..00000000 --- a/src/utils/btree.ts +++ /dev/null @@ -1,4 +0,0 @@ -import __BTree from "btree"; - -// https://github.com/qwertie/btree-typescript/issues/36 -export const BTree = (__BTree as unknown as { default: typeof __BTree }).default; diff --git a/src/utils/ipq.ts b/src/utils/ipq.ts deleted file mode 100644 index 47e9e01c..00000000 --- a/src/utils/ipq.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { BTree } from "./btree.ts"; - -export class IPQ { - private readonly tree = new BTree>(); - private readonly priority = new Map(); - - public set(priority: number, key: K, value: V): void { - const oldPriority = this.priority.get(key); - if (oldPriority !== undefined) { - const entries = this.tree.get(oldPriority); - if (entries) { - entries.delete(key); - if (entries.size === 0) { - this.tree.delete(oldPriority); - } - } - } - - let entries = this.tree.get(priority); - if (!entries) { - entries = new Map(); - this.tree.set(priority, entries); - } - entries.set(key, value); - this.priority.set(key, priority); - } - - public get(key: K): V | undefined { - const priority = this.priority.get(key); - if (priority === undefined) return; - - return this.tree.get(priority)?.get(key); - } - - public has(key: K): boolean { - return this.priority.has(key); - } - - public delete(key: K): boolean { - const priority = this.priority.get(key); - if (priority === undefined) return false; - - const entries = this.tree.get(priority); - if (entries) { - entries.delete(key); - if (entries.size === 0) { - this.tree.delete(priority); - } - } - this.priority.delete(key); - return true; - } - - public *entries(): Generator<[number, K, V], void, unknown> { - for (const [priority, entries] of this.tree.entries()) { - for (const [key, value] of entries) { - yield [priority, key, value]; - } - } - } - - public *drain(): Generator<[number, K, V], void, unknown> { - while (this.tree.size > 0) { - const priority = this.tree.maxKey()!; - const entries = this.tree.get(priority)!; - - for (const [key, value] of entries) { - this.priority.delete(key); - yield [priority, key, value]; - } - - this.tree.delete(priority); - } - } - - public [Symbol.dispose](): void { - this.tree.clear(); - this.priority.clear(); - } -} From 9cbb32625f61b63be36961c58022264cbebe764c Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Thu, 8 Jan 2026 15:09:09 +0100 Subject: [PATCH 40/47] Use hono tiny preset (#263) --- biome.json | 8 ++++++++ deno.lock | 8 ++++---- package.json | 2 +- src/endpoints/document/v1/delete.ts | 2 +- src/endpoints/document/v1/get.ts | 2 +- src/endpoints/document/v1/index.ts | 2 +- src/endpoints/document/v1/patch.ts | 2 +- src/endpoints/document/v1/post.ts | 2 +- src/endpoints/legacy/v2/documents/access.route.ts | 2 +- src/endpoints/legacy/v2/documents/accessRaw.route.ts | 2 +- src/endpoints/legacy/v2/documents/edit.route.ts | 2 +- src/endpoints/legacy/v2/documents/exists.route.ts | 2 +- src/endpoints/legacy/v2/documents/index.ts | 2 +- src/endpoints/legacy/v2/documents/publish.route.ts | 2 +- src/endpoints/legacy/v2/documents/remove.route.ts | 2 +- src/endpoints/user/v1/create.ts | 2 +- src/endpoints/user/v1/index.ts | 2 +- src/http/router.ts | 2 +- 18 files changed, 28 insertions(+), 20 deletions(-) diff --git a/biome.json b/biome.json index cd179e29..b022bbd0 100644 --- a/biome.json +++ b/biome.json @@ -79,6 +79,14 @@ "noNestedTernary": "error", "noParameterAssign": "error", "noParameterProperties": "error", + "noRestrictedImports": { + "level": "error", + "options": { + "paths": { + "@hono/hono": "Use `@hono/hono/tiny` instead" + } + } + }, "noSubstr": "error", "noUnusedTemplateLiteral": "error", "noUselessElse": "error", diff --git a/deno.lock b/deno.lock index 1f6da611..d5725b59 100644 --- a/deno.lock +++ b/deno.lock @@ -16,7 +16,7 @@ "npm:@jsr/std__streams@^1.0.16": "1.0.16", "npm:@jsr/std__ulid@1": "1.0.0", "npm:@types/node@^25.0.3": "25.0.3", - "npm:arkenv@~0.8.2": "0.8.2_arktype@2.1.29", + "npm:arkenv@~0.8.3": "0.8.3_arktype@2.1.29", "npm:arktype@^2.1.29": "2.1.29", "npm:hono-openapi@^1.1.2": "1.1.2_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.1.29__quansync@0.2.11_@standard-community+standard-openapi@0.2.9__@standard-community+standard-json@0.3.5___@standard-schema+spec@1.1.0___@types+json-schema@7.0.15___arktype@2.1.29___quansync@0.2.11__@standard-schema+spec@1.1.0__arktype@2.1.29__openapi-types@12.1.3__@types+json-schema@7.0.15_@types+json-schema@7.0.15_openapi-types@12.1.3_arktype@2.1.29", "npm:nanoid@^5.1.6": "5.1.6", @@ -313,8 +313,8 @@ "undici-types" ] }, - "arkenv@0.8.2_arktype@2.1.29": { - "integrity": "sha512-FD5p4G0TcgeFXm6aMDEUVGd1hL6Hytz8bzynnwTXkwQ1s9QZPGKZiZHq6H7G0tWx/UryvgE5QNFp1S7eWU5lKQ==", + "arkenv@0.8.3_arktype@2.1.29": { + "integrity": "sha512-fndPYpIZ/EvARTXabWG5H+gKxlJEbPgTRvXH8htimmCbdBfEXZsSOgObwdiCCCcBz33tJAYk88goDtj0Ao99NA==", "dependencies": [ "arktype" ] @@ -400,7 +400,7 @@ "npm:@jsr/std__streams@^1.0.16", "npm:@jsr/std__ulid@1", "npm:@types/node@^25.0.3", - "npm:arkenv@~0.8.2", + "npm:arkenv@~0.8.3", "npm:arktype@^2.1.29", "npm:hono-openapi@^1.1.2", "npm:nanoid@^5.1.6", diff --git a/package.json b/package.json index a0cd7a5e..d13eddb8 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@std/streams": "npm:@jsr/std__streams@^1.0.16", "@std/ulid": "npm:@jsr/std__ulid@^1.0.0", "@types/node": "npm:@types/node@^25.0.3", - "arkenv": "npm:arkenv@~0.8.2", + "arkenv": "npm:arkenv@~0.8.3", "arktype": "npm:arktype@^2.1.29", "biome": "npm:@biomejs/biome@2.3.11", "nanoid": "npm:nanoid@^5.1.6", diff --git a/src/endpoints/document/v1/delete.ts b/src/endpoints/document/v1/delete.ts index 36225628..f331e977 100644 --- a/src/endpoints/document/v1/delete.ts +++ b/src/endpoints/document/v1/delete.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import { describeRoute, validator } from "@hono/openapi"; import { type } from "arktype"; import { constant, mutable } from "#/global.ts"; diff --git a/src/endpoints/document/v1/get.ts b/src/endpoints/document/v1/get.ts index 3370918c..44a0a5ac 100644 --- a/src/endpoints/document/v1/get.ts +++ b/src/endpoints/document/v1/get.ts @@ -1,5 +1,5 @@ -import { Hono } from "@hono/hono"; import { stream } from "@hono/hono/streaming"; +import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { decodeTime } from "@std/ulid"; import { type } from "arktype"; diff --git a/src/endpoints/document/v1/index.ts b/src/endpoints/document/v1/index.ts index 008712d9..6c7ee1b4 100644 --- a/src/endpoints/document/v1/index.ts +++ b/src/endpoints/document/v1/index.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import type { Env } from "#http/type.ts"; import delete_ from "./delete.ts"; import get from "./get.ts"; diff --git a/src/endpoints/document/v1/patch.ts b/src/endpoints/document/v1/patch.ts index 3f409511..84f5fe7e 100644 --- a/src/endpoints/document/v1/patch.ts +++ b/src/endpoints/document/v1/patch.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; import { constant, mutable } from "#/global.ts"; diff --git a/src/endpoints/document/v1/post.ts b/src/endpoints/document/v1/post.ts index 59019544..ab65a3f8 100644 --- a/src/endpoints/document/v1/post.ts +++ b/src/endpoints/document/v1/post.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { monotonicUlid } from "@std/ulid"; import { type } from "arktype"; diff --git a/src/endpoints/legacy/v2/documents/access.route.ts b/src/endpoints/legacy/v2/documents/access.route.ts index 167058e4..9fd47bb0 100644 --- a/src/endpoints/legacy/v2/documents/access.route.ts +++ b/src/endpoints/legacy/v2/documents/access.route.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { toText } from "@std/streams"; import { type } from "arktype"; diff --git a/src/endpoints/legacy/v2/documents/accessRaw.route.ts b/src/endpoints/legacy/v2/documents/accessRaw.route.ts index a6d20a33..7d6f425b 100644 --- a/src/endpoints/legacy/v2/documents/accessRaw.route.ts +++ b/src/endpoints/legacy/v2/documents/accessRaw.route.ts @@ -1,5 +1,5 @@ -import { Hono } from "@hono/hono"; import { stream } from "@hono/hono/streaming"; +import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; import { constant, mutable } from "#/global.ts"; diff --git a/src/endpoints/legacy/v2/documents/edit.route.ts b/src/endpoints/legacy/v2/documents/edit.route.ts index a1c70097..ab64b5f9 100644 --- a/src/endpoints/legacy/v2/documents/edit.route.ts +++ b/src/endpoints/legacy/v2/documents/edit.route.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; import { constant, mutable } from "#/global.ts"; diff --git a/src/endpoints/legacy/v2/documents/exists.route.ts b/src/endpoints/legacy/v2/documents/exists.route.ts index 2d4795da..0a50d31f 100644 --- a/src/endpoints/legacy/v2/documents/exists.route.ts +++ b/src/endpoints/legacy/v2/documents/exists.route.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; import { constant, mutable } from "#/global.ts"; diff --git a/src/endpoints/legacy/v2/documents/index.ts b/src/endpoints/legacy/v2/documents/index.ts index 0f563c3c..cc3db7f9 100644 --- a/src/endpoints/legacy/v2/documents/index.ts +++ b/src/endpoints/legacy/v2/documents/index.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import type { Env } from "#http/type.ts"; import access from "./access.route.ts"; import accessRaw from "./accessRaw.route.ts"; diff --git a/src/endpoints/legacy/v2/documents/publish.route.ts b/src/endpoints/legacy/v2/documents/publish.route.ts index e485619f..5c4754a9 100644 --- a/src/endpoints/legacy/v2/documents/publish.route.ts +++ b/src/endpoints/legacy/v2/documents/publish.route.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { monotonicUlid } from "@std/ulid"; import { type } from "arktype"; diff --git a/src/endpoints/legacy/v2/documents/remove.route.ts b/src/endpoints/legacy/v2/documents/remove.route.ts index 34f1e53b..e1d4cbe9 100644 --- a/src/endpoints/legacy/v2/documents/remove.route.ts +++ b/src/endpoints/legacy/v2/documents/remove.route.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; import { constant, mutable } from "#/global.ts"; diff --git a/src/endpoints/user/v1/create.ts b/src/endpoints/user/v1/create.ts index 2cba0423..2bae3a18 100644 --- a/src/endpoints/user/v1/create.ts +++ b/src/endpoints/user/v1/create.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver } from "@hono/openapi"; import { type } from "arktype"; import { constant, mutable } from "#/global.ts"; diff --git a/src/endpoints/user/v1/index.ts b/src/endpoints/user/v1/index.ts index 4c089455..3aa34270 100644 --- a/src/endpoints/user/v1/index.ts +++ b/src/endpoints/user/v1/index.ts @@ -1,4 +1,4 @@ -import { Hono } from "@hono/hono"; +import { Hono } from "@hono/hono/tiny"; import type { Env } from "#http/type.ts"; import create from "./create.ts"; diff --git a/src/http/router.ts b/src/http/router.ts index 384839a0..0fb48b15 100644 --- a/src/http/router.ts +++ b/src/http/router.ts @@ -1,6 +1,6 @@ -import { Hono } from "@hono/hono"; import { cors } from "@hono/hono/cors"; import { HTTPException } from "@hono/hono/http-exception"; +import { Hono } from "@hono/hono/tiny"; import { openAPIRouteHandler } from "@hono/openapi"; import { v1DocumentRouter } from "#endpoint/document/v1/index.ts"; import { v2LegacyDocumentRouter } from "#endpoint/legacy/v2/documents/index.ts"; From 8136bc10b072902db3cbcffb118dbc66044a5a7b Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Thu, 8 Jan 2026 15:43:37 +0100 Subject: [PATCH 41/47] List user documents endpoint (#262) --- src/endpoints/document/v1/get.ts | 22 +++++--- src/endpoints/document/v1/index.ts | 2 + src/endpoints/document/v1/list.ts | 56 +++++++++++++++++++ src/endpoints/document/v1/patch.ts | 8 ++- src/endpoints/document/v1/post.ts | 15 +++-- .../legacy/v2/documents/access.route.ts | 6 +- .../legacy/v2/documents/accessRaw.route.ts | 6 +- .../legacy/v2/documents/edit.route.ts | 22 ++++---- .../legacy/v2/documents/exists.route.ts | 4 +- .../legacy/v2/documents/publish.route.ts | 26 ++++----- .../legacy/v2/documents/remove.route.ts | 6 +- src/endpoints/user/v1/create.ts | 8 +-- src/utils/validator/document.ts | 17 +++++- src/utils/validator/shared.ts | 6 ++ 14 files changed, 148 insertions(+), 56 deletions(-) create mode 100644 src/endpoints/document/v1/list.ts create mode 100644 src/utils/validator/shared.ts diff --git a/src/endpoints/document/v1/get.ts b/src/endpoints/document/v1/get.ts index 44a0a5ac..1aa9f38e 100644 --- a/src/endpoints/document/v1/get.ts +++ b/src/endpoints/document/v1/get.ts @@ -14,39 +14,47 @@ import { validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; +import { validatorCreationTimestamp } from "#util/validator/shared.ts"; const schemaParam = type({ name: validatorDocumentName }); +const schemaQuery = type({ + "dl?": validatorDocumentDownload +}); + const schemaHeader = type({ "x-jspaste-password?": validatorDocumentPassword }); -const schemaQuery = type({ - "dl?": validatorDocumentDownload -}); +const schemaBodyResponse = await resolver(type.unknown).toOpenAPISchema(); -const schemaResponse = resolver(type(type.unknown)); +const schemaHeaderResponse = await resolver( + type({ + "x-jspaste-created": validatorCreationTimestamp + }) +).toOpenAPISchema(); export default new Hono().get( "/:name", describeRoute({ tags: ["DOCUMENT (v1)"], summary: "Get document", - description: `Get the content/metadata of a published document in the instance. + description: `Get the content/metadata of a published document in the instance Note: If you only need to query the document metadata, you should use HEAD method instead`, responses: { 200: { content: { "text/plain": { - schema: schemaResponse + schema: schemaBodyResponse.schema }, "application/octet-stream": { - schema: schemaResponse + schema: schemaBodyResponse.schema } }, + headers: schemaHeaderResponse.components, description: constant.http[200] }, 400: { ...genericErrorResponse, description: constant.http[400] }, diff --git a/src/endpoints/document/v1/index.ts b/src/endpoints/document/v1/index.ts index 6c7ee1b4..74c20b39 100644 --- a/src/endpoints/document/v1/index.ts +++ b/src/endpoints/document/v1/index.ts @@ -2,6 +2,7 @@ import { Hono } from "@hono/hono/tiny"; import type { Env } from "#http/type.ts"; import delete_ from "./delete.ts"; import get from "./get.ts"; +import list from "./list.ts"; import patch from "./patch.ts"; import post from "./post.ts"; @@ -9,5 +10,6 @@ export const v1DocumentRouter = new Hono(); v1DocumentRouter.route("/", delete_); v1DocumentRouter.route("/", get); +v1DocumentRouter.route("/", list); v1DocumentRouter.route("/", patch); v1DocumentRouter.route("/", post); diff --git a/src/endpoints/document/v1/list.ts b/src/endpoints/document/v1/list.ts new file mode 100644 index 00000000..68ceaf49 --- /dev/null +++ b/src/endpoints/document/v1/list.ts @@ -0,0 +1,56 @@ +import { Hono } from "@hono/hono/tiny"; +import { describeRoute, resolver } from "@hono/openapi"; +import { decodeTime } from "@std/ulid"; +import { constant, mutable } from "#/global.ts"; +import { authMiddleware } from "#http/middleware/authorization.ts"; +import type { Env } from "#http/type.ts"; +import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { validatorDocumentListObject } from "#util/validator/document.ts"; + +const schemaBodyResponse = await resolver(validatorDocumentListObject.array()).toOpenAPISchema(); + +export default new Hono().get( + "/", + describeRoute({ + tags: ["DOCUMENT (v1)"], + summary: "List documents", + description: "List all user documents in the instance", + security: [{ bearer: [] }], + responses: { + 200: { + content: { + "application/json": { + schema: schemaBodyResponse.schema + } + }, + description: constant.http[200] + }, + 400: { ...genericErrorResponse, description: constant.http[400] }, + 404: { ...genericErrorResponse, description: constant.http[404] }, + + // auth middleware + 401: { ...genericErrorResponse, description: constant.http[401] } + } + }), + authMiddleware, + async (ctx) => { + const userId = ctx.get("userId"); + if (!userId) { + return error.throw(ErrorCode.userInvalidToken); + } + + // https://github.com/honojs/hono/issues/1130 + if (ctx.req.method === "HEAD") { + return ctx.body(null); + } + + const documents = mutable.database.user.getDocuments(userId).map((document) => { + return { + name: document.name, + created: Temporal.Instant.fromEpochMilliseconds(decodeTime(document.id)).toString() + }; + }); + + return ctx.json(documents); + } +); diff --git a/src/endpoints/document/v1/patch.ts b/src/endpoints/document/v1/patch.ts index 84f5fe7e..d9498674 100644 --- a/src/endpoints/document/v1/patch.ts +++ b/src/endpoints/document/v1/patch.ts @@ -46,8 +46,12 @@ Note: To remove (nullify) a value, send the header with an empty value`, security: [{}, { bearer: [] }], requestBody: { content: { - "text/plain": schemaBody, - "application/octet-stream": schemaBody + "text/plain": { + schema: schemaBody.schema + }, + "application/octet-stream": { + schema: schemaBody.schema + } } }, responses: { diff --git a/src/endpoints/document/v1/post.ts b/src/endpoints/document/v1/post.ts index ab65a3f8..9f621bde 100644 --- a/src/endpoints/document/v1/post.ts +++ b/src/endpoints/document/v1/post.ts @@ -31,11 +31,12 @@ const schemaHeader = type({ "x-jspaste-password?": validatorDocumentPassword }); -const schemaResponse = resolver( +// Object includes not allowed fields +const schemaBodyResponse = await resolver( type({ name: validatorDocumentName }) -); +).toOpenAPISchema(); export default new Hono().post( "/", @@ -46,15 +47,19 @@ export default new Hono().post( security: [{}, { bearer: [] }], requestBody: { content: { - "text/plain": schemaBody, - "application/octet-stream": schemaBody + "text/plain": { + schema: schemaBody.schema + }, + "application/octet-stream": { + schema: schemaBody.schema + } } }, responses: { 200: { content: { "application/json": { - schema: schemaResponse + schema: schemaBodyResponse.schema } }, description: constant.http[200] diff --git a/src/endpoints/legacy/v2/documents/access.route.ts b/src/endpoints/legacy/v2/documents/access.route.ts index 9fd47bb0..1d085b85 100644 --- a/src/endpoints/legacy/v2/documents/access.route.ts +++ b/src/endpoints/legacy/v2/documents/access.route.ts @@ -18,7 +18,7 @@ const schemaHeader = type({ "password?": validatorDocumentPassword }); -const schemaResponse = resolver( +const schemaBodyResponse = await resolver( type({ key: type.string.configure({ description: "The document name (formerly key)", @@ -39,7 +39,7 @@ const schemaResponse = resolver( examples: [0] }) }) -); +).toOpenAPISchema(); export default new Hono().get( "/:name", @@ -51,7 +51,7 @@ export default new Hono().get( 200: { content: { "application/json": { - schema: schemaResponse + schema: schemaBodyResponse.schema } }, description: constant.http[200] diff --git a/src/endpoints/legacy/v2/documents/accessRaw.route.ts b/src/endpoints/legacy/v2/documents/accessRaw.route.ts index 7d6f425b..79e461a4 100644 --- a/src/endpoints/legacy/v2/documents/accessRaw.route.ts +++ b/src/endpoints/legacy/v2/documents/accessRaw.route.ts @@ -22,7 +22,7 @@ const schemaQuery = type({ "p?": validatorDocumentPassword }); -const schemaResponse = resolver(type(type.unknown)); +const schemaBodyResponse = await resolver(type.unknown).toOpenAPISchema(); export default new Hono().get( "/:name/raw", @@ -34,10 +34,10 @@ export default new Hono().get( 200: { content: { "text/plain": { - schema: schemaResponse + schema: schemaBodyResponse.schema }, "application/octet-stream": { - schema: schemaResponse + schema: schemaBodyResponse.schema } }, description: constant.http[200] diff --git a/src/endpoints/legacy/v2/documents/edit.route.ts b/src/endpoints/legacy/v2/documents/edit.route.ts index ab64b5f9..7f8a579a 100644 --- a/src/endpoints/legacy/v2/documents/edit.route.ts +++ b/src/endpoints/legacy/v2/documents/edit.route.ts @@ -14,23 +14,21 @@ const schemaParam = type({ name: validatorDocumentName }); -const schemaRequest = await resolver( - type( - type.string.configure({ - description: "Data to replace in the document", - examples: ["Hello world!"] - }) - ) +const schemaBody = await resolver( + type.string.configure({ + description: "Data to replace in the document", + examples: ["Hello world!"] + }) ).toOpenAPISchema(); -const schemaResponse = resolver( +const schemaBodyResponse = await resolver( type({ edited: type.boolean.configure({ description: "Confirmation of edition", examples: [true] }) }) -); +).toOpenAPISchema(); export default new Hono().patch( "/:name", @@ -40,14 +38,16 @@ export default new Hono().patch( summary: "Edit document", requestBody: { content: { - "text/plain": schemaRequest + "text/plain": { + schema: schemaBody.schema + } } }, responses: { 200: { content: { "application/json": { - schema: schemaResponse + schema: schemaBodyResponse.schema } }, description: constant.http[200] diff --git a/src/endpoints/legacy/v2/documents/exists.route.ts b/src/endpoints/legacy/v2/documents/exists.route.ts index 0a50d31f..bc499335 100644 --- a/src/endpoints/legacy/v2/documents/exists.route.ts +++ b/src/endpoints/legacy/v2/documents/exists.route.ts @@ -11,7 +11,7 @@ const schemaParam = type({ name: validatorDocumentName }); -const schemaResponse = resolver(type(type.boolean)); +const schemaBodyResponse = await resolver(type.boolean).toOpenAPISchema(); export default new Hono().get( "/:name/exists", @@ -23,7 +23,7 @@ export default new Hono().get( 200: { content: { "text/plain": { - schema: schemaResponse + schema: schemaBodyResponse.schema } }, description: constant.http[200] diff --git a/src/endpoints/legacy/v2/documents/publish.route.ts b/src/endpoints/legacy/v2/documents/publish.route.ts index 5c4754a9..fa736eeb 100644 --- a/src/endpoints/legacy/v2/documents/publish.route.ts +++ b/src/endpoints/legacy/v2/documents/publish.route.ts @@ -13,6 +13,13 @@ import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; +const schemaBody = await resolver( + type.string.configure({ + description: "Data to replace in the document", + examples: ["Hello world!"] + }) +).toOpenAPISchema(); + const schemaHeader = type({ "password?": validatorDocumentPassword, "key?": validatorDocumentName, @@ -21,23 +28,14 @@ const schemaHeader = type({ }) }); -const schemaBody = await resolver( - type( - type.string.configure({ - description: "Data to replace in the document", - examples: ["Hello world!"] - }) - ) -).toOpenAPISchema(); - -const schemaResponse = resolver( +const schemaBodyResponse = await resolver( type({ key: type.string.configure({ description: "The document name (formerly key)", examples: ["abc123"] }) }) -); +).toOpenAPISchema(); export default new Hono().post( "/", @@ -47,14 +45,16 @@ export default new Hono().post( summary: "Publish document", requestBody: { content: { - "text/plain": schemaBody + "text/plain": { + schema: schemaBody.schema + } } }, responses: { 200: { content: { "application/json": { - schema: schemaResponse + schema: schemaBodyResponse.schema } }, description: constant.http[200] diff --git a/src/endpoints/legacy/v2/documents/remove.route.ts b/src/endpoints/legacy/v2/documents/remove.route.ts index e1d4cbe9..799ac452 100644 --- a/src/endpoints/legacy/v2/documents/remove.route.ts +++ b/src/endpoints/legacy/v2/documents/remove.route.ts @@ -12,14 +12,14 @@ const schemaParam = type({ name: validatorDocumentName }); -const schemaResponse = resolver( +const schemaBodyResponse = await resolver( type({ removed: type.true.configure({ description: "Confirmation of deletion", examples: [true] }) }) -); +).toOpenAPISchema(); export default new Hono().delete( "/:name", @@ -31,7 +31,7 @@ export default new Hono().delete( 200: { content: { "application/json": { - schema: schemaResponse + schema: schemaBodyResponse.schema } }, description: constant.http[200] diff --git a/src/endpoints/user/v1/create.ts b/src/endpoints/user/v1/create.ts index 2bae3a18..e814b8d0 100644 --- a/src/endpoints/user/v1/create.ts +++ b/src/endpoints/user/v1/create.ts @@ -2,16 +2,16 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver } from "@hono/openapi"; import { type } from "arktype"; import { constant, mutable } from "#/global.ts"; +import { authMiddleware } from "#http/middleware/authorization.ts"; import type { Env } from "#http/type.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; import { validatorUserToken } from "#util/validator/user.ts"; -import { authMiddleware } from "../../../http/middleware/authorization.ts"; -const schemaResponse = resolver( +const schemaBodyResponse = await resolver( type({ token: validatorUserToken }) -); +).toOpenAPISchema(); export default new Hono().post( "/", @@ -24,7 +24,7 @@ export default new Hono().post( 200: { content: { "application/json": { - schema: schemaResponse + schema: schemaBodyResponse.schema } }, description: constant.http[200] diff --git a/src/utils/validator/document.ts b/src/utils/validator/document.ts index 9e49a67e..7a3c7b4f 100644 --- a/src/utils/validator/document.ts +++ b/src/utils/validator/document.ts @@ -1,13 +1,14 @@ import { type } from "arktype"; import { constant } from "#/global.ts"; import { regexBase64URL } from "./regex.ts"; +import { validatorCreationTimestamp } from "./shared.ts"; export const validatorDocumentName = type(regexBase64URL) .atLeastLength(constant.documentNameLengthMin) .atMostLength(constant.documentNameLengthMax) .configure({ ref: "DocumentName", - description: "A name for the document", + description: "The document name", examples: ["myDocumentNameHere"], expected: (ctx) => { switch (ctx.code) { @@ -31,7 +32,7 @@ export const validatorDocumentNameLength = type.keywords.string.integer.parse .to(type.number.atLeast(constant.documentNameLengthMin).atMost(constant.documentNameLengthMax)) .configure({ ref: "DocumentNameLength", - description: "The name length for a document", + description: "The name length for the document", expected: (ctx) => { switch (ctx.code) { case "domain": { @@ -55,7 +56,7 @@ export const validatorDocumentPassword = type.string .atMostLength(constant.documentPasswordLengthMax) .configure({ ref: "DocumentPassword.default", - description: "A password for the document (for read access)", + description: "The password for the document (read access)", examples: ["myDocumentPasswordHere"] }); @@ -69,3 +70,13 @@ export const validatorDocumentDownload = type.unknown.configure({ ref: "DocumentDownload", description: "Indicate the client that downloads the document as a file attachment (only useful in web browsers)" }); + +export const validatorDocumentListObject = type({ + name: validatorDocumentName, + created: validatorCreationTimestamp +}).configure({ + // FIXME: schema references not being generated when using toOpenAPISchema() + // Invalid object key "DocumentListMetadata" at position 2 in "/components/schemas/DocumentListMetadata": key not found in object + //ref: "DocumentListMetadata", + description: "An object with document metadata" +}); diff --git a/src/utils/validator/shared.ts b/src/utils/validator/shared.ts new file mode 100644 index 00000000..af864ad2 --- /dev/null +++ b/src/utils/validator/shared.ts @@ -0,0 +1,6 @@ +import { type } from "arktype"; + +export const validatorCreationTimestamp = type.keywords.string.date.iso.root.configure({ + description: "The ISO 8601 timestamp when the resource was created", + examples: ["2026-01-01T00:00:00.000Z"] +}); From 0df4110e0b9b47aabdcad36b1b684ca266832ead Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Mon, 12 Jan 2026 00:15:21 +0100 Subject: [PATCH 42/47] Hashing en columnas sensibles (#259) --- .env.example | 6 +- deno.lock | 21 +++-- package.json | 5 +- rolldown.config.ts | 18 +++- src/database/database.ts | 81 ++++++++++------- src/database/migration.ts | 91 +++++++++++++++++++ src/database/migrations.ts | 17 ---- .../migrations/{0001.base.sql => 0001.sql} | 0 src/database/migrations/0002.sql | 1 + src/database/query.ts | 87 +++++++++++++----- src/document/storage.ts | 17 ++-- src/endpoints/document/v1/delete.ts | 2 +- src/endpoints/document/v1/get.ts | 3 +- src/endpoints/document/v1/patch.ts | 8 +- src/endpoints/document/v1/post.ts | 12 ++- .../legacy/v2/documents/access.route.ts | 3 +- .../legacy/v2/documents/accessRaw.route.ts | 3 +- .../legacy/v2/documents/publish.route.ts | 12 ++- .../legacy/v2/documents/remove.route.ts | 2 +- src/endpoints/user/v1/create.ts | 10 +- src/global.ts | 11 +-- src/http/middleware/authorization.ts | 30 +++++- src/http/router.ts | 4 +- src/http/server.ts | 12 ++- src/init.ts | 8 +- src/tasks/sweeper.ts | 6 +- src/utils/crypto.ts | 31 ++++--- src/utils/document.ts | 4 +- src/utils/user.ts | 7 ++ src/utils/validator/user.ts | 37 ++++++-- 30 files changed, 379 insertions(+), 170 deletions(-) create mode 100644 src/database/migration.ts delete mode 100644 src/database/migrations.ts rename src/database/migrations/{0001.base.sql => 0001.sql} (100%) create mode 100644 src/database/migrations/0002.sql create mode 100644 src/utils/user.ts diff --git a/.env.example b/.env.example index 8b4dbf4f..9aacd309 100644 --- a/.env.example +++ b/.env.example @@ -55,9 +55,9 @@ #? Root user can always create new users. #JSPB_USER_REGISTER=true -## Authentication token for root user: []:string -#? Will overwrite existing root user token on first run. -#JSPB_USER_ROOT_TOKEN= +## Restore the root user?: [false]:boolean +#? Make sure to disable this again after successful recovery. +#JSPB_USER_ROOT_RECOVERY=false ######## ## TASK: diff --git a/deno.lock b/deno.lock index d5725b59..0b53c4a1 100644 --- a/deno.lock +++ b/deno.lock @@ -8,7 +8,6 @@ "npm:@jsr/std__async@^1.0.16": "1.0.16", "npm:@jsr/std__cache@~0.2.1": "0.2.1", "npm:@jsr/std__collections@^1.1.3": "1.1.3", - "npm:@jsr/std__crypto@^1.0.5": "1.0.5", "npm:@jsr/std__dotenv@~0.225.6": "0.225.6", "npm:@jsr/std__encoding@^1.0.10": "1.0.10", "npm:@jsr/std__fmt@^1.0.8": "1.0.8", @@ -18,9 +17,11 @@ "npm:@types/node@^25.0.3": "25.0.3", "npm:arkenv@~0.8.3": "0.8.3_arktype@2.1.29", "npm:arktype@^2.1.29": "2.1.29", + "npm:hash-wasm@^4.12.0": "4.12.0", "npm:hono-openapi@^1.1.2": "1.1.2_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.1.29__quansync@0.2.11_@standard-community+standard-openapi@0.2.9__@standard-community+standard-json@0.3.5___@standard-schema+spec@1.1.0___@types+json-schema@7.0.15___arktype@2.1.29___quansync@0.2.11__@standard-schema+spec@1.1.0__arktype@2.1.29__openapi-types@12.1.3__@types+json-schema@7.0.15_@types+json-schema@7.0.15_openapi-types@12.1.3_arktype@2.1.29", "npm:nanoid@^5.1.6": "5.1.6", - "npm:rolldown@1.0.0-beta.59": "1.0.0-beta.59" + "npm:rolldown@1.0.0-beta.59": "1.0.0-beta.59", + "npm:vite-bundle-analyzer@^1.3.2": "1.3.2" }, "npm": { "@ark/schema@0.56.0": { @@ -144,10 +145,6 @@ "integrity": "sha512-jGG6mv3IjOyxm6PyT1YVbLyAlZL+Gow6LOpBw+84qb1nkdJY0+t6bi7ICEqAwUz87cNjBS0P+yZQ5HHclJhsfw==", "tarball": "https://npm.jsr.io/~/11/@jsr/std__collections/1.1.3.tgz" }, - "@jsr/std__crypto@1.0.5": { - "integrity": "sha512-iqFCkjeGeQccLgmxH9m1d7abjZcFMW0XrYZu1itNz8vVHzH9crObalonjVQaVDdKHCrNNOklMN1t0u3k46dirA==", - "tarball": "https://npm.jsr.io/~/11/@jsr/std__crypto/1.0.5.tgz" - }, "@jsr/std__dotenv@0.225.6": { "integrity": "sha512-rqh5RrHccbyzmP4v1/vqUyYy4dqopjTRgW8bJqk2ZXTKBbvpmMjPxJ+xy+YAk6XnEvtPCPAgqbFhHWcomjnX+w==", "tarball": "https://npm.jsr.io/~/11/@jsr/std__dotenv/0.225.6.tgz" @@ -333,6 +330,9 @@ "arkregex" ] }, + "hash-wasm@4.12.0": { + "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==" + }, "hono-openapi@1.1.2_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.1.29__quansync@0.2.11_@standard-community+standard-openapi@0.2.9__@standard-community+standard-json@0.3.5___@standard-schema+spec@1.1.0___@types+json-schema@7.0.15___arktype@2.1.29___quansync@0.2.11__@standard-schema+spec@1.1.0__arktype@2.1.29__openapi-types@12.1.3__@types+json-schema@7.0.15_@types+json-schema@7.0.15_openapi-types@12.1.3_arktype@2.1.29": { "integrity": "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g==", "dependencies": [ @@ -380,6 +380,10 @@ }, "undici-types@7.16.0": { "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" + }, + "vite-bundle-analyzer@1.3.2": { + "integrity": "sha512-Od4ILUKRvBV3LuO/E+S+c1XULlxdkRZPSf6Vzzu+UAXG0D3hZYUu9imZIkSj/PU4e1FB14yB+av8g3KiljH8zQ==", + "bin": true } }, "workspace": { @@ -392,7 +396,6 @@ "npm:@jsr/std__async@^1.0.16", "npm:@jsr/std__cache@~0.2.1", "npm:@jsr/std__collections@^1.1.3", - "npm:@jsr/std__crypto@^1.0.5", "npm:@jsr/std__dotenv@~0.225.6", "npm:@jsr/std__encoding@^1.0.10", "npm:@jsr/std__fmt@^1.0.8", @@ -402,9 +405,11 @@ "npm:@types/node@^25.0.3", "npm:arkenv@~0.8.3", "npm:arktype@^2.1.29", + "npm:hash-wasm@^4.12.0", "npm:hono-openapi@^1.1.2", "npm:nanoid@^5.1.6", - "npm:rolldown@1.0.0-beta.59" + "npm:rolldown@1.0.0-beta.59", + "npm:vite-bundle-analyzer@^1.3.2" ] } } diff --git a/package.json b/package.json index d13eddb8..262b02e1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "@std/async": "npm:@jsr/std__async@^1.0.16", "@std/cache": "npm:@jsr/std__cache@~0.2.1", "@std/collections": "npm:@jsr/std__collections@^1.1.3", - "@std/crypto": "npm:@jsr/std__crypto@^1.0.5", "@std/dotenv": "npm:@jsr/std__dotenv@~0.225.6", "@std/encoding": "npm:@jsr/std__encoding@^1.0.10", "@std/fmt": "npm:@jsr/std__fmt@^1.0.8", @@ -21,8 +20,10 @@ "arkenv": "npm:arkenv@~0.8.3", "arktype": "npm:arktype@^2.1.29", "biome": "npm:@biomejs/biome@2.3.11", + "hash-wasm": "npm:hash-wasm@^4.12.0", "nanoid": "npm:nanoid@^5.1.6", - "rolldown": "npm:rolldown@1.0.0-beta.59" + "rolldown": "npm:rolldown@1.0.0-beta.59", + "vite-bundle-analyzer": "npm:vite-bundle-analyzer@^1.3.2" }, "imports": { "#/*": [ diff --git a/rolldown.config.ts b/rolldown.config.ts index 2fe4876e..d1c696cd 100644 --- a/rolldown.config.ts +++ b/rolldown.config.ts @@ -1,4 +1,7 @@ import type { RolldownOptions } from "rolldown"; +import { analyzer, unstableRolldownAdapter } from "vite-bundle-analyzer"; + +const analyze = false; export default { input: "./src/index.ts", @@ -11,8 +14,8 @@ export default { sourcemap: true }, resolve: { - conditionNames: ["import", "require", "node", "default"], - mainFields: ["main", "module"] + conditionNames: ["import", "default"], + mainFields: ["module", "main"] }, platform: "neutral", external: [/^(node:)/], @@ -27,5 +30,14 @@ export default { typescript: { onlyRemoveTypeImports: true } - } + }, + plugins: [ + unstableRolldownAdapter( + analyzer({ + enabled: analyze, + analyzerPort: "auto", + summary: true + }) + ) + ] } satisfies RolldownOptions; diff --git a/src/database/database.ts b/src/database/database.ts index 73e2ea32..af5cc5e1 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -1,8 +1,10 @@ import { DatabaseSync, type StatementSync } from "node:sqlite"; -import { monotonicUlid } from "@std/ulid"; +import { monotonicUlid, ulid } from "@std/ulid"; import { constant } from "#/global.ts"; import { Logger } from "#util/console.ts"; -import { migrations } from "./migrations.ts"; +import { generateHash } from "#util/crypto.ts"; +import { generateToken } from "#util/user.ts"; +import { migrations } from "./migration.ts"; import { DocumentQuery, UserQuery } from "./query.ts"; const log: Logger = new Logger("database"); @@ -17,12 +19,12 @@ export class Database { private readonly database: DatabaseSync; - public constructor(options?: Options) { - const ephemeral = options?.ephemeral ?? constant.env.JSPB_DEBUG_DATABASE_EPHEMERAL; + public constructor(options: Options = {}) { + options.ephemeral ??= constant.env.JSPB_DEBUG_DATABASE_EPHEMERAL; - this.database = new DatabaseSync(ephemeral ? ":memory:" : constant.path.databaseFile); + this.database = new DatabaseSync(options.ephemeral ? ":memory:" : constant.path.databaseFile); - if (ephemeral) { + if (options.ephemeral) { log.warn("Using ephemeral. No changes will persist."); return; } @@ -31,44 +33,55 @@ export class Database { PRAGMA wal_autocheckpoint = 1024;`); } - public migration(): void { + public async migration(): Promise { const query = this.prepare("PRAGMA user_version;", false).get(); if (typeof query?.user_version !== "number") { throw new Deno.errors.InvalidData("Failed to get version."); } - if (query.user_version === migrations.length) { + + if (query.user_version !== migrations.length) { + if (query.user_version > migrations.length) { + throw new Deno.errors.InvalidData("Version is higher than available migrations. Update your JSPaste instance."); + } + + for (const [delta, migration] of migrations.slice(query.user_version).entries()) { + try { + // biome-ignore lint/performance/noAwaitInLoops: serialized + await this.transaction(async () => { + await migration.preMigration?.(this); + this.exec(migration.sql); + await migration.postMigration?.(this); + this.exec(`PRAGMA user_version = ${(query.user_version as number) + delta + 1};`); + }); + } catch (error) { + log.error(`Error while running migration "${migration.id}"..:`); + throw error; + } + + log.info(`Migration "${migration.id}" ran successfully.`); + } + } else { log.debug("Already up to date."); - return; - } - if (query.user_version > migrations.length) { - throw new Deno.errors.InvalidData("Version is higher than available migrations. Update your JSPaste instance."); } - migrations.slice(query.user_version).forEach((migration, delta) => { - try { - this.transaction(() => { - this.exec(migration.sql); - this.exec(`PRAGMA user_version = ${(query.user_version as number) + delta + 1};`); - }); - } catch (error) { - log.error(`Error while running migration "${migration.id}"..:`); - throw error; - } + try { + const rootId = this.user.getRoot()?.id; - log.info(`Migration "${migration.id}" ran successfully.`); - }); + if (constant.env.JSPB_USER_ROOT_RECOVERY && rootId) { + const token = generateToken(rootId); + const hash = generateHash(token); - if (query.user_version === 0) { - try { - const token = this.user.create(constant.ulid.userRoot, constant.env.JSPB_USER_ROOT_TOKEN); + this.user.update("id", rootId, "token", hash.combo); - if (!constant.env.JSPB_USER_ROOT_TOKEN) { - log.warn("Note the root user token as it won't be shown again", ` >> "${token}" <<`); - } - } catch (error) { - log.error("Failed to create root user..:"); - throw error; + log.warn("+-- The root user token was regenerated.", "|", `+--> "${token}"`); + } else if (!rootId?.startsWith("0000000001")) { + const token = this.user.create(ulid(1)); + + log.warn("+-- Note the root user token as it won't be shown again.", "|", `+--> "${token}"`); } + } catch (error) { + log.error("Failed to handle the root user..:"); + throw error; } } @@ -92,7 +105,7 @@ export class Database { public transaction(callback: () => T): T { if (this.database.isTransaction) { - const name = monotonicUlid(); + const name = `_${monotonicUlid()}`; this.exec(`SAVEPOINT ${name};`); try { diff --git a/src/database/migration.ts b/src/database/migration.ts new file mode 100644 index 00000000..463d5716 --- /dev/null +++ b/src/database/migration.ts @@ -0,0 +1,91 @@ +import { mapNotNullish } from "@std/collections"; +import { ulid } from "@std/ulid"; +import { Logger } from "#util/console.ts"; +import { generateHash } from "#util/crypto.ts"; +import { mutable } from "../global.ts"; +import type { Database } from "./database.ts"; + +const log: Logger = new Logger("database::migration"); + +type Migration = { + id: string; + preMigration?: (database: Database) => Promise | void; + sql: string; + postMigration?: (database: Database) => Promise | void; +}; + +export const migrations: Migration[] = [ + /** + * @description + * Base schema. + * + * @date 2025-12-27 + */ + { + id: "0001.base", + sql: (await import("./migrations/0001.sql", { with: { type: "text" } })).default + }, + + /** + * @description + * Hash everything, all future sensitive columns are now hashed. + * This first stage hashes all documents passwords and moves the root user to a compatible id. + * + * @date 2026-01-06 + */ + { + id: "0002.hashingStage1", + preMigration: (database: Database) => { + // migrate document passwords + const documentsHashed = mapNotNullish(database.document.getAll(["id", "password"]), ({ id, password }) => { + if (!password) return; + + const hash = generateHash(password); + database.document.update("id", id, "password", hash.combo); + }); + + if (documentsHashed.length > 0) { + log.debug(`Hashed ${documentsHashed.length} document passwords.`); + } + + // migrate user root id + const userRootIdOld = "0000000000FFFF000000000000"; + const userRootToken = database.user.get("id", userRootIdOld)?.token; + if (userRootToken) { + const id = ulid(1); + database.user.create(id); + + for (const document of database.user.getDocuments(userRootIdOld)) { + database.document.update("id", document.id, "user_id", id); + } + + database.user.delete("id", userRootIdOld); + + const userRootId = mutable.database.user.getRoot()?.id; + if (userRootId) { + database.user.update("id", userRootId, "token", userRootToken); + } + } + + const userTokens = database.user.getAll(["token"]); + + let userTokenUnhashed = false; + for (const entry of userTokens) { + // combo separator + if (!entry.token.includes(" ")) { + userTokenUnhashed = true; + break; + } + } + + if (userTokenUnhashed) { + log.warn( + "Users with plain tokens found!", + "New users in the instance will have their token hashed,", + "In the future we will enforce that every user token is hashed." + ); + } + }, + sql: (await import("./migrations/0002.sql", { with: { type: "text" } })).default + } +] as const; diff --git a/src/database/migrations.ts b/src/database/migrations.ts deleted file mode 100644 index 9f543e9a..00000000 --- a/src/database/migrations.ts +++ /dev/null @@ -1,17 +0,0 @@ -type Migration = { - id: string; - sql: string; -}; - -export const migrations: Migration[] = [ - /** - * @description - * Base schema. - * - * @date 2025-12-27 - */ - { - id: "0001.base", - sql: (await import("./migrations/0001.base.sql", { with: { type: "text" } })).default - } -] as const; diff --git a/src/database/migrations/0001.base.sql b/src/database/migrations/0001.sql similarity index 100% rename from src/database/migrations/0001.base.sql rename to src/database/migrations/0001.sql diff --git a/src/database/migrations/0002.sql b/src/database/migrations/0002.sql new file mode 100644 index 00000000..ec5070fe --- /dev/null +++ b/src/database/migrations/0002.sql @@ -0,0 +1 @@ +DROP INDEX idx_user_token; diff --git a/src/database/query.ts b/src/database/query.ts index 0234d666..2b999ba7 100644 --- a/src/database/query.ts +++ b/src/database/query.ts @@ -2,7 +2,8 @@ import type { SQLInputValue } from "node:sqlite"; import { chunk } from "@std/collections"; import { monotonicUlid } from "@std/ulid"; import { constant } from "#/global.ts"; -import { generateToken } from "#util/document.ts"; +import { generateHash } from "#util/crypto.ts"; +import { generateToken } from "#util/user.ts"; import type { Database } from "./database.ts"; export const DocumentVersion = { @@ -25,7 +26,7 @@ export type User = { token: string; }; export type UserColumn = Pick; -export type UserIndex = UserColumn<"id" | "token">; +export type UserIndex = UserColumn<"id">; abstract class Query
> { protected readonly database: Database; @@ -47,32 +48,48 @@ abstract class Query
> { this.database.transaction(() => { for (const batch of chunk(defaultValues, constant.databaseMaxElements)) { this.database - .prepare(`DELETE FROM ${this.table} WHERE ${column} IN (${batch.map(() => "?").join(", ")})`, false) + .prepare( + `DELETE + FROM ${this.table} + WHERE ${column} IN (${batch.map(() => "?").join(", ")})`, + false + ) .run(...batch); } }); } - protected updateByColumn( - whereColumn: K, - whereValue: Table[K], - setColumn: K, - setValue: Table[K] + protected updateByColumn( + whereColumn: WK, + whereValue: Table[WK], + setColumn: SK, + setValue: Table[SK] ): void { - this.database.prepare(`UPDATE ${this.table} SET ${setColumn} = :setValue WHERE ${whereColumn} = :whereValue`).run({ - setValue: setValue, - whereValue: whereValue - }); + this.database + .prepare(`UPDATE ${this.table} + SET ${setColumn} = :setValue + WHERE ${whereColumn} = :whereValue`) + .run({ + setValue: setValue, + whereValue: whereValue + }); } protected selectByColumn(column: K, value: Table[K]): Table | undefined { - return this.database.prepare(`SELECT * FROM ${this.table} WHERE ${column} = :value`).get({ - value: value - }) as Table | undefined; + return this.database + .prepare(`SELECT * + FROM ${this.table} + WHERE ${column} = :value`) + .get({ + value: value + }) as Table | undefined; } protected selectColumns(columns: K[]): Pick[] { - return this.database.prepare(`SELECT ${columns.join(", ")} FROM ${this.table}`).all() as Pick[]; + return this.database + .prepare(`SELECT ${columns.join(", ")} + FROM ${this.table}`) + .all() as Pick[]; } } @@ -84,7 +101,8 @@ export class DocumentQuery extends Query { public create(params: Document): void { this.database .prepare( - "INSERT INTO document (id, user_id, version, name, password) VALUES (:id, :user_id, :version, :name, :password)" + `INSERT INTO document (id, user_id, version, name, password) + VALUES (:id, :user_id, :version, :name, :password)` ) .run({ id: params.id, @@ -96,7 +114,7 @@ export class DocumentQuery extends Query { } public delete = this.deleteByColumn; - public update = this.updateByColumn; + public update = this.updateByColumn; public get = this.selectByColumn; public getAll = this.selectColumns; } @@ -106,18 +124,36 @@ export class UserQuery extends Query { super(database, "user"); } - public create(id: string = monotonicUlid(), token: string = generateToken()): string { - this.database.prepare("INSERT INTO user (id, token) VALUES (:id, :token)").run({ id: id, token: token }); + public create(id: string = monotonicUlid()): string { + const token = generateToken(id); + const hash = generateHash(token); + + this.database + .prepare(`INSERT INTO user (id, token) + VALUES (:id, :token)`) + .run({ id: id, token: hash.combo }); + return token; } public delete = this.deleteByColumn; - public update = this.updateByColumn; + public update = this.updateByColumn; public get = this.selectByColumn; + public getRoot(): User | undefined { + return this.database + .prepare(`SELECT * + FROM user + WHERE user.id + LIKE '0000000001%' + LIMIT 1`) + .get() as User | undefined; + } + public getDocuments(id: string): DocumentColumn<"id" | "name">[] { return this.database - .prepare("SELECT document.id, document.name FROM document WHERE document.user_id = :id") + .prepare(`SELECT document.id, document.name + FROM document WHERE document.user_id = :id`) .all({ id: id }) as DocumentColumn<"id" | "name">[]; } @@ -125,8 +161,11 @@ export class UserQuery extends Query { public getAllWithoutDocuments(): UserColumn<"id">[] { return this.database - .prepare(`SELECT user.id FROM user WHERE NOT EXISTS ( - SELECT 1 FROM document WHERE document.user_id = user.id + .prepare(`SELECT user.id + FROM user + WHERE NOT EXISTS (SELECT 1 + FROM document + WHERE document.user_id = user.id )`) .all() as UserColumn<"id">[]; } diff --git a/src/document/storage.ts b/src/document/storage.ts index 334ec9eb..3b80602f 100644 --- a/src/document/storage.ts +++ b/src/document/storage.ts @@ -1,5 +1,5 @@ import { constant } from "#/global.ts"; -import { ErrorCode, error } from "../utils/error.ts"; +import { ErrorCode, error } from "#util/error.ts"; export const storage = { delete: async (id: string): Promise => { @@ -34,7 +34,7 @@ export const storage = { } }, - // relaxed exists because races between fs/db may ocurr + // relaxed exists because races between fs/db may occur list: function* (relaxed?: boolean): Iterable { for (const entry of Deno.readDirSync(constant.path.struct.storageData)) { if (entry.isFile) { @@ -42,15 +42,16 @@ export const storage = { const info = Deno.statSync(constant.path.struct.storageData + entry.name); if ( - info.mtime && + !info.mtime || constant.temporal.utc().epochMilliseconds - - info.mtime.toTemporalInstant().toZonedDateTimeISO("Etc/UTC").epochMilliseconds < + info.mtime.toTemporalInstant().toZonedDateTimeISO("Etc/UTC").epochMilliseconds >= 10_000 - ) - continue; + ) { + yield entry.name; + } + } else { + yield entry.name; } - - yield entry.name; } } } diff --git a/src/endpoints/document/v1/delete.ts b/src/endpoints/document/v1/delete.ts index f331e977..b3ff5794 100644 --- a/src/endpoints/document/v1/delete.ts +++ b/src/endpoints/document/v1/delete.ts @@ -34,7 +34,7 @@ export default new Hono().delete( }), validator("param", schemaParam, validatorHandler), authMiddleware, - async (ctx) => { + (ctx) => { const { name // @ts-expect-error upstream diff --git a/src/endpoints/document/v1/get.ts b/src/endpoints/document/v1/get.ts index 1aa9f38e..3c13396c 100644 --- a/src/endpoints/document/v1/get.ts +++ b/src/endpoints/document/v1/get.ts @@ -7,6 +7,7 @@ import { constant, mutable } from "#/global.ts"; import { compression } from "#document/compression.ts"; import { storage } from "#document/storage.ts"; import type { Env } from "#http/type.ts"; +import { verifyHash } from "#util/crypto.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; import { validatorDocumentDownload, @@ -87,7 +88,7 @@ Note: If you only need to query the document metadata, you should use HEAD metho return error.throw(ErrorCode.documentPasswordNeeded); } - if (password !== document.password) { + if (!verifyHash(password, document.password)) { return error.throw(ErrorCode.documentInvalidPassword); } } diff --git a/src/endpoints/document/v1/patch.ts b/src/endpoints/document/v1/patch.ts index d9498674..50194690 100644 --- a/src/endpoints/document/v1/patch.ts +++ b/src/endpoints/document/v1/patch.ts @@ -8,6 +8,7 @@ import { authMiddleware } from "#http/middleware/authorization.ts"; import { bodyCheck } from "#http/middleware/bodyCheck.ts"; import { bodySize } from "#http/middleware/bodySize.ts"; import type { Env } from "#http/type.ts"; +import { generateHash } from "#util/crypto.ts"; import { isOwner } from "#util/document.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; import { @@ -77,7 +78,7 @@ Note: To remove (nullify) a value, send the header with an empty value`, bodySize, bodyCheck, async (ctx) => { - let { + const { actualName // @ts-expect-error upstream } = ctx.req.valid("param") as typeof schemaParam.infer; @@ -102,7 +103,9 @@ Note: To remove (nullify) a value, send the header with an empty value`, if (newPassword === "") { mutable.database.document.update("name", actualName, "password", null); } else { - mutable.database.document.update("name", actualName, "password", newPassword); + const hash = generateHash(newPassword); + + mutable.database.document.update("name", actualName, "password", hash.combo); } } @@ -113,7 +116,6 @@ Note: To remove (nullify) a value, send the header with an empty value`, } mutable.database.document.update("name", actualName, "name", newName); - actualName = newName; } if (ctx.get("hasBody")) { diff --git a/src/endpoints/document/v1/post.ts b/src/endpoints/document/v1/post.ts index 9f621bde..13ed37dc 100644 --- a/src/endpoints/document/v1/post.ts +++ b/src/endpoints/document/v1/post.ts @@ -9,6 +9,7 @@ import { storage } from "#document/storage.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { bodySize } from "#http/middleware/bodySize.ts"; import type { Env } from "#http/type.ts"; +import { generateHash } from "#util/crypto.ts"; import { generateName } from "#util/document.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; import { @@ -82,7 +83,7 @@ export default new Hono().post( bodySize, async (ctx) => { const { - "x-jspaste-password": password = null, + "x-jspaste-password": password, "x-jspaste-name": name, "x-jspaste-name-length": nameLength // @ts-expect-error upstream @@ -101,12 +102,19 @@ export default new Hono().post( const setId = monotonicUlid(); + let hashCombo: string | null; + if (password) { + hashCombo = generateHash(password).combo; + } else { + hashCombo = null; + } + mutable.database.document.create({ id: setId, user_id: ctx.get("userId") ?? null, version: DocumentVersion.V1, name: setName, - password: password + password: hashCombo }); await storage.write( diff --git a/src/endpoints/legacy/v2/documents/access.route.ts b/src/endpoints/legacy/v2/documents/access.route.ts index 1d085b85..10183ea8 100644 --- a/src/endpoints/legacy/v2/documents/access.route.ts +++ b/src/endpoints/legacy/v2/documents/access.route.ts @@ -6,6 +6,7 @@ import { constant, mutable } from "#/global.ts"; import { compression } from "#document/compression.ts"; import { storage } from "#document/storage.ts"; import type { Env } from "#http/type.ts"; +import { verifyHash } from "#util/crypto.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -82,7 +83,7 @@ export default new Hono().get( return error.throw(ErrorCode.documentPasswordNeeded); } - if (header.password !== document.password) { + if (!verifyHash(header.password, document.password)) { return error.throw(ErrorCode.documentInvalidPassword); } } diff --git a/src/endpoints/legacy/v2/documents/accessRaw.route.ts b/src/endpoints/legacy/v2/documents/accessRaw.route.ts index 79e461a4..fc2c0767 100644 --- a/src/endpoints/legacy/v2/documents/accessRaw.route.ts +++ b/src/endpoints/legacy/v2/documents/accessRaw.route.ts @@ -6,6 +6,7 @@ import { constant, mutable } from "#/global.ts"; import { compression } from "#document/compression.ts"; import { storage } from "#document/storage.ts"; import type { Env } from "#http/type.ts"; +import { verifyHash } from "#util/crypto.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -74,7 +75,7 @@ export default new Hono().get( return error.throw(ErrorCode.documentPasswordNeeded); } - if (options.password !== document.password) { + if (!verifyHash(options.password, document.password)) { return error.throw(ErrorCode.documentInvalidPassword); } } diff --git a/src/endpoints/legacy/v2/documents/publish.route.ts b/src/endpoints/legacy/v2/documents/publish.route.ts index fa736eeb..6a07c90b 100644 --- a/src/endpoints/legacy/v2/documents/publish.route.ts +++ b/src/endpoints/legacy/v2/documents/publish.route.ts @@ -8,6 +8,7 @@ import { compression } from "#document/compression.ts"; import { storage } from "#document/storage.ts"; import { bodySize } from "#http/middleware/bodySize.ts"; import type { Env } from "#http/type.ts"; +import { generateHash } from "#util/crypto.ts"; import { generateName } from "#util/document.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; @@ -67,7 +68,7 @@ export default new Hono().post( bodySize, async (ctx) => { const { - password = null, + password, key: name, keylength: nameLength // @ts-expect-error upstream @@ -86,12 +87,19 @@ export default new Hono().post( const id = monotonicUlid(); + let hashCombo: string | null; + if (password) { + hashCombo = generateHash(password).combo; + } else { + hashCombo = null; + } + mutable.database.document.create({ id: id, user_id: null, version: DocumentVersion.V1, name: setName, - password: password + password: hashCombo }); await storage.write( diff --git a/src/endpoints/legacy/v2/documents/remove.route.ts b/src/endpoints/legacy/v2/documents/remove.route.ts index 799ac452..b9ddd7ea 100644 --- a/src/endpoints/legacy/v2/documents/remove.route.ts +++ b/src/endpoints/legacy/v2/documents/remove.route.ts @@ -41,7 +41,7 @@ export default new Hono().delete( } }), validator("param", schemaParam, validatorHandler), - async (ctx) => { + (ctx) => { // @ts-expect-error upstream const param = ctx.req.valid("param") as typeof schemaParam.infer; diff --git a/src/endpoints/user/v1/create.ts b/src/endpoints/user/v1/create.ts index e814b8d0..e6b33814 100644 --- a/src/endpoints/user/v1/create.ts +++ b/src/endpoints/user/v1/create.ts @@ -7,11 +7,11 @@ import type { Env } from "#http/type.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; import { validatorUserToken } from "#util/validator/user.ts"; -const schemaBodyResponse = await resolver( +const schemaBodyResponse = resolver( type({ token: validatorUserToken }) -).toOpenAPISchema(); +); export default new Hono().post( "/", @@ -24,7 +24,7 @@ export default new Hono().post( 200: { content: { "application/json": { - schema: schemaBodyResponse.schema + schema: schemaBodyResponse } }, description: constant.http[200] @@ -37,8 +37,8 @@ export default new Hono().post( } }), authMiddleware, - async (ctx) => { - if (!constant.env.JSPB_USER_REGISTER && ctx.get("userId") !== constant.ulid.userRoot) { + (ctx) => { + if (!constant.env.JSPB_USER_REGISTER && ctx.get("userId") !== mutable.database.user.getRoot()?.id) { return error.throw(ErrorCode.userInvalidToken); } diff --git a/src/global.ts b/src/global.ts index 2b6258ec..805bcdd8 100644 --- a/src/global.ts +++ b/src/global.ts @@ -21,9 +21,7 @@ export const constant = { documentNameLengthMin: 2, documentPasswordLengthMax: 128, documentPasswordLengthMin: 2, - userTokenLengthDefault: 16, - userTokenLengthMax: 64, - userTokenLengthMin: 16, + userTokenLength: 59, env: env( { JSPB_LOG_VERBOSITY: type.keywords.number.integer.atLeast(0).atMost(4).default(3), @@ -47,8 +45,8 @@ export const constant = { JSPB_DOCUMENT_ANONYMOUS_AGE: type.string.pipe(humanizeTime).default("7d"), // user - "JSPB_USER_ROOT_TOKEN?": type.string, JSPB_USER_REGISTER: type.boolean.default(true), + JSPB_USER_ROOT_RECOVERY: type.boolean.default(false), // task JSPB_TASK_SWEEPER: type( @@ -79,8 +77,5 @@ export const constant = { }, http: STATUS_CODES as Record, textEncoder: new TextEncoder(), - textDecoder: new TextDecoder(), - ulid: { - userRoot: "0000000000FFFF000000000000" - } + textDecoder: new TextDecoder() } as const; diff --git a/src/http/middleware/authorization.ts b/src/http/middleware/authorization.ts index a7a84e66..cc4fb998 100644 --- a/src/http/middleware/authorization.ts +++ b/src/http/middleware/authorization.ts @@ -1,6 +1,7 @@ import { createMiddleware } from "@hono/hono/factory"; import { type } from "arktype"; import { mutable } from "#/global.ts"; +import { verifyHash } from "#util/crypto.ts"; import { ErrorCode, error } from "#util/error.ts"; import { validatorUserHeader } from "#util/validator/user.ts"; import type { Env } from "../type.ts"; @@ -16,12 +17,35 @@ export const authMiddleware = createMiddleware(async (ctx, next) => { return error.throw(ErrorCode.validation, token.summary); } - const userId = mutable.database.user.get("token", token)?.id; - if (!userId) { + if (!token.includes(".")) { + // unhashed token + if (token.length === 32) { + // @ts-expect-error unindexed select + const id = mutable.database.user.get("token", token)?.id; + if (!id) { + return error.throw(ErrorCode.userInvalidToken); + } + + ctx.set("userId", id); + + return next(); + } + + return error.throw(ErrorCode.userInvalidToken); + } + + const [id] = token.split("."); + if (!id) { + return error.throw(ErrorCode.userInvalidToken); + } + + // trying to minimize timing attacks by always calling verifyHash + const combo = mutable.database.user.get("id", id)?.token ?? "0 0"; + if (!verifyHash(token, combo)) { return error.throw(ErrorCode.userInvalidToken); } - ctx.set("userId", userId); + ctx.set("userId", id); await next(); }); diff --git a/src/http/router.ts b/src/http/router.ts index 0fb48b15..51f26416 100644 --- a/src/http/router.ts +++ b/src/http/router.ts @@ -2,12 +2,12 @@ import { cors } from "@hono/hono/cors"; import { HTTPException } from "@hono/hono/http-exception"; import { Hono } from "@hono/hono/tiny"; import { openAPIRouteHandler } from "@hono/openapi"; +import { constant } from "#/global.ts"; import { v1DocumentRouter } from "#endpoint/document/v1/index.ts"; import { v2LegacyDocumentRouter } from "#endpoint/legacy/v2/documents/index.ts"; +import { v1UserRouter } from "#endpoint/user/v1/index.ts"; import { Logger } from "#util/console.ts"; import { ErrorCode, error } from "#util/error.ts"; -import { v1UserRouter } from "../endpoints/user/v1/index.ts"; -import { constant } from "../global.ts"; import type { Env } from "./type.ts"; const log: Logger = new Logger("http"); diff --git a/src/http/server.ts b/src/http/server.ts index 5700e0d7..e598ca61 100644 --- a/src/http/server.ts +++ b/src/http/server.ts @@ -1,6 +1,6 @@ import { constant } from "#/global.ts"; import { Logger } from "#util/console.ts"; -import { ErrorCode, error } from "../utils/error.ts"; +import { ErrorCode, error } from "#util/error.ts"; const log: Logger = new Logger("http"); @@ -24,16 +24,18 @@ type Options = { handler?: Deno.ServeHandler; }; -export const server = (options?: Options): Deno.HttpServer => { - const handlerDefault: Deno.ServeHandler = options?.handler ?? dummyHandler; +export const server = (options: Options = {}): Deno.HttpServer => { + const usingHandler: boolean = typeof options.handler !== "undefined"; + + options.handler ??= dummyHandler; return Deno.serve({ transport: "tcp", hostname: constant.env.JSPB_HOSTNAME.root, port: constant.env.JSPB_PORT, - handler: handlerDefault, + handler: options.handler, onListen: () => { - if (options?.handler) { + if (usingHandler) { log.info( `Listening on ${constant.env.JSPB_HOSTNAME.isIPv6 ? `[${constant.env.JSPB_HOSTNAME.root}]` : constant.env.JSPB_HOSTNAME.root}:${constant.env.JSPB_PORT}` ); diff --git a/src/init.ts b/src/init.ts index cfa0f034..b87d5222 100644 --- a/src/init.ts +++ b/src/init.ts @@ -45,11 +45,7 @@ const initDatabase = async (): Promise => { constant.store.dispose.set(id, [0, async () => mutable.database[Symbol.dispose]()]); - mutable.database.migration(); - - if (constant.env.JSPB_USER_ROOT_TOKEN) { - mutable.database.user.update("id", constant.ulid.userRoot, "token", constant.env.JSPB_USER_ROOT_TOKEN); - } + await mutable.database.migration(); }; const initTask = async (): Promise => { @@ -58,7 +54,7 @@ const initTask = async (): Promise => { }); }; -export const init = async () => { +export const init = async (): Promise => { for (const signal of ["SIGINT", "SIGTERM", "SIGHUP", "SIGUSR1", "SIGUSR2"] satisfies Deno.Signal[]) { Deno.addSignalListener(signal, async () => { if (mutable.shutdown) return; diff --git a/src/tasks/sweeper.ts b/src/tasks/sweeper.ts index bd60b67c..c0fc7412 100644 --- a/src/tasks/sweeper.ts +++ b/src/tasks/sweeper.ts @@ -1,11 +1,11 @@ import { mapNotNullish } from "@std/collections"; import { decodeTime } from "@std/ulid"; -import { constant } from "#/global.ts"; +import { constant, mutable } from "#/global.ts"; import { Database } from "#db/database.ts"; import { storage } from "#document/storage.ts"; import { Logger } from "#util/console.ts"; -const log = new Logger("task::sweeper"); +const log: Logger = new Logger("task::sweeper"); export const sweeper = async (): Promise => { sweeperDatabaseUser(); @@ -24,7 +24,7 @@ const sweeperDatabaseUser = (): void => { const users = mapNotNullish(database.user.getAllWithoutDocuments(), ({ id }) => { if (!id) return; - if (id === constant.ulid.userRoot) return; + if (id === mutable.database.user.getRoot()?.id) return; if (temporalFuture.epochMilliseconds > decodeTime(id)) { return id; diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts index e258a336..6c531785 100644 --- a/src/utils/crypto.ts +++ b/src/utils/crypto.ts @@ -1,32 +1,35 @@ -import { crypto } from "@std/crypto"; import { decodeAscii85, encodeAscii85 } from "@std/encoding"; -import { constant } from "../global.ts"; +import { createBLAKE3 } from "hash-wasm"; +import { constant } from "#/global.ts"; -export const generateHash = async (input: string, salt?: Uint8Array) => { - const defaultSalt = salt ?? crypto.getRandomValues(new Uint8Array(4)); +const hasher = await createBLAKE3(); - const dataBytes = constant.textEncoder.encode(input); - const combo = new Uint8Array(defaultSalt.length + dataBytes.length); - combo.set(defaultSalt, 0); - combo.set(dataBytes, defaultSalt.length); +export const generateSalt = (length: number): Uint8Array => { + return crypto.getRandomValues(new Uint8Array(length)); +}; + +export const generateHash = (input: string, salt?: Uint8Array) => { + const defaultSalt = salt ?? generateSalt(4); + + hasher.init(); + hasher.update(defaultSalt); + hasher.update(constant.textEncoder.encode(input)); - const hash = await crypto.subtle.digest("BLAKE3", combo); - const encodedHash = encodeAscii85(hash, { standard: "Z85" }); + const encodedHash = encodeAscii85(hasher.digest("binary"), { standard: "Z85" }); return { combo: `${encodedHash} ${encodeAscii85(defaultSalt, { standard: "Z85" })}`, - hash: encodedHash, - salt: defaultSalt + hash: encodedHash }; }; -export const verifyHash = async (input: string, combo: string): Promise => { +export const verifyHash = (input: string, combo: string): boolean => { const [hash, salt] = combo.split(" "); if (!(hash && salt)) { throw new Error("Invalid hash combo"); } - const { hash: inputHash } = await generateHash(input, decodeAscii85(salt, { standard: "Z85" })); + const { hash: inputHash } = generateHash(input, decodeAscii85(salt, { standard: "Z85" })); return inputHash === hash; }; diff --git a/src/utils/document.ts b/src/utils/document.ts index 848b1b9a..ea7d03d5 100644 --- a/src/utils/document.ts +++ b/src/utils/document.ts @@ -1,7 +1,5 @@ import { constant, mutable } from "#/global.ts"; -export const generateToken = (): string => constant.nanoid(32); - export const generateName = (length = 8): string => { let name: string; do { @@ -24,7 +22,7 @@ export const isOwner = (userId?: string | null, documentUserId?: string | null): } // the root user can alter everything - if (userId === constant.ulid.userRoot) { + if (userId === mutable.database.user.getRoot()?.id) { return true; } } diff --git a/src/utils/user.ts b/src/utils/user.ts new file mode 100644 index 00000000..7f0582c2 --- /dev/null +++ b/src/utils/user.ts @@ -0,0 +1,7 @@ +import { constant } from "#/global.ts"; + +export const generateToken = (id: string): string => { + const noise = constant.nanoid(32); + + return `${id}.${noise}`; +}; diff --git a/src/utils/validator/user.ts b/src/utils/validator/user.ts index 52aecb5c..4e378df2 100644 --- a/src/utils/validator/user.ts +++ b/src/utils/validator/user.ts @@ -2,22 +2,39 @@ import { type } from "arktype"; import { constant } from "#/global.ts"; import { regexBase64URL, regexHeaderBearer } from "./regex.ts"; -export const validatorUserToken = type(regexBase64URL) - .atLeastLength(constant.userTokenLengthMin) - .atMostLength(constant.userTokenLengthMax) +// FIXME: schema references not being generated when using toOpenAPISchema() +export const validatorUserToken = type.string.exactlyLength(constant.userTokenLength).configure({ + ref: "UserToken.default", + description: "A user token", + examples: ["myUserTokenHere"], + expected: (ctx) => { + switch (ctx.code) { + case "domain": { + return "a string"; + } + case "exactLength": { + return `exactly ${ctx.rule} characters`; + } + default: { + return "valid"; + } + } + } +}); + +export const validatorUserTokenLegacy = type(regexBase64URL) + .exactlyLength(32) .configure({ - description: "A user token", + ref: "UserToken.legacy", + description: "An unhashed user token", examples: ["myUserTokenHere"], expected: (ctx) => { switch (ctx.code) { case "pattern": { return "a valid Base64URL"; } - case "minLength": { - return `more than ${ctx.rule} characters`; - } - case "maxLength": { - return `less than ${ctx.rule} characters`; + case "exactLength": { + return `exactly ${ctx.rule} characters`; } default: { return "valid"; @@ -31,4 +48,4 @@ export const validatorUserHeader = type(regexHeaderBearer) description: "A RFC 6750 structured Bearer header", expected: "a valid header" }) - .pipe((string) => string.split(" ")[1], validatorUserToken); + .pipe((string) => string.split(" ")[1], validatorUserToken.or(validatorUserTokenLegacy)); From addee9b43f32627f5b14728fcf071a3285c401cd Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Mon, 12 Jan 2026 11:24:09 +0100 Subject: [PATCH 43/47] Document compression options (#264) --- .env.example | 6 +++++- src/database/query.ts | 7 +------ src/endpoints/document/v1/get.ts | 11 +++++++--- src/endpoints/document/v1/patch.ts | 21 +++++++++++++------ src/endpoints/document/v1/post.ts | 18 +++++++++------- .../legacy/v2/documents/access.route.ts | 11 ++++++++-- .../legacy/v2/documents/accessRaw.route.ts | 11 +++++++--- .../legacy/v2/documents/edit.route.ts | 17 ++++++++++----- .../legacy/v2/documents/publish.route.ts | 18 +++++++++------- src/global.ts | 11 ++++++++++ 10 files changed, 91 insertions(+), 40 deletions(-) diff --git a/.env.example b/.env.example index 9aacd309..e45eff73 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,10 @@ #? 0=disabled, units: b/k(i)b/m(i)b/g(i)b/t(i)b #JSPB_DOCUMENT_SIZE=1mb +## Compress document?: [true]:boolean +#? It doesn't apply retroactively to existing documents. +#JSPB_DOCUMENT_COMPRESSION=true + ## Delete documents older than: [0]:string #? 0=disabled, units: s/m/h/d/w/M/y #JSPB_DOCUMENT_AGE=0 @@ -51,7 +55,7 @@ ######## ## USER: ######## -## Allow user registration: [true]:boolean +## Allow user registration?: [true]:boolean #? Root user can always create new users. #JSPB_USER_REGISTER=true diff --git a/src/database/query.ts b/src/database/query.ts index 2b999ba7..2eed13d7 100644 --- a/src/database/query.ts +++ b/src/database/query.ts @@ -1,16 +1,11 @@ import type { SQLInputValue } from "node:sqlite"; import { chunk } from "@std/collections"; import { monotonicUlid } from "@std/ulid"; -import { constant } from "#/global.ts"; +import { constant, type DocumentVersionType } from "#/global.ts"; import { generateHash } from "#util/crypto.ts"; import { generateToken } from "#util/user.ts"; import type { Database } from "./database.ts"; -export const DocumentVersion = { - V1: 1 -} as const; -export type DocumentVersionType = (typeof DocumentVersion)[keyof typeof DocumentVersion]; - export type Document = { id: string; user_id: string | null; diff --git a/src/endpoints/document/v1/get.ts b/src/endpoints/document/v1/get.ts index 3c13396c..6ef2efbe 100644 --- a/src/endpoints/document/v1/get.ts +++ b/src/endpoints/document/v1/get.ts @@ -3,7 +3,7 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { decodeTime } from "@std/ulid"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; +import { constant, DocumentVersion, mutable } from "#/global.ts"; import { compression } from "#document/compression.ts"; import { storage } from "#document/storage.ts"; import type { Env } from "#http/type.ts"; @@ -105,10 +105,15 @@ Note: If you only need to query the document metadata, you should use HEAD metho const fileHandle = await storage.read(document.id); + const clientHasDeflate = ctx.req.header("accept-encoding")?.includes("deflate"); + let fileContent: ReadableStream; - if (ctx.req.header("accept-encoding")?.includes("deflate")) { + if (document.version === DocumentVersion.V2 || clientHasDeflate) { fileContent = fileHandle.readable; - ctx.res.headers.set("content-encoding", "deflate"); + + if (document.version === DocumentVersion.V1 && clientHasDeflate) { + ctx.res.headers.set("content-encoding", "deflate"); + } } else { fileContent = compression.decode(fileHandle.readable); } diff --git a/src/endpoints/document/v1/patch.ts b/src/endpoints/document/v1/patch.ts index 50194690..05dfccce 100644 --- a/src/endpoints/document/v1/patch.ts +++ b/src/endpoints/document/v1/patch.ts @@ -1,7 +1,7 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; +import { constant, DocumentVersion, mutable } from "#/global.ts"; import { compression } from "#document/compression.ts"; import { storage } from "#document/storage.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; @@ -78,7 +78,7 @@ Note: To remove (nullify) a value, send the header with an empty value`, bodySize, bodyCheck, async (ctx) => { - const { + let { actualName // @ts-expect-error upstream } = ctx.req.valid("param") as typeof schemaParam.infer; @@ -116,14 +116,23 @@ Note: To remove (nullify) a value, send the header with an empty value`, } mutable.database.document.update("name", actualName, "name", newName); + + actualName = newName; } if (ctx.get("hasBody")) { - await storage.write( - document.id, + mutable.database.document.update("name", actualName, "version", constant.env.JSPB_DOCUMENT_COMPRESSION); + + let contentStream: ReadableStream; + if (constant.env.JSPB_DOCUMENT_COMPRESSION === DocumentVersion.V1) { // ctx.req.raw.body is only null on GET/HEAD - compression.encode(ctx.req.raw.body as NonNullable) - ); + contentStream = compression.encode(ctx.req.raw.body as NonNullable); + } else { + // ctx.req.raw.body is only null on GET/HEAD + contentStream = ctx.req.raw.body as NonNullable; + } + + await storage.write(document.id, contentStream); } return ctx.body(null); diff --git a/src/endpoints/document/v1/post.ts b/src/endpoints/document/v1/post.ts index 13ed37dc..3972655e 100644 --- a/src/endpoints/document/v1/post.ts +++ b/src/endpoints/document/v1/post.ts @@ -2,8 +2,7 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { monotonicUlid } from "@std/ulid"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; -import { DocumentVersion } from "#db/query.ts"; +import { constant, DocumentVersion, mutable } from "#/global.ts"; import { compression } from "#document/compression.ts"; import { storage } from "#document/storage.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; @@ -112,16 +111,21 @@ export default new Hono().post( mutable.database.document.create({ id: setId, user_id: ctx.get("userId") ?? null, - version: DocumentVersion.V1, + version: constant.env.JSPB_DOCUMENT_COMPRESSION, name: setName, password: hashCombo }); - await storage.write( - setId, + let contentStream: ReadableStream; + if (constant.env.JSPB_DOCUMENT_COMPRESSION === DocumentVersion.V1) { // ctx.req.raw.body is only null on GET/HEAD - compression.encode(ctx.req.raw.body as NonNullable) - ); + contentStream = compression.encode(ctx.req.raw.body as NonNullable); + } else { + // ctx.req.raw.body is only null on GET/HEAD + contentStream = ctx.req.raw.body as NonNullable; + } + + await storage.write(setId, contentStream); return ctx.json({ name: setName diff --git a/src/endpoints/legacy/v2/documents/access.route.ts b/src/endpoints/legacy/v2/documents/access.route.ts index 10183ea8..084a94da 100644 --- a/src/endpoints/legacy/v2/documents/access.route.ts +++ b/src/endpoints/legacy/v2/documents/access.route.ts @@ -2,7 +2,7 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { toText } from "@std/streams"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; +import { constant, DocumentVersion, mutable } from "#/global.ts"; import { compression } from "#document/compression.ts"; import { storage } from "#document/storage.ts"; import type { Env } from "#http/type.ts"; @@ -90,9 +90,16 @@ export default new Hono().get( await using fileHandle = await storage.read(document.id); + let fileContent: ReadableStream; + if (document.version === DocumentVersion.V2) { + fileContent = fileHandle.readable; + } else { + fileContent = compression.decode(fileHandle.readable); + } + return ctx.json({ key: param.name, - data: await toText(compression.decode(fileHandle.readable)), + data: await toText(fileContent), url: new URL(ctx.req.url).host.concat("/", param.name), expirationTimestamp: 0 }); diff --git a/src/endpoints/legacy/v2/documents/accessRaw.route.ts b/src/endpoints/legacy/v2/documents/accessRaw.route.ts index fc2c0767..cc4a0ac1 100644 --- a/src/endpoints/legacy/v2/documents/accessRaw.route.ts +++ b/src/endpoints/legacy/v2/documents/accessRaw.route.ts @@ -2,7 +2,7 @@ import { stream } from "@hono/hono/streaming"; import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; +import { constant, DocumentVersion, mutable } from "#/global.ts"; import { compression } from "#document/compression.ts"; import { storage } from "#document/storage.ts"; import type { Env } from "#http/type.ts"; @@ -82,10 +82,15 @@ export default new Hono().get( const fileHandle = await storage.read(document.id); + const clientHasDeflate = ctx.req.header("accept-encoding")?.includes("deflate"); + let fileContent: ReadableStream; - if (ctx.req.header("accept-encoding")?.includes("deflate")) { + if (document.version === DocumentVersion.V2 || clientHasDeflate) { fileContent = fileHandle.readable; - ctx.res.headers.set("Content-Encoding", "deflate"); + + if (document.version === DocumentVersion.V1 && clientHasDeflate) { + ctx.res.headers.set("content-encoding", "deflate"); + } } else { fileContent = compression.decode(fileHandle.readable); } diff --git a/src/endpoints/legacy/v2/documents/edit.route.ts b/src/endpoints/legacy/v2/documents/edit.route.ts index 7f8a579a..32f19570 100644 --- a/src/endpoints/legacy/v2/documents/edit.route.ts +++ b/src/endpoints/legacy/v2/documents/edit.route.ts @@ -1,7 +1,7 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; +import { constant, DocumentVersion, mutable } from "#/global.ts"; import { compression } from "#document/compression.ts"; import { storage } from "#document/storage.ts"; import { bodySize } from "#http/middleware/bodySize.ts"; @@ -67,11 +67,18 @@ export default new Hono().patch( return error.throw(ErrorCode.documentNotFound); } - await storage.write( - document.id, + mutable.database.document.update("name", param.name, "version", constant.env.JSPB_DOCUMENT_COMPRESSION); + + let contentStream: ReadableStream; + if (constant.env.JSPB_DOCUMENT_COMPRESSION === DocumentVersion.V1) { + // ctx.req.raw.body is only null on GET/HEAD + contentStream = compression.encode(ctx.req.raw.body as NonNullable); + } else { // ctx.req.raw.body is only null on GET/HEAD - compression.encode(ctx.req.raw.body as NonNullable) - ); + contentStream = ctx.req.raw.body as NonNullable; + } + + await storage.write(document.id, contentStream); return ctx.json({ edited: true diff --git a/src/endpoints/legacy/v2/documents/publish.route.ts b/src/endpoints/legacy/v2/documents/publish.route.ts index 6a07c90b..716cbf59 100644 --- a/src/endpoints/legacy/v2/documents/publish.route.ts +++ b/src/endpoints/legacy/v2/documents/publish.route.ts @@ -2,8 +2,7 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { monotonicUlid } from "@std/ulid"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; -import { DocumentVersion } from "#db/query.ts"; +import { constant, DocumentVersion, mutable } from "#/global.ts"; import { compression } from "#document/compression.ts"; import { storage } from "#document/storage.ts"; import { bodySize } from "#http/middleware/bodySize.ts"; @@ -97,16 +96,21 @@ export default new Hono().post( mutable.database.document.create({ id: id, user_id: null, - version: DocumentVersion.V1, + version: constant.env.JSPB_DOCUMENT_COMPRESSION, name: setName, password: hashCombo }); - await storage.write( - id, + let contentStream: ReadableStream; + if (constant.env.JSPB_DOCUMENT_COMPRESSION === DocumentVersion.V1) { // ctx.req.raw.body is only null on GET/HEAD - compression.encode(ctx.req.raw.body as NonNullable) - ); + contentStream = compression.encode(ctx.req.raw.body as NonNullable); + } else { + // ctx.req.raw.body is only null on GET/HEAD + contentStream = ctx.req.raw.body as NonNullable; + } + + await storage.write(id, contentStream); return ctx.json({ key: setName, diff --git a/src/global.ts b/src/global.ts index 805bcdd8..b9a017e4 100644 --- a/src/global.ts +++ b/src/global.ts @@ -8,6 +8,14 @@ import { customAlphabet } from "nanoid"; import type { Database } from "#db/database"; import { humanizeSize, humanizeTime } from "#util/humanize.ts"; +export const DocumentVersion = { + // deflate + V1: 1, + // no compression + V2: 2 +} as const; +export type DocumentVersionType = (typeof DocumentVersion)[keyof typeof DocumentVersion]; + export const mutable = { database: undefined as unknown as Database, http: undefined as Deno.HttpServer | undefined, @@ -41,6 +49,9 @@ export const constant = { // document JSPB_DOCUMENT_SIZE: type.string.pipe(humanizeSize).default("1mb"), + JSPB_DOCUMENT_COMPRESSION: type.boolean + .pipe((boolean): DocumentVersionType => (boolean ? DocumentVersion.V1 : DocumentVersion.V2)) + .default(true), JSPB_DOCUMENT_AGE: type.string.pipe(humanizeTime).default("0"), JSPB_DOCUMENT_ANONYMOUS_AGE: type.string.pipe(humanizeTime).default("7d"), From 588efafc8224cc6650246b1f0917088373a524a6 Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Wed, 14 Jan 2026 11:18:46 +0100 Subject: [PATCH 44/47] Document streams optimization (#265) --- biome.json | 5 +- src/database/query.ts | 14 +-- src/document/compression.ts | 10 -- src/document/storage.ts | 58 --------- src/endpoints/document/v1/delete.ts | 4 +- src/endpoints/document/v1/get.ts | 22 +--- src/endpoints/document/v1/patch.ts | 23 +--- src/endpoints/document/v1/post.ts | 21 +--- .../legacy/v2/documents/access.route.ts | 16 +-- .../legacy/v2/documents/accessRaw.route.ts | 22 +--- .../legacy/v2/documents/edit.route.ts | 21 +--- .../legacy/v2/documents/publish.route.ts | 21 +--- .../legacy/v2/documents/remove.route.ts | 4 +- src/global.ts | 3 +- src/http/middleware/bodyCheck.ts | 50 -------- src/http/middleware/bodySize.ts | 51 -------- src/http/middleware/bodyStream.ts | 63 ++++++++++ src/tasks/sweeper.ts | 10 +- src/utils/fs.ts | 111 ++++++++++++++++++ 19 files changed, 224 insertions(+), 305 deletions(-) delete mode 100644 src/document/compression.ts delete mode 100644 src/document/storage.ts delete mode 100644 src/http/middleware/bodyCheck.ts delete mode 100644 src/http/middleware/bodySize.ts create mode 100644 src/http/middleware/bodyStream.ts create mode 100644 src/utils/fs.ts diff --git a/biome.json b/biome.json index b022bbd0..f7f2d7ff 100644 --- a/biome.json +++ b/biome.json @@ -83,7 +83,10 @@ "level": "error", "options": { "paths": { - "@hono/hono": "Use `@hono/hono/tiny` instead" + "@hono/hono": { + "message": "Use `@hono/hono/tiny` instead", + "allowImportNames": ["Context"] + } } } }, diff --git a/src/database/query.ts b/src/database/query.ts index 2eed13d7..7fc2d7e2 100644 --- a/src/database/query.ts +++ b/src/database/query.ts @@ -13,15 +13,13 @@ export type Document = { name: string; password: string | null; }; -export type DocumentColumn = Pick; -export type DocumentIndex = DocumentColumn<"id" | "name">; +export type DocumentIndex = Pick; export type User = { id: string; token: string; }; -export type UserColumn = Pick; -export type UserIndex = UserColumn<"id">; +export type UserIndex = Pick; abstract class Query
> { protected readonly database: Database; @@ -145,16 +143,16 @@ export class UserQuery extends Query { .get() as User | undefined; } - public getDocuments(id: string): DocumentColumn<"id" | "name">[] { + public getDocuments(id: string): Pick[] { return this.database .prepare(`SELECT document.id, document.name FROM document WHERE document.user_id = :id`) - .all({ id: id }) as DocumentColumn<"id" | "name">[]; + .all({ id: id }) as Pick[]; } public getAll = this.selectColumns; - public getAllWithoutDocuments(): UserColumn<"id">[] { + public getAllWithoutDocuments(): Pick[] { return this.database .prepare(`SELECT user.id FROM user @@ -162,6 +160,6 @@ export class UserQuery extends Query { FROM document WHERE document.user_id = user.id )`) - .all() as UserColumn<"id">[]; + .all() as Pick[]; } } diff --git a/src/document/compression.ts b/src/document/compression.ts deleted file mode 100644 index 94ade5a2..00000000 --- a/src/document/compression.ts +++ /dev/null @@ -1,10 +0,0 @@ -// node:zlib buffers the stream into memory -export const compression = { - encode: (readable: ReadableStream>): ReadableStream => { - return readable.pipeThrough(new CompressionStream("deflate")); - }, - - decode: (readable: ReadableStream>): ReadableStream => { - return readable.pipeThrough(new DecompressionStream("deflate")); - } -} as const; diff --git a/src/document/storage.ts b/src/document/storage.ts deleted file mode 100644 index 3b80602f..00000000 --- a/src/document/storage.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { constant } from "#/global.ts"; -import { ErrorCode, error } from "#util/error.ts"; - -export const storage = { - delete: async (id: string): Promise => { - try { - await Deno.remove(constant.path.struct.storageData + id); - } catch { - // already deleted (probably) - } - }, - - read: async (id: string): Promise => { - return Deno.open(constant.path.struct.storageData + id); - }, - - write: async (id: string, data: ReadableStream): Promise => { - await using handle = await Deno.open(constant.path.struct.storageData + id, { - create: true, - write: true, - truncate: true - }); - - try { - await data.pipeTo(handle.writable, { preventClose: true }); - } catch (why) { - void storage.delete(id); - - if (why instanceof Deno.errors.BrokenPipe) { - return error.throw(ErrorCode.documentInvalidSize); - } - - throw why; - } - }, - - // relaxed exists because races between fs/db may occur - list: function* (relaxed?: boolean): Iterable { - for (const entry of Deno.readDirSync(constant.path.struct.storageData)) { - if (entry.isFile) { - if (relaxed) { - const info = Deno.statSync(constant.path.struct.storageData + entry.name); - - if ( - !info.mtime || - constant.temporal.utc().epochMilliseconds - - info.mtime.toTemporalInstant().toZonedDateTimeISO("Etc/UTC").epochMilliseconds >= - 10_000 - ) { - yield entry.name; - } - } else { - yield entry.name; - } - } - } - } -} as const; diff --git a/src/endpoints/document/v1/delete.ts b/src/endpoints/document/v1/delete.ts index b3ff5794..654ad124 100644 --- a/src/endpoints/document/v1/delete.ts +++ b/src/endpoints/document/v1/delete.ts @@ -2,11 +2,11 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, validator } from "@hono/openapi"; import { type } from "arktype"; import { constant, mutable } from "#/global.ts"; -import { storage } from "#document/storage.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import type { Env } from "#http/type.ts"; import { isOwner } from "#util/document.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { fsDelete } from "#util/fs.ts"; import { validatorDocumentName } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -52,7 +52,7 @@ export default new Hono().delete( } mutable.database.document.delete("name", name); - void storage.delete(document.id); + void fsDelete(document); return ctx.body(null); } diff --git a/src/endpoints/document/v1/get.ts b/src/endpoints/document/v1/get.ts index 6ef2efbe..c3a4d17c 100644 --- a/src/endpoints/document/v1/get.ts +++ b/src/endpoints/document/v1/get.ts @@ -3,12 +3,11 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { decodeTime } from "@std/ulid"; import { type } from "arktype"; -import { constant, DocumentVersion, mutable } from "#/global.ts"; -import { compression } from "#document/compression.ts"; -import { storage } from "#document/storage.ts"; +import { constant, mutable } from "#/global.ts"; import type { Env } from "#http/type.ts"; import { verifyHash } from "#util/crypto.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { fsRead } from "#util/fs.ts"; import { validatorDocumentDownload, validatorDocumentName, @@ -103,21 +102,6 @@ Note: If you only need to query the document metadata, you should use HEAD metho return ctx.body(null); } - const fileHandle = await storage.read(document.id); - - const clientHasDeflate = ctx.req.header("accept-encoding")?.includes("deflate"); - - let fileContent: ReadableStream; - if (document.version === DocumentVersion.V2 || clientHasDeflate) { - fileContent = fileHandle.readable; - - if (document.version === DocumentVersion.V1 && clientHasDeflate) { - ctx.res.headers.set("content-encoding", "deflate"); - } - } else { - fileContent = compression.decode(fileHandle.readable); - } - if (typeof dl !== "undefined") { ctx.res.headers.set("content-disposition", `attachment; filename="jspaste_${name}"`); } @@ -125,6 +109,6 @@ Note: If you only need to query the document metadata, you should use HEAD metho ctx.res.headers.set("content-type", "text/plain"); ctx.res.headers.set("transfer-encoding", "chunked"); - return stream(ctx, async (stream) => await stream.pipe(fileContent)); + return stream(ctx, async (stream) => await stream.pipe(await fsRead(ctx, document))); } ); diff --git a/src/endpoints/document/v1/patch.ts b/src/endpoints/document/v1/patch.ts index 05dfccce..1cc5fe5f 100644 --- a/src/endpoints/document/v1/patch.ts +++ b/src/endpoints/document/v1/patch.ts @@ -1,16 +1,14 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, DocumentVersion, mutable } from "#/global.ts"; -import { compression } from "#document/compression.ts"; -import { storage } from "#document/storage.ts"; +import { constant, mutable } from "#/global.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; -import { bodyCheck } from "#http/middleware/bodyCheck.ts"; -import { bodySize } from "#http/middleware/bodySize.ts"; +import { bodyStream } from "#http/middleware/bodyStream.ts"; import type { Env } from "#http/type.ts"; import { generateHash } from "#util/crypto.ts"; import { isOwner } from "#util/document.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName, validatorDocumentPassword, @@ -75,8 +73,7 @@ Note: To remove (nullify) a value, send the header with an empty value`, validator("param", schemaParam, validatorHandler), validator("header", schemaHeader, validatorHandler), authMiddleware, - bodySize, - bodyCheck, + bodyStream, async (ctx) => { let { actualName @@ -122,17 +119,7 @@ Note: To remove (nullify) a value, send the header with an empty value`, if (ctx.get("hasBody")) { mutable.database.document.update("name", actualName, "version", constant.env.JSPB_DOCUMENT_COMPRESSION); - - let contentStream: ReadableStream; - if (constant.env.JSPB_DOCUMENT_COMPRESSION === DocumentVersion.V1) { - // ctx.req.raw.body is only null on GET/HEAD - contentStream = compression.encode(ctx.req.raw.body as NonNullable); - } else { - // ctx.req.raw.body is only null on GET/HEAD - contentStream = ctx.req.raw.body as NonNullable; - } - - await storage.write(document.id, contentStream); + await fsWrite(ctx, document); } return ctx.body(null); diff --git a/src/endpoints/document/v1/post.ts b/src/endpoints/document/v1/post.ts index 3972655e..8d059008 100644 --- a/src/endpoints/document/v1/post.ts +++ b/src/endpoints/document/v1/post.ts @@ -2,15 +2,14 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { monotonicUlid } from "@std/ulid"; import { type } from "arktype"; -import { constant, DocumentVersion, mutable } from "#/global.ts"; -import { compression } from "#document/compression.ts"; -import { storage } from "#document/storage.ts"; +import { constant, mutable } from "#/global.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; -import { bodySize } from "#http/middleware/bodySize.ts"; +import { bodyStream } from "#http/middleware/bodyStream.ts"; import type { Env } from "#http/type.ts"; import { generateHash } from "#util/crypto.ts"; import { generateName } from "#util/document.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName, validatorDocumentNameLength, @@ -79,7 +78,7 @@ export default new Hono().post( }), validator("header", schemaHeader, validatorHandler), authMiddleware, - bodySize, + bodyStream, async (ctx) => { const { "x-jspaste-password": password, @@ -115,17 +114,7 @@ export default new Hono().post( name: setName, password: hashCombo }); - - let contentStream: ReadableStream; - if (constant.env.JSPB_DOCUMENT_COMPRESSION === DocumentVersion.V1) { - // ctx.req.raw.body is only null on GET/HEAD - contentStream = compression.encode(ctx.req.raw.body as NonNullable); - } else { - // ctx.req.raw.body is only null on GET/HEAD - contentStream = ctx.req.raw.body as NonNullable; - } - - await storage.write(setId, contentStream); + await fsWrite(ctx, { id: setId }); return ctx.json({ name: setName diff --git a/src/endpoints/legacy/v2/documents/access.route.ts b/src/endpoints/legacy/v2/documents/access.route.ts index 084a94da..99ebf896 100644 --- a/src/endpoints/legacy/v2/documents/access.route.ts +++ b/src/endpoints/legacy/v2/documents/access.route.ts @@ -2,12 +2,11 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { toText } from "@std/streams"; import { type } from "arktype"; -import { constant, DocumentVersion, mutable } from "#/global.ts"; -import { compression } from "#document/compression.ts"; -import { storage } from "#document/storage.ts"; +import { constant, mutable } from "#/global.ts"; import type { Env } from "#http/type.ts"; import { verifyHash } from "#util/crypto.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { fsRead } from "#util/fs.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -88,18 +87,9 @@ export default new Hono().get( } } - await using fileHandle = await storage.read(document.id); - - let fileContent: ReadableStream; - if (document.version === DocumentVersion.V2) { - fileContent = fileHandle.readable; - } else { - fileContent = compression.decode(fileHandle.readable); - } - return ctx.json({ key: param.name, - data: await toText(fileContent), + data: await toText(await fsRead(ctx, document, true)), url: new URL(ctx.req.url).host.concat("/", param.name), expirationTimestamp: 0 }); diff --git a/src/endpoints/legacy/v2/documents/accessRaw.route.ts b/src/endpoints/legacy/v2/documents/accessRaw.route.ts index cc4a0ac1..c6a40ca6 100644 --- a/src/endpoints/legacy/v2/documents/accessRaw.route.ts +++ b/src/endpoints/legacy/v2/documents/accessRaw.route.ts @@ -2,12 +2,11 @@ import { stream } from "@hono/hono/streaming"; import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, DocumentVersion, mutable } from "#/global.ts"; -import { compression } from "#document/compression.ts"; -import { storage } from "#document/storage.ts"; +import { constant, mutable } from "#/global.ts"; import type { Env } from "#http/type.ts"; import { verifyHash } from "#util/crypto.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { fsRead } from "#util/fs.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -80,24 +79,9 @@ export default new Hono().get( } } - const fileHandle = await storage.read(document.id); - - const clientHasDeflate = ctx.req.header("accept-encoding")?.includes("deflate"); - - let fileContent: ReadableStream; - if (document.version === DocumentVersion.V2 || clientHasDeflate) { - fileContent = fileHandle.readable; - - if (document.version === DocumentVersion.V1 && clientHasDeflate) { - ctx.res.headers.set("content-encoding", "deflate"); - } - } else { - fileContent = compression.decode(fileHandle.readable); - } - ctx.res.headers.set("content-type", "text/plain"); ctx.res.headers.set("transfer-encoding", "chunked"); - return stream(ctx, async (stream) => await stream.pipe(fileContent)); + return stream(ctx, async (stream) => await stream.pipe(await fsRead(ctx, document, true))); } ); diff --git a/src/endpoints/legacy/v2/documents/edit.route.ts b/src/endpoints/legacy/v2/documents/edit.route.ts index 32f19570..ccd017fb 100644 --- a/src/endpoints/legacy/v2/documents/edit.route.ts +++ b/src/endpoints/legacy/v2/documents/edit.route.ts @@ -1,12 +1,11 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, DocumentVersion, mutable } from "#/global.ts"; -import { compression } from "#document/compression.ts"; -import { storage } from "#document/storage.ts"; -import { bodySize } from "#http/middleware/bodySize.ts"; +import { constant, mutable } from "#/global.ts"; +import { bodyStream } from "#http/middleware/bodyStream.ts"; import type { Env } from "#http/type.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -57,7 +56,7 @@ export default new Hono().patch( } }), validator("param", schemaParam, validatorHandler), - bodySize, + bodyStream, async (ctx) => { // @ts-expect-error upstream const param = ctx.req.valid("param") as typeof schemaParam.infer; @@ -68,17 +67,7 @@ export default new Hono().patch( } mutable.database.document.update("name", param.name, "version", constant.env.JSPB_DOCUMENT_COMPRESSION); - - let contentStream: ReadableStream; - if (constant.env.JSPB_DOCUMENT_COMPRESSION === DocumentVersion.V1) { - // ctx.req.raw.body is only null on GET/HEAD - contentStream = compression.encode(ctx.req.raw.body as NonNullable); - } else { - // ctx.req.raw.body is only null on GET/HEAD - contentStream = ctx.req.raw.body as NonNullable; - } - - await storage.write(document.id, contentStream); + await fsWrite(ctx, document); return ctx.json({ edited: true diff --git a/src/endpoints/legacy/v2/documents/publish.route.ts b/src/endpoints/legacy/v2/documents/publish.route.ts index 716cbf59..2487eb31 100644 --- a/src/endpoints/legacy/v2/documents/publish.route.ts +++ b/src/endpoints/legacy/v2/documents/publish.route.ts @@ -2,14 +2,13 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { monotonicUlid } from "@std/ulid"; import { type } from "arktype"; -import { constant, DocumentVersion, mutable } from "#/global.ts"; -import { compression } from "#document/compression.ts"; -import { storage } from "#document/storage.ts"; -import { bodySize } from "#http/middleware/bodySize.ts"; +import { constant, mutable } from "#/global.ts"; +import { bodyStream } from "#http/middleware/bodyStream.ts"; import type { Env } from "#http/type.ts"; import { generateHash } from "#util/crypto.ts"; import { generateName } from "#util/document.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -64,7 +63,7 @@ export default new Hono().post( } }), validator("header", schemaHeader, validatorHandler), - bodySize, + bodyStream, async (ctx) => { const { password, @@ -100,17 +99,7 @@ export default new Hono().post( name: setName, password: hashCombo }); - - let contentStream: ReadableStream; - if (constant.env.JSPB_DOCUMENT_COMPRESSION === DocumentVersion.V1) { - // ctx.req.raw.body is only null on GET/HEAD - contentStream = compression.encode(ctx.req.raw.body as NonNullable); - } else { - // ctx.req.raw.body is only null on GET/HEAD - contentStream = ctx.req.raw.body as NonNullable; - } - - await storage.write(id, contentStream); + await fsWrite(ctx, { id: id }); return ctx.json({ key: setName, diff --git a/src/endpoints/legacy/v2/documents/remove.route.ts b/src/endpoints/legacy/v2/documents/remove.route.ts index b9ddd7ea..944ad2ff 100644 --- a/src/endpoints/legacy/v2/documents/remove.route.ts +++ b/src/endpoints/legacy/v2/documents/remove.route.ts @@ -2,9 +2,9 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; import { constant, mutable } from "#/global.ts"; -import { storage } from "#document/storage.ts"; import type { Env } from "#http/type.ts"; import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { fsDelete } from "#util/fs.ts"; import { validatorDocumentName } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -51,7 +51,7 @@ export default new Hono().delete( } mutable.database.document.delete("name", param.name); - void storage.delete(document.id); + void fsDelete(document); return ctx.json({ removed: true }); } diff --git a/src/global.ts b/src/global.ts index b9a017e4..28400ff0 100644 --- a/src/global.ts +++ b/src/global.ts @@ -83,7 +83,8 @@ export const constant = { dispose: new Map Promise]>() }, temporal: { - utc: () => Temporal.Now.zonedDateTimeISO("Etc/UTC"), + UTC: () => Temporal.Now.zonedDateTimeISO("Etc/UTC"), + toUTC: (temporal: Temporal.Instant) => temporal.toZonedDateTimeISO("Etc/UTC"), instant: Temporal.Now.instant }, http: STATUS_CODES as Record, diff --git a/src/http/middleware/bodyCheck.ts b/src/http/middleware/bodyCheck.ts deleted file mode 100644 index 9dc34bf5..00000000 --- a/src/http/middleware/bodyCheck.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createMiddleware } from "@hono/hono/factory"; -import type { Env } from "../type.ts"; - -export const bodyCheck = createMiddleware(async (ctx, next) => { - if (!ctx.req.raw.body) { - ctx.set("hasBody", false); - return next(); - } - - const contentLength = ctx.req.raw.headers.get("content-length"); - const transferEncoding = ctx.req.raw.headers.has("transfer-encoding"); - - if (contentLength !== null && !transferEncoding) { - ctx.set("hasBody", Number.parseInt(contentLength, 10) > 0); - return next(); - } - - const stream = ctx.req.raw.body.getReader(); - const chunk = await stream.read(); - if (chunk.done || chunk.value.length === 0) { - ctx.set("hasBody", false); - return next(); - } - - ctx.set("hasBody", true); - - const reader = new ReadableStream({ - start: (controller) => { - // reinsert the validation chunk - controller.enqueue(chunk.value); - }, - pull: async (controller) => { - const { done, value } = await stream.read(); - if (done) { - controller.close(); - return; - } - - controller.enqueue(value); - }, - cancel: () => { - stream.cancel(); - } - }); - - const requestInit: RequestInit & { duplex: "half" } = { body: reader, duplex: "half" }; - ctx.req.raw = new Request(ctx.req.raw, requestInit as RequestInit); - - await next(); -}); diff --git a/src/http/middleware/bodySize.ts b/src/http/middleware/bodySize.ts deleted file mode 100644 index 8115be2a..00000000 --- a/src/http/middleware/bodySize.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createMiddleware } from "@hono/hono/factory"; -import { constant } from "#/global.ts"; -import { ErrorCode, error } from "#util/error.ts"; -import type { Env } from "../type.ts"; - -// https://github.com/honojs/hono/blob/main/src/middleware/body-limit/index.ts -export const bodySize = createMiddleware(async (ctx, next) => { - if (!ctx.req.raw.body) { - return next(); - } - - const contentLength = ctx.req.raw.headers.get("content-length"); - const transferEncoding = ctx.req.raw.headers.has("transfer-encoding"); - - if (contentLength !== null && !transferEncoding) { - if (Number.parseInt(contentLength, 10) > constant.env.JSPB_DOCUMENT_SIZE) { - return error.throw(ErrorCode.documentInvalidSize); - } - - return next(); - } - - let size = 0; - const stream = ctx.req.raw.body.getReader(); - const reader = new ReadableStream({ - pull: async (controller) => { - const { done, value } = await stream.read(); - if (done) { - controller.close(); - return; - } - - size += value.length; - if (size > constant.env.JSPB_DOCUMENT_SIZE) { - stream.cancel(); - controller.error(new Deno.errors.BrokenPipe()); - return; - } - - controller.enqueue(value); - }, - cancel: () => { - stream.cancel(); - } - }); - - const requestInit: RequestInit & { duplex: "half" } = { body: reader, duplex: "half" }; - ctx.req.raw = new Request(ctx.req.raw, requestInit as RequestInit); - - await next(); -}); diff --git a/src/http/middleware/bodyStream.ts b/src/http/middleware/bodyStream.ts new file mode 100644 index 00000000..b150f7d5 --- /dev/null +++ b/src/http/middleware/bodyStream.ts @@ -0,0 +1,63 @@ +import { createMiddleware } from "@hono/hono/factory"; +import { constant } from "#/global.ts"; +import { ErrorCode, error } from "#util/error.ts"; +import type { Env } from "../type.ts"; + +export const bodyStream = createMiddleware(async (ctx, next) => { + if (!ctx.req.raw.body) { + ctx.set("hasBody", false); + + return next(); + } + + const contentLengthHeader = ctx.req.raw.headers.get("content-length"); + if (contentLengthHeader !== null && !ctx.req.raw.headers.has("transfer-encoding")) { + const size = Number.parseInt(contentLengthHeader, 10); + if (size > constant.env.JSPB_DOCUMENT_SIZE) { + return error.throw(ErrorCode.documentInvalidSize); + } + + ctx.set("hasBody", size > 0); + + return next(); + } + + const bodyReader = ctx.req.raw.body.getReader(); + const head = await bodyReader.read(); + + bodyReader.releaseLock(); + + if (head.done || head.value.length === 0) { + ctx.set("hasBody", false); + + return next(); + } + + ctx.set("hasBody", true); + + let size = head.value.length; + const transformer = new TransformStream, Uint8Array>({ + start: (controller) => { + // reinsert head + controller.enqueue(head.value); + }, + transform: (chunk, controller) => { + size += chunk.length; + + if (size > constant.env.JSPB_DOCUMENT_SIZE) { + controller.error(new Deno.errors.BrokenPipe()); + return; + } + + controller.enqueue(chunk); + } + }); + + const requestInit: RequestInit & { duplex: "half" } = { + body: ctx.req.raw.body.pipeThrough(transformer), + duplex: "half" + }; + ctx.req.raw = new Request(ctx.req.raw, requestInit); + + await next(); +}); diff --git a/src/tasks/sweeper.ts b/src/tasks/sweeper.ts index c0fc7412..c16bc442 100644 --- a/src/tasks/sweeper.ts +++ b/src/tasks/sweeper.ts @@ -2,8 +2,8 @@ import { mapNotNullish } from "@std/collections"; import { decodeTime } from "@std/ulid"; import { constant, mutable } from "#/global.ts"; import { Database } from "#db/database.ts"; -import { storage } from "#document/storage.ts"; import { Logger } from "#util/console.ts"; +import { fsDelete, fsList } from "../utils/fs.ts"; const log: Logger = new Logger("task::sweeper"); @@ -20,7 +20,7 @@ export const sweeper = async (): Promise => { const sweeperDatabaseUser = (): void => { using database = new Database(); - const temporalFuture = constant.temporal.utc().add({ days: 3 }); + const temporalFuture = constant.temporal.UTC().add({ days: 3 }); const users = mapNotNullish(database.user.getAllWithoutDocuments(), ({ id }) => { if (!id) return; @@ -42,7 +42,7 @@ const sweeperDatabaseUser = (): void => { const sweeperDatabaseDocument = (): void => { using database = new Database(); - const temporalNow = constant.temporal.utc(); + const temporalNow = constant.temporal.UTC(); const documents = mapNotNullish(database.document.getAll(["id", "user_id"]), ({ id, user_id }) => { if (!id) return; @@ -68,7 +68,7 @@ const sweeperDangling = async (): Promise => { using database = new Database(); const databaseDocuments = mapNotNullish(database.document.getAll(["id"]), ({ id }) => id); - const storageDocuments = storage.list(true); + const storageDocuments = fsList(true); const databaseDocumentsSet = new Set(databaseDocuments); const storageDocumentsSet = new Set(storageDocuments); @@ -85,7 +85,7 @@ const sweeperDangling = async (): Promise => { if (storageDocumentsDangling.size > 0) { for (const id of storageDocumentsDangling) { - queue.push(storage.delete(id)); + queue.push(fsDelete({ id: id })); } await Promise.all(queue); diff --git a/src/utils/fs.ts b/src/utils/fs.ts new file mode 100644 index 00000000..c599f3d5 --- /dev/null +++ b/src/utils/fs.ts @@ -0,0 +1,111 @@ +import type { Context } from "@hono/hono"; +import type { Document } from "#db/query.ts"; +import { constant, DocumentVersion } from "../global.ts"; +import type { Env } from "../http/type.ts"; +import { ErrorCode, error } from "./error.ts"; + +export const fsWrite = async (ctx: Context, { id }: Pick): Promise => { + await using handle = await Deno.open(constant.path.struct.storageData + id, { + create: true, + write: true, + truncate: true + }); + + let stream: ReadableStream; + switch (constant.env.JSPB_DOCUMENT_COMPRESSION) { + case DocumentVersion.V1: { + // ctx.req.raw.body is only null on GET/HEAD + stream = (ctx.req.raw.body as NonNullable).pipeThrough(new CompressionStream("deflate")); + + break; + } + case DocumentVersion.V2: { + // ctx.req.raw.body is only null on GET/HEAD + stream = ctx.req.raw.body as NonNullable; + + break; + } + default: { + return error.throw(ErrorCode.documentCorrupted); + } + } + + try { + await stream.pipeTo(handle.writable, { preventClose: true }); + } catch (why) { + void fsDelete({ id: id }); + + if (why instanceof Deno.errors.BrokenPipe) { + return error.throw(ErrorCode.documentInvalidSize); + } + + throw why; + } +}; + +export const fsDelete = async ({ id }: Pick): Promise => { + try { + await Deno.remove(constant.path.struct.storageData + id); + } catch (why) { + // already deleted + if (why instanceof Deno.errors.NotFound) return; + + throw why; + } +}; + +export const fsRead = async ( + ctx: Context, + { id, version }: Pick, + clientIgnoreCapabilities = false +): Promise>> => { + const handle = await Deno.open(constant.path.struct.storageData + id); + + const hasClientDeflate = clientIgnoreCapabilities ? false : ctx.req.header("accept-encoding")?.includes("deflate"); + + let stream: ReadableStream; + switch (version) { + case DocumentVersion.V1: { + if (hasClientDeflate) { + ctx.res.headers.set("content-encoding", "deflate"); + stream = handle.readable; + } else { + stream = handle.readable.pipeThrough(new DecompressionStream("deflate")); + } + + break; + } + case DocumentVersion.V2: { + stream = handle.readable; + + break; + } + default: { + return error.throw(ErrorCode.documentCorrupted); + } + } + + return stream; +}; + +// relaxed exists because races between fs/db may occur +export function* fsList(relaxed?: boolean): Iterable { + for (const entry of Deno.readDirSync(constant.path.struct.storageData)) { + if (entry.isFile) { + if (relaxed) { + const info = Deno.statSync(constant.path.struct.storageData + entry.name); + + if ( + !info.mtime || + constant.temporal.UTC().epochMilliseconds - + constant.temporal.toUTC(info.mtime.toTemporalInstant()).epochMilliseconds >= + 10_000 + ) { + yield entry.name; + } + } else { + yield entry.name; + } + } + } +} From 7b011028e0141d74b1c416f84b3fc35e707da71e Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Wed, 14 Jan 2026 13:51:32 +0100 Subject: [PATCH 45/47] "Deobjectify" (#266) --- src/database/{database.ts => index.ts} | 15 +-- src/database/migration.ts | 2 +- src/database/query.ts | 7 +- src/endpoints/document/v1/delete.ts | 18 +-- src/endpoints/document/v1/get.ts | 24 ++-- src/endpoints/document/v1/index.ts | 14 +-- src/endpoints/document/v1/list.ts | 16 +-- src/endpoints/document/v1/patch.ts | 33 +++-- src/endpoints/document/v1/post.ts | 23 ++-- .../legacy/v2/documents/access.route.ts | 24 ++-- .../legacy/v2/documents/accessRaw.route.ts | 24 ++-- .../legacy/v2/documents/edit.route.ts | 17 +-- .../legacy/v2/documents/exists.route.ts | 10 +- src/endpoints/legacy/v2/documents/index.ts | 16 +-- .../legacy/v2/documents/publish.route.ts | 24 ++-- .../legacy/v2/documents/remove.route.ts | 14 +-- src/endpoints/user/v1/create.ts | 19 +-- src/endpoints/user/v1/index.ts | 6 +- src/global.ts | 104 ++++------------ src/http/{router.ts => handler.ts} | 56 +++++---- src/http/{server.ts => index.ts} | 14 +-- src/http/middleware/authorization.ts | 14 +-- src/http/middleware/bodyStream.ts | 12 +- src/http/type.ts | 6 - src/init.ts | 35 +++--- src/task.ts | 6 +- src/tasks/sweeper.ts | 15 +-- src/utils/console.ts | 6 +- src/utils/crypto.ts | 4 +- src/utils/document.ts | 13 +- src/utils/env.ts | 45 +++++++ src/utils/error.ts | 115 ++++++++++-------- src/utils/fs.ts | 38 +++--- src/utils/{validator => }/regex.ts | 0 src/utils/user.ts | 4 +- src/utils/validator/document.ts | 19 +-- src/utils/validator/handler.ts | 4 +- src/utils/validator/user.ts | 6 +- 38 files changed, 438 insertions(+), 384 deletions(-) rename src/database/{database.ts => index.ts} (90%) rename src/http/{router.ts => handler.ts} (64%) rename src/http/{server.ts => index.ts} (61%) delete mode 100644 src/http/type.ts create mode 100644 src/utils/env.ts rename src/utils/{validator => }/regex.ts (100%) diff --git a/src/database/database.ts b/src/database/index.ts similarity index 90% rename from src/database/database.ts rename to src/database/index.ts index af5cc5e1..3d1503ae 100644 --- a/src/database/database.ts +++ b/src/database/index.ts @@ -1,9 +1,10 @@ import { DatabaseSync, type StatementSync } from "node:sqlite"; import { monotonicUlid, ulid } from "@std/ulid"; -import { constant } from "#/global.ts"; import { Logger } from "#util/console.ts"; import { generateHash } from "#util/crypto.ts"; import { generateToken } from "#util/user.ts"; +import { constantPathDatabaseFile, constantStoreStatements } from "../global.ts"; +import { env } from "../utils/env.ts"; import { migrations } from "./migration.ts"; import { DocumentQuery, UserQuery } from "./query.ts"; @@ -20,9 +21,9 @@ export class Database { private readonly database: DatabaseSync; public constructor(options: Options = {}) { - options.ephemeral ??= constant.env.JSPB_DEBUG_DATABASE_EPHEMERAL; + options.ephemeral ??= env.JSPB_DEBUG_DATABASE_EPHEMERAL; - this.database = new DatabaseSync(options.ephemeral ? ":memory:" : constant.path.databaseFile); + this.database = new DatabaseSync(options.ephemeral ? ":memory:" : constantPathDatabaseFile); if (options.ephemeral) { log.warn("Using ephemeral. No changes will persist."); @@ -67,7 +68,7 @@ export class Database { try { const rootId = this.user.getRoot()?.id; - if (constant.env.JSPB_USER_ROOT_RECOVERY && rootId) { + if (env.JSPB_USER_ROOT_RECOVERY && rootId) { const token = generateToken(rootId); const hash = generateHash(token); @@ -94,10 +95,10 @@ export class Database { return this.database.prepare(sql); } - let statement = constant.store.statements.get(sql); + let statement = constantStoreStatements.get(sql); if (!statement) { statement = this.database.prepare(sql); - constant.store.statements.set(sql, statement); + constantStoreStatements.set(sql, statement); } return statement; @@ -134,7 +135,7 @@ export class Database { } public [Symbol.dispose](): void { - constant.store.statements.clear(); + constantStoreStatements.clear(); this.database.close(); } } diff --git a/src/database/migration.ts b/src/database/migration.ts index 463d5716..ae7f5233 100644 --- a/src/database/migration.ts +++ b/src/database/migration.ts @@ -1,9 +1,9 @@ import { mapNotNullish } from "@std/collections"; import { ulid } from "@std/ulid"; +import type { Database } from "#db/index.ts"; import { Logger } from "#util/console.ts"; import { generateHash } from "#util/crypto.ts"; import { mutable } from "../global.ts"; -import type { Database } from "./database.ts"; const log: Logger = new Logger("database::migration"); diff --git a/src/database/query.ts b/src/database/query.ts index 7fc2d7e2..ecb38589 100644 --- a/src/database/query.ts +++ b/src/database/query.ts @@ -1,10 +1,11 @@ import type { SQLInputValue } from "node:sqlite"; import { chunk } from "@std/collections"; import { monotonicUlid } from "@std/ulid"; -import { constant, type DocumentVersionType } from "#/global.ts"; +import type { Database } from "#db/index.ts"; import { generateHash } from "#util/crypto.ts"; import { generateToken } from "#util/user.ts"; -import type { Database } from "./database.ts"; +import { constantDatabaseMaxElements } from "../global.ts"; +import type { DocumentVersionType } from "../utils/document.ts"; export type Document = { id: string; @@ -39,7 +40,7 @@ abstract class Query
> { } this.database.transaction(() => { - for (const batch of chunk(defaultValues, constant.databaseMaxElements)) { + for (const batch of chunk(defaultValues, constantDatabaseMaxElements)) { this.database .prepare( `DELETE diff --git a/src/endpoints/document/v1/delete.ts b/src/endpoints/document/v1/delete.ts index 654ad124..fb437636 100644 --- a/src/endpoints/document/v1/delete.ts +++ b/src/endpoints/document/v1/delete.ts @@ -1,11 +1,11 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; -import type { Env } from "#http/type.ts"; import { isOwner } from "#util/document.ts"; -import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { errorCodeDocumentNotFound, errorCodeUserInvalidToken, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsDelete } from "#util/fs.ts"; import { validatorDocumentName } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -23,13 +23,13 @@ export default new Hono().delete( security: [{}, { bearer: [] }], responses: { 200: { - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] }, + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] }, // auth middleware - 401: { ...genericErrorResponse, description: constant.http[401] } + 401: { ...genericErrorResponse, description: constantHttpStatusCodes[401] } } }), validator("param", schemaParam, validatorHandler), @@ -42,13 +42,13 @@ export default new Hono().delete( const document = mutable.database.document.get("name", name); if (!document?.id) { - return error.throw(ErrorCode.documentNotFound); + return errorThrow(errorCodeDocumentNotFound); } const userId = ctx.get("userId"); const owner = isOwner(userId, document.user_id); if (!owner) { - return error.throw(ErrorCode.userInvalidToken); + return errorThrow(errorCodeUserInvalidToken); } mutable.database.document.delete("name", name); diff --git a/src/endpoints/document/v1/get.ts b/src/endpoints/document/v1/get.ts index c3a4d17c..127da649 100644 --- a/src/endpoints/document/v1/get.ts +++ b/src/endpoints/document/v1/get.ts @@ -3,10 +3,16 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { decodeTime } from "@std/ulid"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; -import type { Env } from "#http/type.ts"; +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { verifyHash } from "#util/crypto.ts"; -import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { + errorCodeDocumentInvalidPassword, + errorCodeDocumentNotFound, + errorCodeDocumentPasswordNeeded, + errorThrow, + genericErrorResponse +} from "#util/error.ts"; import { fsRead } from "#util/fs.ts"; import { validatorDocumentDownload, @@ -55,10 +61,10 @@ Note: If you only need to query the document metadata, you should use HEAD metho } }, headers: schemaHeaderResponse.components, - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] } + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] } } }), validator("param", schemaParam, validatorHandler), @@ -80,15 +86,15 @@ Note: If you only need to query the document metadata, you should use HEAD metho const document = mutable.database.document.get("name", name); if (!document?.id) { - return error.throw(ErrorCode.documentNotFound); + return errorThrow(errorCodeDocumentNotFound); } if (document.password) { if (!password) { - return error.throw(ErrorCode.documentPasswordNeeded); + return errorThrow(errorCodeDocumentPasswordNeeded); } if (!verifyHash(password, document.password)) { - return error.throw(ErrorCode.documentInvalidPassword); + return errorThrow(errorCodeDocumentInvalidPassword); } } diff --git a/src/endpoints/document/v1/index.ts b/src/endpoints/document/v1/index.ts index 74c20b39..0bb8d9a3 100644 --- a/src/endpoints/document/v1/index.ts +++ b/src/endpoints/document/v1/index.ts @@ -1,15 +1,15 @@ import { Hono } from "@hono/hono/tiny"; -import type { Env } from "#http/type.ts"; +import type { Env } from "#http/handler.ts"; import delete_ from "./delete.ts"; import get from "./get.ts"; import list from "./list.ts"; import patch from "./patch.ts"; import post from "./post.ts"; -export const v1DocumentRouter = new Hono(); +export const v1DocumentHandler = new Hono(); -v1DocumentRouter.route("/", delete_); -v1DocumentRouter.route("/", get); -v1DocumentRouter.route("/", list); -v1DocumentRouter.route("/", patch); -v1DocumentRouter.route("/", post); +v1DocumentHandler.route("/", delete_); +v1DocumentHandler.route("/", get); +v1DocumentHandler.route("/", list); +v1DocumentHandler.route("/", patch); +v1DocumentHandler.route("/", post); diff --git a/src/endpoints/document/v1/list.ts b/src/endpoints/document/v1/list.ts index 68ceaf49..b5576001 100644 --- a/src/endpoints/document/v1/list.ts +++ b/src/endpoints/document/v1/list.ts @@ -1,10 +1,10 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver } from "@hono/openapi"; import { decodeTime } from "@std/ulid"; -import { constant, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; -import type { Env } from "#http/type.ts"; -import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { errorCodeUserInvalidToken, errorThrow, genericErrorResponse } from "#util/error.ts"; import { validatorDocumentListObject } from "#util/validator/document.ts"; const schemaBodyResponse = await resolver(validatorDocumentListObject.array()).toOpenAPISchema(); @@ -23,20 +23,20 @@ export default new Hono().get( schema: schemaBodyResponse.schema } }, - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] }, + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] }, // auth middleware - 401: { ...genericErrorResponse, description: constant.http[401] } + 401: { ...genericErrorResponse, description: constantHttpStatusCodes[401] } } }), authMiddleware, async (ctx) => { const userId = ctx.get("userId"); if (!userId) { - return error.throw(ErrorCode.userInvalidToken); + return errorThrow(errorCodeUserInvalidToken); } // https://github.com/honojs/hono/issues/1130 diff --git a/src/endpoints/document/v1/patch.ts b/src/endpoints/document/v1/patch.ts index 1cc5fe5f..240fd7af 100644 --- a/src/endpoints/document/v1/patch.ts +++ b/src/endpoints/document/v1/patch.ts @@ -1,13 +1,20 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { bodyStream } from "#http/middleware/bodyStream.ts"; -import type { Env } from "#http/type.ts"; import { generateHash } from "#util/crypto.ts"; import { isOwner } from "#util/document.ts"; -import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { env } from "#util/env.ts"; +import { + errorCodeDocumentNameAlreadyExists, + errorCodeDocumentNotFound, + errorCodeUserInvalidToken, + errorThrow, + genericErrorResponse +} from "#util/error.ts"; import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName, @@ -55,19 +62,19 @@ Note: To remove (nullify) a value, send the header with an empty value`, }, responses: { 200: { - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] }, + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] }, // auth middleware - 401: { ...genericErrorResponse, description: constant.http[401] }, + 401: { ...genericErrorResponse, description: constantHttpStatusCodes[401] }, // document name already exists - 409: { ...genericErrorResponse, description: constant.http[409] }, + 409: { ...genericErrorResponse, description: constantHttpStatusCodes[409] }, // bodyLimit middleware - 413: { ...genericErrorResponse, description: constant.http[413] } + 413: { ...genericErrorResponse, description: constantHttpStatusCodes[413] } } }), validator("param", schemaParam, validatorHandler), @@ -87,13 +94,13 @@ Note: To remove (nullify) a value, send the header with an empty value`, const document = mutable.database.document.get("name", actualName); if (!document?.id) { - return error.throw(ErrorCode.documentNotFound); + return errorThrow(errorCodeDocumentNotFound); } const userId = ctx.get("userId"); const owner = isOwner(userId, document.user_id); if (!owner) { - return error.throw(ErrorCode.userInvalidToken); + return errorThrow(errorCodeUserInvalidToken); } if (newPassword !== undefined) { @@ -109,7 +116,7 @@ Note: To remove (nullify) a value, send the header with an empty value`, // keep newName last thing to alter in case of race conditions if (newName) { if (mutable.database.document.get("name", newName)?.name) { - return error.throw(ErrorCode.documentNameAlreadyExists); + return errorThrow(errorCodeDocumentNameAlreadyExists); } mutable.database.document.update("name", actualName, "name", newName); @@ -118,7 +125,7 @@ Note: To remove (nullify) a value, send the header with an empty value`, } if (ctx.get("hasBody")) { - mutable.database.document.update("name", actualName, "version", constant.env.JSPB_DOCUMENT_COMPRESSION); + mutable.database.document.update("name", actualName, "version", env.JSPB_DOCUMENT_COMPRESSION); await fsWrite(ctx, document); } diff --git a/src/endpoints/document/v1/post.ts b/src/endpoints/document/v1/post.ts index 8d059008..e4c2f044 100644 --- a/src/endpoints/document/v1/post.ts +++ b/src/endpoints/document/v1/post.ts @@ -2,13 +2,14 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { monotonicUlid } from "@std/ulid"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; import { bodyStream } from "#http/middleware/bodyStream.ts"; -import type { Env } from "#http/type.ts"; import { generateHash } from "#util/crypto.ts"; import { generateName } from "#util/document.ts"; -import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { env } from "#util/env.ts"; +import { errorCodeDocumentNameAlreadyExists, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName, @@ -61,19 +62,19 @@ export default new Hono().post( schema: schemaBodyResponse.schema } }, - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] }, + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] }, // auth middleware - 401: { ...genericErrorResponse, description: constant.http[401] }, + 401: { ...genericErrorResponse, description: constantHttpStatusCodes[401] }, // document name already exists - 409: { ...genericErrorResponse, description: constant.http[409] }, + 409: { ...genericErrorResponse, description: constantHttpStatusCodes[409] }, // bodyLimit middleware - 413: { ...genericErrorResponse, description: constant.http[413] } + 413: { ...genericErrorResponse, description: constantHttpStatusCodes[413] } } }), validator("header", schemaHeader, validatorHandler), @@ -90,7 +91,7 @@ export default new Hono().post( let setName: string; if (name) { if (mutable.database.document.get("name", name)?.name) { - return error.throw(ErrorCode.documentNameAlreadyExists); + return errorThrow(errorCodeDocumentNameAlreadyExists); } setName = name; @@ -110,7 +111,7 @@ export default new Hono().post( mutable.database.document.create({ id: setId, user_id: ctx.get("userId") ?? null, - version: constant.env.JSPB_DOCUMENT_COMPRESSION, + version: env.JSPB_DOCUMENT_COMPRESSION, name: setName, password: hashCombo }); diff --git a/src/endpoints/legacy/v2/documents/access.route.ts b/src/endpoints/legacy/v2/documents/access.route.ts index 99ebf896..17aba3c9 100644 --- a/src/endpoints/legacy/v2/documents/access.route.ts +++ b/src/endpoints/legacy/v2/documents/access.route.ts @@ -2,10 +2,16 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { toText } from "@std/streams"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; -import type { Env } from "#http/type.ts"; +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { verifyHash } from "#util/crypto.ts"; -import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { + errorCodeDocumentInvalidPassword, + errorCodeDocumentNotFound, + errorCodeDocumentPasswordNeeded, + errorThrow, + genericErrorResponse +} from "#util/error.ts"; import { fsRead } from "#util/fs.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -54,10 +60,10 @@ export default new Hono().get( schema: schemaBodyResponse.schema } }, - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] } + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] } } }), validator("param", schemaParam, validatorHandler), @@ -75,15 +81,15 @@ export default new Hono().get( const document = mutable.database.document.get("name", param.name); if (!document?.id) { - return error.throw(ErrorCode.documentNotFound); + return errorThrow(errorCodeDocumentNotFound); } if (document.password) { if (!header.password) { - return error.throw(ErrorCode.documentPasswordNeeded); + return errorThrow(errorCodeDocumentPasswordNeeded); } if (!verifyHash(header.password, document.password)) { - return error.throw(ErrorCode.documentInvalidPassword); + return errorThrow(errorCodeDocumentInvalidPassword); } } diff --git a/src/endpoints/legacy/v2/documents/accessRaw.route.ts b/src/endpoints/legacy/v2/documents/accessRaw.route.ts index c6a40ca6..a01fdd21 100644 --- a/src/endpoints/legacy/v2/documents/accessRaw.route.ts +++ b/src/endpoints/legacy/v2/documents/accessRaw.route.ts @@ -2,10 +2,16 @@ import { stream } from "@hono/hono/streaming"; import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; -import type { Env } from "#http/type.ts"; +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { verifyHash } from "#util/crypto.ts"; -import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { + errorCodeDocumentInvalidPassword, + errorCodeDocumentNotFound, + errorCodeDocumentPasswordNeeded, + errorThrow, + genericErrorResponse +} from "#util/error.ts"; import { fsRead } from "#util/fs.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -40,10 +46,10 @@ export default new Hono().get( schema: schemaBodyResponse.schema } }, - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] } + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] } } }), validator("param", schemaParam, validatorHandler), @@ -67,15 +73,15 @@ export default new Hono().get( const document = mutable.database.document.get("name", param.name); if (!document?.id) { - return error.throw(ErrorCode.documentNotFound); + return errorThrow(errorCodeDocumentNotFound); } if (document.password) { if (!options.password) { - return error.throw(ErrorCode.documentPasswordNeeded); + return errorThrow(errorCodeDocumentPasswordNeeded); } if (!verifyHash(options.password, document.password)) { - return error.throw(ErrorCode.documentInvalidPassword); + return errorThrow(errorCodeDocumentInvalidPassword); } } diff --git a/src/endpoints/legacy/v2/documents/edit.route.ts b/src/endpoints/legacy/v2/documents/edit.route.ts index ccd017fb..256567fa 100644 --- a/src/endpoints/legacy/v2/documents/edit.route.ts +++ b/src/endpoints/legacy/v2/documents/edit.route.ts @@ -1,10 +1,11 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { bodyStream } from "#http/middleware/bodyStream.ts"; -import type { Env } from "#http/type.ts"; -import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { env } from "#util/env.ts"; +import { errorCodeDocumentNotFound, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -49,10 +50,10 @@ export default new Hono().patch( schema: schemaBodyResponse.schema } }, - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] } + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] } } }), validator("param", schemaParam, validatorHandler), @@ -63,10 +64,10 @@ export default new Hono().patch( const document = mutable.database.document.get("name", param.name); if (!document?.id || document.user_id) { - return error.throw(ErrorCode.documentNotFound); + return errorThrow(errorCodeDocumentNotFound); } - mutable.database.document.update("name", param.name, "version", constant.env.JSPB_DOCUMENT_COMPRESSION); + mutable.database.document.update("name", param.name, "version", env.JSPB_DOCUMENT_COMPRESSION); await fsWrite(ctx, document); return ctx.json({ diff --git a/src/endpoints/legacy/v2/documents/exists.route.ts b/src/endpoints/legacy/v2/documents/exists.route.ts index bc499335..72c10a1d 100644 --- a/src/endpoints/legacy/v2/documents/exists.route.ts +++ b/src/endpoints/legacy/v2/documents/exists.route.ts @@ -1,8 +1,8 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; -import type { Env } from "#http/type.ts"; +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { genericErrorResponse } from "#util/error.ts"; import { validatorDocumentName } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -26,10 +26,10 @@ export default new Hono().get( schema: schemaBodyResponse.schema } }, - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] } + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] } } }), validator("param", schemaParam, validatorHandler), diff --git a/src/endpoints/legacy/v2/documents/index.ts b/src/endpoints/legacy/v2/documents/index.ts index cc3db7f9..5ba784b7 100644 --- a/src/endpoints/legacy/v2/documents/index.ts +++ b/src/endpoints/legacy/v2/documents/index.ts @@ -1,5 +1,5 @@ import { Hono } from "@hono/hono/tiny"; -import type { Env } from "#http/type.ts"; +import type { Env } from "#http/handler.ts"; import access from "./access.route.ts"; import accessRaw from "./accessRaw.route.ts"; import edit from "./edit.route.ts"; @@ -7,11 +7,11 @@ import exists from "./exists.route.ts"; import publish from "./publish.route.ts"; import remove from "./remove.route.ts"; -export const v2LegacyDocumentRouter = new Hono(); +export const v2LegacyDocumentHandler = new Hono(); -v2LegacyDocumentRouter.route("/", access); -v2LegacyDocumentRouter.route("/", accessRaw); -v2LegacyDocumentRouter.route("/", edit); -v2LegacyDocumentRouter.route("/", exists); -v2LegacyDocumentRouter.route("/", publish); -v2LegacyDocumentRouter.route("/", remove); +v2LegacyDocumentHandler.route("/", access); +v2LegacyDocumentHandler.route("/", accessRaw); +v2LegacyDocumentHandler.route("/", edit); +v2LegacyDocumentHandler.route("/", exists); +v2LegacyDocumentHandler.route("/", publish); +v2LegacyDocumentHandler.route("/", remove); diff --git a/src/endpoints/legacy/v2/documents/publish.route.ts b/src/endpoints/legacy/v2/documents/publish.route.ts index 2487eb31..8c83cfa3 100644 --- a/src/endpoints/legacy/v2/documents/publish.route.ts +++ b/src/endpoints/legacy/v2/documents/publish.route.ts @@ -2,12 +2,18 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { monotonicUlid } from "@std/ulid"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; +import { + constantDocumentNameLengthMax, + constantDocumentNameLengthMin, + constantHttpStatusCodes, + mutable +} from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { bodyStream } from "#http/middleware/bodyStream.ts"; -import type { Env } from "#http/type.ts"; import { generateHash } from "#util/crypto.ts"; import { generateName } from "#util/document.ts"; -import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { env } from "#util/env.ts"; +import { errorCodeDocumentNameAlreadyExists, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsWrite } from "#util/fs.ts"; import { validatorDocumentName, validatorDocumentPassword } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -22,7 +28,7 @@ const schemaBody = await resolver( const schemaHeader = type({ "password?": validatorDocumentPassword, "key?": validatorDocumentName, - "keylength?": type.number.atLeast(constant.documentNameLengthMin).atMost(constant.documentNameLengthMax).configure({ + "keylength?": type.number.atLeast(constantDocumentNameLengthMin).atMost(constantDocumentNameLengthMax).configure({ description: "The document name length" }) }); @@ -56,10 +62,10 @@ export default new Hono().post( schema: schemaBodyResponse.schema } }, - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] } + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] } } }), validator("header", schemaHeader, validatorHandler), @@ -75,7 +81,7 @@ export default new Hono().post( let setName: string; if (name) { if (mutable.database.document.get("name", name)?.name) { - return error.throw(ErrorCode.documentNameAlreadyExists); + return errorThrow(errorCodeDocumentNameAlreadyExists); } setName = name; @@ -95,7 +101,7 @@ export default new Hono().post( mutable.database.document.create({ id: id, user_id: null, - version: constant.env.JSPB_DOCUMENT_COMPRESSION, + version: env.JSPB_DOCUMENT_COMPRESSION, name: setName, password: hashCombo }); diff --git a/src/endpoints/legacy/v2/documents/remove.route.ts b/src/endpoints/legacy/v2/documents/remove.route.ts index 944ad2ff..6beb7196 100644 --- a/src/endpoints/legacy/v2/documents/remove.route.ts +++ b/src/endpoints/legacy/v2/documents/remove.route.ts @@ -1,9 +1,9 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver, validator } from "@hono/openapi"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; -import type { Env } from "#http/type.ts"; -import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; +import { errorCodeDocumentNotFound, errorThrow, genericErrorResponse } from "#util/error.ts"; import { fsDelete } from "#util/fs.ts"; import { validatorDocumentName } from "#util/validator/document.ts"; import { validatorHandler } from "#util/validator/handler.ts"; @@ -34,10 +34,10 @@ export default new Hono().delete( schema: schemaBodyResponse.schema } }, - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] } + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] } } }), validator("param", schemaParam, validatorHandler), @@ -47,7 +47,7 @@ export default new Hono().delete( const document = mutable.database.document.get("name", param.name); if (!document?.id || document.user_id) { - return error.throw(ErrorCode.documentNotFound); + return errorThrow(errorCodeDocumentNotFound); } mutable.database.document.delete("name", param.name); diff --git a/src/endpoints/user/v1/create.ts b/src/endpoints/user/v1/create.ts index e6b33814..9b558f12 100644 --- a/src/endpoints/user/v1/create.ts +++ b/src/endpoints/user/v1/create.ts @@ -1,10 +1,11 @@ import { Hono } from "@hono/hono/tiny"; import { describeRoute, resolver } from "@hono/openapi"; import { type } from "arktype"; -import { constant, mutable } from "#/global.ts"; +import { constantHttpStatusCodes, mutable } from "#/global.ts"; +import type { Env } from "#http/handler.ts"; import { authMiddleware } from "#http/middleware/authorization.ts"; -import type { Env } from "#http/type.ts"; -import { ErrorCode, error, genericErrorResponse } from "#util/error.ts"; +import { env } from "#util/env.ts"; +import { errorCodeUserInvalidToken, errorThrow, genericErrorResponse } from "#util/error.ts"; import { validatorUserToken } from "#util/validator/user.ts"; const schemaBodyResponse = resolver( @@ -27,19 +28,19 @@ export default new Hono().post( schema: schemaBodyResponse } }, - description: constant.http[200] + description: constantHttpStatusCodes[200] }, - 400: { ...genericErrorResponse, description: constant.http[400] }, - 404: { ...genericErrorResponse, description: constant.http[404] }, + 400: { ...genericErrorResponse, description: constantHttpStatusCodes[400] }, + 404: { ...genericErrorResponse, description: constantHttpStatusCodes[404] }, // auth middleware - 401: { ...genericErrorResponse, description: constant.http[401] } + 401: { ...genericErrorResponse, description: constantHttpStatusCodes[401] } } }), authMiddleware, (ctx) => { - if (!constant.env.JSPB_USER_REGISTER && ctx.get("userId") !== mutable.database.user.getRoot()?.id) { - return error.throw(ErrorCode.userInvalidToken); + if (!env.JSPB_USER_REGISTER && ctx.get("userId") !== mutable.database.user.getRoot()?.id) { + return errorThrow(errorCodeUserInvalidToken); } return ctx.json({ diff --git a/src/endpoints/user/v1/index.ts b/src/endpoints/user/v1/index.ts index 3aa34270..a977e204 100644 --- a/src/endpoints/user/v1/index.ts +++ b/src/endpoints/user/v1/index.ts @@ -1,7 +1,7 @@ import { Hono } from "@hono/hono/tiny"; -import type { Env } from "#http/type.ts"; +import type { Env } from "#http/handler.ts"; import create from "./create.ts"; -export const v1UserRouter = new Hono(); +export const v1UserHandler = new Hono(); -v1UserRouter.route("/", create); +v1UserHandler.route("/", create); diff --git a/src/global.ts b/src/global.ts index 28400ff0..9f0e1e49 100644 --- a/src/global.ts +++ b/src/global.ts @@ -2,92 +2,30 @@ import { STATUS_CODES } from "node:http"; import type { StatementSync } from "node:sqlite"; import type { StatusCode } from "@hono/hono/utils/http-status"; import { LruCache } from "@std/cache"; -import env from "arkenv"; -import { type } from "arktype"; import { customAlphabet } from "nanoid"; -import type { Database } from "#db/database"; -import { humanizeSize, humanizeTime } from "#util/humanize.ts"; - -export const DocumentVersion = { - // deflate - V1: 1, - // no compression - V2: 2 -} as const; -export type DocumentVersionType = (typeof DocumentVersion)[keyof typeof DocumentVersion]; +import type { Database } from "#db/index.ts"; export const mutable = { database: undefined as unknown as Database, - http: undefined as Deno.HttpServer | undefined, - shutdown: false + http: undefined as Deno.HttpServer | undefined }; -export const constant = { - databaseMaxElements: 10_000, - documentNameLengthDefault: 8, - documentNameLengthMax: 32, - documentNameLengthMin: 2, - documentPasswordLengthMax: 128, - documentPasswordLengthMin: 2, - userTokenLength: 59, - env: env( - { - JSPB_LOG_VERBOSITY: type.keywords.number.integer.atLeast(0).atMost(4).default(3), - JSPB_LOG_TIME: type.boolean.default(true), - JSPB_HOSTNAME: type.keywords.string.ip.root - .pipe((hostname) => { - return { - isIPv6: hostname.includes(":"), - root: hostname - }; - }) - .default("::"), - JSPB_PORT: type.keywords.number.integer.atLeast(0).atMost(65_535).default(4000), - - // debug - JSPB_DEBUG_DATABASE_EPHEMERAL: type.boolean.default(false), - - // document - JSPB_DOCUMENT_SIZE: type.string.pipe(humanizeSize).default("1mb"), - JSPB_DOCUMENT_COMPRESSION: type.boolean - .pipe((boolean): DocumentVersionType => (boolean ? DocumentVersion.V1 : DocumentVersion.V2)) - .default(true), - JSPB_DOCUMENT_AGE: type.string.pipe(humanizeTime).default("0"), - JSPB_DOCUMENT_ANONYMOUS_AGE: type.string.pipe(humanizeTime).default("7d"), - - // user - JSPB_USER_REGISTER: type.boolean.default(true), - JSPB_USER_ROOT_RECOVERY: type.boolean.default(false), - - // task - JSPB_TASK_SWEEPER: type( - /^(?:\*|[0-5]?\d(?:-[0-5]?\d)?)(?:\/[1-9]\d*)?(?:,(?:\*|[0-5]?\d(?:-[0-5]?\d)?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[01]?\d|2[0-3])(?:-(?:[01]?\d|2[0-3]))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[01]?\d|2[0-3])(?:-(?:[01]?\d|2[0-3]))?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[1-9]|[12]\d|3[01])(?:-(?:[1-9]|[12]\d|3[01]))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[1-9]|[12]\d|3[01])(?:-(?:[1-9]|[12]\d|3[01]))?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)(?:-(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)(?:-(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec))?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[0-7]|sun|mon|tue|wed|thu|fri|sat)(?:-(?:[0-7]|sun|mon|tue|wed|thu|fri|sat))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[0-7]|sun|mon|tue|wed|thu|fri|sat)(?:-(?:[0-7]|sun|mon|tue|wed|thu|fri|sat))?)(?:\/[1-9]\d*)?)*$/i - ) - .describe("a valid unix based cron: https://man7.org/linux/man-pages/man5/crontab.5.html") - .default("0 1 * * *") - }, - { - env: Deno.env.toObject() - } - ), - nanoid: customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"), - path: { - struct: { - storage: "./storage/", - storageData: "./storage/data/" - }, - databaseFile: "./storage/database.db" - }, - store: { - statements: new LruCache(200), - dispose: new Map Promise]>() - }, - temporal: { - UTC: () => Temporal.Now.zonedDateTimeISO("Etc/UTC"), - toUTC: (temporal: Temporal.Instant) => temporal.toZonedDateTimeISO("Etc/UTC"), - instant: Temporal.Now.instant - }, - http: STATUS_CODES as Record, - textEncoder: new TextEncoder(), - textDecoder: new TextDecoder() -} as const; +export const constantDatabaseMaxElements = 10_000; +export const constantDocumentNameLengthDefault = 8; +export const constantDocumentNameLengthMax = 32; +export const constantDocumentNameLengthMin = 2; +export const constantDocumentPasswordLengthMax = 128; +export const constantDocumentPasswordLengthMin = 2; +export const constantUserTokenLength = 59; +export const constantNanoid = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_"); +export const constantPathStructStorage = "./storage/"; +export const constantPathStructStorageData = "./storage/data/"; +export const constantPathDatabaseFile = "./storage/database.db"; +export const constantStoreStatements = new LruCache(200); +export const constantStoreDispose = new Map Promise]>(); +export const constantTemporalUTC = () => Temporal.Now.zonedDateTimeISO("Etc/UTC"); +export const constantTemporalToUTC = (temporal: Temporal.Instant) => temporal.toZonedDateTimeISO("Etc/UTC"); +export const constantTemporalInstant = Temporal.Now.instant; +export const constantHttpStatusCodes = STATUS_CODES as Record; +export const constantTextEncoder = new TextEncoder(); +export const constantTextDecoder = new TextDecoder(); diff --git a/src/http/router.ts b/src/http/handler.ts similarity index 64% rename from src/http/router.ts rename to src/http/handler.ts index 51f26416..4fea4eeb 100644 --- a/src/http/router.ts +++ b/src/http/handler.ts @@ -2,24 +2,30 @@ import { cors } from "@hono/hono/cors"; import { HTTPException } from "@hono/hono/http-exception"; import { Hono } from "@hono/hono/tiny"; import { openAPIRouteHandler } from "@hono/openapi"; -import { constant } from "#/global.ts"; -import { v1DocumentRouter } from "#endpoint/document/v1/index.ts"; -import { v2LegacyDocumentRouter } from "#endpoint/legacy/v2/documents/index.ts"; -import { v1UserRouter } from "#endpoint/user/v1/index.ts"; +import { v1DocumentHandler } from "#endpoint/document/v1/index.ts"; +import { v2LegacyDocumentHandler } from "#endpoint/legacy/v2/documents/index.ts"; +import { v1UserHandler } from "#endpoint/user/v1/index.ts"; import { Logger } from "#util/console.ts"; -import { ErrorCode, error } from "#util/error.ts"; -import type { Env } from "./type.ts"; +import { env } from "../utils/env.ts"; +import { errorCodeCrash, errorCodeDocumentCorrupted, errorGet } from "../utils/error.ts"; const log: Logger = new Logger("http"); -export const router = (): Hono => { - const router = new Hono().basePath("/api"); +export type Env = { + Variables: { + userId: string | undefined; + hasBody: boolean | undefined; + }; +}; + +export const handler = (): Hono => { + const handler = new Hono().basePath("/api"); - router.notFound((ctx) => { + handler.notFound((ctx) => { return ctx.body(null, 404); }); - router.onError((instance, ctx) => { + handler.onError((instance, ctx) => { if (instance instanceof HTTPException) { return instance.getResponse(); } @@ -35,16 +41,16 @@ export const router = (): Hono => { ) { log.debug(instance); - return ctx.json(error.get(ErrorCode.documentCorrupted)); + return ctx.json(errorGet(errorCodeDocumentCorrupted)); } log.error(instance); - return ctx.json(error.get(ErrorCode.crash)); + return ctx.json(errorGet(errorCodeCrash)); }); - router.use("*", cors()); - router.use(async (ctx, next) => { + handler.use("*", cors()); + handler.use(async (ctx, next) => { await next(); // disable compression @@ -52,9 +58,9 @@ export const router = (): Hono => { ctx.res.headers.append("Cache-Control", "no-transform"); }); - router.get( + handler.get( "/oas.json", - openAPIRouteHandler(router, { + openAPIRouteHandler(handler, { documentation: { openapi: "3.1.0", info: { @@ -72,10 +78,10 @@ export const router = (): Hono => { Each instance can impose restrictions to the API usage. These restrictions may include, but not limited to: (the following values might change without notice) -- Instance registration policy: ${constant.env.JSPB_USER_REGISTER ? "OPEN" : "CLOSED"} -- Document size limit: ${constant.env.JSPB_DOCUMENT_SIZE === 0 ? "unlimited" : (constant.env.JSPB_DOCUMENT_SIZE ?? "unknown")} -- Document lifetime: ${constant.env.JSPB_DOCUMENT_AGE.total("minutes") === 0 ? "unlimited" : (constant.env.JSPB_DOCUMENT_AGE.total("minutes") ?? "unknown")} -- Document anonymous lifetime: ${constant.env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("minutes") === 0 ? "unlimited" : (constant.env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("minutes") ?? "unknown")} +- Instance registration policy: ${env.JSPB_USER_REGISTER ? "OPEN" : "CLOSED"} +- Document size limit: ${env.JSPB_DOCUMENT_SIZE === 0 ? "unlimited" : (env.JSPB_DOCUMENT_SIZE ?? "unknown")} +- Document lifetime: ${env.JSPB_DOCUMENT_AGE.total("minutes") === 0 ? "unlimited" : (env.JSPB_DOCUMENT_AGE.total("minutes") ?? "unknown")} +- Document anonymous lifetime: ${env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("minutes") === 0 ? "unlimited" : (env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("minutes") ?? "unknown")} `, license: { name: "EUPL-1.2", @@ -111,15 +117,15 @@ Each instance can impose restrictions to the API usage. These restrictions may i ); // deprecated - router.get("/documents/*", (ctx) => { + handler.get("/documents/*", (ctx) => { return ctx.redirect(ctx.req.path.replace(/\/documents\//g, "/v2/documents/"), 307); }); - router.route("/document/v1", v1DocumentRouter); - router.route("/user/v1", v1UserRouter); + handler.route("/document/v1", v1DocumentHandler); + handler.route("/user/v1", v1UserHandler); // deprecated - router.route("/v2/documents", v2LegacyDocumentRouter); + handler.route("/v2/documents", v2LegacyDocumentHandler); - return router; + return handler; }; diff --git a/src/http/server.ts b/src/http/index.ts similarity index 61% rename from src/http/server.ts rename to src/http/index.ts index e598ca61..b751e230 100644 --- a/src/http/server.ts +++ b/src/http/index.ts @@ -1,13 +1,13 @@ -import { constant } from "#/global.ts"; import { Logger } from "#util/console.ts"; -import { ErrorCode, error } from "#util/error.ts"; +import { env } from "../utils/env.ts"; +import { errorCodeUnknown, errorGet } from "../utils/error.ts"; const log: Logger = new Logger("http"); const dummyHandler = (): Response => { return Response.json( { - ...error.get(ErrorCode.unknown) + ...errorGet(errorCodeUnknown) }, { status: 503, @@ -24,20 +24,20 @@ type Options = { handler?: Deno.ServeHandler; }; -export const server = (options: Options = {}): Deno.HttpServer => { +export const http = (options: Options = {}): Deno.HttpServer => { const usingHandler: boolean = typeof options.handler !== "undefined"; options.handler ??= dummyHandler; return Deno.serve({ transport: "tcp", - hostname: constant.env.JSPB_HOSTNAME.root, - port: constant.env.JSPB_PORT, + hostname: env.JSPB_HOSTNAME.root, + port: env.JSPB_PORT, handler: options.handler, onListen: () => { if (usingHandler) { log.info( - `Listening on ${constant.env.JSPB_HOSTNAME.isIPv6 ? `[${constant.env.JSPB_HOSTNAME.root}]` : constant.env.JSPB_HOSTNAME.root}:${constant.env.JSPB_PORT}` + `Listening on ${env.JSPB_HOSTNAME.isIPv6 ? `[${env.JSPB_HOSTNAME.root}]` : env.JSPB_HOSTNAME.root}:${env.JSPB_PORT}` ); } } diff --git a/src/http/middleware/authorization.ts b/src/http/middleware/authorization.ts index cc4fb998..74d3a1c0 100644 --- a/src/http/middleware/authorization.ts +++ b/src/http/middleware/authorization.ts @@ -2,9 +2,9 @@ import { createMiddleware } from "@hono/hono/factory"; import { type } from "arktype"; import { mutable } from "#/global.ts"; import { verifyHash } from "#util/crypto.ts"; -import { ErrorCode, error } from "#util/error.ts"; +import { errorCodeUserInvalidToken, errorCodeValidation, errorThrow } from "#util/error.ts"; import { validatorUserHeader } from "#util/validator/user.ts"; -import type { Env } from "../type.ts"; +import type { Env } from "../handler.ts"; export const authMiddleware = createMiddleware(async (ctx, next) => { const authorization = ctx.req.header("authorization"); @@ -14,7 +14,7 @@ export const authMiddleware = createMiddleware(async (ctx, next) => { const token = validatorUserHeader(authorization); if (token instanceof type.errors) { - return error.throw(ErrorCode.validation, token.summary); + return errorThrow(errorCodeValidation, token.summary); } if (!token.includes(".")) { @@ -23,7 +23,7 @@ export const authMiddleware = createMiddleware(async (ctx, next) => { // @ts-expect-error unindexed select const id = mutable.database.user.get("token", token)?.id; if (!id) { - return error.throw(ErrorCode.userInvalidToken); + return errorThrow(errorCodeUserInvalidToken); } ctx.set("userId", id); @@ -31,18 +31,18 @@ export const authMiddleware = createMiddleware(async (ctx, next) => { return next(); } - return error.throw(ErrorCode.userInvalidToken); + return errorThrow(errorCodeUserInvalidToken); } const [id] = token.split("."); if (!id) { - return error.throw(ErrorCode.userInvalidToken); + return errorThrow(errorCodeUserInvalidToken); } // trying to minimize timing attacks by always calling verifyHash const combo = mutable.database.user.get("id", id)?.token ?? "0 0"; if (!verifyHash(token, combo)) { - return error.throw(ErrorCode.userInvalidToken); + return errorThrow(errorCodeUserInvalidToken); } ctx.set("userId", id); diff --git a/src/http/middleware/bodyStream.ts b/src/http/middleware/bodyStream.ts index b150f7d5..6768b36a 100644 --- a/src/http/middleware/bodyStream.ts +++ b/src/http/middleware/bodyStream.ts @@ -1,7 +1,7 @@ import { createMiddleware } from "@hono/hono/factory"; -import { constant } from "#/global.ts"; -import { ErrorCode, error } from "#util/error.ts"; -import type { Env } from "../type.ts"; +import { env } from "#util/env.ts"; +import { errorCodeDocumentInvalidSize, errorThrow } from "#util/error.ts"; +import type { Env } from "../handler.ts"; export const bodyStream = createMiddleware(async (ctx, next) => { if (!ctx.req.raw.body) { @@ -13,8 +13,8 @@ export const bodyStream = createMiddleware(async (ctx, next) => { const contentLengthHeader = ctx.req.raw.headers.get("content-length"); if (contentLengthHeader !== null && !ctx.req.raw.headers.has("transfer-encoding")) { const size = Number.parseInt(contentLengthHeader, 10); - if (size > constant.env.JSPB_DOCUMENT_SIZE) { - return error.throw(ErrorCode.documentInvalidSize); + if (size > env.JSPB_DOCUMENT_SIZE) { + return errorThrow(errorCodeDocumentInvalidSize); } ctx.set("hasBody", size > 0); @@ -44,7 +44,7 @@ export const bodyStream = createMiddleware(async (ctx, next) => { transform: (chunk, controller) => { size += chunk.length; - if (size > constant.env.JSPB_DOCUMENT_SIZE) { + if (size > env.JSPB_DOCUMENT_SIZE) { controller.error(new Deno.errors.BrokenPipe()); return; } diff --git a/src/http/type.ts b/src/http/type.ts deleted file mode 100644 index 6cee3e56..00000000 --- a/src/http/type.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type Env = { - Variables: { - userId: string | undefined; - hasBody: boolean | undefined; - }; -}; diff --git a/src/init.ts b/src/init.ts index b87d5222..b15f293b 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,31 +1,32 @@ import { abortable } from "@std/async"; import { ensureDir } from "@std/fs"; -import { Database } from "#db/database.ts"; -import { router } from "#http/router.ts"; -import { server } from "#http/server.ts"; +import { Database } from "#db/index.ts"; import { sweeper } from "#task/sweeper.ts"; import { Logger } from "#util/console.ts"; -import { constant, mutable } from "./global.ts"; +import { constantPathStructStorage, constantPathStructStorageData, constantStoreDispose, mutable } from "./global.ts"; +import { handler } from "./http/handler.ts"; +import { http } from "./http/index.ts"; import { taskRegister } from "./task.ts"; +import { env } from "./utils/env.ts"; const log: Logger = new Logger(); -const initDirStruct = async (): Promise => { - const paths = Object.values(constant.path.struct); +let shutdown = false; - await Promise.all(paths.map((path) => ensureDir(path))); +const initDirStruct = async (): Promise => { + await Promise.all([await ensureDir(constantPathStructStorage), await ensureDir(constantPathStructStorageData)]); }; const initHTTPServer = async (handler?: Deno.ServeHandler): Promise => { const id = "__httpServer"; - await constant.store.dispose.get(id)?.[1](); + await constantStoreDispose.get(id)?.[1](); - mutable.http = server({ + mutable.http = http({ handler: handler }); - constant.store.dispose.set(id, [ + constantStoreDispose.set(id, [ 10, async () => { mutable.http?.unref(); @@ -39,17 +40,17 @@ const initHTTPServer = async (handler?: Deno.ServeHandler): Promise => { const id = "__databaseServer"; - await constant.store.dispose.get(id)?.[1](); + await constantStoreDispose.get(id)?.[1](); mutable.database = new Database(); - constant.store.dispose.set(id, [0, async () => mutable.database[Symbol.dispose]()]); + constantStoreDispose.set(id, [0, async () => mutable.database[Symbol.dispose]()]); await mutable.database.migration(); }; const initTask = async (): Promise => { - taskRegister(constant.env.JSPB_TASK_SWEEPER, sweeper, { + taskRegister(env.JSPB_TASK_SWEEPER, sweeper, { name: "sweeper" }); }; @@ -57,12 +58,12 @@ const initTask = async (): Promise => { export const init = async (): Promise => { for (const signal of ["SIGINT", "SIGTERM", "SIGHUP", "SIGUSR1", "SIGUSR2"] satisfies Deno.Signal[]) { Deno.addSignalListener(signal, async () => { - if (mutable.shutdown) return; - mutable.shutdown = true; + if (shutdown) return; + shutdown = true; log.debug(`Received ${signal}.`); - const storeDispose = constant.store.dispose + const storeDispose = constantStoreDispose .entries() .toArray() .sort(([, [pa]], [, [pb]]) => pb - pa); @@ -92,7 +93,7 @@ export const init = async (): Promise => { try { await Promise.all([initDirStruct(), initHTTPServer()]); await Promise.all([initDatabase()]); - await Promise.all([initTask(), initHTTPServer(router().fetch)]); + await Promise.all([initTask(), initHTTPServer(handler().fetch)]); } catch (error) { log.error(error); diff --git a/src/task.ts b/src/task.ts index 1d26f702..a627dd5c 100644 --- a/src/task.ts +++ b/src/task.ts @@ -1,5 +1,5 @@ import { Logger } from "#util/console.ts"; -import { constant } from "./global.ts"; +import { constantStoreDispose } from "./global.ts"; const log: Logger = new Logger("task"); @@ -28,7 +28,7 @@ export const taskRegister = ( const id = `__task-${options.name}`; - constant.store.dispose.get(id)?.[1](); + constantStoreDispose.get(id)?.[1](); try { Deno.cron(options.name, expression, { signal: abort.signal }, () => trigger(callback, options)); @@ -36,7 +36,7 @@ export const taskRegister = ( log.error(`Failed to register "${options.name}"..:`, error); } - constant.store.dispose.set(id, [100, async () => abort.abort()]); + constantStoreDispose.set(id, [100, async () => abort.abort()]); log.debug(`Registered "${options.name}".`); }; diff --git a/src/tasks/sweeper.ts b/src/tasks/sweeper.ts index c16bc442..51cd030d 100644 --- a/src/tasks/sweeper.ts +++ b/src/tasks/sweeper.ts @@ -1,8 +1,9 @@ import { mapNotNullish } from "@std/collections"; import { decodeTime } from "@std/ulid"; -import { constant, mutable } from "#/global.ts"; -import { Database } from "#db/database.ts"; +import { constantTemporalUTC, mutable } from "#/global.ts"; +import { Database } from "#db/index.ts"; import { Logger } from "#util/console.ts"; +import { env } from "../utils/env.ts"; import { fsDelete, fsList } from "../utils/fs.ts"; const log: Logger = new Logger("task::sweeper"); @@ -12,7 +13,7 @@ export const sweeper = async (): Promise => { sweeperDatabaseDocument(); // sweeper will remove everything in storage on ephemeral - if (!constant.env.JSPB_DEBUG_DATABASE_EPHEMERAL) { + if (!env.JSPB_DEBUG_DATABASE_EPHEMERAL) { await sweeperDangling(); } }; @@ -20,7 +21,7 @@ export const sweeper = async (): Promise => { const sweeperDatabaseUser = (): void => { using database = new Database(); - const temporalFuture = constant.temporal.UTC().add({ days: 3 }); + const temporalFuture = constantTemporalUTC().add({ days: 3 }); const users = mapNotNullish(database.user.getAllWithoutDocuments(), ({ id }) => { if (!id) return; @@ -42,14 +43,14 @@ const sweeperDatabaseUser = (): void => { const sweeperDatabaseDocument = (): void => { using database = new Database(); - const temporalNow = constant.temporal.UTC(); + const temporalNow = constantTemporalUTC(); const documents = mapNotNullish(database.document.getAll(["id", "user_id"]), ({ id, user_id }) => { if (!id) return; const ageType = user_id - ? constant.env.JSPB_DOCUMENT_AGE.total("milliseconds") - : constant.env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("milliseconds"); + ? env.JSPB_DOCUMENT_AGE.total("milliseconds") + : env.JSPB_DOCUMENT_ANONYMOUS_AGE.total("milliseconds"); if (ageType > 0 && temporalNow.epochMilliseconds - decodeTime(id) > ageType) { return id; diff --git a/src/utils/console.ts b/src/utils/console.ts index c0eb277f..a3d727fb 100644 --- a/src/utils/console.ts +++ b/src/utils/console.ts @@ -1,6 +1,6 @@ import { mapNotNullish } from "@std/collections"; import { blue, gray, red, yellow } from "@std/fmt/colors"; -import { constant } from "#/global.ts"; +import { env } from "./env.ts"; export class Logger { public static readonly level = { @@ -36,11 +36,11 @@ export class Logger { private flush(level: Exclude, message: unknown[]): void { const [levelNumber, color] = Logger.level[level]; - if (levelNumber > constant.env.JSPB_LOG_VERBOSITY) return; + if (levelNumber > env.JSPB_LOG_VERBOSITY) return; const prefix: string[] = []; - if (constant.env.JSPB_LOG_TIME) { + if (env.JSPB_LOG_TIME) { const temporalLocal = Temporal.Now.zonedDateTimeISO(); const temporalYear = temporalLocal.year; const temporalMonth = temporalLocal.month.toString().padStart(2, "0"); diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts index 6c531785..50888a19 100644 --- a/src/utils/crypto.ts +++ b/src/utils/crypto.ts @@ -1,6 +1,6 @@ import { decodeAscii85, encodeAscii85 } from "@std/encoding"; import { createBLAKE3 } from "hash-wasm"; -import { constant } from "#/global.ts"; +import { constantTextEncoder } from "../global.ts"; const hasher = await createBLAKE3(); @@ -13,7 +13,7 @@ export const generateHash = (input: string, salt?: Uint8Array) => { hasher.init(); hasher.update(defaultSalt); - hasher.update(constant.textEncoder.encode(input)); + hasher.update(constantTextEncoder.encode(input)); const encodedHash = encodeAscii85(hasher.digest("binary"), { standard: "Z85" }); diff --git a/src/utils/document.ts b/src/utils/document.ts index ea7d03d5..0cad01e7 100644 --- a/src/utils/document.ts +++ b/src/utils/document.ts @@ -1,9 +1,16 @@ -import { constant, mutable } from "#/global.ts"; +import { constantDocumentNameLengthDefault, constantNanoid, mutable } from "#/global.ts"; -export const generateName = (length = 8): string => { +// deflate +export const documentVersionV1 = 1; +// no compression +export const documentVersionV2 = 2; + +export type DocumentVersionType = typeof documentVersionV1 | typeof documentVersionV2; + +export const generateName = (length = constantDocumentNameLengthDefault): string => { let name: string; do { - name = constant.nanoid(length); + name = constantNanoid(length); } while (mutable.database.document.get("name", name)?.name); return name; diff --git a/src/utils/env.ts b/src/utils/env.ts new file mode 100644 index 00000000..8dcf8850 --- /dev/null +++ b/src/utils/env.ts @@ -0,0 +1,45 @@ +import arkenv from "arkenv"; +import { type } from "arktype"; +import { humanizeSize, humanizeTime } from "#util/humanize.ts"; +import { type DocumentVersionType, documentVersionV1, documentVersionV2 } from "./document.ts"; + +export const env = arkenv( + { + JSPB_LOG_VERBOSITY: type.keywords.number.integer.atLeast(0).atMost(4).default(3), + JSPB_LOG_TIME: type.boolean.default(true), + JSPB_HOSTNAME: type.keywords.string.ip.root + .pipe((hostname) => { + return { + isIPv6: hostname.includes(":"), + root: hostname + }; + }) + .default("::"), + JSPB_PORT: type.keywords.number.integer.atLeast(0).atMost(65_535).default(4000), + + // debug + JSPB_DEBUG_DATABASE_EPHEMERAL: type.boolean.default(false), + + // document + JSPB_DOCUMENT_SIZE: type.string.pipe(humanizeSize).default("1mb"), + JSPB_DOCUMENT_COMPRESSION: type.boolean + .pipe((boolean): DocumentVersionType => (boolean ? documentVersionV1 : documentVersionV2)) + .default(true), + JSPB_DOCUMENT_AGE: type.string.pipe(humanizeTime).default("0"), + JSPB_DOCUMENT_ANONYMOUS_AGE: type.string.pipe(humanizeTime).default("7d"), + + // user + JSPB_USER_REGISTER: type.boolean.default(true), + JSPB_USER_ROOT_RECOVERY: type.boolean.default(false), + + // task + JSPB_TASK_SWEEPER: type( + /^(?:\*|[0-5]?\d(?:-[0-5]?\d)?)(?:\/[1-9]\d*)?(?:,(?:\*|[0-5]?\d(?:-[0-5]?\d)?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[01]?\d|2[0-3])(?:-(?:[01]?\d|2[0-3]))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[01]?\d|2[0-3])(?:-(?:[01]?\d|2[0-3]))?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[1-9]|[12]\d|3[01])(?:-(?:[1-9]|[12]\d|3[01]))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[1-9]|[12]\d|3[01])(?:-(?:[1-9]|[12]\d|3[01]))?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)(?:-(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)(?:-(?:[1-9]|1[0-2]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec))?)(?:\/[1-9]\d*)?)*\s+(?:\*|(?:[0-7]|sun|mon|tue|wed|thu|fri|sat)(?:-(?:[0-7]|sun|mon|tue|wed|thu|fri|sat))?)(?:\/[1-9]\d*)?(?:,(?:\*|(?:[0-7]|sun|mon|tue|wed|thu|fri|sat)(?:-(?:[0-7]|sun|mon|tue|wed|thu|fri|sat))?)(?:\/[1-9]\d*)?)*$/i + ) + .describe("a valid unix based cron: https://man7.org/linux/man-pages/man5/crontab.5.html") + .default("0 1 * * *") + }, + { + env: Deno.env.toObject() + } +); diff --git a/src/utils/error.ts b/src/utils/error.ts index 8ea9795b..b32ad277 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -3,32 +3,53 @@ import type { ContentfulStatusCode } from "@hono/hono/utils/http-status"; import { resolver } from "@hono/openapi"; import { type } from "arktype"; -export const ErrorCode = { - crash: 1000, - unknown: 1001, - validation: 1002, - // parse: 1003, // moved to 1002 - notFound: 1004, - dummy: 1005, +// allow const enum in the future +// https://github.com/rolldown/rolldown/issues/7676 - // document - documentNotFound: 1200, - documentNameAlreadyExists: 1201, - documentPasswordNeeded: 1202, - documentInvalidSize: 1203, - // documentInvalidNameLength: 1204, // moved to 1002 - documentInvalidPassword: 1205, - // documentInvalidPasswordLength: 1206, // moved to 1002 - // documentInvalidSecret: 1207, // deprecated - // documentInvalidSecretLength: 1208, // deprecated - // documentInvalidName: 1209, // moved to 1002 - documentCorrupted: 1210, +export const errorCodeCrash = 1000; +export const errorCodeUnknown = 1001; +export const errorCodeValidation = 1002; +// export const errorCodeParse = 1003; // moved to 1002 +export const errorCodeNotFound = 1004; +export const errorCodeDummy = 1005; - // user - userInvalidToken: 1300 -} as const; +// document +export const errorCodeDocumentNotFound = 1200; +export const errorCodeDocumentNameAlreadyExists = 1201; +export const errorCodeDocumentPasswordNeeded = 1202; +export const errorCodeDocumentInvalidSize = 1203; +// export const errorCodeDocumentInvalidNameLength = 1204; // moved to 1002 +export const errorCodeDocumentInvalidPassword = 1205; +// export const errorCodeDocumentInvalidPasswordLength = 1206; // moved to 1002 +// export const errorCodeDocumentInvalidSecret = 1207; // deprecated +// export const errorCodeDocumentInvalidSecretLength = 1208; // deprecated +// export const errorCodeDocumentInvalidName = 1209; // moved to 1002 +export const errorCodeDocumentCorrupted = 1210; -export type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode]; +// user +export const errorCodeUserInvalidToken = 1300; + +export type ErrorCodeType = + | typeof errorCodeCrash + | typeof errorCodeUnknown + | typeof errorCodeValidation + // | typeof errorCodeParse + | typeof errorCodeNotFound + | typeof errorCodeDummy + // document + | typeof errorCodeDocumentNotFound + | typeof errorCodeDocumentNameAlreadyExists + | typeof errorCodeDocumentPasswordNeeded + | typeof errorCodeDocumentInvalidSize + // | typeof errorCodeDocumentInvalidNameLength + | typeof errorCodeDocumentInvalidPassword + // | typeof errorCodeDocumentInvalidPasswordLength + // | typeof errorCodeDocumentInvalidSecret + // | typeof errorCodeDocumentInvalidSecretLength + // | typeof errorCodeDocumentInvalidName + | typeof errorCodeDocumentCorrupted + // user + | typeof errorCodeUserInvalidToken; export type Schema = { httpCode: ContentfulStatusCode; @@ -36,76 +57,74 @@ export type Schema = { }; const errorDefinition: Record = { - [ErrorCode.crash]: { + [errorCodeCrash]: { httpCode: 500, message: "An unexpected server error occurred. If this persists, open an issue at: https://github.com/jspaste/backend/issues" }, - [ErrorCode.unknown]: { + [errorCodeUnknown]: { httpCode: 503, message: "Server handler has not loaded yet. Wait..." }, - [ErrorCode.validation]: { + [errorCodeValidation]: { httpCode: 400, message: "The request contains invalid or malformed data." }, - [ErrorCode.notFound]: { + [errorCodeNotFound]: { httpCode: 404, message: "The requested resource could not be found." }, - [ErrorCode.dummy]: { + [errorCodeDummy]: { httpCode: 200, message: "Placeholder response for documentation purposes." }, // document - [ErrorCode.documentNotFound]: { + [errorCodeDocumentNotFound]: { httpCode: 404, message: "No document exists with the specified name." }, - [ErrorCode.documentNameAlreadyExists]: { + [errorCodeDocumentNameAlreadyExists]: { httpCode: 409, message: "A document with this name already exists. Choose a different name." }, - [ErrorCode.documentPasswordNeeded]: { + [errorCodeDocumentPasswordNeeded]: { httpCode: 401, message: "This document is password protected. Include the password in your request." }, - [ErrorCode.documentInvalidSize]: { + [errorCodeDocumentInvalidSize]: { httpCode: 413, message: "The document content exceeds the maximum allowed size." }, - [ErrorCode.documentInvalidPassword]: { + [errorCodeDocumentInvalidPassword]: { httpCode: 403, message: "The provided password is incorrect." }, - [ErrorCode.documentCorrupted]: { + [errorCodeDocumentCorrupted]: { httpCode: 500, message: "The document content is corrupted and cannot be retrieved." }, // user - [ErrorCode.userInvalidToken]: { + [errorCodeUserInvalidToken]: { httpCode: 401, message: "The provided authorization token is invalid or missing privileges." } } as const; -export const error = { - get: (code: ErrorCodeType, overrideMessage?: string) => { - const { message } = errorDefinition[code]; +export const errorGet = (code: ErrorCodeType, overrideMessage?: string) => { + const { message } = errorDefinition[code]; - return { code: code, message: overrideMessage ?? message }; - }, + return { code: code, message: overrideMessage ?? message }; +}; - throw: (code: ErrorCodeType, overrideMessage?: string): never => { - const { httpCode, message } = errorDefinition[code]; +export const errorThrow = (code: ErrorCodeType, overrideMessage?: string): never => { + const { httpCode, message } = errorDefinition[code]; - throw new HTTPException(httpCode, { - res: Response.json({ code: code, message: overrideMessage ?? message }) - }); - } -} as const; + throw new HTTPException(httpCode, { + res: Response.json({ code: code, message: overrideMessage ?? message }) + }); +}; export const genericErrorResponse = { content: { @@ -114,7 +133,7 @@ export const genericErrorResponse = { type({ code: type.number.configure({ description: "The error code", - examples: [ErrorCode.dummy] + examples: [errorCodeDummy] }), message: type.string.configure({ description: "The error description" diff --git a/src/utils/fs.ts b/src/utils/fs.ts index c599f3d5..800fdd90 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -1,32 +1,34 @@ import type { Context } from "@hono/hono"; import type { Document } from "#db/query.ts"; -import { constant, DocumentVersion } from "../global.ts"; -import type { Env } from "../http/type.ts"; -import { ErrorCode, error } from "./error.ts"; +import { constantPathStructStorageData, constantTemporalToUTC, constantTemporalUTC } from "../global.ts"; +import type { Env } from "../http/handler.ts"; +import { documentVersionV1, documentVersionV2 } from "./document.ts"; +import { env } from "./env.ts"; +import { errorCodeDocumentCorrupted, errorCodeDocumentInvalidSize, errorThrow } from "./error.ts"; export const fsWrite = async (ctx: Context, { id }: Pick): Promise => { - await using handle = await Deno.open(constant.path.struct.storageData + id, { + await using handle = await Deno.open(constantPathStructStorageData + id, { create: true, write: true, truncate: true }); let stream: ReadableStream; - switch (constant.env.JSPB_DOCUMENT_COMPRESSION) { - case DocumentVersion.V1: { + switch (env.JSPB_DOCUMENT_COMPRESSION) { + case documentVersionV1: { // ctx.req.raw.body is only null on GET/HEAD stream = (ctx.req.raw.body as NonNullable).pipeThrough(new CompressionStream("deflate")); break; } - case DocumentVersion.V2: { + case documentVersionV2: { // ctx.req.raw.body is only null on GET/HEAD stream = ctx.req.raw.body as NonNullable; break; } default: { - return error.throw(ErrorCode.documentCorrupted); + return errorThrow(errorCodeDocumentCorrupted); } } @@ -36,7 +38,7 @@ export const fsWrite = async (ctx: Context, { id }: Pick): void fsDelete({ id: id }); if (why instanceof Deno.errors.BrokenPipe) { - return error.throw(ErrorCode.documentInvalidSize); + return errorThrow(errorCodeDocumentInvalidSize); } throw why; @@ -45,7 +47,7 @@ export const fsWrite = async (ctx: Context, { id }: Pick): export const fsDelete = async ({ id }: Pick): Promise => { try { - await Deno.remove(constant.path.struct.storageData + id); + await Deno.remove(constantPathStructStorageData + id); } catch (why) { // already deleted if (why instanceof Deno.errors.NotFound) return; @@ -59,13 +61,13 @@ export const fsRead = async ( { id, version }: Pick, clientIgnoreCapabilities = false ): Promise>> => { - const handle = await Deno.open(constant.path.struct.storageData + id); + const handle = await Deno.open(constantPathStructStorageData + id); const hasClientDeflate = clientIgnoreCapabilities ? false : ctx.req.header("accept-encoding")?.includes("deflate"); let stream: ReadableStream; switch (version) { - case DocumentVersion.V1: { + case documentVersionV1: { if (hasClientDeflate) { ctx.res.headers.set("content-encoding", "deflate"); stream = handle.readable; @@ -75,13 +77,13 @@ export const fsRead = async ( break; } - case DocumentVersion.V2: { + case documentVersionV2: { stream = handle.readable; break; } default: { - return error.throw(ErrorCode.documentCorrupted); + return errorThrow(errorCodeDocumentCorrupted); } } @@ -90,15 +92,15 @@ export const fsRead = async ( // relaxed exists because races between fs/db may occur export function* fsList(relaxed?: boolean): Iterable { - for (const entry of Deno.readDirSync(constant.path.struct.storageData)) { + for (const entry of Deno.readDirSync(constantPathStructStorageData)) { if (entry.isFile) { if (relaxed) { - const info = Deno.statSync(constant.path.struct.storageData + entry.name); + const info = Deno.statSync(constantPathStructStorageData + entry.name); if ( !info.mtime || - constant.temporal.UTC().epochMilliseconds - - constant.temporal.toUTC(info.mtime.toTemporalInstant()).epochMilliseconds >= + constantTemporalUTC().epochMilliseconds - + constantTemporalToUTC(info.mtime.toTemporalInstant()).epochMilliseconds >= 10_000 ) { yield entry.name; diff --git a/src/utils/validator/regex.ts b/src/utils/regex.ts similarity index 100% rename from src/utils/validator/regex.ts rename to src/utils/regex.ts diff --git a/src/utils/user.ts b/src/utils/user.ts index 7f0582c2..d0f63093 100644 --- a/src/utils/user.ts +++ b/src/utils/user.ts @@ -1,7 +1,7 @@ -import { constant } from "#/global.ts"; +import { constantNanoid } from "../global.ts"; export const generateToken = (id: string): string => { - const noise = constant.nanoid(32); + const noise = constantNanoid(32); return `${id}.${noise}`; }; diff --git a/src/utils/validator/document.ts b/src/utils/validator/document.ts index 7a3c7b4f..6ad8e090 100644 --- a/src/utils/validator/document.ts +++ b/src/utils/validator/document.ts @@ -1,11 +1,16 @@ import { type } from "arktype"; -import { constant } from "#/global.ts"; -import { regexBase64URL } from "./regex.ts"; +import { + constantDocumentNameLengthMax, + constantDocumentNameLengthMin, + constantDocumentPasswordLengthMax, + constantDocumentPasswordLengthMin +} from "#/global.ts"; +import { regexBase64URL } from "../regex.ts"; import { validatorCreationTimestamp } from "./shared.ts"; export const validatorDocumentName = type(regexBase64URL) - .atLeastLength(constant.documentNameLengthMin) - .atMostLength(constant.documentNameLengthMax) + .atLeastLength(constantDocumentNameLengthMin) + .atMostLength(constantDocumentNameLengthMax) .configure({ ref: "DocumentName", description: "The document name", @@ -29,7 +34,7 @@ export const validatorDocumentName = type(regexBase64URL) }); export const validatorDocumentNameLength = type.keywords.string.integer.parse - .to(type.number.atLeast(constant.documentNameLengthMin).atMost(constant.documentNameLengthMax)) + .to(type.number.atLeast(constantDocumentNameLengthMin).atMost(constantDocumentNameLengthMax)) .configure({ ref: "DocumentNameLength", description: "The name length for the document", @@ -52,8 +57,8 @@ export const validatorDocumentNameLength = type.keywords.string.integer.parse }); export const validatorDocumentPassword = type.string - .atLeastLength(constant.documentPasswordLengthMin) - .atMostLength(constant.documentPasswordLengthMax) + .atLeastLength(constantDocumentPasswordLengthMin) + .atMostLength(constantDocumentPasswordLengthMax) .configure({ ref: "DocumentPassword.default", description: "The password for the document (read access)", diff --git a/src/utils/validator/handler.ts b/src/utils/validator/handler.ts index 34a9a69e..434a7159 100644 --- a/src/utils/validator/handler.ts +++ b/src/utils/validator/handler.ts @@ -1,8 +1,8 @@ import type { sValidator } from "@hono/standard-validator"; -import { ErrorCode, error } from "../error.ts"; +import { errorCodeValidation, errorThrow } from "../error.ts"; export const validatorHandler: Parameters[2] = (res) => { if (res.success) return; - return error.throw(ErrorCode.validation, res.error[0]?.message); + return errorThrow(errorCodeValidation, res.error[0]?.message); }; diff --git a/src/utils/validator/user.ts b/src/utils/validator/user.ts index 4e378df2..0f3b5806 100644 --- a/src/utils/validator/user.ts +++ b/src/utils/validator/user.ts @@ -1,9 +1,9 @@ import { type } from "arktype"; -import { constant } from "#/global.ts"; -import { regexBase64URL, regexHeaderBearer } from "./regex.ts"; +import { constantUserTokenLength } from "#/global.ts"; +import { regexBase64URL, regexHeaderBearer } from "../regex.ts"; // FIXME: schema references not being generated when using toOpenAPISchema() -export const validatorUserToken = type.string.exactlyLength(constant.userTokenLength).configure({ +export const validatorUserToken = type.string.exactlyLength(constantUserTokenLength).configure({ ref: "UserToken.default", description: "A user token", examples: ["myUserTokenHere"], From 8d717cfe42041dc85f5bd6c2eec7b56bdf6fe2b1 Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Wed, 14 Jan 2026 14:02:39 +0100 Subject: [PATCH 46/47] Update dependencies (#268) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- deno.lock | 96 ++++++++++++++++++++++++++-------------------------- mise.toml | 10 ++---- package.json | 8 ++--- 3 files changed, 55 insertions(+), 59 deletions(-) diff --git a/deno.lock b/deno.lock index 0b53c4a1..8761d376 100644 --- a/deno.lock +++ b/deno.lock @@ -2,8 +2,8 @@ "version": "5", "specifiers": { "npm:@biomejs/biome@2.3.11": "2.3.11", - "npm:@jsr/hono__hono@^4.11.3": "4.11.3", - "npm:@jsr/hono__standard-validator@~0.2.1": "0.2.1", + "npm:@jsr/hono__hono@^4.11.4": "4.11.4", + "npm:@jsr/hono__standard-validator@~0.2.2": "0.2.2", "npm:@jsr/std__assert@^1.0.16": "1.0.16", "npm:@jsr/std__async@^1.0.16": "1.0.16", "npm:@jsr/std__cache@~0.2.1": "0.2.1", @@ -14,13 +14,13 @@ "npm:@jsr/std__fs@^1.0.21": "1.0.21", "npm:@jsr/std__streams@^1.0.16": "1.0.16", "npm:@jsr/std__ulid@1": "1.0.0", - "npm:@types/node@^25.0.3": "25.0.3", + "npm:@types/node@^25.0.8": "25.0.8", "npm:arkenv@~0.8.3": "0.8.3_arktype@2.1.29", "npm:arktype@^2.1.29": "2.1.29", "npm:hash-wasm@^4.12.0": "4.12.0", "npm:hono-openapi@^1.1.2": "1.1.2_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.1.29__quansync@0.2.11_@standard-community+standard-openapi@0.2.9__@standard-community+standard-json@0.3.5___@standard-schema+spec@1.1.0___@types+json-schema@7.0.15___arktype@2.1.29___quansync@0.2.11__@standard-schema+spec@1.1.0__arktype@2.1.29__openapi-types@12.1.3__@types+json-schema@7.0.15_@types+json-schema@7.0.15_openapi-types@12.1.3_arktype@2.1.29", "npm:nanoid@^5.1.6": "5.1.6", - "npm:rolldown@1.0.0-beta.59": "1.0.0-beta.59", + "npm:rolldown@1.0.0-beta.60": "1.0.0-beta.60", "npm:vite-bundle-analyzer@^1.3.2": "1.3.2" }, "npm": { @@ -106,17 +106,17 @@ "tslib" ] }, - "@jsr/hono__hono@4.11.3": { - "integrity": "sha512-1K5jN5tabn9NzylJUQBdYuz25Nv3WarXRXfkSZeiCZK05ahzGZW2aXtKx1odkE1ztTIdkVGDkfht1CQHdGh4iA==", - "tarball": "https://npm.jsr.io/~/11/@jsr/hono__hono/4.11.3.tgz" + "@jsr/hono__hono@4.11.4": { + "integrity": "sha512-GdHrXgX+q2Q9LCse3RVXo5vmg8wJDlQoavZVXW3eXN9bczlzdkWBiy712FELfRdLZ0ij16BDQM5nkcc86O/fwg==", + "tarball": "https://npm.jsr.io/~/11/@jsr/hono__hono/4.11.4.tgz" }, - "@jsr/hono__standard-validator@0.2.1": { - "integrity": "sha512-93mG2IHjrCzb8A2705N03SW3LyWaNT7EkVwcZ4TqmRDzvX6OZJSe2fCeI3+39qJKon+SkgsdoTqRt00deNg0Gw==", + "@jsr/hono__standard-validator@0.2.2": { + "integrity": "sha512-BXapiKtKoY63PuQlxs/dYmCcqWkwpOUi7yKfwZyHdBhuW9WtII/C5QDx6kimzXShG/e/Jk7U5aAtnlUeDpgUmA==", "dependencies": [ "@jsr/hono__hono", "@jsr/standard-schema__spec" ], - "tarball": "https://npm.jsr.io/~/11/@jsr/hono__standard-validator/0.2.1.tgz" + "tarball": "https://npm.jsr.io/~/11/@jsr/hono__standard-validator/0.2.2.tgz" }, "@jsr/standard-schema__spec@1.1.0": { "integrity": "sha512-mWncLgOE1ZVd/xXG+SPmmDmYPQ9Q3OcIbkCn/3oPpp4WOw3RpbHWdxk/jiG8m4Bd9utE2b1yyfynSStpuhdXew==", @@ -195,78 +195,78 @@ "@tybys/wasm-util" ] }, - "@oxc-project/types@0.107.0": { - "integrity": "sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==" + "@oxc-project/types@0.108.0": { + "integrity": "sha512-7lf13b2IA/kZO6xgnIZA88sq3vwrxWk+2vxf6cc+omwYCRTiA5e63Beqf3fz/v8jEviChWWmFYBwzfSeyrsj7Q==" }, - "@rolldown/binding-android-arm64@1.0.0-beta.59": { - "integrity": "sha512-6yLLgyswYwiCfls9+hoNFY9F8TQdwo15hpXDHzlAR0X/GojeKF+AuNcXjYNbOJ4zjl/5D6lliE8CbpB5t1OWIQ==", + "@rolldown/binding-android-arm64@1.0.0-beta.60": { + "integrity": "sha512-hOW6iQXtpG4uCW1zGK56+KhEXGttSkTp2ykncW/nkOIF/jOKTqbM944Q73HVeMXP1mPRvE2cZwNp3xeLIeyIGQ==", "os": ["android"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-arm64@1.0.0-beta.59": { - "integrity": "sha512-hqGXRc162qCCIOAcHN2Cw4eXiVTwYsMFLOhAy1IG2CxY+dwc/l4Ga+dLPkLor3Ikqy5WDn+7kxHbbh6EmshEpQ==", + "@rolldown/binding-darwin-arm64@1.0.0-beta.60": { + "integrity": "sha512-vyDA4HXY2mP8PPtl5UE17uGPxUNG4m1wkfa3kAkR8JWrFbarV97UmLq22IWrNhtBPa89xqerzLK8KoVmz5JqCQ==", "os": ["darwin"], "cpu": ["arm64"] }, - "@rolldown/binding-darwin-x64@1.0.0-beta.59": { - "integrity": "sha512-ezvvGuhteE15JmMhJW0wS7BaXmhwLy1YHeEwievYaPC1PgGD86wgBKfOpHr9tSKllAXbCe0BeeMvasscWLhKdA==", + "@rolldown/binding-darwin-x64@1.0.0-beta.60": { + "integrity": "sha512-WnxyqxAKP2BsxouwGY/RCF5UFw/LA4QOHhJ7VEl+UCelHokiwqNHRbryLAyRy3TE1FZ5eae+vAFcaetAu/kWLw==", "os": ["darwin"], "cpu": ["x64"] }, - "@rolldown/binding-freebsd-x64@1.0.0-beta.59": { - "integrity": "sha512-4fhKVJiEYVd5n6no/mrL3LZ9kByfCGwmONOrdtvx8DJGDQhehH/q3RfhG3V/4jGKhpXgbDjpIjkkFdybCTcgew==", + "@rolldown/binding-freebsd-x64@1.0.0-beta.60": { + "integrity": "sha512-JtyWJ+zXOHof5gOUYwdTWI2kL6b8q9eNwqB/oD4mfUFaC/COEB2+47JMhcq78dey9Ahmec3DZKRDZPRh9hNAMQ==", "os": ["freebsd"], "cpu": ["x64"] }, - "@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59": { - "integrity": "sha512-T3Y52sW6JAhvIqArBw+wtjNU1Ieaz4g0NBxyjSJoW971nZJBZygNlSYx78G4cwkCmo1dYTciTPDOnQygLV23pA==", + "@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.60": { + "integrity": "sha512-LrMoKqpHx+kCaNSk84iSBd4yVOymLIbxJQtvFjDN2CjQraownR+IXcwYDblFcj9ivmS54T3vCboXBbm3s1zbPQ==", "os": ["linux"], "cpu": ["arm"] }, - "@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59": { - "integrity": "sha512-NIW40jQDSQap2KDdmm9z3B/4OzWJ6trf8dwx3FD74kcQb3v34ThsBFTtzE5KjDuxnxgUlV+DkAu+XgSMKrgufw==", + "@rolldown/binding-linux-arm64-gnu@1.0.0-beta.60": { + "integrity": "sha512-sqI+Vdx1gmXJMsXN3Fsewm3wlt7RHvRs1uysSp//NLsCoh9ZFEUr4ZzGhWKOg6Rvf+njNu/vCsz96x7wssLejQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-arm64-musl@1.0.0-beta.59": { - "integrity": "sha512-CCKEk+H+8c0WGe/8n1E20n85Tq4Pv+HNAbjP1KfUXW+01aCWSMjU56ChNrM2tvHnXicfm7QRNoZyfY8cWh7jLQ==", + "@rolldown/binding-linux-arm64-musl@1.0.0-beta.60": { + "integrity": "sha512-8xlqGLDtTP8sBfYwneTDu8+PRm5reNEHAuI/+6WPy9y350ls0KTFd3EJCOWEXWGW0F35ko9Fn9azmurBTjqOrQ==", "os": ["linux"], "cpu": ["arm64"] }, - "@rolldown/binding-linux-x64-gnu@1.0.0-beta.59": { - "integrity": "sha512-VlfwJ/HCskPmQi8R0JuAFndySKVFX7yPhE658o27cjSDWWbXVtGkSbwaxstii7Q+3Rz87ZXN+HLnb1kd4R9Img==", + "@rolldown/binding-linux-x64-gnu@1.0.0-beta.60": { + "integrity": "sha512-iR4nhVouVZK1CiGGGyz+prF5Lw9Lmz30Rl36Hajex+dFVFiegka604zBwzTp5Tl0BZnr50ztnVJ30tGrBhDr8Q==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-linux-x64-musl@1.0.0-beta.59": { - "integrity": "sha512-kuO92hTRyGy0Ts3Nsqll0rfO8eFsEJe9dGQGktkQnZ2hrJrDVN0y419dMgKy/gB2S2o7F2dpWhpfQOBehZPwVA==", + "@rolldown/binding-linux-x64-musl@1.0.0-beta.60": { + "integrity": "sha512-HbfNcqNeqxFjSMf1Kpe8itr2e2lr0Bm6HltD2qXtfU91bSSikVs9EWsa1ThshQ1v2ZvxXckGjlVLtah6IoslPg==", "os": ["linux"], "cpu": ["x64"] }, - "@rolldown/binding-openharmony-arm64@1.0.0-beta.59": { - "integrity": "sha512-PXAebvNL4sYfCqi8LdY4qyFRacrRoiPZLo3NoUmiTxm7MPtYYR8CNtBGNokqDmMuZIQIecRaD/jbmFAIDz7DxQ==", + "@rolldown/binding-openharmony-arm64@1.0.0-beta.60": { + "integrity": "sha512-BiiamFcgTJ+ZFOUIMO9AHXUo9WXvHVwGfSrJ+Sv0AsTd2w3VN7dJGiH3WRcxKFetljJHWvGbM4fdpY5lf6RIvw==", "os": ["openharmony"], "cpu": ["arm64"] }, - "@rolldown/binding-wasm32-wasi@1.0.0-beta.59": { - "integrity": "sha512-yJoklQg7XIZq8nAg0bbkEXcDK6sfpjxQGxpg2Nd6ERNtvg+eOaEBRgPww0BVTrYFQzje1pB5qPwC2VnJHT3koQ==", + "@rolldown/binding-wasm32-wasi@1.0.0-beta.60": { + "integrity": "sha512-6roXGbHMdR2ucnxXuwbmQvk8tuYl3VGu0yv13KxspyKBxxBd4RS6iykzLD6mX2gMUHhfX8SVWz7n/62gfyKHow==", "dependencies": [ "@napi-rs/wasm-runtime" ], "cpu": ["wasm32"] }, - "@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59": { - "integrity": "sha512-ljZ4+McmCbIuZwEBaoGtiG8Rq2nJjaXEnLEIx+usWetXn1ECjXY0LAhkELxOV6ytv4ensEmoJJ8nXg47hRMjlw==", + "@rolldown/binding-win32-arm64-msvc@1.0.0-beta.60": { + "integrity": "sha512-JBOm8/DC/CKnHyMHoJFdvzVHxUixid4dGkiTqGflxOxO43uSJMpl77pSPXvzwZ/VXwqblU2V0/PanyCBcRLowQ==", "os": ["win32"], "cpu": ["arm64"] }, - "@rolldown/binding-win32-x64-msvc@1.0.0-beta.59": { - "integrity": "sha512-bMY4tTIwbdZljW+xe/ln1hvs0SRitahQSXfWtvgAtIzgSX9Ar7KqJzU7lRm33YTRFIHLULRi53yNjw9nJGd6uQ==", + "@rolldown/binding-win32-x64-msvc@1.0.0-beta.60": { + "integrity": "sha512-MKF0B823Efp+Ot8KsbwIuGhKH58pf+2rSM6VcqyNMlNBHheOM0Gf7JmEu+toc1jgN6fqjH7Et+8hAzsLVkIGfA==", "os": ["win32"], "cpu": ["x64"] }, - "@rolldown/pluginutils@1.0.0-beta.59": { - "integrity": "sha512-aoh6LAJRyhtazs98ydgpNOYstxUlsOV1KJXcpf/0c0vFcUA8uyd/hwKRhqE/AAPNqAho9RliGsvitCoOzREoVA==" + "@rolldown/pluginutils@1.0.0-beta.60": { + "integrity": "sha512-Jz4aqXRPVtqkH1E3jRDzLO5cgN5JwW+WG0wXGE4NiJd25nougv/AHzxmKCzmVQUYnxLmTM0M4wrZp+LlC2FKLg==" }, "@standard-community/standard-json@0.3.5_@standard-schema+spec@1.1.0_@types+json-schema@7.0.15_arktype@2.1.29_quansync@0.2.11": { "integrity": "sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==", @@ -304,8 +304,8 @@ "@types/json-schema@7.0.15": { "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, - "@types/node@25.0.3": { - "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "@types/node@25.0.8": { + "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==", "dependencies": [ "undici-types" ] @@ -352,8 +352,8 @@ "quansync@0.2.11": { "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==" }, - "rolldown@1.0.0-beta.59": { - "integrity": "sha512-Slm000Gd8/AO9z4Kxl4r8mp/iakrbAuJ1L+7ddpkNxgQ+Vf37WPvY63l3oeyZcfuPD1DRrUYBsRPIXSOhvOsmw==", + "rolldown@1.0.0-beta.60": { + "integrity": "sha512-YYgpv7MiTp9LdLj1fzGzCtij8Yi2OKEc3HQtfbIxW4yuSgpQz9518I69U72T5ErPA/ATOXqlcisiLrWy+5V9YA==", "dependencies": [ "@oxc-project/types", "@rolldown/pluginutils" @@ -390,8 +390,8 @@ "packageJson": { "dependencies": [ "npm:@biomejs/biome@2.3.11", - "npm:@jsr/hono__hono@^4.11.3", - "npm:@jsr/hono__standard-validator@~0.2.1", + "npm:@jsr/hono__hono@^4.11.4", + "npm:@jsr/hono__standard-validator@~0.2.2", "npm:@jsr/std__assert@^1.0.16", "npm:@jsr/std__async@^1.0.16", "npm:@jsr/std__cache@~0.2.1", @@ -402,13 +402,13 @@ "npm:@jsr/std__fs@^1.0.21", "npm:@jsr/std__streams@^1.0.16", "npm:@jsr/std__ulid@1", - "npm:@types/node@^25.0.3", + "npm:@types/node@^25.0.8", "npm:arkenv@~0.8.3", "npm:arktype@^2.1.29", "npm:hash-wasm@^4.12.0", "npm:hono-openapi@^1.1.2", "npm:nanoid@^5.1.6", - "npm:rolldown@1.0.0-beta.59", + "npm:rolldown@1.0.0-beta.60", "npm:vite-bundle-analyzer@^1.3.2" ] } diff --git a/mise.toml b/mise.toml index 54abd52a..1456c634 100644 --- a/mise.toml +++ b/mise.toml @@ -9,13 +9,9 @@ run = [ [tasks."install:deno"] description = "Install Deno dependencies" -run = ''' -if [ "${GITHUB_ACTIONS}" = "true" ]; then - mise exec -- deno install --frozen; -else - mise exec -- deno install; -fi -''' +run = [ + "mise exec -- deno install" +] [tasks."clean"] description = "Clean project environment" diff --git a/package.json b/package.json index 262b02e1..f92477c0 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "license": "EUPL-1.2", "type": "module", "dependencies": { - "@hono/hono": "npm:@jsr/hono__hono@^4.11.3", + "@hono/hono": "npm:@jsr/hono__hono@^4.11.4", "@hono/openapi": "npm:hono-openapi@^1.1.2", - "@hono/standard-validator": "npm:@jsr/hono__standard-validator@~0.2.1", + "@hono/standard-validator": "npm:@jsr/hono__standard-validator@~0.2.2", "@std/assert": "npm:@jsr/std__assert@^1.0.16", "@std/async": "npm:@jsr/std__async@^1.0.16", "@std/cache": "npm:@jsr/std__cache@~0.2.1", @@ -16,13 +16,13 @@ "@std/fs": "npm:@jsr/std__fs@^1.0.21", "@std/streams": "npm:@jsr/std__streams@^1.0.16", "@std/ulid": "npm:@jsr/std__ulid@^1.0.0", - "@types/node": "npm:@types/node@^25.0.3", + "@types/node": "npm:@types/node@^25.0.8", "arkenv": "npm:arkenv@~0.8.3", "arktype": "npm:arktype@^2.1.29", "biome": "npm:@biomejs/biome@2.3.11", "hash-wasm": "npm:hash-wasm@^4.12.0", "nanoid": "npm:nanoid@^5.1.6", - "rolldown": "npm:rolldown@1.0.0-beta.59", + "rolldown": "npm:rolldown@1.0.0-beta.60", "vite-bundle-analyzer": "npm:vite-bundle-analyzer@^1.3.2" }, "imports": { From 795a468b5c6713ff15dcab3c93ef55284dff0dbc Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Thu, 15 Jan 2026 13:04:35 +0100 Subject: [PATCH 47/47] Patch module resolution (#269) --- .dockerignore | 2 + .gitignore | 3 + .npmrc | 1 - deno.json | 72 ++++++------ deno.lock | 240 +++++++++++++++++++------------------- lib/deno-rolldown/LICENSE | 21 ++++ lib/deno-rolldown/mod.ts | 240 ++++++++++++++++++++++++++++++++++++++ package.json | 49 +------- rolldown.config.ts | 8 +- tsconfig.json | 42 +++++++ 10 files changed, 461 insertions(+), 217 deletions(-) delete mode 100644 .npmrc create mode 100644 lib/deno-rolldown/LICENSE create mode 100644 lib/deno-rolldown/mod.ts create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore index 62d0536e..6c9d86bd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ * +!/lib/** !/src/** !/.npmrc !/deno.json @@ -8,3 +9,4 @@ !/mise.toml !/package.json !/rolldown.config.ts +!/tsconfig.json \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8abd6b55..e1a4075e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ !/.github/workflows/*.yml !/.zed/ !/.zed/settings.json +!/lib/ +!/lib/** !/src/ !/src/** !/.dockerignore @@ -23,3 +25,4 @@ !/package.json !/README.md !/rolldown.config.ts +!/tsconfig.json \ No newline at end of file diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 41583e36..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -@jsr:registry=https://npm.jsr.io diff --git a/deno.json b/deno.json index 5e1ae0f3..0687cade 100644 --- a/deno.json +++ b/deno.json @@ -4,43 +4,38 @@ "lock": true, "nodeModulesDir": "manual", "unstable": ["cron", "temporal", "raw-imports"], - "compilerOptions": { - "lib": ["deno.window", "deno.unstable"], - "types": ["node"], - "module": "esnext", - "moduleResolution": "bundler", - - "checkJs": false, - "skipLibCheck": true, - - "strict": true, - "allowUnreachableCode": false, - "allowUnusedLabels": false, - "exactOptionalPropertyTypes": false, - "isolatedDeclarations": false, - "noErrorTruncation": false, - "noFallthroughCasesInSwitch": true, - "noImplicitAny": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noPropertyAccessFromIndexSignature": false, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "useUnknownInCatchVariables": true, - "verbatimModuleSyntax": true, - - "baseUrl": ".", - "paths": { - "#/*": ["./src/*"], - "#db/*": ["./src/database/*"], - "#document/*": ["./src/document/*"], - "#endpoint/*": ["./src/endpoints/*"], - "#http/*": ["./src/http/*"], - "#task/*": ["./src/tasks/*"], - "#util/*": ["./src/utils/*"] - } + "allowScripts": [], + "imports": { + "#/": "./src/", + "#db/": "./src/database/", + "#document/": "./src/document/", + "#endpoint/": "./src/endpoints/", + "#http/": "./src/http/", + "#task/": "./src/tasks/", + "#util/": "./src/utils/", + "@deno/loader": "jsr:@deno/loader@~0.3.11", + "@hono/hono": "jsr:@hono/hono@^4.11.4", + "@hono/openapi": "npm:hono-openapi@^1.1.2", + "@hono/standard-validator": "jsr:@hono/standard-validator@~0.2.2", + "@std/assert": "jsr:@std/assert@^1.0.16", + "@std/async": "jsr:@std/async@^1.0.16", + "@std/cache": "jsr:@std/cache@~0.2.1", + "@std/collections": "jsr:@std/collections@^1.1.3", + "@std/dotenv": "jsr:@std/dotenv@~0.225.6", + "@std/encoding": "jsr:@std/encoding@^1.0.10", + "@std/fmt": "jsr:@std/fmt@^1.0.8", + "@std/fs": "jsr:@std/fs@^1.0.21", + "@std/path": "jsr:@std/path@^1.1.4", + "@std/streams": "jsr:@std/streams@^1.0.16", + "@std/ulid": "jsr:@std/ulid@^1.0.0", + "@types/node": "npm:@types/node@^25.0.8", + "arkenv": "npm:arkenv@~0.8.3", + "arktype": "npm:arktype@^2.1.29", + "biome": "npm:@biomejs/biome@2.3.11", + "hash-wasm": "npm:hash-wasm@^4.12.0", + "nanoid": "jsr:@sitnik/nanoid@^5.1.5", + "rolldown": "npm:rolldown@1.0.0-beta.60", + "vite-bundle-analyzer": "npm:vite-bundle-analyzer@^1.3.2" }, "fmt": { "exclude": ["**"] @@ -48,6 +43,5 @@ "lint": { "exclude": ["**"] }, - "exclude": ["./dist/", "./node_modules/", "./storage/"], - "allowScripts": [] + "exclude": ["./dist/", "./node_modules/", "./storage/"] } diff --git a/deno.lock b/deno.lock index 8761d376..dc73d2ab 100644 --- a/deno.lock +++ b/deno.lock @@ -1,28 +1,107 @@ { "version": "5", "specifiers": { + "jsr:@deno/loader@~0.3.11": "0.3.11", + "jsr:@hono/hono@^4.11.4": "4.11.4", + "jsr:@hono/hono@^4.8.3": "4.11.4", + "jsr:@hono/standard-validator@~0.2.2": "0.2.2", + "jsr:@sitnik/nanoid@^5.1.5": "5.1.5", + "jsr:@standard-schema/spec@1": "1.1.0", + "jsr:@std/assert@^1.0.16": "1.0.16", + "jsr:@std/async@^1.0.16": "1.0.16", + "jsr:@std/bytes@^1.0.6": "1.0.6", + "jsr:@std/cache@~0.2.1": "0.2.1", + "jsr:@std/collections@^1.1.3": "1.1.3", + "jsr:@std/dotenv@~0.225.6": "0.225.6", + "jsr:@std/encoding@^1.0.10": "1.0.10", + "jsr:@std/fmt@^1.0.8": "1.0.8", + "jsr:@std/fs@^1.0.21": "1.0.21", + "jsr:@std/internal@^1.0.12": "1.0.12", + "jsr:@std/path@^1.1.4": "1.1.4", + "jsr:@std/streams@^1.0.16": "1.0.16", + "jsr:@std/ulid@1": "1.0.0", "npm:@biomejs/biome@2.3.11": "2.3.11", - "npm:@jsr/hono__hono@^4.11.4": "4.11.4", - "npm:@jsr/hono__standard-validator@~0.2.2": "0.2.2", - "npm:@jsr/std__assert@^1.0.16": "1.0.16", - "npm:@jsr/std__async@^1.0.16": "1.0.16", - "npm:@jsr/std__cache@~0.2.1": "0.2.1", - "npm:@jsr/std__collections@^1.1.3": "1.1.3", - "npm:@jsr/std__dotenv@~0.225.6": "0.225.6", - "npm:@jsr/std__encoding@^1.0.10": "1.0.10", - "npm:@jsr/std__fmt@^1.0.8": "1.0.8", - "npm:@jsr/std__fs@^1.0.21": "1.0.21", - "npm:@jsr/std__streams@^1.0.16": "1.0.16", - "npm:@jsr/std__ulid@1": "1.0.0", "npm:@types/node@^25.0.8": "25.0.8", "npm:arkenv@~0.8.3": "0.8.3_arktype@2.1.29", "npm:arktype@^2.1.29": "2.1.29", "npm:hash-wasm@^4.12.0": "4.12.0", "npm:hono-openapi@^1.1.2": "1.1.2_@standard-community+standard-json@0.3.5__@standard-schema+spec@1.1.0__@types+json-schema@7.0.15__arktype@2.1.29__quansync@0.2.11_@standard-community+standard-openapi@0.2.9__@standard-community+standard-json@0.3.5___@standard-schema+spec@1.1.0___@types+json-schema@7.0.15___arktype@2.1.29___quansync@0.2.11__@standard-schema+spec@1.1.0__arktype@2.1.29__openapi-types@12.1.3__@types+json-schema@7.0.15_@types+json-schema@7.0.15_openapi-types@12.1.3_arktype@2.1.29", - "npm:nanoid@^5.1.6": "5.1.6", "npm:rolldown@1.0.0-beta.60": "1.0.0-beta.60", "npm:vite-bundle-analyzer@^1.3.2": "1.3.2" }, + "jsr": { + "@deno/loader@0.3.11": { + "integrity": "7c62f4f09cdfc34e66ba25b5a775a1830cbb5266b3e39f67b0f620c75484df8d" + }, + "@hono/hono@4.11.4": { + "integrity": "aaf7b9d5a6b2422b0778c091b712ee1f018bc7e82138067d21eb27d7c2e1f5be" + }, + "@hono/standard-validator@0.2.2": { + "integrity": "bc94e1ab41d677a571cb6dd5012823f1162b9856ca24dfd60233734824bb0b0c", + "dependencies": [ + "jsr:@hono/hono@^4.8.3", + "jsr:@standard-schema/spec" + ] + }, + "@sitnik/nanoid@5.1.5": { + "integrity": "55bd5f57087d67b1dcb7c1f4a07efdfe77a3ac57ca0af90f162c1f676ebf8f4b" + }, + "@standard-schema/spec@1.1.0": { + "integrity": "2ccd54513cd9c960bd155ab569b1a901bc99c6f9ad29559d3f38a28c91c1822d" + }, + "@std/assert@1.0.16": { + "integrity": "6a7272ed1eaa77defe76e5ff63ca705d9c495077e2d5fd0126d2b53fc5bd6532", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/async@1.0.16": { + "integrity": "6c9e43035313b67b5de43e2b3ee3eadb39a488a0a0a3143097f112e025d3ee9a" + }, + "@std/bytes@1.0.6": { + "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" + }, + "@std/cache@0.2.1": { + "integrity": "b6f1abfd118d35b1c4ca90f2b3f4c709a2014ae368f244bdc7533bf1c169d759" + }, + "@std/collections@1.1.3": { + "integrity": "bf8b0818886df6a32b64c7d3b037a425111f28278d69fd0995aeb62777c986b0" + }, + "@std/dotenv@0.225.6": { + "integrity": "1d6f9db72f565bd26790fa034c26e45ecb260b5245417be76c2279e5734c421b" + }, + "@std/encoding@1.0.10": { + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" + }, + "@std/fmt@1.0.8": { + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" + }, + "@std/fs@1.0.21": { + "integrity": "d720fe1056d78d43065a4d6e0eeb2b19f34adb8a0bc7caf3a4dbf1d4178252cd", + "dependencies": [ + "jsr:@std/internal", + "jsr:@std/path" + ] + }, + "@std/internal@1.0.12": { + "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" + }, + "@std/path@1.1.4": { + "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/streams@1.0.16": { + "integrity": "85030627befb1767c60d4f65cb30fa2f94af1d6ee6e5b2515b76157a542e89c4", + "dependencies": [ + "jsr:@std/bytes" + ] + }, + "@std/ulid@1.0.0": { + "integrity": "d41c3d27a907714413649fee864b7cde8d42ee68437d22b79d5de4f81d808780" + } + }, "npm": { "@ark/schema@0.56.0": { "integrity": "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==", @@ -106,87 +185,6 @@ "tslib" ] }, - "@jsr/hono__hono@4.11.4": { - "integrity": "sha512-GdHrXgX+q2Q9LCse3RVXo5vmg8wJDlQoavZVXW3eXN9bczlzdkWBiy712FELfRdLZ0ij16BDQM5nkcc86O/fwg==", - "tarball": "https://npm.jsr.io/~/11/@jsr/hono__hono/4.11.4.tgz" - }, - "@jsr/hono__standard-validator@0.2.2": { - "integrity": "sha512-BXapiKtKoY63PuQlxs/dYmCcqWkwpOUi7yKfwZyHdBhuW9WtII/C5QDx6kimzXShG/e/Jk7U5aAtnlUeDpgUmA==", - "dependencies": [ - "@jsr/hono__hono", - "@jsr/standard-schema__spec" - ], - "tarball": "https://npm.jsr.io/~/11/@jsr/hono__standard-validator/0.2.2.tgz" - }, - "@jsr/standard-schema__spec@1.1.0": { - "integrity": "sha512-mWncLgOE1ZVd/xXG+SPmmDmYPQ9Q3OcIbkCn/3oPpp4WOw3RpbHWdxk/jiG8m4Bd9utE2b1yyfynSStpuhdXew==", - "tarball": "https://npm.jsr.io/~/11/@jsr/standard-schema__spec/1.1.0.tgz" - }, - "@jsr/std__assert@1.0.16": { - "integrity": "sha512-bX9ih0nR1kQ12/cnQRCQU0ppTCV7MFkP0qjyWxJRoDI8RC5cpTAmLFH/KcFgxmdN4flKkRbub8VtLuyKq+4OxA==", - "dependencies": [ - "@jsr/std__internal" - ], - "tarball": "https://npm.jsr.io/~/11/@jsr/std__assert/1.0.16.tgz" - }, - "@jsr/std__async@1.0.16": { - "integrity": "sha512-WoYmNEPSh+Bs09HvVceERknVX813wQjSb2D9Z0KdxGTMYl5Pm13e5xqa3mYu9QBRlxIxpTivGhIQYaslEezrhw==", - "tarball": "https://npm.jsr.io/~/11/@jsr/std__async/1.0.16.tgz" - }, - "@jsr/std__bytes@1.0.6": { - "integrity": "sha512-St6yKggjFGhxS52IFLJWvkchRFbAKg2Xh8UxA4S1EGz7GJ2Ui+ssDDldj/w2c8vCxvl6qgR0HaYbKeFJNqujmA==", - "tarball": "https://npm.jsr.io/~/11/@jsr/std__bytes/1.0.6.tgz" - }, - "@jsr/std__cache@0.2.1": { - "integrity": "sha512-K4qXWEOWiwo04zyJNxBogPdoXHcmmKjX+O8aKFEbVAd22/AiD9JFK25ZQ85/jCRZu+tDq99mSUz4x8NKqLsISQ==", - "tarball": "https://npm.jsr.io/~/11/@jsr/std__cache/0.2.1.tgz" - }, - "@jsr/std__collections@1.1.3": { - "integrity": "sha512-jGG6mv3IjOyxm6PyT1YVbLyAlZL+Gow6LOpBw+84qb1nkdJY0+t6bi7ICEqAwUz87cNjBS0P+yZQ5HHclJhsfw==", - "tarball": "https://npm.jsr.io/~/11/@jsr/std__collections/1.1.3.tgz" - }, - "@jsr/std__dotenv@0.225.6": { - "integrity": "sha512-rqh5RrHccbyzmP4v1/vqUyYy4dqopjTRgW8bJqk2ZXTKBbvpmMjPxJ+xy+YAk6XnEvtPCPAgqbFhHWcomjnX+w==", - "tarball": "https://npm.jsr.io/~/11/@jsr/std__dotenv/0.225.6.tgz" - }, - "@jsr/std__encoding@1.0.10": { - "integrity": "sha512-WK2njnDTyKefroRNk2Ooq7GStp6Y0ccAvr4To+Z/zecRAGe7+OSvH9DbiaHpAKwEi2KQbmpWMOYsdNt+TsdmSw==", - "tarball": "https://npm.jsr.io/~/11/@jsr/std__encoding/1.0.10.tgz" - }, - "@jsr/std__fmt@1.0.8": { - "integrity": "sha512-miZHzj9OgjuajrcMKzpqNVwFb9O71UHZzV/FHVq0E0Uwmv/1JqXgmXAoBNPrn+MP0fHT3mMgaZ6XvQO7dam67Q==", - "tarball": "https://npm.jsr.io/~/11/@jsr/std__fmt/1.0.8.tgz" - }, - "@jsr/std__fs@1.0.21": { - "integrity": "sha512-k/agrcKGm6KD89ci3AEyRmu3wRWf9JZNliOF4ZUxagTHiySmxjiKU3Lk+d2ksRtwEi7oWlLGS0AVM9Lciwc/xg==", - "dependencies": [ - "@jsr/std__internal", - "@jsr/std__path" - ], - "tarball": "https://npm.jsr.io/~/11/@jsr/std__fs/1.0.21.tgz" - }, - "@jsr/std__internal@1.0.12": { - "integrity": "sha512-6xReMW9p+paJgqoFRpOE2nogJFvzPfaLHLIlyADYjKMUcwDyjKZxryIbgcU+gxiTygn8yCjld1HoI0ET4/iZeA==", - "tarball": "https://npm.jsr.io/~/11/@jsr/std__internal/1.0.12.tgz" - }, - "@jsr/std__path@1.1.4": { - "integrity": "sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA==", - "dependencies": [ - "@jsr/std__internal" - ], - "tarball": "https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz" - }, - "@jsr/std__streams@1.0.16": { - "integrity": "sha512-8vQHEDIpAr5m9upZEcF1UO2ylZCJsOs5mlsXaJNehQmSm8Iiz/XaKtb73Fh6fj8Ybc2jNw2zyi9CLfqd3Ph6mA==", - "dependencies": [ - "@jsr/std__bytes" - ], - "tarball": "https://npm.jsr.io/~/11/@jsr/std__streams/1.0.16.tgz" - }, - "@jsr/std__ulid@1.0.0": { - "integrity": "sha512-RvVolUwRoFtoSuYZROBmhCYEIuI4xsxeHjGGbJQZVDCBnDBJwfm16vOJyE1Va9+BnTl1iW7o1nCloBI+EQAWVg==", - "tarball": "https://npm.jsr.io/~/11/@jsr/std__ulid/1.0.0.tgz" - }, "@napi-rs/wasm-runtime@1.1.1": { "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", "dependencies": [ @@ -342,10 +340,6 @@ "openapi-types" ] }, - "nanoid@5.1.6": { - "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", - "bin": true - }, "openapi-types@12.1.3": { "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" }, @@ -387,30 +381,30 @@ } }, "workspace": { - "packageJson": { - "dependencies": [ - "npm:@biomejs/biome@2.3.11", - "npm:@jsr/hono__hono@^4.11.4", - "npm:@jsr/hono__standard-validator@~0.2.2", - "npm:@jsr/std__assert@^1.0.16", - "npm:@jsr/std__async@^1.0.16", - "npm:@jsr/std__cache@~0.2.1", - "npm:@jsr/std__collections@^1.1.3", - "npm:@jsr/std__dotenv@~0.225.6", - "npm:@jsr/std__encoding@^1.0.10", - "npm:@jsr/std__fmt@^1.0.8", - "npm:@jsr/std__fs@^1.0.21", - "npm:@jsr/std__streams@^1.0.16", - "npm:@jsr/std__ulid@1", - "npm:@types/node@^25.0.8", - "npm:arkenv@~0.8.3", - "npm:arktype@^2.1.29", - "npm:hash-wasm@^4.12.0", - "npm:hono-openapi@^1.1.2", - "npm:nanoid@^5.1.6", - "npm:rolldown@1.0.0-beta.60", - "npm:vite-bundle-analyzer@^1.3.2" - ] - } + "dependencies": [ + "jsr:@deno/loader@~0.3.11", + "jsr:@hono/hono@^4.11.4", + "jsr:@hono/standard-validator@~0.2.2", + "jsr:@sitnik/nanoid@^5.1.5", + "jsr:@std/assert@^1.0.16", + "jsr:@std/async@^1.0.16", + "jsr:@std/cache@~0.2.1", + "jsr:@std/collections@^1.1.3", + "jsr:@std/dotenv@~0.225.6", + "jsr:@std/encoding@^1.0.10", + "jsr:@std/fmt@^1.0.8", + "jsr:@std/fs@^1.0.21", + "jsr:@std/path@^1.1.4", + "jsr:@std/streams@^1.0.16", + "jsr:@std/ulid@1", + "npm:@biomejs/biome@2.3.11", + "npm:@types/node@^25.0.8", + "npm:arkenv@~0.8.3", + "npm:arktype@^2.1.29", + "npm:hash-wasm@^4.12.0", + "npm:hono-openapi@^1.1.2", + "npm:rolldown@1.0.0-beta.60", + "npm:vite-bundle-analyzer@^1.3.2" + ] } } diff --git a/lib/deno-rolldown/LICENSE b/lib/deno-rolldown/LICENSE new file mode 100644 index 00000000..24295035 --- /dev/null +++ b/lib/deno-rolldown/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018-2025 the Deno authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/deno-rolldown/mod.ts b/lib/deno-rolldown/mod.ts new file mode 100644 index 00000000..abd0256f --- /dev/null +++ b/lib/deno-rolldown/mod.ts @@ -0,0 +1,240 @@ +// biome-ignore-all lint: vibecode +// based on: https://github.com/denoland/deno-rolldown-plugin + +import { + type Loader, + type LoadResponse, + MediaType, + RequestedModuleType, + ResolutionMode, + Workspace, + type WorkspaceOptions +} from "@deno/loader"; +import { fromFileUrl } from "@std/path"; + +const MARegex = /.*/; + +type Module = { + specifier: string; + code: string; +}; + +/** Options for creating the Deno plugin. */ +export interface DenoPluginOptions extends WorkspaceOptions { + /** Entry points for the build (optional, can be provided in buildStart) */ + entrypoints?: string[]; + /** + * Patterns to treat as external when Deno loader can't resolve them. + * Useful for npm packages that should remain external. + */ + externalPatterns?: (string | RegExp)[]; +} + +export type BuildStartOptions = { + input?: string | string[] | Record; +}; + +export type ResolveIdOptions = { + kind: "import-statement" | "dynamic-import" | "require-call"; +}; + +export interface DenoPlugin extends Disposable { + name: string; + buildStart(options?: BuildStartOptions): Promise; + resolveId: { + filter: { id: RegExp }; + handler( + source: string, + importer: string | undefined, + options: ResolveIdOptions + ): Promise; + }; + load: { + filter: { id: RegExp }; + handler(id: string): string | undefined; + }; +} + +/** + * Creates a deno plugin for use with rolldown. + * @returns The plugin. + */ +export default function denoPlugin(pluginOptions: DenoPluginOptions = {}): DenoPlugin { + let loader: Loader | undefined; + const loads = new Map>(); + const modules = new Map(); + + return { + name: "deno-plugin", + + [Symbol.dispose]: () => { + if (loader && typeof loader[Symbol.dispose] === "function") { + loader[Symbol.dispose](); + } + }, + + buildStart: async (options) => { + let inputs: string[] = []; + + if (options?.input != null) { + const { input } = options; + if (Array.isArray(input)) { + inputs = input; + } else if (typeof input === "object") { + inputs = Object.values(input); + } else if (typeof input === "string") { + inputs = [input]; + } + } else if (pluginOptions.entrypoints?.length) { + inputs = pluginOptions.entrypoints; + } + + if (inputs.length === 0) return; + + const workspace = new Workspace({ ...pluginOptions }); + loader = await workspace.createLoader(); + await loader.addEntrypoints(inputs); + }, + + resolveId: { + filter: { id: MARegex }, + + handler: async (source, importer, options) => { + if (!loader) { + throw new Error("Deno loader not initialized. Make sure buildStart was called."); + } + + const resolutionMode = resolveKindToResolutionMode(options.kind); + const normalizedImporter = importer != null ? (modules.get(importer)?.specifier ?? importer) : undefined; + + let resolvedSpecifier: string; + try { + resolvedSpecifier = await loader.resolve(source, normalizedImporter, resolutionMode); + } catch (error: unknown) { + if ((error as { code?: string })?.code === "ERR_MODULE_NOT_FOUND") { + if (pluginOptions.externalPatterns) { + for (const pattern of pluginOptions.externalPatterns) { + if (typeof pattern === "string") { + if (source === pattern || source.startsWith(`${pattern}/`)) { + return { id: source, external: true }; + } + } else if (pattern.test(source)) { + return { id: source, external: true }; + } + } + } + + if ( + !( + source.startsWith(".") || + source.startsWith("/") || + source.startsWith("file:") || + source.startsWith("http") + ) + ) { + return { id: source, external: true }; + } + + return; + } + throw error; + } + + let loadPromise = loads.get(resolvedSpecifier); + if (!loadPromise) { + loadPromise = loader.load(resolvedSpecifier, RequestedModuleType.Default); + loads.set(resolvedSpecifier, loadPromise); + } + + const result = await loadPromise; + + if (!result) { + modules.set(resolvedSpecifier, undefined); + return resolvedSpecifier; + } + + if (result.kind === "external") { + return { id: result.specifier, external: true }; + } + + const ext = mediaTypeToExtension(result.mediaType); + let { specifier } = result; + + if (!specifier.endsWith(ext)) { + specifier += `.rolldown${ext}`; + } + + if (specifier.startsWith("file:///")) { + specifier = fromFileUrl(specifier); + } + + modules.set(specifier, { + specifier: result.specifier, + code: new TextDecoder().decode(result.code) + }); + + return specifier; + } + }, + + load: { + filter: { id: MARegex }, + + handler: (id) => { + return modules.get(id)?.code; + } + } + }; +} + +function mediaTypeToExtension(mediaType: MediaType): string { + switch (mediaType) { + case MediaType.JavaScript: + return ".js"; + case MediaType.Mjs: + return ".mjs"; + case MediaType.Cjs: + return ".cjs"; + case MediaType.Jsx: + return ".jsx"; + case MediaType.TypeScript: + case MediaType.Mts: + return ".ts"; + case MediaType.Cts: + return ".cts"; + case MediaType.Dts: + return ".d.ts"; + case MediaType.Dmts: + return ".d.mts"; + case MediaType.Dcts: + return ".d.cts"; + case MediaType.Tsx: + return ".tsx"; + case MediaType.Css: + return ".css"; + case MediaType.Json: + return ".json"; + case MediaType.Html: + return ".html"; + case MediaType.Sql: + return ".sql"; + case MediaType.Wasm: + return ".wasm"; + case MediaType.SourceMap: + return ".map"; + default: + return ""; + } +} + +function resolveKindToResolutionMode(kind: string): ResolutionMode { + switch (kind) { + case "import-statement": + case "dynamic-import": + return ResolutionMode.Import; + case "require-call": + return ResolutionMode.Require; + default: + throw new Error(`not implemented: ${kind}`); + } +} diff --git a/package.json b/package.json index f92477c0..e018ade4 100644 --- a/package.json +++ b/package.json @@ -1,51 +1,4 @@ { - "$schema": "https://www.schemastore.org/package.json", "license": "EUPL-1.2", - "type": "module", - "dependencies": { - "@hono/hono": "npm:@jsr/hono__hono@^4.11.4", - "@hono/openapi": "npm:hono-openapi@^1.1.2", - "@hono/standard-validator": "npm:@jsr/hono__standard-validator@~0.2.2", - "@std/assert": "npm:@jsr/std__assert@^1.0.16", - "@std/async": "npm:@jsr/std__async@^1.0.16", - "@std/cache": "npm:@jsr/std__cache@~0.2.1", - "@std/collections": "npm:@jsr/std__collections@^1.1.3", - "@std/dotenv": "npm:@jsr/std__dotenv@~0.225.6", - "@std/encoding": "npm:@jsr/std__encoding@^1.0.10", - "@std/fmt": "npm:@jsr/std__fmt@^1.0.8", - "@std/fs": "npm:@jsr/std__fs@^1.0.21", - "@std/streams": "npm:@jsr/std__streams@^1.0.16", - "@std/ulid": "npm:@jsr/std__ulid@^1.0.0", - "@types/node": "npm:@types/node@^25.0.8", - "arkenv": "npm:arkenv@~0.8.3", - "arktype": "npm:arktype@^2.1.29", - "biome": "npm:@biomejs/biome@2.3.11", - "hash-wasm": "npm:hash-wasm@^4.12.0", - "nanoid": "npm:nanoid@^5.1.6", - "rolldown": "npm:rolldown@1.0.0-beta.60", - "vite-bundle-analyzer": "npm:vite-bundle-analyzer@^1.3.2" - }, - "imports": { - "#/*": [ - "./src/*" - ], - "#db/*": [ - "./src/database/*" - ], - "#document/*": [ - "./src/document/*" - ], - "#endpoint/*": [ - "./src/endpoints/*" - ], - "#http/*": [ - "./src/http/*" - ], - "#task/*": [ - "./src/tasks/*" - ], - "#util/*": [ - "./src/utils/*" - ] - } + "type": "module" } diff --git a/rolldown.config.ts b/rolldown.config.ts index d1c696cd..3578f3b3 100644 --- a/rolldown.config.ts +++ b/rolldown.config.ts @@ -1,5 +1,6 @@ import type { RolldownOptions } from "rolldown"; import { analyzer, unstableRolldownAdapter } from "vite-bundle-analyzer"; +import deno from "./lib/deno-rolldown/mod.ts"; const analyze = false; @@ -25,13 +26,8 @@ export default { optimization: { inlineConst: true }, - transform: { - // deno.json compilerOptions - typescript: { - onlyRemoveTypeImports: true - } - }, plugins: [ + deno(), unstableRolldownAdapter( analyzer({ enabled: analyze, diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..52a5cb2c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://www.schemastore.org/tsconfig.json", + "compilerOptions": { + "lib": ["ESNext", "deno.window", "deno.unstable"], + "types": ["node"], + "module": "esnext", + "moduleResolution": "bundler", + + "checkJs": false, + "skipLibCheck": true, + + "strict": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "exactOptionalPropertyTypes": false, + "isolatedDeclarations": false, + "noErrorTruncation": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "useUnknownInCatchVariables": true, + "verbatimModuleSyntax": true, + + "baseUrl": ".", + "paths": { + "#/*": ["./src/*"], + "#db/*": ["./src/database/*"], + "#document/*": ["./src/document/*"], + "#endpoint/*": ["./src/endpoints/*"], + "#http/*": ["./src/http/*"], + "#task/*": ["./src/tasks/*"], + "#util/*": ["./src/utils/*"] + } + }, + "exclude": ["./dist/", "./node_modules/", "./storage/"] +}