Merge remote-tracking branch 'origin/main' into fix/security-hardening-review

This commit is contained in:
J3vb
2026-07-19 08:09:56 +02:00
49 changed files with 622 additions and 535 deletions
+9
View File
@@ -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
+32 -2
View File
@@ -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
+61 -7
View File
@@ -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[@]}"
+6
View File
@@ -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
+16 -15
View File
@@ -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).
+211 -197
View File
@@ -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": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "owncord-client",
"private": true,
"version": "1.0.0",
"version": "1.1.0-alpha.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -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",
]
+63 -28
View File
@@ -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"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "owncord-client"
version = "1.0.0"
version = "1.1.0-alpha.1"
edition = "2021"
description = "OwnCord Desktop Client"
@@ -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();
}
// ---------------------------------------------------------------------------
+1 -1
View File
@@ -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)
@@ -167,7 +167,7 @@ fn tofu_check<R: Runtime>(
// 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<R: Runtime>(
// 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}"));
@@ -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": {
+4 -12
View File
@@ -10,9 +10,7 @@
import { invoke } from "@tauri-apps/api/core";
import * as types from "./types";
export async function startLivekitProxy(
params: types.StartLivekitProxyParams,
): Promise<number> {
export async function startLivekitProxy(params: types.StartLivekitProxyParams): Promise<number> {
return invoke("start_livekit_proxy", params);
}
@@ -52,9 +50,7 @@ export async function pttListenForKey(): Promise<number> {
return invoke("ptt_listen_for_key");
}
export async function saveCredential(
params: types.SaveCredentialParams,
): Promise<void> {
export async function saveCredential(params: types.SaveCredentialParams): Promise<void> {
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<void> {
export async function deleteCredential(params: types.DeleteCredentialParams): Promise<void> {
return invoke("delete_credential", params);
}
@@ -92,9 +86,7 @@ export async function getSettings(): Promise<types.Value> {
return invoke("get_settings");
}
export async function saveSettings(
params: types.SaveSettingsParams,
): Promise<void> {
export async function saveSettings(params: types.SaveSettingsParams): Promise<void> {
return invoke("save_settings", params);
}
+4 -10
View File
@@ -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<UnlistenFn> {
export async function onStatusChange(handler: (payload: string) => void): Promise<UnlistenFn> {
return listen<string>("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<UnlistenFn> {
export async function onWsState(handler: (payload: string) => void): Promise<UnlistenFn> {
return listen<string>("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<UnlistenFn> {
export async function onCertTofu(handler: (payload: types.Value) => void): Promise<UnlistenFn> {
return listen<types.Value>("cert-tofu", (event) => {
handler(event.payload);
});
+1 -1
View File
@@ -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`
+34
View File
@@ -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).
+6 -1
View File
@@ -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 ; \
)
+1 -1
View File
@@ -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))
+10 -1
View File
@@ -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,
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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)
+6 -6
View File
@@ -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
+14 -14
View File
@@ -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=
+1
View File
@@ -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 (
+1
View File
@@ -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/<name>/ui/...
// and the Solid.js client bridge renders each tab inside a sandboxed iframe.
package plugin
import (
+1
View File
@@ -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 (
+1
View File
@@ -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
+1
View File
@@ -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 (
+1
View File
@@ -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 (
+1 -1
View File
@@ -1 +1 @@
dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEFCQjA3OEZEOEVCRkY1RkEKUldUNjliK08vWGl3cStHamIrVHhNbWNLT3Bwb3ppeTIwdDBkQkFlaytHSWVqZkExSmFxRHZDVVoK
dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFEMUUzM0FDNTBCMTFCQzIKUldUQ0c3RlFyRE1lSFUvK1M1Wk1PZFcwVmJMMnZLc0o3TThjSnNVZEY3NDFaVkxPekUyemJRVzAK
+16 -1
View File
@@ -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 {
+35
View File
@@ -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")
}
}
+1 -1
View File
@@ -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 {
+2 -1
View File
@@ -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)
+1
View File
@@ -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 (
+3 -2
View File
@@ -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),
}
}
+5 -62
View File
@@ -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)
}
}
+1
View File
@@ -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 (
+1 -1
View File
@@ -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")
+3 -20
View File
@@ -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()
+6 -1
View File
@@ -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())
}
-14
View File
@@ -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)
}
}
+7 -3
View File
@@ -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()
+23
View File
@@ -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)
-113
View File
@@ -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('<table>_id_seq', COALESCE(MAX(id), 1)) FROM <table>` so subsequent inserts don't collide with migrated IDs.
- Write a marker row into `settings`: `migrated_from_sqlite = <RFC3339 timestamp>`. 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 `<table>_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
+1 -1
View File
@@ -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:** 12 weeks of focused work
## Why
+4
View File
@@ -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: