-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: make some cli operations available through a maintenance endpoint
- Loading branch information
Showing
6 changed files
with
103 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { program } from '@etabli/src/cli/program'; | ||
|
||
// This would break imports from Next.js so isolating it to be run only by CLI | ||
program.parse(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { Command } from '@commander-js/extra-typings'; | ||
import { NextApiRequest, NextApiResponse } from 'next'; | ||
import { z } from 'zod'; | ||
|
||
import { program } from '@etabli/src/cli/program'; | ||
import { unexpectedCliMaintenanceCommandError } from '@etabli/src/models/entities/errors'; | ||
import { apiHandlerWrapper } from '@etabli/src/utils/api'; | ||
import { assertMaintenanceOperationAuthenticated } from '@etabli/src/utils/maintenance'; | ||
|
||
export const HandlerBodySchema = z | ||
.object({ | ||
command: z.string().min(1).max(1000), | ||
}) | ||
.strict(); | ||
export type HandlerBodySchemaType = z.infer<typeof HandlerBodySchema>; | ||
|
||
const commandsPrefixesWhitelist: string[] = [ | ||
'domain fetch', | ||
'domain format', | ||
'domain enhance', | ||
'repository fetch', | ||
'repository format', | ||
'repository enhance', | ||
'tool fetch', | ||
'tool format', | ||
'tool enhance', | ||
'llm initialize', | ||
'llm initiatives', | ||
'llm tools', | ||
'initiative infer', | ||
'initiative feed', | ||
]; | ||
|
||
// This endpoint allows running cron job commands (that are defined in the CLI) without connecting to the host (note also it would imply from us to make another build in addition to the Next.js build) | ||
// (it avoids maintaining 2 implementations when changing parameters and so on) | ||
export async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
assertMaintenanceOperationAuthenticated(req); | ||
|
||
const body = HandlerBodySchema.parse(req.body); | ||
|
||
// It could be risky to expose the whole CLI due to future implementations, so whitelisting commands we allow | ||
// Note: `commander.js` was not made to reuse easily subcommands to create another program, so we hack a bit we filter based on raw strings | ||
if (!commandsPrefixesWhitelist.some((commandPrefix) => body.command.startsWith(commandPrefix))) { | ||
throw unexpectedCliMaintenanceCommandError; | ||
} | ||
|
||
// [WORKAROUND] We cannot check if it matches a command or not, in case none it terminates the running process | ||
// We can add a catcher but we cannot custom the response since global to the instance... so using a clone to wait for it | ||
let commandFound = true; | ||
|
||
const cloneProgram = new Command(); | ||
for (const command of program.commands) { | ||
cloneProgram.addCommand(command); | ||
} | ||
|
||
cloneProgram.on('command:*', () => { | ||
commandFound = false; | ||
}); | ||
|
||
await cloneProgram.parseAsync(body.command.split(' '), { from: 'user' }); | ||
|
||
if (!commandFound) { | ||
throw unexpectedCliMaintenanceCommandError; | ||
} | ||
|
||
res.send(`the following command has been executed with success:\n${body.command}`); | ||
} | ||
|
||
export default apiHandlerWrapper(handler, { | ||
restrictMethods: ['POST'], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import createHttpError from 'http-errors'; | ||
import { NextApiRequest } from 'next'; | ||
|
||
const maintenanceApiKey = process.env.MAINTENANCE_API_KEY; | ||
|
||
export function isAuthenticated(apiKeyHeader?: string): boolean { | ||
// If the maintenance api key is not defined on the server we prevent executing operations | ||
return !!maintenanceApiKey && maintenanceApiKey === apiKeyHeader; | ||
} | ||
|
||
// Check the originator has the maintenance secret | ||
export function assertMaintenanceOperationAuthenticated(req: NextApiRequest) { | ||
if (!isAuthenticated((req.headers as any)['x-api-key'])) { | ||
console.log('someone is trying to trigger a maintenance operation without being authenticated'); | ||
|
||
throw new createHttpError.Unauthorized(`invalid api key`); | ||
} | ||
} |