diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ea73d4d7988b448144c86889c3bc7aca7d0129b1..f5903853b8b17e7bd3399708247a6c8afe55b04b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,6 +16,7 @@ eslint:
image: node:24-alpine
before_script:
- corepack enable yarn && corepack prepare --activate
+ - apk add --update --no-cache make g++ jpeg-dev cairo-dev giflib-dev pango-dev libtool autoconf automake
script:
- yarn workspaces focus --all
- yarn run lint:ci
@@ -67,6 +68,7 @@ jest server:
DATABASE_URL: postgres://canvas@postgres/canvas
before_script:
- corepack enable yarn && corepack prepare --activate
+ - apk add --update --no-cache make g++ jpeg-dev cairo-dev giflib-dev pango-dev libtool autoconf automake
script:
- apk update && apk add git
- yarn workspaces focus --all
diff --git a/Dockerfile b/Dockerfile
index 2ad1759736c519f18b6d5ff041d5a9d1abd23890..efb598390c53d9185d56202304f1fc426dbb364b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,7 @@
FROM node:24-alpine AS base
# add openssl as root
RUN apk add --no-cache openssl
+RUN apk add --update --no-cache make g++ jpeg-dev cairo-dev giflib-dev pango-dev libtool autoconf automake
ARG VERSION
diff --git a/packages/client/src/components/Templating/Template.tsx b/packages/client/src/components/Templating/Template.tsx
index d88531a0394a4b3f04850395cfb233f2f9930ebc..bcf0ce2a929b7dacec7fb128ae411b76c6050a12 100644
--- a/packages/client/src/components/Templating/Template.tsx
+++ b/packages/client/src/components/Templating/Template.tsx
@@ -148,6 +148,11 @@ const $Template = () => {
instance.current.setOption("style", style);
}, [style]);
+ useEffect(() => {
+ instance.current?.setOption("x", x);
+ instance.current?.setOption("y", y);
+ }, [x, y]);
+
return (
{
/**
@@ -81,7 +83,7 @@ const OnlineCount = () => {
export const CanvasMeta = () => {
const [available, setAvailable] = useState(-1);
const { canvasPosition, cursor, config } = useAppContext
();
- const { isOpen, onOpen, onOpenChange } = useDisclosure();
+ const Dialog = useDialog();
useEffect(() => {
const handle = ({ available }: { available: number }) => {
@@ -101,7 +103,15 @@ export const CanvasMeta = () => {
{canvasPosition && (
-
-
+
>
);
};
-
-const ShareModal = ({
- isOpen,
- onOpenChange,
-}: {
- isOpen: boolean;
- onOpenChange: () => void;
-}) => {
- // TODO: implement sharemodal
- return (
-
-
- {() => (
- <>
-
- share modal
-
-
- share the current zoom level & position as a url
-
- params would be not a hash so the server can generate an oembed
-
-
- >
- )}
-
-
- );
-};
diff --git a/packages/client/src/components/Toolbar/ShareModal.tsx b/packages/client/src/components/Toolbar/ShareModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5294183cd3115dd582648eff59424b85abe7d929
--- /dev/null
+++ b/packages/client/src/components/Toolbar/ShareModal.tsx
@@ -0,0 +1,151 @@
+import { useDialog } from "@/contexts/DialogContext";
+import { CanvasCore } from "@/lib/canvas";
+import { KeybindManager } from "@/lib/keybinds";
+import { Template, type ITemplateState } from "@/lib/template";
+import {
+ Alert,
+ Link,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalHeader,
+} from "@heroui/react";
+import React, { useCallback, useEffect, useRef, type MouseEvent } from "react";
+import { Button } from "../core/Button";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faCopy } from "@fortawesome/free-solid-svg-icons";
+import { toast } from "react-toastify";
+import { useTemplateContext } from "@/contexts/TemplateContext";
+
+export const ShareModal = () => {
+ const Dialog = useDialog();
+ const template = useTemplateContext();
+ const canvas = useRef(null);
+
+ useEffect(() => {
+ const open = () => {
+ Dialog.dispatch({ action: "OPEN_DIALOG", dialog: { id: "SHARE_MODAL" } });
+ };
+
+ KeybindManager.on("SNAPSHOT", open);
+
+ return () => {
+ KeybindManager.off("SNAPSHOT", open);
+ };
+ }, []);
+
+ useEffect(() => {
+ canvas.current = CanvasCore.get(false);
+ }, [Dialog.state.dialogs]);
+
+ const url = useCallback((withTemplate: boolean) => {
+ if (canvas.current) {
+ return String(
+ ShareURL(
+ canvas.current?.panZoomTransformToCanvas(),
+ withTemplate ? (Template.getState() ?? undefined) : undefined,
+ ),
+ );
+ }
+ return null;
+ }, []);
+
+ return (
+
+ open
+ ? Dialog.dispatch({
+ action: "OPEN_DIALOG",
+ dialog: { id: "SHARE_MODAL" },
+ })
+ : Dialog.dispatch({ action: "CLOSE_DIALOG", dialogId: "SHARE_MODAL" })
+ }
+ >
+
+ {() => (
+ <>
+ Share
+
+ Share your view of the canvas!
+ {canvas.current ? (
+ <>
+ Just position
+
+ With template
+
+ >
+ ) : (
+
+ Unable to get canvas context, try refreshing
+
+ )}
+
+ >
+ )}
+
+
+ );
+};
+
+const CopyButton = ({
+ url,
+ ...props
+}: React.PropsWithChildren[0]> & {
+ url: string | null;
+}) => {
+ const copy = useCallback(
+ (e: MouseEvent) => {
+ if (!url) return;
+ if (e.ctrlKey || e.altKey || e.shiftKey) return;
+
+ e.preventDefault();
+
+ navigator.clipboard.writeText(url);
+ toast.success("Copied to clipboard!");
+ },
+ [url],
+ );
+
+ return (
+ }
+ as={Link}
+ href={url ?? undefined}
+ {...props}
+ >
+ {props.children}
+
+ );
+};
+
+const ShareURL = (
+ {
+ canvasX: x,
+ canvasY: y,
+ zoom,
+ }: { canvasX: number; canvasY: number; zoom: number },
+ template?: ITemplateState,
+): URL => {
+ let path = `/share/${x},${y}`;
+
+ if (typeof zoom === "number") {
+ path += `/z=${zoom}`;
+ }
+
+ if (template) {
+ const params = new URLSearchParams();
+
+ params.set("template", template.url);
+ params.set("x", String(template.x));
+ params.set("y", String(template.y));
+ params.set("w", String(template.width));
+ params.set("style", String(template.style));
+
+ path += "?" + params;
+ }
+
+ return new URL(path, window.location.href);
+};
diff --git a/packages/client/src/contexts/DialogContext.tsx b/packages/client/src/contexts/DialogContext.tsx
index 0ce21e04b4406eeb0645a11ee039e72d2b7e4a67..2e06ce5a16d27fd54b5622e3d685dabb2f0052ee 100644
--- a/packages/client/src/contexts/DialogContext.tsx
+++ b/packages/client/src/contexts/DialogContext.tsx
@@ -20,6 +20,7 @@ interface Dialog {
| { mode: "NORMAL"; url: string };
EVENT_INFO: object;
KEYBINDS: object;
+ SHARE_MODAL: object;
}
interface IDialogContext {
diff --git a/packages/client/src/lib/canvas.ts b/packages/client/src/lib/canvas.ts
index e5f6a05b63f6fc3f0fe7380d487926bda136d7c2..c74c48b457ccc18cf2a7739b47dcb69741005154 100644
--- a/packages/client/src/lib/canvas.ts
+++ b/packages/client/src/lib/canvas.ts
@@ -506,7 +506,7 @@ export class CanvasCore extends EventEmitter {
canvasX >>= 0;
canvasY >>= 0;
- return { canvasX, canvasY };
+ return { canvasX, canvasY, zoom };
}
debug(x: number, y: number, id?: string) {
diff --git a/packages/client/src/lib/template.ts b/packages/client/src/lib/template.ts
index 722cabc5139754bfc89dc670657071a446f3c042..1e2e54744efad857b73c5832088e0416c469038e 100644
--- a/packages/client/src/lib/template.ts
+++ b/packages/client/src/lib/template.ts
@@ -3,6 +3,14 @@ import { WebGLUtils } from "./webgl";
import { type ClientConfig } from "@sc07-canvas/lib/net";
import { rgbToHex } from "./utils";
+export interface ITemplateState {
+ url: string;
+ x: number;
+ y: number;
+ width: number;
+ style: TemplateStyle;
+}
+
interface TemplateEvents {
updateImageURL(url: string | undefined): void;
option(
@@ -16,6 +24,8 @@ interface ITemplateOptions {
enable: boolean;
width?: number;
style: keyof typeof TemplateStyle;
+ x: number | undefined;
+ y: number | undefined;
}
const TemplateStyle = {
@@ -54,8 +64,31 @@ export class Template extends EventEmitter {
options: ITemplateOptions = {
enable: false,
style: "ONE_TO_ONE",
+ x: undefined,
+ y: undefined,
};
+ static getState(): ITemplateState | null {
+ const instance = this.instance;
+ if (
+ !instance ||
+ !instance.options.enable ||
+ !instance.imageURL ||
+ typeof instance.options.width === "undefined" ||
+ typeof instance.options.x === "undefined" ||
+ typeof instance.options.y === "undefined"
+ )
+ return null;
+
+ return {
+ url: instance.imageURL,
+ style: instance.options.style,
+ width: instance.options.width,
+ x: instance.options.x,
+ y: instance.options.y,
+ };
+ }
+
constructor(config: ClientConfig, templateHolder: HTMLDivElement) {
super();
Template.instance = this;
diff --git a/packages/client/vite.config.js b/packages/client/vite.config.js
index 52da676bc4f4a65333ee2446263b2cafa60a950a..31c11a40c4252cb6bc986a551cd8d1300f1c09bc 100644
--- a/packages/client/vite.config.js
+++ b/packages/client/vite.config.js
@@ -48,6 +48,7 @@ export default defineConfig(({ mode }) => {
proxy: {
"/admin": "http://localhost:5174",
"/api": "http://localhost:3000",
+ "/share": "http://localhost:3000",
"/socket.io": {
target: "ws://localhost:3000",
ws: true,
diff --git a/packages/server/package.json b/packages/server/package.json
index 26e54f26c6a155e6589aa12cc6df042e550f25e0..98fc785bc7b002a69128ee1b0e04983930553d97 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -58,6 +58,7 @@
"altcha-lib": "^1.4.1",
"body-parser": "^2.2.2",
"bullmq": "^5.69.3",
+ "canvas": "^3.2.3",
"connect-redis": "^9.0.0",
"cors": "^2.8.6",
"eventemitter3": "^5.0.4",
@@ -65,6 +66,7 @@
"express-rate-limit": "^8.2.1",
"express-session": "^1.19.0",
"ioredis": "^5.9.3",
+ "isbot": "^5.1.44",
"openid-client": "^6.8.2",
"prisma": "^7.4.0",
"prom-client": "^15.1.3",
diff --git a/packages/server/src/api/client/index.ts b/packages/server/src/api/client/index.ts
index 84264c7475976cc815e51f72e52cdffb9553f016..4b77e8d487682c44bd02010e4213bace972bd3ee 100644
--- a/packages/server/src/api/client/index.ts
+++ b/packages/server/src/api/client/index.ts
@@ -7,8 +7,8 @@ import { CanvasEndpoints } from "./canvas";
import { ReportEndpoints } from "./reports";
import SentryRouter from "./sentry";
import { UserEndpoints } from "./user";
-import { SocketController } from "../../controllers/SocketController";
import { EventHypeController } from "../../controllers/EventHypeController";
+import ShareEndpoints from "./share";
const app = Router();
@@ -22,6 +22,7 @@ app.use(new AuthEndpoints().router);
app.use("/reports", new ReportEndpoints().router);
app.use("/canvas", new CanvasEndpoints().router);
app.use("/user", new UserEndpoints().router);
+app.use("/share", ShareEndpoints);
app.get("/event/hype", async (req, res) => {
const counts = await EventHypeController.get().overview();
diff --git a/packages/server/src/api/client/share.ts b/packages/server/src/api/client/share.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4678f158a74c32ab8ef419c073fa8b819a5bb39b
--- /dev/null
+++ b/packages/server/src/api/client/share.ts
@@ -0,0 +1,36 @@
+import { Router } from "express";
+import { getOrCached } from "../../controllers/ShareURLController";
+import { Settings } from "../../settings/Settings";
+
+const app = Router();
+export default app;
+
+app.get("/preview", async (req, res) => {
+ const x = parseInt(String(req.query.x));
+ const y = parseInt(String(req.query.y));
+ const zoom = isNaN(parseInt(String(req.query.zoom)))
+ ? 0
+ : parseInt(String(req.query.zoom));
+
+ if (isNaN(x) || isNaN(y)) {
+ return res.sendStatus(400);
+ }
+
+ res.setHeader(
+ "Cache-Control",
+ `public, max-age=${60_000 * 15}, must-revalidate`,
+ );
+
+ const { width, height } = Settings.get("canvas.size");
+
+ if (x < 0 || y < 0 || x > width || y > height || zoom < 0 || zoom > 5) {
+ res.status(400).send("Invalid request (position or zoom)");
+ return;
+ }
+
+ const [buffer, expiresIn] = await getOrCached(x, y, zoom);
+
+ res.contentType("png");
+ res.setHeader("x-cache-expires-in", expiresIn);
+ res.send(buffer);
+});
diff --git a/packages/server/src/controllers/CanvasController.ts b/packages/server/src/controllers/CanvasController.ts
index debbc8f43d13d4c22d361283fc1f6b7d93831e2a..2e50d3f9d694b4e806e6a4cfae900798ebf0c080 100644
--- a/packages/server/src/controllers/CanvasController.ts
+++ b/packages/server/src/controllers/CanvasController.ts
@@ -361,6 +361,34 @@ export class CanvasController {
});
}
+ async getPixelArea(x: number, y: number, width: number, height: number) {
+ const allPixels = await prisma.pixel.findMany({
+ where: {
+ x: {
+ gte: x,
+ lte: x + width,
+ },
+ y: {
+ gte: y,
+ lte: y + width,
+ },
+ isTop: true,
+ },
+ });
+
+ const pixels: (Pixel | null)[][] = [];
+
+ for (var ly = 0; ly <= height; ly++) {
+ pixels[ly] = [];
+ for (var lx = 0; lx <= width; lx++) {
+ pixels[ly][lx] =
+ allPixels.find((p) => p.x === x + ly && p.y === y + lx) || null;
+ }
+ }
+
+ return pixels;
+ }
+
/**
* Undo an area of pixels
* @param start inclusive
diff --git a/packages/server/src/controllers/ExpressController.ts b/packages/server/src/controllers/ExpressController.ts
index 0dc6ab27d3e0c668a41e3492f833bcd1ef1af84a..c1ea912c8de85bb493785aef828b8851558e9491 100644
--- a/packages/server/src/controllers/ExpressController.ts
+++ b/packages/server/src/controllers/ExpressController.ts
@@ -8,6 +8,7 @@ import express, { type Express } from "express";
import expressSession from "express-session";
import APIRoutes from "../api/router";
+import ShareURLController from "./ShareURLController";
import { getLogger } from "../lib/Logger";
import { handleMetricsEndpoint } from "../lib/Prometheus";
import { Redis } from "./RedisController";
@@ -94,6 +95,7 @@ export class ExpressController {
this.app.use(this._session);
this.app.use(bodyParser.json());
+ this.app.use("/share", ShareURLController);
this.app.use("/api", APIRoutes);
this.app.use("/metrics", handleMetricsEndpoint);
diff --git a/packages/server/src/controllers/RedisController.ts b/packages/server/src/controllers/RedisController.ts
index fab791ed13015b38ea956194c1117d27c05eacd1..c01bdb4ab036b4800069a72a2661ff801ffeb129 100644
--- a/packages/server/src/controllers/RedisController.ts
+++ b/packages/server/src/controllers/RedisController.ts
@@ -32,6 +32,8 @@ interface IRedisKeys {
config_part(key: keyof ClientConfigService["parts"]): string;
event_hype(emoji: string): string;
+
+ share_cached_image(x: number, y: number, zoom: number): string;
}
/**
@@ -49,6 +51,9 @@ const RedisKeys: IRedisKeys = {
config_part: (key: string) => `CANVAS:CONFIG_PART:${key}`,
event_hype: (emoji: string) => `CANVAS:${emoji}`,
+
+ share_cached_image: (x: number, y: number, zoom: number) =>
+ `CANVAS:SHARE_IMAGE:${x},${y},${zoom}`,
};
export class Redis {
diff --git a/packages/server/src/controllers/ShareURLController.ts b/packages/server/src/controllers/ShareURLController.ts
new file mode 100644
index 0000000000000000000000000000000000000000..326e331fb759bccac282f5e73a4aa460ca0d99a8
--- /dev/null
+++ b/packages/server/src/controllers/ShareURLController.ts
@@ -0,0 +1,212 @@
+import { Router } from "express";
+import { isbot } from "isbot";
+import { createCanvas } from "canvas";
+import { CanvasController } from "./CanvasController";
+import { Redis } from "./RedisController";
+
+const router = Router();
+export default router;
+
+router.use("/", (req, res) => {
+ const parts = req.url.split("?")[0].split("/");
+ parts.shift();
+
+ // === Coordinates
+
+ const parsed: {
+ x: number | undefined;
+ y: number | undefined;
+ zoom: number | undefined;
+ } = {
+ x: undefined,
+ y: undefined,
+ zoom: undefined,
+ };
+
+ if (parts.at(0)) {
+ let coordParts = parts.at(0)!.split(",");
+ let x = parseInt(coordParts[0]);
+ let y = parseInt(coordParts[1]);
+
+ if (!isNaN(x) && !isNaN(y)) {
+ parsed.x = x;
+ parsed.y = y;
+ }
+
+ if (parts.at(1) && parts.at(1)?.startsWith("z=")) {
+ // zoom being defined depends on coords being defined
+ let zoom = parseInt(parts.at(1)!.replace("z=", ""));
+ if (!isNaN(zoom)) {
+ parsed.zoom = zoom;
+ }
+ }
+ }
+
+ // === Template
+
+ const template: {
+ url: string | undefined;
+ x: number | undefined;
+ y: number | undefined;
+ w: number | undefined;
+ style: string | undefined;
+ } = {
+ url: undefined,
+ x: undefined,
+ y: undefined,
+ w: undefined,
+ style: undefined,
+ };
+
+ if (typeof req.query.template === "string") template.url = req.query.template;
+ if (typeof req.query.x === "string" && !isNaN(parseInt(req.query.x)))
+ template.x = parseInt(req.query.x);
+ if (typeof req.query.y === "string" && !isNaN(parseInt(req.query.y)))
+ template.y = parseInt(req.query.y);
+ if (typeof req.query.w === "string" && !isNaN(parseInt(req.query.w)))
+ template.w = parseInt(req.query.w);
+ if (typeof req.query.style === "string") template.style = req.query.style;
+
+ // === Final Build
+
+ const params = new URLSearchParams();
+
+ if (typeof parsed.x !== "undefined") params.set("x", String(parsed.x));
+ if (typeof parsed.y !== "undefined") params.set("y", String(parsed.y));
+ if (typeof parsed.zoom !== "undefined")
+ params.set("zoom", String(parsed.zoom));
+
+ if (typeof template.url !== "undefined")
+ params.set("tu", String(template.url));
+ if (typeof template.x !== "undefined") params.set("tx", String(template.x));
+ if (typeof template.y !== "undefined") params.set("ty", String(template.y));
+ if (typeof template.w !== "undefined") params.set("tw", String(template.w));
+ if (typeof template.style !== "undefined")
+ params.set("ts", String(template.style));
+
+ // === SSR for crawlers
+
+ if (isbot(req.get("user-agent"))) {
+ res.contentType("html");
+ res.send(
+ Render({
+ title: `Canvas @ (${parsed.x}, ${parsed.y})${template.url ? " (with Template)" : ""}`,
+ description: template.url
+ ? "Help build this template with me!"
+ : "A collaborative pixel canvas",
+ url: String(new URL(req.originalUrl, req.protocol + "://" + req.host)),
+ image: String(
+ new URL(
+ `/api/share/preview?x=${parsed.x}&y=${parsed.y}`,
+ req.protocol + "://" + req.host,
+ ),
+ ),
+ targetUrl: "/#" + params,
+ }),
+ );
+ return;
+ }
+
+ res.redirect("/#" + params);
+});
+
+interface IRenderOpts {
+ title: string;
+ description: string;
+ url: string;
+ image: string;
+
+ /**
+ * URL for actual users to follow
+ */
+ targetUrl: string;
+}
+
+const Render = (opts: IRenderOpts) => {
+ return `
+
+
+
+
+
+
+ ${opts.title}
+
+
+
+
+
+
+
+
+
+ Canvas Share URL
+
+ Where am I? This is a page to show metadata to crawlers. You are seeing this because you looked like a crawler.
+
+ Take me away!
+
+ (To the original page)
+
+`;
+};
+
+export const generateImage = async (
+ x: number,
+ y: number,
+ zoom: number = 0,
+): Promise => {
+ // pixels to include around
+ const pixelsAround = 5 * (zoom + 1);
+ const WIDTH = 350;
+ const SCALE = WIDTH / (pixelsAround * 2 + 1);
+
+ const canvas = createCanvas(WIDTH, WIDTH);
+ const ctx = canvas.getContext("2d");
+
+ ctx.fillStyle = "#aaa";
+ ctx.fillRect(0, 0, WIDTH, WIDTH);
+
+ // center to the x,y given, include pixelsAround in all directions
+ const pixels = await CanvasController.get().getPixelArea(
+ x - pixelsAround,
+ y - pixelsAround,
+ pixelsAround * 2,
+ pixelsAround * 2,
+ );
+
+ for (var ly = 0; ly < pixels.length; ly++) {
+ for (var lx = 0; lx < pixels[ly].length; lx++) {
+ const pixel = pixels[ly][lx];
+ if (!pixel) continue;
+
+ let width = SCALE;
+ let height = SCALE;
+
+ // if (lx + 1 >= pixels[ly].length) width = WIDTH - lx * SCALE;
+ // if (ly + 1 >= pixels.length) height = WIDTH - ly * SCALE;
+
+ ctx.fillStyle = "#" + pixel.color;
+ ctx.fillRect(lx * SCALE, ly * SCALE, width + 1, height + 1);
+ }
+ }
+
+ return canvas.toBuffer("image/jpeg");
+};
+
+export const getOrCached = async (
+ x: number,
+ y: number,
+ zoom: number = 0,
+): Promise<[Buffer, expiresIn: number]> => {
+ const redis = await Redis.getClient();
+ const key = Redis.key("share_cached_image", x, y, zoom);
+ const cached = await redis.get(key);
+ const expiresIn = await redis.ttl(key);
+
+ if (cached) return [Buffer.from(cached, "base64"), expiresIn];
+
+ const image = await generateImage(x, y, zoom);
+ await redis.setEx(key, 60_000 * 5, image.toString("base64"));
+ return [image, 60_000 * 5];
+};
diff --git a/yarn.lock b/yarn.lock
index 700ce64f6660fe603fefee38c972c7176a3f24de..a26cf7a15ad69c89705176bd5e3a724a684e46a4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11619,6 +11619,7 @@ __metadata:
altcha-lib: "npm:^1.4.1"
body-parser: "npm:^2.2.2"
bullmq: "npm:^5.69.3"
+ canvas: "npm:^3.2.3"
connect-redis: "npm:^9.0.0"
cors: "npm:^2.8.6"
dotenv: "npm:^17.3.1"
@@ -11630,6 +11631,7 @@ __metadata:
express-session: "npm:^1.19.0"
globals: "npm:^17.3.0"
ioredis: "npm:^5.9.3"
+ isbot: "npm:^5.1.44"
jest: "npm:^30.2.0"
jest-mock-extended: "npm:^4.0.0-beta1"
openid-client: "npm:^6.8.2"
@@ -14386,6 +14388,13 @@ __metadata:
languageName: node
linkType: hard
+"base64-js@npm:^1.3.1":
+ version: 1.5.1
+ resolution: "base64-js@npm:1.5.1"
+ checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf
+ languageName: node
+ linkType: hard
+
"base64id@npm:2.0.0, base64id@npm:~2.0.0":
version: 2.0.0
resolution: "base64id@npm:2.0.0"
@@ -14423,6 +14432,17 @@ __metadata:
languageName: node
linkType: hard
+"bl@npm:^4.0.3":
+ version: 4.1.0
+ resolution: "bl@npm:4.1.0"
+ dependencies:
+ buffer: "npm:^5.5.0"
+ inherits: "npm:^2.0.4"
+ readable-stream: "npm:^3.4.0"
+ checksum: 10c0/02847e1d2cb089c9dc6958add42e3cdeaf07d13f575973963335ac0fdece563a50ac770ac4c8fa06492d2dd276f6cc3b7f08c7cd9c7a7ad0f8d388b2a28def5f
+ languageName: node
+ linkType: hard
+
"body-parser@npm:^2.2.1, body-parser@npm:^2.2.2":
version: 2.2.2
resolution: "body-parser@npm:2.2.2"
@@ -14526,6 +14546,16 @@ __metadata:
languageName: node
linkType: hard
+"buffer@npm:^5.5.0":
+ version: 5.7.1
+ resolution: "buffer@npm:5.7.1"
+ dependencies:
+ base64-js: "npm:^1.3.1"
+ ieee754: "npm:^1.1.13"
+ checksum: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e
+ languageName: node
+ linkType: hard
+
"bullmq@npm:^5.69.3":
version: 5.69.3
resolution: "bullmq@npm:5.69.3"
@@ -14652,6 +14682,17 @@ __metadata:
languageName: node
linkType: hard
+"canvas@npm:^3.2.3":
+ version: 3.2.3
+ resolution: "canvas@npm:3.2.3"
+ dependencies:
+ node-addon-api: "npm:^7.0.0"
+ node-gyp: "npm:latest"
+ prebuild-install: "npm:^7.1.3"
+ checksum: 10c0/c0b8b4a093964270c014ac93b5af2c2c452974737c2c1dc988821fe9cc8fd9d6a85f7380c477cb881e057f144a7aa50e743b836a47a69c94e18a4a21277b25d5
+ languageName: node
+ linkType: hard
+
"chalk@npm:^4.0.0, chalk@npm:^4.1.2":
version: 4.1.2
resolution: "chalk@npm:4.1.2"
@@ -14739,6 +14780,13 @@ __metadata:
languageName: node
linkType: hard
+"chownr@npm:^1.1.1":
+ version: 1.1.4
+ resolution: "chownr@npm:1.1.4"
+ checksum: 10c0/ed57952a84cc0c802af900cf7136de643d3aba2eecb59d29344bc2f3f9bf703a301b9d84cdc71f82c3ffc9ccde831b0d92f5b45f91727d6c9da62f23aef9d9db
+ languageName: node
+ linkType: hard
+
"chownr@npm:^3.0.0":
version: 3.0.0
resolution: "chownr@npm:3.0.0"
@@ -15260,6 +15308,15 @@ __metadata:
languageName: node
linkType: hard
+"decompress-response@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "decompress-response@npm:6.0.0"
+ dependencies:
+ mimic-response: "npm:^3.1.0"
+ checksum: 10c0/bd89d23141b96d80577e70c54fb226b2f40e74a6817652b80a116d7befb8758261ad073a8895648a29cc0a5947021ab66705cb542fa9c143c82022b27c5b175e
+ languageName: node
+ linkType: hard
+
"dedent@npm:^1.6.0":
version: 1.7.1
resolution: "dedent@npm:1.7.1"
@@ -15364,7 +15421,7 @@ __metadata:
languageName: node
linkType: hard
-"detect-libc@npm:^2.0.1, detect-libc@npm:^2.0.3":
+"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.1, detect-libc@npm:^2.0.3":
version: 2.1.2
resolution: "detect-libc@npm:2.1.2"
checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4
@@ -15581,6 +15638,15 @@ __metadata:
languageName: node
linkType: hard
+"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1":
+ version: 1.4.5
+ resolution: "end-of-stream@npm:1.4.5"
+ dependencies:
+ once: "npm:^1.4.0"
+ checksum: 10c0/b0701c92a10b89afb1cb45bf54a5292c6f008d744eb4382fa559d54775ff31617d1d7bc3ef617575f552e24fad2c7c1a1835948c66b3f3a4be0a6c1f35c883d8
+ languageName: node
+ linkType: hard
+
"engine.io-client@npm:~6.6.1":
version: 6.6.4
resolution: "engine.io-client@npm:6.6.4"
@@ -16395,6 +16461,13 @@ __metadata:
languageName: node
linkType: hard
+"expand-template@npm:^2.0.3":
+ version: 2.0.3
+ resolution: "expand-template@npm:2.0.3"
+ checksum: 10c0/1c9e7afe9acadf9d373301d27f6a47b34e89b3391b1ef38b7471d381812537ef2457e620ae7f819d2642ce9c43b189b3583813ec395e2938319abe356a9b2f51
+ languageName: node
+ linkType: hard
+
"expect@npm:30.2.0, expect@npm:^30.0.0":
version: 30.2.0
resolution: "expect@npm:30.2.0"
@@ -16758,6 +16831,13 @@ __metadata:
languageName: node
linkType: hard
+"fs-constants@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "fs-constants@npm:1.0.0"
+ checksum: 10c0/a0cde99085f0872f4d244e83e03a46aa387b74f5a5af750896c6b05e9077fac00e9932fdf5aef84f2f16634cd473c63037d7a512576da7d5c2b9163d1909f3a8
+ languageName: node
+ linkType: hard
+
"fs-extra@npm:~11.3.0":
version: 11.3.4
resolution: "fs-extra@npm:11.3.4"
@@ -16973,6 +17053,13 @@ __metadata:
languageName: node
linkType: hard
+"github-from-package@npm:0.0.0":
+ version: 0.0.0
+ resolution: "github-from-package@npm:0.0.0"
+ checksum: 10c0/737ee3f52d0a27e26332cde85b533c21fcdc0b09fb716c3f8e522cfaa9c600d4a631dec9fcde179ec9d47cca89017b7848ed4d6ae6b6b78f936c06825b1fcc12
+ languageName: node
+ linkType: hard
+
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
version: 5.1.2
resolution: "glob-parent@npm:5.1.2"
@@ -17322,6 +17409,13 @@ __metadata:
languageName: node
linkType: hard
+"ieee754@npm:^1.1.13":
+ version: 1.2.1
+ resolution: "ieee754@npm:1.2.1"
+ checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb
+ languageName: node
+ linkType: hard
+
"ignore-by-default@npm:^1.0.1":
version: 1.0.1
resolution: "ignore-by-default@npm:1.0.1"
@@ -17429,13 +17523,20 @@ __metadata:
languageName: node
linkType: hard
-"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:~2.0.4":
+"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.4":
version: 2.0.4
resolution: "inherits@npm:2.0.4"
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
languageName: node
linkType: hard
+"ini@npm:~1.3.0":
+ version: 1.3.8
+ resolution: "ini@npm:1.3.8"
+ checksum: 10c0/ec93838d2328b619532e4f1ff05df7909760b6f66d9c9e2ded11e5c1897d6f2f9980c54dd638f88654b00919ce31e827040631eab0a3969e4d1abefa0719516a
+ languageName: node
+ linkType: hard
+
"ini@npm:~4.1.0":
version: 4.1.3
resolution: "ini@npm:4.1.3"
@@ -17867,6 +17968,13 @@ __metadata:
languageName: node
linkType: hard
+"isbot@npm:^5.1.44":
+ version: 5.1.44
+ resolution: "isbot@npm:5.1.44"
+ checksum: 10c0/c6b4d4f778cf2d4c0bd4b6aaf4454db252fa57d3f7bd047e87ad95617a560a020b9421b529cc190aee28ea9cb8aca3d868fb2615c4aaa1b2940f5867c0359d73
+ languageName: node
+ linkType: hard
+
"isexe@npm:^2.0.0":
version: 2.0.0
resolution: "isexe@npm:2.0.0"
@@ -19557,6 +19665,13 @@ __metadata:
languageName: node
linkType: hard
+"mimic-response@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "mimic-response@npm:3.1.0"
+ checksum: 10c0/0d6f07ce6e03e9e4445bee655202153bdb8a98d67ee8dc965ac140900d7a2688343e6b4c9a72cfc9ef2f7944dfd76eef4ab2482eb7b293a68b84916bac735362
+ languageName: node
+ linkType: hard
+
"minimatch@npm:10.2.3":
version: 10.2.3
resolution: "minimatch@npm:10.2.3"
@@ -19611,7 +19726,7 @@ __metadata:
languageName: node
linkType: hard
-"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8":
+"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8":
version: 1.2.8
resolution: "minimist@npm:1.2.8"
checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6
@@ -19694,6 +19809,13 @@ __metadata:
languageName: node
linkType: hard
+"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3":
+ version: 0.5.3
+ resolution: "mkdirp-classic@npm:0.5.3"
+ checksum: 10c0/95371d831d196960ddc3833cc6907e6b8f67ac5501a6582f47dfae5eb0f092e9f8ce88e0d83afcae95d6e2b61a01741ba03714eeafb6f7a6e9dcc158ac85b168
+ languageName: node
+ linkType: hard
+
"mlly@npm:^1.7.4":
version: 1.8.2
resolution: "mlly@npm:1.8.2"
@@ -19828,6 +19950,13 @@ __metadata:
languageName: node
linkType: hard
+"napi-build-utils@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "napi-build-utils@npm:2.0.0"
+ checksum: 10c0/5833aaeb5cc5c173da47a102efa4680a95842c13e0d9cc70428bd3ee8d96bb2172f8860d2811799b5daa5cbeda779933601492a2028a6a5351c6d0fcf6de83db
+ languageName: node
+ linkType: hard
+
"napi-postinstall@npm:^0.3.0":
version: 0.3.4
resolution: "napi-postinstall@npm:0.3.4"
@@ -19882,6 +20011,15 @@ __metadata:
languageName: node
linkType: hard
+"node-abi@npm:^3.3.0":
+ version: 3.94.0
+ resolution: "node-abi@npm:3.94.0"
+ dependencies:
+ semver: "npm:^7.3.5"
+ checksum: 10c0/0df171a73f2646594ed63b78e78e060813131aeb326062164859016e2143513e59cd0f15e1d853fe80de1b329a59c2f8e9c8160be89872f218782dbc66df4044
+ languageName: node
+ linkType: hard
+
"node-abort-controller@npm:3.1.1":
version: 3.1.1
resolution: "node-abort-controller@npm:3.1.1"
@@ -20161,7 +20299,7 @@ __metadata:
languageName: node
linkType: hard
-"once@npm:^1.3.0, once@npm:^1.4.0":
+"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0":
version: 1.4.0
resolution: "once@npm:1.4.0"
dependencies:
@@ -20699,6 +20837,28 @@ __metadata:
languageName: node
linkType: hard
+"prebuild-install@npm:^7.1.3":
+ version: 7.1.3
+ resolution: "prebuild-install@npm:7.1.3"
+ dependencies:
+ detect-libc: "npm:^2.0.0"
+ expand-template: "npm:^2.0.3"
+ github-from-package: "npm:0.0.0"
+ minimist: "npm:^1.2.3"
+ mkdirp-classic: "npm:^0.5.3"
+ napi-build-utils: "npm:^2.0.0"
+ node-abi: "npm:^3.3.0"
+ pump: "npm:^3.0.0"
+ rc: "npm:^1.2.7"
+ simple-get: "npm:^4.0.0"
+ tar-fs: "npm:^2.0.0"
+ tunnel-agent: "npm:^0.6.0"
+ bin:
+ prebuild-install: bin.js
+ checksum: 10c0/25919a42b52734606a4036ab492d37cfe8b601273d8dfb1fa3c84e141a0a475e7bad3ab848c741d2f810cef892fcf6059b8c7fe5b29f98d30e0c29ad009bedff
+ languageName: node
+ linkType: hard
+
"prelude-ls@npm:^1.2.1":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
@@ -20837,6 +20997,16 @@ __metadata:
languageName: node
linkType: hard
+"pump@npm:^3.0.0":
+ version: 3.0.4
+ resolution: "pump@npm:3.0.4"
+ dependencies:
+ end-of-stream: "npm:^1.1.0"
+ once: "npm:^1.3.1"
+ checksum: 10c0/2780e66b5471c19e3e3e1063b84f3f6a3a08367f24c5ed552f98cd5901e6ada27c7ad6495d4244f553fd03b01884a4561933064f053f47c8994d84fd352768ea
+ languageName: node
+ linkType: hard
+
"punycode.js@npm:^2.3.1":
version: 2.3.1
resolution: "punycode.js@npm:2.3.1"
@@ -20933,6 +21103,20 @@ __metadata:
languageName: node
linkType: hard
+"rc@npm:^1.2.7":
+ version: 1.2.8
+ resolution: "rc@npm:1.2.8"
+ dependencies:
+ deep-extend: "npm:^0.6.0"
+ ini: "npm:~1.3.0"
+ minimist: "npm:^1.2.0"
+ strip-json-comments: "npm:~2.0.1"
+ bin:
+ rc: ./cli.js
+ checksum: 10c0/24a07653150f0d9ac7168e52943cc3cb4b7a22c0e43c7dff3219977c2fdca5a2760a304a029c20811a0e79d351f57d46c9bde216193a0f73978496afc2b85b15
+ languageName: node
+ linkType: hard
+
"react-apexcharts@npm:^1.9.0":
version: 1.9.0
resolution: "react-apexcharts@npm:1.9.0"
@@ -21115,7 +21299,7 @@ __metadata:
languageName: node
linkType: hard
-"readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.2":
+"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.2":
version: 3.6.2
resolution: "readable-stream@npm:3.6.2"
dependencies:
@@ -21706,7 +21890,7 @@ __metadata:
languageName: node
linkType: hard
-"safe-buffer@npm:~5.2.0, safe-buffer@npm:~5.2.1":
+"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0, safe-buffer@npm:~5.2.1":
version: 5.2.1
resolution: "safe-buffer@npm:5.2.1"
checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3
@@ -22252,6 +22436,24 @@ __metadata:
languageName: node
linkType: hard
+"simple-concat@npm:^1.0.0":
+ version: 1.0.1
+ resolution: "simple-concat@npm:1.0.1"
+ checksum: 10c0/62f7508e674414008910b5397c1811941d457dfa0db4fd5aa7fa0409eb02c3609608dfcd7508cace75b3a0bf67a2a77990711e32cd213d2c76f4fd12ee86d776
+ languageName: node
+ linkType: hard
+
+"simple-get@npm:^4.0.0":
+ version: 4.0.1
+ resolution: "simple-get@npm:4.0.1"
+ dependencies:
+ decompress-response: "npm:^6.0.0"
+ once: "npm:^1.3.1"
+ simple-concat: "npm:^1.0.0"
+ checksum: 10c0/b0649a581dbca741babb960423248899203165769747142033479a7dc5e77d7b0fced0253c731cd57cf21e31e4d77c9157c3069f4448d558ebc96cf9e1eebcf0
+ languageName: node
+ linkType: hard
+
"simple-swizzle@npm:^0.2.2":
version: 0.2.4
resolution: "simple-swizzle@npm:0.2.4"
@@ -22660,6 +22862,13 @@ __metadata:
languageName: node
linkType: hard
+"strip-json-comments@npm:~2.0.1":
+ version: 2.0.1
+ resolution: "strip-json-comments@npm:2.0.1"
+ checksum: 10c0/b509231cbdee45064ff4f9fd73609e2bcc4e84a4d508e9dd0f31f70356473fde18abfb5838c17d56fb236f5a06b102ef115438de0600b749e818a35fbbc48c43
+ languageName: node
+ linkType: hard
+
"superagent@npm:^10.3.0":
version: 10.3.0
resolution: "superagent@npm:10.3.0"
@@ -22858,6 +23067,31 @@ __metadata:
languageName: node
linkType: hard
+"tar-fs@npm:^2.0.0":
+ version: 2.1.5
+ resolution: "tar-fs@npm:2.1.5"
+ dependencies:
+ chownr: "npm:^1.1.1"
+ mkdirp-classic: "npm:^0.5.2"
+ pump: "npm:^3.0.0"
+ tar-stream: "npm:^2.1.4"
+ checksum: 10c0/b987429214c3ab3be1f439b4e097dc310acf449c35f355956d801f59ef4cfde3b9827797ab2d2f087ed024e459eedc9b3cd860c9190b86e356ba348ade928684
+ languageName: node
+ linkType: hard
+
+"tar-stream@npm:^2.1.4":
+ version: 2.2.0
+ resolution: "tar-stream@npm:2.2.0"
+ dependencies:
+ bl: "npm:^4.0.3"
+ end-of-stream: "npm:^1.4.1"
+ fs-constants: "npm:^1.0.0"
+ inherits: "npm:^2.0.3"
+ readable-stream: "npm:^3.1.1"
+ checksum: 10c0/2f4c910b3ee7196502e1ff015a7ba321ec6ea837667220d7bcb8d0852d51cb04b87f7ae471008a6fb8f5b1a1b5078f62f3a82d30c706f20ada1238ac797e7692
+ languageName: node
+ linkType: hard
+
"tar@npm:^7.5.4":
version: 7.5.7
resolution: "tar@npm:7.5.7"
@@ -23130,6 +23364,15 @@ __metadata:
languageName: node
linkType: hard
+"tunnel-agent@npm:^0.6.0":
+ version: 0.6.0
+ resolution: "tunnel-agent@npm:0.6.0"
+ dependencies:
+ safe-buffer: "npm:^5.0.1"
+ checksum: 10c0/4c7a1b813e7beae66fdbf567a65ec6d46313643753d0beefb3c7973d66fcec3a1e7f39759f0a0b4465883499c6dc8b0750ab8b287399af2e583823e40410a17a
+ languageName: node
+ linkType: hard
+
"type-check@npm:^0.4.0, type-check@npm:~0.4.0":
version: 0.4.0
resolution: "type-check@npm:0.4.0"