refactor: use validate files on the tool level (#2152)
Fixes https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/2138 Closes #2150
This commit is contained in:
@@ -220,6 +220,12 @@ export class ToolHandler {
|
||||
|
||||
response.setRedactNetworkHeaders(this.serverArgs.redactNetworkHeaders);
|
||||
try {
|
||||
if (this.tool.verifyFilesSchema) {
|
||||
for (const key of this.tool.verifyFilesSchema) {
|
||||
const filePath = params[key];
|
||||
await context.validatePath(filePath as string);
|
||||
}
|
||||
}
|
||||
if (isPageScopedTool(this.tool)) {
|
||||
const pageId =
|
||||
typeof params.pageId === 'number' ? params.pageId : undefined;
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface BaseToolDefinition<
|
||||
};
|
||||
schema: Schema;
|
||||
blockedByDialog: boolean;
|
||||
verifyFilesSchema: Array<keyof Schema>;
|
||||
}
|
||||
|
||||
export interface ToolDefinition<
|
||||
|
||||
@@ -79,6 +79,7 @@ export const listConsoleMessages = definePageTool(cliArgs => {
|
||||
),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
response.setIncludeConsoleData(true, {
|
||||
pageSize: request.params.pageSize,
|
||||
@@ -105,6 +106,7 @@ export const getConsoleMessage = definePageTool({
|
||||
),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
response.attachConsoleMessage(request.params.msgid);
|
||||
},
|
||||
|
||||
@@ -100,6 +100,7 @@ export const emulate = definePageTool({
|
||||
),
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const page = request.page;
|
||||
await context.emulate(request.params, page.pptrPage);
|
||||
|
||||
@@ -22,9 +22,9 @@ export const installExtension = defineTool({
|
||||
.describe('Absolute path to the unpacked extension folder.'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: ['path'],
|
||||
handler: async (request, response, context) => {
|
||||
const {path} = request.params;
|
||||
await context.validatePath(path);
|
||||
const id = await context.installExtension(path);
|
||||
response.appendResponseLine(`Extension installed. Id: ${id}`);
|
||||
},
|
||||
@@ -41,6 +41,7 @@ export const uninstallExtension = defineTool({
|
||||
id: zod.string().describe('ID of the extension to uninstall.'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const {id} = request.params;
|
||||
await context.uninstallExtension(id);
|
||||
@@ -58,6 +59,7 @@ export const listExtensions = defineTool({
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (_request, response, _context) => {
|
||||
response.setListExtensions();
|
||||
},
|
||||
@@ -74,13 +76,13 @@ export const reloadExtension = defineTool({
|
||||
id: zod.string().describe('ID of the extension to reload.'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const {id} = request.params;
|
||||
const extension = await context.getExtension(id);
|
||||
if (!extension) {
|
||||
throw new Error(`Extension with ID ${id} not found.`);
|
||||
}
|
||||
await context.validatePath(extension.path);
|
||||
await context.installExtension(extension.path);
|
||||
response.appendResponseLine('Extension reloaded.');
|
||||
},
|
||||
@@ -97,6 +99,7 @@ export const triggerExtensionAction = defineTool({
|
||||
id: zod.string().describe('ID of the extension to trigger the action for.'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const {id} = request.params;
|
||||
await context.triggerExtensionAction(id);
|
||||
|
||||
+10
-2
@@ -103,6 +103,7 @@ export const click = definePageTool({
|
||||
includeSnapshot: includeSnapshotSchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
const uid = request.params.uid;
|
||||
const handle = await request.page.getElementByUid(uid);
|
||||
@@ -154,6 +155,7 @@ export const clickAt = definePageTool({
|
||||
includeSnapshot: includeSnapshotSchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
const page = request.page;
|
||||
const result = await page.waitForEventsAfterAction(async () => {
|
||||
@@ -189,6 +191,7 @@ export const hover = definePageTool({
|
||||
includeSnapshot: includeSnapshotSchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
const uid = request.params.uid;
|
||||
const handle = await request.page.getElementByUid(uid);
|
||||
@@ -316,6 +319,7 @@ export const fill = definePageTool({
|
||||
includeSnapshot: includeSnapshotSchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const page = request.page;
|
||||
const result = await page.waitForEventsAfterAction(async () => {
|
||||
@@ -346,6 +350,7 @@ export const typeText = definePageTool({
|
||||
submitKey: submitKeySchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
const page = request.page;
|
||||
const result = await page.waitForEventsAfterAction(async () => {
|
||||
@@ -376,6 +381,7 @@ export const drag = definePageTool({
|
||||
includeSnapshot: includeSnapshotSchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
const fromHandle = await request.page.getElementByUid(
|
||||
request.params.from_uid,
|
||||
@@ -423,6 +429,7 @@ export const fillForm = definePageTool({
|
||||
includeSnapshot: includeSnapshotSchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const page = request.page;
|
||||
let lastResult: WaitForEventsResult = {};
|
||||
@@ -461,9 +468,9 @@ export const uploadFile = definePageTool({
|
||||
includeSnapshot: includeSnapshotSchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
handler: async (request, response, context) => {
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response, _context) => {
|
||||
const {uid, filePath} = request.params;
|
||||
await context.validatePath(filePath);
|
||||
const handle = (await request.page.getElementByUid(
|
||||
uid,
|
||||
)) as ElementHandle<HTMLInputElement>;
|
||||
@@ -512,6 +519,7 @@ export const pressKey = definePageTool({
|
||||
includeSnapshot: includeSnapshotSchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
const page = request.page;
|
||||
const tokens = parseKey(request.params.key);
|
||||
|
||||
@@ -44,6 +44,7 @@ export const lighthouseAudit = definePageTool({
|
||||
.describe('Directory for reports. If omitted, uses temporary files.'),
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: ['outputDirPath'],
|
||||
handler: async (request, response, context) => {
|
||||
const page = request.page;
|
||||
const categories = [
|
||||
@@ -59,8 +60,6 @@ export const lighthouseAudit = definePageTool({
|
||||
outputDirPath,
|
||||
} = request.params;
|
||||
|
||||
await context.validatePath(outputDirPath);
|
||||
|
||||
const flags: Flags = {
|
||||
onlyCategories: categories,
|
||||
output: formats,
|
||||
|
||||
+6
-7
@@ -23,9 +23,9 @@ export const takeHeapSnapshot = definePageTool({
|
||||
.describe('A path to a .heapsnapshot file to save the heapsnapshot to.'),
|
||||
},
|
||||
blockedByDialog: true,
|
||||
handler: async (request, response, context) => {
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response) => {
|
||||
const page = request.page;
|
||||
await context.validatePath(request.params.filePath);
|
||||
|
||||
await page.pptrPage.captureHeapSnapshot({
|
||||
path: ensureExtension(request.params.filePath, '.heapsnapshot'),
|
||||
@@ -50,8 +50,8 @@ export const getHeapSnapshotSummary = defineTool({
|
||||
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response, context) => {
|
||||
await context.validatePath(request.params.filePath);
|
||||
const stats = await context.getHeapSnapshotStats(request.params.filePath);
|
||||
const staticData = await context.getHeapSnapshotStaticData(
|
||||
request.params.filePath,
|
||||
@@ -82,8 +82,8 @@ export const getHeapSnapshotDetails = defineTool({
|
||||
.describe('The page size for pagination of aggregates.'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response, context) => {
|
||||
await context.validatePath(request.params.filePath);
|
||||
const aggregates = await context.getHeapSnapshotAggregates(
|
||||
request.params.filePath,
|
||||
);
|
||||
@@ -111,8 +111,8 @@ export const getHeapSnapshotClassNodes = defineTool({
|
||||
pageSize: zod.number().optional().describe('The page size for pagination.'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response, context) => {
|
||||
await context.validatePath(request.params.filePath);
|
||||
const nodes = await context.getHeapSnapshotNodesById(
|
||||
request.params.filePath,
|
||||
request.params.id,
|
||||
@@ -135,6 +135,7 @@ export const getHeapSnapshotRetainers = defineTool({
|
||||
conditions: ['experimentalMemory'],
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: ['filePath'],
|
||||
schema: {
|
||||
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
|
||||
nodeId: zod.number().describe('The node ID to get retainers for.'),
|
||||
@@ -142,8 +143,6 @@ export const getHeapSnapshotRetainers = defineTool({
|
||||
pageSize: zod.number().optional().describe('The page size for pagination.'),
|
||||
},
|
||||
handler: async (request, response, context) => {
|
||||
await context.validatePath(request.params.filePath);
|
||||
|
||||
const retainers = await context.getHeapSnapshotRetainers(
|
||||
request.params.filePath,
|
||||
request.params.nodeId,
|
||||
|
||||
@@ -71,6 +71,7 @@ export const listNetworkRequests = definePageTool({
|
||||
),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const data = await request.page.getDevToolsData();
|
||||
response.attachDevToolsData(data);
|
||||
@@ -115,9 +116,8 @@ export const getNetworkRequest = definePageTool({
|
||||
),
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: ['requestFilePath', 'responseFilePath'],
|
||||
handler: async (request, response, context) => {
|
||||
await context.validatePath(request.params.requestFilePath);
|
||||
await context.validatePath(request.params.responseFilePath);
|
||||
if (request.params.reqid) {
|
||||
response.attachNetworkRequest(request.params.reqid, {
|
||||
requestFilePath: request.params.requestFilePath,
|
||||
|
||||
@@ -85,6 +85,7 @@ export const listPages = defineTool(args => {
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (_request, response) => {
|
||||
response.setIncludePages(true);
|
||||
response.setListThirdPartyDeveloperTools();
|
||||
@@ -112,6 +113,7 @@ export const selectPage = defineTool({
|
||||
.describe('Whether to focus the page and bring it to the top.'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const page = context.getPageById(request.params.pageId);
|
||||
context.selectPage(page);
|
||||
@@ -137,6 +139,7 @@ export const closePage = defineTool({
|
||||
.describe('The ID of the page to close. Call list_pages to list pages.'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
try {
|
||||
await context.closePage(request.params.pageId);
|
||||
@@ -189,6 +192,7 @@ export const newPage = defineTool(args => {
|
||||
...timeoutSchema,
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const page = await context.newPage(
|
||||
request.params.background,
|
||||
@@ -256,6 +260,7 @@ export const navigatePage = definePageTool(args => {
|
||||
...timeoutSchema,
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
const page = request.page;
|
||||
const options = {
|
||||
@@ -391,6 +396,7 @@ export const resizePage = definePageTool({
|
||||
height: zod.number().describe('Page height'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, _context) => {
|
||||
const page = request.page;
|
||||
|
||||
@@ -436,6 +442,7 @@ export const handleDialog = definePageTool({
|
||||
.describe('Optional prompt text to enter into the dialog.'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, _context) => {
|
||||
const page = request.page;
|
||||
const dialog = page.getDialog();
|
||||
@@ -487,6 +494,7 @@ export const getTabId = definePageTool({
|
||||
),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const page = context.getPageById(request.params.pageId);
|
||||
const tabId = (page.pptrPage as unknown as CdpPage)._tabId;
|
||||
|
||||
@@ -48,8 +48,8 @@ export const startTrace = definePageTool({
|
||||
filePath: filePathSchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response, context) => {
|
||||
await context.validatePath(request.params.filePath);
|
||||
if (context.isRunningPerformanceTrace()) {
|
||||
response.appendResponseLine(
|
||||
'Error: a performance trace is already running. Use performance_stop_trace to stop it. Only one trace can be running at any given time.',
|
||||
@@ -127,8 +127,8 @@ export const stopTrace = definePageTool({
|
||||
filePath: filePathSchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response, context) => {
|
||||
await context.validatePath(request.params.filePath);
|
||||
if (!context.isRunningPerformanceTrace()) {
|
||||
return;
|
||||
}
|
||||
@@ -163,6 +163,7 @@ export const analyzeInsight = definePageTool({
|
||||
),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const lastRecording = context.recordedTraces().at(-1);
|
||||
if (!lastRecording) {
|
||||
|
||||
@@ -39,8 +39,8 @@ export const startScreencast = definePageTool(args => ({
|
||||
),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response, context) => {
|
||||
await context.validatePath(request.params.filePath);
|
||||
if (context.getScreenRecorder() !== null) {
|
||||
response.appendResponseLine(
|
||||
'Error: a screencast recording is already in progress. Use screencast_stop to stop it before starting a new one.',
|
||||
@@ -103,6 +103,7 @@ export const stopScreencast = definePageTool({
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (_request, response, context) => {
|
||||
const data = context.getScreenRecorder();
|
||||
if (!data) {
|
||||
|
||||
@@ -51,8 +51,8 @@ export const screenshot = definePageTool({
|
||||
),
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response, context) => {
|
||||
await context.validatePath(request.params.filePath);
|
||||
if (request.params.uid && request.params.fullPage) {
|
||||
throw new Error('Providing both "uid" and "fullPage" is not allowed.');
|
||||
}
|
||||
|
||||
+1
-2
@@ -71,6 +71,7 @@ Example with arguments: \`(el) => {
|
||||
: {}),
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response, context) => {
|
||||
const {
|
||||
serviceWorkerId,
|
||||
@@ -81,8 +82,6 @@ Example with arguments: \`(el) => {
|
||||
filePath,
|
||||
} = request.params;
|
||||
|
||||
await context.validatePath(filePath);
|
||||
|
||||
if (cliArgs?.categoryExtensions && serviceWorkerId) {
|
||||
if (uidArgs && uidArgs.length > 0) {
|
||||
throw new Error(
|
||||
|
||||
@@ -19,6 +19,7 @@ export const screenshot = definePageTool({
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const page = request.page;
|
||||
const screenshot = await page.pptrPage.screenshot({
|
||||
@@ -44,6 +45,7 @@ export const navigate = definePageTool({
|
||||
url: zod.string().describe('URL to navigate to'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
const page = request.page;
|
||||
|
||||
@@ -82,6 +84,7 @@ export const evaluate = definePageTool({
|
||||
script: zod.string().describe(`JS script to run on the page`),
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
const page = request.page;
|
||||
try {
|
||||
|
||||
@@ -34,8 +34,8 @@ in the DevTools Elements panel (if any).`,
|
||||
),
|
||||
},
|
||||
blockedByDialog: true,
|
||||
handler: async (request, response, context) => {
|
||||
await context.validatePath(request.params.filePath);
|
||||
verifyFilesSchema: ['filePath'],
|
||||
handler: async (request, response) => {
|
||||
response.includeSnapshot({
|
||||
verbose: request.params.verbose ?? false,
|
||||
filePath: request.params.filePath,
|
||||
@@ -60,6 +60,7 @@ export const waitFor = definePageTool({
|
||||
...timeoutSchema,
|
||||
},
|
||||
blockedByDialog: true,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response, context) => {
|
||||
const page = request.page;
|
||||
await context.waitForTextOnPage(
|
||||
|
||||
@@ -51,6 +51,7 @@ export const listThirdPartyDeveloperTools = definePageTool({
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (_request, response, _context) => {
|
||||
response.setListThirdPartyDeveloperTools();
|
||||
},
|
||||
@@ -71,6 +72,7 @@ export const executeThirdPartyDeveloperTool = definePageTool({
|
||||
.describe('The JSON-stringified parameters to pass to the tool'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
const toolName = request.params.toolName;
|
||||
let params: Record<string, unknown> = {};
|
||||
|
||||
@@ -18,6 +18,7 @@ export const listWebMcpTools = definePageTool({
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (_request, response, _context) => {
|
||||
response.setListWebMcpTools();
|
||||
},
|
||||
@@ -38,6 +39,7 @@ export const executeWebMcpTool = definePageTool({
|
||||
.describe('The JSON-stringified parameters to pass to the WebMCP tool'),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async (request, response) => {
|
||||
const toolName = request.params.toolName;
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ describe('ToolHandler', () => {
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
pageScoped: true,
|
||||
handler: async () => {
|
||||
handlerCalled = true;
|
||||
@@ -78,6 +79,7 @@ describe('ToolHandler', () => {
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async () => {
|
||||
handlerCalled = true;
|
||||
},
|
||||
@@ -120,6 +122,7 @@ describe('ToolHandler', () => {
|
||||
url: zod.string(),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async () => {
|
||||
handlerCalled = true;
|
||||
},
|
||||
@@ -167,6 +170,7 @@ describe('ToolHandler', () => {
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async () => {
|
||||
handlerCalled = true;
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@ describe('metricsRegistry', () => {
|
||||
uid: zod.string(), // Should be blocked
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async () => {
|
||||
// no-op
|
||||
},
|
||||
@@ -72,6 +73,7 @@ describe('metricsRegistry', () => {
|
||||
argEnum: zod.enum(['foo', 'bar']),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async () => {
|
||||
// no-op
|
||||
},
|
||||
@@ -93,6 +95,7 @@ describe('metricsRegistry', () => {
|
||||
},
|
||||
schema: {},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async () => {
|
||||
// no-op
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user