fix(19): BL-04 keep context oidcIss/oidcSub null for local users (no fabricated identity sentinels)

This commit is contained in:
Lucas Berger
2026-06-17 20:24:01 -04:00
parent 71537601ce
commit 40666e1cc5
3 changed files with 62 additions and 12 deletions
+21 -4
View File
@@ -44,15 +44,32 @@ export const DEV_USER = {
color: COLOR_PALETTE[0], // '#4A90D9' — first palette slot
} as const;
/**
* The shape stored on c.get('user') across the bypass, local-session, and OIDC paths.
*
* BL-04: oidcIss/oidcSub are NULLABLE. Local users have null OIDC fields, and
* localAuthMiddleware must NOT fabricate sentinel ('local'/String(id)) values — those
* share the uniqueness domain (uniq_oidc_identity) with real OIDC identities and could
* collide with a genuine (iss,sub) pair if ever persisted. DEV_USER carries non-null
* 'dev'/'dev-user' values and remains assignable to this widened shape.
*/
export interface ContextUser {
id: number;
oidcIss: string | null;
oidcSub: string | null;
displayName: string | null;
color: string;
}
/**
* Extend Hono's ContextVariableMap so that c.get('user') / c.set('user', ...)
* are statically typed throughout the app. The value type is the DEV_USER shape,
* which is compatible with both the bypass path and any future app-level user object
* stored on context (they share the same id/displayName/color subset).
* are statically typed throughout the app. The value type is ContextUser — the shape
* shared by the dev-bypass path, the local-session path (nullable oidc fields), and any
* future app-level user object stored on context.
*/
declare module 'hono' {
interface ContextVariableMap {
user: typeof DEV_USER;
user: ContextUser;
}
}
+11 -8
View File
@@ -33,7 +33,7 @@ import { eq } from 'drizzle-orm';
import { db } from '../db/client.js';
import { users } from '../db/schema.js';
import { verifyLocalSessionCookie } from './localSession.js';
import type { DEV_USER } from './devBypass.js';
import type { ContextUser } from './devBypass.js';
/**
* Returns a Hono MiddlewareHandler that:
@@ -81,17 +81,20 @@ export function localAuthMiddleware(): MiddlewareHandler {
return;
}
// Populate c.get('user') with the same shape as DEV_USER (devBypass.ts ContextVariableMap).
// oidcIss/oidcSub: local users have nullable oidcIss/oidcSub — use fallback strings so the
// shape is compatible with typeof DEV_USER at runtime. Cast required because ContextVariableMap
// is narrowed to the const DEV_USER literal type.
// Populate c.get('user') with the ContextUser shape (devBypass.ts ContextVariableMap).
// BL-04: keep oidcIss/oidcSub as NULL for local users — do NOT fabricate
// 'local'/String(id) sentinels. Those values share the uniq_oidc_identity uniqueness
// domain with real OIDC identities, so persisting them (e.g. a future upsertUser call
// using these context values) would let two local users collide or a local user shadow
// a genuine OIDC identity. ContextUser widens oidcIss/oidcSub to string | null so no
// cast is needed.
c.set('user', {
id: row.id,
oidcIss: row.oidcIss ?? 'local',
oidcSub: row.oidcSub ?? String(row.id),
oidcIss: row.oidcIss ?? null,
oidcSub: row.oidcSub ?? null,
displayName: row.displayName ?? null,
color: row.color ?? '#4A90D9',
} as typeof DEV_USER);
} satisfies ContextUser);
await next();
};