resolve api keys at the websocket doors too

there were four doors, not two. the ws upgrade in server.tsx and the
vault notifications socket each verified the jwt themselves, so a key
that worked against /api would have 401'd on cliamp — signed in and can
play audio would have been two different questions for the music app.

both now call resolveAuthToken. verified: owner key upgrades cliamp
(101), bogus key 401, member key 403 on terminal exactly as their jwt
is.

reset-password and verify-token deliberately keep verify() — they read
a purpose-scoped reset token and a key must not be spendable as one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 10:40:04 +00:00
co-authored by Claude Opus 5
parent ae54df7c30
commit ae275ee607
3 changed files with 14 additions and 7 deletions
+6 -3
View File
@@ -4,9 +4,12 @@ import { getUserById } from 'officerdb';
import { verify } from '@@/jwt';
import * as errors from '@@/custom-errors';
// Validates a password-reset link before the reset form is rendered. Officer is single-user, so the
// account-verification and invitation flows this used to serve no longer exist — the sole account is
// created directly by /auth/bootstrap.
// Validates a password-reset link before the reset form is rendered. The account-verification and
// invitation flows this used to serve no longer exist — the owner is created by /auth/bootstrap and
// every other account was inserted by hand, there being no signup route yet.
//
// Deliberately `verify` and not `resolveAuthToken`: this is not an authentication door. It reads a
// purpose-scoped, short-lived reset token, and an API key must never be spendable as one.
export const verifyTokenHandler: Handler = async function (ctx) {
const { verificationCode } = ctx.get('body');
if (!verificationCode) throw errors.BAD_REQUEST('Missing verification code');
+2 -2
View File
@@ -1,5 +1,5 @@
import type { ServerWebSocket } from 'bun';
import { verify } from '../../jwt';
import { resolveAuthToken } from '../../auth-token';
import { isTokenBlacklisted } from 'officerdb';
import { isSuperAdmin } from '../../super-admin';
import { isOriginAllowed } from '../../_middlewares';
@@ -60,7 +60,7 @@ export const vaultWebsocket = {
// Deferred session validation (Bun requires the upgrade itself to be synchronous).
let userId: number;
try {
const payload = await verify(ws.data.platformToken);
const payload = await resolveAuthToken(ws.data.platformToken);
if (!payload?.id) return closeClient(ws, state, 4001, 'Unauthorized');
if (payload.jti && (await isTokenBlacklisted(payload.jti))) return closeClient(ws, state, 4001, 'Unauthorized');
if (!(await isSuperAdmin(payload))) return closeClient(ws, state, 4001, 'Forbidden');