@@ -5,7 +5,9 @@ import { eq } from 'drizzle-orm'
55import { type NextRequest , NextResponse } from 'next/server'
66import { z } from 'zod'
77import { checkServerSideUsageLimits } from '@/lib/billing/calculations/usage-monitor'
8+ import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
89import { checkInternalApiKey } from '@/lib/copilot/request/http'
10+ import { withIncomingGoSpan } from '@/lib/copilot/request/otel'
911
1012const logger = createLogger ( 'CopilotApiKeysValidate' )
1113
@@ -14,54 +16,83 @@ const ValidateApiKeySchema = z.object({
1416} )
1517
1618export async function POST ( req : NextRequest ) {
17- try {
18- const auth = checkInternalApiKey ( req )
19- if ( ! auth . success ) {
20- return new NextResponse ( null , { status : 401 } )
21- }
19+ // Incoming-from-Go: extracts traceparent so this handler's work shows
20+ // up as a child of the Go-side `sim.validate_api_key` span in the same
21+ // trace. If there's no traceparent (manual curl / browser), the helper
22+ // falls back to a new root span.
23+ return withIncomingGoSpan (
24+ req . headers ,
25+ TraceSpan . CopilotAuthValidateApiKey ,
26+ {
27+ 'http.method' : 'POST' ,
28+ 'http.route' : '/api/copilot/api-keys/validate' ,
29+ } ,
30+ async ( span ) => {
31+ try {
32+ const auth = checkInternalApiKey ( req )
33+ if ( ! auth . success ) {
34+ span . setAttribute ( 'copilot.validate.outcome' , 'internal_auth_failed' )
35+ span . setAttribute ( 'http.status_code' , 401 )
36+ return new NextResponse ( null , { status : 401 } )
37+ }
2238
23- const body = await req . json ( ) . catch ( ( ) => null )
39+ const body = await req . json ( ) . catch ( ( ) => null )
40+ const validationResult = ValidateApiKeySchema . safeParse ( body )
41+ if ( ! validationResult . success ) {
42+ logger . warn ( 'Invalid validation request' , { errors : validationResult . error . errors } )
43+ span . setAttribute ( 'copilot.validate.outcome' , 'invalid_body' )
44+ span . setAttribute ( 'http.status_code' , 400 )
45+ return NextResponse . json (
46+ {
47+ error : 'userId is required' ,
48+ details : validationResult . error . errors ,
49+ } ,
50+ { status : 400 }
51+ )
52+ }
2453
25- const validationResult = ValidateApiKeySchema . safeParse ( body )
54+ const { userId } = validationResult . data
55+ span . setAttribute ( 'user.id' , userId )
2656
27- if ( ! validationResult . success ) {
28- logger . warn ( 'Invalid validation request' , { errors : validationResult . error . errors } )
29- return NextResponse . json (
30- {
31- error : 'userId is required' ,
32- details : validationResult . error . errors ,
33- } ,
34- { status : 400 }
35- )
36- }
57+ const [ existingUser ] = await db . select ( ) . from ( user ) . where ( eq ( user . id , userId ) ) . limit ( 1 )
58+ if ( ! existingUser ) {
59+ logger . warn ( '[API VALIDATION] userId does not exist' , { userId } )
60+ span . setAttribute ( 'copilot.validate.outcome' , 'user_not_found' )
61+ span . setAttribute ( 'http.status_code' , 403 )
62+ return NextResponse . json ( { error : 'User not found' } , { status : 403 } )
63+ }
3764
38- const { userId } = validationResult . data
65+ logger . info ( '[API VALIDATION] Validating usage limit' , { userId } )
66+ const { isExceeded, currentUsage, limit } = await checkServerSideUsageLimits ( userId )
67+ span . setAttributes ( {
68+ 'billing.usage.current' : currentUsage ,
69+ 'billing.usage.limit' : limit ,
70+ 'billing.usage.exceeded' : isExceeded ,
71+ } )
3972
40- const [ existingUser ] = await db . select ( ) . from ( user ) . where ( eq ( user . id , userId ) ) . limit ( 1 )
41- if ( ! existingUser ) {
42- logger . warn ( '[API VALIDATION] userId does not exist' , { userId } )
43- return NextResponse . json ( { error : 'User not found' } , { status : 403 } )
44- }
73+ logger . info ( '[API VALIDATION] Usage limit validated' , {
74+ userId,
75+ currentUsage,
76+ limit,
77+ isExceeded,
78+ } )
4579
46- logger . info ( '[API VALIDATION] Validating usage limit' , { userId } )
80+ if ( isExceeded ) {
81+ logger . info ( '[API VALIDATION] Usage exceeded' , { userId, currentUsage, limit } )
82+ span . setAttribute ( 'copilot.validate.outcome' , 'usage_exceeded' )
83+ span . setAttribute ( 'http.status_code' , 402 )
84+ return new NextResponse ( null , { status : 402 } )
85+ }
4786
48- const { isExceeded, currentUsage, limit } = await checkServerSideUsageLimits ( userId )
49-
50- logger . info ( '[API VALIDATION] Usage limit validated' , {
51- userId,
52- currentUsage,
53- limit,
54- isExceeded,
55- } )
56-
57- if ( isExceeded ) {
58- logger . info ( '[API VALIDATION] Usage exceeded' , { userId, currentUsage, limit } )
59- return new NextResponse ( null , { status : 402 } )
60- }
61-
62- return new NextResponse ( null , { status : 200 } )
63- } catch ( error ) {
64- logger . error ( 'Error validating usage limit' , { error } )
65- return NextResponse . json ( { error : 'Failed to validate usage' } , { status : 500 } )
66- }
87+ span . setAttribute ( 'copilot.validate.outcome' , 'ok' )
88+ span . setAttribute ( 'http.status_code' , 200 )
89+ return new NextResponse ( null , { status : 200 } )
90+ } catch ( error ) {
91+ logger . error ( 'Error validating usage limit' , { error } )
92+ span . setAttribute ( 'copilot.validate.outcome' , 'internal_error' )
93+ span . setAttribute ( 'http.status_code' , 500 )
94+ return NextResponse . json ( { error : 'Failed to validate usage' } , { status : 500 } )
95+ }
96+ } ,
97+ )
6798}
0 commit comments