tfrere HF Staff commited on
Commit
6a34a48
·
1 Parent(s): 5576f9a

test(backend): add Playwright E2E suite + deps for editor basics, chat persistence and publish

Browse files

- Covers 9 flows: title persistence through Hocuspocus reload, settings drawer, chat panel
open/close, new-conversation clear, stable user identity across reloads, publish round-trip
- Bundled as `npm run test:e2e` (opt-in, not part of the fast hermetic suite) with spec-driven
fixtures that spin up a real Express + Hocuspocus server on an ephemeral port
- Brings playwright + @playwright /test into backend/package.json

Made-with: Cursor

backend/e2e/chat-persistence.test.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * E2E: Chat persistence across page reloads.
3
+ *
4
+ * Validates: send message -> receive AI response -> reload -> conversation restored.
5
+ * Uses mocked /api/chat endpoint via page.route() (no real LLM calls).
6
+ */
7
+ import { test, expect } from "./fixtures.js";
8
+
9
+ test.describe("Chat persistence", () => {
10
+ test("conversation survives page reload", async ({ appPage, serverUrl }) => {
11
+ // 1. Open chat panel
12
+ await appPage.click("[aria-label='AI Assistant']");
13
+ const input = appPage.getByPlaceholder("Ask anything...");
14
+ await expect(input).toBeVisible({ timeout: 3_000 });
15
+
16
+ // 2. Send a message
17
+ await input.fill("say hello");
18
+ await input.press("Enter");
19
+
20
+ // 3. Wait for AI response to appear
21
+ await expect(appPage.locator("text=Hello! I'm the AI assistant.")).toBeVisible({ timeout: 10_000 });
22
+
23
+ // 4. Wait for persistence to trigger (status transitions to "ready")
24
+ await appPage.waitForTimeout(2_000);
25
+
26
+ // 5. Verify localStorage has chat data
27
+ const chatKeys = await appPage.evaluate(() =>
28
+ Object.keys(localStorage).filter((k) => k.startsWith("chat:"))
29
+ );
30
+ expect(chatKeys.length).toBeGreaterThan(0);
31
+ expect(chatKeys[0]).toMatch(/^chat:.+:article$/);
32
+
33
+ // 6. Reload the page
34
+ await appPage.reload();
35
+ await appPage.waitForSelector("[aria-label='Undo']", { timeout: 15_000 });
36
+
37
+ // 7. Re-open chat and verify conversation is restored
38
+ await appPage.click("[aria-label='AI Assistant']");
39
+ await expect(appPage.locator("text=say hello")).toBeVisible({ timeout: 5_000 });
40
+ await expect(appPage.locator("text=Hello! I'm the AI assistant.")).toBeVisible({ timeout: 5_000 });
41
+ });
42
+
43
+ test("new conversation button clears messages", async ({ appPage }) => {
44
+ // 1. Open chat and send a message
45
+ await appPage.click("[aria-label='AI Assistant']");
46
+ const input = appPage.getByPlaceholder("Ask anything...");
47
+ await expect(input).toBeVisible({ timeout: 3_000 });
48
+ await input.fill("test message");
49
+ await input.press("Enter");
50
+ await expect(appPage.locator("text=Hello! I'm the AI assistant.")).toBeVisible({ timeout: 10_000 });
51
+
52
+ // Wait for persistence
53
+ await appPage.waitForTimeout(2_000);
54
+
55
+ // 2. Click "New conversation"
56
+ await appPage.click("[aria-label='New conversation']");
57
+
58
+ // 3. Verify the empty state is back
59
+ await expect(
60
+ appPage.locator("text=Ask me to write, edit, expand, or improve your article.")
61
+ ).toBeVisible({ timeout: 3_000 });
62
+
63
+ // 4. Verify localStorage was cleared
64
+ const chatKeys = await appPage.evaluate(() =>
65
+ Object.keys(localStorage).filter((k) => k.startsWith("chat:"))
66
+ );
67
+ expect(chatKeys.length).toBe(0);
68
+ });
69
+
70
+ test("stable user identity across reloads", async ({ appPage }) => {
71
+ const userName1 = await appPage.evaluate(
72
+ () => JSON.parse(localStorage.getItem("collab-editor:fallback-user")!).name
73
+ );
74
+
75
+ await appPage.reload();
76
+ await appPage.waitForSelector("[aria-label='Undo']", { timeout: 15_000 });
77
+
78
+ const userName2 = await appPage.evaluate(
79
+ () => JSON.parse(localStorage.getItem("collab-editor:fallback-user")!).name
80
+ );
81
+
82
+ expect(userName1).toBe(userName2);
83
+ });
84
+ });
backend/e2e/editor-basics.test.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * E2E: Editor basics.
3
+ *
4
+ * Validates: app loads, editor is interactive, content is persisted via Hocuspocus.
5
+ */
6
+ import { test, expect } from "./fixtures.js";
7
+
8
+ test.describe("Editor basics", () => {
9
+ test("app loads with editor toolbar", async ({ appPage }) => {
10
+ await expect(appPage.getByRole("button", { name: "Undo" })).toBeVisible();
11
+ await expect(appPage.getByRole("button", { name: "Redo" })).toBeVisible();
12
+ await expect(appPage.getByRole("button", { name: "AI Assistant" })).toBeVisible();
13
+ await expect(appPage.getByRole("button", { name: "Article settings" })).toBeVisible();
14
+ await expect(appPage.getByRole("button", { name: "Publish article" })).toBeVisible();
15
+ });
16
+
17
+ test("title field is editable", async ({ appPage }) => {
18
+ const titleInput = appPage.getByPlaceholder("Article title");
19
+ await titleInput.click();
20
+ await titleInput.fill("My Test Article");
21
+ await expect(titleInput).toHaveValue("My Test Article");
22
+ });
23
+
24
+ test("title persists across reload via Hocuspocus", async ({ appPage }) => {
25
+ const titleInput = appPage.getByPlaceholder("Article title");
26
+ await titleInput.click();
27
+ await titleInput.pressSequentially("Persistent Title", { delay: 20 });
28
+ await expect(titleInput).toHaveValue("Persistent Title");
29
+
30
+ // Blur to trigger commit() -> Y.Map update -> Hocuspocus sync
31
+ await appPage.click("body");
32
+ await appPage.waitForTimeout(2_000);
33
+
34
+ await appPage.reload();
35
+ await appPage.waitForSelector("[aria-label='Undo']", { timeout: 15_000 });
36
+
37
+ const reloadedTitle = appPage.getByPlaceholder("Article title");
38
+ await expect(reloadedTitle).toHaveValue("Persistent Title", { timeout: 10_000 });
39
+ });
40
+
41
+ test("settings drawer opens and shows template selector", async ({ appPage }) => {
42
+ await appPage.click("[aria-label='Article settings']");
43
+ // The drawer header "ARTICLE SETTINGS" is visible text
44
+ await expect(appPage.locator("text=ARTICLE SETTINGS")).toBeVisible({ timeout: 3_000 });
45
+ // Template select has the correct default value
46
+ const templateSelect = appPage.locator("select").first();
47
+ await expect(templateSelect).toHaveValue("article");
48
+ });
49
+
50
+ test("chat panel opens and closes", async ({ appPage }) => {
51
+ // Open
52
+ await appPage.click("[aria-label='AI Assistant']");
53
+ await expect(appPage.getByPlaceholder("Ask anything...")).toBeVisible({ timeout: 3_000 });
54
+
55
+ // Close
56
+ await appPage.click("[aria-label='Close chat']");
57
+ await expect(appPage.getByPlaceholder("Ask anything...")).not.toBeVisible({ timeout: 3_000 });
58
+ });
59
+ });
backend/e2e/fixtures.ts ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * E2E test fixtures: spins up a real server on a random port with
3
+ * mocked /api/chat and /api/embed-chat endpoints (no LLM calls).
4
+ */
5
+ import { test as base, type Page } from "@playwright/test";
6
+ import { mkdtempSync, rmSync } from "fs";
7
+ import { join, dirname } from "path";
8
+ import { fileURLToPath } from "url";
9
+ import type { AddressInfo } from "net";
10
+ import { createApp, resetSaveTimers } from "../src/create-app.js";
11
+ import { setDataDir } from "../src/utils.js";
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const BACKEND_DIR = join(dirname(__filename), "..");
15
+
16
+ const STREAM_HEADERS = {
17
+ "Content-Type": "text/event-stream",
18
+ "Cache-Control": "no-cache",
19
+ "Connection": "keep-alive",
20
+ "X-Accel-Buffering": "no",
21
+ "X-Vercel-AI-UI-Message-Stream": "v1",
22
+ };
23
+
24
+ function buildFakeStream(text: string): string {
25
+ const textId = `txt-${Date.now()}`;
26
+ const events = [
27
+ { type: "start" },
28
+ { type: "text-start", id: textId },
29
+ { type: "text-delta", id: textId, delta: text },
30
+ { type: "text-end", id: textId },
31
+ { type: "finish-step" },
32
+ { type: "finish" },
33
+ ];
34
+ const lines = events.map((e) => `data: ${JSON.stringify(e)}\n`).join("\n");
35
+ return lines + "\ndata: [DONE]\n\n";
36
+ }
37
+
38
+ interface TestFixtures {
39
+ serverUrl: string;
40
+ appPage: Page;
41
+ }
42
+
43
+ export const test = base.extend<TestFixtures>({
44
+ serverUrl: [
45
+ async ({}, use) => {
46
+ // Keep tmpDir under backend/ so relative staticDir path (../../frontend/dist) resolves
47
+ const tmpDir = mkdtempSync(join(BACKEND_DIR, ".e2e-data-"));
48
+ setDataDir(tmpDir);
49
+
50
+ const { app, httpServer, hocuspocus } = createApp();
51
+
52
+ await new Promise<void>((resolve) => httpServer.listen(0, resolve));
53
+ const port = (httpServer.address() as AddressInfo).port;
54
+ const url = `http://localhost:${port}`;
55
+
56
+ await use(url);
57
+
58
+ resetSaveTimers();
59
+ try { await hocuspocus.destroy(); } catch {}
60
+ try { httpServer.close(); } catch {}
61
+ setDataDir(undefined);
62
+ try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
63
+ },
64
+ { scope: "test" },
65
+ ],
66
+
67
+ appPage: async ({ page, serverUrl }, use) => {
68
+ // Intercept chat API calls at the browser level
69
+ await page.route("**/api/chat", async (route) => {
70
+ await route.fulfill({
71
+ status: 200,
72
+ headers: STREAM_HEADERS,
73
+ body: buildFakeStream("Hello! I'm the AI assistant."),
74
+ });
75
+ });
76
+
77
+ await page.route("**/api/embed-chat", async (route) => {
78
+ await route.fulfill({
79
+ status: 200,
80
+ headers: STREAM_HEADERS,
81
+ body: buildFakeStream("Chart created successfully."),
82
+ });
83
+ });
84
+
85
+ await page.goto(serverUrl);
86
+ // Wait for editor to be ready (toolbar visible)
87
+ await page.waitForSelector("[aria-label='Undo']", { timeout: 15_000 });
88
+ await use(page);
89
+ },
90
+ });
91
+
92
+ export { expect } from "@playwright/test";
backend/e2e/publish.test.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * E2E: Publish workflow.
3
+ *
4
+ * Validates: open publish dialog -> publish -> success feedback -> published HTML accessible.
5
+ */
6
+ import { test, expect } from "./fixtures.js";
7
+
8
+ test.describe("Publish", () => {
9
+ test("publish creates accessible HTML page", async ({ appPage, serverUrl }) => {
10
+ // Wait for editor content to load
11
+ await appPage.waitForSelector("[aria-label='Undo']", { timeout: 15_000 });
12
+
13
+ // Open publish dialog
14
+ await appPage.click("[aria-label='Publish article']");
15
+
16
+ // Look for the publish button inside the dialog
17
+ const publishDialog = appPage.locator("dialog");
18
+ const publishButton = publishDialog.getByRole("button", { name: /publish/i });
19
+ await publishButton.click();
20
+
21
+ // Wait for success (the dialog should update or close)
22
+ await appPage.waitForSelector("text=Published", { timeout: 15_000 });
23
+
24
+ // Verify published HTML is accessible at /published/default/index.html
25
+ const response = await appPage.request.get(`${serverUrl}/published/default/index.html`);
26
+ expect(response.status()).toBe(200);
27
+ const html = await response.text();
28
+ expect(html).toContain("<!DOCTYPE html");
29
+ expect(html).toContain("</html>");
30
+ });
31
+ });
backend/package-lock.json CHANGED
@@ -20,7 +20,6 @@
20
  "@huggingface/hub": "^2.11.0",
21
  "@openrouter/ai-sdk-provider": "^2.5.1",
22
  "@tiptap/core": "^3.22.3",
23
- "@tiptap/extension-code-block-lowlight": "^3.22.3",
24
  "@tiptap/extension-image": "^3.22.3",
25
  "@tiptap/extension-link": "^3.22.3",
26
  "@tiptap/extension-mathematics": "^3.22.3",
@@ -38,15 +37,19 @@
38
  "lowlight": "^3.3.0",
39
  "multer": "^2.1.1",
40
  "playwright": "^1.59.1",
 
41
  "ws": "^8.20.0",
42
  "yjs": "^13.6.0",
43
  "zod": "^4.3.6"
44
  },
45
  "devDependencies": {
 
46
  "@types/express": "^5.0.0",
47
  "@types/multer": "^2.1.0",
48
  "@types/node": "^22.0.0",
 
49
  "@types/ws": "^8.18.1",
 
50
  "tsx": "^4.19.0",
51
  "typescript": "^5.6.0",
52
  "vitest": "^4.1.4"
@@ -769,6 +772,19 @@
769
  "@emnapi/runtime": "^1.7.1"
770
  }
771
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  "node_modules/@openrouter/ai-sdk-provider": {
773
  "version": "2.5.1",
774
  "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-2.5.1.tgz",
@@ -801,6 +817,32 @@
801
  "url": "https://github.com/sponsors/Boshen"
802
  }
803
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
  "node_modules/@remirror/core-constants": {
805
  "version": "3.0.0",
806
  "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
@@ -1030,6 +1072,29 @@
1030
  "node": ">=14.0.0"
1031
  }
1032
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1033
  "node_modules/@rolldown/binding-win32-arm64-msvc": {
1034
  "version": "1.0.0-rc.15",
1035
  "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
@@ -1071,6 +1136,106 @@
1071
  "dev": true,
1072
  "license": "MIT"
1073
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1074
  "node_modules/@standard-schema/spec": {
1075
  "version": "1.1.0",
1076
  "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -1158,23 +1323,6 @@
1158
  "@tiptap/pm": "^3.22.3"
1159
  }
1160
  },
1161
- "node_modules/@tiptap/extension-code-block-lowlight": {
1162
- "version": "3.22.3",
1163
- "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.22.3.tgz",
1164
- "integrity": "sha512-NGFuD9zb1QfnCgD2zW4XaUEdQvd/ydm9FmXXh8eawx/+C8xt3p21DIKRERvxWrCvvVNzIUEpZRRYehPOJiD1eg==",
1165
- "license": "MIT",
1166
- "funding": {
1167
- "type": "github",
1168
- "url": "https://github.com/sponsors/ueberdosis"
1169
- },
1170
- "peerDependencies": {
1171
- "@tiptap/core": "^3.22.3",
1172
- "@tiptap/extension-code-block": "^3.22.3",
1173
- "@tiptap/pm": "^3.22.3",
1174
- "highlight.js": "^11",
1175
- "lowlight": "^2 || ^3"
1176
- }
1177
- },
1178
  "node_modules/@tiptap/extension-document": {
1179
  "version": "3.22.3",
1180
  "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.22.3.tgz",
@@ -1612,6 +1760,13 @@
1612
  "@types/node": "*"
1613
  }
1614
  },
 
 
 
 
 
 
 
1615
  "node_modules/@types/deep-eql": {
1616
  "version": "4.0.2",
1617
  "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -1683,12 +1838,28 @@
1683
  "@types/mdurl": "^2"
1684
  }
1685
  },
 
 
 
 
 
 
 
 
 
1686
  "node_modules/@types/mdurl": {
1687
  "version": "2.0.0",
1688
  "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
1689
  "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
1690
  "license": "MIT"
1691
  },
 
 
 
 
 
 
 
1692
  "node_modules/@types/multer": {
1693
  "version": "2.1.0",
1694
  "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz",
@@ -1743,6 +1914,30 @@
1743
  "@types/node": "*"
1744
  }
1745
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1746
  "node_modules/@types/unist": {
1747
  "version": "3.0.3",
1748
  "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -1764,6 +1959,12 @@
1764
  "@types/node": "*"
1765
  }
1766
  },
 
 
 
 
 
 
1767
  "node_modules/@vercel/oidc": {
1768
  "version": "3.1.0",
1769
  "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz",
@@ -1946,6 +2147,13 @@
1946
  "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
1947
  "license": "MIT"
1948
  },
 
 
 
 
 
 
 
1949
  "node_modules/assertion-error": {
1950
  "version": "2.0.1",
1951
  "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -1971,6 +2179,13 @@
1971
  "tslib": "^2.4.0"
1972
  }
1973
  },
 
 
 
 
 
 
 
1974
  "node_modules/base64-js": {
1975
  "version": "1.5.1",
1976
  "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -2100,6 +2315,16 @@
2100
  "url": "https://github.com/sponsors/ljharb"
2101
  }
2102
  },
 
 
 
 
 
 
 
 
 
 
2103
  "node_modules/chai": {
2104
  "version": "6.2.2",
2105
  "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -2110,6 +2335,26 @@
2110
  "node": ">=18"
2111
  }
2112
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2113
  "node_modules/citeproc": {
2114
  "version": "2.4.63",
2115
  "resolved": "https://registry.npmjs.org/citeproc/-/citeproc-2.4.63.tgz",
@@ -2129,6 +2374,29 @@
2129
  "node": ">=4"
2130
  }
2131
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2132
  "node_modules/commander": {
2133
  "version": "8.3.0",
2134
  "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
@@ -2138,6 +2406,16 @@
2138
  "node": ">= 12"
2139
  }
2140
  },
 
 
 
 
 
 
 
 
 
 
2141
  "node_modules/concat-stream": {
2142
  "version": "2.0.0",
2143
  "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
@@ -2196,6 +2474,13 @@
2196
  "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
2197
  "license": "MIT"
2198
  },
 
 
 
 
 
 
 
2199
  "node_modules/crelt": {
2200
  "version": "1.0.6",
2201
  "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
@@ -2245,6 +2530,16 @@
2245
  "ms": "2.0.0"
2246
  }
2247
  },
 
 
 
 
 
 
 
 
 
 
2248
  "node_modules/depd": {
2249
  "version": "2.0.0",
2250
  "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -2296,6 +2591,17 @@
2296
  "url": "https://github.com/sponsors/wooorm"
2297
  }
2298
  },
 
 
 
 
 
 
 
 
 
 
 
2299
  "node_modules/dom-serializer": {
2300
  "version": "2.0.0",
2301
  "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -2460,6 +2766,22 @@
2460
  "node": ">= 0.4"
2461
  }
2462
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2463
  "node_modules/esbuild": {
2464
  "version": "0.27.7",
2465
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
@@ -2604,6 +2926,13 @@
2604
  "url": "https://opencollective.com/express"
2605
  }
2606
  },
 
 
 
 
 
 
 
2607
  "node_modules/fdir": {
2608
  "version": "6.5.0",
2609
  "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -2649,6 +2978,41 @@
2649
  "node": ">= 0.8"
2650
  }
2651
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2652
  "node_modules/forwarded": {
2653
  "version": "0.2.0",
2654
  "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -2783,6 +3147,22 @@
2783
  "url": "https://github.com/sponsors/ljharb"
2784
  }
2785
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2786
  "node_modules/hasown": {
2787
  "version": "2.0.2",
2788
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -2795,6 +3175,42 @@
2795
  "node": ">= 0.4"
2796
  }
2797
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2798
  "node_modules/highlight.js": {
2799
  "version": "11.11.1",
2800
  "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
@@ -2811,6 +3227,16 @@
2811
  "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
2812
  "license": "MIT"
2813
  },
 
 
 
 
 
 
 
 
 
 
2814
  "node_modules/htmlparser2": {
2815
  "version": "10.1.0",
2816
  "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
@@ -3334,6 +3760,27 @@
3334
  "node": ">= 0.4"
3335
  }
3336
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3337
  "node_modules/mdurl": {
3338
  "version": "2.0.0",
3339
  "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
@@ -3367,6 +3814,95 @@
3367
  "node": ">= 0.6"
3368
  }
3369
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3370
  "node_modules/mime": {
3371
  "version": "1.6.0",
3372
  "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -3526,6 +4062,33 @@
3526
  "node": ">= 0.8"
3527
  }
3528
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3529
  "node_modules/orderedmap": {
3530
  "version": "2.1.1",
3531
  "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
@@ -3648,6 +4211,16 @@
3648
  "node": "^10 || ^12 || >=14"
3649
  }
3650
  },
 
 
 
 
 
 
 
 
 
 
3651
  "node_modules/prosemirror-changeset": {
3652
  "version": "2.4.0",
3653
  "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
@@ -3921,6 +4494,30 @@
3921
  "node": ">= 6"
3922
  }
3923
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3924
  "node_modules/resolve-pkg-maps": {
3925
  "version": "1.0.0",
3926
  "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@@ -4048,6 +4645,25 @@
4048
  "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
4049
  "license": "ISC"
4050
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4051
  "node_modules/side-channel": {
4052
  "version": "1.1.0",
4053
  "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@@ -4137,6 +4753,16 @@
4137
  "node": ">=0.10.0"
4138
  }
4139
  },
 
 
 
 
 
 
 
 
 
 
4140
  "node_modules/stackback": {
4141
  "version": "0.0.2",
4142
  "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -4192,6 +4818,20 @@
4192
  "node": ">=8"
4193
  }
4194
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4195
  "node_modules/strip-ansi": {
4196
  "version": "6.0.1",
4197
  "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -4205,6 +4845,90 @@
4205
  "node": ">=8"
4206
  }
4207
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4208
  "node_modules/sync-fetch": {
4209
  "version": "0.4.5",
4210
  "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.5.tgz",
@@ -4277,6 +5001,16 @@
4277
  "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
4278
  "license": "MIT"
4279
  },
 
 
 
 
 
 
 
 
 
 
4280
  "node_modules/tslib": {
4281
  "version": "2.8.1",
4282
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -4355,6 +5089,74 @@
4355
  "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
4356
  "license": "MIT"
4357
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4358
  "node_modules/unpipe": {
4359
  "version": "1.0.0",
4360
  "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -4388,6 +5190,34 @@
4388
  "node": ">= 0.8"
4389
  }
4390
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4391
  "node_modules/vite": {
4392
  "version": "8.0.8",
4393
  "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
@@ -4605,6 +5435,13 @@
4605
  "node": ">=8"
4606
  }
4607
  },
 
 
 
 
 
 
 
4608
  "node_modules/ws": {
4609
  "version": "8.20.0",
4610
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
@@ -4699,6 +5536,16 @@
4699
  "funding": {
4700
  "url": "https://github.com/sponsors/colinhacks"
4701
  }
 
 
 
 
 
 
 
 
 
 
4702
  }
4703
  }
4704
  }
 
20
  "@huggingface/hub": "^2.11.0",
21
  "@openrouter/ai-sdk-provider": "^2.5.1",
22
  "@tiptap/core": "^3.22.3",
 
23
  "@tiptap/extension-image": "^3.22.3",
24
  "@tiptap/extension-link": "^3.22.3",
25
  "@tiptap/extension-mathematics": "^3.22.3",
 
37
  "lowlight": "^3.3.0",
38
  "multer": "^2.1.1",
39
  "playwright": "^1.59.1",
40
+ "shiki": "^4.0.2",
41
  "ws": "^8.20.0",
42
  "yjs": "^13.6.0",
43
  "zod": "^4.3.6"
44
  },
45
  "devDependencies": {
46
+ "@playwright/test": "^1.59.1",
47
  "@types/express": "^5.0.0",
48
  "@types/multer": "^2.1.0",
49
  "@types/node": "^22.0.0",
50
+ "@types/supertest": "^7.2.0",
51
  "@types/ws": "^8.18.1",
52
+ "supertest": "^7.2.2",
53
  "tsx": "^4.19.0",
54
  "typescript": "^5.6.0",
55
  "vitest": "^4.1.4"
 
772
  "@emnapi/runtime": "^1.7.1"
773
  }
774
  },
775
+ "node_modules/@noble/hashes": {
776
+ "version": "1.8.0",
777
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
778
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
779
+ "dev": true,
780
+ "license": "MIT",
781
+ "engines": {
782
+ "node": "^14.21.3 || >=16"
783
+ },
784
+ "funding": {
785
+ "url": "https://paulmillr.com/funding/"
786
+ }
787
+ },
788
  "node_modules/@openrouter/ai-sdk-provider": {
789
  "version": "2.5.1",
790
  "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-2.5.1.tgz",
 
817
  "url": "https://github.com/sponsors/Boshen"
818
  }
819
  },
820
+ "node_modules/@paralleldrive/cuid2": {
821
+ "version": "2.3.1",
822
+ "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
823
+ "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
824
+ "dev": true,
825
+ "license": "MIT",
826
+ "dependencies": {
827
+ "@noble/hashes": "^1.1.5"
828
+ }
829
+ },
830
+ "node_modules/@playwright/test": {
831
+ "version": "1.59.1",
832
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
833
+ "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
834
+ "dev": true,
835
+ "license": "Apache-2.0",
836
+ "dependencies": {
837
+ "playwright": "1.59.1"
838
+ },
839
+ "bin": {
840
+ "playwright": "cli.js"
841
+ },
842
+ "engines": {
843
+ "node": ">=18"
844
+ }
845
+ },
846
  "node_modules/@remirror/core-constants": {
847
  "version": "3.0.0",
848
  "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
 
1072
  "node": ">=14.0.0"
1073
  }
1074
  },
1075
+ "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
1076
+ "version": "1.9.2",
1077
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
1078
+ "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
1079
+ "dev": true,
1080
+ "license": "MIT",
1081
+ "optional": true,
1082
+ "dependencies": {
1083
+ "@emnapi/wasi-threads": "1.2.1",
1084
+ "tslib": "^2.4.0"
1085
+ }
1086
+ },
1087
+ "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
1088
+ "version": "1.9.2",
1089
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
1090
+ "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
1091
+ "dev": true,
1092
+ "license": "MIT",
1093
+ "optional": true,
1094
+ "dependencies": {
1095
+ "tslib": "^2.4.0"
1096
+ }
1097
+ },
1098
  "node_modules/@rolldown/binding-win32-arm64-msvc": {
1099
  "version": "1.0.0-rc.15",
1100
  "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
 
1136
  "dev": true,
1137
  "license": "MIT"
1138
  },
1139
+ "node_modules/@shikijs/core": {
1140
+ "version": "4.0.2",
1141
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz",
1142
+ "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==",
1143
+ "license": "MIT",
1144
+ "dependencies": {
1145
+ "@shikijs/primitive": "4.0.2",
1146
+ "@shikijs/types": "4.0.2",
1147
+ "@shikijs/vscode-textmate": "^10.0.2",
1148
+ "@types/hast": "^3.0.4",
1149
+ "hast-util-to-html": "^9.0.5"
1150
+ },
1151
+ "engines": {
1152
+ "node": ">=20"
1153
+ }
1154
+ },
1155
+ "node_modules/@shikijs/engine-javascript": {
1156
+ "version": "4.0.2",
1157
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz",
1158
+ "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==",
1159
+ "license": "MIT",
1160
+ "dependencies": {
1161
+ "@shikijs/types": "4.0.2",
1162
+ "@shikijs/vscode-textmate": "^10.0.2",
1163
+ "oniguruma-to-es": "^4.3.4"
1164
+ },
1165
+ "engines": {
1166
+ "node": ">=20"
1167
+ }
1168
+ },
1169
+ "node_modules/@shikijs/engine-oniguruma": {
1170
+ "version": "4.0.2",
1171
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz",
1172
+ "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==",
1173
+ "license": "MIT",
1174
+ "dependencies": {
1175
+ "@shikijs/types": "4.0.2",
1176
+ "@shikijs/vscode-textmate": "^10.0.2"
1177
+ },
1178
+ "engines": {
1179
+ "node": ">=20"
1180
+ }
1181
+ },
1182
+ "node_modules/@shikijs/langs": {
1183
+ "version": "4.0.2",
1184
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz",
1185
+ "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==",
1186
+ "license": "MIT",
1187
+ "dependencies": {
1188
+ "@shikijs/types": "4.0.2"
1189
+ },
1190
+ "engines": {
1191
+ "node": ">=20"
1192
+ }
1193
+ },
1194
+ "node_modules/@shikijs/primitive": {
1195
+ "version": "4.0.2",
1196
+ "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz",
1197
+ "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==",
1198
+ "license": "MIT",
1199
+ "dependencies": {
1200
+ "@shikijs/types": "4.0.2",
1201
+ "@shikijs/vscode-textmate": "^10.0.2",
1202
+ "@types/hast": "^3.0.4"
1203
+ },
1204
+ "engines": {
1205
+ "node": ">=20"
1206
+ }
1207
+ },
1208
+ "node_modules/@shikijs/themes": {
1209
+ "version": "4.0.2",
1210
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz",
1211
+ "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==",
1212
+ "license": "MIT",
1213
+ "dependencies": {
1214
+ "@shikijs/types": "4.0.2"
1215
+ },
1216
+ "engines": {
1217
+ "node": ">=20"
1218
+ }
1219
+ },
1220
+ "node_modules/@shikijs/types": {
1221
+ "version": "4.0.2",
1222
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz",
1223
+ "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==",
1224
+ "license": "MIT",
1225
+ "dependencies": {
1226
+ "@shikijs/vscode-textmate": "^10.0.2",
1227
+ "@types/hast": "^3.0.4"
1228
+ },
1229
+ "engines": {
1230
+ "node": ">=20"
1231
+ }
1232
+ },
1233
+ "node_modules/@shikijs/vscode-textmate": {
1234
+ "version": "10.0.2",
1235
+ "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz",
1236
+ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
1237
+ "license": "MIT"
1238
+ },
1239
  "node_modules/@standard-schema/spec": {
1240
  "version": "1.1.0",
1241
  "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
 
1323
  "@tiptap/pm": "^3.22.3"
1324
  }
1325
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1326
  "node_modules/@tiptap/extension-document": {
1327
  "version": "3.22.3",
1328
  "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.22.3.tgz",
 
1760
  "@types/node": "*"
1761
  }
1762
  },
1763
+ "node_modules/@types/cookiejar": {
1764
+ "version": "2.1.5",
1765
+ "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
1766
+ "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
1767
+ "dev": true,
1768
+ "license": "MIT"
1769
+ },
1770
  "node_modules/@types/deep-eql": {
1771
  "version": "4.0.2",
1772
  "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
 
1838
  "@types/mdurl": "^2"
1839
  }
1840
  },
1841
+ "node_modules/@types/mdast": {
1842
+ "version": "4.0.4",
1843
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
1844
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
1845
+ "license": "MIT",
1846
+ "dependencies": {
1847
+ "@types/unist": "*"
1848
+ }
1849
+ },
1850
  "node_modules/@types/mdurl": {
1851
  "version": "2.0.0",
1852
  "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
1853
  "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
1854
  "license": "MIT"
1855
  },
1856
+ "node_modules/@types/methods": {
1857
+ "version": "1.1.4",
1858
+ "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
1859
+ "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
1860
+ "dev": true,
1861
+ "license": "MIT"
1862
+ },
1863
  "node_modules/@types/multer": {
1864
  "version": "2.1.0",
1865
  "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz",
 
1914
  "@types/node": "*"
1915
  }
1916
  },
1917
+ "node_modules/@types/superagent": {
1918
+ "version": "8.1.9",
1919
+ "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz",
1920
+ "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==",
1921
+ "dev": true,
1922
+ "license": "MIT",
1923
+ "dependencies": {
1924
+ "@types/cookiejar": "^2.1.5",
1925
+ "@types/methods": "^1.1.4",
1926
+ "@types/node": "*",
1927
+ "form-data": "^4.0.0"
1928
+ }
1929
+ },
1930
+ "node_modules/@types/supertest": {
1931
+ "version": "7.2.0",
1932
+ "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-7.2.0.tgz",
1933
+ "integrity": "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==",
1934
+ "dev": true,
1935
+ "license": "MIT",
1936
+ "dependencies": {
1937
+ "@types/methods": "^1.1.4",
1938
+ "@types/superagent": "^8.1.0"
1939
+ }
1940
+ },
1941
  "node_modules/@types/unist": {
1942
  "version": "3.0.3",
1943
  "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
 
1959
  "@types/node": "*"
1960
  }
1961
  },
1962
+ "node_modules/@ungap/structured-clone": {
1963
+ "version": "1.3.0",
1964
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
1965
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
1966
+ "license": "ISC"
1967
+ },
1968
  "node_modules/@vercel/oidc": {
1969
  "version": "3.1.0",
1970
  "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz",
 
2147
  "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
2148
  "license": "MIT"
2149
  },
2150
+ "node_modules/asap": {
2151
+ "version": "2.0.6",
2152
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
2153
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
2154
+ "dev": true,
2155
+ "license": "MIT"
2156
+ },
2157
  "node_modules/assertion-error": {
2158
  "version": "2.0.1",
2159
  "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
 
2179
  "tslib": "^2.4.0"
2180
  }
2181
  },
2182
+ "node_modules/asynckit": {
2183
+ "version": "0.4.0",
2184
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
2185
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
2186
+ "dev": true,
2187
+ "license": "MIT"
2188
+ },
2189
  "node_modules/base64-js": {
2190
  "version": "1.5.1",
2191
  "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
 
2315
  "url": "https://github.com/sponsors/ljharb"
2316
  }
2317
  },
2318
+ "node_modules/ccount": {
2319
+ "version": "2.0.1",
2320
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
2321
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
2322
+ "license": "MIT",
2323
+ "funding": {
2324
+ "type": "github",
2325
+ "url": "https://github.com/sponsors/wooorm"
2326
+ }
2327
+ },
2328
  "node_modules/chai": {
2329
  "version": "6.2.2",
2330
  "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
 
2335
  "node": ">=18"
2336
  }
2337
  },
2338
+ "node_modules/character-entities-html4": {
2339
+ "version": "2.1.0",
2340
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
2341
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
2342
+ "license": "MIT",
2343
+ "funding": {
2344
+ "type": "github",
2345
+ "url": "https://github.com/sponsors/wooorm"
2346
+ }
2347
+ },
2348
+ "node_modules/character-entities-legacy": {
2349
+ "version": "3.0.0",
2350
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
2351
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
2352
+ "license": "MIT",
2353
+ "funding": {
2354
+ "type": "github",
2355
+ "url": "https://github.com/sponsors/wooorm"
2356
+ }
2357
+ },
2358
  "node_modules/citeproc": {
2359
  "version": "2.4.63",
2360
  "resolved": "https://registry.npmjs.org/citeproc/-/citeproc-2.4.63.tgz",
 
2374
  "node": ">=4"
2375
  }
2376
  },
2377
+ "node_modules/combined-stream": {
2378
+ "version": "1.0.8",
2379
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
2380
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
2381
+ "dev": true,
2382
+ "license": "MIT",
2383
+ "dependencies": {
2384
+ "delayed-stream": "~1.0.0"
2385
+ },
2386
+ "engines": {
2387
+ "node": ">= 0.8"
2388
+ }
2389
+ },
2390
+ "node_modules/comma-separated-tokens": {
2391
+ "version": "2.0.3",
2392
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
2393
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
2394
+ "license": "MIT",
2395
+ "funding": {
2396
+ "type": "github",
2397
+ "url": "https://github.com/sponsors/wooorm"
2398
+ }
2399
+ },
2400
  "node_modules/commander": {
2401
  "version": "8.3.0",
2402
  "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
 
2406
  "node": ">= 12"
2407
  }
2408
  },
2409
+ "node_modules/component-emitter": {
2410
+ "version": "1.3.1",
2411
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
2412
+ "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
2413
+ "dev": true,
2414
+ "license": "MIT",
2415
+ "funding": {
2416
+ "url": "https://github.com/sponsors/sindresorhus"
2417
+ }
2418
+ },
2419
  "node_modules/concat-stream": {
2420
  "version": "2.0.0",
2421
  "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
 
2474
  "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
2475
  "license": "MIT"
2476
  },
2477
+ "node_modules/cookiejar": {
2478
+ "version": "2.1.4",
2479
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
2480
+ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
2481
+ "dev": true,
2482
+ "license": "MIT"
2483
+ },
2484
  "node_modules/crelt": {
2485
  "version": "1.0.6",
2486
  "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
 
2530
  "ms": "2.0.0"
2531
  }
2532
  },
2533
+ "node_modules/delayed-stream": {
2534
+ "version": "1.0.0",
2535
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
2536
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
2537
+ "dev": true,
2538
+ "license": "MIT",
2539
+ "engines": {
2540
+ "node": ">=0.4.0"
2541
+ }
2542
+ },
2543
  "node_modules/depd": {
2544
  "version": "2.0.0",
2545
  "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
 
2591
  "url": "https://github.com/sponsors/wooorm"
2592
  }
2593
  },
2594
+ "node_modules/dezalgo": {
2595
+ "version": "1.0.4",
2596
+ "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
2597
+ "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
2598
+ "dev": true,
2599
+ "license": "ISC",
2600
+ "dependencies": {
2601
+ "asap": "^2.0.0",
2602
+ "wrappy": "1"
2603
+ }
2604
+ },
2605
  "node_modules/dom-serializer": {
2606
  "version": "2.0.0",
2607
  "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
 
2766
  "node": ">= 0.4"
2767
  }
2768
  },
2769
+ "node_modules/es-set-tostringtag": {
2770
+ "version": "2.1.0",
2771
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
2772
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
2773
+ "dev": true,
2774
+ "license": "MIT",
2775
+ "dependencies": {
2776
+ "es-errors": "^1.3.0",
2777
+ "get-intrinsic": "^1.2.6",
2778
+ "has-tostringtag": "^1.0.2",
2779
+ "hasown": "^2.0.2"
2780
+ },
2781
+ "engines": {
2782
+ "node": ">= 0.4"
2783
+ }
2784
+ },
2785
  "node_modules/esbuild": {
2786
  "version": "0.27.7",
2787
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
 
2926
  "url": "https://opencollective.com/express"
2927
  }
2928
  },
2929
+ "node_modules/fast-safe-stringify": {
2930
+ "version": "2.1.1",
2931
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
2932
+ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
2933
+ "dev": true,
2934
+ "license": "MIT"
2935
+ },
2936
  "node_modules/fdir": {
2937
  "version": "6.5.0",
2938
  "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
 
2978
  "node": ">= 0.8"
2979
  }
2980
  },
2981
+ "node_modules/form-data": {
2982
+ "version": "4.0.5",
2983
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
2984
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
2985
+ "dev": true,
2986
+ "license": "MIT",
2987
+ "dependencies": {
2988
+ "asynckit": "^0.4.0",
2989
+ "combined-stream": "^1.0.8",
2990
+ "es-set-tostringtag": "^2.1.0",
2991
+ "hasown": "^2.0.2",
2992
+ "mime-types": "^2.1.12"
2993
+ },
2994
+ "engines": {
2995
+ "node": ">= 6"
2996
+ }
2997
+ },
2998
+ "node_modules/formidable": {
2999
+ "version": "3.5.4",
3000
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
3001
+ "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
3002
+ "dev": true,
3003
+ "license": "MIT",
3004
+ "dependencies": {
3005
+ "@paralleldrive/cuid2": "^2.2.2",
3006
+ "dezalgo": "^1.0.4",
3007
+ "once": "^1.4.0"
3008
+ },
3009
+ "engines": {
3010
+ "node": ">=14.0.0"
3011
+ },
3012
+ "funding": {
3013
+ "url": "https://ko-fi.com/tunnckoCore/commissions"
3014
+ }
3015
+ },
3016
  "node_modules/forwarded": {
3017
  "version": "0.2.0",
3018
  "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
 
3147
  "url": "https://github.com/sponsors/ljharb"
3148
  }
3149
  },
3150
+ "node_modules/has-tostringtag": {
3151
+ "version": "1.0.2",
3152
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
3153
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
3154
+ "dev": true,
3155
+ "license": "MIT",
3156
+ "dependencies": {
3157
+ "has-symbols": "^1.0.3"
3158
+ },
3159
+ "engines": {
3160
+ "node": ">= 0.4"
3161
+ },
3162
+ "funding": {
3163
+ "url": "https://github.com/sponsors/ljharb"
3164
+ }
3165
+ },
3166
  "node_modules/hasown": {
3167
  "version": "2.0.2",
3168
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
 
3175
  "node": ">= 0.4"
3176
  }
3177
  },
3178
+ "node_modules/hast-util-to-html": {
3179
+ "version": "9.0.5",
3180
+ "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
3181
+ "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==",
3182
+ "license": "MIT",
3183
+ "dependencies": {
3184
+ "@types/hast": "^3.0.0",
3185
+ "@types/unist": "^3.0.0",
3186
+ "ccount": "^2.0.0",
3187
+ "comma-separated-tokens": "^2.0.0",
3188
+ "hast-util-whitespace": "^3.0.0",
3189
+ "html-void-elements": "^3.0.0",
3190
+ "mdast-util-to-hast": "^13.0.0",
3191
+ "property-information": "^7.0.0",
3192
+ "space-separated-tokens": "^2.0.0",
3193
+ "stringify-entities": "^4.0.0",
3194
+ "zwitch": "^2.0.4"
3195
+ },
3196
+ "funding": {
3197
+ "type": "opencollective",
3198
+ "url": "https://opencollective.com/unified"
3199
+ }
3200
+ },
3201
+ "node_modules/hast-util-whitespace": {
3202
+ "version": "3.0.0",
3203
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
3204
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
3205
+ "license": "MIT",
3206
+ "dependencies": {
3207
+ "@types/hast": "^3.0.0"
3208
+ },
3209
+ "funding": {
3210
+ "type": "opencollective",
3211
+ "url": "https://opencollective.com/unified"
3212
+ }
3213
+ },
3214
  "node_modules/highlight.js": {
3215
  "version": "11.11.1",
3216
  "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
 
3227
  "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
3228
  "license": "MIT"
3229
  },
3230
+ "node_modules/html-void-elements": {
3231
+ "version": "3.0.0",
3232
+ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
3233
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
3234
+ "license": "MIT",
3235
+ "funding": {
3236
+ "type": "github",
3237
+ "url": "https://github.com/sponsors/wooorm"
3238
+ }
3239
+ },
3240
  "node_modules/htmlparser2": {
3241
  "version": "10.1.0",
3242
  "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
 
3760
  "node": ">= 0.4"
3761
  }
3762
  },
3763
+ "node_modules/mdast-util-to-hast": {
3764
+ "version": "13.2.1",
3765
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
3766
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
3767
+ "license": "MIT",
3768
+ "dependencies": {
3769
+ "@types/hast": "^3.0.0",
3770
+ "@types/mdast": "^4.0.0",
3771
+ "@ungap/structured-clone": "^1.0.0",
3772
+ "devlop": "^1.0.0",
3773
+ "micromark-util-sanitize-uri": "^2.0.0",
3774
+ "trim-lines": "^3.0.0",
3775
+ "unist-util-position": "^5.0.0",
3776
+ "unist-util-visit": "^5.0.0",
3777
+ "vfile": "^6.0.0"
3778
+ },
3779
+ "funding": {
3780
+ "type": "opencollective",
3781
+ "url": "https://opencollective.com/unified"
3782
+ }
3783
+ },
3784
  "node_modules/mdurl": {
3785
  "version": "2.0.0",
3786
  "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
 
3814
  "node": ">= 0.6"
3815
  }
3816
  },
3817
+ "node_modules/micromark-util-character": {
3818
+ "version": "2.1.1",
3819
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
3820
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
3821
+ "funding": [
3822
+ {
3823
+ "type": "GitHub Sponsors",
3824
+ "url": "https://github.com/sponsors/unifiedjs"
3825
+ },
3826
+ {
3827
+ "type": "OpenCollective",
3828
+ "url": "https://opencollective.com/unified"
3829
+ }
3830
+ ],
3831
+ "license": "MIT",
3832
+ "dependencies": {
3833
+ "micromark-util-symbol": "^2.0.0",
3834
+ "micromark-util-types": "^2.0.0"
3835
+ }
3836
+ },
3837
+ "node_modules/micromark-util-encode": {
3838
+ "version": "2.0.1",
3839
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
3840
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
3841
+ "funding": [
3842
+ {
3843
+ "type": "GitHub Sponsors",
3844
+ "url": "https://github.com/sponsors/unifiedjs"
3845
+ },
3846
+ {
3847
+ "type": "OpenCollective",
3848
+ "url": "https://opencollective.com/unified"
3849
+ }
3850
+ ],
3851
+ "license": "MIT"
3852
+ },
3853
+ "node_modules/micromark-util-sanitize-uri": {
3854
+ "version": "2.0.1",
3855
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
3856
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
3857
+ "funding": [
3858
+ {
3859
+ "type": "GitHub Sponsors",
3860
+ "url": "https://github.com/sponsors/unifiedjs"
3861
+ },
3862
+ {
3863
+ "type": "OpenCollective",
3864
+ "url": "https://opencollective.com/unified"
3865
+ }
3866
+ ],
3867
+ "license": "MIT",
3868
+ "dependencies": {
3869
+ "micromark-util-character": "^2.0.0",
3870
+ "micromark-util-encode": "^2.0.0",
3871
+ "micromark-util-symbol": "^2.0.0"
3872
+ }
3873
+ },
3874
+ "node_modules/micromark-util-symbol": {
3875
+ "version": "2.0.1",
3876
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
3877
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
3878
+ "funding": [
3879
+ {
3880
+ "type": "GitHub Sponsors",
3881
+ "url": "https://github.com/sponsors/unifiedjs"
3882
+ },
3883
+ {
3884
+ "type": "OpenCollective",
3885
+ "url": "https://opencollective.com/unified"
3886
+ }
3887
+ ],
3888
+ "license": "MIT"
3889
+ },
3890
+ "node_modules/micromark-util-types": {
3891
+ "version": "2.0.2",
3892
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
3893
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
3894
+ "funding": [
3895
+ {
3896
+ "type": "GitHub Sponsors",
3897
+ "url": "https://github.com/sponsors/unifiedjs"
3898
+ },
3899
+ {
3900
+ "type": "OpenCollective",
3901
+ "url": "https://opencollective.com/unified"
3902
+ }
3903
+ ],
3904
+ "license": "MIT"
3905
+ },
3906
  "node_modules/mime": {
3907
  "version": "1.6.0",
3908
  "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
 
4062
  "node": ">= 0.8"
4063
  }
4064
  },
4065
+ "node_modules/once": {
4066
+ "version": "1.4.0",
4067
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
4068
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
4069
+ "dev": true,
4070
+ "license": "ISC",
4071
+ "dependencies": {
4072
+ "wrappy": "1"
4073
+ }
4074
+ },
4075
+ "node_modules/oniguruma-parser": {
4076
+ "version": "0.12.2",
4077
+ "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
4078
+ "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
4079
+ "license": "MIT"
4080
+ },
4081
+ "node_modules/oniguruma-to-es": {
4082
+ "version": "4.3.6",
4083
+ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
4084
+ "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
4085
+ "license": "MIT",
4086
+ "dependencies": {
4087
+ "oniguruma-parser": "^0.12.2",
4088
+ "regex": "^6.1.0",
4089
+ "regex-recursion": "^6.0.2"
4090
+ }
4091
+ },
4092
  "node_modules/orderedmap": {
4093
  "version": "2.1.1",
4094
  "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
 
4211
  "node": "^10 || ^12 || >=14"
4212
  }
4213
  },
4214
+ "node_modules/property-information": {
4215
+ "version": "7.1.0",
4216
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
4217
+ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
4218
+ "license": "MIT",
4219
+ "funding": {
4220
+ "type": "github",
4221
+ "url": "https://github.com/sponsors/wooorm"
4222
+ }
4223
+ },
4224
  "node_modules/prosemirror-changeset": {
4225
  "version": "2.4.0",
4226
  "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
 
4494
  "node": ">= 6"
4495
  }
4496
  },
4497
+ "node_modules/regex": {
4498
+ "version": "6.1.0",
4499
+ "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
4500
+ "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==",
4501
+ "license": "MIT",
4502
+ "dependencies": {
4503
+ "regex-utilities": "^2.3.0"
4504
+ }
4505
+ },
4506
+ "node_modules/regex-recursion": {
4507
+ "version": "6.0.2",
4508
+ "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz",
4509
+ "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==",
4510
+ "license": "MIT",
4511
+ "dependencies": {
4512
+ "regex-utilities": "^2.3.0"
4513
+ }
4514
+ },
4515
+ "node_modules/regex-utilities": {
4516
+ "version": "2.3.0",
4517
+ "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
4518
+ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
4519
+ "license": "MIT"
4520
+ },
4521
  "node_modules/resolve-pkg-maps": {
4522
  "version": "1.0.0",
4523
  "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
 
4645
  "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
4646
  "license": "ISC"
4647
  },
4648
+ "node_modules/shiki": {
4649
+ "version": "4.0.2",
4650
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz",
4651
+ "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==",
4652
+ "license": "MIT",
4653
+ "dependencies": {
4654
+ "@shikijs/core": "4.0.2",
4655
+ "@shikijs/engine-javascript": "4.0.2",
4656
+ "@shikijs/engine-oniguruma": "4.0.2",
4657
+ "@shikijs/langs": "4.0.2",
4658
+ "@shikijs/themes": "4.0.2",
4659
+ "@shikijs/types": "4.0.2",
4660
+ "@shikijs/vscode-textmate": "^10.0.2",
4661
+ "@types/hast": "^3.0.4"
4662
+ },
4663
+ "engines": {
4664
+ "node": ">=20"
4665
+ }
4666
+ },
4667
  "node_modules/side-channel": {
4668
  "version": "1.1.0",
4669
  "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
 
4753
  "node": ">=0.10.0"
4754
  }
4755
  },
4756
+ "node_modules/space-separated-tokens": {
4757
+ "version": "2.0.2",
4758
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
4759
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
4760
+ "license": "MIT",
4761
+ "funding": {
4762
+ "type": "github",
4763
+ "url": "https://github.com/sponsors/wooorm"
4764
+ }
4765
+ },
4766
  "node_modules/stackback": {
4767
  "version": "0.0.2",
4768
  "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
 
4818
  "node": ">=8"
4819
  }
4820
  },
4821
+ "node_modules/stringify-entities": {
4822
+ "version": "4.0.4",
4823
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
4824
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
4825
+ "license": "MIT",
4826
+ "dependencies": {
4827
+ "character-entities-html4": "^2.0.0",
4828
+ "character-entities-legacy": "^3.0.0"
4829
+ },
4830
+ "funding": {
4831
+ "type": "github",
4832
+ "url": "https://github.com/sponsors/wooorm"
4833
+ }
4834
+ },
4835
  "node_modules/strip-ansi": {
4836
  "version": "6.0.1",
4837
  "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
 
4845
  "node": ">=8"
4846
  }
4847
  },
4848
+ "node_modules/superagent": {
4849
+ "version": "10.3.0",
4850
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz",
4851
+ "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==",
4852
+ "dev": true,
4853
+ "license": "MIT",
4854
+ "dependencies": {
4855
+ "component-emitter": "^1.3.1",
4856
+ "cookiejar": "^2.1.4",
4857
+ "debug": "^4.3.7",
4858
+ "fast-safe-stringify": "^2.1.1",
4859
+ "form-data": "^4.0.5",
4860
+ "formidable": "^3.5.4",
4861
+ "methods": "^1.1.2",
4862
+ "mime": "2.6.0",
4863
+ "qs": "^6.14.1"
4864
+ },
4865
+ "engines": {
4866
+ "node": ">=14.18.0"
4867
+ }
4868
+ },
4869
+ "node_modules/superagent/node_modules/debug": {
4870
+ "version": "4.4.3",
4871
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
4872
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
4873
+ "dev": true,
4874
+ "license": "MIT",
4875
+ "dependencies": {
4876
+ "ms": "^2.1.3"
4877
+ },
4878
+ "engines": {
4879
+ "node": ">=6.0"
4880
+ },
4881
+ "peerDependenciesMeta": {
4882
+ "supports-color": {
4883
+ "optional": true
4884
+ }
4885
+ }
4886
+ },
4887
+ "node_modules/superagent/node_modules/mime": {
4888
+ "version": "2.6.0",
4889
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
4890
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
4891
+ "dev": true,
4892
+ "license": "MIT",
4893
+ "bin": {
4894
+ "mime": "cli.js"
4895
+ },
4896
+ "engines": {
4897
+ "node": ">=4.0.0"
4898
+ }
4899
+ },
4900
+ "node_modules/superagent/node_modules/ms": {
4901
+ "version": "2.1.3",
4902
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
4903
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
4904
+ "dev": true,
4905
+ "license": "MIT"
4906
+ },
4907
+ "node_modules/supertest": {
4908
+ "version": "7.2.2",
4909
+ "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz",
4910
+ "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==",
4911
+ "dev": true,
4912
+ "license": "MIT",
4913
+ "dependencies": {
4914
+ "cookie-signature": "^1.2.2",
4915
+ "methods": "^1.1.2",
4916
+ "superagent": "^10.3.0"
4917
+ },
4918
+ "engines": {
4919
+ "node": ">=14.18.0"
4920
+ }
4921
+ },
4922
+ "node_modules/supertest/node_modules/cookie-signature": {
4923
+ "version": "1.2.2",
4924
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
4925
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
4926
+ "dev": true,
4927
+ "license": "MIT",
4928
+ "engines": {
4929
+ "node": ">=6.6.0"
4930
+ }
4931
+ },
4932
  "node_modules/sync-fetch": {
4933
  "version": "0.4.5",
4934
  "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.5.tgz",
 
5001
  "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
5002
  "license": "MIT"
5003
  },
5004
+ "node_modules/trim-lines": {
5005
+ "version": "3.0.1",
5006
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
5007
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
5008
+ "license": "MIT",
5009
+ "funding": {
5010
+ "type": "github",
5011
+ "url": "https://github.com/sponsors/wooorm"
5012
+ }
5013
+ },
5014
  "node_modules/tslib": {
5015
  "version": "2.8.1",
5016
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
 
5089
  "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
5090
  "license": "MIT"
5091
  },
5092
+ "node_modules/unist-util-is": {
5093
+ "version": "6.0.1",
5094
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
5095
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
5096
+ "license": "MIT",
5097
+ "dependencies": {
5098
+ "@types/unist": "^3.0.0"
5099
+ },
5100
+ "funding": {
5101
+ "type": "opencollective",
5102
+ "url": "https://opencollective.com/unified"
5103
+ }
5104
+ },
5105
+ "node_modules/unist-util-position": {
5106
+ "version": "5.0.0",
5107
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
5108
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
5109
+ "license": "MIT",
5110
+ "dependencies": {
5111
+ "@types/unist": "^3.0.0"
5112
+ },
5113
+ "funding": {
5114
+ "type": "opencollective",
5115
+ "url": "https://opencollective.com/unified"
5116
+ }
5117
+ },
5118
+ "node_modules/unist-util-stringify-position": {
5119
+ "version": "4.0.0",
5120
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
5121
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
5122
+ "license": "MIT",
5123
+ "dependencies": {
5124
+ "@types/unist": "^3.0.0"
5125
+ },
5126
+ "funding": {
5127
+ "type": "opencollective",
5128
+ "url": "https://opencollective.com/unified"
5129
+ }
5130
+ },
5131
+ "node_modules/unist-util-visit": {
5132
+ "version": "5.1.0",
5133
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
5134
+ "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
5135
+ "license": "MIT",
5136
+ "dependencies": {
5137
+ "@types/unist": "^3.0.0",
5138
+ "unist-util-is": "^6.0.0",
5139
+ "unist-util-visit-parents": "^6.0.0"
5140
+ },
5141
+ "funding": {
5142
+ "type": "opencollective",
5143
+ "url": "https://opencollective.com/unified"
5144
+ }
5145
+ },
5146
+ "node_modules/unist-util-visit-parents": {
5147
+ "version": "6.0.2",
5148
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
5149
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
5150
+ "license": "MIT",
5151
+ "dependencies": {
5152
+ "@types/unist": "^3.0.0",
5153
+ "unist-util-is": "^6.0.0"
5154
+ },
5155
+ "funding": {
5156
+ "type": "opencollective",
5157
+ "url": "https://opencollective.com/unified"
5158
+ }
5159
+ },
5160
  "node_modules/unpipe": {
5161
  "version": "1.0.0",
5162
  "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
 
5190
  "node": ">= 0.8"
5191
  }
5192
  },
5193
+ "node_modules/vfile": {
5194
+ "version": "6.0.3",
5195
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
5196
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
5197
+ "license": "MIT",
5198
+ "dependencies": {
5199
+ "@types/unist": "^3.0.0",
5200
+ "vfile-message": "^4.0.0"
5201
+ },
5202
+ "funding": {
5203
+ "type": "opencollective",
5204
+ "url": "https://opencollective.com/unified"
5205
+ }
5206
+ },
5207
+ "node_modules/vfile-message": {
5208
+ "version": "4.0.3",
5209
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
5210
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
5211
+ "license": "MIT",
5212
+ "dependencies": {
5213
+ "@types/unist": "^3.0.0",
5214
+ "unist-util-stringify-position": "^4.0.0"
5215
+ },
5216
+ "funding": {
5217
+ "type": "opencollective",
5218
+ "url": "https://opencollective.com/unified"
5219
+ }
5220
+ },
5221
  "node_modules/vite": {
5222
  "version": "8.0.8",
5223
  "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
 
5435
  "node": ">=8"
5436
  }
5437
  },
5438
+ "node_modules/wrappy": {
5439
+ "version": "1.0.2",
5440
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
5441
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
5442
+ "dev": true,
5443
+ "license": "ISC"
5444
+ },
5445
  "node_modules/ws": {
5446
  "version": "8.20.0",
5447
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
 
5536
  "funding": {
5537
  "url": "https://github.com/sponsors/colinhacks"
5538
  }
5539
+ },
5540
+ "node_modules/zwitch": {
5541
+ "version": "2.0.4",
5542
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
5543
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
5544
+ "license": "MIT",
5545
+ "funding": {
5546
+ "type": "github",
5547
+ "url": "https://github.com/sponsors/wooorm"
5548
+ }
5549
  }
5550
  }
5551
  }
backend/package.json CHANGED
@@ -8,7 +8,8 @@
8
  "build": "tsc",
9
  "start": "node dist/server.js",
10
  "test": "vitest run",
11
- "test:watch": "vitest"
 
12
  },
13
  "dependencies": {
14
  "@ai-sdk/openai": "^3.0.52",
@@ -23,7 +24,6 @@
23
  "@huggingface/hub": "^2.11.0",
24
  "@openrouter/ai-sdk-provider": "^2.5.1",
25
  "@tiptap/core": "^3.22.3",
26
- "@tiptap/extension-code-block-lowlight": "^3.22.3",
27
  "@tiptap/extension-image": "^3.22.3",
28
  "@tiptap/extension-link": "^3.22.3",
29
  "@tiptap/extension-mathematics": "^3.22.3",
@@ -41,15 +41,19 @@
41
  "lowlight": "^3.3.0",
42
  "multer": "^2.1.1",
43
  "playwright": "^1.59.1",
 
44
  "ws": "^8.20.0",
45
  "yjs": "^13.6.0",
46
  "zod": "^4.3.6"
47
  },
48
  "devDependencies": {
 
49
  "@types/express": "^5.0.0",
50
  "@types/multer": "^2.1.0",
51
  "@types/node": "^22.0.0",
 
52
  "@types/ws": "^8.18.1",
 
53
  "tsx": "^4.19.0",
54
  "typescript": "^5.6.0",
55
  "vitest": "^4.1.4"
 
8
  "build": "tsc",
9
  "start": "node dist/server.js",
10
  "test": "vitest run",
11
+ "test:watch": "vitest",
12
+ "test:e2e": "npx playwright test"
13
  },
14
  "dependencies": {
15
  "@ai-sdk/openai": "^3.0.52",
 
24
  "@huggingface/hub": "^2.11.0",
25
  "@openrouter/ai-sdk-provider": "^2.5.1",
26
  "@tiptap/core": "^3.22.3",
 
27
  "@tiptap/extension-image": "^3.22.3",
28
  "@tiptap/extension-link": "^3.22.3",
29
  "@tiptap/extension-mathematics": "^3.22.3",
 
41
  "lowlight": "^3.3.0",
42
  "multer": "^2.1.1",
43
  "playwright": "^1.59.1",
44
+ "shiki": "^4.0.2",
45
  "ws": "^8.20.0",
46
  "yjs": "^13.6.0",
47
  "zod": "^4.3.6"
48
  },
49
  "devDependencies": {
50
+ "@playwright/test": "^1.59.1",
51
  "@types/express": "^5.0.0",
52
  "@types/multer": "^2.1.0",
53
  "@types/node": "^22.0.0",
54
+ "@types/supertest": "^7.2.0",
55
  "@types/ws": "^8.18.1",
56
+ "supertest": "^7.2.2",
57
  "tsx": "^4.19.0",
58
  "typescript": "^5.6.0",
59
  "vitest": "^4.1.4"
backend/playwright.config.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "@playwright/test";
2
+
3
+ export default defineConfig({
4
+ testDir: "./e2e",
5
+ timeout: 30_000,
6
+ retries: 0,
7
+ use: {
8
+ baseURL: "http://localhost:0", // replaced dynamically in fixtures
9
+ headless: true,
10
+ screenshot: "only-on-failure",
11
+ },
12
+ projects: [{ name: "chromium", use: { browserName: "chromium" } }],
13
+ });