@@ -25,6 +25,14 @@ type MonitorEvent = {
2525 data ?: unknown
2626}
2727
28+ type FunctionHistoryItem = {
29+ ts : number
30+ name : string
31+ args : unknown [ ]
32+ runAsSystem : boolean
33+ user ?: { id ?: string ; email ?: string }
34+ }
35+
2836type EventQuery = {
2937 q ?: string
3038 type ?: string
@@ -91,11 +99,91 @@ const redactString = (value: string) => {
9199 return result
92100}
93101
102+ const stripSensitiveFields = ( value : Record < string , unknown > ) => {
103+ const out : Record < string , unknown > = { }
104+ Object . keys ( value ) . forEach ( ( key ) => {
105+ if ( SENSITIVE_KEYS . some ( ( re ) => re . test ( key ) ) ) return
106+ out [ key ] = value [ key ]
107+ } )
108+ return out
109+ }
110+
111+ const isErrorLike = ( value : unknown ) : value is Record < string , unknown > => {
112+ if ( ! value || typeof value !== 'object' ) return false
113+ if ( value instanceof Error ) return true
114+ const record = value as Record < string , unknown >
115+ return (
116+ typeof record . message === 'string' ||
117+ typeof record . stack === 'string' ||
118+ typeof record . name === 'string'
119+ )
120+ }
121+
122+ const sanitizeErrorLike = ( value : Record < string , unknown > , depth : number ) : Record < string , unknown > => {
123+ const out : Record < string , unknown > = { }
124+ const names = new Set < string > ( Object . getOwnPropertyNames ( value ) )
125+ ; [ 'name' , 'message' , 'stack' , 'code' , 'statusCode' , 'cause' ] . forEach ( ( key ) => names . add ( key ) )
126+
127+ names . forEach ( ( key ) => {
128+ if ( SENSITIVE_KEYS . some ( ( re ) => re . test ( key ) ) ) {
129+ out [ key ] = '[redacted]'
130+ return
131+ }
132+ const raw = value [ key ]
133+ if ( raw === value ) {
134+ out [ key ] = '[Circular]'
135+ return
136+ }
137+ if ( ( key === 'message' || key === 'stack' ) && typeof raw === 'string' ) {
138+ out [ key ] = DEFAULT_CONFIG . MONIT_REDACT_ERROR_DETAILS ? redactString ( raw ) : raw
139+ return
140+ }
141+ if ( typeof raw === 'string' ) {
142+ out [ key ] = redactString ( raw )
143+ return
144+ }
145+ if ( raw !== undefined ) {
146+ out [ key ] = sanitize ( raw , depth + 1 )
147+ }
148+ } )
149+
150+ if ( value instanceof Error ) {
151+ if ( ! out . name ) out . name = value . name
152+ if ( ! out . message ) {
153+ out . message = DEFAULT_CONFIG . MONIT_REDACT_ERROR_DETAILS ? redactString ( value . message ) : value . message
154+ }
155+ if ( ! out . stack && value . stack ) {
156+ out . stack = DEFAULT_CONFIG . MONIT_REDACT_ERROR_DETAILS ? redactString ( value . stack ) : value . stack
157+ }
158+ }
159+
160+ return out
161+ }
162+
163+ const getErrorDetails = ( error : unknown ) => {
164+ if ( isErrorLike ( error ) ) {
165+ const sanitized = sanitizeErrorLike ( error as Record < string , unknown > , 0 )
166+ const message =
167+ typeof sanitized . message === 'string' && sanitized . message
168+ ? sanitized . message
169+ : typeof sanitized . name === 'string' && sanitized . name
170+ ? sanitized . name
171+ : safeStringify ( error )
172+ const stack = typeof sanitized . stack === 'string' ? sanitized . stack : undefined
173+ return { message, stack }
174+ }
175+ if ( typeof error === 'string' ) return { message : error }
176+ return { message : safeStringify ( error ) }
177+ }
178+
94179const sanitize = ( value : unknown , depth = 0 ) : unknown => {
95180 if ( depth > MAX_DEPTH ) return '[max-depth]'
96181 if ( value === null || value === undefined ) return value
97182 if ( value instanceof Date ) return value . toISOString ( )
98183 if ( Buffer . isBuffer ( value ) ) return `[buffer ${ value . length } bytes]`
184+ if ( isErrorLike ( value ) ) {
185+ return sanitizeErrorLike ( value as Record < string , unknown > , depth )
186+ }
99187 if ( typeof value === 'object' ) {
100188 const maybeObjectId = value as { _bsontype ?: string ; toString ?: ( ) => string }
101189 if ( maybeObjectId ?. _bsontype === 'ObjectId' && typeof maybeObjectId . toString === 'function' ) {
@@ -248,6 +336,56 @@ const resolveAssetCandidates = (filename: string, prefix: string) => {
248336 return candidates
249337}
250338
339+ const resolveUserContext = async (
340+ app : FastifyInstance ,
341+ userId ?: string ,
342+ userPayload ?: Record < string , unknown >
343+ ) => {
344+ if ( userPayload && typeof userPayload === 'object' ) {
345+ return stripSensitiveFields ( userPayload )
346+ }
347+ if ( ! userId ) return undefined
348+
349+ const db = app . mongo . client . db ( DB_NAME )
350+ const authCollection = AUTH_CONFIG . authCollection ?? 'auth_users'
351+ const userCollection = AUTH_CONFIG . userCollection
352+ const userIdField = AUTH_CONFIG . user_id_field ?? 'id'
353+ const isObjectId = ObjectId . isValid ( userId )
354+ const lowerId = userId . toLowerCase ( )
355+ const emailLookup = userId . includes ( '@' ) ? lowerId : ''
356+
357+ const authSelector = isObjectId
358+ ? { _id : new ObjectId ( userId ) }
359+ : ( emailLookup ? { email : emailLookup } : { id : userId } )
360+ const authUser = await db . collection ( authCollection ) . findOne ( authSelector )
361+
362+ let customUser : Record < string , unknown > | null = null
363+ if ( userCollection ) {
364+ const customSelector = isObjectId
365+ ? { [ userIdField ] : userId }
366+ : ( emailLookup ? { email : emailLookup } : { [ userIdField ] : userId } )
367+ customUser = await db . collection ( userCollection ) . findOne ( customSelector )
368+ if ( ! customUser && isObjectId ) {
369+ customUser = await db . collection ( userCollection ) . findOne ( { _id : new ObjectId ( userId ) } )
370+ }
371+ }
372+
373+ const merged : Record < string , unknown > = {
374+ ...( customUser ? stripSensitiveFields ( customUser ) : { } ) ,
375+ ...( authUser ? stripSensitiveFields ( authUser as Record < string , unknown > ) : { } )
376+ }
377+
378+ if ( authUser && typeof ( authUser as { _id ?: unknown } ) . _id !== 'undefined' ) {
379+ merged . id = String ( ( authUser as { _id ?: ObjectId } ) . _id )
380+ } else if ( customUser && typeof customUser [ userIdField ] !== 'undefined' ) {
381+ merged . id = String ( customUser [ userIdField ] )
382+ } else {
383+ merged . id = userId
384+ }
385+
386+ return Object . keys ( merged ) . length ? merged : { id : userId }
387+ }
388+
251389const readAsset = ( filename : string , prefix : string ) => {
252390 const candidates = resolveAssetCandidates ( filename , prefix )
253391 for ( const candidate of candidates ) {
@@ -400,7 +538,15 @@ const createMonitoringPlugin = fp(async (
400538 const maxEvents = Math . max ( 1000 , DEFAULT_CONFIG . MONIT_MAX_EVENTS )
401539
402540 const eventStore = createEventStore ( maxAgeMs || DAY_MS , maxEvents )
541+ const functionHistory : FunctionHistoryItem [ ] = [ ]
542+ const maxHistory = 30
403543 const clients = new Set < { send : ( data : string ) => void ; readyState : number } > ( )
544+ const addFunctionHistory = ( entry : FunctionHistoryItem ) => {
545+ functionHistory . unshift ( entry )
546+ if ( functionHistory . length > maxHistory ) {
547+ functionHistory . splice ( maxHistory )
548+ }
549+ }
404550
405551 const addEvent = ( event : MonitorEvent ) => {
406552 const sanitizedEvent : MonitorEvent = {
@@ -499,7 +645,7 @@ const createMonitoringPlugin = fp(async (
499645 return
500646 }
501647 }
502- ; ( req as { __monitStart ?: number } ) . __monitStart = Date . now ( )
648+ ; ( req as { __monitStart ?: number } ) . __monitStart = Date . now ( )
503649 done ( )
504650 } )
505651
@@ -647,8 +793,18 @@ const createMonitoringPlugin = fp(async (
647793 return { items }
648794 } )
649795
796+ app . get ( `${ prefix } /api/functions/history` , async ( ) => ( {
797+ items : functionHistory . slice ( 0 , maxHistory )
798+ } ) )
799+
650800 app . post ( `${ prefix } /api/functions/invoke` , async ( req , reply ) => {
651- const body = req . body as { name ?: string ; arguments ?: unknown [ ] ; runAsSystem ?: boolean }
801+ const body = req . body as {
802+ name ?: string
803+ arguments ?: unknown [ ]
804+ runAsSystem ?: boolean
805+ userId ?: string
806+ user ?: Record < string , unknown >
807+ }
652808 const name = body ?. name
653809 const args = Array . isArray ( body ?. arguments ) ? body . arguments : [ ]
654810 if ( ! name ) {
@@ -665,21 +821,45 @@ const createMonitoringPlugin = fp(async (
665821 return { error : `Function "${ name } " not found` }
666822 }
667823
824+ const resolvedUser = await resolveUserContext ( app , body ?. userId , body ?. user )
825+ const safeArgs = ( Array . isArray ( args ) ? sanitize ( args ) : sanitize ( [ args ] ) ) as unknown [ ]
826+ const userInfo = resolvedUser
827+ ? {
828+ id : typeof ( resolvedUser as { id ?: unknown } ) . id === 'string'
829+ ? ( resolvedUser as { id ?: string } ) . id
830+ : undefined ,
831+ email : typeof ( resolvedUser as { email ?: unknown } ) . email === 'string'
832+ ? ( resolvedUser as { email ?: string } ) . email
833+ : undefined
834+ }
835+ : undefined
836+ addFunctionHistory ( {
837+ ts : Date . now ( ) ,
838+ name,
839+ args : safeArgs ,
840+ runAsSystem : body ?. runAsSystem !== false ,
841+ user : userInfo
842+ } )
843+
668844 addEvent ( {
669845 id : createEventId ( ) ,
670846 ts : Date . now ( ) ,
671847 type : 'function' ,
672848 source : 'monit' ,
673849 message : `invoke ${ name } ` ,
674- data : sanitize ( { args } )
850+ data : sanitize ( {
851+ args,
852+ user : userInfo ,
853+ runAsSystem : body ?. runAsSystem !== false
854+ } )
675855 } )
676856
677857 try {
678858 const result = await GenerateContext ( {
679859 args,
680860 app : appRef ,
681861 rules,
682- user : { id : 'monitor' , role : 'system' } ,
862+ user : resolvedUser ?? { id : 'monitor' , role : 'system' } ,
683863 currentFunction,
684864 functionsList,
685865 services,
@@ -696,7 +876,8 @@ const createMonitoringPlugin = fp(async (
696876 data : sanitize ( { error } )
697877 } )
698878 reply . code ( 500 )
699- return { error : 'Function invocation failed' }
879+ const details = getErrorDetails ( error )
880+ return { error : details . message , stack : details . stack }
700881 }
701882 } )
702883
0 commit comments