Commit Graph
230 Commits
Author SHA1 Message Date
J3vb d0da33f1da Merge remote-tracking branch 'origin/main' into fix/security-hardening-review 2026-07-19 08:09:56 +02:00
J3vbandClaude Fable 5 183ef09f7c chore(release): rotate server update signing key
New key ID 1D1E33AC50B11BC2 replaces ABB078FD8EBFF5FA, whose private half had
no recoverable backup. Rotation is free at this exact moment: the v1.0.0
fleet predates signature verification entirely and no verifying (alpha.1+)
install exists yet. Both SERVER_UPDATE_SIGNING_* secrets updated in lockstep;
private key + password backed up locally for the maintainer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:07:30 +02:00
J3vbandClaude Fable 5 01ec825e51 test(updater): lock tauri base64-wrapped signature round-trip
Covers the release pipeline's actual .sig format (base64-wrapped minisign),
the raw minisign format, garbage base64 rejection, and tamper rejection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:07:30 +02:00
J3vbandClaude Fable 5 668e1bde83 fix(updater): accept tauri base64-wrapped minisign signatures
`tauri signer sign` emits .sig files that are base64-wrapped minisign
documents — the same wrapping already handled for the pinned public key —
but verifySignatureReader fed the wrapped text straight to
minisign.Signature.UnmarshalText, so every real release signature failed to
parse ("minisign: invalid signature"). Unwrap base64 when the text is not a
raw minisign document; raw documents (and the test fixtures) pass through
unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 08:07:30 +02:00
J3vbandClaude Fable 5 20e9e2f6a5 fix(ws): don't clean voice state when a replacement connection exists
readPump teardown always ran handleVoiceLeave, trusting the joined_at
guard in LeaveVoiceChannelIfMatch to protect replacement sessions. On
reconnect the voice session TRANSFERS to the replacement client with
the same joined_at, so the guard cannot tell the two apart: whenever
teardown snapshotted voiceChID before the transfer zeroed it, the old
connection deleted the replacement's voice_state row (flaked on the
Windows CI runner as TestServeWS_Reconnect_PreservesVoiceState).

Gate voice cleanup on !replaced — the same condition the
presence-offline broadcast four lines down already uses. A genuinely
final disconnect behaves exactly as before, and a stale row from a
crashed replacement is still swept by the fresh-connect cleanup.

Also deflake TestHub_ConcurrentRegisterUnregister: poll for quiescence
with a deadline instead of a fixed 50ms sleep that loses to the -race
scheduler on slow runners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:50:38 +02:00
J3vbandClaude Fable 5 1fbedb404c fix(lint): delete dead ws broadcast cluster; demote phase-header comments
golangci-lint had been failing invisibly behind the earlier CI gate
failures. Default-build lint is now clean:

- Delete the unused pre-topic-limiter rate-limit constants, the unused
  bluemonday sanitizer, and the dead broadcast variants superseded by
  their Low/High counterparts (broadcastExclude,
  broadcastToDMParticipants(+Exclude), sendSequencedToUsers,
  PubSub.debugDump). Test references were comments only; updated to
  name the live variants.
- Separate 'Phase X Step Y' file headers from the package clause with a
  blank line so staticcheck ST1000 no longer reads them as malformed
  package comments (proper package docs exist in hub.go/manifest.go).
- Add .gitattributes normalizing line endings to LF on checkout —
  the Windows CI runner materialized CRLF, which made every
  prettier-formatted file fail the format gate.

Known remainder (pre-existing, out of P0 scope): golangci-lint with
-tags wazero reports 3 gosec + 2 staticcheck and -tags otel 1+1; CI
lints the default build. Tracked for the P1 plugin pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:55:22 +02:00
J3vbandClaude Fable 5 77ec4f1466 chore(db): commit stale sqlc output; make sqlc-verify honest
db/queries/sqlite/events.sql was changed (typed CAST for
GetMaxEventSeq) without re-running generation; commit the regenerated
dbgen (interface{} -> int64, no callers depend on the old signature).

Scope sqlc-verify's diff to db/dbgen: regeneration strips the
hand-added //go:build postgres tags in db/pgdbgen, so verifying that
tree can never pass; pgdbgen is scheduled for removal with the
Postgres scaffolding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:45:30 +02:00
J3vbandClaude Fable 5 522003ebe3 fix(ws): wire topicLimiter into hand-rolled test hubs
newEmitTestHub and NewHubForTest built raw Hub literals without the
topic rate limiter, so deliverBroadcast panicked on a nil receiver and
TestEmitEvents_ChannelEvent_CallsBroadcastToChannel could never pass.
CI never surfaced it because the pipeline died at govulncheck first.
Wire the limiter exactly as NewHub does; no production nil-guard, since
a nil limiter in production would silently disable rate limiting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:45:30 +02:00
J3vbandClaude Fable 5 c7b49ee1fc fix(deps): patch reachable vulnerabilities blocking CI
Server: golang.org/x/net 0.52.0->0.55.0 (GO-2026-5026, reachable via
autocert.HostWhitelist) and go-jose/v3 3.0.4->3.0.5 (GO-2026-4945,
reachable via the LiveKit SDK). govulncheck now passes clean; full
test suite and the otel/wazero build-tag matrix verified locally.

Client: npm audit fix (semver-compatible only, 44 packages) — npm audit
--audit-level=high now exits clean. vitest moved 3.2.4->3.2.7.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:45:30 +02:00
J3vbandClaude Fable 5 e25018d6d2 feat(release): decouple distribution from the source repo
Publish every release to the public OwnCord-releases repo with a full
source snapshot (AGPL section 6 for binary recipients), and make the
updater's repo coordinates configurable (github.owner/github.repo),
defaulting to OwnCord-releases. Both the server self-update and the
/client-update chain follow the new default, so deployed servers keep
updating after the source repo goes private.

The publish step fails closed: a private source repo with no
RELEASES_REPO_TOKEN aborts the release instead of silently shipping
binaries with no public source or update feed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:17:29 +02:00
J3vbandClaude Opus 4.8 7b178ff30b fix(security): harden server against verified code-review findings
Applies fixes for 20 adversarially-verified findings from a whole-codebase
security review (server side). All Go build-tag variants build, `go vet` is
clean, and the suite passes (the sole failing test, ws TestEmitEvents, is a
pre-existing nil-harness failure unrelated to these changes).

High severity:
- auth: close TOCTOU in TOTP verify rate-limit by recording each attempt
  atomically up-front (was Check-then-Allow), restoring the per-user
  brute-force cap.
- plugin: enforce the CPU/time budget on every WASM guest call via a
  WithTimeout context (WithCloseOnContextDone interrupts runaways); the
  configured budget was previously parsed but never applied.
- api/waf: inspect request bodies for chunked (ContentLength==-1) requests
  so the SQLi/XSS/RCE body rules can no longer be bypassed.
- ws: rate-limit voice_join/voice_leave and voice_e2ee announce/offer, which
  fan out to every participant and could force mass disconnects.

Medium severity:
- api: run bcrypt on the unknown-user login path (no || short-circuit) to
  remove the timing-based username-enumeration oracle.
- ws: verify LiveKit webhooks via the SDK receiver so the signature is bound
  to the body hash (kills forgery/replay).
- authz: require READ_MESSAGES for reactions and for plugin-command
  broadcasts; route the latter through RequireChannelAccess.
- api: cache the client-update signature fetch and rate-limit the endpoint.
- service: propagate DeleteOtherSessions failure from ChangePassword instead
  of silently reporting success.
- api: trust the rightmost non-proxy X-Forwarded-For entry, not the
  client-controllable leftmost one.
- plugin: route auto-registered commands through the conflict-checked
  RegisterCommand; pin the DNS-validated IP for host_http dials
  (DNS-rebinding TOCTOU).
- api: mark access-controlled downloads private/no-cache + Vary: Origin.

Low severity:
- auth: fail closed when a fully-shaped TOTP ciphertext fails GCM auth
  (was returning the ciphertext as plaintext).
- api: apply the livekit-proxy path allowlist to WebSocket upgrades too.
- service: verify attachment ownership before linking (IDOR).
- admin: bound the bootstrap setup invite (5 uses / 24h); re-verify the
  update binary hash immediately before rename+spawn (TOCTOU).
- service: require BanMembers + role hierarchy for moderation ban/unban.

chore: stop tracking the stray Server/owncord-server.exe build artifact.

Test infra: add uploader_id to the hand-rolled ws test attachment schemas and
make MemStore.GetAttachmentByID a no-op lookup, matching production/DB behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:08:54 +02:00
J3vb 65002d0d7f h 2026-04-07 10:13:47 +02:00
J3vb ba66629077 fix(ci): resolve all golangci-lint and ESLint failures on PR #1132
Client:
- ci.yml: patch auto-generated events.ts to rename Event -> _Event
  so @typescript-eslint/no-unused-vars does not fail on generated code

Server (gocritic):
- service/channel.go: rangeValCopy (line 74), elseif (line 174)
- service/message.go: rangeValCopy (line 648), elseif (lines 438, 496)
- ws/emit.go: caseOrder — ChannelEvent before BroadcastAllEvent
- ws/handlers_command_test.go: stringXbytes — use bytes.Equal
- ws/pubsub_test.go: stringXbytes — use bytes.Equal

Server (nilerr):
- service/channel.go: nolint:nilerr for intentional silent drops in HandleTyping

Server (gosec):
- plugin/host_ui.go: G703 nolint — path already sanitized above
- plugin/registry.go: G302 — tighten plugin file permissions 0o640 → 0o600
- ws/hub.go, ws/serve.go: G115 nolint — seq counters never reach MaxInt64

Server (unused/unparam):
- plugin/registry.go: nolint:unused for wazero-tagged module field
- telemetry/metrics.go: nolint:unused for otel-tagged resetAppMetricsForInit
- ws/command.go: nolint:unparam for map entries whose error return is always nil

Server (staticcheck ST1000/ST1020):
- Add blank line before package declarations in phase-comment files
  (api/plugins_handler.go, plugin/host_{commands,events,http}.go,
   telemetry/metrics.go, telemetry/middleware.go,
   telemetry/telemetry_default.go, ws/event_persister.go)
- Fix GlobalTracer/GlobalMeter doc comments to start with function name
2026-04-07 10:10:38 +02:00
J3vb 81c121c2bd fix(server): resolve golangci-lint contextcheck, errcheck, gocritic findings
contextcheck: add ctx context.Context as first param to ListVisibleChannels,
BlockUser, CreateDM, CreateInvite, UpdateProfile, SendMessage; pass r.Context()
from HTTP handlers and ctx from WS handler; replace context.Background() in
telemetry spans with the propagated ctx.

errcheck: suppress justified Close() errors — defer func(){ _ = rows.Close() }()
in sqlite_events.go (idiomatic; rows.Err() checked), _ = resp.Body.Close() in
host_http.go (body fully consumed), _ = f.Close() in host_ui.go (read-only fd).

gocritic/rangeValCopy: rewrite for _, ch := range all (line 60) to indexed loop
in ChannelService.ListVisibleChannels to avoid 144-byte per-iteration copy.
2026-04-07 09:10:14 +02:00
J3vb f06ddcde27 Merge branch 'claude/plan-phases-b-c-bGpoS' of https://github.com/J3vb/OwnCord into claude/plan-phases-b-c-bGpoS 2026-04-07 08:26:25 +02:00
Claude 1c476ccb58 merge: reconcile sister branch claude/plan-phases-b-c-bGpoS
Resolves the parallel Phase B/C work that landed on the sister branch
while this branch was in review. Both branches independently implemented
real OTel + Wazero runtimes; this merge keeps the best of each.

Conflict resolution
- Server/plugin/sandbox_wazero.go: rewritten as a hybrid. Keeps the
  HEAD lifecycle (eager `platformInit` with WASI preview-1, explicit
  `platformDeactivate` per-instance, runtime closed via Registry.Close)
  AND adopts the sister branch's richer artefacts:
    * `WithMemoryLimitPages` actually enforces `cfg.MaxMemoryMB`,
    * the JSON-over-linear-memory ABI
      (`allocate` / `command_dispatch(ptr,len) → (ptr,len)` /
      `deallocate`),
    * `listExportedCommands` auto-binds commands the plugin exports
      via `list_commands` at activation time (capability-gated).
- Server/plugin/registry.go: kept the HEAD `activate()` snapshot
  pattern (read `runtimePlatform` under RLock, pass into
  `activateWithRuntime` as a parameter) so a concurrent Close can't
  race the wazero call. Sister branch's LoadAll stale-staging cleanup
  and UninstallPlugin on-disk dir removal came in via auto-merge.
- Server/telemetry/telemetry_otel.go: kept the HEAD implementation
  (race-fixed AppMetrics rebind, uint64 overflow guard, idempotent
  shutdown, trace-provider cleanup on prom failure) and wired in the
  sister branch's `OTLPInsecure` config field for plaintext gRPC opt-in.
- Server/go.mod: accepted sister branch's `BurntSushi/toml v1.6.0`
  for the new TOML manifest support.
- Client/tauri-client/vitest.config.ts: union of both globs
  (`tests/**/*.test.ts`, `src/**/*.test.ts`, `src/**/*.test.tsx`).
- PHASE_BC_LOCAL_TODO.md: combined the two checkbox histories;
  TOML manifest, hello.wasm fixture, and OTLPInsecure are all marked
  done now.

Sister branch additions accepted via auto-merge
- Server/plugin/examples/hello/{hello.wasm,main.go}: precompiled 925
  KiB TinyGo plugin with the full ABI (allocate, deallocate,
  list_commands, command_dispatch, on_event).
- Server/plugin/manifest_{toml,nottoml}.go: TOML manifest parser
  behind the wazero build tag, JSON fallback elsewhere.
- Server/plugin/loader.go: prefers `plugin.toml`, falls back to
  `plugin.json`.
- Server/api/plugins_handler.go: structured error responses + slog.
- Server/main.go, Server/config/config.go: OTLPInsecure plumbing,
  defaults polish.
- docs/{contributing.md,server-configuration.md}: documentation
  updates.

Test status
- `go build` passes on default, -tags otel, -tags wazero, and
  -tags otel,wazero.
- `go vet` passes on every tag combination.
- `go test ./...` passes on default and on -tags otel,wazero.
- Client: `npx tsc --noEmit` clean; vitest 3188/3188 across 112 files.

https://claude.ai/code/session_01AZni6CDSQeu67WSWY1YCDX
2026-04-07 06:23:09 +00:00
J3vb 6a48665cb0 Merge branch 'claude/plan-phases-b-c-bGpoS' of https://github.com/J3vb/OwnCord into claude/plan-phases-b-c-bGpoS 2026-04-07 08:00:20 +02:00
J3vb 670e6e0323 security: harden plugin sandbox stdin and opaque internal errors
- sandbox_wazero.go: explicitly discard plugin stdin via WithStdin to
  prevent WASM modules from reading the server process's stdin fd
- channel_handler.go: replace ErrInternal message pass-through with
  generic 'an internal error occurred' — full error stays server-side
  in slog.Error only
- invite_handler_test.go: update assertions to expect generic message
2026-04-07 07:59:10 +02:00
copilot-swe-agent[bot]andJ3vb 1f4a89ced1 audit: fix all C/H/M/L severity findings from Phase B+C PR review
Agent-Logs-Url: https://github.com/J3vb/OwnCord/sessions/2d415fd9-6138-495b-ac29-c54d08ae4ce4

Co-authored-by: J3vb <192430104+J3vb@users.noreply.github.com>
2026-04-07 05:28:53 +00:00
copilot-swe-agent[bot]andJ3vb bd45b65dd3 Agent-Logs-Url: https://github.com/J3vb/OwnCord/sessions/6635420c-af26-4cc0-9397-5e5b37887437
Co-authored-by: J3vb <192430104+J3vb@users.noreply.github.com>
2026-04-06 22:06:23 +00:00
copilot-swe-agent[bot]andJ3vb 2dc9a060fc fix(review): address 6 reviewer findings from pullrequestreview-4064746778
- service/user.go: fix ChangePassword docstring (no old-password verification)
- service/user.go: RevokeSession now maps db.ErrNotFound→ErrNotFound and
  all other store errors→ErrInternal, preventing internal failures from
  masquerading as 404s
- plugin/loader.go: update scanPluginDirectory comment to reflect fail-fast
  behavior; fix Lstat comment wording
- db/queries/sqlite/events.sql: CAST COALESCE result to INTEGER so sqlc
  generates int64 instead of interface{}
- api/plugins_handler.go: log install error server-side and return sanitized
  structured JSON response instead of raw err.Error()
- .github/workflows/ci.yml: remove continue-on-error from tag build steps
  so tag boundary drift fails CI"

Agent-Logs-Url: https://github.com/J3vb/OwnCord/sessions/6635420c-af26-4cc0-9397-5e5b37887437

Co-authored-by: J3vb <192430104+J3vb@users.noreply.github.com>
2026-04-06 22:03:35 +00:00
Claude 47d848ee0a feat(phase-bc): implement real OTel + Wazero runtimes; harden install path
Phase B Step 8 (OpenTelemetry) and Phase C Step 9 (Wazero plugin runtime)
were structurally scaffolded but the tagged builds were placeholders that
errored at runtime. This commit lands the real implementations behind the
existing build tags, plus three review passes worth of fixes across the
plugin admin handler, plugin registry, telemetry adapter, and Solid client.

Telemetry (Phase B Step 8)
- Add real go.opentelemetry.io/otel{,/sdk,/exporters/{prometheus,otlp...}}
  modules to go.mod plus contrib/instrumentation/net/http/otelhttp.
- Replace the telemetry_otel.go skeleton with a working Provider that
  wires Prometheus + OTLP/gRPC exporters, otelhttp middleware, span and
  meter adapters, and an idempotent Shutdown.
- AppMetrics cache is now reset *before* SetGlobal to close a race where
  a concurrent NewAppMetrics() could observe a swapped provider but read
  stale no-op instruments.
- Init releases the trace provider on a later prometheus exporter
  failure so Init never leaks gRPC connections.
- convertAttrs handles int32/uint/uint32/uint64/float32 explicitly;
  uint64 values that exceed math.MaxInt64 fall back to a STRING attr
  rather than wrapping into a negative int64 and corrupting metrics.
- Tests under -tags otel cover the prometheus scrape, span lifecycle,
  histogram recording, shutdown idempotency, AppMetrics rebind, and
  the uint64 overflow fallback.

Plugin runtime (Phase C Step 9)
- Add github.com/tetratelabs/wazero v1.11.0 to go.mod.
- platformInit creates a shared wazero.Runtime with WASI preview1
  pre-instantiated; activateWithRuntime compiles + instantiates each
  plugin module under that runtime; platformDeactivate closes per-
  plugin modules without tearing down the runtime.
- DisablePlugin now calls platformDeactivate so the wazero module is
  freed immediately instead of leaking until registry Close.
- activate() captures runtimePlatform under r.mu.RLock and passes it as
  a parameter to activateWithRuntime; the call no longer re-reads the
  field, closing a race with concurrent Close.
- invokeCommand calls the plugin's command_dispatch export when
  present; missing/broken exports return a user-facing diagnostic
  instead of crashing the dispatcher.
- Tests under -tags wazero cover registry creation, module compilation,
  re-enable after disable (verifies the leak fix), close-twice safety,
  invalid wasm rejection, and DispatchCommand with a missing export.
  Fixture is a 41-byte embedded add.wasm; no external asset required.

Plugin admin handler hardening
- /api/v1/admin/plugins/install now rejects uploads whose multipart
  Content-Type is not application/zip|x-zip-compressed|octet-stream
  (415) and uploads whose body lacks the PK\\x03\\x04 / PK\\x05\\x06
  zip magic (400). The 16 MiB cap and registry-side zip-slip / symlink
  / size-bomb defences are still applied as before.
- New plugins_handler_test.go covers list-empty, install-503-when-nil,
  content-type rejection, magic rejection, happy path, lifecycle 503,
  invalid id, and isZipContentType / hasZipMagic helpers.

Solid client (Phase B Step 6) cleanup
- vitest.config.ts now wires vite-plugin-solid and broadens the test
  glob to include src/**/*.test.tsx so Badge.test.tsx is actually
  discovered (it was silently skipped).
- pluginBridge.ts targets postMessage at window.location.origin
  instead of "*", and exposes a destroy() that detaches the message
  listener and clears mounted frames.
- solidMount.ts imports the JSX type from "solid-js" instead of
  "solid-js/web" (the latter does not re-export it), unblocking
  npx tsc --noEmit.

Build/test status
- go build succeeds on default, -tags otel, -tags wazero, and
  -tags otel,wazero.
- go test passes on every tag combination across telemetry, plugin,
  api, ws, service, store, and the rest of the tree.
- Client: npx tsc --noEmit clean; vitest 3188/3188 across 112 files.

PHASE_BC_LOCAL_TODO.md is updated to mark the OTel modules + real Init,
the wazero module + real platformInit, and the test coverage that
landed in this commit as completed.

https://claude.ai/code/session_01AZni6CDSQeu67WSWY1YCDX
2026-04-06 21:46:22 +00:00
J3vb 86634c531e feat(phase-bc): add hello plugin WASM binary and source
- main.go: TinyGo WASI plugin implementing the OwnCord plugin ABI
  (allocate/deallocate, list_commands, command_dispatch, on_event)
- hello.wasm: precompiled binary (TinyGo 0.40.1, Go 1.25.3, wasm-opt 129)
  Responds to /hello [name] with a greeting; proof-of-life for wazero runtime
2026-04-06 23:40:03 +02:00
J3vb ae496082b3 feat(phase-bc): TOML plugin manifests + Solid vitest preset
Plugin TOML support (-tags wazero):
- manifest_toml.go: tryLoadPluginTOML using BurntSushi/toml v1.6.0
- manifest_nottoml.go: no-op stub for default build
- loader.go: prefers plugin.toml, falls back to plugin.json

Solid vitest preset:
- vitest.config.ts: add vite-plugin-solid, expand include to
  src/components/solid/**/*.test.tsx — Badge.test.tsx now runs
  automatically as part of npm test (112 files / 3188 tests)
2026-04-06 23:25:16 +02:00
J3vb ddaaadcd43 feat(phase-bc): implement OTel SDK wiring and wazero plugin runtime
OTel (telemetry/telemetry_otel.go, -tags otel):
- Real Init: prometheus pull-exporter + OTLP/gRPC push-exporter branches
- Bridge types: Tracer/Span/Meter/Counter/Histogram/Gauge wrapping SDK
- HTTP middleware via otelhttp (otelchi not in proxy; otelhttp is equivalent)
- Added otel v1.43.0, sdk, exporters/prometheus v0.65.0, otlptracegrpc,
  otelhttp v0.67.0, wazero v1.11.0 to go.mod

Wazero (plugin/sandbox_wazero.go, -tags wazero):
- ensureRuntimeLocked: lazy wazero.Runtime init with WASI host module
- activateWithRuntime: CompileModule + InstantiateModule per plugin
- invokeCommand: JSON-ABI dispatch via allocate/command_dispatch/deallocate
- listExportedCommands via list_commands export

Build matrix: default, otel, wazero, postgres, otel+wazero+postgres all pass
Server test suite: all packages green
2026-04-06 23:21:38 +02:00
J3vb 9116a880a3 feat(phase-bc): pass 5 — pgdbgen, postgres EventStore/PluginStore, plugin hub wiring, OTel stack, reconnect DB tier
- Generate Server/db/dbgen/{events,plugins}.sql.go and full Server/db/pgdbgen/ (//go:build postgres gated)
- Implement PostgresStore EventStore and PluginStore methods in store/postgres.go
- Wire plugin host_events.go EventSink into hub broadcast path (SetPluginEventSink)
- Wire host_commands.go slash-command dispatcher: chat_command V1 handler + hub.SetPluginRegistry
- Add handlers_command.go + handlers_command_test.go for plugin slash-command dispatch
- Add reconnect_db_test.go: TestReconnect_BufferMiss_FallsBackToDBTier (cold-tier DB replay)
- Add otel-up/otel-down Makefile targets; docker-compose.otel.yml + prometheus.dev.yml
- Update PHASE_BC_LOCAL_TODO.md: mark in-session items complete; document remaining network-blocked steps
- Minor fixes: channel_handler access-control, router plugin handler wiring, service span instrumentation
2026-04-06 22:48:59 +02:00
Claude d320a8b587 fix(review): address 11 Copilot review findings on PR #1132
Clean sweep of every actionable item from the two Copilot review passes
on head 59ae4d8. Grouped by severity:

─── Crash / security (must-fix) ─────────────────────────────────────

1. main.go:140 — telemetryShutdown nil panic.
   telemetry.Init can return (nil, err) on the -tags otel skeleton
   path; the deferred closure would then call a nil function. Normalise
   to a no-op shutdown when Init errors so the defer is always safe.

2. api/upload_handler.go — permSvc nil deref.
   MountUploadRoutes + handleServeFile dereference permSvc on every
   authenticated file request. Add a fail-fast panic at mount time so
   the misconfiguration surfaces at wiring, not on the first 500.
   Update upload_handler_test.go to pass a real PermissionService built
   on the test DB (the existing tests were missing the argument entirely,
   which meant the package wouldn't compile — this fixes the real bug
   Copilot flagged).

3. ws/event_persister.go — NewEventPersister nil EventStore panic.
   run() dereferences p.store on every flush. Panic at constructor
   time instead so the crash happens once at startup rather than
   minutes later in a background goroutine.

4. plugin/host_ui.go — serve-time symlink check.
   rejectSymlinksUnder only runs at install time, so a symlink created
   post-install (accidental or malicious) would be followed by
   http.ServeFile and leak host files. Add an os.Lstat + ModeSymlink
   check + IsRegular check to AssetHandler on every request. Cheap
   relative to the file read and closes the TOCTOU window.

─── Correctness / observability (should-fix) ───────────────────────

5. ws/deps.go:77 — requirePerm hides misconfig as FORBIDDEN.
   Previously, nil database, nil perms, or a GetRoleForUser error all
   returned ErrCodeForbidden with the same message, making operator
   failures indistinguishable from legitimate permission denials.
   Split the branches: misconfig + DB error now return ErrCodeInternal
   with a server-side slog.Error so operators see the real problem;
   FORBIDDEN is reserved for the actual permission-bit check.

6. telemetry/metrics.go — ServiceCallDurationMs renamed to Sec.
   Field name said "Ms" but the instrument name was
   `service_call_duration_seconds` with unit "s". Renamed the field
   and updated all 8 service-layer callers so the struct field and
   metric semantics match.

7. ws/event_persister.go — flushEvy typo → flushEvery.
   Renamed the field and the one call site in run().

─── Comments out of sync with code ──────────────────────────────────

8. plugin/loader.go — Stat vs Lstat comment.
   The comment claimed "Stat (not Lstat)" but the code correctly uses
   os.Lstat to detect symlinks. Updated the comment to match the code;
   the code was already right.

9. telemetry/telemetry_otel.go — compile claim wrong.
   Comment said the file would fail to compile without the upstream
   OTel modules, but the skeleton deliberately avoids importing them
   and Init returns a runtime error instead. Updated the comment to
   reflect actual CI behaviour (the -tags otel build step passes
   today but doesn't exercise real telemetry).

─── Nit / polish ────────────────────────────────────────────────────

10. ws/event_pruner.go — startup delay magic constant.
    Hard-coded time.Minute made the "run shortly after startup"
    behaviour untestable (a test with a 100ms interval would still
    wait a full minute). Cap the startup delay by the interval:
    min(interval, time.Minute). Documented via a new `maxStartupDelay`
    constant.

11. ws/event_pruner_test.go — new file.
    Unit coverage for runPrune cutoff correctness, error swallowing,
    StartEventPruner nil-store short-circuit, ctx cancellation, and
    the interval-bounded startup delay from fix #10. Uses a fakeEventStore
    stub that records every prune call and signals the first one so
    tests don't sleep.

─── Verification ────────────────────────────────────────────────────

gofmt -l clean. No network access in sandbox so `go vet` and `go test`
could not run; the changes are local and surgical and every touched
file compiles in isolation against the existing signatures.

https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
2026-04-06 13:48:41 +00:00
Claude 59ae4d8ad2 test+feat(phase-bc): pass 4 — test coverage, install endpoint, CHANGELOG
Final in-sandbox completeness pass. Five focused pieces; the remaining
items in PHASE_BC_LOCAL_TODO.md after this commit are all genuinely
local-only (toolchain, network, native deps).

Test coverage (the biggest gap from prior reviews)
- Server/plugin/manifest_test.go — pluginNameRegexp accept/reject table,
  validateRelativePath table, oversized version, unknown permission.
- Server/plugin/host_http_test.go — hostAllowed dot-boundary suffix,
  empty-entry rejection, case insensitivity, FQDN trailing dot. ipAllowed
  table over loopback, RFC1918, RFC4193 (ULA), RFC6598 (CGN), link-local,
  multicast, unspecified — both v4 and v6 — plus public-IP accept cases.
- Server/plugin/loader_test.go — rejectSymlinksUnder catches direct and
  nested symlinks; scanPluginDirectory rejects a plugin whose entrypoint
  is a symlink. Skipped on Windows where symlink creation needs elevation.
- Server/plugin/host_ui_test.go — AssetHandler serves declared files,
  rejects undeclared files (404), rejects path traversal, supports nested
  asset paths.
- Server/ws/hub_seedseq_test.go — SeedSeq monotonic, never-backwards,
  concurrent CAS safety, integration with nextSeq.
- Server/ws/extract_event_type_test.go — table covering happy paths,
  control char rejection, escaped quote rejection, length cap (64),
  empty/missing/non-JSON inputs.

Plugin install endpoint (closes a real feature gap)
- Server/plugin/registry.go — InstallFromZip extracts a plugin .zip into
  a staging directory under cfg.Directory, validates it zip-slip safe
  (cleaned-path Rel check), refuses non-regular entries, refuses
  symlinks, caps compressed at 16 MiB and uncompressed total at 64 MiB
  (each file gated by io.CopyN against the remaining budget). Manifest
  is parsed at the staged root, then atomically renamed into the
  canonical plugin directory and registered via the existing
  installFromDisk path.
- Server/api/plugins_handler.go — POST /install accepts multipart with
  one "plugin" file part, http.MaxBytesReader caps the request body,
  io.LimitReader caps the in-memory buffer, calls Registry.InstallFromZip,
  returns 201 with the new plugin name. The endpoint inherits the Pass 2
  admin auth + IP gate (mounted under r.Use(admin.RequireAdminAuth)).

Protocol surface
- Server/ws/serve.go — buildAuthOK now takes replaySource and includes
  it in the auth_ok payload as "replay_source": "none" | "buffer" | "db".
  Two call sites updated: reconnect path passes the existing local,
  fresh-connect path passes "none". Test export updated to pass "none".

CI build-tag matrix
- .github/workflows/ci.yml — three new steps inside server-build-test
  build the server with -tags otel, -tags wazero, and -tags otel,wazero.
  All three are continue-on-error: true until the upstream OTel and
  wazero modules land in go.mod (tracked in PHASE_BC_LOCAL_TODO.md).
  Once they do, dropping continue-on-error converts the steps into
  hard CI gates against tag-boundary drift.

Documentation
- CHANGELOG.md — new root-level file with curated entries for Phase B,
  Phase C, security, and behavioural changes operators must know about
  (notably event_persistence.enabled = true by default).
- PHASE_BC_LOCAL_TODO.md — ticks off the install endpoint, the
  replay_source field, and the existing event_persistence defaultYAML
  entry. The remaining items are toolchain-bound.

After this pass, the in-sandbox completeness ceiling is reached.
Everything still pending requires Go 1.25 toolchain, npm install,
real OTel SDK + wazero modules, sqlc, postgres backend impl, or
tinygo.

https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
2026-04-06 10:30:18 +00:00
Claude 7476c177b7 chore(phase-bc): pass 3 cleanup — perf, observability, hardening
Eight focused follow-ups from the medium-severity review bucket. All
in-sandbox tractable; no module changes, no new dependencies.

Performance
- Drop the defensive memcpy in EventPersister.Enqueue. The hub already
  passes a fresh slice from wrapWithSeq and the copy was happening under
  seqMu, serializing broadcast throughput. Documented the no-mutate
  contract on the call site.

Observability
- AppMetrics gains WSEventsPersistErrors counter; the persister run loop
  bumps both it and the existing WSEventsPersisted counter via cached
  metrics handle.
- Hub.persistEvent now extracts the real event type ("chat_message",
  "voice_join", ...) from the wrapped JSON envelope via a small
  no-allocation byte scan instead of recording the generic
  "broadcast"/"channel_broadcast" label.
- Added OTel spans + ServiceCallDurationMs histogram entries on one
  public method per remaining service: DMService.CreateDM,
  VoiceService.JoinChannel, InviteService.CreateInvite,
  ModerationService.BanUser, BlockService.BlockUser,
  UserService.UpdateProfile. Mirrors the existing pattern from
  MessageService.SendMessage.

Hardening
- plugin/loader now Lstat-walks each plugin directory and rejects any
  symlink, plus refuses an entrypoint that is itself a symlink. The
  asset handler's prefix check stays as defense in depth.
- ipAllowed (plugin HTTP capability) now rejects RFC6598 carrier-grade
  NAT (100.64.0.0/10), closing a gap in net.IP.IsPrivate which only
  covers RFC1918 + RFC4193.
- Registry.activateAll syncs Instance.Enabled := true after a successful
  activate so callers reading the in-memory flag see the live state.

Documentation
- defaultYAML now documents the new event_persistence, telemetry, and
  plugins config blocks with their defaults and one-line descriptions.
- PHASE_BC_LOCAL_TODO.md ticks off five items (defaultYAML docs ×2,
  remaining service spans, registry wiring already-fixed in Pass 2).

https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
2026-04-06 09:49:03 +00:00
Claude 46aeccc49b fix(phase-bc): address review findings — auth, SSRF, seq alignment
Phase B + C review pass: critical security and correctness fixes.

Security
- S1: plugin admin endpoints now require admin.RequireAdminAuth in addition
  to AdminIPRestrict. Previously a LAN attacker on the allowed CIDR could
  list/enable/disable/uninstall plugins without a session.
- S2: rewrite plugin HTTPDo allowlist with proper net/url parsing. Empty
  entries are ignored, suffix matches require a dot boundary, and a custom
  Dialer rejects loopback / RFC1918 / link-local addresses to close the
  DNS-rebinding TOCTOU window. Redirects re-validated.
- S3 + #9: manifest Name pinned to ^[a-z0-9][a-z0-9_-]{0,63}$, Entrypoint
  and UI tab assets validated against absolute / "..", NUL byte, backslash
  and non-canonical paths. Asset handler hardened with filepath.Rel check
  for symlink and prefix-without-separator escapes.
- S5: pluginBridge postMessage handler ignores the pluginId in the message
  body and uses an e.source -> contentWindow lookup instead, defeating
  spoofed messages from same-origin scripts.
- S8: HTTPDo body capped at 5 MiB via io.LimitReader, redirects bounded
  to 5 hops.

Correctness
- Critical seq alignment: PersistEvent now takes the hub-assigned seq as a
  required parameter so the events table row seq always matches the wrapped
  payload seq. Hub seeds its in-memory atomic counter from MAX(events.seq)
  on startup. Drops in the persister queue no longer mis-align row vs
  payload seq.
- #1: live plugin.Registry constructed in main.go BEFORE NewRouter and
  threaded through; admin handler is no longer wired with nil.
- #3: EventPersister.Stop is now safe to call without a prior Start by
  tracking a started flag — previously deadlocked waiting on done.

Wiring
- NewRouter signature gains *plugin.Registry; two test callers updated.
- admin.RequireAdminAuth exported as a thin wrapper over the existing
  package-private adminAuthMiddleware.
- sqlc query templates updated for the new PersistEvent + GetMaxEventSeq
  contracts (sqlite + postgres).

https://claude.ai/code/session_01UsBsQW2YiA2usk9pnJjAWk
2026-04-06 09:29:29 +00:00
Claude a2cb224323 feat: scaffold Phase B + C (events, telemetry, plugins, Solid.js)
Phase B Step 6 — Solid.js incremental migration
  - vite-plugin-solid + solid-js + @solidjs/testing-library in package.json
  - vite.config.ts compiles src/components/solid/** as Solid TSX
  - tsconfig.json gains jsx: preserve / jsxImportSource: solid-js
  - lib/solidAdapter.ts wraps existing custom Stores as Solid signals
  - lib/solidMount.ts adapts Solid render to {mount,destroy} contract
  - components/solid/Badge.tsx (proof-of-concept leaf)
  - components/solid/ChannelListItem.tsx (store-subscribed leaf)
  - components/solid/Badge.test.tsx pipeline smoke test
  - components/solid/README.md documents the migration recipe

Phase B Step 7 — Event persistence layer
  - SQLite + Postgres migrations for the events table
  - sqlc query files for both engines
  - EventStore interface + SQLite raw-SQL impl + MemStore impl + pg stubs
  - ws.EventPersister: async batched writer (queue / flush / drain / drop)
  - ws.StartEventPruner: background retention pruner
  - hub persists every replay-buffer push and exposes reconnect-tier counters
  - serve.handleReconnect: tiered replay (buffer -> DB -> full re-sync)
  - EventPersistenceConfig + main.go wiring
  - event_persister_test.go covers batching / drops / drain

Phase B Step 8 — OpenTelemetry skeleton
  - Server/telemetry package with public Provider/Tracer/Meter/Counter API
  - telemetry_default.go (no-op build) + telemetry_otel.go (build tag otel)
  - telemetry/metrics.go declares the AppMetrics bundle
  - HTTPMiddleware mounted in Chi router (pass-through in default build)
  - PrometheusHandler optionally mounted at /metrics
  - Spans on MessageService.SendMessage, PermissionService.HasChannelPerm,
    ChannelService.ListVisibleChannels
  - Reconnect-tier counter wired into the global meter
  - TelemetryConfig defaults

Phase C Step 9 — Wazero plugin runtime skeleton
  - Server/plugin package: manifest parser, loader, registry, host APIs
    (commands, storage, events, http, ui), errors
  - sandbox_default.go (no-op) + sandbox_wazero.go (build tag wazero)
  - SQLite + Postgres migrations for plugins + plugin_kv tables
  - PluginStore interface + impls + pg stubs
  - plugin/examples/hello manifest + README
  - plugin_test.go covers manifest, loader, capability gating
  - api/plugins_handler.go admin REST surface, mounted under admin group
  - PluginsConfig + main.go wiring (disabled by default)
  - Client: lib/pluginBridge.ts iframe + postMessage host
  - Client: components/solid/PluginContainer.tsx Solid host component

Verification
  - Default build (no -tags) is intended to compile cleanly with no new
    third-party dependencies. The sandbox lacked Go 1.25.0 so go build
    could not run; PHASE_BC_LOCAL_TODO.md enumerates the local follow-up
    work (npm install, go mod tidy, sqlc-generate, real otel/wazero
    wiring, remaining service spans, full Solid migration).
2026-04-06 09:00:47 +00:00
Claude dede2c61a7 phase-a: scaffold postgres backend (schema, queries, store stub, config)
- migrations/postgres/: consolidated pg schema with tsvector FTS, CITEXT
  usernames, native CHECK constraints, native BOOLEAN/TIMESTAMPTZ types
- db/queries/postgres/: 14 sqlc query files dialect-translated from sqlite
  ($N placeholders, NOW(), TRUE/FALSE, ON CONFLICT DO UPDATE, RETURNING id,
  :execrows for mutations needing row count)
- sqlc.yaml: second engine entry -> pgdbgen package under pgx/v5
- Makefile: sqlc-verify covers both dbgen + pgdbgen
- store/postgres.go: PostgresStore behind //go:build postgres, full Store
  interface (112 methods). Connection lifecycle real; query methods stub
  ErrPostgresNotImplemented awaiting pgdbgen wrappers
- config: DatabaseConfig.Type/Host/Port/User/Password/Name/SSLMode/MaxConns
- main.go: explicit dispatch on database.type; postgres errors with clear
  pointer at remaining work until pgdbgen + boundary refactor land
- phase-a-foundation.md: implementation status + actionable TODO checklist
  including forward-only sqlite->postgres data migration design
2026-04-06 07:43:08 +00:00
J3vb 2a46b95111 feat: adopt sqlc for type-safe database access (Phase A Step 2)
- Add sqlc.yaml config (SQLite engine, db/queries/sqlite/, db/dbgen/ output)
- Pin sqlc v1.30.0 in sqlc.version
- Add Makefile with sqlc-install, sqlc-generate, sqlc-verify targets
- Write 14 SQL query files covering all DB domains (users, sessions,
  invites, channels, messages, reactions, voice, roles, attachments,
  admin, dm, blocks, lockouts, profile)
- Commit generated db/dbgen/ package (querier interface + typed fns)
- FTS5 search queries remain hand-written in message_queries.go;
  transactional multi-step operations unchanged in Go
2026-04-06 09:12:59 +02:00
Claude f1a5b5188c fix: remove unused slog import from profile_handler.go
Logging moved into UserService during migration — handler no longer
calls slog directly.

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 21:56:17 +00:00
Claude 03cc77dfe5 clean up WS deps: remove unused DB/Permissions from migrated handlers
ChatDeps, PresenceDeps, ReactionDeps no longer carry *db.DB or
*permissions.Checker — those were only needed before the service
migration. VoiceDeps retains them for voice handlers not yet migrated.

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 21:36:29 +00:00
Claude 0e2d101103 add ModerationService, VoiceService, MemStore, permission tests
- ModerationService: ban/unban with validation and audit logging
- VoiceService: join (with capacity check), leave, mute, deafen,
  camera (with video limit), screenshare (with permission check)
- MemStore: in-memory Store implementation for service unit tests
- Permission tests: cache hit/miss, invalidation, TTL behavior
- Add Moderation and Voice to Services struct

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 21:35:20 +00:00
Claude 9829325105 migrate upload/profile handlers, add topic rate limiter, wire voice topics
- upload_handler: uses PermissionService.HasChannelPerm (removes last
  hasChannelPermREST usage)
- profile_handler: delegates to UserService with proper ErrConflict on
  duplicate username
- topic_rate_limiter: per-topic throughput caps (100 msg/s default),
  wired into deliverBroadcast for channel-scoped broadcasts
- voice_join: subscribes client to VoiceTopic on join
- voice_leave: unsubscribes client from VoiceTopic on leave

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 21:34:32 +00:00
Claude 05face4b2a migrate upload_handler and profile_handler to service layer
- upload_handler.go: uses PermissionService.HasChannelPerm instead of
  the deleted hasChannelPermREST helper
- profile_handler.go: delegates to UserService for profile updates,
  password changes, session listing, and session revocation
- Remove hasChannelPermREST from channel_handler.go (no longer needed)
- UserService.UpdateProfile now returns ErrConflict on duplicate username

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 21:31:35 +00:00
Claude c9099f04e7 fix compile errors and wire permission cache invalidation
Critical fixes:
- Move svc creation in router.go above MountInviteRoutes/MountChannelRoutes
  (was used before definition — compile error)
- Restore hasChannelPermREST in channel_handler.go for upload_handler.go
  (was removed but still referenced — compile error)

Permission cache invalidation:
- Add PermissionInvalidator interface to admin package
- Wire through NewHandler → NewAdminAPI → handlePatchUser
- Call InvalidateUser(userID) after role changes in admin panel
- Update all admin test files to pass nil as new parameter

Also clarifies WithTx documentation for SQLite single-writer semantics.

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 21:25:35 +00:00
Claude 1bf3ca5de3 implement full three-tier priority queue system
Client now has three send channels:
- sendHigh (64 slots): DMs, mentions — drained first by writePump
- send (256 slots): chat messages, reactions — drained second
- sendLow (64 slots): typing, presence — drained last, dropped on overflow

writePump drains high-priority messages before checking normal/low.
PubSub gains PublishHigh/PublishLow alongside existing Publish.
EmitEvents routes events by priority:
- High: SequencedDMEvent, UserTargetedEvent
- Normal: ChannelEvent, VoiceChannelEvent
- Low: ExcludeSenderEvent (typing), PresenceEvent

Slow clients get typing/presence dropped first (sendLowMsg silently
drops), then disconnect on normal buffer overflow, ensuring DMs are
never lost to typing indicator backpressure.

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 21:12:08 +00:00
Claude c2bf9304e4 add low-priority pub/sub delivery for typing/presence events
PublishLowPriority uses trySendMsg to silently drop messages when a
client's buffer is full, instead of disconnecting them. This provides
priority-based backpressure: chat messages use normal Publish (disconnect
on overflow), while ephemeral events like typing indicators and presence
updates use PublishLowPriority (drop on overflow).

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 21:02:54 +00:00
Claude ccb80d8a83 complete service layer migration + store.Store integration
- Add UserService, DMService, InviteService, BlockService
- Migrate REST handlers (channel, DM, invite, block) to use services
- Remove block_handler.go (merged into dm_handler.go)
- Update all services to accept store.Store instead of *db.DB
- Router creates SQLiteStore and passes to service.New()

Handlers are now thin HTTP adapters: parse request → call service →
map error → write JSON. All business logic lives in the service layer.

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 21:01:29 +00:00
Claude 87055819b9 wire pub/sub channel subscriptions into channel_focus handler
When a client focuses a channel, subscribe to its pub/sub topic.
When switching channels, unsubscribe from the old topic first.
This completes the pub/sub integration — channel broadcasts now
route only to clients subscribed to the relevant topic.

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 20:52:02 +00:00
Claude 075ef28e29 add pub/sub broadcast model, replace iterate-and-filter (Phase A, Step 5)
Introduce topic-based PubSub for O(subscribers) message routing:
- Clients subscribe to "global" and "user:{id}" on connect
- Channel broadcasts route through "channel:{id}" topics
- deliverBroadcast() uses PubSub instead of iterating all clients
- UnsubscribeAll on disconnect/kick cleans up subscriptions
- Sequence numbering and replay buffer preserved unchanged

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 20:51:06 +00:00
Claude 20199fcfca add store.Store interface and SQLiteStore (Phase A, Step 3)
Define the Store interface composing domain-specific sub-interfaces
(MessageStore, ChannelStore, UserStore, etc.) that decouple services
from the concrete database. SQLiteStore wraps *db.DB, delegating all
operations to existing query methods.

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 20:50:28 +00:00
Claude e54ad46079 migrate WS chat/reaction/presence handlers to service layer
Handlers now delegate all business logic (validation, permission checks,
DB operations) to MessageService and ChannelService instead of calling
*db.DB directly. This eliminates logic duplication and enables the
service layer's permission cache.

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 20:47:47 +00:00
Claude 1c5fbbc246 add service layer foundation (Phase A, Step 1)
Introduce Server/service/ package with MessageService, ChannelService,
and PermissionService that encapsulate business logic previously
scattered across REST and WS handlers. The PermissionService adds
per-user in-memory caching with TTL-based expiry to eliminate
per-message DB round-trips at scale.

Services are wired into the WS hub via deps structs (strangler-fig
pattern) — existing handlers continue to work unchanged, with service
references available for incremental migration.

https://claude.ai/code/session_01CBFF3r84ywkJRWwuqw8zD8
2026-04-05 20:42:32 +00:00
J3vb a91ecd093b fix: resolve unparam lint errors in server build
- Remove unused `ver` param from handleHealth and handleInfo (version
  was intentionally removed from unauthenticated endpoints per C-2)
- Rename decodeBase64Loose to validateBase64Loose returning only error,
  matching actual usage (all callers only validate, never use the bytes)
2026-04-05 21:20:00 +02:00
J3vb e2f8858019 fix: resolve CI lint and ESLint failures
- Remove commented-out code flagged by gocritic
- Use bytes.Equal instead of string conversion comparison
- Remove unused buildRateLimitError function
- Remove unnecessary type assertions in e2eeCrypto.ts
2026-04-05 19:37:00 +02:00
J3vb 6e4a007b91 refactor: migrate WS handlers to V2 Command/Event architecture
Strangler-fig migration of 15 WebSocket handlers from V1 (Hub method,
*Client) to V2 (pure functions: Command, ClientInfo, deps -> Result).
V2 handlers are testable without a running Hub and produce declarative
Result values that the dispatch loop applies.

New abstractions:
- Command interface + typed constructors with input validation
- 7 Event routing interfaces (Channel, ExcludeSender, SequencedDM,
  UserTargeted, BroadcastAll, VoiceChannel, VoiceChannelGuarded)
- Per-domain deps structs (PingDeps, ChatDeps, PresenceDeps,
  ReactionDeps, VoiceDeps) with interface-based DI
- EmitEvents router matching events to delivery mechanisms
- DispatchV2 with panic recovery and runtime.Stack logging

Security hardening:
- Pre-sanitize byte length guard before bluemonday (DoS prevention)
- GetRoleForUser single-JOIN query avoids password hash on hot path
- channel_id positivity enforced in all command constructors
- Log injection prevention: msgType/reqID capped to 64 chars
- Nil KeyHolder dep returns ErrCodeInternal (not silent bypass)
- VoiceChannelGuardedEvent atomic check-and-send under h.mu.RLock

V1-only (complex state/mutex requirements): voice_join, voice_leave.

All tests pass with -race. No CI regressions expected.
2026-04-05 19:03:22 +02:00