@@ -3,14 +3,27 @@ import { NextResponse } from 'next/server';
33import { getUserFromAuth } from '@/lib/user.server' ;
44import { getEnvVariable } from '@/lib/dotenvx' ;
55
6- type QueryType = 'instance-events' ;
6+ type QueryType = 'instance-events' | 'all-events' ;
77
8- const validQueryTypes = new Set < QueryType > ( [ 'instance-events' ] ) ;
8+ const validQueryTypes = new Set < QueryType > ( [ 'instance-events' , 'all-events' ] ) ;
99
10- function buildQuery ( queryType : QueryType , sandboxId : string ) : string {
11- // sandboxId is validated as base64url [A-Za-z0-9_-]+ before reaching here
10+ // Validates that a value is safe to interpolate into SQL (alphanumeric, hyphens, underscores only)
11+ function isSafeIdentifier ( value : string ) : boolean {
12+ return / ^ [ A - Z a - z 0 - 9 _ - ] + $ / . test ( value ) ;
13+ }
14+
15+ type AllEventsParams = {
16+ sandboxId : string ;
17+ userId : string ;
18+ flyAppName : string | null ;
19+ flyMachineId : string | null ;
20+ offset : number ;
21+ } ;
22+
23+ function buildQuery ( queryType : QueryType , sandboxId : string , params ?: AllEventsParams ) : string {
1224 switch ( queryType ) {
1325 case 'instance-events' :
26+ // sandboxId is validated as base64url [A-Za-z0-9_-]+ before reaching here
1427 return `SELECT
1528 timestamp,
1629 blob1 AS event,
3346ORDER BY timestamp DESC
3447LIMIT 20
3548FORMAT JSON` ;
49+
50+ case 'all-events' : {
51+ // All identifiers are validated before reaching here
52+ const p = params as AllEventsParams ;
53+ const orClauses = [ `blob2 = '${ p . userId } '` , `blob8 = '${ p . sandboxId } '` ] ;
54+ if ( p . flyMachineId ) orClauses . push ( `blob7 = '${ p . flyMachineId } '` ) ;
55+ if ( p . flyAppName ) orClauses . push ( `blob6 = '${ p . flyAppName } '` ) ;
56+ return `SELECT
57+ timestamp,
58+ blob1 AS event,
59+ blob2 AS user_id,
60+ blob3 AS delivery,
61+ blob4 AS route,
62+ blob5 AS error,
63+ blob6 AS fly_app_name,
64+ blob7 AS fly_machine_id,
65+ blob8 AS sandbox_id,
66+ blob9 AS status,
67+ blob10 AS openclaw_version,
68+ blob11 AS image_tag,
69+ blob12 AS fly_region,
70+ blob13 AS label,
71+ double1 AS duration_ms,
72+ double2 AS value
73+ FROM kiloclaw_events
74+ WHERE
75+ (${ orClauses . join ( ' OR ' ) } )
76+ AND blob3 IN ('http', 'do', 'reconcile', 'queue')
77+ AND blob1 != 'platform.gateway.status.get'
78+ ORDER BY timestamp DESC
79+ LIMIT 100
80+ OFFSET ${ p . offset }
81+ FORMAT JSON` ;
82+ }
3683 }
3784}
3885
@@ -61,7 +108,7 @@ export async function GET(
61108 ) ;
62109 }
63110
64- if ( ! sandboxId || ! / ^ [ A - Z a - z 0 - 9 _ - ] + $ / . test ( sandboxId ) ) {
111+ if ( ! sandboxId || ! isSafeIdentifier ( sandboxId ) ) {
65112 return NextResponse . json ( { error : 'Invalid or missing sandboxId' } , { status : 400 } ) ;
66113 }
67114
@@ -75,7 +122,38 @@ export async function GET(
75122 ) ;
76123 }
77124
78- const sqlQuery = buildQuery ( queryType as QueryType , sandboxId ) ;
125+ let sqlQuery : string ;
126+
127+ if ( queryType === 'all-events' ) {
128+ const userId = searchParams . get ( 'userId' ) ;
129+ const flyAppName = searchParams . get ( 'flyAppName' ) ;
130+ const flyMachineId = searchParams . get ( 'flyMachineId' ) ;
131+ const offsetParam = searchParams . get ( 'offset' ) ;
132+ const offset = offsetParam ? parseInt ( offsetParam , 10 ) : 0 ;
133+
134+ if ( ! userId || ! isSafeIdentifier ( userId ) ) {
135+ return NextResponse . json ( { error : 'Invalid or missing userId' } , { status : 400 } ) ;
136+ }
137+ if ( flyAppName && ! isSafeIdentifier ( flyAppName ) ) {
138+ return NextResponse . json ( { error : 'Invalid flyAppName' } , { status : 400 } ) ;
139+ }
140+ if ( flyMachineId && ! isSafeIdentifier ( flyMachineId ) ) {
141+ return NextResponse . json ( { error : 'Invalid flyMachineId' } , { status : 400 } ) ;
142+ }
143+ if ( isNaN ( offset ) || offset < 0 ) {
144+ return NextResponse . json ( { error : 'Invalid offset' } , { status : 400 } ) ;
145+ }
146+
147+ sqlQuery = buildQuery ( 'all-events' , sandboxId , {
148+ sandboxId,
149+ userId,
150+ flyAppName : flyAppName ?? null ,
151+ flyMachineId : flyMachineId ?? null ,
152+ offset,
153+ } ) ;
154+ } else {
155+ sqlQuery = buildQuery ( queryType as QueryType , sandboxId ) ;
156+ }
79157
80158 const response = await fetch (
81159 `https://api.cloudflare.com/client/v4/accounts/${ accountId } /analytics_engine/sql` ,
0 commit comments