diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d9224c7..f7b9752 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,13 +8,18 @@ on:
jobs:
test:
- name: Test and Lint
+ name: Test and Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - name: Setup Node
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ with:
+ node-version: 24.x
+
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
@@ -26,8 +31,11 @@ jobs:
- name: Lint code
run: bun run lint
- - name: Build userscript (test)
+ - name: Run tests
+ run: bun run test
+
+ - name: Build userscript
run: bun run build:userscript
- - name: Build apps (test)
+ - name: Build apps
run: bun run build:apps
diff --git a/.gitignore b/.gitignore
index 443b409..896a6ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@ dist/
.astro/
.wrangler/
+
+.env
diff --git a/apps/icbe/README.md b/apps/icbe/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/apps/icbe/astro.config.ts b/apps/icbe/astro.config.ts
new file mode 100644
index 0000000..d5e5041
--- /dev/null
+++ b/apps/icbe/astro.config.ts
@@ -0,0 +1,29 @@
+import cloudflare from "@astrojs/cloudflare";
+import solid from "@astrojs/solid-js";
+import tailwindcss from "@tailwindcss/vite";
+import { defineConfig, envField } from "astro/config";
+
+export default defineConfig({
+ output: "server",
+ adapter: cloudflare(),
+ integrations: [solid()],
+ vite: { plugins: [tailwindcss()] },
+ site: "https://icbe.rman.dev",
+
+ env: {
+ schema: {
+ GITLAB_ACCESS_TOKEN: envField.string({
+ context: "server",
+ access: "secret",
+ startsWith: "glpat",
+ length: 26,
+ }),
+ GITLAB_PROJECT_ID: envField.number({
+ context: "server",
+ access: "secret",
+ int: true,
+ gt: 1,
+ }),
+ },
+ },
+});
diff --git a/apps/icbe/package.json b/apps/icbe/package.json
index 46e648f..7a1753f 100644
--- a/apps/icbe/package.json
+++ b/apps/icbe/package.json
@@ -2,13 +2,28 @@
"name": "web-app",
"type": "module",
"private": true,
+ "imports": {
+ "#styles": "./src/styles/global.css",
+ "#layout": "./src/layouts/Layout.astro",
+ "#schemas": "./src/schemas/index.ts",
+ "#utils/*": "./src/utils/*.ts",
+ "#components/*": "./src/components/*.tsx"
+ },
"scripts": {
+ "typegen": "wrangler types ./src/worker-configuration.d.ts",
+ "test": "bun test",
"build": "astro build",
"deploy": "wrangler deploy",
"dev": "astro dev"
},
"dependencies": {
- "astro": "catalog:"
+ "@astrojs/cloudflare": "^13.1.3",
+ "@astrojs/solid-js": "^6.0.1",
+ "@gitbeaker/rest": "^43.8.0",
+ "@tailwindcss/vite": "catalog:",
+ "astro": "catalog:",
+ "solid-js": "^1.9.11",
+ "tailwindcss": "catalog:"
},
"devDependencies": {
"wrangler": "catalog:"
diff --git a/apps/icbe/public/.assetsignore b/apps/icbe/public/.assetsignore
new file mode 100644
index 0000000..b7ccb6c
--- /dev/null
+++ b/apps/icbe/public/.assetsignore
@@ -0,0 +1,2 @@
+_worker.js
+_routes.json
diff --git a/apps/icbe/src/actions/index.ts b/apps/icbe/src/actions/index.ts
new file mode 100644
index 0000000..18801f5
--- /dev/null
+++ b/apps/icbe/src/actions/index.ts
@@ -0,0 +1,8 @@
+import {
+ submitDisprovenElements,
+ submitProvenElements,
+} from "./submit-elements";
+
+export const server = {
+ elements: { submitDisprovenElements, submitProvenElements },
+};
diff --git a/apps/icbe/src/actions/submit-elements.ts b/apps/icbe/src/actions/submit-elements.ts
new file mode 100644
index 0000000..81c0a83
--- /dev/null
+++ b/apps/icbe/src/actions/submit-elements.ts
@@ -0,0 +1,65 @@
+import { ActionError, defineAction } from "astro:actions";
+import * as z from "astro/zod";
+import { DataProvenSchema } from "#schemas";
+import { saveToCloud } from "#utils/gitlab";
+import { getDisprovenToSave, getProvenToSave } from "#utils/submit-elements";
+
+const submitDisprovenElements = defineAction({
+ input: z.object({ elements: z.array(z.object({ name: z.string() })) }),
+ async handler(input, context) {
+ const role = null;
+
+ if (!role) {
+ throw new ActionError({
+ code: "UNAUTHORIZED",
+ message: "Please login to use this feature.",
+ });
+ }
+
+ const canSubmitElements = false;
+
+ if (!canSubmitElements) {
+ throw new ActionError({
+ code: "FORBIDDEN",
+ message: "You don't have permissions to use this feature.",
+ });
+ }
+
+ const elements = input.elements.map((element) => element.name);
+
+ const data = await getDisprovenToSave(elements);
+ await saveToCloud(data);
+
+ return { success: true } as const;
+ },
+});
+
+const submitProvenElements = defineAction({
+ input: z.object({ elements: DataProvenSchema }),
+ handler: async (input, context) => {
+ const role = null;
+
+ if (!role) {
+ throw new ActionError({
+ code: "UNAUTHORIZED",
+ message: "Please login to use this feature.",
+ });
+ }
+
+ const canSubmitElements = false;
+
+ if (!canSubmitElements) {
+ throw new ActionError({
+ code: "FORBIDDEN",
+ message: "You don't have permissions to use this feature.",
+ });
+ }
+
+ const data = await getProvenToSave(input.elements);
+ await saveToCloud(data);
+
+ return { success: true } as const;
+ },
+});
+
+export { submitDisprovenElements, submitProvenElements };
diff --git a/apps/icbe/src/components/AdminDashboard.tsx b/apps/icbe/src/components/AdminDashboard.tsx
new file mode 100644
index 0000000..ac2a318
--- /dev/null
+++ b/apps/icbe/src/components/AdminDashboard.tsx
@@ -0,0 +1,11 @@
+import type { JSX } from "solid-js";
+
+export default function AdminDashboard(): JSX.Element {
+ return (
+
+
+ Admin Controls
+
+
+ );
+}
diff --git a/apps/icbe/src/components/UserDashboard.tsx b/apps/icbe/src/components/UserDashboard.tsx
new file mode 100644
index 0000000..946a347
--- /dev/null
+++ b/apps/icbe/src/components/UserDashboard.tsx
@@ -0,0 +1,40 @@
+import { type JSX, Show } from "solid-js";
+
+import SubmitDisprovenElementsSection from "./dashboard/SubmitDisprovenElementsSection";
+import SubmitProvenElementsSection from "./dashboard/SubmitProvenElementsSection";
+import Divider from "./ui/Divider";
+import Link from "./ui/Link";
+
+const TopBar = (): JSX.Element => (
+
+
+ User Dashboard
+
+
+);
+
+export default function UserDashboard(props: {
+ canSubmitElements: boolean;
+}): JSX.Element {
+ const isAdmin = false;
+
+ return (
+
+ );
+}
diff --git a/apps/icbe/src/components/dashboard/SubmitDisprovenElementsSection.tsx b/apps/icbe/src/components/dashboard/SubmitDisprovenElementsSection.tsx
new file mode 100644
index 0000000..52dd14e
--- /dev/null
+++ b/apps/icbe/src/components/dashboard/SubmitDisprovenElementsSection.tsx
@@ -0,0 +1,97 @@
+import { actions } from "astro:actions";
+import type { JSX } from "solid-js";
+import { createSignal, Show } from "solid-js";
+import Button from "#components/ui/Button";
+
+type SubmitState =
+ | { status: "not-started" }
+ | { status: "submitting" }
+ | { status: "complete"; success: boolean }
+ | { status: "error"; error?: string };
+
+const SubmitDisprovenElementsResultContainer = (props: {
+ state: SubmitState;
+}) => (
+
+ {(state) => (
+
+ {state.success
+ ? "Elements submitted successfully!"
+ : "Elements submission failed."}
+
+ )}
+
+);
+
+function SubmitDisprovenElementsSection(): JSX.Element {
+ const [elementsToBeSubmitted, setElementsToBeSubmitted] = createSignal<
+ string[]
+ >([]);
+ const [submitState, setSubmitState] = createSignal({
+ status: "not-started",
+ });
+
+ const handleSubmit = async () => {
+ try {
+ setSubmitState({ status: "submitting" });
+ const disprovenElements = elementsToBeSubmitted().map((name) => ({
+ name,
+ }));
+ const result = await actions.elements.submitDisprovenElements({
+ elements: disprovenElements,
+ });
+ if (result.error) {
+ console.error("Submit failed:", result.error);
+ setSubmitState({ status: "error", error: result.error.message });
+ } else {
+ setSubmitState({ status: "complete", success: result.data.success });
+ }
+ } catch (err) {
+ console.error("Submit error:", err);
+ setSubmitState({ status: "error" });
+ }
+ };
+
+ return (
+
+
+ );
+}
+
+export default SubmitDisprovenElementsSection;
diff --git a/apps/icbe/src/components/dashboard/SubmitProvenElementsSection.tsx b/apps/icbe/src/components/dashboard/SubmitProvenElementsSection.tsx
new file mode 100644
index 0000000..c170312
--- /dev/null
+++ b/apps/icbe/src/components/dashboard/SubmitProvenElementsSection.tsx
@@ -0,0 +1,159 @@
+import { actions } from "astro:actions";
+import type { JSX, Setter } from "solid-js";
+import { createSignal, Show } from "solid-js";
+import Button from "#components/ui/Button";
+import type { DataProven, DataProvenEntry } from "#schemas";
+
+type SubmitState =
+ | { status: "not-started" }
+ | { status: "submitting" }
+ | { status: "complete"; success: boolean }
+ | { status: "error"; error?: string };
+
+const BASE_ELEMENTS = ["earth", "water", "fire", "wind"] as const;
+type BASE_ELEMENT = (typeof BASE_ELEMENTS)[number];
+
+const SubmitProvenElementsResultContainer = (props: { state: SubmitState }) => (
+
+ {(state) => (
+
+ {state.success
+ ? "Elements submitted successfully!"
+ : "Elements submission failed."}
+
+ )}
+
+);
+
+const SubmitProvenElemenStepCountAndProof = (props: {
+ baseElement: BASE_ELEMENT;
+ element(): DataProvenEntry;
+ setElement: Setter;
+}) => (
+
+);
+
+const SubmitProvenElementsForm = (props: {
+ element: () => DataProvenEntry;
+ setElement: Setter;
+}) => (
+
+
+ props.setElement((prev) => ({ ...prev, name: e.currentTarget.value }))
+ }
+ placeholder="Element name"
+ class="flex-1 p-2 border rounded-md dark:bg-gray-800 dark:border-gray-700"
+ />
+
+
+
+
+
+
+);
+
+function SubmitProvenElementsSection(): JSX.Element {
+ const [elementToBeSubmitted, setElementToBeSubmitted] =
+ createSignal({ name: "" });
+ const [submitState, setSubmitState] = createSignal({
+ status: "not-started",
+ });
+
+ const handleSubmit = async () => {
+ const elements: DataProven = [elementToBeSubmitted()];
+
+ try {
+ setSubmitState({ status: "submitting" });
+ const result = await actions.elements.submitProvenElements({ elements });
+ if (result.error) {
+ console.error("Submit failed:", result.error);
+ setSubmitState({ status: "error", error: result.error.message });
+ } else {
+ setSubmitState({ status: "complete", success: result.data.success });
+ }
+ } catch (err) {
+ console.error("Submit error:", err);
+ setSubmitState({ status: "error" });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export default SubmitProvenElementsSection;
diff --git a/apps/icbe/src/components/ui/Button.tsx b/apps/icbe/src/components/ui/Button.tsx
new file mode 100644
index 0000000..f3d295f
--- /dev/null
+++ b/apps/icbe/src/components/ui/Button.tsx
@@ -0,0 +1,50 @@
+import type { JSX, JSXElement } from "solid-js";
+import { splitProps } from "solid-js";
+
+type ButtonAttributes = JSX.ButtonHTMLAttributes;
+
+type LabeledOrNot =
+ | { label: string; children?: null }
+ | { label?: null; children?: JSXElement };
+
+const baseStyles = `px-4 py-2 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer`;
+
+const colors = {
+ default: `bg-gray-600 text-white enabled:hover:bg-gray-700 focus:ring-gray-500`,
+ purple: `bg-indigo-600 text-white enabled:hover:bg-indigo-700 focus:ring-indigo-500`,
+ red: `bg-red-500 text-white enabled:hover:bg-red-600`,
+};
+
+const variants = {
+ default: "",
+ bold: "font-medium",
+};
+
+type Color = keyof typeof colors;
+type Variant = keyof typeof variants;
+
+type ButtonProps = (ButtonAttributes & LabeledOrNot) & {
+ color?: Color;
+ variant?: Variant;
+};
+
+function Button(props: ButtonProps) {
+ const [local, rest] = splitProps(props, [
+ "label",
+ "class",
+ "color",
+ "variant",
+ "children",
+ ]);
+
+ return (
+
+ );
+}
+
+export default Button;
diff --git a/apps/icbe/src/components/ui/Divider.tsx b/apps/icbe/src/components/ui/Divider.tsx
new file mode 100644
index 0000000..a2e1b4a
--- /dev/null
+++ b/apps/icbe/src/components/ui/Divider.tsx
@@ -0,0 +1,5 @@
+const Divider = () => (
+
+);
+
+export default Divider;
diff --git a/apps/icbe/src/components/ui/Link.tsx b/apps/icbe/src/components/ui/Link.tsx
new file mode 100644
index 0000000..3c80e47
--- /dev/null
+++ b/apps/icbe/src/components/ui/Link.tsx
@@ -0,0 +1,48 @@
+import type { JSX, JSXElement } from "solid-js";
+import { splitProps } from "solid-js";
+
+type AnchorAttributes = JSX.AnchorHTMLAttributes;
+
+type LabeledOrNot =
+ | { label: string; children?: null }
+ | { label?: null; children?: JSXElement };
+
+const baseStyles = `px-4 py-2 rounded-lg`;
+
+const colors = {
+ default: ``,
+ blue: `bg-blue-600 dark:bg-blue-500 text-white hover:bg-blue-700 dark:hover:bg-blue-600`,
+};
+
+const variants = {
+ default: "",
+};
+
+type Color = keyof typeof colors;
+type Variant = keyof typeof variants;
+
+type LinkProps = (AnchorAttributes & LabeledOrNot) & {
+ color?: Color;
+ variant?: Variant;
+};
+
+function Link(props: LinkProps) {
+ const [local, rest] = splitProps(props, [
+ "label",
+ "class",
+ "color",
+ "variant",
+ "children",
+ ]);
+
+ return (
+
+ {local.label ?? local.children}
+
+ );
+}
+
+export default Link;
diff --git a/apps/icbe/src/layouts/Layout.astro b/apps/icbe/src/layouts/Layout.astro
new file mode 100644
index 0000000..ad6ea26
--- /dev/null
+++ b/apps/icbe/src/layouts/Layout.astro
@@ -0,0 +1,39 @@
+---
+import "#styles";
+
+interface Props {
+ title: string | null;
+ description: string | null;
+ canonical: string | null;
+ children: unknown;
+}
+
+const { title, description, canonical } = Astro.props;
+---
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/icbe/src/pages/admin/dashboard.astro b/apps/icbe/src/pages/admin/dashboard.astro
new file mode 100644
index 0000000..c4df600
--- /dev/null
+++ b/apps/icbe/src/pages/admin/dashboard.astro
@@ -0,0 +1,16 @@
+---
+import AdminDashboard from "#components/AdminDashboard";
+import Layout from "#layout";
+
+if (!Math.random()) {
+ return Astro.redirect("/");
+}
+
+const title = null;
+const description = null;
+const canonical = null;
+---
+
+
+
+
diff --git a/apps/icbe/src/pages/api/get-elements.ts b/apps/icbe/src/pages/api/get-elements.ts
new file mode 100644
index 0000000..3f1ee54
--- /dev/null
+++ b/apps/icbe/src/pages/api/get-elements.ts
@@ -0,0 +1,20 @@
+import type { APIRoute } from "astro";
+import { getDisprovenData, getProvenData } from "#utils/gitlab";
+import { respondWithJSON } from "#utils/respond-json";
+
+export const GET: APIRoute = async () => {
+ const [provenData, disprovenData] = await Promise.all([
+ getProvenData(),
+ getDisprovenData(),
+ ]);
+
+ const data = {
+ proven: provenData.map((x) => x.name),
+ disproven: disprovenData,
+ } as const;
+
+ return respondWithJSON<{
+ readonly proven: string[];
+ readonly disproven: string[];
+ }>(data).response;
+};
diff --git a/apps/icbe/src/pages/dashboard/index.astro b/apps/icbe/src/pages/dashboard/index.astro
new file mode 100644
index 0000000..86ace6e
--- /dev/null
+++ b/apps/icbe/src/pages/dashboard/index.astro
@@ -0,0 +1,18 @@
+---
+import UserDashboard from "#components/UserDashboard";
+import Layout from "#layout";
+
+if (!Math.random()) {
+ return Astro.redirect("/");
+}
+
+const canSubmitElements = false;
+
+const title = null;
+const description = null;
+const canonical = null;
+---
+
+
+
+
diff --git a/apps/icbe/src/pages/index.astro b/apps/icbe/src/pages/index.astro
new file mode 100644
index 0000000..d987258
--- /dev/null
+++ b/apps/icbe/src/pages/index.astro
@@ -0,0 +1,22 @@
+---
+export const prerender = true;
+
+import Layout from "#layout";
+
+const title = null;
+const description = null;
+const canonical = null;
+---
+
+
+
+
+
+ Infinite Craft Base Elements
+
+
+
+
+
diff --git a/apps/icbe/src/schemas/data/disproven.ts b/apps/icbe/src/schemas/data/disproven.ts
new file mode 100644
index 0000000..5e1a77e
--- /dev/null
+++ b/apps/icbe/src/schemas/data/disproven.ts
@@ -0,0 +1,6 @@
+import * as z from "astro/zod";
+
+const DataDisprovenSchema = z.array(z.string());
+type DataDisproven = z.infer;
+
+export { type DataDisproven, DataDisprovenSchema };
diff --git a/apps/icbe/src/schemas/data/history.ts b/apps/icbe/src/schemas/data/history.ts
new file mode 100644
index 0000000..c644f7c
--- /dev/null
+++ b/apps/icbe/src/schemas/data/history.ts
@@ -0,0 +1,22 @@
+import * as z from "astro/zod";
+
+const ElementsHistoryEntrySchema = z
+ .object({
+ timestamp: z.number(),
+ "iso-timestamp": z.iso.datetime(),
+ proven: z.nullable(z.number()),
+ disproven: z.nullable(z.number()),
+ })
+ .readonly();
+
+const ElementsHistorySchema = z.array(ElementsHistoryEntrySchema);
+
+type ElementsHistoryEntry = z.infer;
+
+type ElementsHistory = z.infer;
+
+export {
+ type ElementsHistory,
+ type ElementsHistoryEntry,
+ ElementsHistorySchema,
+};
diff --git a/apps/icbe/src/schemas/data/index.ts b/apps/icbe/src/schemas/data/index.ts
new file mode 100644
index 0000000..5330e08
--- /dev/null
+++ b/apps/icbe/src/schemas/data/index.ts
@@ -0,0 +1,16 @@
+import { DataDisprovenSchema } from "./disproven";
+import { ElementsHistorySchema } from "./history";
+import { MetadataSchema } from "./metadata";
+import { DataProvenSchema } from "./proven";
+
+export * from "./disproven";
+export * from "./history";
+export * from "./metadata";
+export * from "./proven";
+
+export const schemas = {
+ meta: MetadataSchema,
+ history: ElementsHistorySchema,
+ proven: DataProvenSchema,
+ disproven: DataDisprovenSchema,
+} as const;
diff --git a/apps/icbe/src/schemas/data/metadata.ts b/apps/icbe/src/schemas/data/metadata.ts
new file mode 100644
index 0000000..901ba4a
--- /dev/null
+++ b/apps/icbe/src/schemas/data/metadata.ts
@@ -0,0 +1,13 @@
+import * as z from "astro/zod";
+
+const MetadataSchema = z
+ .object({
+ lastUpdated: z.object({ "unix-timestamp": z.number() }).readonly(),
+ proven: z.object({ count: z.number() }).readonly(),
+ disproven: z.object({ count: z.number() }).readonly(),
+ })
+ .readonly();
+
+type Metadata = z.infer;
+
+export { type Metadata, MetadataSchema };
diff --git a/apps/icbe/src/schemas/data/proven.ts b/apps/icbe/src/schemas/data/proven.ts
new file mode 100644
index 0000000..c4ec96a
--- /dev/null
+++ b/apps/icbe/src/schemas/data/proven.ts
@@ -0,0 +1,25 @@
+import * as z from "astro/zod";
+
+const ProvenToBaseElementInfoSchema = z
+ .object({
+ steps: z.number(),
+ proof: z.object({ url: z.url() }),
+ })
+ .partial();
+
+const DataProvenEntrySchema = z.object({
+ name: z.string(),
+ earth: z.optional(ProvenToBaseElementInfoSchema),
+ water: z.optional(ProvenToBaseElementInfoSchema),
+ fire: z.optional(ProvenToBaseElementInfoSchema),
+ wind: z.optional(ProvenToBaseElementInfoSchema),
+ all: z.optional(ProvenToBaseElementInfoSchema),
+});
+
+const DataProvenSchema = z.array(DataProvenEntrySchema);
+
+type DataProvenEntry = z.infer;
+
+type DataProven = z.infer;
+
+export { type DataProven, type DataProvenEntry, DataProvenSchema };
diff --git a/apps/icbe/src/schemas/index.ts b/apps/icbe/src/schemas/index.ts
new file mode 100644
index 0000000..ff7ba27
--- /dev/null
+++ b/apps/icbe/src/schemas/index.ts
@@ -0,0 +1 @@
+export * from "./data";
diff --git a/apps/icbe/src/styles/global.css b/apps/icbe/src/styles/global.css
new file mode 100644
index 0000000..f1d8c73
--- /dev/null
+++ b/apps/icbe/src/styles/global.css
@@ -0,0 +1 @@
+@import "tailwindcss";
diff --git a/apps/icbe/src/utils/gitlab.ts b/apps/icbe/src/utils/gitlab.ts
new file mode 100644
index 0000000..aa81893
--- /dev/null
+++ b/apps/icbe/src/utils/gitlab.ts
@@ -0,0 +1,82 @@
+import { GITLAB_ACCESS_TOKEN, GITLAB_PROJECT_ID } from "astro:env/server";
+import { type CommitAction, Gitlab } from "@gitbeaker/rest";
+import type * as z from "astro/zod";
+import {
+ type DataDisproven,
+ type DataProven,
+ type ElementsHistory,
+ type Metadata,
+ schemas,
+} from "#schemas";
+
+const BRANCH = "main" as const;
+const BASE_DATA_DIR = "base-elements/data" as const;
+
+const api = new Gitlab({ token: GITLAB_ACCESS_TOKEN });
+
+type SchemaMap = typeof schemas;
+type SchemaKey = keyof SchemaMap;
+type OutputSchema = z.infer;
+
+async function getGitlabFileData(
+ key: TKey,
+): Promise> {
+ const fileContent = await api.RepositoryFiles.showRaw(
+ GITLAB_PROJECT_ID,
+ `${BASE_DATA_DIR}/${key}.json`,
+ BRANCH,
+ );
+
+ const data: unknown = JSON.parse(fileContent as string);
+ return schemas[key].parse(data) as OutputSchema;
+}
+
+export async function getElementsMetadata(): Promise {
+ const data = getGitlabFileData("meta");
+ return data;
+}
+
+export async function getElementsHistory(): Promise {
+ const data = getGitlabFileData("history");
+ return data;
+}
+
+export async function getDisprovenData(): Promise {
+ const data = getGitlabFileData("disproven");
+ return data;
+}
+
+export async function getProvenData(): Promise {
+ const data = getGitlabFileData("proven");
+ return data;
+}
+
+function getUpdateAction(
+ type: TKey,
+ data: OutputSchema,
+): CommitAction {
+ const filePath = `${BASE_DATA_DIR}/${type}.json`;
+ const stringifiedData = JSON.stringify(data, null, 2);
+ const content = `${stringifiedData}\n`;
+ return { action: "update", filePath, content };
+}
+
+export interface SaveToCloudData {
+ elements:
+ | { type: "proven"; data: DataProven }
+ | { type: "disproven"; data: DataDisproven };
+ metadata: Metadata;
+ history: ElementsHistory;
+}
+
+export async function saveToCloud(data: SaveToCloudData) {
+ const actions: CommitAction[] = [
+ getUpdateAction(data.elements.type, data.elements.data),
+ getUpdateAction("meta", data.metadata),
+ getUpdateAction("history", data.history),
+ ];
+
+ const message = "Updated data";
+
+ await api.Commits.create(GITLAB_PROJECT_ID, BRANCH, message, actions);
+}
diff --git a/apps/icbe/src/utils/merge-data.ts b/apps/icbe/src/utils/merge-data.ts
new file mode 100644
index 0000000..705b53c
--- /dev/null
+++ b/apps/icbe/src/utils/merge-data.ts
@@ -0,0 +1,42 @@
+import type { DataProven, DataProvenEntry } from "#schemas";
+
+function mergeOne(
+ existing: DataProvenEntry,
+ item: DataProvenEntry,
+ key: "earth" | "water" | "fire" | "wind" | "all",
+): Record {
+ const existingVal = existing[key];
+ const itemVal = item[key];
+
+ if (!existingVal && !itemVal) return {};
+
+ return { [key]: { ...existingVal, ...itemVal } };
+}
+
+export function mergeProvenData(
+ existing: DataProven,
+ elements: DataProven,
+): DataProven {
+ const combinedMap = new Map();
+
+ for (const item of [...existing, ...elements]) {
+ const existingItem = combinedMap.get(item.name);
+
+ if (existingItem) {
+ combinedMap.set(item.name, {
+ ...existingItem,
+ ...item,
+ // Only include the key if it exists in either object
+ ...mergeOne(existingItem, item, "earth"),
+ ...mergeOne(existingItem, item, "water"),
+ ...mergeOne(existingItem, item, "fire"),
+ ...mergeOne(existingItem, item, "wind"),
+ ...mergeOne(existingItem, item, "all"),
+ });
+ } else {
+ combinedMap.set(item.name, item);
+ }
+ }
+
+ return Array.from(combinedMap.values());
+}
diff --git a/apps/icbe/src/utils/respond-json.ts b/apps/icbe/src/utils/respond-json.ts
new file mode 100644
index 0000000..87c937b
--- /dev/null
+++ b/apps/icbe/src/utils/respond-json.ts
@@ -0,0 +1,11 @@
+type JsonLike = Record;
+
+export function respondWithJSON(
+ data: T,
+): { response: Response; data: T } {
+ const response = new Response(JSON.stringify(data), {
+ headers: { "Content-Type": "application/json" },
+ status: 200,
+ });
+ return { response, data };
+}
diff --git a/apps/icbe/src/utils/submit-elements.ts b/apps/icbe/src/utils/submit-elements.ts
new file mode 100644
index 0000000..f2c73e2
--- /dev/null
+++ b/apps/icbe/src/utils/submit-elements.ts
@@ -0,0 +1,93 @@
+import type {
+ DataDisproven,
+ DataProven,
+ ElementsHistory,
+ ElementsHistoryEntry,
+ Metadata,
+} from "#schemas";
+import {
+ getDisprovenData,
+ getElementsHistory,
+ getProvenData,
+ type SaveToCloudData,
+} from "./gitlab";
+import { mergeProvenData } from "./merge-data";
+
+function getUpdatedMetaInfo(
+ oldHistory: ElementsHistory,
+ elements: { type: T; count: number },
+): { metadata: Metadata; history: ElementsHistory } {
+ const unixTimestamp = Date.now();
+ const isoTimestamp = new Date(unixTimestamp).toISOString();
+
+ const lastEntry = oldHistory.at(-1);
+
+ const counts =
+ elements.type === "proven"
+ ? { proven: elements.count, disproven: lastEntry?.disproven ?? null }
+ : { proven: lastEntry?.proven ?? null, disproven: elements.count };
+
+ const newHistoryEntry: ElementsHistoryEntry = {
+ timestamp: unixTimestamp,
+ "iso-timestamp": isoTimestamp,
+ proven: counts.proven,
+ disproven: counts.disproven,
+ };
+
+ const history: ElementsHistory = [...oldHistory, newHistoryEntry];
+
+ const metadata: Metadata = {
+ lastUpdated: { "unix-timestamp": unixTimestamp },
+ proven: { count: counts.proven ?? 0 },
+ disproven: { count: counts.disproven ?? 0 },
+ };
+ return { metadata, history };
+}
+
+export async function getProvenToSave(
+ elements: DataProven,
+): Promise {
+ const [existing, oldHistory] = await Promise.all([
+ getProvenData(),
+ getElementsHistory(),
+ ]);
+
+ const combined = mergeProvenData(existing, elements);
+
+ const { metadata, history } = getUpdatedMetaInfo(oldHistory, {
+ type: "proven",
+ count: combined.length,
+ });
+
+ const dataToSave: SaveToCloudData = {
+ elements: { type: "proven", data: combined },
+ history,
+ metadata,
+ };
+
+ return dataToSave;
+}
+
+export async function getDisprovenToSave(
+ elements: DataDisproven,
+): Promise {
+ const [existing, oldHistory] = await Promise.all([
+ getDisprovenData(),
+ getElementsHistory(),
+ ]);
+
+ const combined = [...new Set([...existing, ...elements])];
+
+ const { metadata, history } = getUpdatedMetaInfo(oldHistory, {
+ type: "disproven",
+ count: combined.length,
+ });
+
+ const dataToSave: SaveToCloudData = {
+ elements: { type: "disproven", data: combined },
+ history,
+ metadata,
+ };
+
+ return dataToSave;
+}
diff --git a/apps/icbe/src/worker-configuration.d.ts b/apps/icbe/src/worker-configuration.d.ts
new file mode 100644
index 0000000..a0b3d76
--- /dev/null
+++ b/apps/icbe/src/worker-configuration.d.ts
@@ -0,0 +1,13623 @@
+/* eslint-disable */
+// Generated by Wrangler by running `wrangler types ./src/worker-configuration.d.ts` (hash: 316bc669d48ec7820de95225fe2274a3)
+// Runtime types generated with workerd@1.20260317.1 2026-03-17 nodejs_compat
+declare namespace Cloudflare {
+ interface Env {
+ GITLAB_ACCESS_TOKEN: string;
+ GITLAB_PROJECT_ID: string;
+ }
+}
+interface Env extends Cloudflare.Env {}
+type StringifyValues> = {
+ [Binding in keyof EnvType]: EnvType[Binding] extends string
+ ? EnvType[Binding]
+ : string;
+};
+declare namespace NodeJS {
+ interface ProcessEnv
+ extends StringifyValues<
+ Pick
+ > {}
+}
+
+// Begin runtime types
+/*! *****************************************************************************
+Copyright (c) Cloudflare. All rights reserved.
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at http://www.apache.org/licenses/LICENSE-2.0
+THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+MERCHANTABLITY OR NON-INFRINGEMENT.
+See the Apache Version 2.0 License for specific language governing permissions
+and limitations under the License.
+***************************************************************************** */
+/* eslint-disable */
+// noinspection JSUnusedGlobalSymbols
+declare var onmessage: never;
+/**
+ * The **`DOMException`** interface represents an abnormal event (called an **exception**) that occurs as a result of calling a method or accessing a property of a web API.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException)
+ */
+declare class DOMException extends Error {
+ constructor(message?: string, name?: string);
+ /**
+ * The **`message`** read-only property of the a message or description associated with the given error name.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message)
+ */
+ readonly message: string;
+ /**
+ * The **`name`** read-only property of the one of the strings associated with an error name.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name)
+ */
+ readonly name: string;
+ /**
+ * The **`code`** read-only property of the DOMException interface returns one of the legacy error code constants, or `0` if none match.
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code)
+ */
+ readonly code: number;
+ static readonly INDEX_SIZE_ERR: number;
+ static readonly DOMSTRING_SIZE_ERR: number;
+ static readonly HIERARCHY_REQUEST_ERR: number;
+ static readonly WRONG_DOCUMENT_ERR: number;
+ static readonly INVALID_CHARACTER_ERR: number;
+ static readonly NO_DATA_ALLOWED_ERR: number;
+ static readonly NO_MODIFICATION_ALLOWED_ERR: number;
+ static readonly NOT_FOUND_ERR: number;
+ static readonly NOT_SUPPORTED_ERR: number;
+ static readonly INUSE_ATTRIBUTE_ERR: number;
+ static readonly INVALID_STATE_ERR: number;
+ static readonly SYNTAX_ERR: number;
+ static readonly INVALID_MODIFICATION_ERR: number;
+ static readonly NAMESPACE_ERR: number;
+ static readonly INVALID_ACCESS_ERR: number;
+ static readonly VALIDATION_ERR: number;
+ static readonly TYPE_MISMATCH_ERR: number;
+ static readonly SECURITY_ERR: number;
+ static readonly NETWORK_ERR: number;
+ static readonly ABORT_ERR: number;
+ static readonly URL_MISMATCH_ERR: number;
+ static readonly QUOTA_EXCEEDED_ERR: number;
+ static readonly TIMEOUT_ERR: number;
+ static readonly INVALID_NODE_TYPE_ERR: number;
+ static readonly DATA_CLONE_ERR: number;
+ get stack(): any;
+ set stack(value: any);
+}
+type WorkerGlobalScopeEventMap = {
+ fetch: FetchEvent;
+ scheduled: ScheduledEvent;
+ queue: QueueEvent;
+ unhandledrejection: PromiseRejectionEvent;
+ rejectionhandled: PromiseRejectionEvent;
+};
+declare abstract class WorkerGlobalScope extends EventTarget {
+ EventTarget: typeof EventTarget;
+}
+/* The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). *
+ * The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox).
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console)
+ */
+interface Console {
+ assert(condition?: boolean, ...data: any[]): void;
+ /**
+ * The **`console.clear()`** static method clears the console if possible.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static)
+ */
+ clear(): void;
+ /**
+ * The **`console.count()`** static method logs the number of times that this particular call to `count()` has been called.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static)
+ */
+ count(label?: string): void;
+ /**
+ * The **`console.countReset()`** static method resets counter used with console/count_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static)
+ */
+ countReset(label?: string): void;
+ /**
+ * The **`console.debug()`** static method outputs a message to the console at the 'debug' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static)
+ */
+ debug(...data: any[]): void;
+ /**
+ * The **`console.dir()`** static method displays a list of the properties of the specified JavaScript object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static)
+ */
+ dir(item?: any, options?: any): void;
+ /**
+ * The **`console.dirxml()`** static method displays an interactive tree of the descendant elements of the specified XML/HTML element.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static)
+ */
+ dirxml(...data: any[]): void;
+ /**
+ * The **`console.error()`** static method outputs a message to the console at the 'error' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static)
+ */
+ error(...data: any[]): void;
+ /**
+ * The **`console.group()`** static method creates a new inline group in the Web console log, causing any subsequent console messages to be indented by an additional level, until console/groupEnd_static is called.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static)
+ */
+ group(...data: any[]): void;
+ /**
+ * The **`console.groupCollapsed()`** static method creates a new inline group in the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static)
+ */
+ groupCollapsed(...data: any[]): void;
+ /**
+ * The **`console.groupEnd()`** static method exits the current inline group in the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static)
+ */
+ groupEnd(): void;
+ /**
+ * The **`console.info()`** static method outputs a message to the console at the 'info' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static)
+ */
+ info(...data: any[]): void;
+ /**
+ * The **`console.log()`** static method outputs a message to the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
+ */
+ log(...data: any[]): void;
+ /**
+ * The **`console.table()`** static method displays tabular data as a table.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static)
+ */
+ table(tabularData?: any, properties?: string[]): void;
+ /**
+ * The **`console.time()`** static method starts a timer you can use to track how long an operation takes.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static)
+ */
+ time(label?: string): void;
+ /**
+ * The **`console.timeEnd()`** static method stops a timer that was previously started by calling console/time_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static)
+ */
+ timeEnd(label?: string): void;
+ /**
+ * The **`console.timeLog()`** static method logs the current value of a timer that was previously started by calling console/time_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static)
+ */
+ timeLog(label?: string, ...data: any[]): void;
+ timeStamp(label?: string): void;
+ /**
+ * The **`console.trace()`** static method outputs a stack trace to the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static)
+ */
+ trace(...data: any[]): void;
+ /**
+ * The **`console.warn()`** static method outputs a warning message to the console at the 'warning' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static)
+ */
+ warn(...data: any[]): void;
+}
+declare const console: Console;
+type BufferSource = ArrayBufferView | ArrayBuffer;
+type TypedArray =
+ | Int8Array
+ | Uint8Array
+ | Uint8ClampedArray
+ | Int16Array
+ | Uint16Array
+ | Int32Array
+ | Uint32Array
+ | Float32Array
+ | Float64Array
+ | BigInt64Array
+ | BigUint64Array;
+declare namespace WebAssembly {
+ class CompileError extends Error {
+ constructor(message?: string);
+ }
+ class RuntimeError extends Error {
+ constructor(message?: string);
+ }
+ type ValueType =
+ | "anyfunc"
+ | "externref"
+ | "f32"
+ | "f64"
+ | "i32"
+ | "i64"
+ | "v128";
+ interface GlobalDescriptor {
+ value: ValueType;
+ mutable?: boolean;
+ }
+ class Global {
+ constructor(descriptor: GlobalDescriptor, value?: any);
+ value: any;
+ valueOf(): any;
+ }
+ type ImportValue = ExportValue | number;
+ type ModuleImports = Record;
+ type Imports = Record;
+ type ExportValue = Function | Global | Memory | Table;
+ type Exports = Record;
+ class Instance {
+ constructor(module: Module, imports?: Imports);
+ readonly exports: Exports;
+ }
+ interface MemoryDescriptor {
+ initial: number;
+ maximum?: number;
+ shared?: boolean;
+ }
+ class Memory {
+ constructor(descriptor: MemoryDescriptor);
+ readonly buffer: ArrayBuffer;
+ grow(delta: number): number;
+ }
+ type ImportExportKind = "function" | "global" | "memory" | "table";
+ interface ModuleExportDescriptor {
+ kind: ImportExportKind;
+ name: string;
+ }
+ interface ModuleImportDescriptor {
+ kind: ImportExportKind;
+ module: string;
+ name: string;
+ }
+ abstract class Module {
+ static customSections(module: Module, sectionName: string): ArrayBuffer[];
+ static exports(module: Module): ModuleExportDescriptor[];
+ static imports(module: Module): ModuleImportDescriptor[];
+ }
+ type TableKind = "anyfunc" | "externref";
+ interface TableDescriptor {
+ element: TableKind;
+ initial: number;
+ maximum?: number;
+ }
+ class Table {
+ constructor(descriptor: TableDescriptor, value?: any);
+ readonly length: number;
+ get(index: number): any;
+ grow(delta: number, value?: any): number;
+ set(index: number, value?: any): void;
+ }
+ function instantiate(module: Module, imports?: Imports): Promise;
+ function validate(bytes: BufferSource): boolean;
+}
+/**
+ * The **`ServiceWorkerGlobalScope`** interface of the Service Worker API represents the global execution context of a service worker.
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope)
+ */
+interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
+ DOMException: typeof DOMException;
+ WorkerGlobalScope: typeof WorkerGlobalScope;
+ btoa(data: string): string;
+ atob(data: string): string;
+ setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;
+ setTimeout(
+ callback: (...args: Args) => void,
+ msDelay?: number,
+ ...args: Args
+ ): number;
+ clearTimeout(timeoutId: number | null): void;
+ setInterval(callback: (...args: any[]) => void, msDelay?: number): number;
+ setInterval(
+ callback: (...args: Args) => void,
+ msDelay?: number,
+ ...args: Args
+ ): number;
+ clearInterval(timeoutId: number | null): void;
+ queueMicrotask(task: Function): void;
+ structuredClone(value: T, options?: StructuredSerializeOptions): T;
+ reportError(error: any): void;
+ fetch(
+ input: RequestInfo | URL,
+ init?: RequestInit,
+ ): Promise;
+ self: ServiceWorkerGlobalScope;
+ crypto: Crypto;
+ caches: CacheStorage;
+ scheduler: Scheduler;
+ performance: Performance;
+ Cloudflare: Cloudflare;
+ readonly origin: string;
+ Event: typeof Event;
+ ExtendableEvent: typeof ExtendableEvent;
+ CustomEvent: typeof CustomEvent;
+ PromiseRejectionEvent: typeof PromiseRejectionEvent;
+ FetchEvent: typeof FetchEvent;
+ TailEvent: typeof TailEvent;
+ TraceEvent: typeof TailEvent;
+ ScheduledEvent: typeof ScheduledEvent;
+ MessageEvent: typeof MessageEvent;
+ CloseEvent: typeof CloseEvent;
+ ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader;
+ ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader;
+ ReadableStream: typeof ReadableStream;
+ WritableStream: typeof WritableStream;
+ WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter;
+ TransformStream: typeof TransformStream;
+ ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy;
+ CountQueuingStrategy: typeof CountQueuingStrategy;
+ ErrorEvent: typeof ErrorEvent;
+ MessageChannel: typeof MessageChannel;
+ MessagePort: typeof MessagePort;
+ EventSource: typeof EventSource;
+ ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest;
+ ReadableStreamDefaultController: typeof ReadableStreamDefaultController;
+ ReadableByteStreamController: typeof ReadableByteStreamController;
+ WritableStreamDefaultController: typeof WritableStreamDefaultController;
+ TransformStreamDefaultController: typeof TransformStreamDefaultController;
+ CompressionStream: typeof CompressionStream;
+ DecompressionStream: typeof DecompressionStream;
+ TextEncoderStream: typeof TextEncoderStream;
+ TextDecoderStream: typeof TextDecoderStream;
+ Headers: typeof Headers;
+ Body: typeof Body;
+ Request: typeof Request;
+ Response: typeof Response;
+ WebSocket: typeof WebSocket;
+ WebSocketPair: typeof WebSocketPair;
+ WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair;
+ AbortController: typeof AbortController;
+ AbortSignal: typeof AbortSignal;
+ TextDecoder: typeof TextDecoder;
+ TextEncoder: typeof TextEncoder;
+ navigator: Navigator;
+ Navigator: typeof Navigator;
+ URL: typeof URL;
+ URLSearchParams: typeof URLSearchParams;
+ URLPattern: typeof URLPattern;
+ Blob: typeof Blob;
+ File: typeof File;
+ FormData: typeof FormData;
+ Crypto: typeof Crypto;
+ SubtleCrypto: typeof SubtleCrypto;
+ CryptoKey: typeof CryptoKey;
+ CacheStorage: typeof CacheStorage;
+ Cache: typeof Cache;
+ FixedLengthStream: typeof FixedLengthStream;
+ IdentityTransformStream: typeof IdentityTransformStream;
+ HTMLRewriter: typeof HTMLRewriter;
+}
+declare function addEventListener(
+ type: Type,
+ handler: EventListenerOrEventListenerObject,
+ options?: EventTargetAddEventListenerOptions | boolean,
+): void;
+declare function removeEventListener<
+ Type extends keyof WorkerGlobalScopeEventMap,
+>(
+ type: Type,
+ handler: EventListenerOrEventListenerObject,
+ options?: EventTargetEventListenerOptions | boolean,
+): void;
+/**
+ * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)
+ */
+declare function dispatchEvent(
+ event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap],
+): boolean;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */
+declare function btoa(data: string): string;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */
+declare function atob(data: string): string;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(
+ callback: (...args: any[]) => void,
+ msDelay?: number,
+): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(
+ callback: (...args: Args) => void,
+ msDelay?: number,
+ ...args: Args
+): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */
+declare function clearTimeout(timeoutId: number | null): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(
+ callback: (...args: any[]) => void,
+ msDelay?: number,
+): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(
+ callback: (...args: Args) => void,
+ msDelay?: number,
+ ...args: Args
+): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */
+declare function clearInterval(timeoutId: number | null): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */
+declare function queueMicrotask(task: Function): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */
+declare function structuredClone(
+ value: T,
+ options?: StructuredSerializeOptions,
+): T;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */
+declare function reportError(error: any): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */
+declare function fetch(
+ input: RequestInfo | URL,
+ init?: RequestInit,
+): Promise;
+declare const self: ServiceWorkerGlobalScope;
+/**
+ * The Web Crypto API provides a set of low-level functions for common cryptographic tasks.
+ * The Workers runtime implements the full surface of this API, but with some differences in
+ * the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)
+ * compared to those implemented in most browsers.
+ *
+ * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)
+ */
+declare const crypto: Crypto;
+/**
+ * The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.
+ *
+ * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)
+ */
+declare const caches: CacheStorage;
+declare const scheduler: Scheduler;
+/**
+ * The Workers runtime supports a subset of the Performance API, used to measure timing and performance,
+ * as well as timing of subrequests and other operations.
+ *
+ * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/)
+ */
+declare const performance: Performance;
+declare const Cloudflare: Cloudflare;
+declare const origin: string;
+declare const navigator: Navigator;
+interface TestController {}
+interface ExecutionContext {
+ waitUntil(promise: Promise): void;
+ passThroughOnException(): void;
+ readonly exports: Cloudflare.Exports;
+ readonly props: Props;
+}
+type ExportedHandlerFetchHandler<
+ Env = unknown,
+ CfHostMetadata = unknown,
+ Props = unknown,
+> = (
+ request: Request>,
+ env: Env,
+ ctx: ExecutionContext,
+) => Response | Promise;
+type ExportedHandlerTailHandler = (
+ events: TraceItem[],
+ env: Env,
+ ctx: ExecutionContext,
+) => void | Promise;
+type ExportedHandlerTraceHandler = (
+ traces: TraceItem[],
+ env: Env,
+ ctx: ExecutionContext,
+) => void | Promise;
+type ExportedHandlerTailStreamHandler = (
+ event: TailStream.TailEvent,
+ env: Env,
+ ctx: ExecutionContext,
+) => TailStream.TailEventHandlerType | Promise;
+type ExportedHandlerScheduledHandler = (
+ controller: ScheduledController,
+ env: Env,
+ ctx: ExecutionContext,
+) => void | Promise;
+type ExportedHandlerQueueHandler<
+ Env = unknown,
+ Message = unknown,
+ Props = unknown,
+> = (
+ batch: MessageBatch,
+ env: Env,
+ ctx: ExecutionContext,
+) => void | Promise;
+type ExportedHandlerTestHandler = (
+ controller: TestController,
+ env: Env,
+ ctx: ExecutionContext,
+) => void | Promise;
+interface ExportedHandler<
+ Env = unknown,
+ QueueHandlerMessage = unknown,
+ CfHostMetadata = unknown,
+ Props = unknown,
+> {
+ fetch?: ExportedHandlerFetchHandler;
+ tail?: ExportedHandlerTailHandler;
+ trace?: ExportedHandlerTraceHandler;
+ tailStream?: ExportedHandlerTailStreamHandler;
+ scheduled?: ExportedHandlerScheduledHandler;
+ test?: ExportedHandlerTestHandler;
+ email?: EmailExportedHandler;
+ queue?: ExportedHandlerQueueHandler;
+}
+interface StructuredSerializeOptions {
+ transfer?: any[];
+}
+declare abstract class Navigator {
+ sendBeacon(url: string, body?: BodyInit): boolean;
+ readonly userAgent: string;
+ readonly hardwareConcurrency: number;
+ readonly language: string;
+ readonly languages: string[];
+}
+interface AlarmInvocationInfo {
+ readonly isRetry: boolean;
+ readonly retryCount: number;
+}
+interface Cloudflare {
+ readonly compatibilityFlags: Record;
+}
+interface DurableObject {
+ fetch(request: Request): Response | Promise;
+ alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise;
+ webSocketMessage?(
+ ws: WebSocket,
+ message: string | ArrayBuffer,
+ ): void | Promise;
+ webSocketClose?(
+ ws: WebSocket,
+ code: number,
+ reason: string,
+ wasClean: boolean,
+ ): void | Promise;
+ webSocketError?(ws: WebSocket, error: unknown): void | Promise;
+}
+type DurableObjectStub<
+ T extends Rpc.DurableObjectBranded | undefined = undefined,
+> = Fetcher<
+ T,
+ "alarm" | "webSocketMessage" | "webSocketClose" | "webSocketError"
+> & {
+ readonly id: DurableObjectId;
+ readonly name?: string;
+};
+interface DurableObjectId {
+ toString(): string;
+ equals(other: DurableObjectId): boolean;
+ readonly name?: string;
+}
+declare abstract class DurableObjectNamespace<
+ T extends Rpc.DurableObjectBranded | undefined = undefined,
+> {
+ newUniqueId(
+ options?: DurableObjectNamespaceNewUniqueIdOptions,
+ ): DurableObjectId;
+ idFromName(name: string): DurableObjectId;
+ idFromString(id: string): DurableObjectId;
+ get(
+ id: DurableObjectId,
+ options?: DurableObjectNamespaceGetDurableObjectOptions,
+ ): DurableObjectStub;
+ getByName(
+ name: string,
+ options?: DurableObjectNamespaceGetDurableObjectOptions,
+ ): DurableObjectStub;
+ jurisdiction(
+ jurisdiction: DurableObjectJurisdiction,
+ ): DurableObjectNamespace;
+}
+type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high";
+interface DurableObjectNamespaceNewUniqueIdOptions {
+ jurisdiction?: DurableObjectJurisdiction;
+}
+type DurableObjectLocationHint =
+ | "wnam"
+ | "enam"
+ | "sam"
+ | "weur"
+ | "eeur"
+ | "apac"
+ | "oc"
+ | "afr"
+ | "me";
+type DurableObjectRoutingMode = "primary-only";
+interface DurableObjectNamespaceGetDurableObjectOptions {
+ locationHint?: DurableObjectLocationHint;
+ routingMode?: DurableObjectRoutingMode;
+}
+interface DurableObjectClass<
+ _T extends Rpc.DurableObjectBranded | undefined = undefined,
+> {}
+interface DurableObjectState {
+ waitUntil(promise: Promise): void;
+ readonly exports: Cloudflare.Exports;
+ readonly props: Props;
+ readonly id: DurableObjectId;
+ readonly storage: DurableObjectStorage;
+ container?: Container;
+ blockConcurrencyWhile(callback: () => Promise): Promise;
+ acceptWebSocket(ws: WebSocket, tags?: string[]): void;
+ getWebSockets(tag?: string): WebSocket[];
+ setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void;
+ getWebSocketAutoResponse(): WebSocketRequestResponsePair | null;
+ getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null;
+ setHibernatableWebSocketEventTimeout(timeoutMs?: number): void;
+ getHibernatableWebSocketEventTimeout(): number | null;
+ getTags(ws: WebSocket): string[];
+ abort(reason?: string): void;
+}
+interface DurableObjectTransaction {
+ get(
+ key: string,
+ options?: DurableObjectGetOptions,
+ ): Promise;
+ get(
+ keys: string[],
+ options?: DurableObjectGetOptions,
+ ): Promise