1616 * 4. Anonymous (if mode=none or auth not required)
1717 */
1818
19- import type {
20- AuthContext ,
21- AuthMode ,
22- AuthResolverOptions ,
23- } from "../types/auth" ;
19+ import { extractBearerToken , verifyJWT } from "../services/jwt" ;
20+ import type { AuthContext , AuthResolverOptions } from "../types/auth" ;
2421import { getAuthConfig } from "../types/auth" ;
25- import { verifyJWT , extractBearerToken } from "../services/jwt" ;
26- import { generateUUID } from "../utils/uuid" ;
27- import { walletAddressToUUID } from "../utils/uuid" ;
2822import logger from "../utils/logger" ;
23+ import { generateUUID , walletAddressToUUID } from "../utils/uuid" ;
2924
3025/**
3126 * Constant-time string comparison to prevent timing attacks
@@ -43,6 +38,36 @@ function constantTimeCompare(a: string, b: string): boolean {
4338 return result === 0 ;
4439}
4540
41+ /**
42+ * Resolve a caller-provided userId from the X-User-Id header, request body,
43+ * or fall back to generating a new UUID.
44+ *
45+ * Priority: X-User-Id header > body.userId > generated UUID
46+ *
47+ * This is only used when the caller is already trusted (valid API key or
48+ * AUTH_MODE=none), so accepting the provided userId is safe.
49+ */
50+ function resolveProvidedUserId ( request : Request , body ?: any ) : string {
51+ // 1. Check X-User-Id header (useful for GET requests that have no body)
52+ const headerUserId = request . headers . get ( "X-User-Id" ) ;
53+ if (
54+ headerUserId &&
55+ typeof headerUserId === "string" &&
56+ headerUserId . length > 0
57+ ) {
58+ return headerUserId ;
59+ }
60+
61+ // 2. Check body.userId
62+ const bodyUserId = body ?. userId ;
63+ if ( bodyUserId && typeof bodyUserId === "string" && bodyUserId . length > 0 ) {
64+ return bodyUserId ;
65+ }
66+
67+ // 3. Generate a new UUID
68+ return generateUUID ( ) ;
69+ }
70+
4671/**
4772 * Check if request has valid API key (legacy support)
4873 */
@@ -115,7 +140,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
115140 required,
116141 hasX402Settlement : ! ! ( request as any ) . x402Settlement ,
117142 } ,
118- "auth_resolver_start"
143+ "auth_resolver_start" ,
119144 ) ;
120145
121146 // =====================================================
@@ -139,7 +164,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
139164 wallet : x402Settlement . payer ,
140165 method : "x402" ,
141166 } ,
142- "auth_resolved_x402"
167+ "auth_resolved_x402" ,
143168 ) ;
144169 }
145170
@@ -154,7 +179,8 @@ export function authResolver(options: AuthResolverOptions = {}) {
154179 const token = extractBearerToken ( authHeader ) ;
155180
156181 // Only verify if it looks like a JWT (has dots) or if we're in JWT mode
157- const shouldVerifyJwt = token && ( config . mode === "jwt" || token . includes ( "." ) ) ;
182+ const shouldVerifyJwt =
183+ token && ( config . mode === "jwt" || token . includes ( "." ) ) ;
158184
159185 if ( shouldVerifyJwt && token ) {
160186 const result = await verifyJWT ( token ) ;
@@ -176,7 +202,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
176202 hasEmail : ! ! auth . email ,
177203 hasOrgId : ! ! auth . orgId ,
178204 } ,
179- "auth_resolved_jwt"
205+ "auth_resolved_jwt" ,
180206 ) ;
181207 } else if ( config . mode === "jwt" ) {
182208 // JWT provided but invalid - only reject in strict JWT mode
@@ -185,7 +211,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
185211 error : result . error ,
186212 path,
187213 } ,
188- "auth_jwt_invalid"
214+ "auth_jwt_invalid" ,
189215 ) ;
190216
191217 // In JWT mode, invalid JWT = reject (don't fall through)
@@ -207,14 +233,8 @@ export function authResolver(options: AuthResolverOptions = {}) {
207233 // =====================================================
208234 // Only used if JWT mode is not active or no JWT provided
209235 if ( ! auth && isValidApiKey ( request ) ) {
210- // API key is valid - trust the caller's userId from body
211- const providedUserId = body ?. userId ;
212- const userId =
213- providedUserId &&
214- typeof providedUserId === "string" &&
215- providedUserId . length > 0
216- ? providedUserId
217- : generateUUID ( ) ;
236+ // API key is valid - trust the caller's userId from header/body
237+ const userId = resolveProvidedUserId ( request , body ) ;
218238
219239 auth = {
220240 userId,
@@ -226,23 +246,16 @@ export function authResolver(options: AuthResolverOptions = {}) {
226246 {
227247 userId,
228248 method : "api_key" ,
229- userIdSource : providedUserId ? "body" : "generated" ,
230249 } ,
231- "auth_resolved_api_key"
250+ "auth_resolved_api_key" ,
232251 ) ;
233252 }
234253
235254 // =====================================================
236255 // Priority 4: None Mode (Development)
237256 // =====================================================
238257 if ( ! auth && config . mode === "none" ) {
239- const providedUserId = body ?. userId ;
240- const userId =
241- providedUserId &&
242- typeof providedUserId === "string" &&
243- providedUserId . length > 0
244- ? providedUserId
245- : generateUUID ( ) ;
258+ const userId = resolveProvidedUserId ( request , body ) ;
246259
247260 auth = {
248261 userId,
@@ -254,9 +267,8 @@ export function authResolver(options: AuthResolverOptions = {}) {
254267 {
255268 userId,
256269 method : "anonymous" ,
257- userIdSource : providedUserId ? "body" : "generated" ,
258270 } ,
259- "auth_resolved_anonymous"
271+ "auth_resolved_anonymous" ,
260272 ) ;
261273 }
262274
@@ -270,7 +282,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
270282 path,
271283 mode : config . mode ,
272284 } ,
273- "auth_required_but_missing"
285+ "auth_required_but_missing" ,
274286 ) ;
275287
276288 set . status = 401 ;
@@ -302,7 +314,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
302314 userId : auth . userId ,
303315 method : "anonymous" ,
304316 } ,
305- "auth_resolved_anonymous_fallback"
317+ "auth_resolved_anonymous_fallback" ,
306318 ) ;
307319 }
308320
@@ -316,7 +328,7 @@ export function authResolver(options: AuthResolverOptions = {}) {
316328 verified : auth . verified ,
317329 path,
318330 } ,
319- "auth_resolver_complete"
331+ "auth_resolver_complete" ,
320332 ) ;
321333 } ;
322334}
@@ -341,7 +353,10 @@ export function authBeforeHandle(options: { optional?: boolean } = {}) {
341353 * @param body - Optional parsed request body (for userId extraction in dev mode)
342354 * @returns AuthContext with userId if authenticated
343355 */
344- export async function resolveAuth ( request : Request , body ?: any ) : Promise < {
356+ export async function resolveAuth (
357+ request : Request ,
358+ body ?: any ,
359+ ) : Promise < {
345360 authenticated : boolean ;
346361 userId ?: string ;
347362 method ?: string ;
@@ -373,21 +388,11 @@ export async function resolveAuth(request: Request, body?: any): Promise<{
373388 }
374389 }
375390
376- // Helper to get userId from body (same logic as authResolver middleware)
377- const getUserIdFromBody = ( ) => {
378- const providedUserId = body ?. userId ;
379- return providedUserId &&
380- typeof providedUserId === "string" &&
381- providedUserId . length > 0
382- ? providedUserId
383- : generateUUID ( ) ;
384- } ;
385-
386391 // Check API key (legacy)
387392 if ( isValidApiKey ( request ) ) {
388393 return {
389394 authenticated : true ,
390- userId : getUserIdFromBody ( ) ,
395+ userId : resolveProvidedUserId ( request , body ) ,
391396 method : "api_key" ,
392397 } ;
393398 }
@@ -396,7 +401,7 @@ export async function resolveAuth(request: Request, body?: any): Promise<{
396401 if ( config . mode === "none" ) {
397402 return {
398403 authenticated : true ,
399- userId : getUserIdFromBody ( ) ,
404+ userId : resolveProvidedUserId ( request , body ) ,
400405 method : "anonymous" ,
401406 } ;
402407 }
0 commit comments