This commit is contained in:
J3vb
2026-04-07 10:13:47 +02:00
parent ba66629077
commit 65002d0d7f
8 changed files with 558 additions and 18 deletions
@@ -25,7 +25,5 @@ export function PluginContainer(props: PluginContainerProps): JSX.Element {
dispose?.();
});
return (
<div class="plugin-container" data-plugin-id={props.binding.pluginId} ref={host} />
);
return <div class="plugin-container" data-plugin-id={props.binding.pluginId} ref={host} />;
}
@@ -19,7 +19,9 @@ directory, and existing leaf components are ported one PR at a time.
handle has the same `{ destroy }` shape that the rest of the codebase uses.
4. **Test the pipeline.** New components get a `*.test.tsx` next to them
using `@solidjs/testing-library`. The tests run under the existing Vitest
configuration without any extra setup.
configuration without any extra setup. Do **not** call `cleanup()` manually
in test files — `tests/setup-solid.ts` registers `afterEach(cleanup)` globally
via Vitest's `setupFiles` (added in T-500).
5. **Delete vanilla DOM code.** Once a component is fully migrated, remove
the old factory function and update its callers to import from
`@components/solid/...`.
@@ -38,6 +40,14 @@ directory, and existing leaf components are ported one PR at a time.
custom store via `fromStore` instead.
- Touching framework-agnostic code (`lib/ws.ts`, `lib/dispatcher.ts`,
`lib/livekitSession.ts`, `lib/api.ts`). These never need to know about Solid.
- Using `innerHTML`, `insertAdjacentHTML`, or `dangerouslySetInnerHTML` in any
Solid component. JSX interpolation (`{value}`) auto-escapes user content and
is the correct approach. Components rendering user-supplied text (e.g. message
attachments, toast messages) must never bypass this escaping.
- Placing `.tsx` files outside `src/components/solid/`. The `vite-plugin-solid`
transform is scoped to this directory in both `vite.config.ts` and
`vitest.config.ts`. A `.tsx` file elsewhere will fail with cryptic JSX parse
errors at build time.
## Existing components
@@ -0,0 +1,75 @@
/**
* Phase B Step 6 — mountSolid adapter lifecycle smoke tests (T-500).
*
* Verifies that the mountSolid adapter in @lib/solidMount correctly inserts a
* Solid reactive root into the DOM and that destroy() disposes the root and
* removes the host element, preventing memory leaks across test suites.
*
* These tests complement Badge.test.tsx (which validates the Solid rendering
* pipeline end-to-end) by proving the *adapter* contract used by vanilla-DOM
* container components.
*/
import { describe, it, expect } from "vitest";
import { mountSolid } from "@lib/solidMount";
describe("mountSolid adapter", () => {
it("appends a data-solid-root host element to the parent on mount", () => {
const parent = document.createElement("div");
document.body.appendChild(parent);
const handle = mountSolid(() => <span>test</span>, parent);
expect(parent.querySelector("[data-solid-root]")).not.toBeNull();
expect(handle.el.dataset.solidRoot).toBe("true");
expect(handle.el.parentElement).toBe(parent);
handle.destroy();
parent.remove();
});
it("removes the host element from the DOM after destroy()", () => {
const parent = document.createElement("div");
document.body.appendChild(parent);
const handle = mountSolid(() => <span>cleanup-check</span>, parent);
expect(parent.children).toHaveLength(1);
handle.destroy();
expect(parent.querySelector("[data-solid-root]")).toBeNull();
expect(parent.children).toHaveLength(0);
parent.remove();
});
it("supports multiple independent mounts under the same parent", () => {
const parent = document.createElement("div");
document.body.appendChild(parent);
const a = mountSolid(() => <span>a</span>, parent);
const b = mountSolid(() => <span>b</span>, parent);
expect(parent.querySelectorAll("[data-solid-root]")).toHaveLength(2);
a.destroy();
expect(parent.querySelectorAll("[data-solid-root]")).toHaveLength(1);
b.destroy();
expect(parent.querySelectorAll("[data-solid-root]")).toHaveLength(0);
parent.remove();
});
it("exposes the host element via handle.el", () => {
const parent = document.createElement("div");
document.body.appendChild(parent);
const handle = mountSolid(() => <span>el-check</span>, parent);
expect(handle.el).toBeInstanceOf(HTMLElement);
expect(handle.el).toBe(parent.firstElementChild);
handle.destroy();
parent.remove();
});
});
+22
View File
@@ -0,0 +1,22 @@
/**
* Vitest global setup for Solid.js component tests (T-500).
*
* Loaded via `test.setupFiles` in vitest.config.ts so every test suite
* automatically gets Solid's afterEach cleanup without having to import
* or call it manually.
*
* Add future Solid testing helpers here (custom matchers, query extensions,
* aria-query configuration, etc.). Do NOT import application code here —
* this file executes once before every test suite, including non-Solid suites.
*
* Security note: Solid JSX auto-escapes interpolated values ({expr}), so
* user-controlled strings passed through JSX are safe. Never use innerHTML,
* insertAdjacentHTML, or dangerouslySetInnerHTML in Solid components.
*/
import { cleanup } from "@solidjs/testing-library";
import { afterEach } from "vitest";
// Register cleanup after every test so Solid reactive roots are disposed
// and host DOM nodes are removed. Without this, roots accumulate across tests
// and can cause state leakage between test cases.
afterEach(cleanup);
+8 -6
View File
@@ -9,7 +9,9 @@ export default defineConfig({
// the angle brackets.
plugins: [
solidPlugin({
include: ["src/components/solid/**/*.{ts,tsx,js,jsx}"],
// Phase B Step 6: cover both the component directory and test files
// under tests/ that use JSX (e.g. setup-solid.test.tsx, T-500).
include: ["src/components/solid/**/*.{ts,tsx,js,jsx}", "tests/**/*.tsx"],
}),
],
resolve: {
@@ -27,11 +29,11 @@ export default defineConfig({
// `src/**/*.test.{ts,tsx}` files are picked up. The latter is required
// for Phase B Step 6 Solid components, whose tests live alongside the
// component file (see src/components/solid/README.md).
include: [
"tests/**/*.test.ts",
"src/**/*.test.ts",
"src/**/*.test.tsx",
],
// T-500: also pick up .tsx test files under tests/ (e.g. setup-solid.test.tsx).
include: ["tests/**/*.test.ts", "tests/**/*.test.tsx", "src/**/*.test.ts", "src/**/*.test.tsx"],
// T-500: global Solid.js test setup — registers afterEach(cleanup) so
// individual *.test.tsx files do not need to call cleanup() manually.
setupFiles: ["./tests/setup-solid.ts"],
coverage: {
provider: "v8",
include: ["src/**/*.ts"],
+24 -8
View File
@@ -10,17 +10,33 @@ the registry persists it into the plugins table without executing the .wasm.
## Building the WASM
The .wasm binary is intentionally NOT checked in. Build it locally with TinyGo
or any other WASM toolchain that emits a module exporting `command_dispatch`,
`on_event`, and `_start`:
`main.go` in this directory implements the full plugin ABI
(`allocate`, `deallocate`, `list_commands`, `command_dispatch`, `on_event`).
The pre-built `hello.wasm` (925 KiB) is checked in, but you can rebuild it:
```sh
# TinyGo example (writes hello.wasm into this directory)
tinygo build -o hello.wasm -target wasi ./main.go
### Prerequisites
| Tool | Version | Notes |
|------|---------|-------|
| TinyGo | 0.40.1 | Supports Go 1.191.25 only |
| Go | 1.25.x | TinyGo 0.40.1 rejects Go 1.26+ |
| wasm-opt | Binaryen 129 | Required by TinyGo for the `wasi` target |
On Windows, extract TinyGo to e.g. `D:\Local-Lab\Coding\Software\tinygo` and
add `<tinygo>\bin` plus `<binaryen>\bin` to `PATH`. Then point TinyGo at the
compatible Go SDK:
```pwsh
$env:GOROOT = "$env:USERPROFILE\sdk\go1.25.3" # installed via: go install golang.org/dl/go1.25.3@latest
$env:PATH = "$env:GOROOT\bin;$env:PATH"
```
A trivial main.go that satisfies the host API is sketched in
`Server/plugin/sandbox_wazero.go`'s docstring.
### Build command
```sh
# Run from this directory (Server/plugin/examples/hello/)
tinygo build -o hello.wasm -target wasi ./main.go
```
## Tests
+399
View File
@@ -0,0 +1,399 @@
# OwnCord — Comprehensive Project Audit
**Date:** 2026-04-07
**Branch:** claude/plan-phases-b-c-bGpoS
**Audited by:** 6 parallel Claude agents across 8 dimensions
---
## Table of Contents
1. [Architecture](#1-architecture)
2. [Code Quality](#2-code-quality)
3. [Security](#3-security)
4. [Dependencies & Supply Chain](#4-dependencies--supply-chain)
5. [Test Coverage & Quality](#5-test-coverage--quality)
6. [CI/CD & DevEx](#6-cicd--devex)
7. [Observability](#7-observability)
8. [Plugin System Governance](#8-plugin-system-governance)
9. [Prioritized Top-10 Action List](#9-prioritized-top-10-action-list)
---
## 1. Architecture
### Layer Map
| Layer | Location | Responsibility |
|-------|----------|---------------|
| **Client UI** | `Client/tauri-client/src/components/` | SolidJS UI components, settings, voice |
| **Client Pages** | `Client/tauri-client/src/pages/` | MainPage, ConnectPage entry points |
| **Client Stores** | `Client/tauri-client/src/stores/` | Reactive state (auth, channels, messages, voice, dm, ui, roles) |
| **Client Lib** | `Client/tauri-client/src/lib/` | API client, WebSocket client, dispatcher, LiveKit session, theme |
| **Tauri Rust** | `Client/tauri-client/src-tauri/` | WS proxy, credential manager, PTT, update notifications |
| **Server API** | `Server/api/` | REST handlers (auth, channels, messages, DMs, invites, uploads) |
| **Server WS** | `Server/ws/` | Real-time hub, event persistence, command dispatch, LiveKit integration |
| **Server Service** | `Server/service/` | Domain business logic (Message, Channel, Permission, User, DM, Invite, Block, Voice) |
| **Server Store** | `Server/store/` | Data access layer (SQLiteStore, PostgresStore stub, MemStore) |
| **Server DB** | `Server/db/` | SQL schema, migrations, sqlc-generated queries |
| **Server Plugin** | `Server/plugin/` | WASM runtime (Wazero), loader, manifest, registry, capability-scoped host APIs |
| **Server Admin** | `Server/admin/` | Admin UI, backup management, log streaming |
| **Server Auth** | `Server/auth/` | Authentication, session management, rate limiting, TOTP |
### Communication Patterns
| Channel | Usage |
|---------|-------|
| **REST API** | Auth, channel CRUD, file uploads, initial data fetch — uses Tauri HTTP plugin for self-signed cert support |
| **WebSocket** | Real-time events (messages, presence, typing, voice) — single connection per client, envelope format `{type, id, payload}` |
| **Tauri IPC** | `ws_connect/send/disconnect`, credential manager, PTT, update checks — desktop-only features |
| **LiveKit** | HTTP voice token acquisition, reverse-proxy WebSocket signaling at `/livekit/*`, webhook for participant events |
### Dependency Direction
Layering is **healthy and mostly respected**:
-`api/ → service/ → store/` holds
-`ws/` does not import `api/` (no circular deps)
- ✅ Plugin system imports service layer cleanly
- ✅ Auth middleware isolated; not interspersed with handlers
### Anti-patterns
| SEVERITY | File | Finding |
|----------|------|---------|
| MEDIUM | `Server/api/auth_handler.go` | Auth handler queries DB directly, bypassing service layer — inconsistent with `channel_handler.go` pattern |
| MEDIUM | `Client/tauri-client/src/lib/dispatcher.ts` | 84 registered listeners creates implicit coupling; hard to trace data flow |
| MEDIUM | `Client/tauri-client/src/lib/livekitSession.ts` (1710 lines) | Monolith mixing LiveKit SDK, audio pipeline, diagnostics — should decompose |
| MEDIUM | `Client/tauri-client/src/pages/MainPage.ts` (554 lines) | Mixes routing, layout, store subscriptions, cleanup — violates single responsibility |
| MEDIUM | `Server/service/message.go` (715 lines) | Combines send/search/delete/edit/fetch — split into focused services |
| LOW | `Client/tauri-client/src/main.ts` (538 lines) | Entry point handles init, error handlers, theme, health checks, connection orchestration |
| LOW | `Server/api/router.go` (394 lines) | Mixes middleware setup, route registration, and business logic wiring |
| LOW | `Server/admin/` | Admin package operates directly on DB; not using service layer — may diverge from REST/WS semantics |
| LOW | PostgreSQL backend | Fully scaffolded but query methods stubbed; migration path undocumented |
---
## 2. Code Quality
### Go — Error Handling
| SEVERITY | File:Line | Finding |
|----------|-----------|---------|
| MEDIUM | `Server/admin/logstream.go:398-426` | SSE handler ignores write errors with `_, _ = fmt.Fprintf()` — client disconnections not detectable |
| MEDIUM | `Server/admin/handlers_backup.go:55,139` | `database.LogAudit()` errors silently discarded with `_ =` — audit trail may silently fail |
| MEDIUM | `Server/admin/handlers_users.go` (5 instances) | `LogAudit()` errors discarded — affects compliance/security audit trail |
| LOW | `Server/updater/updater.go` (13 instances) | Excessive `_ =` error suppression; no logging fallback |
| LOW | `Server/db/db.go` (9 instances) | Unchecked errors in setup/teardown paths |
### Go — Interface Design
-**Strong**: Store package has clear interface hierarchy (`Store → MessageStore, ChannelStore, UserStore, SessionStore, RoleStore`) with proper concrete implementations
- ⚠️ **Concern**: Service layer handlers receive concrete `*db.DB` or `*Hub` rather than small interfaces — reduces testability
### Go — Large Files (>800 lines)
| File | Lines |
|------|-------|
| `Server/ws/hub.go` | 919 — mixes client management, event persistence, plugin integration, voice key-holding |
| `Server/store/memstore.go` | 766 |
| `Server/service/message.go` | 715 |
| `Server/store/postgres.go` | 833 — largely stubbed scaffolding |
### Go — Security-Relevant TODOs
| File | TODO |
|------|------|
| `Server/ws/command.go` | Validate attachment URL scheme (require HTTPS) — security gap |
| `Server/ws/registry.go` | Stack trace may contain sensitive function arguments |
| `Server/ws/voice_leave.go` | Propagate context through `livekit.RemoveParticipant()` call |
| `Server/ws/voice_e2ee.go` | Re-check key-holder status inside `sendToUserIfInVoiceChannel` |
| `Server/store/sqlite.go` | Incomplete transaction-scoped store wrapper |
### TypeScript — Type Safety
| SEVERITY | File:Line | Finding |
|----------|-----------|---------|
| MEDIUM | Multiple components | ~43 event handlers with untyped `(e)` parameters — should be typed as `MouseEvent`, `DragEvent`, etc. |
| LOW | `src/lib/audioPipeline.ts:75` | Single `as any` cast (justified by eslint-disable comment; acceptable) |
### TypeScript — Large Components
| File | Lines | Action |
|------|-------|--------|
| `src/components/settings/AccountTab.ts` | 845 | **Extract**: password-section.ts, totp-section.ts, status-selector.ts |
| `src/components/EmojiPicker.ts` | 665 | Extract emoji data file |
| `src/components/MessageList.ts` | 662 | Extract height-calc module |
| `src/components/MessageInput.ts` | 599 | Reasonable given feature density |
### TypeScript — Error Handling Gaps
| SEVERITY | File | Finding |
|----------|------|---------|
| MEDIUM | `src/components/InviteManager.ts:96,151` | `.then()` chains without `.catch()` — silent rejection |
| MEDIUM | `src/components/message-input/file-upload.ts:57` | `.then()` without catch — corrupt file failures silent |
| MEDIUM | `src/components/message-list/attachments.ts:341` | `void fetchImageAsDataUrl().then()` — image load failures not surfaced |
---
## 3. Security
### Overall Posture: **GOOD** (no critical issues in core app security)
### Secrets & Configuration
-`.env.example` uses placeholder values; file gitignored
- ✅ No hardcoded secrets found in source, Tauri config, or configs
- ✅ LiveKit credentials require 32+ character minimum
### Input Validation
- ✅ Username validation with `auth.ValidateUsername()` + bluemonday HTML sanitization
- ✅ File uploads use `filepath.Base()` to prevent path traversal
- ✅ Plugin uploads: magic-byte ZIP validation, 16 MiB hard cap, decompression bomb protection, symlink rejection
- MEDIUM | `Server/api/auth_handler.go:151` | Username validation doesn't explicitly check for Unicode control characters, RTL overrides, or zero-width chars — potential homograph attack vector
### SQL Injection
- ✅ All standard queries use parameterized statements
-`SearchMessagesInChannels` uses `fmt.Sprintf` only for `?` placeholder structure (not user data) — **safe by design**
- ✅ FTS query sanitization: whitelist-only (letters, digits, spaces, hyphens), max 200 runes
### Authentication & Authorization
-`AuthMiddleware`: Bearer token extraction → hash → session lookup → expiry → ban check → context injection
-`RequirePermission`: Admin bit (0x40000000) bypasses; 403 on insufficient permissions
- ✅ WebSocket in-band auth within 10-second deadline; re-validated every 10 messages
- ✅ Fail-closed: nil role = zero access
- ✅ Permission-filtered reconnect replay (prevents data leakage after permission changes)
### Rate Limiting
- ✅ Per-endpoint configurable rate limits with 429 + Retry-After
- ✅ Login, register, TOTP verification all rate-limited
- ✅ Sensitive endpoints (account deletion, TOTP management) rate-limited
- ✅ IP extraction validates trusted CIDR proxies — prevents rate-limit bypass via spoofed headers
### Tauri Security
- ✅ CSP enforced; `withGlobalTauri: false` (Tauri API not exposed on `window`)
- ✅ Capabilities-based permission model (Tauri v2)
- ✅ No devtools in production build
### WebSocket Security
- ✅ Auth deadline prevents resource exhaustion from slow clients
- ✅ Priority-based send channels (high/normal/low) prevent low-priority spam blocking critical messages
- ✅ Read limit enforced (`wsReadLimitBytes = config.MaxMessageBytes`)
### Observations (not blocking)
| SEVERITY | Area | Finding |
|----------|------|---------|
| MEDIUM | Username validation | Add explicit Unicode control character / zero-width char rejection |
| MEDIUM | TOTP secrets | Confirm AES-256 encryption of stored secrets with per-record salt |
| MEDIUM | LiveKit webhook | Confirm webhook signature validation cannot be bypassed with malformed JWT |
| LOW | Session tokens | Confirm cryptographically secure RNG and per-session salt for token hashing |
---
## 4. Dependencies & Supply Chain
### Overall Posture: **MODERATE RISK** (Go excellent, npm floating)
### Go Modules — 30 direct deps, ALL exact-pinned ✅
Notable: `golang.org/x/crypto v0.49.0`, `github.com/corazawaf/coraza/v3 v3.6.0`, `github.com/tetratelabs/wazero v1.11.0`, `modernc.org/sqlite v1.48.0`
### npm — 13 production deps, ALL floating (^) ⚠️
| Risk | Package | Version |
|------|---------|---------|
| HIGH | `@tauri-apps/plugin-updater` | `^2.10.0` — update mechanism, should pin |
| HIGH | `@tauri-apps/plugin-global-shortcut` | `^2` — any 2.x allowed |
| HIGH | `@tauri-apps/plugin-notification` | `^2` — any 2.x allowed |
| HIGH | `@tauri-apps/plugin-store` | `^2` — any 2.x allowed |
| MEDIUM | `livekit-client` | `^2.18.0` |
| MEDIUM | `solid-js` | `^1.9.3` |
| LOW | `zod` | `^4.3.6` |
**Mitigating factor:** `package-lock.json` (8,832 lines) is committed and locks transitive deps.
### Lockfile Status
| Ecosystem | Status |
|-----------|--------|
| Go | ✅ `go.sum` committed, 455 entries |
| npm (client) | ✅ `package-lock.json` committed, 8,832 lines |
| npm (root) | ✅ `package-lock.json` committed, 492 lines |
### License Compliance
- OwnCord: **AGPLv3**
- All direct dependencies: Apache 2.0 or MIT — **no copyleft incompatibilities**
### Known Vulnerabilities
- No CRITICAL CVEs in direct dependencies as of Feb 2025
- `golang.org/x/crypto v0.49.0` — verify no disclosed CVEs since mid-2024 release
- Run `npm audit` and `govulncheck` periodically
---
## 5. Test Coverage & Quality
### Go — Package Coverage
| Package | Test Files | Status |
|---------|-----------|--------|
| `api/` | 23 | ✅ Heavy behavioral coverage |
| `ws/` | 32 | ✅ Connect/disconnect, commands, auth, deadlock |
| `auth/` | 8 | ✅ Core flows, TOTP, helpers |
| `db/` | 18 | ✅ Operations and migrations |
| `admin/` | 11 | ✅ Good coverage |
| `plugin/` | 6 | ✅ Loading, sandbox, manifest |
| `permissions/` | 3 | ✅ |
| `service/` | 2 | ⚠️ Only message + permission |
| `**store/**` | **0** | ❌ **CRITICAL GAP — data persistence untested** |
| `syncutil/` | 0 | ⚠️ Concurrency utils untested |
| `scripts/` | 0 | Low priority (CLI tool) |
### Go — Test Quality: GOOD
- Behavioral tests using in-memory SQLite (`:memory:`) with full schema
- Table-driven patterns used throughout
- `goleak.VerifyTestMain()` in `auth/` — goroutine leak detection
- Deadlock detection tests via `go test -tags deadlock`
### Go — Critical Coverage Gaps
| SEVERITY | Gap |
|----------|-----|
| HIGH | `Server/store/` — data persistence layer has zero test coverage |
| HIGH | No PostgreSQL integration tests — all use in-memory SQLite; schema drift not caught |
| MEDIUM | `Server/syncutil/` — concurrency utilities untested |
| MEDIUM | Migration safety not validated across versions |
### TypeScript — Test Files
- **1 component unit test**: `src/components/solid/Badge.test.tsx`
- **44 E2E specs**: Playwright (20 browser-mode, 22 native-mode + helpers)
- **1 jsdom smoke test**: `tests/browser/smoke.test.ts`
### TypeScript — E2E Coverage: EXCELLENT
Auth flow, channels, messages, DMs, health/reconnect, UI overlays, voice controls, theme persistence — all covered in 44 Playwright specs.
### TypeScript — Unit Coverage: MINIMAL (<10%)
| SEVERITY | Gap |
|----------|-----|
| HIGH | No tests for `src/lib/` utilities (api.ts, ws.ts, dispatcher.ts, livekitSession.ts) |
| HIGH | No tests for `src/stores/` (messages, channels, voice, auth) |
| MEDIUM | 7 files explicitly excluded from coverage thresholds: `main.ts`, `updater.ts`, `credentials.ts`, etc. |
| MEDIUM | 70% coverage threshold configured in vitest but unit tests too sparse to enforce |
---
## 6. CI/CD & DevEx
### Pipeline Gates
| Job | OS | Gates |
|-----|----|-------|
| `server-build-test` | Windows + Ubuntu | ✅ Build (4 tag variants), Tests (race + deadlock), golangci-lint, govulncheck |
| `client-check` | Windows | ✅ TypeScript check, ESLint, Oxlint, Prettier, Vitest, npm audit, Knip |
| `server-docker-build` | Ubuntu | ✅ Docker build verification |
| `tauri-build` | Windows + Ubuntu + Ubuntu-ARM | ✅ Rust lint, Rust audit, full Tauri build — PR to main only |
### Linting Enforcement
- **Go**: golangci-lint v2.11.3 — hard fail; rules: gocritic, gosec, errcheck, bodyclose, contextcheck, staticcheck
- **TypeScript**: ESLint with `no-floating-promises: error`, `no-unused-vars: error`; Prettier format check; Oxlint
- **Enforcement**: Hard fail for all linting/formatting in CI ✅
### Gaps
| SEVERITY | Finding |
|----------|---------|
| MEDIUM | E2E tests not in main CI — only run on PRs to main, not on merge. Code could merge without E2E gate |
| MEDIUM | No branch protection rules visible in repo config |
| LOW | No `.nvmrc` / `.node-version` file — Node 20 only enforced in CI, not locally |
| LOW | Rust toolchain pinned to `stable` (not specific version) — builds can drift |
| LOW | `Knip` runs with `|| true` (advisory only, not blocking) |
### Build Reproducibility
- ✅ Go: `go 1.25.0` pinned in `go.mod`; `go.sum` committed
- ✅ Node: Version 20 pinned in CI; `npm ci` used (clean install)
- ✅ 4 Go build tag variants tested: default, otel, wazero, otel+wazero
- ✅ Lockfiles current (go.sum: Apr 6, package-lock: Apr 7)
---
## 7. Observability
### Logging: STRONG ✅
- **Library**: `log/slog` (stdlib, Go 1.21) with multi-handler — stdout (INFO+) + ring buffer (DEBUG+)
- **Structured**: All logs use key-value pairs (`slog.Info("msg", "key", val)`)
- **Context present**: actor_id, user_id, channel_id, operation names logged consistently
- **Admin log viewer**: 2000-entry ring buffer in `Server/admin/logstream.go`
- **Minor**: `Server/scripts/seed.go` uses `fmt.Printf` (non-production CLI, acceptable)
### Metrics & Tracing: PRESENT (build-tag gated)
- OpenTelemetry SDK with no-op default; full telemetry via `-tags otel`
- Prometheus metrics export at `/metrics`
- Request ID propagation via `X-Request-Id` header ✅
- Health endpoints: `GET /health`, `GET /api/v1/health`, `GET /api/v1/livekit/health`
### Error Surfacing: GOOD ✅
- Errors logged server-side with `slog.Error` before HTTP response
- Consistent HTTP status codes (400 validation, 503 feature unavailable, 500 internal)
- `writeErr()` / `writeJSON()` helper in admin ensure consistent error response shape
### Client-Side: LIMITED ⚠️
| SEVERITY | Finding |
|----------|---------|
| MEDIUM | No global error boundary component — unhandled promise rejections may fail silently |
| LOW | No crash/error reporting integration (Sentry, etc.) |
---
## 8. Plugin System Governance
### Plugin Architecture
- WASM isolation via Wazero (behind `-tags wazero` build flag)
- Manifest-declared capabilities: `commands`, `events`, `storage`, `http`, `ui`
- Memory capped per runtime (default 64 MiB); CPU budget configurable
### CRITICAL Issues
| SEVERITY | File:Line | Finding |
|----------|-----------|---------|
| **CRITICAL** | `Server/plugin/sandbox_wazero.go:162,211` | `invokeCommand` has **no timeout** — a looping plugin hangs the goroutine indefinitely |
| **CRITICAL** | `Server/plugin/host_storage.go` | Storage capability has **no key isolation** — plugin can read/write ANY key in the plugin store, not just its own |
| **CRITICAL** | `Server/plugin/host_http.go:162-240` | HTTP capability allows plugins to **exfiltrate data** by POSTing captured payload to any allowlisted host |
| **CRITICAL** | `Server/plugin/registry.go:129-133` | All commands auto-registered if manifest declares `commands` capability — **no per-command ACL** |
| **CRITICAL** | `Server/plugin/host_events.go` | **No rate limit** on event delivery to plugins — malicious plugin could slow server by processing events slowly |
| MEDIUM | `Server/plugin/host_http.go:64-96` | DNS rebinding TOCTOU between `rejectPrivateAddrs` check and TCP dial — not fully atomic |
| LOW | `Server/plugin/registry.go` | Default (non-Wazero) stub build doesn't emit WARN if plugins are configured but stub is running |
| LOW | `Server/plugin/loader.go` | Plugin discovery walks filesystem on every server start — no manifest caching |
### Strengths
- ✅ Per-plugin WASM module isolation (memory, execution)
- ✅ Private IP SSRF protection via `rejectPrivateAddrs()`
- ✅ Symlink rejection in ZIP extraction (zip-slip protection)
- ✅ Decompression bomb protection on plugin upload
- ✅ Graceful lifecycle (disable/uninstall/close) with module cleanup
- ✅ Plugin registry tracks all loaded instances with mutex-protected map
---
## 9. Prioritized Top-10 Action List
| Priority | SEVERITY | Area | Action | File |
|----------|----------|------|--------|------|
| 1 | CRITICAL | Plugin | Add `context.WithTimeout(ctx, cfg.CPUBudgetMs)` around `invokeCommand` to prevent infinite hangs | `Server/plugin/sandbox_wazero.go:162` |
| 2 | CRITICAL | Plugin | Namespace storage keys by plugin ID: `fmt.Sprintf("%d/%s", pluginID, key)` in `PluginGet/Set/Delete` | `Server/plugin/host_storage.go` |
| 3 | CRITICAL | Plugin | Add per-command ACL in registry — check `plugin.Manifest.AllowedCommands` or require explicit command declaration | `Server/plugin/registry.go:129` |
| 4 | CRITICAL | Plugin | Rate-limit event delivery to plugins (token bucket per plugin) to prevent event flooding DoS | `Server/plugin/host_events.go` |
| 5 | HIGH | Tests | Add `store/` package tests — data persistence is completely untested; cover SQLiteStore CRUD and query methods | `Server/store/` |
| 6 | HIGH | Tests | Add TypeScript unit tests for `src/lib/` (api.ts, ws.ts, dispatcher.ts) and `src/stores/` — currently <10% unit coverage | `Client/tauri-client/src/lib/` |
| 7 | HIGH | Deps | Pin critical npm packages to exact versions: `@tauri-apps/plugin-updater`, `vite`, `@tauri-apps/plugin-store` | `Client/tauri-client/package.json` |
| 8 | MEDIUM | Architecture | Refactor `Server/api/auth_handler.go` to route through `AuthService` — currently bypasses service layer | `Server/api/auth_handler.go` |
| 9 | MEDIUM | Code Quality | Silence audit trail loss — wrap `database.LogAudit()` calls with `slog.Error` on failure instead of `_ =` | `Server/admin/handlers_*.go` |
| 10 | MEDIUM | CI/CD | Add E2E gate to `ci.yml` on push to main (not only on PR) + add `.nvmrc` for local Node version enforcement | `.github/workflows/ci.yml` |
### Bonus (quick wins)
- Add `.catch()` handlers to `.then()` chains in `InviteManager.ts`, `file-upload.ts`, `attachments.ts`
- Add `context.WithTimeout` guard in `Server/ws/voice_leave.go` (`livekit.RemoveParticipant` has `//nolint:contextcheck`)
- Add HTTPS-only URL validation in `Server/ws/command.go` (existing TODO)
- Add frontend error boundary component wrapping `MainPage`
- Add Unicode/invisible-character validation to username registration
---
*Report generated from 6 parallel audit agents. All findings are read-only analysis; no code was modified.*
+18
View File
@@ -83,6 +83,24 @@ How to set up the development environment and contribute to OwnCord.
| `npm run format:check` | Prettier check only (no writes) |
| `npm run knip` | Dead code and unused export detection |
## Plugin Development
Plugins are WASM modules loaded at runtime when the server is built with `-tags wazero`.
See `Server/plugin/examples/hello/README.md` for the full plugin ABI and build instructions.
**Toolchain requirements for building `.wasm` plugins with TinyGo:**
| Tool | Version | Notes |
|------|---------|-------|
| TinyGo | 0.40.1 | Supports Go 1.191.25 only |
| Go SDK | 1.25.x | Install alongside the system Go via `go install golang.org/dl/go1.25.3@latest && go1.25.3 download` |
| wasm-opt | Binaryen 129 | Required by TinyGo for the `wasi` target; download from Binaryen GitHub releases |
Any WASM toolchain (Rust/`wasm32-wasi`, AssemblyScript, etc.) that exports the five ABI
functions is equally valid — TinyGo is just the example toolchain used by `examples/hello/`.
---
## Active Branches
- `main` -- stable releases