Files
OwnCord/CHANGELOG.md
T
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

109 lines
5.4 KiB
Markdown

# Changelog
All notable changes to OwnCord are listed here. The repository's release
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
### Phase B — Acceleration
- **Event persistence layer (Step 7).** A new `events` table backs the
WebSocket reconnect path. When a client's `last_seq` is too old for
the in-memory ring buffer (~1000 events), the server now falls back to
a SQLite query before forcing a full re-sync. The hub seeds its
monotonic sequence counter from `MAX(events.seq)` at startup so row
seqs and wrapped-payload seqs stay aligned across restarts. Configurable
via the new `event_persistence` block; **enabled by default** (see
"Behavioural changes" below).
- **Tiered reconnect telemetry.** `auth_ok` now includes a `replay_source`
field (`"none" | "buffer" | "db"`) so clients can attribute reconnection
behaviour. The same tier label is exported as the
`ws_reconnect_tier_total{tier}` counter.
- **OpenTelemetry skeleton (Step 8).** Public API + no-op default
provider in `Server/telemetry/`. Chi router middleware mounted
unconditionally. Service-layer spans on `MessageService.SendMessage`,
`PermissionService.HasChannelPerm`,
`ChannelService.ListVisibleChannels`, `DMService.CreateDM`,
`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`.
- **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;
migration is mechanical and tracked in the local TODO.
### Phase C — Differentiation
- **Plugin runtime skeleton (Step 9).** New `Server/plugin/` package
with manifest parser, on-disk loader, registry, and host capability
surfaces (`commands`, `events`, `storage`, `http`, `ui`). Manifest
format is JSON (`plugin.json`); the design's TOML format is gated
behind the `-tags wazero` build and tracked locally.
- **Plugin admin REST surface.** Lifecycle endpoints under
`/api/v1/admin/plugins`: list, enable, disable, uninstall, and the
new install path that accepts a multipart zip upload, validates it
zip-slip safe with size + symlink rejection, and atomically installs
it. Mounted under both `AdminIPRestrict` and the
`admin.RequireAdminAuth` session/permission middleware.
- **Plugin admin client bridge.** `pluginBridge.ts` mounts plugin UI
tabs in sandboxed iframes with origin-validated postMessage routing.
### Security
- **SSRF defense for `http` capability.** Plugin outbound HTTP requests
are now validated through `net/url.Parse`, suffix-matched with a dot
boundary (so `evil-api.example.com` does not match
`api.example.com`), and rejected for empty allowlist entries. A custom
`Transport.DialContext` re-resolves DNS on every dial and refuses any
resolved address in loopback / RFC1918 / RFC4193 / RFC6598 (CGN) /
link-local / multicast / unspecified ranges. Closes the DNS-rebinding
TOCTOU window. Response body is capped at 5 MiB.
- **Plugin manifest hardening.** `Manifest.Name` must match
`^[a-z0-9][a-z0-9_-]{0,63}$`. Entrypoint and UI tab asset paths are
rejected if absolute, non-canonical, contain `..`, or contain NUL
bytes / backslashes.
- **Plugin asset handler.** Defends against symlink escapes (rejected
at install time via `filepath.Walk` + `Lstat`) and prefix-without-
separator path traversal (via `filepath.Rel` check after join).
- **Plugin postMessage routing.** The host bridge looks up the trusted
pluginId via `e.source -> contentWindow` instead of trusting the
`pluginId` field in the message body. Spoofed messages from any
non-iframe source are dropped.
### Behavioural changes operators must know about
- **`event_persistence.enabled` defaults to `true`.** Every broadcast
WebSocket event is written to the `events` table, retained for
24 hours by default, and pruned by a background goroutine every hour.
This is a new on-disk write path that did not exist before. Disable
it by adding to `config.yaml`:
```yaml
event_persistence:
enabled: false
```
- **DM events are persisted under the same retention.** Operators with
GDPR or compliance requirements should review the retention window
and consider setting `event_persistence.enabled: false` until a
per-channel-type opt-out lands.
- **Plugin admin endpoints require admin session auth in addition to
the existing IP restriction.** A previous prerelease shipped with only
the IP gate; that has been corrected.
### Known follow-up work (local toolchain required)
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.