email: give the sidecar the whole user, not just the id
The mailbox read as empty after the migration — "No emails synced yet", and Sync Now
reporting no new mail against 18,430 messages that were sitting on disk untouched.
The store is keyed by the owner's email: DATA_PATH/<owner>/email_accounts/<account>/emails.db.
The platform's userMiddleware put the entire user row on the context, so `user.email`
resolved. The proxy injects only an id, and the middleware I wrote to replace it set
`{ id }` and nothing else — so `user.email` was undefined, the path never resolved to the
real mailbox, reads found nothing, and the sync compared against an empty set and concluded
there was nothing new. Nothing was written to the wrong place and no data was touched.
The sidecar loads the full row via getUserById now, matching what the middleware provided.
Cached: it runs on every request and single-user is a hard invariant.
Worth keeping in mind for the sidecars still to be extracted — moving routes across a
process boundary silently changes what is on the context, and it type-checks either way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Hono } from 'hono';
|
||||
import { getUserById } from 'officerdb';
|
||||
import { emailRouter } from './routes';
|
||||
|
||||
// The email sidecar's own listener. `/api/email/*` on the platform is a proxy onto this — the platform
|
||||
@@ -12,12 +13,24 @@ type HonoVariables = { user: { id: number }; body: Record<string, unknown>; orig
|
||||
|
||||
const app = new Hono<{ Variables: HonoVariables }>();
|
||||
|
||||
// The platform's userMiddleware set `user` from the JWT. Here it comes from the header the proxy injects —
|
||||
// trusted because this server binds loopback only and nothing else can reach it.
|
||||
// The platform's userMiddleware set the whole user row on the context from the JWT. The proxy injects only
|
||||
// the id (X-Officer-User), so the rest is loaded here — and it has to be the WHOLE row, not just the id:
|
||||
// the mail store is keyed by the owner's email (DATA_PATH/<owner>/email_accounts/<account>/emails.db), so
|
||||
// a user object without `email` silently resolves to the wrong path and the mailbox reads as empty.
|
||||
//
|
||||
// Cached because this runs on every request and the owner does not change; single-user is a hard invariant.
|
||||
let cachedUser: { id: number; email: string } | null = null;
|
||||
|
||||
app.use('*', async (ctx, next) => {
|
||||
const id = Number(ctx.req.header('X-Officer-User'));
|
||||
if (!Number.isFinite(id) || id <= 0) return ctx.json({ error: 'missing X-Officer-User' }, 401);
|
||||
ctx.set('user', { id } as never);
|
||||
|
||||
if (!cachedUser || cachedUser.id !== id) {
|
||||
const row = await getUserById(id);
|
||||
if (!row?.email) return ctx.json({ error: `unknown user ${id}` }, 401);
|
||||
cachedUser = row as { id: number; email: string };
|
||||
}
|
||||
ctx.set('user', cachedUser as never);
|
||||
await next();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user