* feat(auth): add revocable API tokens (bot/service auth)
Add long-lived, revocable API tokens so headless clients (the introspection
MCP tool, bots, CI) can authenticate without a password. Presented as
"Authorization: Bearer <token>", a token authenticates as a specific user,
inheriting that user's role and permissions.
- migration 018 + dedicated api_tokens table (kept separate from sessions so
bulk logout and the per-user session cap never touch these); only the
SHA-256 hash is stored, raw token shown once at creation
- auth.ResolveTokenHash: one shared bearer resolver that both AuthMiddleware
and adminAuthMiddleware now call. Sessions are matched first so existing
login behavior is unchanged; API tokens are a fallback only on session miss.
A DB outage is returned wrapped, never mistaken for a bad token.
- `server token create|list|revoke` CLI: mints directly against the DB with no
HTTP and no login — the password-free bootstrap path
- tests: resolver (8 cases incl. outage-not-fallthrough), db queries (6),
api middleware integration (valid + revoked token)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(tools): add owncord-introspect MCP server
A local MCP dev tool that lets Claude Code introspect a running OwnCord
instance: read its logs, query any REST endpoint, and tail the desktop
client's log file. It is a thin wrapper over the existing API plus the
client log — no new product surface.
- tools/mcp-introspect/index.mjs (Node/ESM, one dep: @modelcontextprotocol/sdk)
exposes api_request (full read-write passthrough), server_logs (admin SSE
ring-buffer stream), client_logs (reads the desktop log file)
- authenticates with an API token (OWNCORD_API_TOKEN); pins the self-signed
cert and skips hostname checks (the cert has no SAN)
- registered in .mcp.json (secret-free ${OWNCORD_API_TOKEN})
- un-ignore tools/mcp-introspect/ so this shared dev tool is committed, while
tools/livekit-server.exe and node_modules stay ignored
- docs/mcp-introspect.md: how it works, tool reference, setup, troubleshooting
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(dependencies): update and add various crate versions in Cargo.lock
* feat(admin): manage API tokens from the admin panel
Add Owner-gated HTTP endpoints and a UI card to create, list, and revoke
API tokens from the web admin panel. Previously only the `server token`
CLI could manage them, which requires shell access to the host.
- POST|GET|DELETE /admin/api/tokens in admin/handlers_tokens.go, wired in
admin/api.go. All three are Owner-only (ownerOnlyMiddleware, like
backups/updates): an HTTP token-mint endpoint is a network-reachable
credential-minting surface, and API tokens deliberately survive password
change + bulk logout, so a hijacked admin session must not mint one.
- Reuses the same db.*APIToken calls as the CLI; create sources the actor
from request context (audits who clicked, not the bound user); the raw
token is returned once in the 201 body, never stored.
- Add json tags to db.APITokenListItem for snake_case wire consistency.
- Admin panel: "API Tokens" nav item + create modal, show-once reveal,
revoke confirm in admin/static/index.html.
- Tests: 7 in admin/api_test.go (+api_tokens table in the in-memory schema).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: modernize to Go 1.26 idioms + enable modernize linter
Apply `golangci-lint modernize` autofixes across the server and enable the
linter in .golangci.yml so these stop re-accumulating (they built up only
because modernize was never in the config).
Production code: slices.Contains for hand-rolled membership loops (api
router, ws origin, db/account, plugin manifest); strings.SplitSeq for
allocation-free line/segment iteration (db/migrate, updater, livekit_proxy);
strings.Cut (config); fmt.Appendf (dm_handler); min() (event_pruner);
any (ws client). Tests: range-over-int, t.Context(), WaitGroup.Go,
slices.Sort, maps.Copy, new(expr), interface{}->any.
- plugin/manifest.go parent-traversal check applied by hand: modernize
skipped it (two conflicting rewrites); used the slices.Contains form.
- Removed the now-dead ptr() test helper after newexpr inlined its callers.
- Dropped dangling sort imports left by the sort.Slice->slices.Sort rewrite.
No behavior change. All four tag variants build, full test suite is green,
and golangci-lint (with modernize enabled) reports 0 issues.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FetchTextAssetCached served the unauthenticated, unrate-limited client-update
endpoint. On TTL expiry every concurrent caller missed the cache and issued its
own outbound fetch: a measured 25 requests for 25 callers. Failures were not
cached at all, so an upstream outage produced one outbound request per caller
for as long as it lasted.
Guards the refresh with singleflight so a burst issues one fetch, caches
failures for errorCacheTTL (mirroring the release cache's existing cachedErr
idiom), and evicts expired keys so the map no longer grows by one entry per
release for the process lifetime.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>