From cc838962599b97f5a64f10f1404414437fc8347c Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Sun, 18 Aug 2024 14:39:20 +0300 Subject: [PATCH 1/4] impr: use tsrest for dev endpoints (@fehmer) --- backend/__tests__/api/controllers/dev.spec.ts | 0 backend/scripts/openapi.ts | 6 ++ backend/src/api/controllers/dev.ts | 16 +++-- backend/src/api/routes/dev.ts | 49 ++++++---------- backend/src/api/routes/index.ts | 4 +- frontend/src/ts/ape/endpoints/dev.ts | 15 ----- frontend/src/ts/ape/endpoints/index.ts | 2 - frontend/src/ts/ape/index.ts | 4 +- frontend/src/ts/ape/types/dev.d.ts | 14 ----- frontend/src/ts/modals/simple-modals.ts | 7 ++- packages/contracts/src/dev.ts | 58 +++++++++++++++++++ packages/contracts/src/index.ts | 2 + packages/contracts/src/schemas/api.ts | 5 +- 13 files changed, 105 insertions(+), 77 deletions(-) create mode 100644 backend/__tests__/api/controllers/dev.spec.ts delete mode 100644 frontend/src/ts/ape/endpoints/dev.ts delete mode 100644 frontend/src/ts/ape/types/dev.d.ts create mode 100644 packages/contracts/src/dev.ts diff --git a/backend/__tests__/api/controllers/dev.spec.ts b/backend/__tests__/api/controllers/dev.spec.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/backend/scripts/openapi.ts b/backend/scripts/openapi.ts index d3a9354d17dd..6d505c8cb90c 100644 --- a/backend/scripts/openapi.ts +++ b/backend/scripts/openapi.ts @@ -105,6 +105,12 @@ export function getOpenApi(): OpenAPIObject { "x-displayName": "Server configuration", "x-public": "yes", }, + { + name: "dev", + description: + "Development related endpoints. Only available on dev environment", + "x-displayName": "Development", + }, ], }, diff --git a/backend/src/api/controllers/dev.ts b/backend/src/api/controllers/dev.ts index 37175651eed6..5e6964bbcfae 100644 --- a/backend/src/api/controllers/dev.ts +++ b/backend/src/api/controllers/dev.ts @@ -1,4 +1,4 @@ -import { MonkeyResponse } from "../../utils/monkey-response"; +import { MonkeyResponse2 } from "../../utils/monkey-response"; import * as UserDal from "../../dal/user"; import FirebaseAdmin from "../../init/firebase-admin"; import Logger from "../../utils/logger"; @@ -15,10 +15,14 @@ import { PersonalBest, PersonalBests, } from "@monkeytype/contracts/schemas/shared"; +import { + GenerateDataRequest, + GenerateDataResponse, +} from "@monkeytype/contracts/dev"; type GenerateDataOptions = { - firstTestTimestamp: Date; - lastTestTimestamp: Date; + firstTestTimestamp: Date | number; + lastTestTimestamp: Date | number; minTestsPerDay: number; maxTestsPerDay: number; }; @@ -31,8 +35,8 @@ const CREATE_RESULT_DEFAULT_OPTIONS: GenerateDataOptions = { }; export async function createTestData( - req: MonkeyTypes.Request -): Promise { + req: MonkeyTypes.Request2 +): Promise { const { username, createUser } = req.body; const user = await getOrCreateUser(username, "password", createUser); @@ -42,7 +46,7 @@ export async function createTestData( await updateUser(uid); await updateLeaderboard(); - return new MonkeyResponse("test data created", { uid, email }, 200); + return new MonkeyResponse2("test data created", { uid, email }); } async function getOrCreateUser( diff --git a/backend/src/api/routes/dev.ts b/backend/src/api/routes/dev.ts index 2c2dd2a42cb4..2fa2c2e6bffb 100644 --- a/backend/src/api/routes/dev.ts +++ b/backend/src/api/routes/dev.ts @@ -1,35 +1,22 @@ -import { Router } from "express"; -import joi from "joi"; -import { createTestData } from "../controllers/dev"; -import { isDevEnvironment } from "../../utils/misc"; +import { devContract } from "@monkeytype/contracts/dev"; +import { initServer } from "@ts-rest/express"; import { validate } from "../../middlewares/configuration"; -import { validateRequest } from "../../middlewares/validation"; -import { asyncHandler } from "../../middlewares/utility"; - -const router = Router(); +import { isDevEnvironment } from "../../utils/misc"; +import * as DevController from "../controllers/dev"; +import { callController } from "../ts-rest-adapter"; -router.use( - validate({ - criteria: () => { - return isDevEnvironment(); - }, - invalidMessage: "Development endpoints are only available in DEV mode.", - }) -); +const onlyAvailableOnDev = validate({ + criteria: () => { + return isDevEnvironment(); + }, + invalidMessage: "Development endpoints are only available in DEV mode.", +}); -router.post( - "/generateData", - validateRequest({ - body: { - username: joi.string().required(), - createUser: joi.boolean().optional(), - firstTestTimestamp: joi.number().optional(), - lastTestTimestamp: joi.number().optional(), - minTestsPerDay: joi.number().optional(), - maxTestsPerDay: joi.number().optional(), - }, - }), - asyncHandler(createTestData) -); +const s = initServer(); -export default router; +export default s.router(devContract, { + generateData: { + middleware: [onlyAvailableOnDev], + handler: async (r) => callController(DevController.createTestData)(r), + }, +}); diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index df4f7988ab87..725360033ff8 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -57,6 +57,7 @@ const router = s.router(contract, { leaderboards, results, configuration, + dev, }); export function addApiRoutes(app: Application): void { @@ -139,9 +140,6 @@ function applyDevApiRoutes(app: Application): void { } next(); }); - - //enable dev edpoints - app.use("/dev", dev); } } diff --git a/frontend/src/ts/ape/endpoints/dev.ts b/frontend/src/ts/ape/endpoints/dev.ts deleted file mode 100644 index 0128ed376e7f..000000000000 --- a/frontend/src/ts/ape/endpoints/dev.ts +++ /dev/null @@ -1,15 +0,0 @@ -const BASE_PATH = "/dev"; - -export default class Dev { - constructor(private httpClient: Ape.HttpClient) { - this.httpClient = httpClient; - } - - async generateData( - params: Ape.Dev.GenerateData - ): Ape.EndpointResponse { - return await this.httpClient.post(BASE_PATH + "/generateData", { - payload: params, - }); - } -} diff --git a/frontend/src/ts/ape/endpoints/index.ts b/frontend/src/ts/ape/endpoints/index.ts index 240fbcbf1bc0..8cf7685b8aee 100644 --- a/frontend/src/ts/ape/endpoints/index.ts +++ b/frontend/src/ts/ape/endpoints/index.ts @@ -1,9 +1,7 @@ import Quotes from "./quotes"; import Users from "./users"; -import Dev from "./dev"; export default { Quotes, Users, - Dev, }; diff --git a/frontend/src/ts/ape/index.ts b/frontend/src/ts/ape/index.ts index 79013111f124..67ea6615b5cd 100644 --- a/frontend/src/ts/ape/index.ts +++ b/frontend/src/ts/ape/index.ts @@ -3,6 +3,7 @@ import { buildHttpClient } from "./adapters/axios-adapter"; import { envConfig } from "../constants/env-config"; import { buildClient } from "./adapters/ts-rest-adapter"; import { contract } from "@monkeytype/contracts"; +import { devContract } from "@monkeytype/contracts/dev"; const API_PATH = ""; const BASE_URL = envConfig.backendUrl; @@ -10,13 +11,14 @@ const API_URL = `${BASE_URL}${API_PATH}`; const httpClient = buildHttpClient(API_URL, 10_000); const tsRestClient = buildClient(contract, BASE_URL, 10_000); +const devClient = buildClient(devContract, BASE_URL, 240_000); // API Endpoints const Ape = { ...tsRestClient, users: new endpoints.Users(httpClient), quotes: new endpoints.Quotes(httpClient), - dev: new endpoints.Dev(buildHttpClient(API_URL, 240_000)), + dev: devClient, }; export default Ape; diff --git a/frontend/src/ts/ape/types/dev.d.ts b/frontend/src/ts/ape/types/dev.d.ts deleted file mode 100644 index 9d3755a3b72b..000000000000 --- a/frontend/src/ts/ape/types/dev.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare namespace Ape.Dev { - type GenerateData = { - username: string; - createUser?: boolean; - firstTestTimestamp?: number; - lastTestTimestamp?: number; - minTestsPerDay?: number; - maxTestsPerDay?: number; - }; - type GenerateDataResponse = { - uid: string; - email: string; - }; -} diff --git a/frontend/src/ts/modals/simple-modals.ts b/frontend/src/ts/modals/simple-modals.ts index ab13d34c0d14..f40330366939 100644 --- a/frontend/src/ts/modals/simple-modals.ts +++ b/frontend/src/ts/modals/simple-modals.ts @@ -34,6 +34,7 @@ import { TextInput, } from "../utils/simple-modal"; import { ShowOptions } from "../utils/animated-modal"; +import { GenerateDataRequest } from "@monkeytype/contracts/dev"; type PopupKey = | "updateEmail" @@ -1308,7 +1309,7 @@ list.devGenerateData = new SimpleModal({ minTestsPerDay, maxTestsPerDay ): Promise => { - const request: Ape.Dev.GenerateData = { + const request: GenerateDataRequest = { username, createUser: createUser === "true", }; @@ -1321,11 +1322,11 @@ list.devGenerateData = new SimpleModal({ if (maxTestsPerDay !== undefined && maxTestsPerDay.length > 0) request.maxTestsPerDay = Number.parseInt(maxTestsPerDay); - const result = await Ape.dev.generateData(request); + const result = await Ape.dev.generateData({ body: request }); return { status: result.status === 200 ? 1 : -1, - message: result.message, + message: result.body.message, hideOptions: { clearModalChain: true, }, diff --git a/packages/contracts/src/dev.ts b/packages/contracts/src/dev.ts new file mode 100644 index 000000000000..1a5a19623e61 --- /dev/null +++ b/packages/contracts/src/dev.ts @@ -0,0 +1,58 @@ +import { initContract } from "@ts-rest/core"; +import { z } from "zod"; +import { + CommonResponses, + EndpointMetadata, + responseWithData, +} from "./schemas/api"; +import { IdSchema } from "./schemas/util"; + +export const GenerateDataRequestSchema = z.object({ + username: z.string(), + createUser: z + .boolean() + .optional() + .describe( + "If `true` create user with @example.com and password `password`. If false user has to exist." + ), + firstTestTimestamp: z.number().int().nonnegative().optional(), + lastTestTimestamp: z.number().int().nonnegative().optional(), + minTestsPerDay: z.number().int().nonnegative().optional(), + maxTestsPerDay: z.number().int().nonnegative().optional(), +}); +export type GenerateDataRequest = z.infer; + +export const GenerateDataResponseSchema = responseWithData( + z.object({ + uid: IdSchema, + email: z.string().email(), + }) +); +export type GenerateDataResponse = z.infer; + +const c = initContract(); +export const devContract = c.router( + { + generateData: { + summary: "generate data", + description: "Generate test results for the given user.", + method: "POST", + path: "/generateData", + body: GenerateDataRequestSchema.strict(), + responses: { + 200: GenerateDataResponseSchema, + }, + }, + }, + { + pathPrefix: "/dev", + strictStatusCodes: true, + metadata: { + openApiTags: "dev", + authenticationOptions: { + isPublic: true, + }, + } as EndpointMetadata, + commonResponses: CommonResponses, + } +); diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 321af82ac64b..dd7068fd1a03 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -8,6 +8,7 @@ import { publicContract } from "./public"; import { leaderboardsContract } from "./leaderboards"; import { resultsContract } from "./results"; import { configurationContract } from "./configuration"; +import { devContract } from "./dev"; const c = initContract(); @@ -21,4 +22,5 @@ export const contract = c.router({ leaderboards: leaderboardsContract, results: resultsContract, configuration: configurationContract, + dev: devContract, }); diff --git a/packages/contracts/src/schemas/api.ts b/packages/contracts/src/schemas/api.ts index ff7a39240502..affdeda6a04e 100644 --- a/packages/contracts/src/schemas/api.ts +++ b/packages/contracts/src/schemas/api.ts @@ -9,7 +9,8 @@ export type OpenApiTag = | "public" | "leaderboards" | "results" - | "configuration"; + | "configuration" + | "dev"; export type EndpointMetadata = { /** Authentication options, by default a bearer token is required. */ @@ -18,7 +19,7 @@ export type EndpointMetadata = { }; export type RequestAuthenticationOptions = { - /** Endpoint is accessible without any authentication. If `false` bearer authentication is required. */ + /** Endpoint is accessible without any authentication. If `false` bearer authentication is requuired. */ isPublic?: boolean; /** Endpoint is accessible with ape key authentication in _addition_ to the bearer authentication. */ acceptApeKeys?: boolean; From 8f682bf0a5e8e8bf7d7f56c2691ec8657b78ff51 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Sun, 18 Aug 2024 15:42:08 +0300 Subject: [PATCH 2/4] cleanup --- backend/__tests__/api/controllers/dev.spec.ts | 32 +++++++++++++++++++ backend/src/api/controllers/dev.ts | 31 +++++++----------- backend/src/api/routes/dev.ts | 13 ++------ backend/src/middlewares/utility.ts | 10 ++++++ 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/backend/__tests__/api/controllers/dev.spec.ts b/backend/__tests__/api/controllers/dev.spec.ts index e69de29bb2d1..fc96df7db906 100644 --- a/backend/__tests__/api/controllers/dev.spec.ts +++ b/backend/__tests__/api/controllers/dev.spec.ts @@ -0,0 +1,32 @@ +import request from "supertest"; +import app from "../../../src/app"; + +import { ObjectId } from "mongodb"; +import * as Misc from "../../../src/utils/misc"; + +const uid = new ObjectId().toHexString(); +const mockApp = request(app); + +describe("DevController", () => { + describe("generate testData", () => { + const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment"); + + beforeEach(() => { + isDevEnvironmentMock.mockReset(); + isDevEnvironmentMock.mockReturnValue(true); + }); + it("should fail on prod", async () => { + //GIVEN + isDevEnvironmentMock.mockReturnValue(false); + //WHEN + const { body } = await mockApp + .post("/dev/generateData") + .send({ username: "test" }) + .expect(503); + //THEN + expect(body.message).toEqual( + "Development endpoints are only available in DEV mode." + ); + }); + }); +}); diff --git a/backend/src/api/controllers/dev.ts b/backend/src/api/controllers/dev.ts index 5e6964bbcfae..c576cb54b231 100644 --- a/backend/src/api/controllers/dev.ts +++ b/backend/src/api/controllers/dev.ts @@ -9,7 +9,7 @@ import { roundTo2 } from "../../utils/misc"; import { ObjectId } from "mongodb"; import * as LeaderboardDal from "../../dal/leaderboards"; import MonkeyError from "../../utils/error"; -import isNumber from "lodash/isNumber"; + import { Mode, PersonalBest, @@ -20,16 +20,9 @@ import { GenerateDataResponse, } from "@monkeytype/contracts/dev"; -type GenerateDataOptions = { - firstTestTimestamp: Date | number; - lastTestTimestamp: Date | number; - minTestsPerDay: number; - maxTestsPerDay: number; -}; - -const CREATE_RESULT_DEFAULT_OPTIONS: GenerateDataOptions = { - firstTestTimestamp: DateUtils.startOfDay(new UTCDate(Date.now())), - lastTestTimestamp: DateUtils.endOfDay(new UTCDate(Date.now())), +const CREATE_RESULT_DEFAULT_OPTIONS: Partial = { + firstTestTimestamp: DateUtils.startOfDay(new UTCDate(Date.now())).valueOf(), + lastTestTimestamp: DateUtils.endOfDay(new UTCDate(Date.now())).valueOf(), minTestsPerDay: 0, maxTestsPerDay: 50, }; @@ -77,23 +70,23 @@ async function getOrCreateUser( async function createTestResults( user: MonkeyTypes.DBUser, - configOptions: Partial + configOptions: GenerateDataRequest ): Promise { const config = { ...CREATE_RESULT_DEFAULT_OPTIONS, ...configOptions, }; - if (isNumber(config.firstTestTimestamp)) - config.firstTestTimestamp = toDate(config.firstTestTimestamp); - if (isNumber(config.lastTestTimestamp)) - config.lastTestTimestamp = toDate(config.lastTestTimestamp); + const start = toDate(config.firstTestTimestamp as number); + const end = toDate(config.lastTestTimestamp as number); const days = DateUtils.eachDayOfInterval({ - start: config.firstTestTimestamp, - end: config.lastTestTimestamp, + start, + end, }).map((day) => ({ timestamp: DateUtils.startOfDay(day), - amount: Math.round(random(config.minTestsPerDay, config.maxTestsPerDay)), + amount: Math.round( + random(config.minTestsPerDay as number, config.maxTestsPerDay as number) + ), })); for (const day of days) { diff --git a/backend/src/api/routes/dev.ts b/backend/src/api/routes/dev.ts index 2fa2c2e6bffb..3b4678a20bc6 100644 --- a/backend/src/api/routes/dev.ts +++ b/backend/src/api/routes/dev.ts @@ -1,22 +1,15 @@ import { devContract } from "@monkeytype/contracts/dev"; import { initServer } from "@ts-rest/express"; -import { validate } from "../../middlewares/configuration"; -import { isDevEnvironment } from "../../utils/misc"; + import * as DevController from "../controllers/dev"; import { callController } from "../ts-rest-adapter"; - -const onlyAvailableOnDev = validate({ - criteria: () => { - return isDevEnvironment(); - }, - invalidMessage: "Development endpoints are only available in DEV mode.", -}); +import { onlyAvailableOnDev } from "../../middlewares/utility"; const s = initServer(); export default s.router(devContract, { generateData: { - middleware: [onlyAvailableOnDev], + middleware: [onlyAvailableOnDev()], handler: async (r) => callController(DevController.createTestData)(r), }, }); diff --git a/backend/src/middlewares/utility.ts b/backend/src/middlewares/utility.ts index d202ffc6aeaa..f2dcb6a04903 100644 --- a/backend/src/middlewares/utility.ts +++ b/backend/src/middlewares/utility.ts @@ -2,6 +2,7 @@ import _ from "lodash"; import type { Request, Response, NextFunction, RequestHandler } from "express"; import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response"; import { recordClientVersion as prometheusRecordClientVersion } from "../utils/prometheus"; +import { validate } from "./configuration"; export const emptyMiddleware = ( _req: MonkeyTypes.Request, @@ -49,3 +50,12 @@ export function recordClientVersion(): RequestHandler { next(); }; } + +export function onlyAvailableOnDev(): RequestHandler { + return validate({ + criteria: () => { + return isDevEnvironment(); + }, + invalidMessage: "Development endpoints are only available in DEV mode.", + }); +} From 9e83918b2ff2ad648c63f358b1bf27a48f316b35 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 19 Aug 2024 11:35:54 +0300 Subject: [PATCH 3/4] tests --- backend/__tests__/api/controllers/dev.spec.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/backend/__tests__/api/controllers/dev.spec.ts b/backend/__tests__/api/controllers/dev.spec.ts index fc96df7db906..0ac16cab2065 100644 --- a/backend/__tests__/api/controllers/dev.spec.ts +++ b/backend/__tests__/api/controllers/dev.spec.ts @@ -15,6 +15,7 @@ describe("DevController", () => { isDevEnvironmentMock.mockReset(); isDevEnvironmentMock.mockReturnValue(true); }); + it("should fail on prod", async () => { //GIVEN isDevEnvironmentMock.mockReturnValue(false); @@ -28,5 +29,31 @@ describe("DevController", () => { "Development endpoints are only available in DEV mode." ); }); + it("should fail without mandatory properties", async () => { + //WHEN + const { body } = await mockApp + .post("/dev/generateData") + .send({}) + .expect(422); + + //THEN + expect(body).toEqual({ + message: "Invalid request data schema", + validationErrors: [`"username" Required`], + }); + }); + it("should fail with unknown properties", async () => { + //WHEN + const { body } = await mockApp + .post("/dev/generateData") + .send({ username: "Bob", extra: "value" }) + .expect(422); + + //THEN + expect(body).toEqual({ + message: "Invalid request data schema", + validationErrors: ["Unrecognized key(s) in object: 'extra'"], + }); + }); }); }); From 97374ae83a5365c1be10bf7663bfd3876b402d2f Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 23 Aug 2024 20:38:20 +0300 Subject: [PATCH 4/4] tag visibility --- backend/scripts/openapi.ts | 1 + backend/src/api/controllers/dev.ts | 10 ++++------ backend/src/middlewares/utility.ts | 1 + packages/contracts/src/schemas/api.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/scripts/openapi.ts b/backend/scripts/openapi.ts index 6d505c8cb90c..02b814e2f157 100644 --- a/backend/scripts/openapi.ts +++ b/backend/scripts/openapi.ts @@ -110,6 +110,7 @@ export function getOpenApi(): OpenAPIObject { description: "Development related endpoints. Only available on dev environment", "x-displayName": "Development", + "x-public": "no", }, ], }, diff --git a/backend/src/api/controllers/dev.ts b/backend/src/api/controllers/dev.ts index c576cb54b231..e380cfc9d671 100644 --- a/backend/src/api/controllers/dev.ts +++ b/backend/src/api/controllers/dev.ts @@ -20,7 +20,7 @@ import { GenerateDataResponse, } from "@monkeytype/contracts/dev"; -const CREATE_RESULT_DEFAULT_OPTIONS: Partial = { +const CREATE_RESULT_DEFAULT_OPTIONS = { firstTestTimestamp: DateUtils.startOfDay(new UTCDate(Date.now())).valueOf(), lastTestTimestamp: DateUtils.endOfDay(new UTCDate(Date.now())).valueOf(), minTestsPerDay: 0, @@ -76,17 +76,15 @@ async function createTestResults( ...CREATE_RESULT_DEFAULT_OPTIONS, ...configOptions, }; - const start = toDate(config.firstTestTimestamp as number); - const end = toDate(config.lastTestTimestamp as number); + const start = toDate(config.firstTestTimestamp); + const end = toDate(config.lastTestTimestamp); const days = DateUtils.eachDayOfInterval({ start, end, }).map((day) => ({ timestamp: DateUtils.startOfDay(day), - amount: Math.round( - random(config.minTestsPerDay as number, config.maxTestsPerDay as number) - ), + amount: Math.round(random(config.minTestsPerDay, config.maxTestsPerDay)), })); for (const day of days) { diff --git a/backend/src/middlewares/utility.ts b/backend/src/middlewares/utility.ts index f2dcb6a04903..383675c15768 100644 --- a/backend/src/middlewares/utility.ts +++ b/backend/src/middlewares/utility.ts @@ -3,6 +3,7 @@ import type { Request, Response, NextFunction, RequestHandler } from "express"; import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response"; import { recordClientVersion as prometheusRecordClientVersion } from "../utils/prometheus"; import { validate } from "./configuration"; +import { isDevEnvironment } from "../utils/misc"; export const emptyMiddleware = ( _req: MonkeyTypes.Request, diff --git a/packages/contracts/src/schemas/api.ts b/packages/contracts/src/schemas/api.ts index affdeda6a04e..aec157b19209 100644 --- a/packages/contracts/src/schemas/api.ts +++ b/packages/contracts/src/schemas/api.ts @@ -19,7 +19,7 @@ export type EndpointMetadata = { }; export type RequestAuthenticationOptions = { - /** Endpoint is accessible without any authentication. If `false` bearer authentication is requuired. */ + /** Endpoint is accessible without any authentication. If `false` bearer authentication is required. */ isPublic?: boolean; /** Endpoint is accessible with ape key authentication in _addition_ to the bearer authentication. */ acceptApeKeys?: boolean;