chore: port client-side JS to TypeScript (#1100)

* chore(challenge/preact): port to typescript

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(js/algorithms): port to typescript

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(js/worker): port to typescript

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(web): fix TypeScript build logic

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(web): port bench.mjs to typescript

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore(web): port main.mjs to typescript

Signed-off-by: Xe Iaso <me@xeiaso.net>

* Update metadata

check-spelling run (pull_request) for Xe/use-typescript

Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
on-behalf-of: @check-spelling <check-spelling-bot@check-spelling.dev>

* fix(js/algorithms/fast): handle old browsers

Closes #1082

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Signed-off-by: check-spelling-bot <check-spelling-bot@users.noreply.github.com>
This commit is contained in:
Xe Iaso 2025-09-11 10:03:10 -04:00 committed by GitHub
parent 8ed89a6c6e
commit 2011b83a44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 190 additions and 130 deletions

View file

@ -214,6 +214,7 @@ nicksnyder
nobots nobots
NONINFRINGEMENT NONINFRINGEMENT
nosleep nosleep
nullglob
OCOB OCOB
ogtag ogtag
oklch oklch
@ -278,6 +279,7 @@ Seo
setsebool setsebool
shellcheck shellcheck
shirou shirou
shopt
Sidetrade Sidetrade
simprint simprint
sitemap sitemap

View file

@ -93,9 +93,9 @@ func page(redir string, difficulty int, loc *localization.SimpleLocalizer) templ
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty, redir)) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d; url=%s", difficulty+1, redir))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 83} return templ.Error{Err: templ_7745c5c3_Err, FileName: `metarefresh.templ`, Line: 16, Col: 85}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {

View file

@ -40,9 +40,9 @@ for the JavaScript code in this page.
mkdir -p static/js mkdir -p static/js
for file in js/*.jsx; do for file in js/*.tsx; do
filename="${file##*/}" # Extracts "app.jsx" from "./js/app.jsx" filename="${file##*/}" # Extracts "app.jsx" from "./js/app.jsx"
output="${filename%.jsx}.js" # Changes "app.jsx" to "app.js" output="${filename%.tsx}.js" # Changes "app.jsx" to "app.js"
echo $output echo $output
esbuild "${file}" --minify --bundle --outfile=static/"${output}" --banner:js="${LICENSE}" esbuild "${file}" --minify --bundle --outfile=static/"${output}" --banner:js="${LICENSE}"

View file

@ -1,62 +0,0 @@
import { render, h, Fragment } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { g, j, u, x } from "./xeact.js";
import { Sha256 } from '@aws-crypto/sha256-js';
/** @jsx h */
/** @jsxFrag Fragment */
function toHexString(arr) {
return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0"))
.join("");
}
const App = () => {
const [state, setState] = useState(null);
const [imageURL, setImageURL] = useState(null);
const [passed, setPassed] = useState(false);
const [challenge, setChallenge] = useState(null);
useEffect(() => {
setState(j("preact_info"));
});
useEffect(() => {
setImageURL(state.pensive_url);
const hash = new Sha256('');
hash.update(state.challenge);
setChallenge(toHexString(hash.digestSync()));
}, [state]);
useEffect(() => {
const timer = setTimeout(() => {
setPassed(true);
}, state.difficulty * 125);
return () => clearTimeout(timer);
}, [challenge]);
useEffect(() => {
window.location.href = u(state.redir, {
result: challenge,
});
}, [passed]);
return (
<>
{imageURL !== null && (
<img src={imageURL} style="width:100%;max-width:256px;" />
)}
{state !== null && (
<>
<p id="status">{state.loading_message}</p>
<p>{state.connection_security_message}</p>
</>
)}
</>
);
};
x(g("app"));
render(<App />, g("app"));

View file

@ -0,0 +1,87 @@
import { render, h, Fragment } from "preact";
import { useState, useEffect } from "preact/hooks";
import { g, j, r, u, x } from "./xeact.js";
import { Sha256 } from "@aws-crypto/sha256-js";
/** @jsx h */
/** @jsxFrag Fragment */
function toHexString(arr: Uint8Array) {
return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0"))
.join("");
}
interface PreactInfo {
redir: string;
challenge: string;
difficulty: number;
connection_security_message: string;
loading_message: string;
pensive_url: string;
}
const App = () => {
const [state, setState] = useState<PreactInfo>();
const [imageURL, setImageURL] = useState<string | null>(null);
const [passed, setPassed] = useState<boolean>(false);
const [challenge, setChallenge] = useState<string | null>(null);
useEffect(() => {
setState(j("preact_info"));
});
useEffect(() => {
if (state === undefined) {
return;
}
setImageURL(state?.pensive_url);
const hash = new Sha256("");
hash.update(state.challenge);
setChallenge(toHexString(hash.digestSync()));
}, [state]);
useEffect(() => {
if (state === undefined) {
return;
}
const timer = setTimeout(() => {
setPassed(true);
}, state?.difficulty * 125);
return () => clearTimeout(timer);
}, [challenge]);
useEffect(() => {
if (state === undefined) {
return;
}
if (challenge === null) {
return;
}
window.location.href = u(state.redir, {
result: challenge,
});
}, [passed]);
return (
<>
{imageURL !== null && (
<img src={imageURL} style={{ width: "100%", maxWidth: "256px" }} />
)}
{state !== undefined && (
<>
<p id="status">{state.loading_message}</p>
<p>{state.connection_security_message}</p>
</>
)}
</>
);
};
x(g("app"));
render(<App />, g("app"));

View file

@ -39,9 +39,18 @@ for the JavaScript code in this page.
mkdir -p static/locales mkdir -p static/locales
cp ../lib/localization/locales/*.json static/locales/ cp ../lib/localization/locales/*.json static/locales/
for file in js/*.mjs js/worker/*.mjs; do shopt -s nullglob globstar
esbuild "${file}" --sourcemap --bundle --minify --outfile=static/"${file}" --banner:js="${LICENSE}"
gzip -f -k -n static/${file} for file in js/**/*.ts js/**/*.mjs; do
zstd -f -k --ultra -22 static/${file} out="static/${file}"
brotli -fZk static/${file} if [[ "$file" == *.ts ]]; then
out="static/${file%.ts}.mjs"
fi
mkdir -p "$(dirname "$out")"
esbuild "$file" --sourcemap --bundle --minify --outfile="$out" --banner:js="$LICENSE"
gzip -f -k -n "$out"
zstd -f -k --ultra -22 "$out"
brotli -fZk "$out"
done done

View file

@ -1,11 +1,21 @@
type ProgressCallback = (nonce: number) => void;
interface ProcessOptions {
basePrefix: string;
version: string;
}
const getHardwareConcurrency = () =>
navigator.hardwareConcurrency !== undefined ? navigator.hardwareConcurrency : 1;
export default function process( export default function process(
{ basePrefix, version }, options: ProcessOptions,
data, data: string,
difficulty = 5, difficulty: number = 5,
signal = null, signal: AbortSignal | null = null,
progressCallback = null, progressCallback?: ProgressCallback,
threads = Math.trunc(Math.max(navigator.hardwareConcurrency / 2, 1)), threads: number = Math.trunc(Math.max(getHardwareConcurrency() / 2, 1)),
) { ): Promise<string> {
console.debug("fast algo"); console.debug("fast algo");
let workerMethod = window.crypto !== undefined ? "webcrypto" : "purejs"; let workerMethod = window.crypto !== undefined ? "webcrypto" : "purejs";
@ -16,13 +26,17 @@ export default function process(
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let webWorkerURL = `${basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${version}`; let webWorkerURL = `${options.basePrefix}/.within.website/x/cmd/anubis/static/js/worker/sha256-${workerMethod}.mjs?cacheBuster=${options.version}`;
console.log(webWorkerURL); const workers: Worker[] = [];
const workers = [];
let settled = false; let settled = false;
const onAbort = () => {
console.log("PoW aborted");
cleanup();
reject(new DOMException("Aborted", "AbortError"));
};
const cleanup = () => { const cleanup = () => {
if (settled) { if (settled) {
return; return;
@ -34,12 +48,6 @@ export default function process(
} }
}; };
const onAbort = () => {
console.log("PoW aborted");
cleanup();
reject(new DOMException("Aborted", "AbortError"));
};
if (signal != null) { if (signal != null) {
if (signal.aborted) { if (signal.aborted) {
return onAbort(); return onAbort();

View file

@ -1,4 +1,4 @@
import fast from "./fast.mjs"; import fast from "./fast";
export default { export default {
fast: fast, fast: fast,

View file

@ -1,20 +1,24 @@
import algorithms from "./algorithms/index.mjs"; import algorithms from "./algorithms";
const defaultDifficulty = 4; const defaultDifficulty = 4;
const status = document.getElementById("status"); const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement;
const difficultyInput = document.getElementById("difficulty-input"); const difficultyInput: HTMLInputElement = document.getElementById("difficulty-input") as HTMLInputElement;
const algorithmSelect = document.getElementById("algorithm-select"); const algorithmSelect: HTMLSelectElement = document.getElementById("algorithm-select") as HTMLSelectElement;
const compareSelect = document.getElementById("compare-select"); const compareSelect: HTMLSelectElement = document.getElementById("compare-select") as HTMLSelectElement;
const header = document.getElementById("table-header"); const header: HTMLTableRowElement = document.getElementById("table-header") as HTMLTableRowElement;
const headerCompare = document.getElementById("table-header-compare"); const headerCompare: HTMLTableSectionElement = document.getElementById("table-header-compare") as HTMLTableSectionElement;
const results = document.getElementById("results"); const results: HTMLTableRowElement = document.getElementById("results") as HTMLTableRowElement;
const setupControls = () => { const setupControls = () => {
difficultyInput.value = defaultDifficulty; if (defaultDifficulty == null) {
return;
}
difficultyInput.value = defaultDifficulty.toString();
for (const alg of Object.keys(algorithms)) { for (const alg of Object.keys(algorithms)) {
const option1 = document.createElement("option"); const option1 = document.createElement("option");
algorithmSelect.append(option1); algorithmSelect?.append(option1);
const option2 = document.createElement("option"); const option2 = document.createElement("option");
compareSelect.append(option2); compareSelect.append(option2);
option1.value = option1.innerText = option2.value = option2.innerText = alg; option1.value = option1.innerText = option2.value = option2.innerText = alg;
@ -116,13 +120,13 @@ const benchmarkLoop = async (controller) => {
await benchmarkLoop(controller); await benchmarkLoop(controller);
}; };
let controller = null; let controller: AbortController | null = null;
const reset = () => { const reset = () => {
stats.time = stats.iters = 0; stats.time = stats.iters = 0;
comparison.time = comparison.iters = 0; comparison.time = comparison.iters = 0;
results.innerHTML = status.innerText = ""; results.innerHTML = status.innerText = "";
const table = results.parentElement; const table = results.parentElement as HTMLElement;
if (compareSelect.value !== "NONE") { if (compareSelect.value !== "NONE") {
table.style.gridTemplateColumns = "repeat(4,auto)"; table.style.gridTemplateColumns = "repeat(4,auto)";
header.style.display = "none"; header.style.display = "none";

View file

@ -1,12 +1,21 @@
import algorithms from "./algorithms/index.mjs"; import algorithms from "./algorithms";
// from Xeact // from Xeact
const u = (url = "", params = {}) => { const u = (url: string = "", params: Record<string, any> = {}) => {
let result = new URL(url, window.location.href); let result = new URL(url, window.location.href);
Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v)); Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v));
return result.toString(); return result.toString();
}; };
const j = (id: string): any | null => {
const elem = document.getElementById(id);
if (elem === null) {
return null;
}
return JSON.parse(elem.textContent);
};
const imageURL = (mood, cacheBuster, basePrefix) => const imageURL = (mood, cacheBuster, basePrefix) =>
u(`${basePrefix}/.within.website/x/cmd/anubis/static/img/${mood}.webp`, { u(`${basePrefix}/.within.website/x/cmd/anubis/static/img/${mood}.webp`, {
cacheBuster, cacheBuster,
@ -14,9 +23,10 @@ const imageURL = (mood, cacheBuster, basePrefix) =>
// Detect available languages by loading the manifest // Detect available languages by loading the manifest
const getAvailableLanguages = async () => { const getAvailableLanguages = async () => {
const basePrefix = JSON.parse( const basePrefix = j("anubis_base_prefix");
document.getElementById("anubis_base_prefix").textContent, if (basePrefix === null) {
); return;
}
try { try {
const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/manifest.json`); const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/manifest.json`);
@ -38,9 +48,11 @@ const getBrowserLanguage = async () =>
// Load translations from JSON files // Load translations from JSON files
const loadTranslations = async (lang) => { const loadTranslations = async (lang) => {
const basePrefix = JSON.parse( const basePrefix = j("anubis_base_prefix");
document.getElementById("anubis_base_prefix").textContent, if (basePrefix === null) {
); return;
}
try { try {
const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`); const response = await fetch(`${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`);
return await response.json(); return await response.json();
@ -54,9 +66,10 @@ const loadTranslations = async (lang) => {
}; };
const getRedirectUrl = () => { const getRedirectUrl = () => {
const publicUrl = JSON.parse( const publicUrl = j("anubis_public_url");
document.getElementById("anubis_public_url").textContent, if (publicUrl === null) {
); return;
}
if (publicUrl && window.location.href.startsWith(publicUrl)) { if (publicUrl && window.location.href.startsWith(publicUrl)) {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('redir'); return urlParams.get('redir');
@ -91,16 +104,14 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
value: navigator.cookieEnabled, value: navigator.cookieEnabled,
}, },
]; ];
const status = document.getElementById("status");
const image = document.getElementById("image"); const status: HTMLParagraphElement = document.getElementById("status") as HTMLParagraphElement;
const title = document.getElementById("title"); const image: HTMLImageElement = document.getElementById("image") as HTMLImageElement;
const progress = document.getElementById("progress"); const title: HTMLHeadingElement = document.getElementById("title") as HTMLHeadingElement;
const anubisVersion = JSON.parse( const progress: HTMLDivElement = document.getElementById("progress") as HTMLDivElement;
document.getElementById("anubis_version").textContent,
); const anubisVersion = j("anubis_version");
const basePrefix = JSON.parse( const basePrefix = j("anubis_base_prefix");
document.getElementById("anubis_base_prefix").textContent,
);
const details = document.querySelector("details"); const details = document.querySelector("details");
let userReadDetails = false; let userReadDetails = false;
@ -132,9 +143,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
} }
} }
const { challenge, rules } = JSON.parse( const { challenge, rules } = j("anubis_challenge");
document.getElementById("anubis_challenge").textContent,
);
const process = algorithms[rules.algorithm]; const process = algorithms[rules.algorithm];
if (!process) { if (!process) {
@ -182,7 +191,9 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
const probability = Math.pow(1 - likelihood, iters); const probability = Math.pow(1 - likelihood, iters);
const distance = (1 - Math.pow(probability, 2)) * 100; const distance = (1 - Math.pow(probability, 2)) * 100;
progress["aria-valuenow"] = distance; progress["aria-valuenow"] = distance;
progress.firstElementChild.style.width = `${distance}%`; if (progress.firstElementChild !== null) {
(progress.firstElementChild as HTMLElement).style.width = `${distance}%`;
}
if (probability < 0.1 && !showingApology) { if (probability < 0.1 && !showingApology) {
status.append( status.append(
@ -197,7 +208,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
console.log({ hash, nonce }); console.log({ hash, nonce });
if (userReadDetails) { if (userReadDetails) {
const container = document.getElementById("progress"); const container: HTMLDivElement = document.getElementById("progress") as HTMLDivElement;
// Style progress bar as a continue button // Style progress bar as a continue button
container.style.display = "flex"; container.style.display = "flex";

View file

@ -6,7 +6,7 @@ const calculateSHA256 = (text) => {
return hash.digest(); return hash.digest();
}; };
function toHexString(arr) { function toHexString(arr: Uint8Array): string {
return Array.from(arr) return Array.from(arr)
.map((c) => c.toString(16).padStart(2, "0")) .map((c) => c.toString(16).padStart(2, "0"))
.join(""); .join("");

View file

@ -1,10 +1,11 @@
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const calculateSHA256 = async (input) => {
const calculateSHA256 = async (input: string) => {
const data = encoder.encode(input); const data = encoder.encode(input);
return await crypto.subtle.digest("SHA-256", data); return await crypto.subtle.digest("SHA-256", data);
}; };
const toHexString = (byteArray) => { const toHexString = (byteArray: Uint8Array) => {
return byteArray.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); return byteArray.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
}; };