Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@
"not op_mini all",
"not dead"
],
"lint-staged": {
"*.{json,scss,css,html}": [
"prettier --write"
],
"*.{ts,js}": [
"prettier --write",
"oxlint",
"eslint"
]
},
"devDependencies": {
"@fortawesome/fontawesome-free": "5.15.4",
"@monkeytype/eslint-config": "workspace:*",
Expand Down
66 changes: 66 additions & 0 deletions frontend/scripts/language-hashes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Plugin } from "vite";
import { readdirSync, readFileSync } from "fs";
import { TextEncoder } from "util";
import { createHash } from "crypto";

const virtualModuleId = "virtual:language-hashes";
const resolvedVirtualModuleId = "\0" + virtualModuleId;
let skip = false;

export function languageHashes(): Plugin {
return {
name: "virtual-language-hashes",
resolveId(id) {
if (id === virtualModuleId) return resolvedVirtualModuleId;
return;
},
load(id) {
if (id === resolvedVirtualModuleId) {
const hashes: Record<string, string> = skip ? {} : getHashes();
return `
export const languageHashes = ${JSON.stringify(hashes)};
`;
}
return;
},
configResolved(resolvedConfig) {
if (resolvedConfig?.define?.["IS_DEVELOPMENT"] === "true") {
skip = true;
console.log("Skipping language hashing in dev environment.");
}
},
};
}

function getHashes(): Record<string, string> {
const start = performance.now();

console.log("\nHashing languages...");

const hashes = Object.fromEntries(
readdirSync("./static/languages").map((file) => {
return [file.slice(0, -5), calcHash(file)];
})
);

const end = performance.now();

console.log(`Creating language hashes took ${Math.round(end - start)} ms`);

return hashes;
}

function calcHash(file: string): string {
const currentLanguage = JSON.stringify(
JSON.parse(readFileSync("./static/languages/" + file).toString()),
null,
0
);
const encoder = new TextEncoder();
const data = encoder.encode(currentLanguage);
return createHash("sha256").update(data).digest("hex");
}

if (import.meta.url.endsWith(process.argv[1] as string)) {
console.log(JSON.stringify(getHashes(), null, 4));
}
5 changes: 5 additions & 0 deletions frontend/src/ts/module.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Language } from "@monkeytype/schemas/languages";

declare module "virtual:language-hashes" {
export const languageHashes: Record<Language, string>;
}
8 changes: 4 additions & 4 deletions frontend/src/ts/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ export async function activateSentry(): Promise<void> {
environment: envConfig.isDevelopment ? "development" : "production",
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
unmask: ["#notificationCenter"],
block: ["#commandLine .modal .suggestions"],
}),
// Sentry.replayIntegration({
// unmask: ["#notificationCenter"],
// block: ["#commandLine .modal .suggestions"],
// }),
Sentry.thirdPartyErrorFilterIntegration({
filterKeys: ["monkeytype-frontend"],
// Defines how to handle errors that contain third party stack frames.
Expand Down
1 change: 0 additions & 1 deletion frontend/src/ts/test/test-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,6 @@ async function init(): Promise<boolean> {
}

if (!language || language.name !== Config.language) {
UpdateConfig.setLanguage("english");
return await init();
}

Expand Down
20 changes: 19 additions & 1 deletion frontend/src/ts/utils/json-data.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Language, LanguageObject } from "@monkeytype/schemas/languages";
import { Challenge } from "@monkeytype/schemas/challenges";
import { LayoutObject } from "@monkeytype/schemas/layouts";
import { toHex } from "./strings";
import { languageHashes } from "virtual:language-hashes";
import { isDevEnvironment } from "./misc";

//pin implementation
const fetch = window.fetch;
const cryptoSubtle = window.crypto.subtle;

/**
* Fetches JSON data from the specified URL using the fetch API.
Expand Down Expand Up @@ -96,9 +100,23 @@ let currentLanguage: LanguageObject;
export async function getLanguage(lang: Language): Promise<LanguageObject> {
// try {
if (currentLanguage === undefined || currentLanguage.name !== lang) {
currentLanguage = await cachedFetchJson<LanguageObject>(
const loaded = await cachedFetchJson<LanguageObject>(
`/languages/${lang}.json`
);

if (!isDevEnvironment()) {
//check the content to make it less easy to manipulate
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(loaded, null, 0));
const hashBuffer = await cryptoSubtle.digest("SHA-256", data);
const hash = toHex(hashBuffer);
if (hash !== languageHashes[lang]) {
throw new Error(
"Integrity check failed. Try refreshing the page. If this error persists, please contact support."
);
}
}
currentLanguage = loaded;
}
return currentLanguage;
}
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/ts/utils/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,20 @@ export function areCharactersVisuallyEqual(
return false;
}

export function toHex(buffer: ArrayBuffer): string {
// @ts-expect-error modern browsers
if (Uint8Array.prototype.toHex !== undefined) {
// @ts-expect-error modern browsers
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
return new Uint8Array(buffer).toHex() as string;
}
const hashArray = Array.from(new Uint8Array(buffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return hashHex;
}

// Export testing utilities for unit tests
export const __testing = {
hasRTLCharacters,
Expand Down
5 changes: 4 additions & 1 deletion frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"module": "ESNext",
"allowUmdGlobalAccess": true,
"target": "ES6",
"noEmit": true
"noEmit": true,
"paths": {
"virtual:language-hashes": ["./src/ts/module.d.ts"]
}
},
"include": ["./src/**/*.ts", "./scripts/**/*.ts"],
"exclude": ["node_modules", "build", "setup-tests.ts", "**/*.spec.ts"]
Expand Down
2 changes: 2 additions & 0 deletions frontend/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import PROD_CONFIG from "./vite.config.prod";
import DEV_CONFIG from "./vite.config.dev";
import MagicString from "magic-string";
import { Fonts } from "./src/ts/constants/fonts";
import { languageHashes } from "./scripts/language-hashes";

/** @type {import("vite").UserConfig} */
const BASE_CONFIG = {
plugins: [
languageHashes(),
{
name: "simple-jquery-inject",
async transform(src, id) {
Expand Down
3 changes: 3 additions & 0 deletions frontend/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from "vitest/config";
import { languageHashes } from "./scripts/language-hashes";

export default defineConfig({
test: {
Expand All @@ -17,4 +18,6 @@ export default defineConfig({
},
},
},

plugins: [languageHashes()],
});
Loading