diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..bb26545e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# Normalize line endings: store LF in the repo and check out LF on every +# platform, so prettier/gofmt see identical bytes locally and in CI. +* text=auto eol=lf + +# Binary assets +*.png binary +*.ico binary +*.wasm binary +*.exe binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index deca0042..c1d269d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,12 @@ jobs: - name: Go vulnerability check run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 && govulncheck ./... + # Generated sqlc output must never drift from db/queries/. One leg of + # the matrix is enough; make is not guaranteed on the Windows runner. + - name: Verify generated sqlc output (make sqlc-verify) + if: matrix.os == 'ubuntu-latest' + run: make sqlc-install sqlc-verify + - name: Run tests with race detection and coverage run: go test -race -timeout 20m ./... -coverprofile=coverage.out -cover @@ -73,7 +79,7 @@ jobs: working-directory: Server/ client-check: - name: Client Typecheck & Test + name: Client Static Checks runs-on: windows-latest defaults: run: @@ -101,6 +107,7 @@ jobs: if (fs.existsSync(p)) { let c = fs.readFileSync(p, 'utf8'); c = c.replace(/^type Event\b/gm, 'type _Event').replace(/^interface Event\b/gm, 'interface _Event'); + c = c.replace(/,\s*type Event\s*(?=\})/g, ' '); // unused named import from @tauri-apps/api/event fs.writeFileSync(p, c); console.log('Patched: renamed Event -> _Event in generated/events.ts'); } else { @@ -126,6 +133,28 @@ jobs: - name: Knip (unused code & deps) run: npx knip || true + # Unit tests live in their own job so a red suite is visible as exactly one + # failing check instead of masking the static gates above. The suite is + # KNOWN RED pending the reboot-plan P2 triage — do not "fix" tests here by + # editing assertions; see docs/plans and the P2 triage rules. + client-tests: + name: Client Unit Tests + runs-on: windows-latest + defaults: + run: + working-directory: Client/tauri-client/ + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 20 + cache: npm + cache-dependency-path: Client/tauri-client/package-lock.json + + - name: Install npm dependencies + run: npm ci + - name: Run unit tests with coverage run: npx vitest run --coverage --reporter=default @@ -192,7 +221,8 @@ jobs: libasound2-dev \ libssl-dev \ patchelf \ - librsvg2-dev + librsvg2-dev \ + xdg-utils - name: Install Rust uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79ff824a..4025edfa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,7 +83,8 @@ jobs: libasound2-dev \ libssl-dev \ patchelf \ - librsvg2-dev + librsvg2-dev \ + xdg-utils - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -211,7 +212,8 @@ jobs: libasound2-dev \ libssl-dev \ patchelf \ - librsvg2-dev + librsvg2-dev \ + xdg-utils - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -361,9 +363,21 @@ jobs: VERSION="${GITHUB_REF_NAME#v}" echo "VERSION=$VERSION" >> "$GITHUB_ENV" - - name: Generate SHA256 checksums + - name: Create source snapshot (AGPL source availability) + shell: bash run: | - find windows linux -type f -exec sha256sum {} \; > checksums.sha256 + git archive --format=tar.gz --prefix="OwnCord-${VERSION}/" \ + -o "owncord-src-${{ github.ref_name }}.tar.gz" HEAD + + # Checksum lines must use bare asset filenames: the v1.0.0 updater's + # ParseChecksumFile does an exact match on the last field, so a + # "windows/" prefix would strand every deployed server on 1.0.0. + - name: Generate SHA256 checksums + shell: bash + run: | + (cd windows && sha256sum *) > checksums.sha256 + (cd linux && sha256sum *) >> checksums.sha256 + sha256sum owncord-src-*.tar.gz >> checksums.sha256 - name: Generate server update manifest shell: bash @@ -382,8 +396,22 @@ jobs: printf '%s' "$SERVER_UPDATE_SIGNING_PRIVATE_KEY" > "$KEY_PATH" trap 'rm -f "$KEY_PATH"' EXIT npm ci - npx tauri signer sign -k "$KEY_PATH" -p "$SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD" ../../windows/chatserver.exe - npx tauri signer sign -k "$KEY_PATH" -p "$SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD" ../../windows/server-update-manifest.json + npx tauri signer sign -f "$KEY_PATH" -p "$SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD" ../../windows/chatserver.exe + npx tauri signer sign -f "$KEY_PATH" -p "$SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD" ../../windows/server-update-manifest.json + + # Fail closed before publishing: prove the freshly signed assets verify + # against the pinned public key that ships inside the server binary. + # Catches key/pubkey mismatch, signature format drift, and signer flag + # regressions — each of which has silently broken this pipeline before. + - name: Verify signed assets against pinned server update key + shell: bash + run: | + sudo apt-get update && sudo apt-get install -y minisign + base64 -d Server/updater/server_update_public_key.txt > "$RUNNER_TEMP/server_update.pub" + for f in windows/chatserver.exe windows/server-update-manifest.json; do + base64 -d "$f.sig" > "$RUNNER_TEMP/asset.minisig" + minisign -Vm "$f" -x "$RUNNER_TEMP/asset.minisig" -p "$RUNNER_TEMP/server_update.pub" + done - name: Install root dependencies (changelogen) run: npm ci @@ -397,7 +425,33 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | mapfile -t assets < <(find windows linux -type f) - assets+=(checksums.sha256) + assets+=(checksums.sha256 owncord-src-*.tar.gz) gh release create "${{ github.ref_name }}" \ --notes-file CHANGELOG.md \ "${assets[@]}" + + # The public releases repo is what deployed servers and clients poll for + # updates, and it carries the AGPL source snapshot while the source repo + # is private. Publishing there must never be skipped silently once the + # source repo is private. + - name: Publish to public releases repo + shell: bash + env: + RELEASES_TOKEN: ${{ secrets.RELEASES_REPO_TOKEN }} + SOURCE_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ -z "$RELEASES_TOKEN" ]; then + PRIVATE=$(GH_TOKEN="$SOURCE_REPO_TOKEN" gh api "repos/$GITHUB_REPOSITORY" --jq .private) + if [ "$PRIVATE" = "true" ]; then + echo "::error::Source repo is private and RELEASES_REPO_TOKEN is unset — binaries would ship with no public source or update feed (AGPL violation, broken updater)." + exit 1 + fi + echo "::warning::RELEASES_REPO_TOKEN not set — skipping publish to J3vb/OwnCord-releases." + exit 0 + fi + mapfile -t assets < <(find windows linux -type f) + assets+=(checksums.sha256 owncord-src-*.tar.gz) + GH_TOKEN="$RELEASES_TOKEN" gh release create "${{ github.ref_name }}" \ + --repo J3vb/OwnCord-releases \ + --notes-file CHANGELOG.md \ + "${assets[@]}" diff --git a/.gitignore b/.gitignore index 0e2d5c6a..4dcdb234 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,9 @@ TODOS.md CLAUDE.md DESIGN.md Client/CLIENT-REVIEW.md + +# Agent tooling state +.serena/ + +# Client env (holds API keys - never commit) +Client/tauri-client/.env diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc901b9..8dc79a1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,13 @@ tooling (`npm run changelog`) auto-generates entries from commit messages on each release; this file is the curated counterpart that calls out behavioural changes operators must know about. -## Unreleased — Phase B + C +## Unreleased — v1.1.0-alpha series (Phase B + C) + +> **Project reset note:** OwnCord has re-entered alpha. The `v1.0.0` release is +> superseded; versioning continues forward as `v1.1.0-alpha.N` so deployed +> servers and clients keep receiving updates. Releases are published to the +> public [`OwnCord-releases`](https://github.com/J3vb/OwnCord-releases) +> repository, including a full source snapshot with every release. ### Phase B — Acceleration @@ -29,8 +35,8 @@ behavioural changes operators must know about. `VoiceService.JoinChannel`, `InviteService.CreateInvite`, `ModerationService.BanUser`, `BlockService.BlockUser`, `UserService.UpdateProfile`. The real OTel SDK is gated behind - `-tags otel` and is currently a placeholder; wiring the upstream - modules is tracked in `PHASE_BC_LOCAL_TODO.md`. + `-tags otel` and is currently a placeholder; completing it is + deferred until after the beta reset. - **Solid.js proof of concept (Step 6).** Two leaf components migrated (`Badge`, `ChannelListItem`), Vite + JSX configured, store→signal adapter landed. The remaining vanilla components remain in place; @@ -93,16 +99,11 @@ behavioural changes operators must know about. the existing IP restriction.** A previous prerelease shipped with only the IP gate; that has been corrected. -### Known follow-up work (local toolchain required) +### Deferred work -See `PHASE_BC_LOCAL_TODO.md` for the full list. Highlights: - -- Real OpenTelemetry SDK wiring (needs `go get` of the upstream modules) -- Real Wazero runtime construction (needs `go get github.com/tetratelabs/wazero`) -- Postgres backend implementation (needs `make sqlc-generate`) -- Tinygo `.wasm` build of the example hello plugin -- Migration of the remaining vanilla TypeScript components to Solid.js -- Slash-command dispatcher in the WS layer (design TBD) - -These items each need a real developer machine with network access; no -in-sandbox pass can land them. +The project is under a feature freeze until the beta reset completes. +Explicitly deferred (not abandoned unless noted): real OpenTelemetry SDK +wiring, the Postgres backend (scaffolding removed pending real demand), +the slash-command dispatcher (`docs/plans/slash-commands.md`), and the +Solid.js migration (abandoned — the experiment is being removed in favor +of the established vanilla component pattern). diff --git a/Client/tauri-client/package-lock.json b/Client/tauri-client/package-lock.json index 8fc51435..1a4ab8eb 100644 --- a/Client/tauri-client/package-lock.json +++ b/Client/tauri-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "owncord-client", - "version": "1.0.0", + "version": "1.1.0-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "owncord-client", - "version": "1.0.0", + "version": "1.1.0-alpha.1", "dependencies": { "@jitsi/rnnoise-wasm": "^0.2.1", "@tauri-apps/api": "^2.10.1", @@ -121,13 +121,13 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -143,9 +143,9 @@ "license": "MIT" }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -153,21 +153,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -194,14 +194,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -224,14 +224,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -293,9 +293,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -317,29 +317,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -404,9 +404,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -414,9 +414,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -424,9 +424,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -434,27 +434,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -631,33 +631,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -665,14 +665,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -4341,16 +4341,16 @@ } }, "node_modules/@vitest/browser": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.2.4.tgz", - "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.2.7.tgz", + "integrity": "sha512-gIzazUkbQfv6T1rJHOLhMMKQnplKAvvQ7QNGaFwI6oCsp4z2aSDZCojGpX3QX3+MYsvJdyy/8BRIYVEbAkMkEA==", "dev": true, "license": "MIT", "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", - "@vitest/mocker": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/mocker": "3.2.7", + "@vitest/utils": "3.2.7", "magic-string": "^0.30.17", "sirv": "^3.0.1", "tinyrainbow": "^2.0.0", @@ -4361,7 +4361,7 @@ }, "peerDependencies": { "playwright": "*", - "vitest": "3.2.4", + "vitest": "3.2.7", "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0" }, "peerDependenciesMeta": { @@ -4377,9 +4377,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", - "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.7.tgz", + "integrity": "sha512-NEGWJS2XNu2PfRLQwOO3CTKj1tTETxNBdk454vDxVBhxJYhPaA/eS0nAI0c+1El1P7a60z8+i+ZrQoGESweGKg==", "dev": true, "license": "MIT", "dependencies": { @@ -4401,8 +4401,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.4", - "vitest": "3.2.4" + "@vitest/browser": "3.2.7", + "vitest": "3.2.7" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4411,15 +4411,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -4428,13 +4428,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -4455,9 +4455,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", "dev": true, "license": "MIT", "dependencies": { @@ -4468,13 +4468,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", + "@vitest/utils": "3.2.7", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -4483,13 +4483,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -4498,9 +4498,9 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4511,13 +4511,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, @@ -4713,9 +4713,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz", - "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==", + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4736,9 +4736,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -4762,9 +4762,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", "dev": true, "funding": [ { @@ -4782,10 +4782,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -4847,9 +4847,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001784", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz", - "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==", + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "dev": true, "funding": [ { @@ -5137,9 +5137,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.330", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.330.tgz", - "integrity": "sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==", + "version": "1.5.393", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.393.tgz", + "integrity": "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg==", "dev": true, "license": "ISC" }, @@ -5191,9 +5191,9 @@ "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -5578,9 +5578,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", "dev": true, "funding": [ { @@ -5977,9 +5977,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", "dependencies": { @@ -6300,10 +6300,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6798,9 +6808,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -6824,11 +6834,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/npm-run-path": { "version": "6.0.0", @@ -7206,9 +7219,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", "dev": true, "funding": [ { @@ -7226,7 +7239,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7335,13 +7348,14 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -7591,15 +7605,15 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -7611,14 +7625,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -8233,9 +8247,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.3.tgz", - "integrity": "sha512-eJdUmK/Wrx2d+mnWWmwwLRyA7OQCkLap60sk3dOK4ViZR7DKwwptwuIvFBg2HaiP9ESaEdhtpSymQPvytpmkCA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "engines": { @@ -8297,9 +8311,9 @@ } }, "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", "dependencies": { @@ -8440,20 +8454,20 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -8483,8 +8497,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, @@ -8732,9 +8746,9 @@ } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", "dev": true, "license": "MIT", "engines": { diff --git a/Client/tauri-client/package.json b/Client/tauri-client/package.json index ff36eb12..d0007cfd 100644 --- a/Client/tauri-client/package.json +++ b/Client/tauri-client/package.json @@ -1,7 +1,7 @@ { "name": "owncord-client", "private": true, - "version": "1.0.0", + "version": "1.1.0-alpha.1", "type": "module", "scripts": { "dev": "vite", diff --git a/Client/tauri-client/src-tauri/.cargo/audit.toml b/Client/tauri-client/src-tauri/.cargo/audit.toml new file mode 100644 index 00000000..46b22dc5 --- /dev/null +++ b/Client/tauri-client/src-tauri/.cargo/audit.toml @@ -0,0 +1,11 @@ +[advisories] +ignore = [ + # quick-xml 0.37 is pinned by tauri-winrt-notification 0.7 (via + # tauri-plugin-notification -> notify-rust); no semver-compatible route + # to the fixed 0.41 exists yet. It only parses toast-notification XML + # templates the library itself constructs — never attacker-controlled + # input — so these parser-DoS advisories are not reachable here. + # Drop both entries when the notification chain moves to quick-xml >= 0.41. + "RUSTSEC-2026-0194", + "RUSTSEC-2026-0195", +] diff --git a/Client/tauri-client/src-tauri/Cargo.lock b/Client/tauri-client/src-tauri/Cargo.lock index 208dccc8..28e14872 100644 --- a/Client/tauri-client/src-tauri/Cargo.lock +++ b/Client/tauri-client/src-tauri/Cargo.lock @@ -519,6 +519,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -745,6 +756,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -775,9 +795,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -1599,11 +1619,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -1613,10 +1631,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -2019,7 +2040,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.58.0", ] [[package]] @@ -2942,7 +2963,7 @@ dependencies = [ [[package]] name = "owncord-client" -version = "1.0.0" +version = "1.1.0-alpha.1" dependencies = [ "device_query", "env_logger", @@ -3532,14 +3553,15 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "bytes", - "getrandom 0.3.4", + "getrandom 0.4.2", "lru-slab", - "rand 0.9.2", + "rand 0.10.2", + "rand_pcg 0.10.2", "ring", "rustc-hash", "rustls", @@ -3597,7 +3619,7 @@ dependencies = [ "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", - "rand_pcg", + "rand_pcg 0.2.1", ] [[package]] @@ -3621,6 +3643,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3678,6 +3711,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_hc" version = "0.2.0" @@ -3696,6 +3735,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -4005,9 +4053,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -4363,7 +4411,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -4374,7 +4422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6194,19 +6242,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-future" version = "0.2.1" diff --git a/Client/tauri-client/src-tauri/Cargo.toml b/Client/tauri-client/src-tauri/Cargo.toml index 96743742..7ac468c3 100644 --- a/Client/tauri-client/src-tauri/Cargo.toml +++ b/Client/tauri-client/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "owncord-client" -version = "1.0.0" +version = "1.1.0-alpha.1" edition = "2021" description = "OwnCord Desktop Client" diff --git a/Client/tauri-client/src-tauri/src/commands.rs b/Client/tauri-client/src-tauri/src/commands.rs index 9f1f901d..bbfd0fd1 100644 --- a/Client/tauri-client/src-tauri/src/commands.rs +++ b/Client/tauri-client/src-tauri/src/commands.rs @@ -114,7 +114,7 @@ pub fn store_cert_fingerprint( // existed, or delete if there was none. Without this, a failed save // during cert rotation would silently lose the previously trusted cert. match old_value { - Some(v) => { let _ = store.set(&host, v); } + Some(v) => { store.set(&host, v); } None => { let _ = store.delete(&host); } } return Err(format!("failed to persist cert fingerprint: {e}")); @@ -150,12 +150,10 @@ pub fn get_cert_fingerprint( // DevTools command // --------------------------------------------------------------------------- +#[cfg(feature = "devtools")] #[tauri::command] -pub fn open_devtools(_window: tauri::WebviewWindow) { - #[cfg(feature = "devtools")] - { - _window.open_devtools(); - } +pub fn open_devtools(window: tauri::WebviewWindow) { + window.open_devtools(); } // --------------------------------------------------------------------------- diff --git a/Client/tauri-client/src-tauri/src/lib.rs b/Client/tauri-client/src-tauri/src/lib.rs index 5b40fc3e..90e2cb91 100644 --- a/Client/tauri-client/src-tauri/src/lib.rs +++ b/Client/tauri-client/src-tauri/src/lib.rs @@ -74,7 +74,7 @@ pub fn run() { #[cfg(not(target_os = "linux"))] rfd::MessageDialog::new() .set_title("OwnCord failed to start") - .set_description(&format!( + .set_description(format!( "The application encountered a startup error and cannot continue.\n\n{e}" )) .set_level(rfd::MessageLevel::Error) diff --git a/Client/tauri-client/src-tauri/src/ws_proxy.rs b/Client/tauri-client/src-tauri/src/ws_proxy.rs index 7327e885..fec2f8fb 100644 --- a/Client/tauri-client/src-tauri/src/ws_proxy.rs +++ b/Client/tauri-client/src-tauri/src/ws_proxy.rs @@ -167,7 +167,7 @@ fn tofu_check( // Restore previous in-memory state: put back old value or delete // if there was none, keeping in-memory consistent with on-disk. match old_value { - Some(v) => { let _ = store.set(host, v); } + Some(v) => { store.set(host, v); } None => { let _ = store.delete(host); } } return Err(format!("failed to persist cert fingerprint: {e}")); @@ -451,7 +451,7 @@ pub fn accept_cert_fingerprint( // fingerprint would be trusted in-process even though it was never // persisted to certs.json. match old_value { - Some(v) => { let _ = store.set(&host, v); } + Some(v) => { store.set(&host, v); } None => { let _ = store.delete(&host); } } return Err(format!("failed to persist cert fingerprint: {e}")); diff --git a/Client/tauri-client/src-tauri/tauri.conf.json b/Client/tauri-client/src-tauri/tauri.conf.json index 79553eab..a47d7e88 100644 --- a/Client/tauri-client/src-tauri/tauri.conf.json +++ b/Client/tauri-client/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "OwnCord", - "version": "1.0.0", + "version": "1.1.0-alpha.1", "identifier": "com.owncord.client", "build": { "frontendDist": "../dist", @@ -54,7 +54,7 @@ "bundleMediaFramework": true } }, - "category": "Network", + "category": "SocialNetworking", "shortDescription": "Self-hosted chat platform", "longDescription": "OwnCord is a self-hosted voice and text chat platform with end-to-end encryption and full media support.", "windows": { diff --git a/Client/tauri-client/src/generated/commands.ts b/Client/tauri-client/src/generated/commands.ts index 3626da55..d5b9393f 100644 --- a/Client/tauri-client/src/generated/commands.ts +++ b/Client/tauri-client/src/generated/commands.ts @@ -10,9 +10,7 @@ import { invoke } from "@tauri-apps/api/core"; import * as types from "./types"; -export async function startLivekitProxy( - params: types.StartLivekitProxyParams, -): Promise { +export async function startLivekitProxy(params: types.StartLivekitProxyParams): Promise { return invoke("start_livekit_proxy", params); } @@ -52,9 +50,7 @@ export async function pttListenForKey(): Promise { return invoke("ptt_listen_for_key"); } -export async function saveCredential( - params: types.SaveCredentialParams, -): Promise { +export async function saveCredential(params: types.SaveCredentialParams): Promise { return invoke("save_credential", params); } @@ -64,9 +60,7 @@ export async function loadCredential( return invoke("load_credential", params); } -export async function deleteCredential( - params: types.DeleteCredentialParams, -): Promise { +export async function deleteCredential(params: types.DeleteCredentialParams): Promise { return invoke("delete_credential", params); } @@ -92,9 +86,7 @@ export async function getSettings(): Promise { return invoke("get_settings"); } -export async function saveSettings( - params: types.SaveSettingsParams, -): Promise { +export async function saveSettings(params: types.SaveSettingsParams): Promise { return invoke("save_settings", params); } diff --git a/Client/tauri-client/src/generated/events.ts b/Client/tauri-client/src/generated/events.ts index bce17bae..a97f7d62 100644 --- a/Client/tauri-client/src/generated/events.ts +++ b/Client/tauri-client/src/generated/events.ts @@ -11,7 +11,7 @@ * Event Listeners * Type-safe event listener helpers for Tauri events */ -import { listen, type UnlistenFn, type Event } from "@tauri-apps/api/event"; +import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import * as types from "./types"; /** @@ -19,9 +19,7 @@ import * as types from "./types"; * @param handler - Callback function to handle the event * @returns Promise that resolves to an unlisten function */ -export async function onStatusChange( - handler: (payload: string) => void, -): Promise { +export async function onStatusChange(handler: (payload: string) => void): Promise { return listen("status-change", (event) => { handler(event.payload); }); @@ -32,9 +30,7 @@ export async function onStatusChange( * @param handler - Callback function to handle the event * @returns Promise that resolves to an unlisten function */ -export async function onWsState( - handler: (payload: string) => void, -): Promise { +export async function onWsState(handler: (payload: string) => void): Promise { return listen("ws-state", (event) => { handler(event.payload); }); @@ -45,9 +41,7 @@ export async function onWsState( * @param handler - Callback function to handle the event * @returns Promise that resolves to an unlisten function */ -export async function onCertTofu( - handler: (payload: types.Value) => void, -): Promise { +export async function onCertTofu(handler: (payload: types.Value) => void): Promise { return listen("cert-tofu", (event) => { handler(event.payload); }); diff --git a/README.md b/README.md index ef896ff2..66caca3e 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ It includes real-time messaging, voice/video via LiveKit, file sharing, and a we ### Option A: Prebuilt binaries -1. Download assets from [GitHub Releases](https://github.com/J3vb/OwnCord/releases). +1. Download assets from [OwnCord-releases](https://github.com/J3vb/OwnCord-releases/releases) (binaries, checksums, signatures, and a full source snapshot per release). 2. Run the server binary: - Windows: `chatserver.exe` - Linux: `./chatserver` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..a7128eda --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,34 @@ +# Security Policy + +## Supported versions + +OwnCord is in alpha. Only the **latest release** receives security fixes. +There are no backports. + +| Version | Supported | +| ------- | --------- | +| Latest release (see [OwnCord-releases](https://github.com/J3vb/OwnCord-releases/releases)) | Yes | +| Anything older | No | + +## Reporting a vulnerability + +**Do not open a public issue for security bugs.** + +Report vulnerabilities privately via GitHub Security Advisories on the +[OwnCord-releases](https://github.com/J3vb/OwnCord-releases/security/advisories/new) +repository ("Report a vulnerability"). This channel works even while the +source repository is private. + +Please include: + +- Affected component (server, desktop client, admin panel, plugin host) +- Reproduction steps or a proof of concept +- The release version (or source snapshot) you tested against + +You will get an initial response within 7 days. Coordinated disclosure is +appreciated; fixes ship in the next release with credit unless you prefer +otherwise. + +## Hardening documentation + +Operator-facing hardening notes live in [docs/security.md](docs/security.md). diff --git a/Server/Makefile b/Server/Makefile index 788a2f46..54d2ca89 100644 --- a/Server/Makefile +++ b/Server/Makefile @@ -17,9 +17,14 @@ sqlc-install: sqlc-generate: sqlc generate +# Verify only db/dbgen: the committed db/pgdbgen files carry hand-added +# `//go:build postgres` tags that `sqlc generate` strips, so a pgdbgen diff +# is expected noise. pgdbgen is scheduled for removal with the Postgres +# scaffolding; restore it after generating so verify leaves a clean tree. sqlc-verify: sqlc generate - @git diff --exit-code db/dbgen db/pgdbgen || ( \ + @git checkout -- db/pgdbgen + @git diff --exit-code db/dbgen || ( \ echo "ERROR: generated sqlc output is stale. Run 'make sqlc-generate' and commit the result." ; \ exit 1 ; \ ) diff --git a/Server/api/router.go b/Server/api/router.go index 02f4732f..e6b50dfe 100644 --- a/Server/api/router.go +++ b/Server/api/router.go @@ -232,7 +232,7 @@ func NewRouter(cfg *config.Config, database *db.DB, ver string, logBuf *admin.Ri // Admin panel: static files + REST API (Phase 6). // Restrict /admin to configured CIDRs (default: private networks only). - u := updater.NewUpdater(ver, cfg.GitHub.Token, "J3vb", "OwnCord") + u := updater.NewUpdater(ver, cfg.GitHub.Token, cfg.GitHub.Owner, cfg.GitHub.Repo) adminHandler := admin.NewHandler(database, ver, hub, u, logBuf, cfg.Server.AllowedOrigins, svc.Permissions) r.Group(func(r chi.Router) { r.Use(AdminIPRestrict(cfg.Server.AdminAllowedCIDRs, cfg.Server.TrustedProxies)) diff --git a/Server/config/config.go b/Server/config/config.go index f60ec240..bcba0651 100644 --- a/Server/config/config.go +++ b/Server/config/config.go @@ -79,8 +79,14 @@ type PluginsConfig struct { } // GitHubConfig holds GitHub API settings for update checking. +// +// Owner/Repo point at the public releases repository. Server and client +// update checks fetch release assets from this repo, so it must stay +// publicly readable even when the source repository is private. type GitHubConfig struct { Token string `koanf:"token"` + Owner string `koanf:"owner"` + Repo string `koanf:"repo"` } // VoiceConfig holds LiveKit server connection and voice quality settings. @@ -188,7 +194,10 @@ func defaults() Config { LiveKitURL: "ws://localhost:7880", Quality: "medium", }, - GitHub: GitHubConfig{}, + GitHub: GitHubConfig{ + Owner: "J3vb", + Repo: "OwnCord-releases", + }, EventPersistence: EventPersistenceConfig{ Enabled: true, RetentionHours: 24, diff --git a/Server/db/dbgen/events.sql.go b/Server/db/dbgen/events.sql.go index fa6e0e29..06b346d8 100644 --- a/Server/db/dbgen/events.sql.go +++ b/Server/db/dbgen/events.sql.go @@ -61,14 +61,14 @@ func (q *Queries) GetEventsSince(ctx context.Context, arg GetEventsSinceParams) } const getMaxEventSeq = `-- name: GetMaxEventSeq :one -SELECT COALESCE(MAX(seq), 0) FROM events +SELECT CAST(COALESCE(MAX(seq), 0) AS INTEGER) AS max_seq FROM events ` -func (q *Queries) GetMaxEventSeq(ctx context.Context) (interface{}, error) { +func (q *Queries) GetMaxEventSeq(ctx context.Context) (int64, error) { row := q.db.QueryRowContext(ctx, getMaxEventSeq) - var coalesce interface{} - err := row.Scan(&coalesce) - return coalesce, err + var max_seq int64 + err := row.Scan(&max_seq) + return max_seq, err } const persistEvent = `-- name: PersistEvent :exec diff --git a/Server/db/dbgen/querier.go b/Server/db/dbgen/querier.go index 8cda0240..61bcfb89 100644 --- a/Server/db/dbgen/querier.go +++ b/Server/db/dbgen/querier.go @@ -61,7 +61,7 @@ type Querier interface { GetEventsSince(ctx context.Context, arg GetEventsSinceParams) ([]GetEventsSinceRow, error) GetInvite(ctx context.Context, code string) (GetInviteRow, error) GetLatestMessageID(ctx context.Context, channelID int64) (interface{}, error) - GetMaxEventSeq(ctx context.Context) (interface{}, error) + GetMaxEventSeq(ctx context.Context) (int64, error) GetMessage(ctx context.Context, id int64) (Message, error) GetMessagesByChannel(ctx context.Context, arg GetMessagesByChannelParams) ([]GetMessagesByChannelRow, error) GetMessagesByChannelBeforeCursor(ctx context.Context, arg GetMessagesByChannelBeforeCursorParams) ([]GetMessagesByChannelBeforeCursorRow, error) diff --git a/Server/go.mod b/Server/go.mod index 41932c58..237e64af 100644 --- a/Server/go.mod +++ b/Server/go.mod @@ -30,8 +30,8 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 go.uber.org/goleak v1.3.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.49.0 - golang.org/x/mod v0.34.0 + golang.org/x/crypto v0.51.0 + golang.org/x/mod v0.35.0 modernc.org/sqlite v1.48.0 nhooyr.io/websocket v1.8.17 ) @@ -57,7 +57,7 @@ require ( github.com/frostbyte73/core v0.1.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gammazero/deque v1.2.1 // indirect - github.com/go-jose/go-jose/v3 v3.0.4 // indirect + github.com/go-jose/go-jose/v3 v3.0.5 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect @@ -135,10 +135,10 @@ require ( go.uber.org/zap/exp v0.3.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect - golang.org/x/net v0.52.0 // indirect + golang.org/x/net v0.55.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.35.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect diff --git a/Server/go.sum b/Server/go.sum index bd430dd7..4bd3b46a 100644 --- a/Server/go.sum +++ b/Server/go.sum @@ -83,8 +83,8 @@ github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= -github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= -github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= +github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -356,21 +356,21 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -385,8 +385,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -398,16 +398,16 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= diff --git a/Server/plugin/host_storage.go b/Server/plugin/host_storage.go index 812bbed9..ccd15047 100644 --- a/Server/plugin/host_storage.go +++ b/Server/plugin/host_storage.go @@ -3,6 +3,7 @@ // Plugins get a per-plugin namespaced KV store backed by the PluginStore // rows in the events/plugin schema. Capacity caps and value-size caps are // enforced here so a misbehaving plugin can't fill the database. + package plugin import ( diff --git a/Server/plugin/host_ui.go b/Server/plugin/host_ui.go index a4045a0f..453c7b6a 100644 --- a/Server/plugin/host_ui.go +++ b/Server/plugin/host_ui.go @@ -3,6 +3,7 @@ // A plugin that declares the `ui` capability ships HTML/CSS/JS assets and a // list of tabs. The host serves those assets at /api/v1/plugins//ui/... // and the Solid.js client bridge renders each tab inside a sandboxed iframe. + package plugin import ( diff --git a/Server/plugin/loader.go b/Server/plugin/loader.go index 458660e0..848daf5e 100644 --- a/Server/plugin/loader.go +++ b/Server/plugin/loader.go @@ -13,6 +13,7 @@ // // Loader walks the directory, parses every plugin.json, and returns a slice // of foundPlugin records. The Registry then persists each into the store. + package plugin import ( diff --git a/Server/plugin/manifest_nottoml.go b/Server/plugin/manifest_nottoml.go index 9a6938c2..248ad5f6 100644 --- a/Server/plugin/manifest_nottoml.go +++ b/Server/plugin/manifest_nottoml.go @@ -1,6 +1,7 @@ //go:build !wazero // Default build stub — TOML manifest parsing is not compiled in without -tags wazero. + package plugin // tryLoadPluginTOML always reports "not present" in the default build so the diff --git a/Server/plugin/registry.go b/Server/plugin/registry.go index 3ecfafcb..a7ac9583 100644 --- a/Server/plugin/registry.go +++ b/Server/plugin/registry.go @@ -9,6 +9,7 @@ // directory and persists each manifest into the PluginStore so admins can see // what is "installed", but the .wasm files are NOT executed. Calling // Dispatch() in the default build returns ErrRuntimeUnavailable. + package plugin import ( diff --git a/Server/plugin/sandbox_default.go b/Server/plugin/sandbox_default.go index 8bb7a506..be4a0f67 100644 --- a/Server/plugin/sandbox_default.go +++ b/Server/plugin/sandbox_default.go @@ -3,6 +3,7 @@ // Default plugin runtime: no Wazero. Plugin manifests are still discovered, // persisted, and surfaced through the admin API, but `.wasm` modules are not // executed. To enable real WASM execution build with `-tags wazero`. + package plugin import ( diff --git a/Server/updater/server_update_public_key.txt b/Server/updater/server_update_public_key.txt index 65e9d690..e427ac06 100644 --- a/Server/updater/server_update_public_key.txt +++ b/Server/updater/server_update_public_key.txt @@ -1 +1 @@ -dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEFCQjA3OEZEOEVCRkY1RkEKUldUNjliK08vWGl3cStHamIrVHhNbWNLT3Bwb3ppeTIwdDBkQkFlaytHSWVqZkExSmFxRHZDVVoK \ No newline at end of file +dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFEMUUzM0FDNTBCMTFCQzIKUldUQ0c3RlFyRE1lSFUvK1M1Wk1PZFcwVmJMMnZLc0o3TThjSnNVZEY3NDFaVkxPekUyemJRVzAK \ No newline at end of file diff --git a/Server/updater/updater.go b/Server/updater/updater.go index 115eb205..032a75e7 100644 --- a/Server/updater/updater.go +++ b/Server/updater/updater.go @@ -564,7 +564,7 @@ func (u *Updater) verifySignatureReader(reader io.Reader, signatureText []byte, return fmt.Errorf("reading file for signature verification: %w", err) } - normalizedSig := []byte(strings.TrimSpace(string(signatureText))) + normalizedSig := normalizeSignatureText(signatureText) var parsedSig minisign.Signature if err := parsedSig.UnmarshalText(normalizedSig); err != nil { return fmt.Errorf("invalid update signature format: %w", err) @@ -576,6 +576,21 @@ func (u *Updater) verifySignatureReader(reader io.Reader, signatureText []byte, return nil } +// normalizeSignatureText returns the raw minisign signature document from +// signatureText. `tauri signer sign` emits .sig files that are base64-wrapped +// minisign documents (the same wrapping used for the pinned public key file); +// raw minisign documents pass through unchanged. +func normalizeSignatureText(signatureText []byte) []byte { + trimmed := []byte(strings.TrimSpace(string(signatureText))) + if bytes.HasPrefix(trimmed, []byte("untrusted comment:")) { + return trimmed + } + if decoded, err := base64.StdEncoding.DecodeString(string(trimmed)); err == nil { + return []byte(strings.TrimSpace(string(decoded))) + } + return trimmed +} + func (u *Updater) serverSignaturePublicKey() (minisign.PublicKey, error) { decoded, err := base64.StdEncoding.DecodeString(u.signingKeyText) if err != nil { diff --git a/Server/updater/updater_test.go b/Server/updater/updater_test.go index f8f546fb..b6d3ef9f 100644 --- a/Server/updater/updater_test.go +++ b/Server/updater/updater_test.go @@ -1130,3 +1130,38 @@ func (rt *rewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) newReq, _ := http.NewRequestWithContext(req.Context(), req.Method, newURL, req.Body) return http.DefaultTransport.RoundTrip(newReq) } + +// TestVerifySignature_TauriBase64WrappedFormat locks in support for the .sig +// format that `tauri signer sign` produces in the release pipeline: a +// base64-wrapped minisign document. Raw minisign documents must keep working. +func TestVerifySignature_TauriBase64WrappedFormat(t *testing.T) { + u, privateKey := newSignedTestUpdater(t, "", "1.0.0") + content := []byte("tauri wrapped signature test") + rawSig := signTestAsset(t, privateKey, content) + wrappedSig := []byte(base64.StdEncoding.EncodeToString(rawSig)) + + path := filepath.Join(t.TempDir(), "chatserver.exe") + if err := os.WriteFile(path, content, 0o600); err != nil { + t.Fatalf("writing test asset: %v", err) + } + + if err := u.VerifySignature(path, wrappedSig); err != nil { + t.Errorf("base64-wrapped tauri signature should verify: %v", err) + } + if err := u.VerifySignature(path, rawSig); err != nil { + t.Errorf("raw minisign signature should verify: %v", err) + } + // Valid base64 that decodes to garbage must fail cleanly, not verify. + garbage := []byte(base64.StdEncoding.EncodeToString([]byte("not a signature"))) + if err := u.VerifySignature(path, garbage); err == nil { + t.Error("garbage base64 signature should fail verification") + } + // Wrapped signature over different content must fail verification. + otherPath := filepath.Join(t.TempDir(), "other.bin") + if err := os.WriteFile(otherPath, []byte("tampered"), 0o600); err != nil { + t.Fatalf("writing tampered asset: %v", err) + } + if err := u.VerifySignature(otherPath, wrappedSig); err == nil { + t.Error("wrapped signature must not verify tampered content") + } +} diff --git a/Server/ws/dm_handlers_test.go b/Server/ws/dm_handlers_test.go index c9204855..cdbf6153 100644 --- a/Server/ws/dm_handlers_test.go +++ b/Server/ws/dm_handlers_test.go @@ -294,7 +294,7 @@ func TestDM_ChatEdit_ParticipantCanEdit(t *testing.T) { hub.HandleMessageForTest(cAlice, dmChatEditMsg(msgID, "edited")) time.Sleep(100 * time.Millisecond) - // Alice should receive the chat_edited broadcast (via broadcastToDMParticipants). + // Alice should receive the chat_edited broadcast (via the sequenced DM event path). msgs := dmDrainAll(sendAlice) edited := dmFindMsgType(msgs, "chat_edited") if edited == nil { diff --git a/Server/ws/emit_test.go b/Server/ws/emit_test.go index b255dfcf..a49087cb 100644 --- a/Server/ws/emit_test.go +++ b/Server/ws/emit_test.go @@ -34,6 +34,7 @@ func newEmitTestHub() *Hub { pubsub: NewPubSub(), replayBuf: NewEventRingBuffer(100), voiceKeyHolders: make(map[int64]int64), + topicLimiter: NewTopicRateLimiter(topicRateLimitPerSecond, time.Second), } } @@ -185,7 +186,7 @@ func TestEmitEvents_ExcludeSenderEvent(t *testing.T) { h.EmitEvents(events) - // broadcastExclude is synchronous — check immediately. + // broadcastExcludeLow is synchronous — check immediately. senderMsgs := drainChan(sendSender, 50*time.Millisecond) otherMsgs := drainChan(sendOther, 50*time.Millisecond) diff --git a/Server/ws/event_pruner.go b/Server/ws/event_pruner.go index 2ce27647..f9b62574 100644 --- a/Server/ws/event_pruner.go +++ b/Server/ws/event_pruner.go @@ -3,6 +3,7 @@ // StartEventPruner runs a background goroutine that deletes events older than // the configured retention window. It is the bounded-storage half of the // event persistence design: the persister appends, the pruner trims. + package ws import ( diff --git a/Server/ws/export_test.go b/Server/ws/export_test.go index e34f3dab..a8a10e9c 100644 --- a/Server/ws/export_test.go +++ b/Server/ws/export_test.go @@ -93,8 +93,9 @@ func (p *LiveKitProcess) SetProcessStoppedForTest() { // NewHubForTest creates a minimal Hub with no DB or limiter for webhook testing. func NewHubForTest() *Hub { return &Hub{ - clients: make(map[int64]*Client), - pubsub: NewPubSub(), + clients: make(map[int64]*Client), + pubsub: NewPubSub(), + topicLimiter: NewTopicRateLimiter(topicRateLimitPerSecond, time.Second), } } diff --git a/Server/ws/handlers.go b/Server/ws/handlers.go index 5c18fb65..05160eca 100644 --- a/Server/ws/handlers.go +++ b/Server/ws/handlers.go @@ -5,30 +5,11 @@ import ( "encoding/json" "fmt" "log/slog" - "time" - "github.com/microcosm-cc/bluemonday" "github.com/owncord/server/auth" "github.com/owncord/server/db" ) -// Rate limit windows. -const ( - chatRateLimit = 10 - chatWindow = time.Second - typingRateLimit = 1 - typingWindow = 3 * time.Second - presenceRateLimit = 1 - presenceWindow = 10 * time.Second - reactionRateLimit = 5 - reactionWindow = time.Second -) - -// maxMessageLen is the maximum allowed message length in runes (Unicode code points). -const maxMessageLen = 4000 - -var sanitizer = bluemonday.StrictPolicy() - // HandleMessageForTest dispatches a raw WebSocket message from client c. // Exported so ws_test package can invoke it directly without a real connection. func (h *Hub) HandleMessageForTest(c *Client, raw []byte) { @@ -226,21 +207,11 @@ func (h *Hub) requireChannelPerm(c *Client, channelID int64, perm int64, permLab return false } -// broadcastExclude sends a message to all clients in the sender's channel -// EXCEPT the sender. Unlike hub.BroadcastToChannel, messages sent via this -// function are NOT stored in the replay ring buffer — they are ephemeral. -// This is correct for typing indicators but would be incorrect for messages -// that should survive reconnection replay. -func (h *Hub) broadcastExclude(channelID, excludeUserID int64, msg []byte) { - if channelID == 0 { - h.pubsub.Publish(TopicGlobal, msg, excludeUserID) - return - } - h.pubsub.Publish(ChannelTopic(channelID), msg, excludeUserID) -} - -// broadcastExcludeLow is like broadcastExclude but at low priority. -// Used for typing indicators — dropped on overflow instead of disconnecting. +// broadcastExcludeLow sends a message at low priority to all clients in the +// sender's channel EXCEPT the sender. Messages sent via this function are NOT +// stored in the replay ring buffer — they are ephemeral. This is correct for +// typing indicators (dropped on overflow instead of disconnecting) but would +// be incorrect for messages that should survive reconnection replay. func (h *Hub) broadcastExcludeLow(channelID, excludeUserID int64, msg []byte) { if channelID == 0 { h.pubsub.PublishLow(TopicGlobal, msg, excludeUserID) @@ -249,31 +220,3 @@ func (h *Hub) broadcastExcludeLow(channelID, excludeUserID int64, msg []byte) { h.pubsub.PublishLow(ChannelTopic(channelID), msg, excludeUserID) } -// broadcastToDMParticipants sends a message to all participants of a DM channel -// while preserving DM semantics (delivery is by participant, not channel focus). -// Unlike broadcastToDMParticipantsExclude, this path is sequenced and replayable. -func (h *Hub) broadcastToDMParticipants(channelID int64, msg []byte) { - participantIDs, err := h.db.GetDMParticipantIDs(channelID) - if err != nil { - slog.Error("broadcastToDMParticipants GetDMParticipantIDs", "err", err, "channel_id", channelID) - return - } - h.sendSequencedToUsers(channelID, participantIDs, msg) -} - -// broadcastToDMParticipantsExclude sends a message to all participants of a DM -// channel EXCEPT the specified user. Used for ephemeral events like typing -// indicators where echoing back to the sender is undesirable. -func (h *Hub) broadcastToDMParticipantsExclude(channelID, excludeUserID int64, msg []byte) { - participantIDs, err := h.db.GetDMParticipantIDs(channelID) - if err != nil { - slog.Error("broadcastToDMParticipantsExclude GetDMParticipantIDs", "err", err, "channel_id", channelID) - return - } - for _, pid := range participantIDs { - if pid == excludeUserID { - continue - } - h.SendToUser(pid, msg) - } -} diff --git a/Server/ws/handlers_command.go b/Server/ws/handlers_command.go index 2d57239c..4a8d98f6 100644 --- a/Server/ws/handlers_command.go +++ b/Server/ws/handlers_command.go @@ -5,6 +5,7 @@ // plugin returns a Reply, it is sent only to the invoking client (ephemeral). // If the plugin returns a Broadcast string, it is broadcast to the channel // only after verifying the invoking client holds SEND_MESSAGES permission. + package ws import ( diff --git a/Server/ws/handlers_test.go b/Server/ws/handlers_test.go index 82688904..2726cb0e 100644 --- a/Server/ws/handlers_test.go +++ b/Server/ws/handlers_test.go @@ -1696,7 +1696,7 @@ func TestTyping_RateLimited_SilentlyDropped(t *testing.T) { // TestBroadcastExclude_SendsToOthersNotSelf verifies that broadcastExclude // delivers to all channel members except the excluded user. -// This is exercised indirectly via typing_start (which calls broadcastExclude). +// This is exercised indirectly via typing_start (which calls broadcastExcludeLow). func TestBroadcastExclude_SendsToOthersNotSelf(t *testing.T) { hub, database := newHandlerHub(t) chID := seedTestChannel(t, database, "excl-chan1") diff --git a/Server/ws/hub.go b/Server/ws/hub.go index 623ce27c..48deda38 100644 --- a/Server/ws/hub.go +++ b/Server/ws/hub.go @@ -555,26 +555,9 @@ func (h *Hub) BroadcastToAllLow(msg []byte) { h.pubsub.PublishGlobalLow(msg) } -// sendSequencedToUsers stamps msg with a monotonic seq, stores it in the replay -// buffer under channelID, and fanouts the wrapped payload to the provided users. -func (h *Hub) sendSequencedToUsers(channelID int64, userIDs []int64, msg []byte) { - h.seqMu.Lock() - defer h.seqMu.Unlock() - - seq := h.nextSeq() - wrapped := wrapWithSeq(msg, seq) - - // Store DM event for reconnect replay; filtering is channel-based and uses - // allowed channel IDs computed at auth time (including open DMs). - h.replayBuf.Push(seq, channelID, wrapped) - h.persistEvent(seq, channelID, wrapped) - - for _, userID := range userIDs { - h.SendToUser(userID, wrapped) - } -} - -// sendSequencedToUsersHigh is like sendSequencedToUsers but uses high-priority delivery. +// sendSequencedToUsersHigh stamps msg with a monotonic seq, stores it in the +// replay buffer under channelID, and fans the wrapped payload out to the +// provided users with high-priority delivery. func (h *Hub) sendSequencedToUsersHigh(channelID int64, userIDs []int64, msg []byte) { h.seqMu.Lock() defer h.seqMu.Unlock() diff --git a/Server/ws/hub_test.go b/Server/ws/hub_test.go index eca03f54..ecbbe1aa 100644 --- a/Server/ws/hub_test.go +++ b/Server/ws/hub_test.go @@ -489,7 +489,12 @@ func TestHub_ConcurrentRegisterUnregister(t *testing.T) { }(i) } wg.Wait() - time.Sleep(50 * time.Millisecond) + // The hub loop drains register/unregister asynchronously; poll instead of + // a fixed sleep, which flakes under -race on slow runners. + deadline := time.Now().Add(5 * time.Second) + for hub.ClientCount() != 0 && time.Now().Before(deadline) { + time.Sleep(10 * time.Millisecond) + } if hub.ClientCount() != 0 { t.Errorf("expected 0 clients after concurrent churn, got %d", hub.ClientCount()) } diff --git a/Server/ws/pubsub.go b/Server/ws/pubsub.go index 7891d229..c86a9ff4 100644 --- a/Server/ws/pubsub.go +++ b/Server/ws/pubsub.go @@ -2,7 +2,6 @@ package ws import ( "fmt" - "log/slog" "sync" ) @@ -217,16 +216,3 @@ func (ps *PubSub) TopicsForClient(userID int64) []Topic { } return result } - -// debugDump logs the current subscription state. For development use only. -func (ps *PubSub) debugDump() { - ps.mu.RLock() - defer ps.mu.RUnlock() - for topic, subs := range ps.topics { - ids := make([]int64, 0, len(subs)) - for uid := range subs { - ids = append(ids, uid) - } - slog.Debug("pubsub: topic", "topic", string(topic), "subscribers", ids) - } -} diff --git a/Server/ws/serve.go b/Server/ws/serve.go index fde604eb..f6fc588b 100644 --- a/Server/ws/serve.go +++ b/Server/ws/serve.go @@ -411,9 +411,13 @@ func readPump(ctx context.Context, conn *websocket.Conn, hub *Hub, c *Client) { voiceChID := c.getVoiceChID() replaced := hub.unregisterNow(c) if c.user != nil { - // Always clean up voice state — LeaveVoiceChannelIfMatch uses a - // join_token guard so it won't remove a replacement client's session. - if voiceChID != 0 { + // Clean up voice state only when this was the user's final + // connection. A replacement connection owns the (transferred) + // voice session, and the join_token guard cannot tell the + // difference — the transfer keeps the same joined_at — so + // cleaning here would delete the replacement's DB row whenever + // teardown snapshots voiceChID before the transfer zeroes it. + if voiceChID != 0 && !replaced { hub.handleVoiceLeave(ctx, c) } c.mu.Lock() diff --git a/docs/audit-2026-04-07.md b/docs/audit-2026-04-07.md index 200cec7a..83195976 100644 --- a/docs/audit-2026-04-07.md +++ b/docs/audit-2026-04-07.md @@ -5,6 +5,29 @@ --- +## Finding closure status (maintained; last updated 2026-07-18) + +Every CRITICAL/HIGH below must end with a closing commit link or an explicit +mitigation before the beta gate. Standing rule: any plugin CRITICAL still +OPEN at the beta gate → plugins ship default-disabled (they already default +to `plugins.enabled: false`). + +| # | Sev | Finding | Status | +|---|-----|---------|--------| +| 1 | CRITICAL | Plugin `invokeCommand` has no timeout | IN PROGRESS — CPU budget added on `fix/security-hardening-review`; regression fix (module bricking, W1-1) required before merge | +| 2 | CRITICAL | Plugin storage has no per-plugin key isolation | OPEN — verify/close in P3 | +| 3 | CRITICAL | Plugin per-command ACL missing (auto-registration) | OPEN — verify/close in P3 | +| 4 | CRITICAL | No rate limit on event delivery to plugins | OPEN — verify/close in P3 | +| 5 | CRITICAL | Plugin HTTP capability allows data exfiltration to allowlisted hosts | OPEN — partially mitigated by SSRF hardening + allowlist; document residual risk in P3 | +| 6 | HIGH | `Server/store/` untested | SUPERSEDED — `store/` package is being removed in P4 (single data layer); tests move to in-memory SQLite | +| 7 | HIGH | Client `src/lib`/`src/stores` <10% unit coverage | CLOSED since audit — large vitest suite exists (113 files); suite health tracked in P2 | +| 8 | HIGH | Unpinned critical npm packages | OPEN — review in P2 | +| 9 | MEDIUM | auth_handler bypasses service layer | OPEN — P4 consolidation candidate | +| 10 | MEDIUM | Audit-trail write failures silently ignored | OPEN — cheap fix, fold into P1 | +| 11 | MEDIUM | E2E not in CI / no .nvmrc | IN PROGRESS — nightly non-blocking e2e job planned in P2 | + +--- + ## Table of Contents 1. [Architecture](#1-architecture) 2. [Code Quality](#2-code-quality) diff --git a/docs/phase-a-status.md b/docs/phase-a-status.md deleted file mode 100644 index 2ed5edfb..00000000 --- a/docs/phase-a-status.md +++ /dev/null @@ -1,113 +0,0 @@ -# Phase A Implementation Status - -**Branch:** `claude/phase-a-foundation-plan-eUys1` - -This document tracks what shipped during Phase A (Foundation) and what is still pending. The original Phase A design brief (`phase-a-foundation.md`) was removed from the repo root on `dev`; this status doc preserves the actionable follow-up work. - -## Done - -- **Step 1 — Service layer + permission cache.** `Server/service/` contains 12 service files covering message, channel, permission, moderation, user, dm, block, invite, voice. Services depend on `store.Store`, not `*db.DB`. PermissionService maintains a per-user cache. REST and WS handlers (auth, channel, dm, profile, upload, chat, reaction, presence, voice) are migrated. Service tests live in `service/message_test.go` and `service/permission_test.go`. -- **Step 2 — sqlc adoption.** `Server/sqlc.yaml` configures both the SQLite engine (queries in `Server/db/queries/sqlite/`, generated output in `Server/db/dbgen/`) and the PostgreSQL engine (queries in `Server/db/queries/postgres/`, output in `Server/db/pgdbgen/`). sqlc v1.30.0 is pinned in `Server/sqlc.version`. `Server/Makefile` exposes `sqlc-install`, `sqlc-generate`, `sqlc-verify` targets covering both engines. 14 SQL query files per engine cover all DB domains; the SQLite `dbgen` package is committed, the PostgreSQL `pgdbgen` package will be generated on the next `make sqlc-generate` run. FTS search queries remain hand-written; transactional multi-step operations are unchanged. -- **Step 3 (partial) — Store interface + SQLiteStore + MemStore.** `Server/store/store.go` defines the full Store interface (12 sub-interfaces). `Server/store/sqlite.go` wraps `*db.DB`. `Server/store/memstore.go` provides an in-memory implementation used by service tests. -- **Step 4 — Logging consolidation.** OwnCord code uses `log/slog` exclusively. `go.uber.org/zap` and `rs/zerolog` appear only as transitive dependencies of livekit and are not imported by any OwnCord `.go` file. The plan's "pick one and find-and-replace" item is satisfied. -- **Step 5 — Pub/sub broadcast model.** `Server/ws/pubsub.go`, `topic_rate_limiter.go`, `ringbuffer.go`, and the three-tier priority queue (commit `7ccc93f`) implement the topic-based broadcast model with global rate limits, backpressure, and priority tiers. - -## Pending - -### Step 3 — PostgreSQL backend (final wiring) - -Scaffolding has landed on this branch: - -- `Server/migrations/postgres/001_initial_schema.sql` — consolidated postgres schema with `tsvector` + GIN full-text search, `CITEXT` usernames, native CHECK constraints replacing SQLite triggers, and seeded role/setting rows. -- `Server/migrations/postgres/migrations.go` — embed FS for the postgres migration set. -- `Server/db/queries/postgres/*.sql` — 14 query files (users, sessions, roles, invites, channels, messages, reactions, voice, attachments, admin, dm, blocks, lockouts, profile) translated to postgres dialect: `$N` placeholders, `NOW()`/native `TIMESTAMPTZ`, `TRUE`/`FALSE` for BOOLEAN columns, `ON CONFLICT ... DO UPDATE` for upserts, `RETURNING id` for creates (postgres has no `LastInsertId`), `:execrows` for mutations that need rows-affected. -- `Server/sqlc.yaml` — second engine entry (`engine: "postgresql"`, `sql_package: "pgx/v5"`) generating into the `pgdbgen` package under `Server/db/pgdbgen/`. -- `Server/Makefile` — `sqlc-generate` and `sqlc-verify` cover both engines. -- `Server/store/postgres.go` — `PostgresStore` type behind the `//go:build postgres` tag, implementing the full `store.Store` interface. Connection lifecycle (`OpenPostgres`, `Close`, `SQLDb`, `WithTx`) is fully implemented using `database/sql` + the pgx stdlib driver. Query methods are stubs that return `ErrPostgresNotImplemented`, waiting for the `pgdbgen` querier to land so they can be replaced with wrappers around generated code. The build tag keeps pgx out of the default build; operators who want to enable postgres run `go get github.com/jackc/pgx/v5 && go build -tags postgres ./...`. -- `Server/config/config.go` — `DatabaseConfig` extended with `Type`, `Host`, `Port`, `User`, `Password`, `Name`, `SSLMode`, `MaxConns`. Defaults set to `type: "sqlite"` so existing operators are unaffected. -- `Server/main.go` — explicit dispatch on `database.type`. Selecting `postgres` produces a clear startup error pointing at the remaining work, instead of silently falling through to sqlite. - -What still needs to land for postgres to be runnable: - -1. Add `github.com/jackc/pgx/v5` to `go.mod` and run `go mod tidy`. This happens naturally the first time an operator runs `go get github.com/jackc/pgx/v5` — the package only needs to be in the module graph when building with `-tags postgres`. -2. Run `make sqlc-generate` to produce `Server/db/pgdbgen/` from the committed query files. This requires either network access to download sqlc or a pre-installed `sqlc` binary at v1.30.0. -3. Replace the stub query methods in `Server/store/postgres.go` with real implementations that wrap the generated `pgdbgen` querier. The connection lifecycle and interface assertion are already in place; each stub carries the same method signature as the sqlite version, so the migration is mechanical. Where the schema produces different Go types than sqlite (e.g. `bool` vs `int64` for boolean columns, `time.Time` vs `string` for timestamps), the store wrapper performs the conversion so services see a uniform API. -4. Refactor `main.go` and `Server/api/router.go` to thread `store.Store` through the boundary instead of `*db.DB`. Today the router constructs `dbstore.NewSQLiteStore(database)` inline, so services are store-aware but everything else still consumes `*db.DB` directly. The store-everywhere migration is the gating step before either backend can be swapped at runtime. -5. FTS dispatch in `MessageStore.SearchMessages` / `SearchMessagesInChannels` — sqlite uses `MATCH` against the FTS5 virtual table, postgres uses `@@ to_tsquery(...)` against the `messages.fts` tsvector column. Both remain hand-written (outside the sqlc-generated set) for their respective backends. -6. CI matrix to run the test suite against both backends. - -### One-way sqlite → postgres data migration - -Operators who start a community on the default sqlite backend and later outgrow it must be able to carry their history over. The migration must be forward-only (once postgres is selected, the server stays on postgres unless the operator deliberately wipes the postgres database and re-initialises), both to keep the contract simple and to avoid the support burden of "I reverted to sqlite yesterday and now my data is gone". - -Proposed design: - -- A one-shot CLI flag, e.g. `chatserver --migrate-to-postgres`, that exits after completion. No continuous sync. -- Pre-flight checks: `database.type` in config is already `postgres`; postgres connection succeeds; the target postgres database contains no rows in `users` (or equivalent sentinel) — if it does, refuse to run, printing the exact row count so the operator can confirm they meant to target this database. -- Open sqlite read-only alongside postgres. Begin one postgres transaction for the entire migration so partial failures roll back cleanly. -- Copy rows in foreign-key order (`roles` → `users` → `channels` → `channel_overrides` → `messages` → `reactions` → `attachments` → `invites` → `sessions` → `voice_states` → `dm_participants` → `dm_open_state` → `read_states` → `audit_log` → `user_blocks` → `rate_lockouts` → `settings` → `emoji` → `sounds`). Disable the `trg_messages_fts_update` trigger for the bulk copy and re-populate `messages.fts` in a single `UPDATE messages SET fts = to_tsvector('simple', content)` at the end, so FTS doesn't fire per row. -- Convert types at the boundary: sqlite RFC3339 strings → postgres `TIMESTAMPTZ` via `time.Parse`, sqlite `INTEGER` boolean (0/1) → postgres `BOOLEAN`. Drop the preserved `id` values straight through since both schemas are `BIGINT`-compatible. -- After copying, reset every postgres sequence with `SELECT setval('_id_seq', COALESCE(MAX(id), 1)) FROM
` so subsequent inserts don't collide with migrated IDs. -- Write a marker row into `settings`: `migrated_from_sqlite = `. On subsequent startups with `type: postgres`, the marker's presence (or simply the presence of a non-empty `users` table) is the signal that the migration has already run and must not be repeated. -- Uploaded files on disk (`data/uploads/`) are not touched — the migration only moves database rows. Attachments reference filenames, not blobs, so the filesystem copy is a separate `cp -a data/uploads old-host:/new-path` step the operator does out-of-band. -- **No reverse migration.** The code does not include a postgres → sqlite path. If an operator wants to return to sqlite, they stop the server, restore a sqlite backup, edit `config.yaml` back to `type: sqlite`, and start fresh. This is deliberately inconvenient. - -This feature is gated on PostgresStore's query methods being real (pending item 3). The migration implementation itself lives in a new `Server/migrate/sqlite_to_postgres.go` file and is invoked from `main.go` before the normal startup path. - -## Verification status - -`go build ./...` and `go test ./...` were not run in the session that produced this branch — the development environment lacked network access to fetch the Go 1.25.0 toolchain required by `go.mod`. Manual audit of the touched Go files (the `go-build-check` skill) found no compile errors. The branch should be verified locally by the next operator before merge. - -## Actionable TODOs - -Scannable checklist extracted from the prose above. Work top-to-bottom; most items unblock the ones below them. - -### Verification (do first, cheap) - -- [ ] Run `go build ./...` in `Server/` to confirm the branch compiles -- [ ] Run `go test ./...` in `Server/` and fix any regressions -- [ ] Run `go build -tags postgres ./...` after adding pgx (below) to verify `store/postgres.go` compiles under the postgres tag - -### Postgres enablement (Step 3 final wiring) - -- [ ] `cd Server && go get github.com/jackc/pgx/v5 && go mod tidy` — add pgx to the module graph -- [ ] `make sqlc-generate` — produce `Server/db/pgdbgen/` from the committed `db/queries/postgres/*.sql` -- [ ] Commit the generated `Server/db/pgdbgen/` output -- [ ] Replace each stub in `Server/store/postgres.go` with a real implementation wrapping `pgdbgen`; keep the `//go:build postgres` tag. Expect per-method type conversion (sqlite `string` timestamps vs postgres `time.Time`, sqlite `int64` bools vs postgres `bool`) -- [ ] Implement FTS dispatch: `SearchMessages` and `SearchMessagesInChannels` currently use sqlite `MATCH`; add a postgres branch using `@@ to_tsquery(...)` against `messages.fts` -- [ ] Remove `ErrPostgresNotImplemented` once every method is real - -### Store-everywhere boundary refactor (unblocks runtime backend selection) - -- [ ] Audit every `*db.DB` parameter in `Server/api/` and `Server/ws/`; replace with `store.Store` where possible, using `store.Store.SQLDb()` at the leaves that truly need `*sql.DB` (backups, migrations) -- [ ] Update `Server/api/router.go`'s `NewRouter` signature: take `store.Store` instead of `*db.DB` -- [ ] Update every `Mount*Routes(r, database, …)` call site to pass the store -- [ ] Update `ws.NewHub` and `auth.NewPersistentRateLimiter` to accept `store.Store` -- [ ] Update `Server/admin/handler.go` (`admin.NewHandler`) the same way -- [ ] Update `Server/main.go` to construct the store via a factory, `store.Open(&cfg.Database)`, that dispatches on `cfg.Database.Type` -- [ ] Delete the explicit postgres error in `main.go`'s switch — selecting postgres should Just Work once PostgresStore is real -- [ ] Fix all handler tests that construct handlers from `*db.DB` — they now take `store.Store` (use `store.MemStore` for unit tests) - -### Data migration (one-way sqlite → postgres) - -- [ ] Create `Server/migrate/sqlite_to_postgres.go` -- [ ] Add `--migrate-to-postgres` CLI flag to `main.go`, parsed before normal startup -- [ ] Pre-flight: verify `database.type == postgres`, verify target postgres `users` table is empty, refuse with exact row count if not -- [ ] Implement bulk copy in FK order: `roles → users → channels → channel_overrides → messages → reactions → attachments → invites → sessions → voice_states → dm_participants → dm_open_state → read_states → audit_log → user_blocks → rate_lockouts → settings → emoji → sounds` -- [ ] Disable `trg_messages_fts_update` during bulk message copy; repopulate `messages.fts` in one statement at the end -- [ ] Convert types at the boundary: `time.Parse(time.RFC3339, …)` for timestamps, `int != 0` for booleans -- [ ] Reset every `
_id_seq` with `SELECT setval(…, COALESCE(MAX(id), 1))` after copy -- [ ] Write marker row: `INSERT INTO settings (key, value) VALUES ('migrated_from_sqlite', NOW()::text)` -- [ ] Wrap everything in one postgres transaction so partial failures roll back -- [ ] **Do NOT implement a reverse migration** — documented as deliberately unavailable - -### CI - -- [ ] Add a postgres job to `.github/workflows/*.yml` that spins up a postgres service container and runs `go test -tags postgres ./...` -- [ ] Keep the existing sqlite job unchanged -- [ ] Add a `sqlc-verify` job that runs `make sqlc-verify` to catch stale generated code - -### Hygiene (optional, do whenever) - -- [ ] Expand `Server/service/*_test.go` coverage — currently only `message_test.go` and `permission_test.go` exist; add channel, dm, voice, invite, moderation tests using `store.MemStore` -- [ ] Split the postgres migration file if it grows beyond ~300 lines; for now it's consolidated intentionally diff --git a/docs/plans/slash-commands.md b/docs/plans/slash-commands.md index 003cacaf..0fa564d2 100644 --- a/docs/plans/slash-commands.md +++ b/docs/plans/slash-commands.md @@ -2,7 +2,7 @@ **Status:** design only, not implemented **Owner:** TBD -**Tracks:** PHASE_D_PARITY_TODO.md item #1 +**Tracks:** deferred feature backlog (post-beta; see CHANGELOG "Deferred work") **Estimated effort:** 1–2 weeks of focused work ## Why diff --git a/docs/server-configuration.md b/docs/server-configuration.md index 2f278f02..8f10d8ea 100644 --- a/docs/server-configuration.md +++ b/docs/server-configuration.md @@ -66,6 +66,8 @@ Configuration is loaded in three layers (later layers override earlier ones): | Key | Type | Default | Description | |-----|------|---------|-------------| | `github.token` | string | `""` | Optional GitHub API token for higher rate limits on update checks (5000 req/hr vs 60) | +| `github.owner` | string | `"J3vb"` | Owner of the GitHub repository server and client updates are fetched from | +| `github.repo` | string | `"OwnCord-releases"` | Public releases repository. Must stay publicly readable — both the server self-update and the client auto-update chain fetch release assets from it | ### Event Persistence (`event_persistence`) @@ -178,6 +180,8 @@ voice: github: token: "" # optional GitHub PAT for update check rate limits + owner: "J3vb" # update source repo owner + repo: "OwnCord-releases" # public releases repo (binaries + source snapshots) # Event persistence (tiered reconnect replay) event_persistence: