diff --git a/.eslintignore b/.eslintignore index 9ca212079..1003a5fae 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ backend/dist backend/__migration__ -docker \ No newline at end of file +docker +backend/scripts diff --git a/backend/__tests__/api/controllers/config.spec.ts b/backend/__tests__/api/controllers/config.spec.ts new file mode 100644 index 000000000..a3c50fb68 --- /dev/null +++ b/backend/__tests__/api/controllers/config.spec.ts @@ -0,0 +1,132 @@ +import request from "supertest"; +import app from "../../../src/app"; +import * as ConfigDal from "../../../src/dal/config"; +import { ObjectId } from "mongodb"; +const mockApp = request(app); + +describe("ConfigController", () => { + describe("get config", () => { + const getConfigMock = vi.spyOn(ConfigDal, "getConfig"); + + afterEach(() => { + getConfigMock.mockReset(); + }); + + it("should get the users config", async () => { + //GIVEN + getConfigMock.mockResolvedValue({ + _id: new ObjectId(), + uid: "123456789", + config: { language: "english" }, + }); + + //WHEN + const { body } = await mockApp + .get("/configs") + .set("authorization", "Uid 123456789") + .expect(200); + + //THEN + expect(body).toStrictEqual({ + message: "Configuration retrieved", + data: { language: "english" }, + }); + + expect(getConfigMock).toHaveBeenCalledWith("123456789"); + }); + }); + describe("update config", () => { + const saveConfigMock = vi.spyOn(ConfigDal, "saveConfig"); + + afterEach(() => { + saveConfigMock.mockReset(); + }); + + it("should update the users config", async () => { + //GIVEN + saveConfigMock.mockResolvedValue({} as any); + + //WHEN + const { body } = await mockApp + .patch("/configs") + .set("authorization", "Uid 123456789") + .accept("application/json") + .send({ language: "english" }) + .expect(200); + + //THEN + expect(body).toStrictEqual({ + message: "Config updated", + data: null, + }); + + expect(saveConfigMock).toHaveBeenCalledWith("123456789", { + language: "english", + }); + }); + it("should fail with unknown config", async () => { + //WHEN + const { body } = await mockApp + .patch("/configs") + .set("authorization", "Uid 123456789") + .accept("application/json") + .send({ unknownValue: "unknown" }) + .expect(422); + + //THEN + expect(body).toStrictEqual({ + message: "Invalid request data schema", + validationErrors: [`Unrecognized key(s) in object: 'unknownValue'`], + }); + + expect(saveConfigMock).not.toHaveBeenCalled(); + }); + it("should fail with invalid configs", async () => { + //WHEN + const { body } = await mockApp + .patch("/configs") + .set("authorization", "Uid 123456789") + .accept("application/json") + .send({ autoSwitchTheme: "yes", confidenceMode: "pretty" }) + .expect(422); + + //THEN + expect(body).toStrictEqual({ + message: "Invalid request data schema", + validationErrors: [ + `"autoSwitchTheme" Expected boolean, received string`, + `"confidenceMode" Invalid enum value. Expected 'off' | 'on' | 'max', received 'pretty'`, + ], + }); + + expect(saveConfigMock).not.toHaveBeenCalled(); + }); + }); + describe("delete config", () => { + const deleteConfigMock = vi.spyOn(ConfigDal, "deleteConfig"); + + afterEach(() => { + deleteConfigMock.mockReset(); + }); + + it("should delete the users config", async () => { + //GIVEN + deleteConfigMock.mockResolvedValue(); + + //WHEN + + const { body } = await mockApp + .delete("/configs") + .set("authorization", "Uid 123456789") + .expect(200); + + //THEN + expect(body).toStrictEqual({ + message: "Config deleted", + data: null, + }); + + expect(deleteConfigMock).toHaveBeenCalledWith("123456789"); + }); + }); +}); diff --git a/backend/__tests__/middlewares/auth.spec.ts b/backend/__tests__/middlewares/auth.spec.ts index f4e672249..5cc1a5b52 100644 --- a/backend/__tests__/middlewares/auth.spec.ts +++ b/backend/__tests__/middlewares/auth.spec.ts @@ -1,6 +1,6 @@ import * as AuthUtils from "../../src/utils/auth"; import * as Auth from "../../src/middlewares/auth"; -import { DecodedIdToken } from "firebase-admin/lib/auth/token-verifier"; +import { DecodedIdToken } from "firebase-admin/auth"; import { NextFunction, Request, Response } from "express"; import { getCachedConfiguration } from "../../src/init/configuration"; import * as ApeKeys from "../../src/dal/ape-keys"; @@ -8,6 +8,11 @@ import { ObjectId } from "mongodb"; import { hashSync } from "bcrypt"; import MonkeyError from "../../src/utils/error"; import * as Misc from "../../src/utils/misc"; +import { + EndpointMetadata, + RequestAuthenticationOptions, +} from "@monkeytype/contracts/schemas/api"; +import * as Prometheus from "../../src/utils/prometheus"; const mockDecodedToken: DecodedIdToken = { uid: "123456789", @@ -31,12 +36,11 @@ const mockApeKey = { vi.spyOn(ApeKeys, "getApeKey").mockResolvedValue(mockApeKey); vi.spyOn(ApeKeys, "updateLastUsedOn").mockResolvedValue(); const isDevModeMock = vi.spyOn(Misc, "isDevEnvironment"); +let mockRequest: Partial; +let mockResponse: Partial; +let nextFunction: NextFunction; describe("middlewares/auth", () => { - let mockRequest: Partial; - let mockResponse: Partial; - let nextFunction: NextFunction; - beforeEach(async () => { isDevModeMock.mockReturnValue(true); let config = await getCachedConfiguration(true); @@ -258,4 +262,253 @@ describe("middlewares/auth", () => { ); }); }); + + describe("authenticateTsRestRequest", () => { + const prometheusRecordAuthTimeMock = vi.spyOn(Prometheus, "recordAuthTime"); + const prometheusIncrementAuthMock = vi.spyOn(Prometheus, "incrementAuth"); + + beforeEach(() => + [prometheusIncrementAuthMock, prometheusRecordAuthTimeMock].forEach( + (it) => it.mockReset() + ) + ); + + it("should fail if token is not fresh", async () => { + //GIVEN + Date.now = vi.fn(() => 60001); + + //WHEN + expect(() => + authenticate({}, { requireFreshToken: true }) + ).rejects.toThrowError( + "Unauthorized\nStack: This endpoint requires a fresh token" + ); + + //THEN + + expect(nextFunction).not.toHaveBeenCalled(); + expect(prometheusIncrementAuthMock).not.toHaveBeenCalled(); + expect(prometheusRecordAuthTimeMock).not.toHaveBeenCalled(); + }); + it("should allow the request if token is fresh", async () => { + //GIVEN + Date.now = vi.fn(() => 10000); + + //WHEN + const result = await authenticate({}, { requireFreshToken: true }); + + //THEN + const decodedToken = result.decodedToken; + expect(decodedToken?.type).toBe("Bearer"); + expect(decodedToken?.email).toBe(mockDecodedToken.email); + expect(decodedToken?.uid).toBe(mockDecodedToken.uid); + expect(nextFunction).toHaveBeenCalledOnce(); + + expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("Bearer"); + expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce(); + }); + it("should allow the request if apeKey is supported", async () => { + //WHEN + const result = await authenticate( + { headers: { authorization: "ApeKey aWQua2V5" } }, + { acceptApeKeys: true } + ); + + //THEN + const decodedToken = result.decodedToken; + expect(decodedToken?.type).toBe("ApeKey"); + expect(decodedToken?.email).toBe(""); + expect(decodedToken?.uid).toBe("123"); + expect(nextFunction).toHaveBeenCalledTimes(1); + }); + it("should fail wit apeKey if apeKey is not supported", async () => { + //WHEN + await expect(() => + authenticate( + { headers: { authorization: "ApeKey aWQua2V5" } }, + { acceptApeKeys: false } + ) + ).rejects.toThrowError("This endpoint does not accept ApeKeys"); + + //THEN + }); + it("should allow the request with authentation on public endpoint", async () => { + //WHEN + const result = await authenticate({}, { isPublic: true }); + + //THEN + const decodedToken = result.decodedToken; + expect(decodedToken?.type).toBe("Bearer"); + expect(decodedToken?.email).toBe(mockDecodedToken.email); + expect(decodedToken?.uid).toBe(mockDecodedToken.uid); + expect(nextFunction).toHaveBeenCalledTimes(1); + }); + it("should allow the request without authentication on public endpoint", async () => { + //WHEN + const result = await authenticate({ headers: {} }, { isPublic: true }); + + //THEN + const decodedToken = result.decodedToken; + expect(decodedToken?.type).toBe("None"); + expect(decodedToken?.email).toBe(""); + expect(decodedToken?.uid).toBe(""); + expect(nextFunction).toHaveBeenCalledTimes(1); + + expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("None"); + expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce(); + }); + it("should allow the request with apeKey on public endpoint", async () => { + //WHEN + const result = await authenticate( + { headers: { authorization: "ApeKey aWQua2V5" } }, + { isPublic: true } + ); + + //THEN + const decodedToken = result.decodedToken; + expect(decodedToken?.type).toBe("ApeKey"); + expect(decodedToken?.email).toBe(""); + expect(decodedToken?.uid).toBe("123"); + expect(nextFunction).toHaveBeenCalledTimes(1); + + expect(prometheusIncrementAuthMock).toHaveBeenCalledWith("ApeKey"); + expect(prometheusRecordAuthTimeMock).toHaveBeenCalledOnce(); + }); + it("should allow request with Uid on dev", async () => { + //WHEN + const result = await authenticate({ + headers: { authorization: "Uid 123" }, + }); + + //THEN + const decodedToken = result.decodedToken; + expect(decodedToken?.type).toBe("Bearer"); + expect(decodedToken?.email).toBe(""); + expect(decodedToken?.uid).toBe("123"); + expect(nextFunction).toHaveBeenCalledTimes(1); + }); + it("should allow request with Uid and email on dev", async () => { + const result = await authenticate({ + headers: { authorization: "Uid 123|test@example.com" }, + }); + + //THEN + const decodedToken = result.decodedToken; + expect(decodedToken?.type).toBe("Bearer"); + expect(decodedToken?.email).toBe("test@example.com"); + expect(decodedToken?.uid).toBe("123"); + expect(nextFunction).toHaveBeenCalledTimes(1); + }); + it("should fail request with Uid on non-dev", async () => { + //GIVEN + isDevModeMock.mockReturnValue(false); + + //WHEN / THEN + await expect(() => + authenticate({ headers: { authorization: "Uid 123" } }) + ).rejects.toThrow( + new MonkeyError(401, "Baerer type uid is not supported") + ); + }); + it("should fail without authentication", async () => { + await expect(() => authenticate({ headers: {} })).rejects.toThrowError( + "Unauthorized\nStack: endpoint: /api/v1 no authorization header found" + ); + + //THEH + expect(prometheusIncrementAuthMock).not.toHaveBeenCalled(); + expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith( + "None", + "failure", + expect.anything(), + expect.anything() + ); + }); + it("should fail with empty authentication", async () => { + await expect(() => + authenticate({ headers: { authorization: "" } }) + ).rejects.toThrowError( + "Unauthorized\nStack: endpoint: /api/v1 no authorization header found" + ); + + //THEH + expect(prometheusIncrementAuthMock).not.toHaveBeenCalled(); + expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith( + "", + "failure", + expect.anything(), + expect.anything() + ); + }); + it("should fail with missing authentication token", async () => { + await expect(() => + authenticate({ headers: { authorization: "Bearer" } }) + ).rejects.toThrowError( + "Missing authentication token\nStack: authenticateWithAuthHeader" + ); + + //THEH + expect(prometheusIncrementAuthMock).not.toHaveBeenCalled(); + expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith( + "Bearer", + "failure", + expect.anything(), + expect.anything() + ); + }); + it("should fail with unknown authentication scheme", async () => { + await expect(() => + authenticate({ headers: { authorization: "unknown format" } }) + ).rejects.toThrowError( + 'Unknown authentication scheme\nStack: The authentication scheme "unknown" is not implemented' + ); + + //THEH + expect(prometheusIncrementAuthMock).not.toHaveBeenCalled(); + expect(prometheusRecordAuthTimeMock).toHaveBeenCalledWith( + "unknown", + "failure", + expect.anything(), + expect.anything() + ); + }); + it("should record country if provided", async () => { + const prometheusRecordRequestCountryMock = vi.spyOn( + Prometheus, + "recordRequestCountry" + ); + + await authenticate( + { headers: { "cf-ipcountry": "gb" } }, + { isPublic: true } + ); + + //THEN + expect(prometheusRecordRequestCountryMock).toHaveBeenCalledWith( + "gb", + expect.anything() + ); + }); + }); }); + +async function authenticate( + request: Partial, + authenticationOptions?: RequestAuthenticationOptions +): Promise<{ decodedToken: MonkeyTypes.DecodedToken }> { + const mergedRequest = { + ...mockRequest, + ...request, + tsRestRoute: { + metadata: { authenticationOptions } as EndpointMetadata, + }, + } as any; + + await Auth.authenticateTsRestRequest()( + mergedRequest, + mockResponse as Response, + nextFunction + ); + + return { decodedToken: mergedRequest.ctx.decodedToken }; +} diff --git a/backend/__tests__/tsconfig.json b/backend/__tests__/tsconfig.json index 0e08eb150..ddfdc95f8 100644 --- a/backend/__tests__/tsconfig.json +++ b/backend/__tests__/tsconfig.json @@ -1,18 +1,6 @@ { + "extends": "@monkeytype/typescript-config/base.json", "compilerOptions": { - "incremental": true, - "module": "commonjs", - "target": "es6", - "sourceMap": false, - "allowJs": true, - "checkJs": true, - "outDir": "build", - "moduleResolution": "node", - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "strictNullChecks": true, - "skipLibCheck": true, "noEmit": true, "types": ["vitest/globals"] }, diff --git a/backend/__tests__/utils/misc.spec.ts b/backend/__tests__/utils/misc.spec.ts index f8c2b0065..b3ab03aff 100644 --- a/backend/__tests__/utils/misc.spec.ts +++ b/backend/__tests__/utils/misc.spec.ts @@ -1,5 +1,6 @@ import _ from "lodash"; import * as misc from "../../src/utils/misc"; +import { ObjectId } from "mongodb"; describe("Misc Utils", () => { afterAll(() => { @@ -605,4 +606,19 @@ describe("Misc Utils", () => { expect(misc.formatSeconds(seconds)).toBe(expected); }); }); + + describe("replaceObjectId", () => { + it("replaces objecId with string", () => { + const fromDatabase = { + _id: new ObjectId(), + test: "test", + number: 1, + }; + expect(misc.replaceObjectId(fromDatabase)).toStrictEqual({ + _id: fromDatabase._id.toHexString(), + test: "test", + number: 1, + }); + }); + }); }); diff --git a/backend/package.json b/backend/package.json index c53dcd8a2..36c775e73 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,17 +5,18 @@ "private": true, "scripts": { "lint": "eslint \"./src/**/*.ts\"", - "build": "tsc --build", + "build": "npm run gen-docs && tsc --build", "watch": "tsc --build --watch", "clean": "tsc --build --clean", "ts-check": "tsc --noEmit", - "start": "npm run build && node ./dist/server.js", + "start": "node ./dist/server.js", "test": "vitest run", "test-coverage": "vitest run --coverage", "dev": "concurrently \"tsx watch --clear-screen=false ./src/server.ts\" \"tsc --preserveWatchOutput --noEmit --watch\" \"npx eslint-watch \"./src/**/*.ts\"\"", "knip": "knip", "docker-db-only": "docker compose -f docker/compose.db-only.yml up", - "docker": "docker compose -f docker/compose.yml up" + "docker": "docker compose -f docker/compose.yml up", + "gen-docs": "tsx scripts/openapi.ts dist/static/api/openapi.json && redocly build-docs -o dist/static/api/internal.html internal@v2 && redocly bundle -o dist/static/api/public.json public-filter && redocly build-docs -o dist/static/api/public.html public@v2" }, "engines": { "node": "18.20.4", @@ -23,6 +24,9 @@ }, "dependencies": { "@date-fns/utc": "1.2.0", + "@monkeytype/contracts": "*", + "@ts-rest/express": "3.45.2", + "@ts-rest/open-api": "3.45.2", "bcrypt": "5.1.1", "bullmq": "1.91.1", "chalk": "4.1.2", @@ -60,6 +64,7 @@ "@monkeytype/shared-types": "*", "@monkeytype/typescript-config": "*", "@monkeytype/eslint-config": "*", + "@redocly/cli": "1.18.1", "@types/bcrypt": "5.0.0", "@types/cors": "2.8.12", "@types/cron": "1.7.3", diff --git a/backend/redocly.yaml b/backend/redocly.yaml new file mode 100644 index 000000000..7bacf646a --- /dev/null +++ b/backend/redocly.yaml @@ -0,0 +1,46 @@ +extends: + - recommended + +apis: + internal@v2: + root: dist/static/api/openapi.json + public-filter: + root: dist/static/api/openapi.json + decorators: + filter-in: + property: x-public + value: yes + public@v2: + root: dist/static/api/public.json + +features.openapi: + theme: + logo: + gutter: "2rem" + colors: + primary: + main: "#e2b714" + border: + dark: "#e2b714" + light: "#e2b714" + error: + main: "#da3333" + success: + main: "#009400" + text: + primary: "#646669" + secondary: "#d1d0c5" + warning: + main: "#FF00FF" + http: + delete: "#da3333" + post: "#004D94" + patch: "#e2b714" + get: "#009400" + sidebar: + backgroundColor: "#323437" + textColor: "#d1d0c5" + activeTextColor: "#e2b714" + rightPanel: + backgroundColor: "#323437" + textColor: "#d1d0c5" diff --git a/backend/scripts/openapi.ts b/backend/scripts/openapi.ts new file mode 100644 index 000000000..f2a0b8637 --- /dev/null +++ b/backend/scripts/openapi.ts @@ -0,0 +1,120 @@ +import { generateOpenApi } from "@ts-rest/open-api"; +import { contract } from "@monkeytype/contracts/index"; +import { writeFileSync, mkdirSync } from "fs"; +import { EndpointMetadata } from "@monkeytype/contracts/schemas/api"; +import type { OpenAPIObject } from "openapi3-ts"; + +type SecurityRequirementObject = { + [name: string]: string[]; +}; + +export function getOpenApi(): OpenAPIObject { + const openApiDocument = generateOpenApi( + contract, + { + openapi: "3.1.0", + info: { + title: "Monkeytype API", + description: + "Documentation for the public endpoints provided by the Monkeytype API server.\n\nNote that authentication is performed with the Authorization HTTP header in the format `Authorization: ApeKey YOUR_APE_KEY`\n\nThere is a rate limit of `30 requests per minute` across all endpoints with some endpoints being more strict. Rate limit rates are shared across all ape keys.", + version: "2.0.0", + termsOfService: "https://monkeytype.com/terms-of-service", + contact: { + name: "Support", + email: "support@monkeytype.com", + }, + "x-logo": { + url: "https://monkeytype.com/images/mtfulllogo.png", + }, + license: { + name: "GPL-3.0", + url: "https://www.gnu.org/licenses/gpl-3.0.html", + }, + }, + servers: [ + { + url: "https://api.monkeytype.com", + description: "Production server", + }, + ], + components: { + securitySchemes: { + BearerAuth: { + type: "http", + scheme: "bearer", + }, + ApeKey: { + type: "http", + scheme: "ApeKey", + }, + }, + }, + tags: [ + { + name: "configs", + description: + "User specific configurations like test settings, theme or tags.", + "x-displayName": "User configuration", + }, + ], + }, + + { + jsonQuery: true, + setOperationId: "concatenated-path", + operationMapper: (operation, route) => ({ + ...operation, + ...addAuth(route.metadata as EndpointMetadata), + ...addTags(route.metadata as EndpointMetadata), + }), + } + ); + return openApiDocument; +} + +function addAuth(metadata: EndpointMetadata | undefined): object { + const auth = metadata?.["authenticationOptions"] ?? {}; + const security: SecurityRequirementObject[] = []; + if (!auth.isPublic === true) { + security.push({ BearerAuth: [] }); + + if (auth.acceptApeKeys === true) { + security.push({ ApeKey: [] }); + } + } + + const includeInPublic = auth.isPublic === true || auth.acceptApeKeys === true; + return { + "x-public": includeInPublic ? "yes" : "no", + security, + }; +} + +function addTags(metadata: EndpointMetadata | undefined): object { + if (metadata === undefined || metadata.openApiTags === undefined) return {}; + return { + tags: Array.isArray(metadata.openApiTags) + ? metadata.openApiTags + : [metadata.openApiTags], + }; +} + +//detect if we run this as a main +if (require.main === module) { + const args = process.argv.slice(2); + if (args.length !== 1) { + console.error("Provide filename."); + process.exit(1); + } + const outFile = args[0] as string; + + //create directories if needed + const lastSlash = outFile.lastIndexOf("/"); + if (lastSlash > 1) { + const dir = outFile.substring(0, lastSlash); + mkdirSync(dir, { recursive: true }); + } + + const openapi = getOpenApi(); + writeFileSync(args[0] as string, JSON.stringify(openapi, null, 2)); +} diff --git a/backend/src/api/controllers/config.ts b/backend/src/api/controllers/config.ts index bb93d7c9c..5ed7b926d 100644 --- a/backend/src/api/controllers/config.ts +++ b/backend/src/api/controllers/config.ts @@ -1,22 +1,33 @@ +import { PartialConfig } from "@monkeytype/contracts/schemas/configs"; import * as ConfigDAL from "../../dal/config"; -import { MonkeyResponse } from "../../utils/monkey-response"; +import { MonkeyResponse2 } from "../../utils/monkey-response"; +import { GetConfigResponse } from "@monkeytype/contracts/configs"; export async function getConfig( - req: MonkeyTypes.Request -): Promise { + req: MonkeyTypes.Request2 +): Promise { const { uid } = req.ctx.decodedToken; + const data = (await ConfigDAL.getConfig(uid))?.config ?? null; - const data = await ConfigDAL.getConfig(uid); - return new MonkeyResponse("Configuration retrieved", data); + return new MonkeyResponse2("Configuration retrieved", data); } export async function saveConfig( - req: MonkeyTypes.Request -): Promise { - const { config } = req.body; + req: MonkeyTypes.Request2 +): Promise { + const config = req.body; const { uid } = req.ctx.decodedToken; await ConfigDAL.saveConfig(uid, config); - return new MonkeyResponse("Config updated"); + return new MonkeyResponse2("Config updated"); +} + +export async function deleteConfig( + req: MonkeyTypes.Request2 +): Promise { + const { uid } = req.ctx.decodedToken; + + await ConfigDAL.deleteConfig(uid); + return new MonkeyResponse2("Config deleted"); } diff --git a/backend/src/api/routes/configs.ts b/backend/src/api/routes/configs.ts index c2e580b62..132e74f34 100644 --- a/backend/src/api/routes/configs.ts +++ b/backend/src/api/routes/configs.ts @@ -1,30 +1,23 @@ -import { Router } from "express"; -import { authenticateRequest } from "../../middlewares/auth"; -import configSchema from "../schemas/config-schema"; -import * as ConfigController from "../controllers/config"; +import { configsContract } from "@monkeytype/contracts/configs"; +import { initServer } from "@ts-rest/express"; import * as RateLimit from "../../middlewares/rate-limit"; -import { asyncHandler } from "../../middlewares/utility"; -import { validateRequest } from "../../middlewares/validation"; +import * as ConfigController from "../controllers/config"; +import { callController } from "../ts-rest-adapter"; -const router = Router(); +const s = initServer(); -router.get( - "/", - authenticateRequest(), - RateLimit.configGet, - asyncHandler(ConfigController.getConfig) -); +export default s.router(configsContract, { + get: { + middleware: [RateLimit.configGet], + handler: async (r) => callController(ConfigController.getConfig)(r), + }, -router.patch( - "/", - authenticateRequest(), - RateLimit.configUpdate, - validateRequest({ - body: { - config: configSchema.required(), - }, - }), - asyncHandler(ConfigController.saveConfig) -); - -export default router; + save: { + middleware: [RateLimit.configUpdate], + handler: async (r) => callController(ConfigController.saveConfig)(r), + }, + delete: { + middleware: [RateLimit.configDelete], + handler: async (r) => callController(ConfigController.deleteConfig)(r), + }, +}); diff --git a/backend/src/api/routes/docs.ts b/backend/src/api/routes/docs.ts new file mode 100644 index 000000000..a2d5fc25c --- /dev/null +++ b/backend/src/api/routes/docs.ts @@ -0,0 +1,40 @@ +import { Router } from "express"; +import * as swaggerUi from "swagger-ui-express"; +import publicSwaggerSpec from "../../documentation/public-swagger.json"; + +const SWAGGER_UI_OPTIONS = { + customCss: ".swagger-ui .topbar { display: none } .try-out { display: none }", + customSiteTitle: "Monkeytype API Documentation", +}; + +const router = Router(); + +const root = __dirname + "../../../static"; + +router.use("/v2/internal", (req, res) => { + res.sendFile("api/internal.html", { root }); +}); + +router.use("/v2/internal.json", (req, res) => { + res.setHeader("Content-Type", "application/json"); + res.sendFile("api/openapi.json", { root }); +}); + +router.use("/v2/public", (req, res) => { + res.sendFile("api/public.html", { root }); +}); + +router.use("/v2/public.json", (req, res) => { + res.setHeader("Content-Type", "application/json"); + res.sendFile("api/public.json", { root }); +}); + +const options = {}; + +router.use( + "/", + swaggerUi.serveFiles(publicSwaggerSpec, options), + swaggerUi.setup(publicSwaggerSpec, SWAGGER_UI_OPTIONS) +); + +export default router; diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index 793da0a41..6d54c4adf 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -1,16 +1,18 @@ import _ from "lodash"; +import { contract } from "@monkeytype/contracts/index"; import psas from "./psas"; import publicStats from "./public"; import users from "./users"; import { join } from "path"; import quotes from "./quotes"; -import configs from "./configs"; import results from "./results"; import presets from "./presets"; import apeKeys from "./ape-keys"; import admin from "./admin"; +import docs from "./docs"; import webhooks from "./webhooks"; import dev from "./dev"; +import configs from "./configs"; import configuration from "./configuration"; import { version } from "../../version"; import leaderboards from "./leaderboards"; @@ -19,15 +21,20 @@ import { asyncHandler } from "../../middlewares/utility"; import { MonkeyResponse } from "../../utils/monkey-response"; import { recordClientVersion } from "../../utils/prometheus"; import { - type Application, - type NextFunction, - type Response, + Application, + IRouter, + NextFunction, + Response, Router, static as expressStatic, } from "express"; import { isDevEnvironment } from "../../utils/misc"; import { getLiveConfiguration } from "../../init/configuration"; import Logger from "../../utils/logger"; +import { createExpressEndpoints, initServer } from "@ts-rest/express"; +import { ZodIssue } from "zod"; +import { MonkeyValidationError } from "@monkeytype/contracts/schemas/api"; +import { authenticateTsRestRequest } from "../../middlewares/auth"; const pathOverride = process.env["API_PATH_OVERRIDE"]; const BASE_ROUTE = pathOverride !== undefined ? `/${pathOverride}` : ""; @@ -35,7 +42,6 @@ const APP_START_TIME = Date.now(); const API_ROUTE_MAP = { "/users": users, - "/configs": configs, "/results": results, "/presets": presets, "/psas": psas, @@ -45,13 +51,52 @@ const API_ROUTE_MAP = { "/ape-keys": apeKeys, "/admin": admin, "/webhooks": webhooks, + "/docs": docs, }; -function addApiRoutes(app: Application): void { - app.get("/leaderboard", (_req, res) => { - res.sendStatus(404); - }); +const s = initServer(); +const router = s.router(contract, { + configs, +}); +export function addApiRoutes(app: Application): void { + applyDevApiRoutes(app); + applyApiRoutes(app); + applyTsRestApiRoutes(app); + + app.use( + asyncHandler(async (req, _res) => { + return new MonkeyResponse( + `Unknown request URL (${req.method}: ${req.path})`, + null, + 404 + ); + }) + ); +} + +function applyTsRestApiRoutes(app: IRouter): void { + createExpressEndpoints(contract, router, app, { + jsonQuery: true, + requestValidationErrorHandler(err, req, res, next) { + if (err.body?.issues === undefined) return next(); + const issues = err.body?.issues.map(prettyErrorMessage); + res.status(422).json({ + message: "Invalid request data schema", + validationErrors: issues, + } as MonkeyValidationError); + }, + globalMiddleware: [authenticateTsRestRequest()], + }); +} + +function prettyErrorMessage(issue: ZodIssue | undefined): string { + if (issue === undefined) return ""; + const path = issue.path.length > 0 ? `"${issue.path.join(".")}" ` : ""; + return `${path}${issue.message}`; +} + +function applyDevApiRoutes(app: Application): void { if (isDevEnvironment()) { //disable csp to allow assets to load from unsecured http app.use((req, res, next) => { @@ -72,7 +117,9 @@ function addApiRoutes(app: Application): void { //enable dev edpoints app.use("/dev", dev); } +} +function applyApiRoutes(app: Application): void { // Cannot be added to the route map because it needs to be added before the maintenance handler app.use("/configuration", configuration); @@ -110,6 +157,7 @@ function addApiRoutes(app: Application): void { }) ); + //legacy route app.get("/psa", (_req, res) => { res.json([ { @@ -124,16 +172,4 @@ function addApiRoutes(app: Application): void { const apiRoute = `${BASE_ROUTE}${route}`; app.use(apiRoute, router); }); - - app.use( - asyncHandler(async (req, _res) => { - return new MonkeyResponse( - `Unknown request URL (${req.method}: ${req.path})`, - null, - 404 - ); - }) - ); } - -export default addApiRoutes; diff --git a/backend/src/api/routes/swagger.ts b/backend/src/api/routes/swagger.ts index 1166e3a5f..e9fb5adb7 100644 --- a/backend/src/api/routes/swagger.ts +++ b/backend/src/api/routes/swagger.ts @@ -1,19 +1,8 @@ -import _ from "lodash"; -import { type Application } from "express"; +import { Application } from "express"; import { getMiddleware as getSwaggerMiddleware } from "swagger-stats"; -import { - serve as serveSwagger, - setup as setupSwaggerUi, -} from "swagger-ui-express"; -import publicSwaggerSpec from "../../documentation/public-swagger.json"; import internalSwaggerSpec from "../../documentation/internal-swagger.json"; import { isDevEnvironment } from "../../utils/misc"; -const SWAGGER_UI_OPTIONS = { - customCss: ".swagger-ui .topbar { display: none } .try-out { display: none }", - customSiteTitle: "Monkeytype API Documentation", -}; - function addSwaggerMiddlewares(app: Application): void { app.use( getSwaggerMiddleware({ @@ -30,12 +19,6 @@ function addSwaggerMiddlewares(app: Application): void { }, }) ); - - app.use( - ["/documentation", "/docs"], - serveSwagger, - setupSwaggerUi(publicSwaggerSpec, SWAGGER_UI_OPTIONS) - ); } export default addSwaggerMiddlewares; diff --git a/backend/src/api/schemas/config-schema.ts b/backend/src/api/schemas/config-schema.ts index baebe8b71..48ad6d07b 100644 --- a/backend/src/api/schemas/config-schema.ts +++ b/backend/src/api/schemas/config-schema.ts @@ -10,7 +10,7 @@ const CARET_STYLES = [ "carrot", "banana", ]; - +//TODO replaced, still used by presets const CONFIG_SCHEMA = joi.object({ theme: joi.string().max(50).token(), themeLight: joi.string().max(50).token(), @@ -80,7 +80,7 @@ const CONFIG_SCHEMA = joi.object({ .valid("lowercase", "uppercase", "blank", "dynamic"), keymapLayout: joi .string() - .regex(/[\w-_]+/) + .regex(/[\w\-_]+/) .valid() .max(50), keymapShowTopRow: joi.string().valid("always", "layout", "never"), diff --git a/backend/src/api/ts-rest-adapter.ts b/backend/src/api/ts-rest-adapter.ts new file mode 100644 index 000000000..4429abc14 --- /dev/null +++ b/backend/src/api/ts-rest-adapter.ts @@ -0,0 +1,73 @@ +import { AppRoute, AppRouter } from "@ts-rest/core"; +import { TsRestRequest } from "@ts-rest/express"; +import { MonkeyResponse2 } from "../utils/monkey-response"; +export function callController< + TRoute extends AppRoute | AppRouter, + TQuery, + TBody, + TParams, + TResponse, + TStatus = 200 +>( + handler: Handler +): (all: RequestType2) => Promise<{ + status: TStatus; + body: { message: string; data: TResponse }; +}> { + return async (all) => { + const req: MonkeyTypes.Request2 = { + body: all.body as TBody, + query: all.query as TQuery, + params: all.params as TParams, + raw: all.req, + ctx: all.req["ctx"], + }; + + const result = await handler(req); + const response = { + status: 200 as TStatus, + body: { + message: result.message, + data: result.data as TResponse, + }, + }; + + return response; + }; +} + +type WithBody = { + body: T; +}; +type WithQuery = { + query: T; +}; + +type WithParams = { + params: T; +}; + +type WithoutBody = { + body?: never; +}; +type WithoutQuery = { + query?: never; +}; +type WithoutParams = { + params?: never; +}; + +type Handler = ( + req: MonkeyTypes.Request2 +) => Promise>; + +type RequestType2< + TRoute extends AppRoute | AppRouter, + TQuery, + TBody, + TParams +> = { + req: TsRestRequest; +} & (TQuery extends undefined ? WithoutQuery : WithQuery) & + (TBody extends undefined ? WithoutBody : WithBody) & + (TParams extends undefined ? WithoutParams : WithParams); diff --git a/backend/src/app.ts b/backend/src/app.ts index 2a5470908..6c85bd10c 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,6 +1,6 @@ import cors from "cors"; import helmet from "helmet"; -import addApiRoutes from "./api/routes"; +import { addApiRoutes } from "./api/routes"; import express, { urlencoded, json } from "express"; import contextMiddleware from "./middlewares/context"; import errorHandlingMiddleware from "./middlewares/error"; diff --git a/backend/src/dal/config.ts b/backend/src/dal/config.ts index f4100cac1..67cd2abc4 100644 --- a/backend/src/dal/config.ts +++ b/backend/src/dal/config.ts @@ -1,7 +1,7 @@ -import { type UpdateResult } from "mongodb"; +import { Collection, UpdateResult } from "mongodb"; import * as db from "../init/db"; import _ from "lodash"; -import { Config } from "@monkeytype/shared-types/config"; +import { Config, PartialConfig } from "@monkeytype/contracts/schemas/configs"; const configLegacyProperties = [ "swapEscAndTab", @@ -23,9 +23,19 @@ const configLegacyProperties = [ "enableAds", ]; +type DBConfig = { + _id: ObjectId; + uid: string; + config: PartialConfig; +}; + +// Export for use in tests +export const getConfigCollection = (): Collection => + db.collection("configs"); + export async function saveConfig( uid: string, - config: Config + config: Partial ): Promise { const configChanges = _.mapKeys(config, (_value, key) => `config.${key}`); @@ -33,20 +43,18 @@ export async function saveConfig( _.map(configLegacyProperties, (key) => [`config.${key}`, ""]) ) as Record; - return await db - .collection("configs") - .updateOne( - { uid }, - { $set: configChanges, $unset: unset }, - { upsert: true } - ); + return await getConfigCollection().updateOne( + { uid }, + { $set: configChanges, $unset: unset }, + { upsert: true } + ); } -export async function getConfig(uid: string): Promise { - const config = await db.collection("configs").findOne({ uid }); +export async function getConfig(uid: string): Promise { + const config = await getConfigCollection().findOne({ uid }); return config; } export async function deleteConfig(uid: string): Promise { - await db.collection("configs").deleteOne({ uid }); + await getConfigCollection().deleteOne({ uid }); } diff --git a/backend/src/middlewares/auth.ts b/backend/src/middlewares/auth.ts index 1befc6da7..7eb34c726 100644 --- a/backend/src/middlewares/auth.ts +++ b/backend/src/middlewares/auth.ts @@ -3,32 +3,52 @@ import { getApeKey, updateLastUsedOn } from "../dal/ape-keys"; import MonkeyError from "../utils/error"; import { verifyIdToken } from "../utils/auth"; import { base64UrlDecode, isDevEnvironment } from "../utils/misc"; -import type { NextFunction, Response, Handler } from "express"; +import { NextFunction, Response, Handler } from "express"; import statuses from "../constants/monkey-status-codes"; import { incrementAuth, recordAuthTime, recordRequestCountry, - // recordRequestForUid, } from "../utils/prometheus"; import crypto from "crypto"; import { performance } from "perf_hooks"; +import { TsRestRequestHandler } from "@ts-rest/express"; +import { AppRoute, AppRouter } from "@ts-rest/core"; +import { RequestAuthenticationOptions } from "@monkeytype/contracts/schemas/api"; import { Configuration } from "@monkeytype/shared-types"; -type RequestAuthenticationOptions = { - isPublic?: boolean; - acceptApeKeys?: boolean; - requireFreshToken?: boolean; - noCache?: boolean; -}; - const DEFAULT_OPTIONS: RequestAuthenticationOptions = { isPublic: false, acceptApeKeys: false, requireFreshToken: false, }; -function authenticateRequest(authOptions = DEFAULT_OPTIONS): Handler { +export type TsRestRequestWithCtx = { + ctx: Readonly; +} & TsRestRequest; + +/** + * Authenticate request based on the auth settings of the route. + * By default a Bearer token with user authentication is required. + * @returns + */ +export function authenticateTsRestRequest< + T extends AppRouter | AppRoute +>(): TsRestRequestHandler { + return async ( + req: TsRestRequestWithCtx, + _res: Response, + next: NextFunction + ): Promise => { + const options = { + ...DEFAULT_OPTIONS, + ...(req.tsRestRoute["metadata"]?.["authenticationOptions"] ?? {}), + }; + return _authenticateRequestInternal(req, _res, next, options); + }; +} + +export function authenticateRequest(authOptions = DEFAULT_OPTIONS): Handler { const options = { ...DEFAULT_OPTIONS, ...authOptions, @@ -39,69 +59,78 @@ function authenticateRequest(authOptions = DEFAULT_OPTIONS): Handler { _res: Response, next: NextFunction ): Promise => { - const startTime = performance.now(); - let token: MonkeyTypes.DecodedToken; - let authType = "None"; + return _authenticateRequestInternal(req, _res, next, options); + }; +} - const { authorization: authHeader } = req.headers; +async function _authenticateRequestInternal( + req: MonkeyTypes.Request | TsRestRequestWithCtx, + _res: Response, + next: NextFunction, + options: RequestAuthenticationOptions +): Promise { + const startTime = performance.now(); + let token: MonkeyTypes.DecodedToken; + let authType = "None"; - try { - if (authHeader !== undefined && authHeader !== "") { - token = await authenticateWithAuthHeader( - authHeader, - req.ctx.configuration, - options - ); - } else if (options.isPublic === true) { - token = { - type: "None", - uid: "", - email: "", - }; - } else { - throw new MonkeyError( - 401, - "Unauthorized", - `endpoint: ${req.baseUrl} no authorization header found` - ); - } + const { authorization: authHeader } = req.headers; - incrementAuth(token.type); - - req.ctx = { - ...req.ctx, - decodedToken: token, - }; - } catch (error) { - authType = authHeader?.split(" ")[0] ?? "None"; - - recordAuthTime( - authType, - "failure", - Math.round(performance.now() - startTime), - req + try { + if (authHeader !== undefined && authHeader !== "") { + token = await authenticateWithAuthHeader( + authHeader, + req.ctx.configuration, + options + ); + } else if (options.isPublic === true) { + token = { + type: "None", + uid: "", + email: "", + }; + } else { + throw new MonkeyError( + 401, + "Unauthorized", + `endpoint: ${req.baseUrl} no authorization header found` ); - - return next(error); } + + incrementAuth(token.type); + + req.ctx = { + ...req.ctx, + decodedToken: token, + }; + } catch (error) { + authType = authHeader?.split(" ")[0] ?? "None"; + recordAuthTime( - token.type, - "success", + authType, + "failure", Math.round(performance.now() - startTime), req ); - const country = req.headers["cf-ipcountry"] as string; - if (country) { - recordRequestCountry(country, req as MonkeyTypes.Request); - } + return next(error); + } + recordAuthTime( + token.type, + "success", + Math.round(performance.now() - startTime), + req + ); - // if (req.method !== "OPTIONS" && req?.ctx?.decodedToken?.uid) { - // recordRequestForUid(req.ctx.decodedToken.uid); - // } + const country = req.headers["cf-ipcountry"] as string; + if (country) { + recordRequestCountry(country, req); + } - next(); - }; + // if (req.method !== "OPTIONS" && req?.ctx?.decodedToken?.uid) { + // recordRequestForUid(req.ctx.decodedToken.uid); + // } + + next(); } async function authenticateWithAuthHeader( @@ -109,24 +138,8 @@ async function authenticateWithAuthHeader( configuration: Configuration, options: RequestAuthenticationOptions ): Promise { - if (authHeader === undefined || authHeader === "") { - throw new MonkeyError( - 401, - "Missing authentication header", - "authenticateWithAuthHeader" - ); - } - const [authScheme, token] = authHeader.split(" "); - if (authScheme === undefined) { - throw new MonkeyError( - 401, - "Missing authentication scheme", - "authenticateWithAuthHeader" - ); - } - if (token === undefined) { throw new MonkeyError( 401, @@ -135,7 +148,7 @@ async function authenticateWithAuthHeader( ); } - const normalizedAuthScheme = authScheme.trim(); + const normalizedAuthScheme = authScheme?.trim(); switch (normalizedAuthScheme) { case "Bearer": @@ -298,7 +311,7 @@ async function authenticateWithUid( }; } -function authenticateGithubWebhook(): Handler { +export function authenticateGithubWebhook(): Handler { return async ( req: MonkeyTypes.Request, _res: Response, @@ -338,5 +351,3 @@ function authenticateGithubWebhook(): Handler { next(); }; } - -export { authenticateRequest, authenticateGithubWebhook }; diff --git a/backend/src/middlewares/rate-limit.ts b/backend/src/middlewares/rate-limit.ts index 551e11ab2..ba8c817f6 100644 --- a/backend/src/middlewares/rate-limit.ts +++ b/backend/src/middlewares/rate-limit.ts @@ -121,6 +121,13 @@ export const configGet = rateLimit({ handler: customHandler, }); +export const configDelete = rateLimit({ + windowMs: ONE_HOUR_MS, + max: 120 * REQUEST_MULTIPLIER, + keyGenerator: getKeyWithUid, + handler: customHandler, +}); + // Leaderboards Routing export const leaderboardsGet = rateLimit({ windowMs: ONE_HOUR_MS, diff --git a/backend/src/types/types.d.ts b/backend/src/types/types.d.ts index 62c0ed02e..ef052a0d6 100644 --- a/backend/src/types/types.d.ts +++ b/backend/src/types/types.d.ts @@ -1,15 +1,19 @@ type ObjectId = import("mongodb").ObjectId; type ExpressRequest = import("express").Request; - +/* eslint-disable @typescript-eslint/no-explicit-any */ +type TsRestRequest = import("@ts-rest/express").TsRestRequest; +/* eslint-enable @typescript-eslint/no-explicit-any */ +type AppRoute = import("@ts-rest/core").AppRoute; +type AppRouter = import("@ts-rest/core").AppRouter; declare namespace MonkeyTypes { - type DecodedToken = { + export type DecodedToken = { type: "Bearer" | "ApeKey" | "None"; uid: string; email: string; }; - type Context = { + export type Context = { configuration: import("@monkeytype/shared-types").Configuration; decodedToken: DecodedToken; }; @@ -18,6 +22,14 @@ declare namespace MonkeyTypes { ctx: Readonly; } & ExpressRequest; + type Request2 = { + query: Readonly; + body: Readonly; + params: Readonly; + ctx: Readonly; + raw: Readonly; + }; + type DBUser = Omit< import("@monkeytype/shared-types").User, | "resultFilterPresets" diff --git a/backend/src/utils/error.ts b/backend/src/utils/error.ts index 7108e3a81..862125492 100644 --- a/backend/src/utils/error.ts +++ b/backend/src/utils/error.ts @@ -1,7 +1,8 @@ import { v4 as uuidv4 } from "uuid"; import { isDevEnvironment } from "./misc"; +import { MonkeyServerErrorType } from "@monkeytype/contracts/schemas/api"; -class MonkeyError extends Error { +class MonkeyError extends Error implements MonkeyServerErrorType { status: number; errorId: string; uid?: string; diff --git a/backend/src/utils/misc.ts b/backend/src/utils/misc.ts index 46dc856ad..2c49c4f8b 100644 --- a/backend/src/utils/misc.ts +++ b/backend/src/utils/misc.ts @@ -1,4 +1,4 @@ -import _ from "lodash"; +import _, { omit } from "lodash"; import uaparser from "ua-parser-js"; //todo split this file into smaller util files (grouped by functionality) @@ -306,3 +306,18 @@ export function stringToNumberOrDefault( export function isDevEnvironment(): boolean { return process.env["MODE"] === "dev"; } + +/** + * convert database object into api object + * @param data database object with `_id: ObjectId` + * @returns api obkect with `id: string` + */ +export function replaceObjectId( + data: T +): T & { _id: string } { + const result = { + _id: data._id.toString(), + ...omit(data, "_id"), + } as T & { _id: string }; + return result; +} diff --git a/backend/src/utils/monkey-response.ts b/backend/src/utils/monkey-response.ts index 42ea5c717..377e1b34e 100644 --- a/backend/src/utils/monkey-response.ts +++ b/backend/src/utils/monkey-response.ts @@ -1,6 +1,10 @@ import { type Response } from "express"; import { isCustomCode } from "../constants/monkey-status-codes"; +import { MonkeyResponseType } from "@monkeytype/contracts/schemas/api"; +export type MonkeyDataAware = { + data: T | null; +}; //TODO FIX ANYS export class MonkeyResponse { @@ -36,3 +40,15 @@ export function handleMonkeyResponse( res.json({ message, data }); } + +export class MonkeyResponse2 + implements MonkeyResponseType, MonkeyDataAware +{ + public message: string; + public data: T | null; + + constructor(message: string, data: T | null = null) { + this.message = message; + this.data = data; + } +} diff --git a/backend/src/utils/prometheus.ts b/backend/src/utils/prometheus.ts index d13c2da0d..b2fb6c535 100644 --- a/backend/src/utils/prometheus.ts +++ b/backend/src/utils/prometheus.ts @@ -2,6 +2,7 @@ import { Result } from "@monkeytype/shared-types"; import { Mode } from "@monkeytype/shared-types/config"; import "dotenv/config"; import { Counter, Histogram, Gauge } from "prom-client"; +import { TsRestRequestWithCtx } from "../middlewares/auth"; const auth = new Counter({ name: "api_request_auth_total", @@ -212,7 +213,7 @@ export function recordAuthTime( type: string, status: "success" | "failure", time: number, - req: MonkeyTypes.Request + req: MonkeyTypes.Request | TsRestRequestWithCtx ): void { const reqPath = req.baseUrl + req.route.path; @@ -234,7 +235,7 @@ const requestCountry = new Counter({ export function recordRequestCountry( country: string, - req: MonkeyTypes.Request + req: MonkeyTypes.Request | TsRestRequestWithCtx ): void { const reqPath = req.baseUrl + req.route.path; diff --git a/frontend/__tests__/root/config.spec.ts b/frontend/__tests__/root/config.spec.ts new file mode 100644 index 000000000..81f34883f --- /dev/null +++ b/frontend/__tests__/root/config.spec.ts @@ -0,0 +1,533 @@ +import * as Config from "../../src/ts/config"; +import { CustomThemeColors } from "@monkeytype/contracts/schemas/configs"; +import { randomBytes } from "crypto"; + +describe("Config", () => { + it("setMode", () => { + expect(Config.setMode("zen")).toBe(true); + expect(Config.setMode("invalid" as any)).toBe(false); + }); + it("setPlaySoundOnError", () => { + expect(Config.setPlaySoundOnError("off")).toBe(true); + expect(Config.setPlaySoundOnError("1")).toBe(true); + expect(Config.setPlaySoundOnError("invalid" as any)).toBe(false); + }); + it("setPlaySoundOnClick", () => { + expect(Config.setPlaySoundOnClick("off")).toBe(true); + expect(Config.setPlaySoundOnClick("15")).toBe(true); + expect(Config.setPlaySoundOnClick("invalid" as any)).toBe(false); + }); + it("setSoundVolume", () => { + expect(Config.setSoundVolume("0.1")).toBe(true); + expect(Config.setSoundVolume("1.0")).toBe(true); + expect(Config.setSoundVolume("invalid" as any)).toBe(false); + }); + it("setDifficulty", () => { + expect(Config.setDifficulty("expert")).toBe(true); + expect(Config.setDifficulty("invalid" as any)).toBe(false); + }); + it("setAccountChart", () => { + expect(Config.setAccountChart(["on", "off", "off", "on"])).toBe(true); + //arrays not having 4 values will get [on, on, on, on] as default + expect(Config.setAccountChart(["on", "off"] as any)).toBe(true); + expect(Config.setAccountChart(["on", "off", "on", "true"] as any)).toBe( + false + ); + }); + it("setAccountChartResults", () => { + expect(Config.setAccountChartResults(true)).toBe(true); + expect(Config.setAccountChartResults("on" as any)).toBe(false); + }); + it("setStopOnError", () => { + expect(Config.setStopOnError("off")).toBe(true); + expect(Config.setStopOnError("word")).toBe(true); + expect(Config.setStopOnError("invalid" as any)).toBe(false); + }); + it("setTypingSpeedUnit", () => { + expect(Config.setTypingSpeedUnit("wpm")).toBe(true); + expect(Config.setTypingSpeedUnit("cpm")).toBe(true); + expect(Config.setTypingSpeedUnit("invalid" as any)).toBe(false); + }); + it("setPaceCaret", () => { + expect(Config.setPaceCaret("average")).toBe(true); + expect(Config.setPaceCaret("last")).toBe(true); + expect(Config.setPaceCaret("invalid" as any)).toBe(false); + }); + it("setMinWpm", () => { + expect(Config.setMinWpm("custom")).toBe(true); + expect(Config.setMinWpm("off")).toBe(true); + expect(Config.setMinWpm("invalid" as any)).toBe(false); + }); + it("setMinAcc", () => { + expect(Config.setMinAcc("custom")).toBe(true); + expect(Config.setMinAcc("off")).toBe(true); + expect(Config.setMinAcc("invalid" as any)).toBe(false); + }); + it("setMinBurst", () => { + expect(Config.setMinBurst("fixed")).toBe(true); + expect(Config.setMinBurst("off")).toBe(true); + expect(Config.setMinBurst("invalid" as any)).toBe(false); + }); + it("setSingleListCommandLine", () => { + expect(Config.setSingleListCommandLine("on")).toBe(true); + expect(Config.setSingleListCommandLine("manual")).toBe(true); + expect(Config.setSingleListCommandLine("invalid" as any)).toBe(false); + }); + it("setAds", () => { + expect(Config.setAds("on")).toBe(true); + expect(Config.setAds("sellout")).toBe(true); + expect(Config.setAds("invalid" as any)).toBe(false); + }); + it("setRepeatQuotes", () => { + expect(Config.setRepeatQuotes("off")).toBe(true); + expect(Config.setRepeatQuotes("typing")).toBe(true); + expect(Config.setRepeatQuotes("invalid" as any)).toBe(false); + }); + it("setOppositeShiftMode", () => { + expect(Config.setOppositeShiftMode("on")).toBe(true); + expect(Config.setOppositeShiftMode("keymap")).toBe(true); + expect(Config.setOppositeShiftMode("invalid" as any)).toBe(false); + }); + it("setCaretStyle", () => { + expect(Config.setCaretStyle("banana")).toBe(true); + expect(Config.setCaretStyle("block")).toBe(true); + expect(Config.setCaretStyle("invalid" as any)).toBe(false); + }); + it("setPaceCaretStyle", () => { + expect(Config.setPaceCaretStyle("carrot")).toBe(true); + expect(Config.setPaceCaretStyle("outline")).toBe(true); + expect(Config.setPaceCaretStyle("invalid" as any)).toBe(false); + }); + it("setShowAverage", () => { + expect(Config.setShowAverage("acc")).toBe(true); + expect(Config.setShowAverage("both")).toBe(true); + expect(Config.setShowAverage("invalid" as any)).toBe(false); + }); + it("setHighlightMode", () => { + expect(Config.setHighlightMode("letter")).toBe(true); + expect(Config.setHighlightMode("next_three_words")).toBe(true); + expect(Config.setHighlightMode("invalid" as any)).toBe(false); + }); + it("setTapeMode", () => { + expect(Config.setTapeMode("letter")).toBe(true); + expect(Config.setTapeMode("off")).toBe(true); + expect(Config.setTapeMode("invalid" as any)).toBe(false); + }); + it("setTimerStyle", () => { + expect(Config.setTimerStyle("bar")).toBe(true); + expect(Config.setTimerStyle("mini")).toBe(true); + expect(Config.setTimerStyle("invalid" as any)).toBe(false); + }); + it("setLiveSpeedStyle", () => { + expect(Config.setLiveSpeedStyle("text")).toBe(true); + expect(Config.setLiveSpeedStyle("mini")).toBe(true); + expect(Config.setLiveSpeedStyle("invalid" as any)).toBe(false); + }); + it("setLiveAccStyle", () => { + expect(Config.setLiveAccStyle("text")).toBe(true); + expect(Config.setLiveAccStyle("mini")).toBe(true); + expect(Config.setLiveAccStyle("invalid" as any)).toBe(false); + }); + it("setLiveBurstStyle", () => { + expect(Config.setLiveBurstStyle("text")).toBe(true); + expect(Config.setLiveBurstStyle("mini")).toBe(true); + expect(Config.setLiveBurstStyle("invalid" as any)).toBe(false); + }); + it("setTimerColor", () => { + expect(Config.setTimerColor("text")).toBe(true); + expect(Config.setTimerColor("sub")).toBe(true); + expect(Config.setTimerColor("invalid" as any)).toBe(false); + }); + it("setTimerOpacity", () => { + expect(Config.setTimerOpacity("1")).toBe(true); + expect(Config.setTimerOpacity("0.5")).toBe(true); + expect(Config.setTimerOpacity("invalid" as any)).toBe(false); + }); + it("setSmoothCaret", () => { + expect(Config.setSmoothCaret("fast")).toBe(true); + expect(Config.setSmoothCaret("medium")).toBe(true); + expect(Config.setSmoothCaret("invalid" as any)).toBe(false); + }); + it("setQuickRestartMode", () => { + expect(Config.setQuickRestartMode("off")).toBe(true); + expect(Config.setQuickRestartMode("tab")).toBe(true); + expect(Config.setQuickRestartMode("invalid" as any)).toBe(false); + }); + it("setConfidenceMode", () => { + expect(Config.setConfidenceMode("max")).toBe(true); + expect(Config.setConfidenceMode("on")).toBe(true); + expect(Config.setConfidenceMode("invalid" as any)).toBe(false); + }); + it("setIndicateTypos", () => { + expect(Config.setIndicateTypos("below")).toBe(true); + expect(Config.setIndicateTypos("off")).toBe(true); + expect(Config.setIndicateTypos("invalid" as any)).toBe(false); + }); + it("setRandomTheme", () => { + expect(Config.setRandomTheme("fav")).toBe(true); + expect(Config.setRandomTheme("off")).toBe(true); + expect(Config.setRandomTheme("invalid" as any)).toBe(false); + }); + it("setKeymapMode", () => { + expect(Config.setKeymapMode("next")).toBe(true); + expect(Config.setKeymapMode("react")).toBe(true); + expect(Config.setKeymapMode("invalid" as any)).toBe(false); + }); + it("setKeymapLegendStyle", () => { + expect(Config.setKeymapLegendStyle("blank")).toBe(true); + expect(Config.setKeymapLegendStyle("lowercase")).toBe(true); + expect(Config.setKeymapLegendStyle("invalid" as any)).toBe(false); + }); + it("setKeymapStyle", () => { + expect(Config.setKeymapStyle("matrix")).toBe(true); + expect(Config.setKeymapStyle("split")).toBe(true); + expect(Config.setKeymapStyle("invalid" as any)).toBe(false); + }); + it("setKeymapShowTopRow", () => { + expect(Config.setKeymapShowTopRow("always")).toBe(true); + expect(Config.setKeymapShowTopRow("never")).toBe(true); + expect(Config.setKeymapShowTopRow("invalid" as any)).toBe(false); + }); + it("setCustomBackgroundSize", () => { + expect(Config.setCustomBackgroundSize("contain")).toBe(true); + expect(Config.setCustomBackgroundSize("cover")).toBe(true); + expect(Config.setCustomBackgroundSize("invalid" as any)).toBe(false); + }); + it("setCustomBackgroundFilter", () => { + expect(Config.setCustomBackgroundFilter([0, 1, 2, 3])).toBe(true); + //gets converted + expect(Config.setCustomBackgroundFilter([0, 1, 2, 3, 4] as any)).toBe(true); + expect(Config.setCustomBackgroundFilter([] as any)).toBe(false); + expect(Config.setCustomBackgroundFilter(["invalid"] as any)).toBe(false); + expect(Config.setCustomBackgroundFilter([1, 2, 3, 4, 5, 6] as any)).toBe( + false + ); + }); + it("setMonkeyPowerLevel", () => { + expect(Config.setMonkeyPowerLevel("2")).toBe(true); + expect(Config.setMonkeyPowerLevel("off")).toBe(true); + + expect(Config.setMonkeyPowerLevel("invalid" as any)).toBe(false); + }); + it("setCustomThemeColors", () => { + expect(Config.setCustomThemeColors(customThemeColors(10))).toBe(true); + + //gets converted + expect(Config.setCustomThemeColors(customThemeColors(9))).toBe(true); + + expect(Config.setCustomThemeColors([] as any)).toBe(false); + expect(Config.setCustomThemeColors(["invalid"] as any)).toBe(false); + expect(Config.setCustomThemeColors(customThemeColors(5))).toBe(false); + expect(Config.setCustomThemeColors(customThemeColors(11))).toBe(false); + + const tenColors = customThemeColors(10); + tenColors[0] = "black"; + expect(Config.setCustomThemeColors(tenColors)).toBe(false); + tenColors[0] = "#123456"; + expect(Config.setCustomThemeColors(tenColors)).toBe(true); + tenColors[0] = "#1234"; + expect(Config.setCustomThemeColors(tenColors)).toBe(false); + }); + it("setNumbers", () => { + testBoolean(Config.setNumbers); + }); + it("setPunctuation", () => { + testBoolean(Config.setPunctuation); + }); + it("setBlindMode", () => { + testBoolean(Config.setBlindMode); + }); + it("setAccountChartResults", () => { + testBoolean(Config.setAccountChartResults); + }); + it("setAccountChartAccuracy", () => { + testBoolean(Config.setAccountChartAccuracy); + }); + it("setAccountChartAvg10", () => { + testBoolean(Config.setAccountChartAvg10); + }); + it("setAccountChartAvg100", () => { + testBoolean(Config.setAccountChartAvg100); + }); + it("setAlwaysShowDecimalPlaces", () => { + testBoolean(Config.setAlwaysShowDecimalPlaces); + }); + it("setShowOutOfFocusWarning", () => { + testBoolean(Config.setShowOutOfFocusWarning); + }); + it("setAlwaysShowWordsHistory", () => { + testBoolean(Config.setAlwaysShowWordsHistory); + }); + it("setCapsLockWarning", () => { + testBoolean(Config.setCapsLockWarning); + }); + it("setShowAllLines", () => { + testBoolean(Config.setShowAllLines); + }); + it("setQuickEnd", () => { + testBoolean(Config.setQuickEnd); + }); + it("setFlipTestColors", () => { + testBoolean(Config.setFlipTestColors); + }); + it("setColorfulMode", () => { + testBoolean(Config.setColorfulMode); + }); + it("setStrictSpace", () => { + testBoolean(Config.setStrictSpace); + }); + it("setHideExtraLetters", () => { + testBoolean(Config.setHideExtraLetters); + }); + it("setKeyTips", () => { + testBoolean(Config.setKeyTips); + }); + it("setStartGraphsAtZero", () => { + testBoolean(Config.setStartGraphsAtZero); + }); + it("setSmoothLineScroll", () => { + testBoolean(Config.setSmoothLineScroll); + }); + it("setFreedomMode", () => { + testBoolean(Config.setFreedomMode); + }); + it("setAutoSwitchTheme", () => { + testBoolean(Config.setAutoSwitchTheme); + }); + it("setCustomTheme", () => { + testBoolean(Config.setCustomTheme); + }); + it("setBritishEnglish", () => { + testBoolean(Config.setBritishEnglish); + }); + it("setLazyMode", () => { + testBoolean(Config.setLazyMode); + }); + it("setMonkey", () => { + testBoolean(Config.setMonkey); + }); + it("setBurstHeatmap", () => { + testBoolean(Config.setBurstHeatmap); + }); + it("setRepeatedPace", () => { + testBoolean(Config.setRepeatedPace); + }); + it("setFavThemes", () => { + expect(Config.setFavThemes([])).toBe(true); + expect(Config.setFavThemes(["test"])).toBe(true); + expect(Config.setFavThemes([stringOfLength(50)])).toBe(true); + + expect(Config.setFavThemes("invalid" as any)).toBe(false); + expect(Config.setFavThemes([stringOfLength(51)])).toBe(false); + }); + it("setFunbox", () => { + expect(Config.setFunbox("one")).toBe(true); + expect(Config.setFunbox("one#two")).toBe(true); + expect(Config.setFunbox("one#two#")).toBe(true); + expect(Config.setFunbox(stringOfLength(100))).toBe(true); + + expect(Config.setFunbox(stringOfLength(101))).toBe(false); + }); + it("setPaceCaretCustomSpeed", () => { + expect(Config.setPaceCaretCustomSpeed(0)).toBe(true); + expect(Config.setPaceCaretCustomSpeed(1)).toBe(true); + expect(Config.setPaceCaretCustomSpeed(11.11)).toBe(true); + + expect(Config.setPaceCaretCustomSpeed("invalid" as any)).toBe(false); + expect(Config.setPaceCaretCustomSpeed(-1)).toBe(false); + }); + it("setMinWpmCustomSpeed", () => { + expect(Config.setMinWpmCustomSpeed(0)).toBe(true); + expect(Config.setMinWpmCustomSpeed(1)).toBe(true); + expect(Config.setMinWpmCustomSpeed(11.11)).toBe(true); + + expect(Config.setMinWpmCustomSpeed("invalid" as any)).toBe(false); + expect(Config.setMinWpmCustomSpeed(-1)).toBe(false); + }); + it("setMinAccCustom", () => { + expect(Config.setMinAccCustom(0)).toBe(true); + expect(Config.setMinAccCustom(1)).toBe(true); + expect(Config.setMinAccCustom(11.11)).toBe(true); + //gets converted + expect(Config.setMinAccCustom(120)).toBe(true); + + expect(Config.setMinAccCustom("invalid" as any)).toBe(false); + expect(Config.setMinAccCustom(-1)).toBe(false); + }); + it("setMinBurstCustomSpeed", () => { + expect(Config.setMinBurstCustomSpeed(0)).toBe(true); + expect(Config.setMinBurstCustomSpeed(1)).toBe(true); + expect(Config.setMinBurstCustomSpeed(11.11)).toBe(true); + + expect(Config.setMinBurstCustomSpeed("invalid" as any)).toBe(false); + expect(Config.setMinBurstCustomSpeed(-1)).toBe(false); + }); + it("setTimeConfig", () => { + expect(Config.setTimeConfig(0)).toBe(true); + expect(Config.setTimeConfig(1)).toBe(true); + + //gets converted + expect(Config.setTimeConfig("invalid" as any)).toBe(true); + expect(Config.setTimeConfig(-1)).toBe(true); + + expect(Config.setTimeConfig(11.11)).toBe(false); + }); + it("setWordCount", () => { + expect(Config.setWordCount(0)).toBe(true); + expect(Config.setWordCount(1)).toBe(true); + + //gets converted + expect(Config.setWordCount(-1)).toBe(true); + + expect(Config.setWordCount("invalid" as any)).toBe(false); + expect(Config.setWordCount(11.11)).toBe(false); + }); + it("setFontFamily", () => { + expect(Config.setFontFamily("Arial")).toBe(true); + expect(Config.setFontFamily("roboto_mono")).toBe(true); + expect(Config.setFontFamily("test_font")).toBe(true); + expect(Config.setFontFamily(stringOfLength(50))).toBe(true); + + expect(Config.setFontFamily(stringOfLength(51))).toBe(false); + expect(Config.setFontFamily("test font")).toBe(false); + expect(Config.setFontFamily("test!font")).toBe(false); + }); + it("setTheme", () => { + expect(Config.setTheme("serika")).toBe(true); + expect(Config.setTheme("serika_dark")).toBe(true); + expect(Config.setTheme(stringOfLength(50))).toBe(true); + + expect(Config.setTheme("serika dark")).toBe(false); + expect(Config.setTheme("serika-dark")).toBe(false); + expect(Config.setTheme(stringOfLength(51))).toBe(false); + }); + it("setThemeLight", () => { + expect(Config.setThemeLight("serika")).toBe(true); + expect(Config.setThemeLight("serika_dark")).toBe(true); + expect(Config.setThemeLight(stringOfLength(50))).toBe(true); + + expect(Config.setThemeLight("serika dark")).toBe(false); + expect(Config.setThemeLight("serika-dark")).toBe(false); + expect(Config.setThemeLight(stringOfLength(51))).toBe(false); + }); + it("setThemeDark", () => { + expect(Config.setThemeDark("serika")).toBe(true); + expect(Config.setThemeDark("serika_dark")).toBe(true); + expect(Config.setThemeDark(stringOfLength(50))).toBe(true); + + expect(Config.setThemeDark("serika dark")).toBe(false); + expect(Config.setThemeDark("serika-dark")).toBe(false); + expect(Config.setThemeDark(stringOfLength(51))).toBe(false); + }); + it("setLanguage", () => { + expect(Config.setLanguage("english")).toBe(true); + expect(Config.setLanguage("english_1k")).toBe(true); + expect(Config.setLanguage(stringOfLength(50))).toBe(true); + + expect(Config.setLanguage("english 1k")).toBe(false); + expect(Config.setLanguage("english-1k")).toBe(false); + expect(Config.setLanguage(stringOfLength(51))).toBe(false); + }); + it("setKeymapLayout", () => { + expect(Config.setKeymapLayout("overrideSync")).toBe(true); + expect(Config.setKeymapLayout("override_sync")).toBe(true); + expect(Config.setKeymapLayout("override sync")).toBe(true); + expect(Config.setKeymapLayout("override-sync!")).toBe(true); + expect(Config.setKeymapLayout(stringOfLength(50))).toBe(true); + + expect(Config.setKeymapLayout(stringOfLength(51))).toBe(false); + }); + it("setLayout", () => { + expect(Config.setLayout("semimak")).toBe(true); + expect(Config.setLayout("semi_mak")).toBe(true); + expect(Config.setLayout(stringOfLength(50))).toBe(true); + + expect(Config.setLayout("semi mak")).toBe(false); + expect(Config.setLayout("semi-mak")).toBe(false); + expect(Config.setLayout(stringOfLength(51))).toBe(false); + }); + it("setFontSize", () => { + expect(Config.setFontSize(1)).toBe(true); + + //gets converted + expect(Config.setFontSize(-1)).toBe(true); + expect(Config.setFontSize("1" as any)).toBe(true); + expect(Config.setFontSize("125" as any)).toBe(true); + expect(Config.setFontSize("15" as any)).toBe(true); + expect(Config.setFontSize("2" as any)).toBe(true); + expect(Config.setFontSize("3" as any)).toBe(true); + expect(Config.setFontSize("4" as any)).toBe(true); + + expect(Config.setFontSize(0)).toBe(false); + expect(Config.setFontSize("5" as any)).toBe(false); + expect(Config.setFontSize("invalid" as any)).toBe(false); + }); + it("setMaxLineWidth", () => { + expect(Config.setMaxLineWidth(0)).toBe(true); + expect(Config.setMaxLineWidth(50)).toBe(true); + expect(Config.setMaxLineWidth(50.5)).toBe(true); + + //gets converted + expect(Config.setMaxLineWidth(10)).toBe(true); + expect(Config.setMaxLineWidth(10_000)).toBe(true); + expect(Config.setMaxLineWidth("invalid" as any)).toBe(false); + }); + it("setCustomBackground", () => { + expect(Config.setCustomBackground("http://example.com/test.png")).toBe( + true + ); + expect(Config.setCustomBackground("https://www.example.com/test.gif")).toBe( + true + ); + expect(Config.setCustomBackground("https://example.com/test.jpg")).toBe( + true + ); + expect(Config.setCustomBackground("http://www.example.com/test.jpeg")).toBe( + true + ); + + //gets converted + expect( + Config.setCustomBackground(" http://example.com/test.png ") + ).toBe(true); + + expect(Config.setCustomBackground("http://www.example.com/test.webp")).toBe( + false + ); + expect( + Config.setCustomBackground("http://www.example.com/test?test=foo&bar=baz") + ).toBe(false); + expect(Config.setCustomBackground("invalid")).toBe(false); + }); + it("setQuoteLength", () => { + expect(Config.setQuoteLength(0)).toBe(true); + expect(Config.setQuoteLength(-3)).toBe(true); + expect(Config.setQuoteLength(3)).toBe(true); + + expect(Config.setQuoteLength(-4 as any)).toBe(false); + expect(Config.setQuoteLength(4 as any)).toBe(false); + + expect(Config.setQuoteLength([0, -3, 2])).toBe(true); + + expect(Config.setQuoteLength([-4 as any, 5 as any])).toBe(false); + }); +}); + +function customThemeColors(n: number): CustomThemeColors { + return new Array(n).fill("#000") as CustomThemeColors; +} + +function testBoolean(fn: (val: boolean) => boolean): void { + expect(fn(true)).toBe(true); + expect(fn(false)).toBe(true); + + expect(fn("true" as any)).toBe(false); + expect(fn("0" as any)).toBe(false); + expect(fn("invalid" as any)).toBe(false); +} + +function stringOfLength(length: number): string { + return randomBytes(Math.ceil(length / 2)) + .toString("hex") + .slice(0, length); +} diff --git a/frontend/__tests__/tsconfig.json b/frontend/__tests__/tsconfig.json index 55fb82015..ce1b8aff4 100644 --- a/frontend/__tests__/tsconfig.json +++ b/frontend/__tests__/tsconfig.json @@ -1,18 +1,8 @@ { + "extends": "@monkeytype/typescript-config/base.json", "compilerOptions": { - "incremental": true, - "module": "commonjs", - "target": "es6", - "sourceMap": false, - "allowJs": true, - "checkJs": true, - "outDir": "build", - "moduleResolution": "node", - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "strictNullChecks": true, - "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ESNext", "noEmit": true, "types": ["vitest/globals"] }, diff --git a/frontend/__tests__/test/config.spec.ts b/frontend/__tests__/utils/config.spec.ts similarity index 100% rename from frontend/__tests__/test/config.spec.ts rename to frontend/__tests__/utils/config.spec.ts diff --git a/frontend/package.json b/frontend/package.json index 3a5cead84..162d87f3e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,7 @@ "ts-check": "tsc --noEmit", "build": "npm run madge && vite build", "madge": " madge --circular --extensions ts ./src", - "live": "npm run build && vite preview --port 3000", + "start": "vite preview --port 3000", "dev": "vite dev", "deploy-live": "npm run validate-json && npm run build && firebase deploy -P live --only hosting", "deploy-preview": "npm run validate-json && npm run build && firebase hosting:channel:deploy preview -P live --expires 2h", @@ -71,6 +71,7 @@ }, "dependencies": { "@date-fns/utc": "1.2.0", + "@monkeytype/contracts": "*", "axios": "1.6.4", "canvas-confetti": "1.5.1", "chart.js": "3.7.1", diff --git a/frontend/src/ts/ape/adapters/ts-rest-adapter.ts b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts new file mode 100644 index 000000000..e3cb2ea2e --- /dev/null +++ b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts @@ -0,0 +1,74 @@ +import { AppRouter, initClient, type ApiFetcherArgs } from "@ts-rest/core"; +import { Method } from "axios"; +import { getIdToken } from "firebase/auth"; +import { envConfig } from "../../constants/env-config"; +import { getAuthenticatedUser, isAuthenticated } from "../../firebase"; +import { EndpointMetadata } from "@monkeytype/contracts/schemas/api"; + +function buildApi(timeout: number): (args: ApiFetcherArgs) => Promise<{ + status: number; + body: unknown; + headers: Headers; +}> { + return async (request: ApiFetcherArgs) => { + const isPublicEndpoint = + (request.route.metadata as EndpointMetadata | undefined) + ?.authenticationOptions?.isPublic ?? false; + + try { + const headers: HeadersInit = { + ...request.headers, + "X-Client-Version": envConfig.clientVersion, + }; + if (!isPublicEndpoint) { + const token = isAuthenticated() + ? await getIdToken(getAuthenticatedUser()) + : ""; + + headers["Authorization"] = `Bearer ${token}`; + } + const response = await fetch(request.path, { + signal: AbortSignal.timeout(timeout), + method: request.method as Method, + headers, + body: request.body, + }); + const body = await response.json(); + if (response.status >= 400) { + console.error(`${request.method} ${request.path} failed`, { + status: response.status, + ...body, + }); + } + + return { + status: response.status, + body, + headers: response.headers ?? new Headers(), + }; + } catch (e: Error | unknown) { + return { + status: 500, + body: { message: e }, + headers: new Headers(), + }; + } + }; +} + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +export function buildClient( + contract: T, + baseUrl: string, + timeout: number = 10_000 +) { + return initClient(contract, { + baseUrl: baseUrl, + jsonQuery: true, + api: buildApi(timeout), + baseHeaders: { + Accept: "application/json", + }, + }); +} +/* eslint-enable @typescript-eslint/explicit-function-return-type */ diff --git a/frontend/src/ts/ape/endpoints/configs.ts b/frontend/src/ts/ape/endpoints/configs.ts deleted file mode 100644 index 5742af78f..000000000 --- a/frontend/src/ts/ape/endpoints/configs.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Config } from "@monkeytype/shared-types/config"; - -const BASE_PATH = "/configs"; - -export default class Configs { - constructor(private httpClient: Ape.HttpClient) { - this.httpClient = httpClient; - } - - async get(): Ape.EndpointResponse { - return await this.httpClient.get(BASE_PATH); - } - - async save(config: Config): Ape.EndpointResponse { - return await this.httpClient.patch(BASE_PATH, { payload: { config } }); - } -} diff --git a/frontend/src/ts/ape/endpoints/index.ts b/frontend/src/ts/ape/endpoints/index.ts index 8656133c4..2e356a541 100644 --- a/frontend/src/ts/ape/endpoints/index.ts +++ b/frontend/src/ts/ape/endpoints/index.ts @@ -1,4 +1,3 @@ -import Configs from "./configs"; import Leaderboards from "./leaderboards"; import Presets from "./presets"; import Psas from "./psas"; @@ -11,7 +10,6 @@ import Configuration from "./configuration"; import Dev from "./dev"; export default { - Configs, Leaderboards, Presets, Psas, diff --git a/frontend/src/ts/ape/index.ts b/frontend/src/ts/ape/index.ts index 30fa84b3a..ecfca4584 100644 --- a/frontend/src/ts/ape/index.ts +++ b/frontend/src/ts/ape/index.ts @@ -1,17 +1,19 @@ import endpoints from "./endpoints"; import { buildHttpClient } from "./adapters/axios-adapter"; import { envConfig } from "../constants/env-config"; +import { buildClient } from "./adapters/ts-rest-adapter"; +import { configsContract } from "@monkeytype/contracts/configs"; const API_PATH = ""; const BASE_URL = envConfig.backendUrl; const API_URL = `${BASE_URL}${API_PATH}`; -const httpClient = buildHttpClient(API_URL, 10000); +const httpClient = buildHttpClient(API_URL, 10_000); // API Endpoints const Ape = { users: new endpoints.Users(httpClient), - configs: new endpoints.Configs(httpClient), + configs: buildClient(configsContract, BASE_URL, 10_000), results: new endpoints.Results(httpClient), psas: new endpoints.Psas(httpClient), quotes: new endpoints.Quotes(httpClient), diff --git a/frontend/src/ts/ape/types/configs.d.ts b/frontend/src/ts/ape/types/configs.d.ts deleted file mode 100644 index 54780ea1d..000000000 --- a/frontend/src/ts/ape/types/configs.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -// for some reason when using the dot notaion, the types are not being recognized as used -declare namespace Ape.Configs { - type GetConfig = { - _id: string; - uid: string; - config: Partial; - }; - type PostConfig = null; -} diff --git a/frontend/src/ts/config-validation.ts b/frontend/src/ts/config-validation.ts index 9d9777422..91b98ec56 100644 --- a/frontend/src/ts/config-validation.ts +++ b/frontend/src/ts/config-validation.ts @@ -1,18 +1,7 @@ import * as Misc from "./utils/misc"; import * as JSONData from "./utils/json-data"; import * as Notifications from "./elements/notifications"; - -type PossibleType = - | "string" - | "number" - | "numberArray" - | "boolean" - | "undefined" - | "null" - | "stringArray" - | "layoutfluid" - | string[] - | number[]; +import { ZodSchema, z } from "zod"; type PossibleTypeAsync = "layoutfluid"; @@ -38,70 +27,19 @@ function invalid(key: string, val: unknown, customMessage?: string): void { console.error(`Invalid value key ${key} value ${val} type ${typeof val}`); } -function isArray(val: unknown): val is unknown[] { - return val instanceof Array; -} - -export function isConfigValueValid( +export function isConfigValueValid( key: string, - val: unknown, - possibleTypes: PossibleType[] + val: T, + schema: ZodSchema ): boolean { - let isValid = false; - - // might be used in the future - // eslint-disable-next-line - let customMessage: string | undefined = undefined; - - for (const possibleType of possibleTypes) { - switch (possibleType) { - case "boolean": - if (typeof val === "boolean") isValid = true; - break; - - case "null": - if (val === null) isValid = true; - break; - - case "number": - if (typeof val === "number" && !isNaN(val)) isValid = true; - break; - - case "numberArray": - if ( - isArray(val) && - val.every((v) => typeof v === "number" && !isNaN(v)) - ) { - isValid = true; - } - break; - - case "string": - if (typeof val === "string") isValid = true; - break; - - case "stringArray": - if (isArray(val) && val.every((v) => typeof v === "string")) { - isValid = true; - } - break; - - case "undefined": - if (typeof val === "undefined" || val === undefined) isValid = true; - break; - - default: - if (isArray(possibleType)) { - if (possibleType.includes(val as never)) isValid = true; - } - break; - } - } - - if (!isValid) invalid(key, val, customMessage); + const isValid = schema.safeParse(val).success; + if (!isValid) invalid(key, val, undefined); return isValid; } +export function isConfigValueValidBoolean(key: string, val: boolean): boolean { + return isConfigValueValid(key, val, z.boolean()); +} export async function isConfigValueValidAsync( key: string, diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index 384930950..45ff6d91a 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -2,8 +2,9 @@ import * as DB from "./db"; import * as OutOfFocus from "./test/out-of-focus"; import * as Notifications from "./elements/notifications"; import { - isConfigValueValid, isConfigValueValidAsync, + isConfigValueValidBoolean, + isConfigValueValid, } from "./config-validation"; import * as ConfigEvent from "./observables/config-event"; import DefaultConfig from "./constants/default-config"; @@ -16,9 +17,10 @@ import { canSetFunboxWithConfig, } from "./test/funbox/funbox-validation"; import { isDevEnvironment, reloadAfter } from "./utils/misc"; -import * as ConfigTypes from "@monkeytype/shared-types/config"; +import * as ConfigSchemas from "@monkeytype/contracts/schemas/configs"; +import { Config } from "@monkeytype/contracts/schemas/configs"; -export let localStorageConfig: ConfigTypes.Config; +export let localStorageConfig: Config; let loadDone: (value?: unknown) => void; @@ -26,7 +28,7 @@ const config = { ...DefaultConfig, }; -let configToSend = {} as ConfigTypes.Config; +let configToSend = {} as Config; const saveToDatabase = debounce(1000, () => { if (Object.keys(configToSend).length > 0) { AccountButton.loading(true); @@ -34,11 +36,11 @@ const saveToDatabase = debounce(1000, () => { AccountButton.loading(false); }); } - configToSend = {} as ConfigTypes.Config; + configToSend = {} as Config; }); function saveToLocalStorage( - key: keyof ConfigTypes.Config, + key: keyof Config, nosave = false, noDbCheck = false ): void { @@ -70,7 +72,7 @@ export function saveFullConfigToLocalStorage(noDbCheck = false): void { //numbers export function setNumbers(numb: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("numbers", numb, ["boolean"])) return false; + if (!isConfigValueValidBoolean("numbers", numb)) return false; if (!canSetConfigWithCurrentFunboxes("numbers", numb, config.funbox)) { return false; @@ -88,7 +90,7 @@ export function setNumbers(numb: boolean, nosave?: boolean): boolean { //punctuation export function setPunctuation(punc: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("punctuation", punc, ["boolean"])) return false; + if (!isConfigValueValidBoolean("punctuation", punc)) return false; if (!canSetConfigWithCurrentFunboxes("punctuation", punc, config.funbox)) { return false; @@ -104,12 +106,8 @@ export function setPunctuation(punc: boolean, nosave?: boolean): boolean { return true; } -export function setMode(mode: ConfigTypes.Mode, nosave?: boolean): boolean { - if ( - !isConfigValueValid("mode", mode, [ - ["time", "words", "quote", "zen", "custom"], - ]) - ) { +export function setMode(mode: ConfigSchemas.Mode, nosave?: boolean): boolean { + if (!isConfigValueValid("mode", mode, ConfigSchemas.ModeSchema)) { return false; } @@ -137,13 +135,15 @@ export function setMode(mode: ConfigTypes.Mode, nosave?: boolean): boolean { } export function setPlaySoundOnError( - val: ConfigTypes.PlaySoundOnError, + val: ConfigSchemas.PlaySoundOnError, nosave?: boolean ): boolean { if ( - !isConfigValueValid("play sound on error", val, [ - ["off", "1", "2", "3", "4"], - ]) + !isConfigValueValid( + "play sound on error", + val, + ConfigSchemas.PlaySoundOnErrorSchema + ) ) { return false; } @@ -156,30 +156,15 @@ export function setPlaySoundOnError( } export function setPlaySoundOnClick( - val: ConfigTypes.PlaySoundOnClick, + val: ConfigSchemas.PlaySoundOnClick, nosave?: boolean ): boolean { if ( - !isConfigValueValid("play sound on click", val, [ - [ - "off", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - "14", - "15", - ], - ]) + !isConfigValueValid( + "play sound on click", + val, + ConfigSchemas.PlaySoundOnClickSchema + ) ) { return false; } @@ -192,10 +177,12 @@ export function setPlaySoundOnClick( } export function setSoundVolume( - val: ConfigTypes.SoundVolume, + val: ConfigSchemas.SoundVolume, nosave?: boolean ): boolean { - if (!isConfigValueValid("sound volume", val, [["0.1", "0.5", "1.0"]])) { + if ( + !isConfigValueValid("sound volume", val, ConfigSchemas.SoundVolumeSchema) + ) { return false; } @@ -208,12 +195,10 @@ export function setSoundVolume( //difficulty export function setDifficulty( - diff: ConfigTypes.Difficulty, + diff: ConfigSchemas.Difficulty, nosave?: boolean ): boolean { - if ( - !isConfigValueValid("difficulty", diff, [["normal", "expert", "master"]]) - ) { + if (!isConfigValueValid("difficulty", diff, ConfigSchemas.DifficultySchema)) { return false; } @@ -225,8 +210,17 @@ export function setDifficulty( } //set fav themes -export function setFavThemes(themes: string[], nosave?: boolean): boolean { - if (!isConfigValueValid("favorite themes", themes, ["stringArray"])) { +export function setFavThemes( + themes: ConfigSchemas.FavThemes, + nosave?: boolean +): boolean { + if ( + !isConfigValueValid( + "favorite themes", + themes, + ConfigSchemas.FavThemesSchema + ) + ) { return false; } config.favThemes = themes; @@ -236,8 +230,12 @@ export function setFavThemes(themes: string[], nosave?: boolean): boolean { return true; } -export function setFunbox(funbox: string, nosave?: boolean): boolean { - if (!isConfigValueValid("funbox", funbox, ["string"])) return false; +export function setFunbox( + funbox: ConfigSchemas.Funbox, + nosave?: boolean +): boolean { + if (!isConfigValueValid("funbox", funbox, ConfigSchemas.FunboxSchema)) + return false; for (const funbox of config.funbox.split("#")) { if (!canSetFunboxWithConfig(funbox, config)) { @@ -254,10 +252,11 @@ export function setFunbox(funbox: string, nosave?: boolean): boolean { } export function toggleFunbox( - funbox: string, + funbox: ConfigSchemas.Funbox, nosave?: boolean ): number | boolean { - if (!isConfigValueValid("funbox", funbox, ["string"])) return false; + if (!isConfigValueValid("funbox", funbox, ConfigSchemas.FunboxSchema)) + return false; let r; @@ -287,7 +286,7 @@ export function toggleFunbox( } export function setBlindMode(blind: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("blind mode", blind, ["boolean"])) return false; + if (!isConfigValueValidBoolean("blind mode", blind)) return false; config.blindMode = blind; saveToLocalStorage("blindMode", nosave); @@ -296,20 +295,24 @@ export function setBlindMode(blind: boolean, nosave?: boolean): boolean { return true; } -function setAccountChart( - array: ConfigTypes.AccountChart, +export function setAccountChart( + array: ConfigSchemas.AccountChart, nosave?: boolean ): boolean { - if ( - !isConfigValueValid("account chart", array, [["on", "off"], "stringArray"]) - ) { - return false; - } - if (array.length !== 4) { array = ["on", "on", "on", "on"]; } + if ( + !isConfigValueValid( + "account chart", + array, + ConfigSchemas.AccountChartSchema + ) + ) { + return false; + } + config.accountChart = array; saveToLocalStorage("accountChart", nosave); ConfigEvent.dispatch("accountChart", config.accountChart); @@ -321,7 +324,7 @@ export function setAccountChartResults( value: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValid("account chart results", value, ["boolean"])) { + if (!isConfigValueValidBoolean("account chart results", value)) { return false; } @@ -336,7 +339,7 @@ export function setAccountChartAccuracy( value: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValid("account chart accuracy", value, ["boolean"])) { + if (!isConfigValueValidBoolean("account chart accuracy", value)) { return false; } @@ -351,7 +354,7 @@ export function setAccountChartAvg10( value: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValid("account chart avg 10", value, ["boolean"])) { + if (!isConfigValueValidBoolean("account chart avg 10", value)) { return false; } @@ -366,7 +369,7 @@ export function setAccountChartAvg100( value: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValid("account chart avg 100", value, ["boolean"])) { + if (!isConfigValueValidBoolean("account chart avg 100", value)) { return false; } @@ -378,10 +381,12 @@ export function setAccountChartAvg100( } export function setStopOnError( - soe: ConfigTypes.StopOnError, + soe: ConfigSchemas.StopOnError, nosave?: boolean ): boolean { - if (!isConfigValueValid("stop on error", soe, [["off", "word", "letter"]])) { + if ( + !isConfigValueValid("stop on error", soe, ConfigSchemas.StopOnErrorSchema) + ) { return false; } @@ -400,7 +405,7 @@ export function setAlwaysShowDecimalPlaces( val: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValid("always show decimal places", val, ["boolean"])) { + if (!isConfigValueValidBoolean("always show decimal places", val)) { return false; } @@ -415,13 +420,15 @@ export function setAlwaysShowDecimalPlaces( } export function setTypingSpeedUnit( - val: ConfigTypes.TypingSpeedUnit, + val: ConfigSchemas.TypingSpeedUnit, nosave?: boolean ): boolean { if ( - !isConfigValueValid("typing speed unit", val, [ - ["wpm", "cpm", "wps", "cps", "wph"], - ]) + !isConfigValueValid( + "typing speed unit", + val, + ConfigSchemas.TypingSpeedUnitSchema + ) ) { return false; } @@ -436,7 +443,7 @@ export function setShowOutOfFocusWarning( val: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValid("show out of focus warning", val, ["boolean"])) { + if (!isConfigValueValidBoolean("show out of focus warning", val)) { return false; } @@ -452,14 +459,10 @@ export function setShowOutOfFocusWarning( //pace caret export function setPaceCaret( - val: ConfigTypes.PaceCaret, + val: ConfigSchemas.PaceCaret, nosave?: boolean ): boolean { - if ( - !isConfigValueValid("pace caret", val, [ - ["custom", "off", "average", "pb", "last", "daily"], - ]) - ) { + if (!isConfigValueValid("pace caret", val, ConfigSchemas.PaceCaretSchema)) { return false; } @@ -481,10 +484,16 @@ export function setPaceCaret( } export function setPaceCaretCustomSpeed( - val: number, + val: ConfigSchemas.PaceCaretCustomSpeed, nosave?: boolean ): boolean { - if (!isConfigValueValid("pace caret custom speed", val, ["number"])) { + if ( + !isConfigValueValid( + "pace caret custom speed", + val, + ConfigSchemas.PaceCaretCustomSpeedSchema + ) + ) { return false; } @@ -496,7 +505,7 @@ export function setPaceCaretCustomSpeed( } export function setRepeatedPace(pace: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("repeated pace", pace, ["boolean"])) return false; + if (!isConfigValueValidBoolean("repeated pace", pace)) return false; config.repeatedPace = pace; saveToLocalStorage("repeatedPace", nosave); @@ -507,10 +516,16 @@ export function setRepeatedPace(pace: boolean, nosave?: boolean): boolean { //min wpm export function setMinWpm( - minwpm: ConfigTypes.MinimumWordsPerMinute, + minwpm: ConfigSchemas.MinimumWordsPerMinute, nosave?: boolean ): boolean { - if (!isConfigValueValid("min speed", minwpm, [["off", "custom"]])) { + if ( + !isConfigValueValid( + "min speed", + minwpm, + ConfigSchemas.MinimumWordsPerMinuteSchema + ) + ) { return false; } @@ -521,8 +536,17 @@ export function setMinWpm( return true; } -export function setMinWpmCustomSpeed(val: number, nosave?: boolean): boolean { - if (!isConfigValueValid("min speed custom", val, ["number"])) { +export function setMinWpmCustomSpeed( + val: ConfigSchemas.MinWpmCustomSpeed, + nosave?: boolean +): boolean { + if ( + !isConfigValueValid( + "min speed custom", + val, + ConfigSchemas.MinWpmCustomSpeedSchema + ) + ) { return false; } @@ -535,10 +559,11 @@ export function setMinWpmCustomSpeed(val: number, nosave?: boolean): boolean { //min acc export function setMinAcc( - min: ConfigTypes.MinimumAccuracy, + min: ConfigSchemas.MinimumAccuracy, nosave?: boolean ): boolean { - if (!isConfigValueValid("min acc", min, [["off", "custom"]])) return false; + if (!isConfigValueValid("min acc", min, ConfigSchemas.MinimumAccuracySchema)) + return false; config.minAcc = min; saveToLocalStorage("minAcc", nosave); @@ -547,9 +572,20 @@ export function setMinAcc( return true; } -export function setMinAccCustom(val: number, nosave?: boolean): boolean { - if (!isConfigValueValid("min acc custom", val, ["number"])) return false; +export function setMinAccCustom( + val: ConfigSchemas.MinimumAccuracyCustom, + nosave?: boolean +): boolean { + //migrate legacy configs if (val > 100) val = 100; + if ( + !isConfigValueValid( + "min acc custom", + val, + ConfigSchemas.MinimumAccuracyCustomSchema + ) + ) + return false; config.minAccCustom = val; saveToLocalStorage("minAccCustom", nosave); @@ -560,10 +596,10 @@ export function setMinAccCustom(val: number, nosave?: boolean): boolean { //min burst export function setMinBurst( - min: ConfigTypes.MinimumBurst, + min: ConfigSchemas.MinimumBurst, nosave?: boolean ): boolean { - if (!isConfigValueValid("min burst", min, [["off", "fixed", "flex"]])) { + if (!isConfigValueValid("min burst", min, ConfigSchemas.MinimumBurstSchema)) { return false; } @@ -574,8 +610,17 @@ export function setMinBurst( return true; } -export function setMinBurstCustomSpeed(val: number, nosave?: boolean): boolean { - if (!isConfigValueValid("min burst custom speed", val, ["number"])) { +export function setMinBurstCustomSpeed( + val: ConfigSchemas.MinimumBurstCustomSpeed, + nosave?: boolean +): boolean { + if ( + !isConfigValueValid( + "min burst custom speed", + val, + ConfigSchemas.MinimumBurstCustomSpeedSchema + ) + ) { return false; } @@ -591,7 +636,7 @@ export function setAlwaysShowWordsHistory( val: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValid("always show words history", val, ["boolean"])) { + if (!isConfigValueValidBoolean("always show words history", val)) { return false; } @@ -604,11 +649,15 @@ export function setAlwaysShowWordsHistory( //single list command line export function setSingleListCommandLine( - option: ConfigTypes.SingleListCommandLine, + option: ConfigSchemas.SingleListCommandLine, nosave?: boolean ): boolean { if ( - !isConfigValueValid("single list command line", option, [["manual", "on"]]) + !isConfigValueValid( + "single list command line", + option, + ConfigSchemas.SingleListCommandLineSchema + ) ) { return false; } @@ -622,7 +671,7 @@ export function setSingleListCommandLine( //caps lock warning export function setCapsLockWarning(val: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("caps lock warning", val, ["boolean"])) return false; + if (!isConfigValueValidBoolean("caps lock warning", val)) return false; config.capsLockWarning = val; saveToLocalStorage("capsLockWarning", nosave); @@ -632,7 +681,7 @@ export function setCapsLockWarning(val: boolean, nosave?: boolean): boolean { } export function setShowAllLines(sal: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("show all lines", sal, ["boolean"])) return false; + if (!isConfigValueValidBoolean("show all lines", sal)) return false; if (sal && config.tapeMode !== "off") { Notifications.add("Show all lines doesn't support tape mode", 0); @@ -647,7 +696,7 @@ export function setShowAllLines(sal: boolean, nosave?: boolean): boolean { } export function setQuickEnd(qe: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("quick end", qe, ["boolean"])) return false; + if (!isConfigValueValidBoolean("quick end", qe)) return false; config.quickEnd = qe; saveToLocalStorage("quickEnd", nosave); @@ -656,8 +705,8 @@ export function setQuickEnd(qe: boolean, nosave?: boolean): boolean { return true; } -export function setAds(val: ConfigTypes.Ads, nosave?: boolean): boolean { - if (!isConfigValueValid("ads", val, [["off", "result", "on", "sellout"]])) { +export function setAds(val: ConfigSchemas.Ads, nosave?: boolean): boolean { + if (!isConfigValueValid("ads", val, ConfigSchemas.AdsSchema)) { return false; } @@ -678,10 +727,12 @@ export function setAds(val: ConfigTypes.Ads, nosave?: boolean): boolean { } export function setRepeatQuotes( - val: ConfigTypes.RepeatQuotes, + val: ConfigSchemas.RepeatQuotes, nosave?: boolean ): boolean { - if (!isConfigValueValid("repeat quotes", val, [["off", "typing"]])) { + if ( + !isConfigValueValid("repeat quotes", val, ConfigSchemas.RepeatQuotesSchema) + ) { return false; } @@ -694,7 +745,7 @@ export function setRepeatQuotes( //flip colors export function setFlipTestColors(flip: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("flip test colors", flip, ["boolean"])) return false; + if (!isConfigValueValidBoolean("flip test colors", flip)) return false; config.flipTestColors = flip; saveToLocalStorage("flipTestColors", nosave); @@ -705,7 +756,7 @@ export function setFlipTestColors(flip: boolean, nosave?: boolean): boolean { //extra color export function setColorfulMode(extra: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("colorful mode", extra, ["boolean"])) return false; + if (!isConfigValueValidBoolean("colorful mode", extra)) return false; config.colorfulMode = extra; saveToLocalStorage("colorfulMode", nosave); @@ -716,7 +767,7 @@ export function setColorfulMode(extra: boolean, nosave?: boolean): boolean { //strict space export function setStrictSpace(val: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("strict space", val, ["boolean"])) return false; + if (!isConfigValueValidBoolean("strict space", val)) return false; config.strictSpace = val; saveToLocalStorage("strictSpace", nosave); @@ -727,11 +778,15 @@ export function setStrictSpace(val: boolean, nosave?: boolean): boolean { //opposite shift space export function setOppositeShiftMode( - val: ConfigTypes.OppositeShiftMode, + val: ConfigSchemas.OppositeShiftMode, nosave?: boolean ): boolean { if ( - !isConfigValueValid("opposite shift mode", val, [["off", "on", "keymap"]]) + !isConfigValueValid( + "opposite shift mode", + val, + ConfigSchemas.OppositeShiftModeSchema + ) ) { return false; } @@ -744,13 +799,15 @@ export function setOppositeShiftMode( } export function setCaretStyle( - caretStyle: ConfigTypes.CaretStyle, + caretStyle: ConfigSchemas.CaretStyle, nosave?: boolean ): boolean { if ( - !isConfigValueValid("caret style", caretStyle, [ - ["off", "default", "block", "outline", "underline", "carrot", "banana"], - ]) + !isConfigValueValid( + "caret style", + caretStyle, + ConfigSchemas.CaretStyleSchema + ) ) { return false; } @@ -786,13 +843,15 @@ export function setCaretStyle( } export function setPaceCaretStyle( - caretStyle: ConfigTypes.CaretStyle, + caretStyle: ConfigSchemas.CaretStyle, nosave?: boolean ): boolean { if ( - !isConfigValueValid("pace caret style", caretStyle, [ - ["off", "default", "block", "outline", "underline", "carrot", "banana"], - ]) + !isConfigValueValid( + "pace caret style", + caretStyle, + ConfigSchemas.CaretStyleSchema + ) ) { return false; } @@ -826,13 +885,11 @@ export function setPaceCaretStyle( } export function setShowAverage( - value: ConfigTypes.ShowAverage, + value: ConfigSchemas.ShowAverage, nosave?: boolean ): boolean { if ( - !isConfigValueValid("show average", value, [ - ["off", "speed", "acc", "both"], - ]) + !isConfigValueValid("show average", value, ConfigSchemas.ShowAverageSchema) ) { return false; } @@ -845,20 +902,15 @@ export function setShowAverage( } export function setHighlightMode( - mode: ConfigTypes.HighlightMode, + mode: ConfigSchemas.HighlightMode, nosave?: boolean ): boolean { if ( - !isConfigValueValid("highlight mode", mode, [ - [ - "off", - "letter", - "word", - "next_word", - "next_two_words", - "next_three_words", - ], - ]) + !isConfigValueValid( + "highlight mode", + mode, + ConfigSchemas.HighlightModeSchema + ) ) { return false; } @@ -875,10 +927,10 @@ export function setHighlightMode( } export function setTapeMode( - mode: ConfigTypes.TapeMode, + mode: ConfigSchemas.TapeMode, nosave?: boolean ): boolean { - if (!isConfigValueValid("tape mode", mode, [["off", "letter", "word"]])) { + if (!isConfigValueValid("tape mode", mode, ConfigSchemas.TapeModeSchema)) { return false; } @@ -894,7 +946,7 @@ export function setTapeMode( } export function setHideExtraLetters(val: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("hide extra letters", val, ["boolean"])) return false; + if (!isConfigValueValidBoolean("hide extra letters", val)) return false; config.hideExtraLetters = val; saveToLocalStorage("hideExtraLetters", nosave); @@ -904,11 +956,11 @@ export function setHideExtraLetters(val: boolean, nosave?: boolean): boolean { } export function setTimerStyle( - style: ConfigTypes.TimerStyle, + style: ConfigSchemas.TimerStyle, nosave?: boolean ): boolean { if ( - !isConfigValueValid("timer style", style, [["off", "bar", "text", "mini"]]) + !isConfigValueValid("timer style", style, ConfigSchemas.TimerStyleSchema) ) { return false; } @@ -921,11 +973,15 @@ export function setTimerStyle( } export function setLiveSpeedStyle( - style: ConfigTypes.LiveSpeedAccBurstStyle, + style: ConfigSchemas.LiveSpeedAccBurstStyle, nosave?: boolean ): boolean { if ( - !isConfigValueValid("live speed style", style, [["off", "text", "mini"]]) + !isConfigValueValid( + "live speed style", + style, + ConfigSchemas.LiveSpeedAccBurstStyleSchema + ) ) { return false; } @@ -938,10 +994,16 @@ export function setLiveSpeedStyle( } export function setLiveAccStyle( - style: ConfigTypes.LiveSpeedAccBurstStyle, + style: ConfigSchemas.LiveSpeedAccBurstStyle, nosave?: boolean ): boolean { - if (!isConfigValueValid("live acc style", style, [["off", "text", "mini"]])) { + if ( + !isConfigValueValid( + "live acc style", + style, + ConfigSchemas.LiveSpeedAccBurstStyleSchema + ) + ) { return false; } @@ -953,11 +1015,15 @@ export function setLiveAccStyle( } export function setLiveBurstStyle( - style: ConfigTypes.LiveSpeedAccBurstStyle, + style: ConfigSchemas.LiveSpeedAccBurstStyle, nosave?: boolean ): boolean { if ( - !isConfigValueValid("live burst style", style, [["off", "text", "mini"]]) + !isConfigValueValid( + "live burst style", + style, + ConfigSchemas.LiveSpeedAccBurstStyleSchema + ) ) { return false; } @@ -970,13 +1036,11 @@ export function setLiveBurstStyle( } export function setTimerColor( - color: ConfigTypes.TimerColor, + color: ConfigSchemas.TimerColor, nosave?: boolean ): boolean { if ( - !isConfigValueValid("timer color", color, [ - ["black", "sub", "text", "main"], - ]) + !isConfigValueValid("timer color", color, ConfigSchemas.TimerColorSchema) ) { return false; } @@ -989,13 +1053,15 @@ export function setTimerColor( return true; } export function setTimerOpacity( - opacity: ConfigTypes.TimerOpacity, + opacity: ConfigSchemas.TimerOpacity, nosave?: boolean ): boolean { if ( - !isConfigValueValid("timer opacity", opacity, [ - ["0.25", "0.5", "0.75", "1"], - ]) + !isConfigValueValid( + "timer opacity", + opacity, + ConfigSchemas.TimerOpacitySchema + ) ) { return false; } @@ -1009,7 +1075,7 @@ export function setTimerOpacity( //key tips export function setKeyTips(keyTips: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("key tips", keyTips, ["boolean"])) return false; + if (!isConfigValueValidBoolean("key tips", keyTips)) return false; config.showKeyTips = keyTips; if (config.showKeyTips) { @@ -1024,48 +1090,56 @@ export function setKeyTips(keyTips: boolean, nosave?: boolean): boolean { } //mode -export function setTimeConfig(time: number, nosave?: boolean): boolean { - if (!isConfigValueValid("time", time, ["number"])) return false; +export function setTimeConfig( + time: ConfigSchemas.TimeConfig, + nosave?: boolean +): boolean { + time = isNaN(time) || time < 0 ? DefaultConfig.time : time; + if (!isConfigValueValid("time", time, ConfigSchemas.TimeConfigSchema)) + return false; if (!canSetConfigWithCurrentFunboxes("words", time, config.funbox)) { return false; } - const newTime = isNaN(time) || time < 0 ? DefaultConfig.time : time; - - config.time = newTime; - + config.time = time; saveToLocalStorage("time", nosave); ConfigEvent.dispatch("time", config.time); return true; } -//quote length export function setQuoteLength( - len: ConfigTypes.QuoteLength[] | ConfigTypes.QuoteLength, + len: ConfigSchemas.QuoteLength[] | ConfigSchemas.QuoteLength, nosave?: boolean, multipleMode?: boolean ): boolean { - if ( - !isConfigValueValid("quote length", len, [ - [-3, -2, -1, 0, 1, 2, 3], - "numberArray", - ]) - ) { - return false; - } - if (Array.isArray(len)) { + if ( + !isConfigValueValid( + "quote length", + len, + ConfigSchemas.QuoteLengthConfigSchema + ) + ) { + return false; + } + //config load if (len.length === 1 && len[0] === -1) len = [1]; config.quoteLength = len; } else { + if ( + !isConfigValueValid("quote length", len, ConfigSchemas.QuoteLengthSchema) + ) { + return false; + } + if (!Array.isArray(config.quoteLength)) config.quoteLength = []; if (len === null || isNaN(len) || len < -3 || len > 3) { len = 1; } - len = parseInt(len.toString()) as ConfigTypes.QuoteLength; + len = parseInt(len.toString()) as ConfigSchemas.QuoteLength; if (len === -1) { config.quoteLength = [0, 1, 2, 3]; @@ -1088,17 +1162,21 @@ export function setQuoteLength( return true; } -export function setWordCount(wordCount: number, nosave?: boolean): boolean { - if (!isConfigValueValid("words", wordCount, ["number"])) return false; +export function setWordCount( + wordCount: ConfigSchemas.WordCount, + nosave?: boolean +): boolean { + wordCount = + wordCount < 0 || wordCount > 100000 ? DefaultConfig.words : wordCount; + + if (!isConfigValueValid("words", wordCount, ConfigSchemas.WordCountSchema)) + return false; if (!canSetConfigWithCurrentFunboxes("words", wordCount, config.funbox)) { return false; } - const newWordCount = - wordCount < 0 || wordCount > 100000 ? DefaultConfig.words : wordCount; - - config.words = newWordCount; + config.words = wordCount; saveToLocalStorage("words", nosave); ConfigEvent.dispatch("words", config.words); @@ -1108,13 +1186,11 @@ export function setWordCount(wordCount: number, nosave?: boolean): boolean { //caret export function setSmoothCaret( - mode: ConfigTypes.Config["smoothCaret"], + mode: ConfigSchemas.SmoothCaret, nosave?: boolean ): boolean { if ( - !isConfigValueValid("smooth caret", mode, [ - ["off", "slow", "medium", "fast"], - ]) + !isConfigValueValid("smooth caret", mode, ConfigSchemas.SmoothCaretSchema) ) { return false; } @@ -1132,7 +1208,7 @@ export function setSmoothCaret( } export function setStartGraphsAtZero(mode: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("start graphs at zero", mode, ["boolean"])) { + if (!isConfigValueValidBoolean("start graphs at zero", mode)) { return false; } @@ -1145,7 +1221,7 @@ export function setStartGraphsAtZero(mode: boolean, nosave?: boolean): boolean { //linescroll export function setSmoothLineScroll(mode: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("smooth line scroll", mode, ["boolean"])) { + if (!isConfigValueValidBoolean("smooth line scroll", mode)) { return false; } @@ -1158,13 +1234,15 @@ export function setSmoothLineScroll(mode: boolean, nosave?: boolean): boolean { //quick restart export function setQuickRestartMode( - mode: "off" | "esc" | "tab" | "enter", + mode: ConfigSchemas.QuickRestart, nosave?: boolean ): boolean { if ( - !isConfigValueValid("quick restart mode", mode, [ - ["off", "esc", "tab", "enter"], - ]) + !isConfigValueValid( + "quick restart mode", + mode, + ConfigSchemas.QuickRestartSchema + ) ) { return false; } @@ -1177,8 +1255,12 @@ export function setQuickRestartMode( } //font family -export function setFontFamily(font: string, nosave?: boolean): boolean { - if (!isConfigValueValid("font family", font, ["string"])) return false; +export function setFontFamily( + font: ConfigSchemas.FontFamily, + nosave?: boolean +): boolean { + if (!isConfigValueValid("font family", font, ConfigSchemas.FontFamilySchema)) + return false; if (font === "") { font = "roboto_mono"; @@ -1210,7 +1292,7 @@ export function setFontFamily(font: string, nosave?: boolean): boolean { //freedom export function setFreedomMode(freedom: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("freedom mode", freedom, ["boolean"])) return false; + if (!isConfigValueValidBoolean("freedom mode", freedom)) return false; if (freedom === null || freedom === undefined) { freedom = false; @@ -1226,10 +1308,16 @@ export function setFreedomMode(freedom: boolean, nosave?: boolean): boolean { } export function setConfidenceMode( - cm: ConfigTypes.ConfidenceMode, + cm: ConfigSchemas.ConfidenceMode, nosave?: boolean ): boolean { - if (!isConfigValueValid("confidence mode", cm, [["off", "on", "max"]])) { + if ( + !isConfigValueValid( + "confidence mode", + cm, + ConfigSchemas.ConfidenceModeSchema + ) + ) { return false; } @@ -1247,11 +1335,15 @@ export function setConfidenceMode( } export function setIndicateTypos( - value: ConfigTypes.IndicateTypos, + value: ConfigSchemas.IndicateTypos, nosave?: boolean ): boolean { if ( - !isConfigValueValid("indicate typos", value, [["off", "below", "replace"]]) + !isConfigValueValid( + "indicate typos", + value, + ConfigSchemas.IndicateTyposSchema + ) ) { return false; } @@ -1267,7 +1359,7 @@ export function setAutoSwitchTheme( boolean: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValid("auto switch theme", boolean, ["boolean"])) { + if (!isConfigValueValidBoolean("auto switch theme", boolean)) { return false; } @@ -1280,7 +1372,7 @@ export function setAutoSwitchTheme( } export function setCustomTheme(boolean: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("custom theme", boolean, ["boolean"])) return false; + if (!isConfigValueValidBoolean("custom theme", boolean)) return false; config.customTheme = boolean; saveToLocalStorage("customTheme", nosave); @@ -1289,8 +1381,12 @@ export function setCustomTheme(boolean: boolean, nosave?: boolean): boolean { return true; } -export function setTheme(name: string, nosave?: boolean): boolean { - if (!isConfigValueValid("theme", name, ["string"])) return false; +export function setTheme( + name: ConfigSchemas.ThemeName, + nosave?: boolean +): boolean { + if (!isConfigValueValid("theme", name, ConfigSchemas.ThemeNameSchema)) + return false; config.theme = name; if (config.customTheme) setCustomTheme(false); @@ -1300,8 +1396,12 @@ export function setTheme(name: string, nosave?: boolean): boolean { return true; } -export function setThemeLight(name: string, nosave?: boolean): boolean { - if (!isConfigValueValid("theme light", name, ["string"])) return false; +export function setThemeLight( + name: ConfigSchemas.ThemeName, + nosave?: boolean +): boolean { + if (!isConfigValueValid("theme light", name, ConfigSchemas.ThemeNameSchema)) + return false; config.themeLight = name; saveToLocalStorage("themeLight", nosave); @@ -1310,8 +1410,12 @@ export function setThemeLight(name: string, nosave?: boolean): boolean { return true; } -export function setThemeDark(name: string, nosave?: boolean): boolean { - if (!isConfigValueValid("theme dark", name, ["string"])) return false; +export function setThemeDark( + name: ConfigSchemas.ThemeName, + nosave?: boolean +): boolean { + if (!isConfigValueValid("theme dark", name, ConfigSchemas.ThemeNameSchema)) + return false; config.themeDark = name; saveToLocalStorage("themeDark", nosave); @@ -1321,14 +1425,16 @@ export function setThemeDark(name: string, nosave?: boolean): boolean { } function setThemes( - theme: string, + theme: ConfigSchemas.ThemeName, customState: boolean, - customThemeColors: string[], + customThemeColors: ConfigSchemas.CustomThemeColors, autoSwitchTheme: boolean, nosave?: boolean ): boolean { - if (!isConfigValueValid("themes", theme, ["string"])) return false; + if (!isConfigValueValid("themes", theme, ConfigSchemas.ThemeNameSchema)) + return false; + //@ts-expect-error if (customThemeColors.length === 9) { //color missing if (customState) { @@ -1354,13 +1460,11 @@ function setThemes( } export function setRandomTheme( - val: ConfigTypes.RandomTheme, + val: ConfigSchemas.RandomTheme, nosave?: boolean ): boolean { if ( - !isConfigValueValid("random theme", val, [ - ["off", "on", "fav", "light", "dark", "custom"], - ]) + !isConfigValueValid("random theme", val, ConfigSchemas.RandomThemeSchema) ) { return false; } @@ -1386,7 +1490,7 @@ export function setRandomTheme( } export function setBritishEnglish(val: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("british english", val, ["boolean"])) return false; + if (!isConfigValueValidBoolean("british english", val)) return false; if (!val) { val = false; @@ -1399,7 +1503,7 @@ export function setBritishEnglish(val: boolean, nosave?: boolean): boolean { } export function setLazyMode(val: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("lazy mode", val, ["boolean"])) return false; + if (!isConfigValueValidBoolean("lazy mode", val)) return false; if (!val) { val = false; @@ -1412,13 +1516,11 @@ export function setLazyMode(val: boolean, nosave?: boolean): boolean { } export function setCustomThemeColors( - colors: string[], + colors: ConfigSchemas.CustomThemeColors, nosave?: boolean ): boolean { - if (!isConfigValueValid("custom theme colors", colors, ["stringArray"])) { - return false; - } - + // migrate existing configs missing sub alt color + // @ts-expect-error if (colors.length === 9) { //color missing Notifications.add( @@ -1431,6 +1533,16 @@ export function setCustomThemeColors( colors.splice(4, 0, "#000000"); } + if ( + !isConfigValueValid( + "custom theme colors", + colors, + ConfigSchemas.CustomThemeColorsSchema + ) + ) { + return false; + } + if (colors !== undefined) { config.customThemeColors = colors; // ThemeController.set("custom"); @@ -1442,8 +1554,12 @@ export function setCustomThemeColors( return true; } -export function setLanguage(language: string, nosave?: boolean): boolean { - if (!isConfigValueValid("language", language, ["string"])) return false; +export function setLanguage( + language: ConfigSchemas.Language, + nosave?: boolean +): boolean { + if (!isConfigValueValid("language", language, ConfigSchemas.LanguageSchema)) + return false; config.language = language; void AnalyticsController.log("changedLanguage", { language }); @@ -1454,7 +1570,7 @@ export function setLanguage(language: string, nosave?: boolean): boolean { } export function setMonkey(monkey: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("monkey", monkey, ["boolean"])) return false; + if (!isConfigValueValidBoolean("monkey", monkey)) return false; config.monkey = monkey; saveToLocalStorage("monkey", nosave); @@ -1464,13 +1580,11 @@ export function setMonkey(monkey: boolean, nosave?: boolean): boolean { } export function setKeymapMode( - mode: ConfigTypes.KeymapMode, + mode: ConfigSchemas.KeymapMode, nosave?: boolean ): boolean { if ( - !isConfigValueValid("keymap mode", mode, [ - ["off", "static", "react", "next"], - ]) + !isConfigValueValid("keymap mode", mode, ConfigSchemas.KeymapModeSchema) ) { return false; } @@ -1485,13 +1599,15 @@ export function setKeymapMode( } export function setKeymapLegendStyle( - style: ConfigTypes.KeymapLegendStyle, + style: ConfigSchemas.KeymapLegendStyle, nosave?: boolean ): boolean { if ( - !isConfigValueValid("keymap legend style", style, [ - ["lowercase", "uppercase", "blank", "dynamic"], - ]) + !isConfigValueValid( + "keymap legend style", + style, + ConfigSchemas.KeymapLegendStyleSchema + ) ) { return false; } @@ -1527,21 +1643,11 @@ export function setKeymapLegendStyle( } export function setKeymapStyle( - style: ConfigTypes.KeymapStyle, + style: ConfigSchemas.KeymapStyle, nosave?: boolean ): boolean { if ( - !isConfigValueValid("keymap style", style, [ - [ - "staggered", - "alice", - "matrix", - "split", - "split_matrix", - "steno", - "steno_matrix", - ], - ]) + !isConfigValueValid("keymap style", style, ConfigSchemas.KeymapStyleSchema) ) { return false; } @@ -1554,8 +1660,18 @@ export function setKeymapStyle( return true; } -export function setKeymapLayout(layout: string, nosave?: boolean): boolean { - if (!isConfigValueValid("keymap layout", layout, ["string"])) return false; +export function setKeymapLayout( + layout: ConfigSchemas.KeymapLayout, + nosave?: boolean +): boolean { + if ( + !isConfigValueValid( + "keymap layout", + layout, + ConfigSchemas.KeymapLayoutSchema + ) + ) + return false; config.keymapLayout = layout; saveToLocalStorage("keymapLayout", nosave); @@ -1565,13 +1681,15 @@ export function setKeymapLayout(layout: string, nosave?: boolean): boolean { } export function setKeymapShowTopRow( - show: ConfigTypes.KeymapShowTopRow, + show: ConfigSchemas.KeymapShowTopRow, nosave?: boolean ): boolean { if ( - !isConfigValueValid("keymapShowTopRow", show, [ - ["always", "layout", "never"], - ]) + !isConfigValueValid( + "keymapShowTopRow", + show, + ConfigSchemas.KeymapShowTopRowSchema + ) ) { return false; } @@ -1583,8 +1701,12 @@ export function setKeymapShowTopRow( return true; } -export function setLayout(layout: string, nosave?: boolean): boolean { - if (!isConfigValueValid("layout", layout, ["string"])) return false; +export function setLayout( + layout: ConfigSchemas.Layout, + nosave?: boolean +): boolean { + if (!isConfigValueValid("layout", layout, ConfigSchemas.LayoutSchema)) + return false; config.layout = layout; saveToLocalStorage("layout", nosave); @@ -1603,7 +1725,13 @@ export function setLayout(layout: string, nosave?: boolean): boolean { // return true; // } -export function setFontSize(fontSize: number, nosave?: boolean): boolean { +export function setFontSize( + fontSize: ConfigSchemas.FontSize, + nosave?: boolean +): boolean { + if (fontSize < 0) { + fontSize = 1; + } if ( typeof fontSize === "string" && ["1", "125", "15", "2", "3", "4"].includes(fontSize) @@ -1617,14 +1745,12 @@ export function setFontSize(fontSize: number, nosave?: boolean): boolean { } } - if (!isConfigValueValid("font size", fontSize, ["number"])) { + if ( + !isConfigValueValid("font size", fontSize, ConfigSchemas.FontSizeSchema) + ) { return false; } - if (fontSize < 0) { - fontSize = 1; - } - // i dont know why the above check is not enough // some people are getting font size 15 when it should be converted to 1.5 // after converting from the string to float system @@ -1651,13 +1777,9 @@ export function setFontSize(fontSize: number, nosave?: boolean): boolean { } export function setMaxLineWidth( - maxLineWidth: number, + maxLineWidth: ConfigSchemas.MaxLineWidth, nosave?: boolean ): boolean { - if (!isConfigValueValid("max line width", maxLineWidth, ["number"])) { - return false; - } - if (maxLineWidth < 20 && maxLineWidth !== 0) { maxLineWidth = 20; } @@ -1665,6 +1787,16 @@ export function setMaxLineWidth( maxLineWidth = 1000; } + if ( + !isConfigValueValid( + "max line width", + maxLineWidth, + ConfigSchemas.MaxLineWidthSchema + ) + ) { + return false; + } + config.maxLineWidth = maxLineWidth; saveToLocalStorage("maxLineWidth", nosave); @@ -1676,23 +1808,23 @@ export function setMaxLineWidth( return true; } -export function setCustomBackground(value: string, nosave?: boolean): boolean { - if (!isConfigValueValid("custom background", value, ["string"])) return false; - +export function setCustomBackground( + value: ConfigSchemas.CustomBackground, + nosave?: boolean +): boolean { value = value.trim(); if ( - (/(https|http):\/\/(www\.|).+\..+\/.+(\.png|\.gif|\.jpeg|\.jpg)/gi.test( - value - ) && - !/[<> "]/.test(value)) || - value === "" - ) { - config.customBackground = value; - saveToLocalStorage("customBackground", nosave); - ConfigEvent.dispatch("customBackground", config.customBackground); - } else { - Notifications.add("Invalid custom background URL", 0); - } + !isConfigValueValid( + "custom background", + value, + ConfigSchemas.CustomBackgroundSchema + ) + ) + return false; + + config.customBackground = value; + saveToLocalStorage("customBackground", nosave); + ConfigEvent.dispatch("customBackground", config.customBackground); return true; } @@ -1712,7 +1844,7 @@ export async function setCustomLayoutfluid( const customLayoutfluid = trimmed.replace( / /g, "#" - ) as ConfigTypes.CustomLayoutFluid; + ) as ConfigSchemas.CustomLayoutFluid; config.customLayoutfluid = customLayoutfluid; saveToLocalStorage("customLayoutfluid", nosave); @@ -1722,13 +1854,15 @@ export async function setCustomLayoutfluid( } export function setCustomBackgroundSize( - value: ConfigTypes.CustomBackgroundSize, + value: ConfigSchemas.CustomBackgroundSize, nosave?: boolean ): boolean { if ( - !isConfigValueValid("custom background size", value, [ - ["max", "cover", "contain"], - ]) + !isConfigValueValid( + "custom background size", + value, + ConfigSchemas.CustomBackgroundSizeSchema + ) ) { return false; } @@ -1741,10 +1875,22 @@ export function setCustomBackgroundSize( } export function setCustomBackgroundFilter( - array: ConfigTypes.CustomBackgroundFilter, + array: ConfigSchemas.CustomBackgroundFilter, nosave?: boolean ): boolean { - if (!isConfigValueValid("custom background filter", array, ["numberArray"])) { + //convert existing configs using five values down to four + //@ts-expect-error + if (array.length === 5) { + array = [array[0], array[1], array[2], array[3]]; + } + + if ( + !isConfigValueValid( + "custom background filter", + array, + ConfigSchemas.CustomBackgroundFilterSchema + ) + ) { return false; } @@ -1754,20 +1900,19 @@ export function setCustomBackgroundFilter( return true; } - export function setMonkeyPowerLevel( - level: ConfigTypes.MonkeyPowerLevel, + level: ConfigSchemas.MonkeyPowerLevel, nosave?: boolean ): boolean { if ( - !isConfigValueValid("monkey power level", level, [ - ["off", "1", "2", "3", "4"], - ]) + !isConfigValueValid( + "monkey power level", + level, + ConfigSchemas.MonkeyPowerLevelSchema + ) ) { return false; } - - if (!["off", "1", "2", "3", "4"].includes(level)) level = "off"; config.monkeyPowerLevel = level; saveToLocalStorage("monkeyPowerLevel", nosave); ConfigEvent.dispatch("monkeyPowerLevel", config.monkeyPowerLevel); @@ -1776,7 +1921,7 @@ export function setMonkeyPowerLevel( } export function setBurstHeatmap(value: boolean, nosave?: boolean): boolean { - if (!isConfigValueValid("burst heatmap", value, ["boolean"])) return false; + if (!isConfigValueValidBoolean("burst heatmap", value)) return false; if (!value) { value = false; @@ -1789,7 +1934,7 @@ export function setBurstHeatmap(value: boolean, nosave?: boolean): boolean { } export async function apply( - configToApply: ConfigTypes.Config | MonkeyTypes.ConfigChanges + configToApply: Config | MonkeyTypes.ConfigChanges ): Promise { if (configToApply === undefined) return; @@ -1797,15 +1942,13 @@ export async function apply( configToApply = replaceLegacyValues(configToApply); - const configObj = configToApply as ConfigTypes.Config; - (Object.keys(DefaultConfig) as (keyof ConfigTypes.Config)[]).forEach( - (configKey) => { - if (configObj[configKey] === undefined) { - const newValue = DefaultConfig[configKey]; - (configObj[configKey] as typeof newValue) = newValue; - } + const configObj = configToApply as Config; + (Object.keys(DefaultConfig) as (keyof Config)[]).forEach((configKey) => { + if (configObj[configKey] === undefined) { + const newValue = DefaultConfig[configKey]; + (configObj[configKey] as typeof newValue) = newValue; } - ); + }); if (configObj !== undefined && configObj !== null) { setAds(configObj.ads, true); setThemeLight(configObj.themeLight, true); @@ -1908,13 +2051,14 @@ export async function apply( export async function reset(): Promise { await apply(DefaultConfig); - saveFullConfigToLocalStorage(); + await DB.resetConfig(); + saveFullConfigToLocalStorage(true); } export async function loadFromLocalStorage(): Promise { console.log("loading localStorage config"); const newConfigString = window.localStorage.getItem("config"); - let newConfig: ConfigTypes.Config; + let newConfig: Config; if ( newConfigString !== undefined && newConfigString !== null && @@ -1923,7 +2067,7 @@ export async function loadFromLocalStorage(): Promise { try { newConfig = JSON.parse(newConfigString); } catch (e) { - newConfig = {} as ConfigTypes.Config; + newConfig = {} as Config; } await apply(newConfig); localStorageConfig = newConfig; @@ -1936,9 +2080,9 @@ export async function loadFromLocalStorage(): Promise { } function replaceLegacyValues( - configToApply: ConfigTypes.Config | MonkeyTypes.ConfigChanges -): ConfigTypes.Config | MonkeyTypes.ConfigChanges { - const configObj = configToApply as ConfigTypes.Config; + configToApply: ConfigSchemas.PartialConfig | MonkeyTypes.ConfigChanges +): ConfigSchemas.Config | MonkeyTypes.ConfigChanges { + const configObj = configToApply as ConfigSchemas.Config; //@ts-expect-error if (configObj.quickTab === true) { @@ -1975,7 +2119,7 @@ function replaceLegacyValues( //@ts-expect-error if (configObj.showLiveWpm === true) { - let val: ConfigTypes.LiveSpeedAccBurstStyle = "mini"; + let val: ConfigSchemas.LiveSpeedAccBurstStyle = "mini"; if (configObj.timerStyle !== "bar" && configObj.timerStyle !== "off") { val = configObj.timerStyle; } @@ -1984,7 +2128,7 @@ function replaceLegacyValues( //@ts-expect-error if (configObj.showLiveBurst === true) { - let val: ConfigTypes.LiveSpeedAccBurstStyle = "mini"; + let val: ConfigSchemas.LiveSpeedAccBurstStyle = "mini"; if (configObj.timerStyle !== "bar" && configObj.timerStyle !== "off") { val = configObj.timerStyle; } @@ -1993,7 +2137,7 @@ function replaceLegacyValues( //@ts-expect-error if (configObj.showLiveAcc === true) { - let val: ConfigTypes.LiveSpeedAccBurstStyle = "mini"; + let val: ConfigSchemas.LiveSpeedAccBurstStyle = "mini"; if (configObj.timerStyle !== "bar" && configObj.timerStyle !== "off") { val = configObj.timerStyle; } @@ -2005,7 +2149,7 @@ function replaceLegacyValues( export function getConfigChanges(): MonkeyTypes.PresetConfig { const configChanges = {} as MonkeyTypes.PresetConfig; - (Object.keys(config) as (keyof ConfigTypes.Config)[]) + (Object.keys(config) as (keyof Config)[]) .filter((key) => { return config[key] !== DefaultConfig[key]; }) diff --git a/frontend/src/ts/constants/default-config.ts b/frontend/src/ts/constants/default-config.ts index 7ff9404f9..6d280542d 100644 --- a/frontend/src/ts/constants/default-config.ts +++ b/frontend/src/ts/constants/default-config.ts @@ -1,4 +1,7 @@ -import { Config } from "@monkeytype/shared-types/config"; +import { + Config, + CustomThemeColors, +} from "@monkeytype/contracts/schemas/configs"; export default { theme: "serika_dark", @@ -17,7 +20,7 @@ export default { "#7e2a33", "#ca4754", "#7e2a33", - ], + ] as CustomThemeColors, favThemes: [], showKeyTips: true, smoothCaret: "medium", @@ -85,7 +88,7 @@ export default { oppositeShiftMode: "off", customBackground: "", customBackgroundSize: "cover", - customBackgroundFilter: [0, 1, 1, 1, 1], + customBackgroundFilter: [0, 1, 1, 1], customLayoutfluid: "qwerty#dvorak#colemak", monkeyPowerLevel: "off", minBurst: "off", diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index f57a1f034..848d5c5c5 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -85,7 +85,7 @@ export async function initSnapshot(): Promise< if (configResponse.status !== 200) { // eslint-disable-next-line @typescript-eslint/no-throw-literal throw { - message: `${configResponse.message} (config)`, + message: `${configResponse.body.message} (config)`, responseCode: configResponse.status, }; } @@ -98,7 +98,7 @@ export async function initSnapshot(): Promise< } const userData = userResponse.data; - const configData = configResponse.data; + const configData = configResponse.body.data; const presetsData = presetsResponse.data; if (userData === null) { @@ -109,7 +109,7 @@ export async function initSnapshot(): Promise< }; } - if (configData !== null && !("config" in configData)) { + if (configData !== null && "config" in configData) { throw new Error( "Config data is not in the correct format. Please refresh the page or contact support." ); @@ -179,7 +179,7 @@ export async function initSnapshot(): Promise< ...DefaultConfig, }; } else { - snap.config = mergeWithDefaultConfig(configData.config); + snap.config = mergeWithDefaultConfig(configData); } // if (ActivePage.get() === "loading") { // LoadingPage.updateBar(67.5); @@ -894,9 +894,18 @@ export async function updateLbMemory( export async function saveConfig(config: Config): Promise { if (isAuthenticated()) { - const response = await Ape.configs.save(config); + const response = await Ape.configs.save({ body: config }); if (response.status !== 200) { - Notifications.add("Failed to save config: " + response.message, -1); + Notifications.add("Failed to save config: " + response.body.message, -1); + } + } +} + +export async function resetConfig(): Promise { + if (isAuthenticated()) { + const response = await Ape.configs.delete(); + if (response.status !== 200) { + Notifications.add("Failed to reset config: " + response.body.message, -1); } } } diff --git a/frontend/src/ts/elements/account/pb-tables.ts b/frontend/src/ts/elements/account/pb-tables.ts index 9db2321ea..869d3dd5f 100644 --- a/frontend/src/ts/elements/account/pb-tables.ts +++ b/frontend/src/ts/elements/account/pb-tables.ts @@ -135,7 +135,7 @@ function buildPbHtml( if (pbData === undefined) throw new Error("No PB data found"); const date = new Date(pbData.timestamp); - if (pbData.timestamp) { + if (pbData.timestamp !== undefined && pbData.timestamp > 0) { dateText = dateFormat(date, "dd MMM yyyy"); } diff --git a/frontend/src/ts/elements/settings/theme-picker.ts b/frontend/src/ts/elements/settings/theme-picker.ts index 1cc0bfcae..451dd96eb 100644 --- a/frontend/src/ts/elements/settings/theme-picker.ts +++ b/frontend/src/ts/elements/settings/theme-picker.ts @@ -11,6 +11,7 @@ import * as DB from "../../db"; import * as ConfigEvent from "../../observables/config-event"; import { isAuthenticated } from "../../firebase"; import * as ActivePage from "../../states/active-page"; +import { CustomThemeColors } from "@monkeytype/contracts/schemas/configs"; function updateActiveButton(): void { let activeThemeName = Config.theme; @@ -281,7 +282,7 @@ function saveCustomThemeColors(): void { ).attr("value") as string ); } - UpdateConfig.setCustomThemeColors(newColors); + UpdateConfig.setCustomThemeColors(newColors as CustomThemeColors); Notifications.add("Custom theme saved", 1); } diff --git a/frontend/src/ts/modals/simple-modals.ts b/frontend/src/ts/modals/simple-modals.ts index 7f9bbf9af..0a6172ac1 100644 --- a/frontend/src/ts/modals/simple-modals.ts +++ b/frontend/src/ts/modals/simple-modals.ts @@ -33,6 +33,7 @@ import AnimatedModal, { } from "../utils/animated-modal"; import { format as dateFormat } from "date-fns/format"; import { Attributes, buildTag } from "../utils/tag-builder"; +import { CustomThemeColors } from "@monkeytype/contracts/schemas/configs"; type CommonInput = { type: TType; @@ -1705,7 +1706,7 @@ list.updateCustomTheme = new SimpleModal({ const newTheme = { name: name.replaceAll(" ", "_"), - colors: newColors, + colors: newColors as CustomThemeColors, }; const validation = await DB.editCustomTheme(customTheme._id, newTheme); if (!validation) { @@ -1714,7 +1715,7 @@ list.updateCustomTheme = new SimpleModal({ message: "Failed to update custom theme", }; } - UpdateConfig.setCustomThemeColors(newColors); + UpdateConfig.setCustomThemeColors(newColors as CustomThemeColors); void ThemePicker.refreshButtons(); return { diff --git a/frontend/src/ts/types/types.d.ts b/frontend/src/ts/types/types.d.ts index f28aae89f..7d736fc3a 100644 --- a/frontend/src/ts/types/types.d.ts +++ b/frontend/src/ts/types/types.d.ts @@ -182,7 +182,7 @@ declare namespace MonkeyTypes { type RawCustomTheme = { name: string; - colors: string[]; + colors: import("@monkeytype/contracts/schemas/configs").CustomThemeColors; }; type CustomTheme = { diff --git a/frontend/src/ts/utils/config.ts b/frontend/src/ts/utils/config.ts index 60df40e55..157e7fa83 100644 --- a/frontend/src/ts/utils/config.ts +++ b/frontend/src/ts/utils/config.ts @@ -1,8 +1,9 @@ import { Config, ConfigValue } from "@monkeytype/shared-types/config"; import DefaultConfig from "../constants/default-config"; import { typedKeys } from "./misc"; +import { PartialConfig } from "@monkeytype/contracts/schemas/configs"; -export function mergeWithDefaultConfig(config: Partial): Config { +export function mergeWithDefaultConfig(config: PartialConfig): Config { const mergedConfig = {} as Config; for (const key of typedKeys(DefaultConfig)) { const newValue = config[key] ?? (DefaultConfig[key] as ConfigValue); diff --git a/monkeytype.code-workspace b/monkeytype.code-workspace index e5b2b5fc1..0621e1fb0 100644 --- a/monkeytype.code-workspace +++ b/monkeytype.code-workspace @@ -8,6 +8,10 @@ "name": "frontend", "path": "frontend" }, + { + "name": "contracts", + "path": "packages/contracts" + }, { "name": "packages", "path": "packages" @@ -21,7 +25,8 @@ "files.exclude": { "frontend": true, "backend": true, - "packages": true + "packages": true, + "contracts": true }, "search.exclude": { //defaults diff --git a/package-lock.json b/package-lock.json index 467378eaf..f68d79016 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,9 @@ "license": "GPL-3.0", "dependencies": { "@date-fns/utc": "1.2.0", + "@monkeytype/contracts": "*", + "@ts-rest/express": "3.45.2", + "@ts-rest/open-api": "3.45.2", "bcrypt": "5.1.1", "bullmq": "1.91.1", "chalk": "4.1.2", @@ -84,6 +87,7 @@ "@monkeytype/eslint-config": "*", "@monkeytype/shared-types": "*", "@monkeytype/typescript-config": "*", + "@redocly/cli": "1.18.1", "@types/bcrypt": "5.0.0", "@types/cors": "2.8.12", "@types/cron": "1.7.3", @@ -137,6 +141,7 @@ "license": "GPL-3.0", "dependencies": { "@date-fns/utc": "1.2.0", + "@monkeytype/contracts": "*", "axios": "1.6.4", "canvas-confetti": "1.5.1", "chart.js": "3.7.1", @@ -230,6 +235,18 @@ "node": ">=6.0.0" } }, + "node_modules/@anatine/zod-openapi": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@anatine/zod-openapi/-/zod-openapi-1.14.2.tgz", + "integrity": "sha512-q0qHfnuNYVKu0Swrnnvfj9971AEyW7c8v9jCOZGCl5ZbyGMNG4RPyJkRcMi/JC8CRfdOe0IDfNm1nNsi2avprg==", + "dependencies": { + "ts-deepmerge": "^6.0.3" + }, + "peerDependencies": { + "openapi3-ts": "^2.0.0 || ^3.0.0", + "zod": "^3.20.0" + } + }, "node_modules/@antfu/utils": { "version": "0.7.10", "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", @@ -2132,6 +2149,24 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@cfaester/enzyme-adapter-react-18": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cfaester/enzyme-adapter-react-18/-/enzyme-adapter-react-18-0.8.0.tgz", + "integrity": "sha512-3Z3ThTUouHwz8oIyhTYQljEMNRFtlVyc3VOOHCbxs47U6cnXs8K9ygi/c1tv49s7MBlTXeIcuN+Ttd9aPtILFQ==", + "dev": true, + "dependencies": { + "enzyme-shallow-equal": "^1.0.0", + "function.prototype.name": "^1.1.6", + "has": "^1.0.4", + "react-is": "^18.2.0", + "react-shallow-renderer": "^16.15.0" + }, + "peerDependencies": { + "enzyme": "^3.11.0", + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2458,6 +2493,27 @@ "node": ">=12" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dev": true, + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "dev": true + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "dev": true + }, "node_modules/@ericcornelissen/bash-parser": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@ericcornelissen/bash-parser/-/bash-parser-0.5.3.tgz", @@ -2783,6 +2839,22 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -2963,6 +3035,12 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "dev": true + }, "node_modules/@fastify/ajv-compiler": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz", @@ -4509,6 +4587,10 @@ "resolved": "backend", "link": true }, + "node_modules/@monkeytype/contracts": { + "resolved": "packages/contracts", + "link": true + }, "node_modules/@monkeytype/eslint-config": { "resolved": "packages/eslint-config", "link": true @@ -5059,6 +5141,238 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@redocly/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-9GWx27t7xWhDIR02PA18nzBdLcKQRgc46xNQvjFkrYk4UOmvKhJ/dawwiX0cCOeetN5LcaaiqQbVOWYK62SGHw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/cli": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-1.18.1.tgz", + "integrity": "sha512-+bRKj46R9wvTzMdnoYfMueJ9/ek0NprEsQNowV7XcHgOXifeFFikRtBFcpkwqCNxaQ/nWAJn4LHZaFcssbcHow==", + "dev": true, + "dependencies": { + "@redocly/openapi-core": "1.18.1", + "abort-controller": "^3.0.0", + "chokidar": "^3.5.1", + "colorette": "^1.2.0", + "core-js": "^3.32.1", + "form-data": "^4.0.0", + "get-port-please": "^3.0.1", + "glob": "^7.1.6", + "handlebars": "^4.7.6", + "mobx": "^6.0.4", + "node-fetch": "^2.6.1", + "pluralize": "^8.0.0", + "react": "^17.0.0 || ^18.2.0", + "react-dom": "^17.0.0 || ^18.2.0", + "redoc": "~2.1.5", + "semver": "^7.5.2", + "simple-websocket": "^9.0.0", + "styled-components": "^6.0.7", + "yargs": "17.0.1" + }, + "bin": { + "openapi": "bin/cli.js", + "redocly": "bin/cli.js" + }, + "engines": { + "node": ">=14.19.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@redocly/cli/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@redocly/cli/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/@redocly/cli/node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "node_modules/@redocly/cli/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@redocly/cli/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@redocly/cli/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@redocly/cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@redocly/cli/node_modules/yargs": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz", + "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@redocly/config": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.7.0.tgz", + "integrity": "sha512-6GKxTo/9df0654Mtivvr4lQnMOp+pRj9neVywmI5+BwfZLTtkJnj2qB3D6d8FHTr4apsNOf6zTa5FojX0Evh4g==", + "dev": true + }, + "node_modules/@redocly/openapi-core": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.18.1.tgz", + "integrity": "sha512-y2ZR3aaVF80XRVoFP0Dp2z5DeCOilPTuS7V4HnHIYZdBTfsqzjkO169h5JqAaifnaLsLBhe3YArdgLb7W7wW6Q==", + "dev": true, + "dependencies": { + "@redocly/ajv": "^8.11.0", + "@redocly/config": "^0.7.0", + "colorette": "^1.2.0", + "https-proxy-agent": "^7.0.4", + "js-levenshtein": "^1.1.6", + "js-yaml": "^4.1.0", + "lodash.isequal": "^4.5.0", + "minimatch": "^5.0.1", + "node-fetch": "^2.6.1", + "pluralize": "^8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=14.19.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@redocly/openapi-core/node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true + }, + "node_modules/@redocly/openapi-core/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@release-it/conventional-changelog": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@release-it/conventional-changelog/-/conventional-changelog-7.0.0.tgz", @@ -5514,6 +5828,61 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "dev": true }, + "node_modules/@ts-rest/core": { + "version": "3.45.2", + "resolved": "https://registry.npmjs.org/@ts-rest/core/-/core-3.45.2.tgz", + "integrity": "sha512-Eiv+Sa23MbsAd1Gx9vNJ+IFCDyLZNdJ+UuGMKbFvb+/NmgcBR1VL1UIVtEkd5DJxpYMMd8SLvW91RgB2TS8iPw==", + "peerDependencies": { + "zod": "^3.22.3" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@ts-rest/express": { + "version": "3.45.2", + "resolved": "https://registry.npmjs.org/@ts-rest/express/-/express-3.45.2.tgz", + "integrity": "sha512-GypL5auYuh3WS2xCc58J9OpgpYYJoPY2NvBxJxHOLdhMOoGHhYI8oIUtxfxIxWZcPJOAokrrQJ2zb2hrPUuR1A==", + "peerDependencies": { + "express": "^4.0.0", + "zod": "^3.22.3" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@ts-rest/open-api": { + "version": "3.45.2", + "resolved": "https://registry.npmjs.org/@ts-rest/open-api/-/open-api-3.45.2.tgz", + "integrity": "sha512-TG5GvD0eyicMR3HjEAMTO2ulO7x/ADSn5bfLF/ZDdVXla48PiQdwhG4NsjVbsUDCghwx+6y5u2aTYjypzdiXQg==", + "dependencies": { + "@anatine/zod-openapi": "^1.12.0", + "openapi3-ts": "^2.0.2" + }, + "peerDependencies": { + "zod": "^3.22.3" + } + }, + "node_modules/@ts-rest/open-api/node_modules/openapi3-ts": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz", + "integrity": "sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==", + "dependencies": { + "yaml": "^1.10.2" + } + }, + "node_modules/@ts-rest/open-api/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -5865,6 +6234,12 @@ "integrity": "sha512-dMS4S07fbtY1AILG/RhuwmptmzK1Ql8scmAebOTJ/8iBtK/KI17NwGwKzu1uipjj8Kk+3mfPxum56kKZE93mzQ==", "dev": true }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "dev": true + }, "node_modules/@types/subset-font": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@types/subset-font/-/subset-font-1.4.3.tgz", @@ -6952,6 +7327,27 @@ "node": ">=0.10.0" } }, + "node_modules/array.prototype.filter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.4.tgz", + "integrity": "sha512-r+mCJ7zXgXElgR4IRC+fkvNCeoaavWBs6EdCso5Tbcf+iEMKzBU/His60lt34WEZ9vlb8wDkZvQGcVI5GwkfoQ==", + "dev": true, + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-array-method-boxes-properly": "^1.0.0", + "es-object-atoms": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.findlastindex": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", @@ -8198,6 +8594,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001643", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", @@ -8462,6 +8867,12 @@ "node": ">= 0.4" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "dev": true + }, "node_modules/clean-css": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", @@ -8720,6 +9131,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -10164,6 +10584,17 @@ "is-plain-object": "^5.0.0" } }, + "node_modules/core-js": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.37.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", @@ -10442,6 +10873,15 @@ "node": ">=8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/css-line-break": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", @@ -10465,6 +10905,17 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dev": true, + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -10476,6 +10927,12 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, "node_modules/csv-parse": { "version": "5.5.6", "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.6.tgz", @@ -10637,6 +11094,12 @@ "node": ">=0.10.0" } }, + "node_modules/decko": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decko/-/decko-1.2.0.tgz", + "integrity": "sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==", + "dev": true + }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -11455,6 +11918,12 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==", + "dev": true + }, "node_modules/domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", @@ -11694,6 +12163,53 @@ "node": ">=6" } }, + "node_modules/enzyme": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", + "integrity": "sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==", + "dev": true, + "peer": true, + "dependencies": { + "array.prototype.flat": "^1.2.3", + "cheerio": "^1.0.0-rc.3", + "enzyme-shallow-equal": "^1.0.1", + "function.prototype.name": "^1.1.2", + "has": "^1.0.3", + "html-element-map": "^1.2.0", + "is-boolean-object": "^1.0.1", + "is-callable": "^1.1.5", + "is-number-object": "^1.0.4", + "is-regex": "^1.0.5", + "is-string": "^1.0.5", + "is-subset": "^0.1.1", + "lodash.escape": "^4.0.1", + "lodash.isequal": "^4.5.0", + "object-inspect": "^1.7.0", + "object-is": "^1.0.2", + "object.assign": "^4.1.0", + "object.entries": "^1.1.1", + "object.values": "^1.1.1", + "raf": "^3.4.1", + "rst-selector-parser": "^2.2.3", + "string.prototype.trim": "^1.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/enzyme-shallow-equal": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.7.tgz", + "integrity": "sha512-/um0GFqUXnpM9SvKtje+9Tjoz3f1fpBC3eXRFrNs8kpYn69JljciYP7KZTqM/YQbUY9KUjvKB4jo/q+L6WGGvg==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0", + "object-is": "^1.1.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/err-code": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", @@ -11903,6 +12419,12 @@ "es6-symbol": "^3.1.1" } }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, "node_modules/es6-symbol": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", @@ -12488,6 +13010,12 @@ "node": ">=6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -14083,6 +14611,12 @@ "node": ">=0.10.0" } }, + "node_modules/foreach": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", + "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", + "dev": true + }, "node_modules/foreground-child": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", @@ -14745,6 +15279,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-port-please": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", + "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==", + "dev": true + }, "node_modules/get-stdin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", @@ -16648,6 +17188,15 @@ "integrity": "sha512-dzf7y6NS8fiAIvPAL/VKwY8wx2HCzUB0vUfOo6h1J5UilFEEf7iYqFsvgwjHwvM3whbjfOMadNvQekU3KuRnWQ==", "dev": true }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -16885,6 +17434,20 @@ "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==" }, + "node_modules/html-element-map": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.1.tgz", + "integrity": "sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg==", + "dev": true, + "peer": true, + "dependencies": { + "array.prototype.filter": "^1.0.0", + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/html-entities": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", @@ -17021,6 +17584,12 @@ "node": ">= 14" } }, + "node_modules/http2-client": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "dev": true + }, "node_modules/http2-wrapper": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", @@ -18010,6 +18579,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==", + "dev": true, + "peer": true + }, "node_modules/is-symbol": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", @@ -18530,6 +19106,15 @@ "node": ">=14" } }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -18601,6 +19186,15 @@ "jju": "^1.1.0" } }, + "node_modules/json-pointer": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", + "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", + "dev": true, + "dependencies": { + "foreach": "^2.0.4" + } + }, "node_modules/json-ptr": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.1.1.tgz", @@ -19515,6 +20109,13 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, + "node_modules/lodash.escape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", + "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==", + "dev": true, + "peer": true + }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", @@ -19526,6 +20127,13 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true, + "peer": true + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -19819,6 +20427,12 @@ "node": ">=10" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "node_modules/luxon": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", @@ -20048,6 +20662,12 @@ "node": ">=0.10.0" } }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true + }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -21267,6 +21887,66 @@ "ufo": "^1.5.3" } }, + "node_modules/mobx": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.1.tgz", + "integrity": "sha512-ekLRxgjWJr8hVxj9ZKuClPwM/iHckx3euIJ3Np7zLVNtqJvfbbq7l370W/98C8EabdQ1pB5Jd3BbDWxJPNnaOg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + } + }, + "node_modules/mobx-react": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-9.1.1.tgz", + "integrity": "sha512-gVV7AdSrAAxqXOJ2bAbGa5TkPqvITSzaPiiEkzpW4rRsMhSec7C2NBCJYILADHKp2tzOAIETGRsIY0UaCV5aEw==", + "dev": true, + "dependencies": { + "mobx-react-lite": "^4.0.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/mobx-react-lite": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.0.7.tgz", + "integrity": "sha512-RjwdseshK9Mg8On5tyJZHtGD+J78ZnCnRaxeQDSiciKVQDUbfZcXhmld0VMxAwvcTnPEHZySGGewm467Fcpreg==", + "dev": true, + "dependencies": { + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/mocha": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.0.tgz", @@ -22083,6 +22763,18 @@ } } }, + "node_modules/node-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "dev": true, + "dependencies": { + "http2-client": "^1.2.5" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -22238,6 +22930,15 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", + "dev": true, + "dependencies": { + "es6-promise": "^3.2.1" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -22467,6 +23168,103 @@ "node": ">=0.10.0" } }, + "node_modules/oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "dev": true, + "dependencies": { + "fast-safe-stringify": "^2.0.7" + } + }, + "node_modules/oas-linter": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", + "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", + "dev": true, + "dependencies": { + "@exodus/schemasafe": "^1.0.0-rc.2", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-linter/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/oas-resolver": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", + "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", + "dev": true, + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "resolve": "resolve.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-resolver/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/oas-schema-walker": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", + "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", + "dev": true, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", + "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", + "dev": true, + "dependencies": { + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.2.2", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "reftools": "^1.1.9", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -22545,6 +23343,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -22614,6 +23428,21 @@ "node": ">=0.10.0" } }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "peer": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/object.fromentries": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", @@ -22765,11 +23594,20 @@ "node": ">=8" } }, + "node_modules/openapi-sampler": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.5.1.tgz", + "integrity": "sha512-tIWIrZUKNAsbqf3bd9U1oH6JEXo8LNYuDlXw26By67EygpjT+ArFnsxxyTMjFWRfbqo5ozkvgSQDK69Gd8CddA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.7", + "json-pointer": "0.6.2" + } + }, "node_modules/openapi3-ts": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.2.0.tgz", "integrity": "sha512-/ykNWRV5Qs0Nwq7Pc0nJ78fgILvOT/60OxEmB3v7yQ8a8Bwcm43D4diaYazG/KBn6czA+52XYy931WFLMCUeSg==", - "dev": true, "dependencies": { "yaml": "^2.2.1" } @@ -23233,6 +24071,12 @@ "util": "^0.10.3" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, "node_modules/path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", @@ -23352,6 +24196,19 @@ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "dev": true }, + "node_modules/perfect-scrollbar": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-1.5.5.tgz", + "integrity": "sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "peer": true + }, "node_modules/pg": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", @@ -23629,6 +24486,18 @@ "node": ">=4" } }, + "node_modules/polished": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.17.8" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -24135,6 +25004,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/proc-log": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", @@ -24235,6 +25113,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -24517,6 +25412,16 @@ "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==", "dev": true }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dev": true, + "peer": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/railroad-diagrams": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", @@ -24647,6 +25552,32 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, + "node_modules/react-shallow-renderer": { + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", + "integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-tabs": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.0.2.tgz", + "integrity": "sha512-aQXTKolnM28k3KguGDBSAbJvcowOQr23A+CUJdzJtOSDOtTwzEaJA+1U4KwhNL9+Obe+jFS7geuvA7ICQPXOnQ==", + "dev": true, + "dependencies": { + "clsx": "^2.0.0", + "prop-types": "^15.5.0" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/read-package-json-fast": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", @@ -24923,6 +25854,56 @@ "node": ">=4" } }, + "node_modules/redoc": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.1.5.tgz", + "integrity": "sha512-POSbVg+7WLf+/5/c6GWLxL7+9t2D+1WlZdLN0a6qaCQc+ih3XYzteRBkXEN5kjrYrRNjdspfxTZkDLN5WV3Tzg==", + "dev": true, + "dependencies": { + "@cfaester/enzyme-adapter-react-18": "^0.8.0", + "@redocly/openapi-core": "^1.4.0", + "classnames": "^2.3.2", + "decko": "^1.2.0", + "dompurify": "^3.0.6", + "eventemitter3": "^5.0.1", + "json-pointer": "^0.6.2", + "lunr": "^2.3.9", + "mark.js": "^8.11.1", + "marked": "^4.3.0", + "mobx-react": "^9.1.1", + "openapi-sampler": "^1.5.0", + "path-browserify": "^1.0.1", + "perfect-scrollbar": "^1.5.5", + "polished": "^4.2.2", + "prismjs": "^1.29.0", + "prop-types": "^15.8.1", + "react-tabs": "^6.0.2", + "slugify": "~1.4.7", + "stickyfill": "^1.1.1", + "swagger2openapi": "^7.0.8", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=6.9", + "npm": ">=3.0.0" + }, + "peerDependencies": { + "core-js": "^3.1.4", + "mobx": "^6.0.4", + "react": "^16.8.4 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.4 || ^17.0.0 || ^18.0.0", + "styled-components": "^4.1.1 || ^5.1.1 || ^6.0.5" + } + }, + "node_modules/reftools": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", + "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", + "dev": true, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -26219,6 +27200,17 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/rst-selector-parser": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", + "integrity": "sha512-nDG1rZeP6oFTLN6yNDV/uiAvs1+FS/KlrEwh7+y7dpuApDBy6bI2HTBcc0/V8lv9OTqfyD34eF7au2pm8aBbhA==", + "dev": true, + "peer": true, + "dependencies": { + "lodash.flattendeep": "^4.4.0", + "nearley": "^2.7.10" + } + }, "node_modules/run-applescript": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", @@ -26668,6 +27660,12 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "dev": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -26762,6 +27760,60 @@ "node": "*" } }, + "node_modules/should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "dev": true, + "dependencies": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "node_modules/should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "dev": true, + "dependencies": { + "should-type": "^1.4.0" + } + }, + "node_modules/should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", + "dev": true, + "dependencies": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "node_modules/should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", + "dev": true + }, + "node_modules/should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "dev": true, + "dependencies": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "node_modules/should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "dev": true + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -26828,6 +27880,33 @@ "node": ">=10" } }, + "node_modules/simple-websocket": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/simple-websocket/-/simple-websocket-9.1.0.tgz", + "integrity": "sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "debug": "^4.3.1", + "queue-microtask": "^1.2.2", + "randombytes": "^2.1.0", + "readable-stream": "^3.6.0", + "ws": "^7.4.2" + } + }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", @@ -26899,6 +27978,15 @@ "react-dom": "^18.2.0" } }, + "node_modules/slugify": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.4.7.tgz", + "integrity": "sha512-tf+h5W1IrjNm/9rKKj0JU2MDMruiopx0jjVA5zCdBtcGjfp0+c5rHw/zADLC3IeKlGHtVbHtpfzvYA0OYT+HKg==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -27423,6 +28511,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stickyfill": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stickyfill/-/stickyfill-1.1.1.tgz", + "integrity": "sha512-GCp7vHAfpao+Qh/3Flh9DXEJ/qSi0KJwJw6zYlZOtRYXWUIpMM6mC2rIep/dK8RQqwW0KxGJIllmjPIBOGN8AA==", + "dev": true + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -27777,6 +28871,74 @@ "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", "devOptional": true }, + "node_modules/styled-components": { + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.12.tgz", + "integrity": "sha512-n/O4PzRPhbYI0k1vKKayfti3C/IGcPf+DqcrOB7O/ab9x4u/zjqraneT5N45+sIe87cxrCApXM8Bna7NYxwoTA==", + "dev": true, + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "dev": true + }, "node_modules/stylus-lookup": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/stylus-lookup/-/stylus-lookup-3.0.2.tgz", @@ -28071,6 +29233,42 @@ "express": ">=4.0.0" } }, + "node_modules/swagger2openapi": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", + "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", + "dev": true, + "dependencies": { + "call-me-maybe": "^1.0.1", + "node-fetch": "^2.6.1", + "node-fetch-h2": "^2.3.0", + "node-readfiles": "^0.2.0", + "oas-kit-common": "^1.0.8", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "oas-validator": "^5.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "boast": "boast.js", + "oas-validate": "oas-validate.js", + "swagger2openapi": "swagger2openapi.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/swagger2openapi/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -28794,6 +29992,14 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-deepmerge": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-6.2.1.tgz", + "integrity": "sha512-8CYSLazCyj0DJDpPIxOFzJG46r93uh6EynYjuey+bxcLltBeqZL7DMfaE5ZPzZNFlav7wx+2TDa/mBl8gkTYzw==", + "engines": { + "node": ">=14.13.1" + } + }, "node_modules/ts-graphviz": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/ts-graphviz/-/ts-graphviz-1.8.2.tgz", @@ -29977,6 +31183,15 @@ "node": ">=0.10.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "dev": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -32542,7 +33757,6 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", - "dev": true, "bin": { "yaml": "bin.mjs" }, @@ -32550,6 +33764,12 @@ "node": ">= 14" } }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -32733,7 +33953,6 @@ "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "dev": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -32750,6 +33969,442 @@ "zod": "^3.18.0" } }, + "packages/contracts": { + "name": "@monkeytype/contracts", + "dependencies": { + "@ts-rest/core": "3.45.2", + "zod": "3.23.8" + }, + "devDependencies": { + "@monkeytype/eslint-config": "*", + "@monkeytype/typescript-config": "*", + "chokidar": "3.6.0", + "esbuild": "0.23.0", + "eslint": "8.57.0", + "rimraf": "5.0.9", + "typescript": "5.5.3" + } + }, + "packages/contracts/node_modules/@esbuild/aix-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/android-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/android-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/android-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/darwin-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/freebsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/linux-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/linux-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/linux-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/linux-loong64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/linux-mips64el": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/linux-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/linux-riscv64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/linux-s390x": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/linux-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/netbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/openbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/sunos-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/win32-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/win32-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/@esbuild/win32-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/contracts/node_modules/esbuild": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" + } + }, + "packages/contracts/node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "packages/eslint-config": { "name": "@monkeytype/eslint-config", "devDependencies": { @@ -32772,6 +34427,9 @@ }, "packages/shared-types": { "name": "@monkeytype/shared-types", + "dependencies": { + "@monkeytype/contracts": "*" + }, "devDependencies": { "@monkeytype/eslint-config": "*", "@monkeytype/typescript-config": "*", diff --git a/package.json b/package.json index bf78d8e5d..2bfcb6014 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "dev-be": "turbo run dev --force --filter @monkeytype/backend", "dev-fe": "turbo run dev --force --filter @monkeytype/frontend", "dev-pkg": "turbo run dev ---force -filter=\"./packages/*\"", - "live": "concurrently --kill-others \"cd frontend && npm run live\" \"cd backend && npm run start\"", + "start": "turbo run start", + "start-be": "turbo run start --filter @monkeytype/backend", + "start-fe": "turbo run start --filter @monkeytype/frontend", "docker": "cd backend && docker compose up", "audit-fe": "cd frontend && npm run audit", "release": "release-it -c .release-it.json", diff --git a/packages/contracts/.eslintrc.cjs b/packages/contracts/.eslintrc.cjs new file mode 100644 index 000000000..922de4abe --- /dev/null +++ b/packages/contracts/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@monkeytype/eslint-config"], +}; diff --git a/packages/contracts/esbuild.config.js b/packages/contracts/esbuild.config.js new file mode 100644 index 000000000..93a823cbd --- /dev/null +++ b/packages/contracts/esbuild.config.js @@ -0,0 +1,94 @@ +const esbuild = require("esbuild"); +const { readdirSync, statSync } = require("fs"); +const { join, extname } = require("path"); +const chokidar = require("chokidar"); + +//check if watch parameter is passed +const isWatch = process.argv.includes("--watch"); + +// Recursive function to get all .ts files in a directory +const getAllFiles = (dirPath, arrayOfFiles = []) => { + const files = readdirSync(dirPath); + + files.forEach((file) => { + const filePath = join(dirPath, file); + if (statSync(filePath).isDirectory()) { + arrayOfFiles = getAllFiles(filePath, arrayOfFiles); + } else if (extname(file) === ".ts") { + arrayOfFiles.push(filePath); + } + }); + + return arrayOfFiles; +}; + +// Get all TypeScript files from the src directory and subdirectories +const entryPoints = getAllFiles("./src"); + +// Function to generate output file names +const getOutfile = (entryPoint, format) => { + const relativePath = entryPoint.replace("src/", ""); + const fileBaseName = relativePath.replace(".ts", ""); + return `./dist/${fileBaseName}.${format === "esm" ? "mjs" : "cjs"}`; +}; + +// Common build settings +const commonSettings = { + bundle: true, + sourcemap: true, + minify: true, +}; + +function buildAll(silent, stopOnError) { + console.log("Building all files..."); + entryPoints.forEach((entry) => { + build(entry, silent, stopOnError); + }); +} + +function build(entry, silent, stopOnError) { + if (!silent) console.log("Building", entry); + + // ESM build + esbuild + .build({ + ...commonSettings, + entryPoints: [entry], + format: "esm", + outfile: getOutfile(entry, "esm"), + }) + .catch((e) => { + console.log(`Failed to build ${entry} to ESM:`, e); + if (stopOnError) process.exit(1); + }); + + // CommonJS build + esbuild + .build({ + ...commonSettings, + entryPoints: [entry], + format: "cjs", + outfile: getOutfile(entry, "cjs"), + }) + .catch((e) => { + console.log(`Failed to build ${entry} to CJS:`, e); + if (stopOnError) process.exit(1); + }); +} + +if (isWatch) { + buildAll(true, false); + console.log("Starting watch mode..."); + chokidar.watch("./src/**/*.ts").on( + "change", + (path) => { + console.log("File change detected..."); + build(path, false, false); + }, + { + ignoreInitial: true, + } + ); +} else { + buildAll(false, true); +} diff --git a/packages/contracts/package.json b/packages/contracts/package.json new file mode 100644 index 000000000..1baf45052 --- /dev/null +++ b/packages/contracts/package.json @@ -0,0 +1,35 @@ +{ + "name": "@monkeytype/contracts", + "private": true, + "scripts": { + "dev": "rimraf ./dist && node esbuild.config.js --watch", + "build": "rimraf ./dist && node esbuild.config.js", + "ts-check": "tsc --noEmit", + "lint": "eslint \"./**/*.ts\"" + }, + "dependencies": { + "@ts-rest/core": "3.45.2", + "zod": "3.23.8" + }, + "devDependencies": { + "@monkeytype/eslint-config": "*", + "@monkeytype/typescript-config": "*", + "chokidar": "3.6.0", + "esbuild": "0.23.0", + "eslint": "8.57.0", + "rimraf": "5.0.9", + "typescript": "5.5.3" + }, + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./*": { + "types": "./src/*.ts", + "import": "./dist/*.mjs", + "require": "./dist/*.cjs" + } + } +} diff --git a/packages/contracts/src/configs.ts b/packages/contracts/src/configs.ts new file mode 100644 index 000000000..8790de4df --- /dev/null +++ b/packages/contracts/src/configs.ts @@ -0,0 +1,61 @@ +import { initContract } from "@ts-rest/core"; +import { z } from "zod"; + +import { + CommonResponses, + EndpointMetadata, + MonkeyResponseSchema, + responseWithNullableData, +} from "./schemas/api"; +import { PartialConfigSchema } from "./schemas/configs"; + +export const GetConfigResponseSchema = + responseWithNullableData(PartialConfigSchema); + +export type GetConfigResponse = z.infer; + +const c = initContract(); + +export const configsContract = c.router( + { + get: { + summary: "get config", + description: "Get config of the current user.", + method: "GET", + path: "/", + responses: { + 200: GetConfigResponseSchema, + }, + }, + save: { + method: "PATCH", + path: "/", + body: PartialConfigSchema.strict(), + responses: { + 200: MonkeyResponseSchema, + }, + summary: "update config", + description: + "Update the config of the current user. Only provided values will be updated while the missing values will be unchanged.", + }, + delete: { + method: "DELETE", + path: "/", + body: c.noBody(), + responses: { + 200: MonkeyResponseSchema, + }, + summary: "delete config", + description: "Delete/reset the config for the current user.", + }, + }, + { + pathPrefix: "/configs", + strictStatusCodes: true, + metadata: { + openApiTags: "configs", + } as EndpointMetadata, + + commonResponses: CommonResponses, + } +); diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts new file mode 100644 index 000000000..088c399d0 --- /dev/null +++ b/packages/contracts/src/index.ts @@ -0,0 +1,8 @@ +import { initContract } from "@ts-rest/core"; +import { configsContract } from "./configs"; + +const c = initContract(); + +export const contract = c.router({ + configs: configsContract, +}); diff --git a/packages/contracts/src/schemas/api.ts b/packages/contracts/src/schemas/api.ts new file mode 100644 index 000000000..f04555610 --- /dev/null +++ b/packages/contracts/src/schemas/api.ts @@ -0,0 +1,77 @@ +import { z, ZodSchema } from "zod"; + +export type OperationTag = "configs"; + +export type EndpointMetadata = { + /** Authentication options, by default a bearer token is required. */ + authenticationOptions?: RequestAuthenticationOptions; + openApiTags?: OperationTag | OperationTag[]; +}; + +export type RequestAuthenticationOptions = { + /** 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; + /** Endpoint requires an authentication token which is younger than one minute. */ + requireFreshToken?: boolean; + noCache?: boolean; +}; + +export const MonkeyResponseSchema = z.object({ + message: z.string(), +}); +export type MonkeyResponseType = z.infer; + +export const MonkeyValidationErrorSchema = MonkeyResponseSchema.extend({ + validationErrors: z.array(z.string()).nonempty(), +}); +export type MonkeyValidationError = z.infer; + +export const MonkeyClientError = MonkeyResponseSchema; +export const MonkeyServerError = MonkeyClientError.extend({ + errorId: z.string(), + uid: z.string().optional(), +}); +export type MonkeyServerErrorType = z.infer; + +export function responseWithNullableData( + dataSchema: T +): z.ZodObject< + z.objectUtil.extendShape< + typeof MonkeyResponseSchema.shape, + { + data: z.ZodNullable; + } + > +> { + return MonkeyResponseSchema.extend({ + data: dataSchema.nullable(), + }); +} + +export function responseWithData( + dataSchema: T +): z.ZodObject< + z.objectUtil.extendShape< + typeof MonkeyResponseSchema.shape, + { + data: T; + } + > +> { + return MonkeyResponseSchema.extend({ + data: dataSchema, + }); +} + +export const CommonResponses = { + 400: MonkeyClientError.describe("Generic client error"), + 401: MonkeyClientError.describe( + "Authentication required but not provided or invalid" + ), + 403: MonkeyClientError.describe("Operation not permitted"), + 422: MonkeyValidationErrorSchema.describe("Request validation failed"), + 429: MonkeyClientError.describe("Rate limit exceeded"), + 500: MonkeyServerError.describe("Generic server error"), +}; diff --git a/packages/contracts/src/schemas/configs.ts b/packages/contracts/src/schemas/configs.ts new file mode 100644 index 000000000..437aee7e6 --- /dev/null +++ b/packages/contracts/src/schemas/configs.ts @@ -0,0 +1,390 @@ +import { z } from "zod"; +import { token } from "./util"; + +export const SmoothCaretSchema = z.enum(["off", "slow", "medium", "fast"]); +export type SmoothCaret = z.infer; + +export const QuickRestartSchema = z.enum(["off", "esc", "tab", "enter"]); +export type QuickRestart = z.infer; + +export const QuoteLengthSchema = z.union([ + z.literal(-3), + z.literal(-2), + z.literal(-1), + z.literal(0), + z.literal(1), + z.literal(2), + z.literal(3), +]); +export type QuoteLength = z.infer; + +export const QuoteLengthConfigSchema = z.array(QuoteLengthSchema); +export type QuoteLengthConfig = z.infer; + +export const CaretStyleSchema = z.enum([ + "off", + "default", + "block", + "outline", + "underline", + "carrot", + "banana", +]); +export type CaretStyle = z.infer; + +export const ConfidenceModeSchema = z.enum(["off", "on", "max"]); +export type ConfidenceMode = z.infer; + +export const IndicateTyposSchema = z.enum(["off", "below", "replace"]); +export type IndicateTypos = z.infer; + +export const TimerStyleSchema = z.enum(["off", "bar", "text", "mini"]); +export type TimerStyle = z.infer; + +export const LiveSpeedAccBurstStyleSchema = z.enum(["off", "text", "mini"]); +export type LiveSpeedAccBurstStyle = z.infer< + typeof LiveSpeedAccBurstStyleSchema +>; + +export const RandomThemeSchema = z.enum([ + "off", + "on", + "fav", + "light", + "dark", + "custom", +]); +export type RandomTheme = z.infer; + +export const TimerColorSchema = z.enum(["black", "sub", "text", "main"]); +export type TimerColor = z.infer; + +export const TimerOpacitySchema = z.enum(["0.25", "0.5", "0.75", "1"]); +export type TimerOpacity = z.infer; + +export const StopOnErrorSchema = z.enum(["off", "word", "letter"]); +export type StopOnError = z.infer; + +export const KeymapModeSchema = z.enum(["off", "static", "react", "next"]); +export type KeymapMode = z.infer; + +export const KeymapStyleSchema = z.enum([ + "staggered", + "alice", + "matrix", + "split", + "split_matrix", + "steno", + "steno_matrix", +]); +export type KeymapStyle = z.infer; + +export const KeymapLegendStyleSchema = z.enum([ + "lowercase", + "uppercase", + "blank", + "dynamic", +]); +export type KeymapLegendStyle = z.infer; + +export const KeymapShowTopRowSchema = z.enum(["always", "layout", "never"]); +export type KeymapShowTopRow = z.infer; + +export const SingleListCommandLineSchema = z.enum(["manual", "on"]); +export type SingleListCommandLine = z.infer; + +export const PlaySoundOnErrorSchema = z.enum(["off", "1", "2", "3", "4"]); +export type PlaySoundOnError = z.infer; + +export const PlaySoundOnClickSchema = z.enum([ + "off", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", +]); +export type PlaySoundOnClick = z.infer; + +export const SoundVolumeSchema = z.enum(["0.1", "0.5", "1.0"]); +export type SoundVolume = z.infer; + +export const PaceCaretSchema = z.enum([ + "off", + "average", + "pb", + "last", + "custom", + "daily", +]); +export type PaceCaret = z.infer; + +export const AccountChartSchema = z.tuple([ + z.enum(["on", "off"]), + z.enum(["on", "off"]), + z.enum(["on", "off"]), + z.enum(["on", "off"]), +]); +export type AccountChart = z.infer; + +export const MinimumWordsPerMinuteSchema = z.enum(["off", "custom"]); +export type MinimumWordsPerMinute = z.infer; + +export const HighlightModeSchema = z.enum([ + "off", + "letter", + "word", + "next_word", + "next_two_words", + "next_three_words", +]); +export type HighlightMode = z.infer; + +export const TapeModeSchema = z.enum(["off", "letter", "word"]); +export type TapeMode = z.infer; + +export const TypingSpeedUnitSchema = z.enum([ + "wpm", + "cpm", + "wps", + "cps", + "wph", +]); +export type TypingSpeedUnit = z.infer; + +export const AdsSchema = z.enum(["off", "result", "on", "sellout"]); +export type Ads = z.infer; + +export const MinimumAccuracySchema = z.enum(["off", "custom"]); +export type MinimumAccuracy = z.infer; + +export const RepeatQuotesSchema = z.enum(["off", "typing"]); +export type RepeatQuotes = z.infer; + +export const OppositeShiftModeSchema = z.enum(["off", "on", "keymap"]); +export type OppositeShiftMode = z.infer; + +export const CustomBackgroundSizeSchema = z.enum(["cover", "contain", "max"]); +export type CustomBackgroundSize = z.infer; + +export const CustomBackgroundFilterSchema = z.tuple([ + z.number(), + z.number(), + z.number(), + z.number(), +]); +export type CustomBackgroundFilter = z.infer< + typeof CustomBackgroundFilterSchema +>; + +export const CustomLayoutFluidSchema = z.string().regex(/^[0-9a-zA-Z_#]+$/); //TODO better regex +export type CustomLayoutFluid = z.infer; + +export const MonkeyPowerLevelSchema = z.enum(["off", "1", "2", "3", "4"]); +export type MonkeyPowerLevel = z.infer; + +export const MinimumBurstSchema = z.enum(["off", "fixed", "flex"]); +export type MinimumBurst = z.infer; + +export const ShowAverageSchema = z.enum(["off", "speed", "acc", "both"]); +export type ShowAverage = z.infer; + +export const ColorHexValueSchema = z.string().regex(/^#([\da-f]{3}){1,2}$/i); +export type ColorHexValue = z.infer; + +export const DifficultySchema = z.enum(["normal", "expert", "master"]); +export type Difficulty = z.infer; + +export const NumberModeSchema = z.enum(["time", "words", "quote"]); +export const CustomModeSchema = z.enum(["custom"]); +export const ZenModeSchema = z.enum(["zen"]); + +export const ModeSchema = z.union([ + NumberModeSchema, + CustomModeSchema, + ZenModeSchema, +]); +export type Mode = z.infer; + +export const CustomThemeColorsSchema = z.tuple([ + ColorHexValueSchema, + ColorHexValueSchema, + ColorHexValueSchema, + ColorHexValueSchema, + ColorHexValueSchema, + ColorHexValueSchema, + ColorHexValueSchema, + ColorHexValueSchema, + ColorHexValueSchema, + ColorHexValueSchema, +]); +export type CustomThemeColors = z.infer; + +export const FavThemesSchema = z.array(token().max(50)); +export type FavThemes = z.infer; + +export const FunboxSchema = z + .string() + .max(100) + .regex(/[\w#]+/); +export type Funbox = z.infer; + +export const PaceCaretCustomSpeedSchema = z.number().nonnegative(); +export type PaceCaretCustomSpeed = z.infer; + +export const MinWpmCustomSpeedSchema = z.number().nonnegative(); +export type MinWpmCustomSpeed = z.infer; + +export const MinimumAccuracyCustomSchema = z.number().nonnegative().max(100); +export type MinimumAccuracyCustom = z.infer; + +export const MinimumBurstCustomSpeedSchema = z.number().nonnegative(); +export type MinimumBurstCustomSpeed = z.infer< + typeof MinimumBurstCustomSpeedSchema +>; + +export const TimeConfigSchema = z.number().int().nonnegative(); +export type TimeConfig = z.infer; + +export const WordCountSchema = z.number().int().nonnegative(); +export type WordCount = z.infer; + +export const FontFamilySchema = z + .string() + .max(50) + .regex(/^[a-zA-Z0-9_\-+.]+$/); +export type FontFamily = z.infer; + +export const ThemeNameSchema = token().max(50); +export type ThemeName = z.infer; + +export const LanguageSchema = z + .string() + .max(50) + .regex(/^[a-zA-Z0-9_+]+$/); +export type Language = z.infer; + +export const KeymapLayoutSchema = z + .string() + .max(50) + .regex(/[\w\-_]+/); +export type KeymapLayout = z.infer; + +export const LayoutSchema = token().max(50); +export type Layout = z.infer; + +export const FontSizeSchema = z.number().positive(); +export type FontSize = z.infer; + +export const MaxLineWidthSchema = z.number().min(20).max(1000).or(z.literal(0)); +export type MaxLineWidth = z.infer; + +export const CustomBackgroundSchema = z + .string() + .regex(/(https|http):\/\/(www\.|).+\..+\/.+(\.png|\.gif|\.jpeg|\.jpg)/gi) + .or(z.literal("")); +export type CustomBackground = z.infer; + +export const ConfigSchema = z + .object({ + theme: ThemeNameSchema, + themeLight: ThemeNameSchema, + themeDark: ThemeNameSchema, + autoSwitchTheme: z.boolean(), + customTheme: z.boolean(), + //customThemeId: token().nonnegative().max(24), + customThemeColors: CustomThemeColorsSchema, + favThemes: FavThemesSchema, + showKeyTips: z.boolean(), + smoothCaret: SmoothCaretSchema, + quickRestart: QuickRestartSchema, + punctuation: z.boolean(), + numbers: z.boolean(), + words: WordCountSchema, + time: TimeConfigSchema, + mode: ModeSchema, + quoteLength: QuoteLengthConfigSchema, + language: LanguageSchema, + fontSize: FontSizeSchema, + freedomMode: z.boolean(), + difficulty: DifficultySchema, + blindMode: z.boolean(), + quickEnd: z.boolean(), + caretStyle: CaretStyleSchema, + paceCaretStyle: CaretStyleSchema, + flipTestColors: z.boolean(), + layout: LayoutSchema, + funbox: FunboxSchema, + confidenceMode: ConfidenceModeSchema, + indicateTypos: IndicateTyposSchema, + timerStyle: TimerStyleSchema, + liveSpeedStyle: LiveSpeedAccBurstStyleSchema, + liveAccStyle: LiveSpeedAccBurstStyleSchema, + liveBurstStyle: LiveSpeedAccBurstStyleSchema, + colorfulMode: z.boolean(), + randomTheme: RandomThemeSchema, + timerColor: TimerColorSchema, + timerOpacity: TimerOpacitySchema, + stopOnError: StopOnErrorSchema, + showAllLines: z.boolean(), + keymapMode: KeymapModeSchema, + keymapStyle: KeymapStyleSchema, + keymapLegendStyle: KeymapLegendStyleSchema, + keymapLayout: KeymapLayoutSchema, + keymapShowTopRow: KeymapShowTopRowSchema, + fontFamily: FontFamilySchema, + smoothLineScroll: z.boolean(), + alwaysShowDecimalPlaces: z.boolean(), + alwaysShowWordsHistory: z.boolean(), + singleListCommandLine: SingleListCommandLineSchema, + capsLockWarning: z.boolean(), + playSoundOnError: PlaySoundOnErrorSchema, + playSoundOnClick: PlaySoundOnClickSchema, + soundVolume: SoundVolumeSchema, + startGraphsAtZero: z.boolean(), + showOutOfFocusWarning: z.boolean(), + paceCaret: PaceCaretSchema, + paceCaretCustomSpeed: PaceCaretCustomSpeedSchema, + repeatedPace: z.boolean(), + accountChart: AccountChartSchema, + minWpm: MinimumWordsPerMinuteSchema, + minWpmCustomSpeed: MinWpmCustomSpeedSchema, + highlightMode: HighlightModeSchema, + tapeMode: TapeModeSchema, + typingSpeedUnit: TypingSpeedUnitSchema, + ads: AdsSchema, + hideExtraLetters: z.boolean(), + strictSpace: z.boolean(), + minAcc: MinimumAccuracySchema, + minAccCustom: MinimumAccuracyCustomSchema, + monkey: z.boolean(), + repeatQuotes: RepeatQuotesSchema, + oppositeShiftMode: OppositeShiftModeSchema, + customBackground: CustomBackgroundSchema, + customBackgroundSize: CustomBackgroundSizeSchema, + customBackgroundFilter: CustomBackgroundFilterSchema, + customLayoutfluid: CustomLayoutFluidSchema, + monkeyPowerLevel: MonkeyPowerLevelSchema, + minBurst: MinimumBurstSchema, + minBurstCustomSpeed: MinimumBurstCustomSpeedSchema, + burstHeatmap: z.boolean(), + britishEnglish: z.boolean(), + lazyMode: z.boolean(), + showAverage: ShowAverageSchema, + maxLineWidth: MaxLineWidthSchema, + }) + .strict(); +export type Config = z.infer; + +export const PartialConfigSchema = ConfigSchema.partial(); +export type PartialConfig = z.infer; diff --git a/packages/contracts/src/schemas/users.ts b/packages/contracts/src/schemas/users.ts new file mode 100644 index 000000000..3e3ed5e25 --- /dev/null +++ b/packages/contracts/src/schemas/users.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; +import { DifficultySchema } from "./configs"; +import { StringNumberSchema } from "./util"; + +export const PersonalBestSchema = z.object({ + acc: z.number().nonnegative().max(100), + consistency: z.number().nonnegative().max(100), + difficulty: DifficultySchema, + lazyMode: z.boolean().optional(), + language: z + .string() + .max(100) + .regex(/[\w+]+/), + punctuation: z.boolean().optional(), + numbers: z.boolean().optional(), + raw: z.number().nonnegative(), + wpm: z.number().nonnegative(), + timestamp: z.number().nonnegative(), +}); +export type PersonalBest = z.infer; + +export const PersonalBestsSchema = z.object({ + time: z.record(StringNumberSchema, z.array(PersonalBestSchema)), + words: z.record(StringNumberSchema, z.array(PersonalBestSchema)), + quote: z.record(StringNumberSchema, z.array(PersonalBestSchema)), + custom: z.record(z.literal("custom"), z.array(PersonalBestSchema)), + zen: z.record(z.literal("zen"), z.array(PersonalBestSchema)), +}); +export type PersonalBests = z.infer; diff --git a/packages/contracts/src/schemas/util.ts b/packages/contracts/src/schemas/util.ts new file mode 100644 index 000000000..743413404 --- /dev/null +++ b/packages/contracts/src/schemas/util.ts @@ -0,0 +1,9 @@ +import { z, ZodString } from "zod"; + +export const StringNumberSchema = z.custom<`${number}`>((val) => { + return typeof val === "string" ? /^\d+$/.test(val) : false; +}); + +export type StringNumber = z.infer; + +export const token = (): ZodString => z.string().regex(/^[a-zA-Z0-9_]+$/); diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json new file mode 100644 index 000000000..5130063ef --- /dev/null +++ b/packages/contracts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@monkeytype/typescript-config/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "moduleResolution": "Node", + "module": "ES6" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/shared-types/package.json b/packages/shared-types/package.json index dcc116822..0a231dff0 100644 --- a/packages/shared-types/package.json +++ b/packages/shared-types/package.json @@ -7,6 +7,9 @@ "ts-check": "tsc --noEmit", "lint": "eslint \"./**/*.ts\"" }, + "dependencies": { + "@monkeytype/contracts": "*" + }, "devDependencies": { "@monkeytype/eslint-config": "*", "@monkeytype/typescript-config": "*", diff --git a/packages/shared-types/src/config.ts b/packages/shared-types/src/config.ts index 6b787628c..17e80b7c0 100644 --- a/packages/shared-types/src/config.ts +++ b/packages/shared-types/src/config.ts @@ -1,174 +1,84 @@ -import { PersonalBests } from "./user"; +import { PersonalBests } from "@monkeytype/contracts/schemas/users"; -export type SmoothCaret = "off" | "slow" | "medium" | "fast"; -export type QuickRestart = "off" | "esc" | "tab" | "enter"; -export type QuoteLength = -3 | -2 | -1 | 0 | 1 | 2 | 3; +export type SmoothCaret = + import("@monkeytype/contracts/schemas/configs").SmoothCaret; +export type QuickRestart = + import("@monkeytype/contracts/schemas/configs").QuickRestart; +export type QuoteLength = + import("@monkeytype/contracts/schemas/configs").QuoteLength; export type CaretStyle = - | "off" - | "default" - | "block" - | "outline" - | "underline" - | "carrot" - | "banana"; -export type Difficulty = "normal" | "expert" | "master"; -export type Mode = "time" | "words" | "quote" | "custom" | "zen"; + import("@monkeytype/contracts/schemas/configs").CaretStyle; +export type Difficulty = + import("@monkeytype/contracts/schemas/configs").Difficulty; +export type Mode = import("@monkeytype/contracts/schemas/configs").Mode; export type Mode2 = M extends M ? keyof PersonalBests[M] : never; export type Mode2Custom = Mode2 | "custom"; -export type ConfidenceMode = "off" | "on" | "max"; -export type IndicateTypos = "off" | "below" | "replace"; -export type TimerStyle = "off" | "bar" | "text" | "mini"; -export type LiveSpeedAccBurstStyle = "off" | "text" | "mini"; -export type RandomTheme = "off" | "on" | "fav" | "light" | "dark" | "custom"; -export type TimerColor = "black" | "sub" | "text" | "main"; -export type TimerOpacity = "0.25" | "0.5" | "0.75" | "1"; -export type StopOnError = "off" | "word" | "letter"; -export type KeymapMode = "off" | "static" | "react" | "next"; +export type ConfidenceMode = + import("@monkeytype/contracts/schemas/configs").ConfidenceMode; +export type IndicateTypos = + import("@monkeytype/contracts/schemas/configs").IndicateTypos; +export type TimerStyle = + import("@monkeytype/contracts/schemas/configs").TimerStyle; +export type LiveSpeedAccBurstStyle = + import("@monkeytype/contracts/schemas/configs").LiveSpeedAccBurstStyle; +export type RandomTheme = + import("@monkeytype/contracts/schemas/configs").RandomTheme; +export type TimerColor = + import("@monkeytype/contracts/schemas/configs").TimerColor; +export type TimerOpacity = + import("@monkeytype/contracts/schemas/configs").TimerOpacity; +export type StopOnError = + import("@monkeytype/contracts/schemas/configs").StopOnError; +export type KeymapMode = + import("@monkeytype/contracts/schemas/configs").KeymapMode; export type KeymapStyle = - | "staggered" - | "alice" - | "matrix" - | "split" - | "split_matrix" - | "steno" - | "steno_matrix"; -export type KeymapLegendStyle = "lowercase" | "uppercase" | "blank" | "dynamic"; -export type KeymapShowTopRow = "always" | "layout" | "never"; -export type SingleListCommandLine = "manual" | "on"; + import("@monkeytype/contracts/schemas/configs").KeymapStyle; +export type KeymapLegendStyle = + import("@monkeytype/contracts/schemas/configs").KeymapLegendStyle; +export type KeymapShowTopRow = + import("@monkeytype/contracts/schemas/configs").KeymapShowTopRow; +export type SingleListCommandLine = + import("@monkeytype/contracts/schemas/configs").SingleListCommandLine; export type PlaySoundOnClick = - | "off" - | "1" - | "2" - | "3" - | "4" - | "5" - | "6" - | "7" - | "8" - | "9" - | "10" - | "11" - | "12" - | "13" - | "14" - | "15"; -export type PlaySoundOnError = "off" | "1" | "2" | "3" | "4"; -export type SoundVolume = "0.1" | "0.5" | "1.0"; -export type PaceCaret = "off" | "average" | "pb" | "last" | "custom" | "daily"; -export type AccountChart = [ - "off" | "on", - "off" | "on", - "off" | "on", - "off" | "on" -]; -export type MinimumWordsPerMinute = "off" | "custom"; + import("@monkeytype/contracts/schemas/configs").PlaySoundOnClick; +export type PlaySoundOnError = + import("@monkeytype/contracts/schemas/configs").PlaySoundOnError; +export type SoundVolume = + import("@monkeytype/contracts/schemas/configs").SoundVolume; +export type PaceCaret = + import("@monkeytype/contracts/schemas/configs").PaceCaret; +export type AccountChart = + import("@monkeytype/contracts/schemas/configs").AccountChart; +export type MinimumWordsPerMinute = + import("@monkeytype/contracts/schemas/configs").MinimumWordsPerMinute; export type HighlightMode = - | "off" - | "letter" - | "word" - | "next_word" - | "next_two_words" - | "next_three_words"; -export type TypingSpeedUnit = "wpm" | "cpm" | "wps" | "cps" | "wph"; -export type Ads = "off" | "result" | "on" | "sellout"; -export type MinimumAccuracy = "off" | "custom"; -export type RepeatQuotes = "off" | "typing"; -export type OppositeShiftMode = "off" | "on" | "keymap"; -export type CustomBackgroundSize = "cover" | "contain" | "max"; -export type CustomBackgroundFilter = [number, number, number, number, number]; -export type CustomLayoutFluid = `${string}#${string}#${string}`; -export type MonkeyPowerLevel = "off" | "1" | "2" | "3" | "4"; -export type MinimumBurst = "off" | "fixed" | "flex"; -export type ShowAverage = "off" | "speed" | "acc" | "both"; -export type TapeMode = "off" | "letter" | "word"; - -export type Config = { - theme: string; - themeLight: string; - themeDark: string; - autoSwitchTheme: boolean; - customTheme: boolean; - customThemeColors: string[]; - favThemes: string[]; - showKeyTips: boolean; - smoothCaret: SmoothCaret; - quickRestart: QuickRestart; - punctuation: boolean; - numbers: boolean; - words: number; - time: number; - mode: Mode; - quoteLength: QuoteLength[]; - language: string; - fontSize: number; - freedomMode: boolean; - difficulty: Difficulty; - blindMode: boolean; - quickEnd: boolean; - caretStyle: CaretStyle; - paceCaretStyle: CaretStyle; - flipTestColors: boolean; - layout: string; - funbox: string; - confidenceMode: ConfidenceMode; - indicateTypos: IndicateTypos; - timerStyle: TimerStyle; - liveSpeedStyle: LiveSpeedAccBurstStyle; - liveAccStyle: LiveSpeedAccBurstStyle; - liveBurstStyle: LiveSpeedAccBurstStyle; - colorfulMode: boolean; - randomTheme: RandomTheme; - timerColor: TimerColor; - timerOpacity: TimerOpacity; - stopOnError: StopOnError; - showAllLines: boolean; - keymapMode: KeymapMode; - keymapStyle: KeymapStyle; - keymapLegendStyle: KeymapLegendStyle; - keymapLayout: string; - keymapShowTopRow: KeymapShowTopRow; - fontFamily: string; - smoothLineScroll: boolean; - alwaysShowDecimalPlaces: boolean; - alwaysShowWordsHistory: boolean; - singleListCommandLine: SingleListCommandLine; - capsLockWarning: boolean; - playSoundOnError: PlaySoundOnError; - playSoundOnClick: PlaySoundOnClick; - soundVolume: SoundVolume; - startGraphsAtZero: boolean; - showOutOfFocusWarning: boolean; - paceCaret: PaceCaret; - paceCaretCustomSpeed: number; - repeatedPace: boolean; - accountChart: AccountChart; - minWpm: MinimumWordsPerMinute; - minWpmCustomSpeed: number; - highlightMode: HighlightMode; - typingSpeedUnit: TypingSpeedUnit; - ads: Ads; - hideExtraLetters: boolean; - strictSpace: boolean; - minAcc: MinimumAccuracy; - minAccCustom: number; - monkey: boolean; - repeatQuotes: RepeatQuotes; - oppositeShiftMode: OppositeShiftMode; - customBackground: string; - customBackgroundSize: CustomBackgroundSize; - customBackgroundFilter: CustomBackgroundFilter; - customLayoutfluid: CustomLayoutFluid; - monkeyPowerLevel: MonkeyPowerLevel; - minBurst: MinimumBurst; - minBurstCustomSpeed: number; - burstHeatmap: boolean; - britishEnglish: boolean; - lazyMode: boolean; - showAverage: ShowAverage; - tapeMode: TapeMode; - maxLineWidth: number; -}; + import("@monkeytype/contracts/schemas/configs").HighlightMode; +export type TypingSpeedUnit = + import("@monkeytype/contracts/schemas/configs").TypingSpeedUnit; +export type Ads = import("@monkeytype/contracts/schemas/configs").Ads; +export type MinimumAccuracy = + import("@monkeytype/contracts/schemas/configs").MinimumAccuracy; +export type RepeatQuotes = + import("@monkeytype/contracts/schemas/configs").RepeatQuotes; +export type OppositeShiftMode = + import("@monkeytype/contracts/schemas/configs").OppositeShiftMode; +export type CustomBackgroundSize = + import("@monkeytype/contracts/schemas/configs").CustomBackgroundSize; +export type CustomBackgroundFilter = + import("@monkeytype/contracts/schemas/configs").CustomBackgroundFilter; +export type CustomLayoutFluid = + import("@monkeytype/contracts/schemas/configs").CustomLayoutFluid; +export type MonkeyPowerLevel = + import("@monkeytype/contracts/schemas/configs").MonkeyPowerLevel; +export type MinimumBurst = + import("@monkeytype/contracts/schemas/configs").MinimumBurst; +export type ShowAverage = + import("@monkeytype/contracts/schemas/configs").ShowAverage; +export type TapeMode = import("@monkeytype/contracts/schemas/configs").TapeMode; +export type CustomThemeColors = + import("@monkeytype/contracts/schemas/configs").CustomThemeColors; +export type Config = import("@monkeytype/contracts/schemas/configs").Config; export type ConfigValue = Config[keyof Config]; diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts index cddd9ed1e..f67434b7d 100644 --- a/packages/shared-types/src/index.ts +++ b/packages/shared-types/src/index.ts @@ -1,4 +1,9 @@ -import { Config, Difficulty, Mode, Mode2 } from "./config"; +import { + Config, + Difficulty, + Mode, +} from "@monkeytype/contracts/schemas/configs"; +import { Mode2 } from "./config"; import { PersonalBest, PersonalBests } from "./user"; export type ValidModeRule = { @@ -389,7 +394,7 @@ export type UserProfileDetails = { export type CustomTheme = { _id: string; name: string; - colors: string[]; + colors: import("@monkeytype/contracts/schemas/configs").CustomThemeColors; }; export type PremiumInfo = { diff --git a/packages/shared-types/src/user.ts b/packages/shared-types/src/user.ts index acd9cc4c2..9881eb10b 100644 --- a/packages/shared-types/src/user.ts +++ b/packages/shared-types/src/user.ts @@ -1,23 +1,5 @@ -import { Difficulty } from "./config"; -import { StringNumber } from "./util"; +export type PersonalBest = + import("@monkeytype/contracts/schemas/users").PersonalBest; -export type PersonalBest = { - acc: number; - consistency?: number; - difficulty: Difficulty; - lazyMode?: boolean; - language: string; - punctuation?: boolean; - numbers?: boolean; - raw: number; - wpm: number; - timestamp: number; -}; - -export type PersonalBests = { - time: Record; - words: Record; - quote: Record; - custom: Partial>; - zen: Partial>; -}; +export type PersonalBests = + import("@monkeytype/contracts/schemas/users").PersonalBests; diff --git a/packages/shared-types/src/util.ts b/packages/shared-types/src/util.ts index b1769ff9f..87c3cb8ac 100644 --- a/packages/shared-types/src/util.ts +++ b/packages/shared-types/src/util.ts @@ -1 +1,2 @@ -export type StringNumber = `${number}`; +export type StringNumber = + import("@monkeytype/contracts/schemas/util").StringNumber; diff --git a/packages/shared-types/tsconfig.json b/packages/shared-types/tsconfig.json index 5130063ef..c0e629adb 100644 --- a/packages/shared-types/tsconfig.json +++ b/packages/shared-types/tsconfig.json @@ -4,9 +4,7 @@ "outDir": "./dist", "rootDir": "./src", "declaration": true, - "declarationMap": true, - "moduleResolution": "Node", - "module": "ES6" + "declarationMap": true }, "include": ["src"], "exclude": ["node_modules", "dist"] diff --git a/turbo.json b/turbo.json index b844759cb..3c46f0a78 100644 --- a/turbo.json +++ b/turbo.json @@ -23,6 +23,9 @@ "persistent": true, "cache": false }, + "start": { + "dependsOn": ["build"] + }, "@monkeytype/frontend#validate-json": { "dependsOn": ["^parallel"] },